@@ -400,4 +400,125 @@ describe('auto-mode-service.ts', () => {
400400 expect ( duration ) . toBeLessThan ( 40 ) ;
401401 } ) ;
402402 } ) ;
403+
404+ describe ( 'isAgentRunning - cross-project isolation (Fix 2)' , ( ) => {
405+ // Helper to inject a running feature entry directly
406+ const addRunningFeature = (
407+ svc : AutoModeService ,
408+ featureId : string ,
409+ projectPath : string
410+ ) : void => {
411+ const runningFeaturesMap = ( svc as any ) . runningFeatures as Map <
412+ string ,
413+ { featureId : string ; projectPath : string ; abortController : AbortController }
414+ > ;
415+ runningFeaturesMap . set ( featureId , {
416+ featureId,
417+ projectPath,
418+ abortController : new AbortController ( ) ,
419+ } ) ;
420+ } ;
421+
422+ it ( 'should return true when featureId and projectPath both match' , ( ) => {
423+ addRunningFeature ( service , 'feat-1' , '/project/alpha' ) ;
424+
425+ expect ( service . isAgentRunning ( 'feat-1' , '/project/alpha' ) ) . toBe ( true ) ;
426+ } ) ;
427+
428+ it ( 'should return false when featureId matches but projectPath differs (cross-project collision)' , ( ) => {
429+ addRunningFeature ( service , 'feat-1' , '/project/alpha' ) ;
430+
431+ expect ( service . isAgentRunning ( 'feat-1' , '/project/beta' ) ) . toBe ( false ) ;
432+ } ) ;
433+
434+ it ( 'should return false when featureId does not exist' , ( ) => {
435+ expect ( service . isAgentRunning ( 'nonexistent' , '/project/alpha' ) ) . toBe ( false ) ;
436+ } ) ;
437+ } ) ;
438+
439+ describe ( 'sendCIFailureToAgent - cross-project isolation (Fix 2)' , ( ) => {
440+ const addRunningFeature = (
441+ svc : AutoModeService ,
442+ featureId : string ,
443+ projectPath : string
444+ ) : void => {
445+ const runningFeaturesMap = ( svc as any ) . runningFeatures as Map <
446+ string ,
447+ { featureId : string ; projectPath : string ; abortController : AbortController }
448+ > ;
449+ runningFeaturesMap . set ( featureId , {
450+ featureId,
451+ projectPath,
452+ abortController : new AbortController ( ) ,
453+ } ) ;
454+ } ;
455+
456+ const getPendingInjections = ( svc : AutoModeService ) : Map < string , string > =>
457+ ( svc as any ) . pendingCIInjections as Map < string , string > ;
458+
459+ it ( 'should queue injection when featureId and projectPath both match' , async ( ) => {
460+ addRunningFeature ( service , 'feat-1' , '/project/alpha' ) ;
461+
462+ const result = await service . sendCIFailureToAgent ( 'feat-1' , '/project/alpha' , 'CI failed' ) ;
463+
464+ expect ( result ) . toBe ( true ) ;
465+ expect ( getPendingInjections ( service ) . get ( 'feat-1' ) ) . toBe ( 'CI failed' ) ;
466+ } ) ;
467+
468+ it ( 'should reject injection when projectPath differs (cross-project protection)' , async ( ) => {
469+ addRunningFeature ( service , 'feat-1' , '/project/alpha' ) ;
470+
471+ const result = await service . sendCIFailureToAgent ( 'feat-1' , '/project/beta' , 'CI failed' ) ;
472+
473+ expect ( result ) . toBe ( false ) ;
474+ expect ( getPendingInjections ( service ) . has ( 'feat-1' ) ) . toBe ( false ) ;
475+ } ) ;
476+
477+ it ( 'should return false when feature is not running' , async ( ) => {
478+ const result = await service . sendCIFailureToAgent ( 'nonexistent' , '/project/alpha' , 'msg' ) ;
479+
480+ expect ( result ) . toBe ( false ) ;
481+ } ) ;
482+ } ) ;
483+
484+ describe ( 'pendingCIInjections teardown (Fix 3)' , ( ) => {
485+ const addRunningFeature = (
486+ svc : AutoModeService ,
487+ featureId : string ,
488+ projectPath : string
489+ ) : void => {
490+ const runningFeaturesMap = ( svc as any ) . runningFeatures as Map <
491+ string ,
492+ { featureId : string ; projectPath : string ; abortController : AbortController }
493+ > ;
494+ runningFeaturesMap . set ( featureId , {
495+ featureId,
496+ projectPath,
497+ abortController : new AbortController ( ) ,
498+ } ) ;
499+ } ;
500+
501+ const getPendingInjections = ( svc : AutoModeService ) : Map < string , string > =>
502+ ( svc as any ) . pendingCIInjections as Map < string , string > ;
503+
504+ it ( 'should clear pendingCIInjections when stopFeature is called' , async ( ) => {
505+ addRunningFeature ( service , 'feat-stop' , '/project/alpha' ) ;
506+ getPendingInjections ( service ) . set ( 'feat-stop' , 'stale CI message' ) ;
507+
508+ await service . stopFeature ( 'feat-stop' ) ;
509+
510+ expect ( getPendingInjections ( service ) . has ( 'feat-stop' ) ) . toBe ( false ) ;
511+ } ) ;
512+
513+ it ( 'should clear all pendingCIInjections on shutdown' , async ( ) => {
514+ addRunningFeature ( service , 'feat-a' , '/project/alpha' ) ;
515+ addRunningFeature ( service , 'feat-b' , '/project/beta' ) ;
516+ getPendingInjections ( service ) . set ( 'feat-a' , 'stale msg a' ) ;
517+ getPendingInjections ( service ) . set ( 'feat-b' , 'stale msg b' ) ;
518+
519+ await service . shutdown ( ) ;
520+
521+ expect ( getPendingInjections ( service ) . size ) . toBe ( 0 ) ;
522+ } ) ;
523+ } ) ;
403524} ) ;
0 commit comments