@@ -371,6 +371,79 @@ describe('SessionOutcomeMirror (Session scope)', () => {
371371 expect ( writes ) . toEqual ( [ ] ) ;
372372 } ) ;
373373
374+ const restoredSession = (
375+ id : string ,
376+ persisted : SessionMeta [ 'lastTurnReason' ] ,
377+ lastEnded : FakeAgentLifecycle [ 'lastEnded' ] ,
378+ ) => {
379+ const scope = host . child ( LifecycleScope . Session , id , [
380+ stubPair ( ISessionMetadata , {
381+ read : async ( ) => ( { lastTurnReason : persisted } ) as SessionMeta ,
382+ update : async (
383+ patch : { lastTurnReason ?: SessionMeta [ 'lastTurnReason' ] } ,
384+ uopts ?: { touchUpdatedAt ?: boolean } ,
385+ ) => {
386+ writes . push ( patch . lastTurnReason ) ;
387+ touches . push ( uopts ?. touchUpdatedAt !== false ) ;
388+ } ,
389+ } as unknown as ISessionMetadata ) ,
390+ ] ) ;
391+ const scopeLifecycle = scope . accessor . get ( IAgentLifecycleService ) as unknown as FakeAgentLifecycle ;
392+ scope . accessor . get ( ISessionOutcomeMirror ) ;
393+ scopeLifecycle . addMain ( ) ;
394+ scopeLifecycle . lastEnded = lastEnded ;
395+ return scopeLifecycle ;
396+ } ;
397+
398+ it ( 'replaces a stale persisted outcome with the replayed completed turn' , async ( ) => {
399+ const restored = restoredSession ( 'session-restored-completed' , 'cancelled' , {
400+ turnId : 3 ,
401+ reason : 'completed' ,
402+ durationMs : 5 ,
403+ } ) ;
404+ await tick ( ) ;
405+ for ( const hook of restored . restoreHooks ) await hook ( undefined , async ( ) => { } ) ;
406+ expect ( writes ) . toEqual ( [ 'completed' ] ) ;
407+ expect ( touches ) . toEqual ( [ false ] ) ;
408+ } ) ;
409+
410+ it ( 'maps a replayed blocked turn onto the persisted failed outcome' , async ( ) => {
411+ const restored = restoredSession ( 'session-restored-blocked' , 'completed' , {
412+ turnId : 3 ,
413+ reason : 'blocked' ,
414+ durationMs : 5 ,
415+ } ) ;
416+ await tick ( ) ;
417+ for ( const hook of restored . restoreHooks ) await hook ( undefined , async ( ) => { } ) ;
418+ expect ( writes ) . toEqual ( [ 'failed' ] ) ;
419+ expect ( touches ) . toEqual ( [ false ] ) ;
420+ } ) ;
421+
422+ it ( 'keeps the persisted outcome when the replayed turn was cancelled' , async ( ) => {
423+ const restored = restoredSession ( 'session-restored-cancelled' , 'completed' , {
424+ turnId : 3 ,
425+ reason : 'cancelled' ,
426+ durationMs : 5 ,
427+ } ) ;
428+ await tick ( ) ;
429+ for ( const hook of restored . restoreHooks ) await hook ( undefined , async ( ) => { } ) ;
430+ expect ( writes ) . toEqual ( [ ] ) ;
431+ } ) ;
432+
433+ it ( 'tracks the replayed turn for the undo range after a restore reconcile' , async ( ) => {
434+ const restored = restoredSession ( 'session-restored-undo' , 'cancelled' , {
435+ turnId : 3 ,
436+ reason : 'completed' ,
437+ durationMs : 5 ,
438+ } ) ;
439+ await tick ( ) ;
440+ for ( const hook of restored . restoreHooks ) await hook ( undefined , async ( ) => { } ) ;
441+ restored . bus . publish ( new ContextUndone ( { agentId : 'main' , turns : 1 , fromTurnId : 4 } ) ) ;
442+ expect ( writes ) . toEqual ( [ 'completed' ] ) ;
443+ restored . bus . publish ( new ContextUndone ( { agentId : 'main' , turns : 1 , fromTurnId : 3 } ) ) ;
444+ expect ( writes ) . toEqual ( [ 'completed' , undefined ] ) ;
445+ } ) ;
446+
374447 it ( 'reattaches when the main agent is disposed and recreated' , async ( ) => {
375448 lifecycle . addMain ( ) ;
376449 await tick ( ) ;
0 commit comments