@@ -54,6 +54,15 @@ function clearSupervisorHints() {
5454 }
5555}
5656
57+ function mockDetachedChild ( pid : number ) {
58+ return {
59+ pid,
60+ kill : vi . fn ( ) ,
61+ on : vi . fn ( ) ,
62+ unref : vi . fn ( ) ,
63+ } ;
64+ }
65+
5766function expectLaunchdSupervisedWithoutKickstart ( params ?: { launchJobLabel ?: string } ) {
5867 setPlatform ( "darwin" ) ;
5968 if ( params ?. launchJobLabel ) {
@@ -302,7 +311,7 @@ describe("respawnGatewayProcessForUpdate", () => {
302311 "gateway" ,
303312 "run" ,
304313 ] ;
305- spawnMock . mockReturnValue ( { pid : 5151 , unref : vi . fn ( ) , kill : vi . fn ( ) } ) ;
314+ spawnMock . mockReturnValue ( mockDetachedChild ( 5151 ) ) ;
306315
307316 const result = respawnGatewayProcessForUpdate ( ) ;
308317
@@ -329,7 +338,7 @@ describe("respawnGatewayProcessForUpdate", () => {
329338 "gateway" ,
330339 "run" ,
331340 ] ;
332- spawnMock . mockReturnValue ( { pid : 7171 , unref : vi . fn ( ) , kill : vi . fn ( ) } ) ;
341+ spawnMock . mockReturnValue ( mockDetachedChild ( 7171 ) ) ;
333342
334343 const result = respawnGatewayProcessForUpdate ( ) ;
335344
@@ -352,7 +361,7 @@ describe("respawnGatewayProcessForUpdate", () => {
352361 const entry =
353362 "/app/node_modules/.pnpm/@[email protected] /node_modules/@anthropic/sdk/dist/index.js" ; 354363 process . argv = [ "/usr/local/bin/node" , entry , "gateway" , "run" ] ;
355- spawnMock . mockReturnValue ( { pid : 8181 , unref : vi . fn ( ) , kill : vi . fn ( ) } ) ;
364+ spawnMock . mockReturnValue ( mockDetachedChild ( 8181 ) ) ;
356365
357366 respawnGatewayProcessForUpdate ( ) ;
358367
@@ -369,7 +378,7 @@ describe("respawnGatewayProcessForUpdate", () => {
369378 process . env . XPC_SERVICE_NAME = "ai.openclaw.mac" ;
370379 process . execArgv = [ ] ;
371380 process . argv = [ "/usr/local/bin/node" , "/repo/dist/index.js" , "gateway" , "run" ] ;
372- spawnMock . mockReturnValue ( { pid : 6161 , unref : vi . fn ( ) , kill : vi . fn ( ) } ) ;
381+ spawnMock . mockReturnValue ( mockDetachedChild ( 6161 ) ) ;
373382
374383 const result = respawnGatewayProcessForUpdate ( ) ;
375384
@@ -386,6 +395,27 @@ describe("respawnGatewayProcessForUpdate", () => {
386395 ) ;
387396 } ) ;
388397
398+ it ( "registers a no-op detached child error listener before unref" , ( ) => {
399+ clearSupervisorHints ( ) ;
400+ setPlatform ( "linux" ) ;
401+ process . execArgv = [ ] ;
402+ process . argv = [ "/usr/local/bin/node" , "/repo/dist/index.js" , "gateway" , "run" ] ;
403+ const child = mockDetachedChild ( 9191 ) ;
404+ spawnMock . mockReturnValue ( child ) ;
405+
406+ const result = respawnGatewayProcessForUpdate ( ) ;
407+
408+ expect ( result . mode ) . toBe ( "spawned" ) ;
409+ expect ( result . child ) . toBe ( child ) ;
410+ expect ( child . on ) . toHaveBeenCalledWith ( "error" , expect . any ( Function ) ) ;
411+ const errorListener = child . on . mock . calls . find ( ( [ event ] ) => event === "error" ) ?. [ 1 ] ;
412+ expect ( ( ) => errorListener ?.( new Error ( "spawn ENOENT" ) ) ) . not . toThrow ( ) ;
413+ expect ( child . unref ) . toHaveBeenCalledOnce ( ) ;
414+ const onCallOrder = child . on . mock . invocationCallOrder [ 0 ] ?? Number . POSITIVE_INFINITY ;
415+ const unrefCallOrder = child . unref . mock . invocationCallOrder [ 0 ] ?? Number . NEGATIVE_INFINITY ;
416+ expect ( onCallOrder ) . toBeLessThan ( unrefCallOrder ) ;
417+ } ) ;
418+
389419 it ( "exits to a managed supervisor for updates even when respawn is disabled" , ( ) => {
390420 clearSupervisorHints ( ) ;
391421 setPlatform ( "linux" ) ;
0 commit comments