@@ -906,6 +906,54 @@ describe('orchestrator', () => {
906906 // Safety net should have corrected the stuck job status
907907 expect ( planStateMod . updatePlanJob ) . toHaveBeenCalledWith ( 'plan-1' , 'stuck-job' , { status : 'completed' } ) ;
908908 } ) ;
909+
910+ it ( 'reconciliation safety net recovers stopped jobs as failed' , async ( ) => {
911+ planState = makePlan ( {
912+ status : 'running' ,
913+ jobs : [
914+ makeJob ( 'killed-job' , { status : 'running' , mergeOrder : 0 } ) ,
915+ makeJob ( 'merged-job' , { status : 'merged' , mergeOrder : 1 } ) ,
916+ ] ,
917+ } ) ;
918+
919+ runningJobs = [ ] ;
920+
921+ spyOn ( jobStateMod , 'loadJobState' ) . mockImplementation ( async ( ) => ( {
922+ version : 2 ,
923+ jobs : [
924+ {
925+ id : 'killed-job-id' ,
926+ name : 'killed-job' ,
927+ planId : 'plan-1' ,
928+ status : 'stopped' as const ,
929+ worktreePath : '/tmp/w1' ,
930+ branch : 'mc/killed-job' ,
931+ tmuxTarget : 'mc-killed-job' ,
932+ placement : 'session' as const ,
933+ prompt : 'do killed-job' ,
934+ mode : 'vanilla' as const ,
935+ createdAt : '2026-01-01T00:00:00.000Z' ,
936+ completedAt : '2026-01-01T00:01:00.000Z' ,
937+ } ,
938+ ] ,
939+ updatedAt : new Date ( ) . toISOString ( ) ,
940+ } ) ) ;
941+
942+ const orchestrator = new Orchestrator ( monitor as any , {
943+ defaultPlacement : 'session' ,
944+ pollInterval : 10000 ,
945+ idleThreshold : 300000 ,
946+ worktreeBasePath : '/tmp' ,
947+ omo : { enabled : false , defaultMode : 'vanilla' } ,
948+ } as any ) ;
949+
950+ await ( orchestrator as any ) . reconcile ( ) ;
951+
952+ expect ( planStateMod . updatePlanJob ) . toHaveBeenCalledWith ( 'plan-1' , 'killed-job' , {
953+ status : 'failed' ,
954+ error : 'recovered from missed completion event' ,
955+ } ) ;
956+ } ) ;
909957} ) ;
910958
911959describe ( 'orchestrator reconcile pending (dirty re-reconcile)' , ( ) => {
0 commit comments