@@ -353,3 +353,54 @@ describe("CL-7990 shell-child reap: sessions holding a live shell child settle",
353353 { timeout : 60_000 } ,
354354 ) ;
355355} ) ;
356+
357+ /**
358+ * CL-7997 kill proof: the tests above prove the run settles under a wedged
359+ * child, but none proves a shell-guard-tracked child is actually KILLED on
360+ * close/dispose. This drives a real `sleep` through `runGuardedShell` (the
361+ * shell-guard tracking primitive) and the exact `reapLiveChildren` call the
362+ * plugin dispose runs, then asserts on the ChildProcess handle itself that
363+ * the process is dead — not just that the run settled.
364+ */
365+ describe ( "CL-7997 shell-guard kill proof: dispose leaves the tracked child dead" , ( ) => {
366+ test (
367+ "reapLiveChildren kills a shell-guard-tracked sleep child" ,
368+ async ( ) => {
369+ if ( process . platform === "win32" ) return ;
370+ const { runGuardedShell, reapLiveChildren } =
371+ await import ( "../plugins/shell-guard-plugin.js" ) ;
372+ const liveChildren = new Set < ChildProcess > ( ) ;
373+ const controller = new AbortController ( ) ;
374+ const running = runGuardedShell (
375+ { command : "sleep 60" } ,
376+ controller . signal ,
377+ liveChildren ,
378+ ) ;
379+ let child : ChildProcess | undefined ;
380+ try {
381+ const started = Date . now ( ) ;
382+ while ( liveChildren . size === 0 && Date . now ( ) - started < 5_000 ) {
383+ await new Promise ( ( resolve ) => setTimeout ( resolve , 10 ) ) ;
384+ }
385+ expect ( liveChildren . size ) . toBe ( 1 ) ;
386+ child = defined ( [ ...liveChildren ] [ 0 ] ) ;
387+ // Live before the reap, so the death assertion below is not vacuous.
388+ expect ( child . exitCode ) . toBeNull ( ) ;
389+ expect ( child . signalCode ) . toBeNull ( ) ;
390+ await reapLiveChildren ( liveChildren ) ;
391+ await waitForChildExit ( child ) ;
392+ await running ;
393+ } finally {
394+ controller . abort ( ) ;
395+ if ( child !== undefined ) {
396+ try {
397+ child . kill ( "SIGKILL" ) ;
398+ } catch {
399+ // Already dead — best-effort orphan guard.
400+ }
401+ }
402+ }
403+ } ,
404+ { timeout : 30_000 } ,
405+ ) ;
406+ } ) ;
0 commit comments