@@ -183,25 +183,64 @@ public async Task StopAsync_Keeps_Session_Rooted_Until_Destroy_Completes()
183183 }
184184
185185 [ Fact ]
186- public async Task ResumeSessionAsync_Throws_When_Same_Client_Already_Tracks_Session ( )
186+ public async Task ResumeSessionAsync_Replaces_Session_Tracked_By_Same_Client ( )
187187 {
188188 await using var server = await FakeCopilotServer . StartAsync ( ) ;
189189 await using var client = new CopilotClient ( new CopilotClientOptions { Connection = RuntimeConnection . ForUri ( server . Url ) } ) ;
190190
191191 var sessionId = "same-session-id" ;
192- await using var session = await client . CreateSessionAsync ( new SessionConfig
192+ var session = await client . CreateSessionAsync ( new SessionConfig
193193 {
194194 SessionId = sessionId ,
195195 OnPermissionRequest = PermissionHandler . ApproveAll
196196 } ) ;
197197 AssertSessionCount ( client , sessions : 1 ) ;
198198
199- var exception = await Assert . ThrowsAsync < InvalidOperationException > ( ( ) => client . ResumeSessionAsync ( sessionId , new ResumeSessionConfig
199+ var resumedSession = await client . ResumeSessionAsync ( sessionId , new ResumeSessionConfig
200+ {
201+ OnPermissionRequest = PermissionHandler . ApproveAll
202+ } ) ;
203+
204+ Assert . NotSame ( session , resumedSession ) ;
205+ AssertSessionCount ( client , sessions : 1 ) ;
206+ Assert . Same ( resumedSession , GetTrackedSession ( client , sessionId ) ) ;
207+ Assert . Equal ( "message-1" , await session . SendAsync ( "The previous wrapper remains callable." ) ) ;
208+ Assert . DoesNotContain ( server . Requests , request => request . Method == "session.destroy" ) ;
209+
210+ await session . DisposeAsync ( ) ;
211+ AssertSessionCount ( client , sessions : 1 ) ;
212+
213+ await resumedSession . DisposeAsync ( ) ;
214+ AssertSessionCount ( client , sessions : 0 ) ;
215+ Assert . Equal ( 2 , server . Requests . Count ( request => request . Method == "session.destroy" ) ) ;
216+ }
217+
218+ [ Fact ]
219+ public async Task Failed_ResumeSessionAsync_Restores_Previous_Registration ( )
220+ {
221+ await using var server = await FakeCopilotServer . StartAsync ( ) ;
222+ await using var client = new CopilotClient ( new CopilotClientOptions { Connection = RuntimeConnection . ForUri ( server . Url ) } ) ;
223+
224+ var sessionId = "same-session-id" ;
225+ var session = await client . CreateSessionAsync ( new SessionConfig
226+ {
227+ SessionId = sessionId ,
228+ OnPermissionRequest = PermissionHandler . ApproveAll
229+ } ) ;
230+ server . FailNextResume ( ) ;
231+
232+ await Assert . ThrowsAsync < IOException > ( ( ) => client . ResumeSessionAsync ( sessionId , new ResumeSessionConfig
200233 {
201234 OnPermissionRequest = PermissionHandler . ApproveAll
202235 } ) ) ;
203- Assert . Contains ( sessionId , exception . Message ) ;
236+
204237 AssertSessionCount ( client , sessions : 1 ) ;
238+ Assert . Same ( session , GetTrackedSession ( client , sessionId ) ) ;
239+ Assert . Equal ( "message-1" , await session . SendAsync ( "The original session remains active." ) ) ;
240+
241+ await session . DisposeAsync ( ) ;
242+ AssertSessionCount ( client , sessions : 0 ) ;
243+ Assert . Single ( server . Requests , request => request . Method == "session.destroy" ) ;
205244 }
206245
207246 [ Fact ]
@@ -438,6 +477,13 @@ private static void AssertSessionCount(CopilotClient client, int sessions)
438477 Assert . Equal ( sessions , GetPrivateDictionaryCount ( client , "_sessions" ) ) ;
439478 }
440479
480+ private static CopilotSession ? GetTrackedSession ( CopilotClient client , string sessionId )
481+ {
482+ var method = typeof ( CopilotClient ) . GetMethod ( "GetSession" , BindingFlags . Instance | BindingFlags . NonPublic )
483+ ?? throw new InvalidOperationException ( "GetSession method was not found." ) ;
484+ return ( CopilotSession ? ) method . Invoke ( client , [ sessionId ] ) ;
485+ }
486+
441487 private static int GetPrivateDictionaryCount ( CopilotClient client , string fieldName )
442488 {
443489 var field = typeof ( CopilotClient ) . GetField ( fieldName , BindingFlags . Instance | BindingFlags . NonPublic )
@@ -518,6 +564,7 @@ private sealed class FakeCopilotServer : IAsyncDisposable
518564 private string ? _lastSessionId ;
519565 private bool _delayDestroy ;
520566 private bool _failRuntimeShutdown ;
567+ private bool _failNextResume ;
521568
522569 private FakeCopilotServer ( TcpListener listener )
523570 {
@@ -579,6 +626,11 @@ public void FailRuntimeShutdown()
579626 _failRuntimeShutdown = true ;
580627 }
581628
629+ public void FailNextResume ( )
630+ {
631+ _failNextResume = true ;
632+ }
633+
582634 public async ValueTask DisposeAsync ( )
583635 {
584636 _allowDestroy . TrySetResult ( ) ;
@@ -623,6 +675,22 @@ private async Task HandleRequestAsync(Stream stream, JsonElement request, Cancel
623675
624676 var id = idElement . Clone ( ) ;
625677 var method = request . GetProperty ( "method" ) . GetString ( ) ;
678+ if ( method == "session.resume" && _failNextResume )
679+ {
680+ _failNextResume = false ;
681+ await WriteMessageAsync ( stream , new Dictionary < string , object ? >
682+ {
683+ [ "jsonrpc" ] = "2.0" ,
684+ [ "id" ] = id ,
685+ [ "error" ] = new Dictionary < string , object ? >
686+ {
687+ [ "code" ] = - 32000 ,
688+ [ "message" ] = "session resume failed"
689+ }
690+ } , cancellationToken ) ;
691+ return ;
692+ }
693+
626694 if ( method == "runtime.shutdown" && _failRuntimeShutdown )
627695 {
628696 RuntimeShutdownCount ++ ;
0 commit comments