@@ -82,6 +82,74 @@ const GATEWAY_CONFIG_WRITE_OPTIONS = {
8282 } ,
8383} ;
8484
85+ function tokenAuthConfig ( token : string ) : OpenClawConfig {
86+ return {
87+ gateway : {
88+ auth : {
89+ mode : "token" ,
90+ token,
91+ } ,
92+ } ,
93+ } ;
94+ }
95+
96+ function trustedProxyConfig ( params : {
97+ trustedProxies ?: string [ ] ;
98+ requiredHeaders ?: string [ ] ;
99+ allowUsers ?: string [ ] ;
100+ } ) : OpenClawConfig {
101+ return {
102+ gateway : {
103+ auth : {
104+ mode : "trusted-proxy" ,
105+ trustedProxy : {
106+ userHeader : "x-forwarded-user" ,
107+ ...( params . requiredHeaders ? { requiredHeaders : params . requiredHeaders } : { } ) ,
108+ ...( params . allowUsers ? { allowUsers : params . allowUsers } : { } ) ,
109+ } ,
110+ } ,
111+ ...( params . trustedProxies ? { trustedProxies : params . trustedProxies } : { } ) ,
112+ } ,
113+ } ;
114+ }
115+
116+ function hotReloadConfig ( ) : OpenClawConfig {
117+ return {
118+ gateway : {
119+ reload : {
120+ mode : "hot" ,
121+ } ,
122+ } ,
123+ } ;
124+ }
125+
126+ function mockPreviousConfig ( config : OpenClawConfig ) : void {
127+ readConfigFileSnapshotForWriteMock . mockResolvedValue ( createConfigWriteSnapshot ( config ) ) ;
128+ }
129+
130+ async function runConfigPatch (
131+ raw : unknown ,
132+ params : { sessionKey ?: string ; restartDelayMs ?: number } = { } ,
133+ ) {
134+ const { options, disconnectClientsUsingSharedGatewayAuth } = createConfigHandlerHarness ( {
135+ method : "config.patch" ,
136+ params : {
137+ baseHash : "base-hash" ,
138+ raw : typeof raw === "string" ? raw : JSON . stringify ( raw ) ,
139+ restartDelayMs : params . restartDelayMs ?? 1_000 ,
140+ ...( params . sessionKey ? { sessionKey : params . sessionKey } : { } ) ,
141+ } ,
142+ } ) ;
143+
144+ await configHandlers [ "config.patch" ] ( options ) ;
145+ await flushConfigHandlerMicrotasks ( ) ;
146+ return { disconnectClientsUsingSharedGatewayAuth } ;
147+ }
148+
149+ function expectNoDirectRestart ( ) : void {
150+ expect ( scheduleGatewaySigusr1RestartMock ) . not . toHaveBeenCalled ( ) ;
151+ }
152+
85153afterEach ( ( ) => {
86154 vi . clearAllMocks ( ) ;
87155} ) ;
@@ -147,23 +215,8 @@ describe("config shared auth disconnects", () => {
147215 } ) ;
148216
149217 it ( "does not disconnect shared-auth clients for config.set auth writes without restart" , async ( ) => {
150- const prevConfig : OpenClawConfig = {
151- gateway : {
152- auth : {
153- mode : "token" ,
154- token : "old-token" ,
155- } ,
156- } ,
157- } ;
158- const nextConfig : OpenClawConfig = {
159- gateway : {
160- auth : {
161- mode : "token" ,
162- token : "new-token" ,
163- } ,
164- } ,
165- } ;
166- readConfigFileSnapshotForWriteMock . mockResolvedValue ( createConfigWriteSnapshot ( prevConfig ) ) ;
218+ const nextConfig = tokenAuthConfig ( "new-token" ) ;
219+ mockPreviousConfig ( tokenAuthConfig ( "old-token" ) ) ;
167220
168221 const { options, disconnectClientsUsingSharedGatewayAuth } = createConfigHandlerHarness ( {
169222 method : "config.set" ,
@@ -178,226 +231,114 @@ describe("config shared auth disconnects", () => {
178231
179232 expect ( writeConfigFileMock ) . toHaveBeenCalledWith ( nextConfig , GATEWAY_CONFIG_WRITE_OPTIONS ) ;
180233 expect ( disconnectClientsUsingSharedGatewayAuth ) . not . toHaveBeenCalled ( ) ;
181- expect ( scheduleGatewaySigusr1RestartMock ) . not . toHaveBeenCalled ( ) ;
234+ expectNoDirectRestart ( ) ;
182235 } ) ;
183236
184237 it ( "lets the config reloader own hybrid-mode auth restarts" , async ( ) => {
185- const prevConfig : OpenClawConfig = {
186- gateway : {
187- auth : {
188- mode : "token" ,
189- token : "old-token" ,
190- } ,
191- } ,
192- } ;
193- readConfigFileSnapshotForWriteMock . mockResolvedValue ( createConfigWriteSnapshot ( prevConfig ) ) ;
238+ mockPreviousConfig ( tokenAuthConfig ( "old-token" ) ) ;
194239
195- const { options, disconnectClientsUsingSharedGatewayAuth } = createConfigHandlerHarness ( {
196- method : "config.patch" ,
197- params : {
198- baseHash : "base-hash" ,
199- raw : JSON . stringify ( { gateway : { auth : { token : "new-token" } } } ) ,
200- restartDelayMs : 1_000 ,
201- } ,
240+ const { disconnectClientsUsingSharedGatewayAuth } = await runConfigPatch ( {
241+ gateway : { auth : { token : "new-token" } } ,
202242 } ) ;
203243
204- await configHandlers [ "config.patch" ] ( options ) ;
205- await flushConfigHandlerMicrotasks ( ) ;
206-
207- expect ( scheduleGatewaySigusr1RestartMock ) . not . toHaveBeenCalled ( ) ;
244+ expectNoDirectRestart ( ) ;
208245 expect ( disconnectClientsUsingSharedGatewayAuth ) . toHaveBeenCalledTimes ( 1 ) ;
209246 } ) ;
210247
211248 it ( "does not disconnect shared-auth clients when config.patch changes only inactive password auth" , async ( ) => {
212- const prevConfig : OpenClawConfig = {
213- gateway : {
214- auth : {
215- mode : "token" ,
216- token : "old-token" ,
217- } ,
218- } ,
219- } ;
220- readConfigFileSnapshotForWriteMock . mockResolvedValue ( createConfigWriteSnapshot ( prevConfig ) ) ;
249+ mockPreviousConfig ( tokenAuthConfig ( "old-token" ) ) ;
221250
222- const { options, disconnectClientsUsingSharedGatewayAuth } = createConfigHandlerHarness ( {
223- method : "config.patch" ,
224- params : {
225- baseHash : "base-hash" ,
226- raw : JSON . stringify ( { gateway : { auth : { password : "new-password" } } } ) ,
227- restartDelayMs : 1_000 ,
228- } ,
251+ const { disconnectClientsUsingSharedGatewayAuth } = await runConfigPatch ( {
252+ gateway : { auth : { password : "new-password" } } ,
229253 } ) ;
230254
231- await configHandlers [ "config.patch" ] ( options ) ;
232- await flushConfigHandlerMicrotasks ( ) ;
233-
234- expect ( scheduleGatewaySigusr1RestartMock ) . not . toHaveBeenCalled ( ) ;
255+ expectNoDirectRestart ( ) ;
235256 expect ( disconnectClientsUsingSharedGatewayAuth ) . not . toHaveBeenCalled ( ) ;
236257 } ) ;
237258
238259 it ( "disconnects gateway-auth clients when active trusted-proxy policy changes" , async ( ) => {
239- const prevConfig : OpenClawConfig = {
260+ mockPreviousConfig (
261+ trustedProxyConfig ( {
262+ allowUsers :
[ "[email protected] " ] , 263+ trustedProxies : [ "127.0.0.1" ] ,
264+ } ) ,
265+ ) ;
266+
267+ const { disconnectClientsUsingSharedGatewayAuth } = await runConfigPatch ( {
240268 gateway : {
241269 auth : {
242- mode : "trusted-proxy" ,
243270 trustedProxy : {
244271 userHeader : "x-forwarded-user" ,
245- allowUsers : [ "alice @example.com" ] ,
272+ allowUsers : [ "bob @example.com" ] ,
246273 } ,
247274 } ,
248- trustedProxies : [ "127.0.0.1" ] ,
249- } ,
250- } ;
251- readConfigFileSnapshotForWriteMock . mockResolvedValue ( createConfigWriteSnapshot ( prevConfig ) ) ;
252-
253- const { options, disconnectClientsUsingSharedGatewayAuth } = createConfigHandlerHarness ( {
254- method : "config.patch" ,
255- params : {
256- baseHash : "base-hash" ,
257- raw : JSON . stringify ( {
258- gateway : {
259- auth : {
260- trustedProxy : {
261- userHeader : "x-forwarded-user" ,
262- allowUsers :
[ "[email protected] " ] , 263- } ,
264- } ,
265- } ,
266- } ) ,
267- restartDelayMs : 1_000 ,
268275 } ,
269276 } ) ;
270277
271- await configHandlers [ "config.patch" ] ( options ) ;
272- await flushConfigHandlerMicrotasks ( ) ;
273-
274- expect ( scheduleGatewaySigusr1RestartMock ) . not . toHaveBeenCalled ( ) ;
278+ expectNoDirectRestart ( ) ;
275279 expect ( disconnectClientsUsingSharedGatewayAuth ) . toHaveBeenCalledTimes ( 1 ) ;
276280 } ) ;
277281
278282 it ( "disconnects gateway-auth clients when trusted-proxy source list changes" , async ( ) => {
279- const prevConfig : OpenClawConfig = {
280- gateway : {
281- auth : {
282- mode : "trusted-proxy" ,
283- trustedProxy : {
284- userHeader : "x-forwarded-user" ,
285- } ,
286- } ,
283+ mockPreviousConfig (
284+ trustedProxyConfig ( {
287285 trustedProxies : [ "127.0.0.1" ] ,
288- } ,
289- } ;
290- readConfigFileSnapshotForWriteMock . mockResolvedValue ( createConfigWriteSnapshot ( prevConfig ) ) ;
286+ } ) ,
287+ ) ;
291288
292- const { options, disconnectClientsUsingSharedGatewayAuth } = createConfigHandlerHarness ( {
293- method : "config.patch" ,
294- params : {
295- baseHash : "base-hash" ,
296- raw : JSON . stringify ( {
297- gateway : {
298- trustedProxies : [ "10.0.0.10" ] ,
299- } ,
300- } ) ,
301- restartDelayMs : 1_000 ,
289+ const { disconnectClientsUsingSharedGatewayAuth } = await runConfigPatch ( {
290+ gateway : {
291+ trustedProxies : [ "10.0.0.10" ] ,
302292 } ,
303293 } ) ;
304294
305- await configHandlers [ "config.patch" ] ( options ) ;
306- await flushConfigHandlerMicrotasks ( ) ;
307-
308- expect ( scheduleGatewaySigusr1RestartMock ) . not . toHaveBeenCalled ( ) ;
295+ expectNoDirectRestart ( ) ;
309296 expect ( disconnectClientsUsingSharedGatewayAuth ) . toHaveBeenCalledTimes ( 1 ) ;
310297 } ) ;
311298
312299 it ( "does not disconnect gateway-auth clients when trusted-proxy lists are reordered" , async ( ) => {
313- const prevConfig : OpenClawConfig = {
300+ mockPreviousConfig (
301+ trustedProxyConfig ( {
302+ requiredHeaders : [ "x-forwarded-proto" , "x-forwarded-host" ] ,
303+ 304+ trustedProxies : [ "127.0.0.1" , "10.0.0.10" ] ,
305+ } ) ,
306+ ) ;
307+
308+ const { disconnectClientsUsingSharedGatewayAuth } = await runConfigPatch ( {
314309 gateway : {
315310 auth : {
316- mode : "trusted-proxy" ,
317311 trustedProxy : {
318312 userHeader : "x-forwarded-user" ,
319- requiredHeaders : [ "x-forwarded-proto " , "x-forwarded-host " ] ,
320- allowUsers : [ "alice @example.com" , "bob @example.com" ] ,
313+ requiredHeaders : [ "x-forwarded-host " , "x-forwarded-proto " ] ,
314+ allowUsers : [ "bob @example.com" , "alice @example.com" ] ,
321315 } ,
322316 } ,
323- trustedProxies : [ "127.0.0.1" , "10.0.0.10" ] ,
324- } ,
325- } ;
326- readConfigFileSnapshotForWriteMock . mockResolvedValue ( createConfigWriteSnapshot ( prevConfig ) ) ;
327-
328- const { options, disconnectClientsUsingSharedGatewayAuth } = createConfigHandlerHarness ( {
329- method : "config.patch" ,
330- params : {
331- baseHash : "base-hash" ,
332- raw : JSON . stringify ( {
333- gateway : {
334- auth : {
335- trustedProxy : {
336- userHeader : "x-forwarded-user" ,
337- requiredHeaders : [ "x-forwarded-host" , "x-forwarded-proto" ] ,
338- 339- } ,
340- } ,
341- trustedProxies : [ "10.0.0.10" , "127.0.0.1" ] ,
342- } ,
343- } ) ,
344- restartDelayMs : 1_000 ,
317+ trustedProxies : [ "10.0.0.10" , "127.0.0.1" ] ,
345318 } ,
346319 } ) ;
347320
348- await configHandlers [ "config.patch" ] ( options ) ;
349- await flushConfigHandlerMicrotasks ( ) ;
350-
351- expect ( scheduleGatewaySigusr1RestartMock ) . not . toHaveBeenCalled ( ) ;
321+ expectNoDirectRestart ( ) ;
352322 expect ( disconnectClientsUsingSharedGatewayAuth ) . not . toHaveBeenCalled ( ) ;
353323 } ) ;
354324
355325 it ( "still schedules a direct restart for hot mode when the reloader cannot apply the change" , async ( ) => {
356- const prevConfig : OpenClawConfig = {
357- gateway : {
358- reload : {
359- mode : "hot" ,
360- } ,
361- } ,
362- } ;
363- readConfigFileSnapshotForWriteMock . mockResolvedValue ( createConfigWriteSnapshot ( prevConfig ) ) ;
364-
365- const { options } = createConfigHandlerHarness ( {
366- method : "config.patch" ,
367- params : {
368- baseHash : "base-hash" ,
369- raw : JSON . stringify ( { gateway : { port : 19001 } } ) ,
370- restartDelayMs : 1_000 ,
371- } ,
372- } ) ;
326+ mockPreviousConfig ( hotReloadConfig ( ) ) ;
373327
374- await configHandlers [ "config.patch" ] ( options ) ;
375- await flushConfigHandlerMicrotasks ( ) ;
328+ await runConfigPatch ( { gateway : { port : 19001 } } ) ;
376329
377330 expect ( scheduleGatewaySigusr1RestartMock ) . toHaveBeenCalledTimes ( 1 ) ;
378331 } ) ;
379332
380333 it ( "does not add an agent continuation from generic control-plane sessionKey params" , async ( ) => {
381- const prevConfig : OpenClawConfig = {
382- gateway : {
383- reload : {
384- mode : "hot" ,
385- } ,
386- } ,
387- } ;
388- readConfigFileSnapshotForWriteMock . mockResolvedValue ( createConfigWriteSnapshot ( prevConfig ) ) ;
334+ mockPreviousConfig ( hotReloadConfig ( ) ) ;
389335
390- const { options } = createConfigHandlerHarness ( {
391- method : "config.patch" ,
392- params : {
393- baseHash : "base-hash" ,
394- raw : JSON . stringify ( { gateway : { port : 19001 } } ) ,
395- restartDelayMs : 1_000 ,
336+ await runConfigPatch (
337+ { gateway : { port : 19001 } } ,
338+ {
396339 sessionKey : "agent:main:main" ,
397340 } ,
398- } ) ;
399-
400- await configHandlers [ "config.patch" ] ( options ) ;
341+ ) ;
401342
402343 const payload = restartSentinelMocks . writeRestartSentinel . mock . calls . at ( - 1 ) ?. [ 0 ] ;
403344 expect ( payload ?. sessionKey ) . toBe ( "agent:main:main" ) ;
0 commit comments