11// Talk config tests cover speech-provider config resolution, secret redaction,
22// device-authenticated access, and protocol payload validation.
3+ import fs from "node:fs" ;
34import os from "node:os" ;
45import path from "node:path" ;
56import { afterAll , beforeAll , describe , expect , it } from "vitest" ;
@@ -17,6 +18,7 @@ import {
1718 connectOk ,
1819 createGatewaySuiteHarness ,
1920 installGatewayTestHooks ,
21+ onceMessage ,
2022 readConnectChallengeNonce ,
2123 rpcReq ,
2224} from "./test-helpers.js" ;
@@ -62,8 +64,9 @@ afterAll(async () => {
6264} ) ;
6365
6466async function createFreshOperatorDevice ( scopes : string [ ] , nonce : string ) {
67+ const identityRoot = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , "openclaw-talk-config-device-" ) ) ;
6568 const identity = loadOrCreateDeviceIdentity (
66- path . join ( os . tmpdir ( ) , `openclaw-talk-config-device- ${ process . pid } -${ talkConfigDeviceSeq ++ } ` ) ,
69+ path . join ( identityRoot , `${ process . pid } -${ talkConfigDeviceSeq ++ } ` ) ,
6770 ) ;
6871 const signedAtMs = Date . now ( ) ;
6972 const payload = buildDeviceAuthPayload ( {
@@ -129,6 +132,32 @@ async function fetchTalkConfig(
129132 return rpcReq < TalkConfigPayload > ( ws , "talk.config" , params ?? { } , 60_000 ) ;
130133}
131134
135+ async function fetchTalkConfigWithRuntimeSnapshot (
136+ ws : GatewaySocket ,
137+ params ?: { includeSecrets ?: boolean } | Record < string , unknown > ,
138+ ) {
139+ const { randomUUID } = await import ( "node:crypto" ) ;
140+ const id = randomUUID ( ) ;
141+ const responsePromise = onceMessage < {
142+ type : "res" ;
143+ id : string ;
144+ ok : boolean ;
145+ payload ?: TalkConfigPayload | null | undefined ;
146+ error ?: { message ?: string ; code ?: string } ;
147+ } > (
148+ ws ,
149+ ( obj ) => {
150+ if ( ! obj || typeof obj !== "object" || Array . isArray ( obj ) ) {
151+ return false ;
152+ }
153+ return obj . type === "res" && obj . id === id ;
154+ } ,
155+ 60_000 ,
156+ ) ;
157+ ws . send ( JSON . stringify ( { type : "req" , id, method : "talk.config" , params : params ?? { } } ) ) ;
158+ return await responsePromise ;
159+ }
160+
132161async function fetchOkTalkConfig (
133162 ws : GatewaySocket ,
134163 params ?: { includeSecrets ?: boolean } | Record < string , unknown > ,
@@ -138,6 +167,15 @@ async function fetchOkTalkConfig(
138167 return res ;
139168}
140169
170+ async function fetchOkTalkConfigWithRuntimeSnapshot (
171+ ws : GatewaySocket ,
172+ params ?: { includeSecrets ?: boolean } | Record < string , unknown > ,
173+ ) {
174+ const res = await fetchTalkConfigWithRuntimeSnapshot ( ws , params ) ;
175+ expect ( res . ok , JSON . stringify ( res . error ) ) . toBe ( true ) ;
176+ return res ;
177+ }
178+
141179async function withTalkConfigConnection < T > (
142180 scopes : string [ ] ,
143181 run : ( ws : GatewaySocket ) => Promise < T > ,
@@ -159,6 +197,25 @@ function talkApiSecretRef() {
159197 } satisfies SecretRef ;
160198}
161199
200+ async function activateCurrentTalkSecretsRuntimeSnapshot ( ) {
201+ const [
202+ { readConfigFileSnapshot } ,
203+ { activateSecretsRuntimeSnapshot, prepareSecretsRuntimeSnapshot } ,
204+ ] = await Promise . all ( [ import ( "../config/config.js" ) , import ( "../secrets/runtime.js" ) ] ) ;
205+ const snapshot = await readConfigFileSnapshot ( ) ;
206+ const prepared = await prepareSecretsRuntimeSnapshot ( {
207+ config : snapshot . sourceConfig ,
208+ includeAuthStoreRefs : false ,
209+ loadablePluginOrigins : new Map ( ) ,
210+ } ) ;
211+ activateSecretsRuntimeSnapshot ( prepared ) ;
212+ }
213+
214+ async function clearTalkSecretsRuntimeSnapshot ( ) {
215+ const { clearSecretsRuntimeSnapshot } = await import ( "../secrets/runtime.js" ) ;
216+ clearSecretsRuntimeSnapshot ( ) ;
217+ }
218+
162219function speechProviderFixture ( params : {
163220 pluginId : string ;
164221 label : string ;
@@ -186,17 +243,22 @@ function speechProviderFixture(params: {
186243async function expectTalkSecretsConfig (
187244 expected : Omit < Parameters < typeof expectTalkConfig > [ 1 ] , "provider" > ,
188245) {
189- await withTalkConfigConnection (
190- [ "operator.read" , "operator.write" , "operator.talk.secrets" ] ,
191- async ( ws ) => {
192- const res = await fetchOkTalkConfig ( ws , { includeSecrets : true } ) ;
193- expect ( validateTalkConfigResult ( res . payload ) ) . toBe ( true ) ;
194- expectTalkConfig ( res . payload ?. config ?. talk , {
195- provider : GENERIC_TALK_PROVIDER_ID ,
196- ...expected ,
197- } ) ;
198- } ,
199- ) ;
246+ await activateCurrentTalkSecretsRuntimeSnapshot ( ) ;
247+ try {
248+ await withTalkConfigConnection (
249+ [ "operator.read" , "operator.write" , "operator.talk.secrets" ] ,
250+ async ( ws ) => {
251+ const res = await fetchOkTalkConfigWithRuntimeSnapshot ( ws , { includeSecrets : true } ) ;
252+ expect ( validateTalkConfigResult ( res . payload ) ) . toBe ( true ) ;
253+ expectTalkConfig ( res . payload ?. config ?. talk , {
254+ provider : GENERIC_TALK_PROVIDER_ID ,
255+ ...expected ,
256+ } ) ;
257+ } ,
258+ ) ;
259+ } finally {
260+ await clearTalkSecretsRuntimeSnapshot ( ) ;
261+ }
200262}
201263
202264function expectTalkConfig (
@@ -307,13 +369,16 @@ describe("gateway talk.config", () => {
307369 } ) ;
308370 } ) ;
309371
310- it ( "returns Talk SecretRef payloads that satisfy the protocol schema" , async ( ) => {
372+ it ( "returns source SecretRef and resolved Talk secret payloads that satisfy the protocol schema" , async ( ) => {
311373 await writeTalkConfig ( {
312374 apiKey : talkApiSecretRef ( ) ,
313375 } ) ;
314376
315377 await withEnvAsync ( { [ GENERIC_TALK_API_ENV ] : "env-acme-key" } , async ( ) => {
316- await expectTalkSecretsConfig ( { apiKey : talkApiSecretRef ( ) } ) ;
378+ await expectTalkSecretsConfig ( {
379+ providerApiKey : talkApiSecretRef ( ) ,
380+ resolvedApiKey : "env-acme-key" ,
381+ } ) ;
317382 } ) ;
318383 } ) ;
319384
@@ -402,7 +467,8 @@ describe("gateway talk.config", () => {
402467
403468 await expectTalkSecretsConfig ( {
404469 voiceId : "voice-secretref" ,
405- apiKey : talkApiSecretRef ( ) ,
470+ providerApiKey : talkApiSecretRef ( ) ,
471+ resolvedApiKey : "env-acme-key" ,
406472 } ) ;
407473 } ,
408474 ) ;
0 commit comments