@@ -782,6 +782,106 @@ describe("shellGuardPlugin", () => {
782782 }
783783 } ) ;
784784
785+ test ( "dispose refuses a queued run_shell so it cannot stay running after reap" , async ( ) => {
786+ if ( process . platform === "win32" ) return ;
787+ const plugin = shellGuardPlugin ( process . cwd ( ) ) ;
788+ const handler = plugin . middleware ! ( fallback ) ;
789+ const token1 = `ic_guard_queued1_${ randomUUID ( ) } ` ;
790+ const token2 = `ic_guard_queued2_${ randomUUID ( ) } ` ;
791+ const first = handler (
792+ {
793+ id : "q-live" ,
794+ name : "run_shell" ,
795+ arguments : { command : `IC_GUARD_TAG=${ token1 } sleep 600` } ,
796+ } ,
797+ neverAbort ( ) ,
798+ ) ;
799+ try {
800+ const started = Date . now ( ) ;
801+ while ( Date . now ( ) - started < 5_000 ) {
802+ const probe = spawnSync ( "pgrep" , [ "-f" , token1 ] , { encoding : "utf8" } ) ;
803+ if ( ( probe . stdout ?. trim ( ) ?? "" ) . length > 0 ) break ;
804+ await new Promise ( ( r ) => setTimeout ( r , 50 ) ) ;
805+ }
806+ expect (
807+ spawnSync ( "pgrep" , [ "-f" , token1 ] , { encoding : "utf8" } ) . stdout ?. trim ( ) ?? "" ,
808+ ) . not . toBe ( "" ) ;
809+
810+ const queued = handler (
811+ {
812+ id : "q-wait" ,
813+ name : "run_shell" ,
814+ arguments : { command : `IC_GUARD_TAG=${ token2 } sleep 600` } ,
815+ } ,
816+ neverAbort ( ) ,
817+ ) ;
818+
819+ let disposeError : unknown ;
820+ try {
821+ await plugin . dispose ! ( ) ;
822+ } catch ( err ) {
823+ disposeError = err ;
824+ }
825+
826+ await first ;
827+ await Promise . race ( [ queued , new Promise ( ( r ) => setTimeout ( r , 400 ) ) ] ) ;
828+ await new Promise ( ( r ) => setTimeout ( r , 200 ) ) ;
829+
830+ const leftover1 =
831+ spawnSync ( "pgrep" , [ "-f" , token1 ] , { encoding : "utf8" } ) . stdout ?. trim ( ) ?? "" ;
832+ const leftover2 =
833+ spawnSync ( "pgrep" , [ "-f" , token2 ] , { encoding : "utf8" } ) . stdout ?. trim ( ) ?? "" ;
834+ expect ( leftover1 ) . toBe ( "" ) ;
835+ expect ( leftover2 ) . toBe ( "" ) ;
836+ if ( leftover2 . length > 0 ) {
837+ expect ( disposeError ) . toBeDefined ( ) ;
838+ } else {
839+ const queuedResult = await queued ;
840+ expect ( queuedResult . isError ) . toBe ( true ) ;
841+ expect ( String ( queuedResult . content ) ) . toMatch ( / d i s p o s e d / ) ;
842+ expect ( disposeError ) . toBeUndefined ( ) ;
843+ }
844+ spawnSync ( "pkill" , [ "-9" , "-f" , token2 ] ) ;
845+ await Promise . race ( [ queued , new Promise ( ( r ) => setTimeout ( r , 1_000 ) ) ] ) ;
846+ } finally {
847+ spawnSync ( "pkill" , [ "-9" , "-f" , token1 ] ) ;
848+ spawnSync ( "pkill" , [ "-9" , "-f" , token2 ] ) ;
849+ }
850+ } , 15_000 ) ;
851+
852+ test ( "overlapping dispose joins the in-flight reap" , async ( ) => {
853+ if ( process . platform === "win32" ) return ;
854+ const plugin = shellGuardPlugin ( process . cwd ( ) ) ;
855+ const handler = plugin . middleware ! ( fallback ) ;
856+ const token = `ic_guard_join_${ randomUUID ( ) } ` ;
857+ const running = handler (
858+ {
859+ id : "join-live" ,
860+ name : "run_shell" ,
861+ arguments : { command : `IC_GUARD_TAG=${ token } sleep 600` } ,
862+ } ,
863+ neverAbort ( ) ,
864+ ) ;
865+ try {
866+ const started = Date . now ( ) ;
867+ while ( Date . now ( ) - started < 5_000 ) {
868+ const probe = spawnSync ( "pgrep" , [ "-f" , token ] , { encoding : "utf8" } ) ;
869+ if ( ( probe . stdout ?. trim ( ) ?? "" ) . length > 0 ) break ;
870+ await new Promise ( ( r ) => setTimeout ( r , 50 ) ) ;
871+ }
872+ expect ( plugin . dispose ) . toBeDefined ( ) ;
873+ const first = plugin . dispose ! ( ) ;
874+ const second = plugin . dispose ! ( ) ;
875+ expect ( second ) . toBe ( first ) ;
876+ await Promise . all ( [ first , second ] ) ;
877+ await running ;
878+ const after = spawnSync ( "pgrep" , [ "-f" , token ] , { encoding : "utf8" } ) ;
879+ expect ( after . stdout ?. trim ( ) ?? "" ) . toBe ( "" ) ;
880+ } finally {
881+ spawnSync ( "pkill" , [ "-9" , "-f" , token ] ) ;
882+ }
883+ } ) ;
884+
785885 test ( "dispose fails when a child survives the reap window" , async ( ) => {
786886 const child = Object . assign ( new EventEmitter ( ) , {
787887 exitCode : null ,
0 commit comments