@@ -464,6 +464,66 @@ public void RestorePreviousSessions_FallbackCoversProcessErrors()
464464 "IsProcessError must be included in the RestorePreviousSessionsAsync fallback condition (not found after the 'Session not found' anchor)" ) ;
465465 }
466466
467+ // ===== Behavior test: process error → CreateSessionAsync fallback =====
468+ // Proves that when RestorePreviousSessionsAsync encounters a stale CLI server process,
469+ // the session is recreated via CreateSessionAsync rather than silently dropped.
470+ //
471+ // Architecture note: CopilotClient is a concrete SDK class (not mockable), and
472+ // ResumeSessionAsync is not virtual, so we can't inject a throwing client through
473+ // the full RestorePreviousSessionsAsync pipeline. Instead, this test verifies the
474+ // behavioral contract at the seam: IsProcessError detects the exception, and
475+ // CreateSessionAsync (the fallback) successfully creates the replacement session.
476+ // The structural test above guarantees these are wired together in RestorePreviousSessionsAsync.
477+
478+ [ Fact ]
479+ public async Task ProcessError_DuringRestore_FallbackCreatesSession ( )
480+ {
481+ // GIVEN: a process error exception (CLI server died, stale handle)
482+ var processError = new InvalidOperationException ( "No process is associated with this object." ) ;
483+
484+ // WHEN: IsProcessError evaluates it
485+ Assert . True ( CopilotService . IsProcessError ( processError ) ) ;
486+ // Also detected as a connection error (broader category)
487+ Assert . True ( CopilotService . IsConnectionError ( processError ) ) ;
488+
489+ // THEN: the CreateSessionAsync fallback path works — session is created and accessible
490+ var svc = CreateService ( ) ;
491+ await svc . ReconnectAsync ( new PolyPilot . Models . ConnectionSettings
492+ {
493+ Mode = PolyPilot . Models . ConnectionMode . Demo
494+ } ) ;
495+
496+ var fallbackSession = await svc . CreateSessionAsync ( "Recovered Session" , "gpt-4" ) ;
497+ Assert . NotNull ( fallbackSession ) ;
498+ Assert . Equal ( "Recovered Session" , fallbackSession . Name ) ;
499+
500+ var allSessions = svc . GetAllSessions ( ) . Select ( s => s . Name ) . ToList ( ) ;
501+ Assert . Contains ( "Recovered Session" , allSessions ) ;
502+ }
503+
504+ [ Fact ]
505+ public async Task ProcessError_WrappedInAggregate_FallbackCreatesSession ( )
506+ {
507+ // GIVEN: a process error wrapped in AggregateException (from TaskScheduler.UnobservedTaskException)
508+ var inner = new InvalidOperationException ( "No process is associated with this object." ) ;
509+ var aggregate = new AggregateException ( "A Task's exception(s) were not observed" , inner ) ;
510+
511+ // WHEN: IsProcessError evaluates the wrapped exception
512+ Assert . True ( CopilotService . IsProcessError ( aggregate ) ) ;
513+ Assert . True ( CopilotService . IsConnectionError ( aggregate ) ) ;
514+
515+ // THEN: the fallback path works
516+ var svc = CreateService ( ) ;
517+ await svc . ReconnectAsync ( new PolyPilot . Models . ConnectionSettings
518+ {
519+ Mode = PolyPilot . Models . ConnectionMode . Demo
520+ } ) ;
521+
522+ var session = await svc . CreateSessionAsync ( "Recovered Aggregate" , "gpt-4" ) ;
523+ Assert . NotNull ( session ) ;
524+ Assert . Equal ( "Recovered Aggregate" , session . Name ) ;
525+ }
526+
467527 // ===== SafeFireAndForget task observation =====
468528 // Prevents UnobservedTaskException from fire-and-forget _chatDb calls.
469529 // See crash log: "A Task's exception(s) were not observed" wrapping ConnectionLostException.
0 commit comments