@@ -304,71 +304,75 @@ describe("Multi-client broadcast", async () => {
304304 }
305305 ) ;
306306
307- it . skipIf ( isInProcessTransport ) ( "disconnecting client removes its tools" , { timeout : 90_000 } , async ( ) => {
308- const toolA = defineTool ( "stable_tool" , {
309- description : "A tool that persists across disconnects" ,
310- parameters : z . object ( { input : z . string ( ) } ) ,
311- handler : ( { input } ) => `STABLE_${ input } ` ,
312- } ) ;
313-
314- const toolB = defineTool ( "ephemeral_tool" , {
315- description : "A tool that will disappear when its client disconnects" ,
316- parameters : z . object ( { input : z . string ( ) } ) ,
317- handler : ( { input } ) => `EPHEMERAL_${ input } ` ,
318- } ) ;
319-
320- // Client 1 creates a session with stable_tool
321- const session1 = await client1 . createSession ( {
322- onPermissionRequest : approveAll ,
323- tools : [ toolA ] ,
324- } ) ;
307+ it . skipIf ( isInProcessTransport ) (
308+ "disconnecting client removes its tools" ,
309+ { timeout : 90_000 } ,
310+ async ( ) => {
311+ const toolA = defineTool ( "stable_tool" , {
312+ description : "A tool that persists across disconnects" ,
313+ parameters : z . object ( { input : z . string ( ) } ) ,
314+ handler : ( { input } ) => `STABLE_${ input } ` ,
315+ } ) ;
325316
326- // Client 2 resumes with ephemeral_tool
327- await client2 . resumeSession ( session1 . sessionId , {
328- onPermissionRequest : approveAll ,
329- tools : [ toolB ] ,
330- } ) ;
317+ const toolB = defineTool ( " ephemeral_tool" , {
318+ description : "A tool that will disappear when its client disconnects" ,
319+ parameters : z . object ( { input : z . string ( ) } ) ,
320+ handler : ( { input } ) => `EPHEMERAL_ ${ input } ` ,
321+ } ) ;
331322
332- // Verify both tools work before disconnect (sequential to avoid nondeterministic tool_call ordering)
333- const stableResponse = await session1 . sendAndWait ( {
334- prompt : "Use the stable_tool with input 'test1' and tell me the result." ,
335- } ) ;
336- expect ( stableResponse ?. data . content ) . toContain ( "STABLE_test1" ) ;
323+ // Client 1 creates a session with stable_tool
324+ const session1 = await client1 . createSession ( {
325+ onPermissionRequest : approveAll ,
326+ tools : [ toolA ] ,
327+ } ) ;
337328
338- const ephemeralResponse = await session1 . sendAndWait ( {
339- prompt : "Use the ephemeral_tool with input 'test2' and tell me the result." ,
340- } ) ;
341- expect ( ephemeralResponse ?. data . content ) . toContain ( "EPHEMERAL_test2" ) ;
342-
343- // Disconnect client 2 without destroying the shared session.
344- // Suppress "Connection is disposed" rejections that occur when the server
345- // broadcasts events (e.g. tool_changed_notice) to the now-dead connection.
346- const suppressDisposed = ( reason : unknown ) => {
347- if ( reason instanceof Error && reason . message . includes ( "Connection is disposed" ) ) {
348- return ;
349- }
350- throw reason ;
351- } ;
352- process . on ( "unhandledRejection" , suppressDisposed ) ;
353- await client2 . stop ( ) ;
329+ // Client 2 resumes with ephemeral_tool
330+ await client2 . resumeSession ( session1 . sessionId , {
331+ onPermissionRequest : approveAll ,
332+ tools : [ toolB ] ,
333+ } ) ;
354334
355- // Give the server time to process the connection close and remove tools
356- await new Promise ( ( resolve ) => setTimeout ( resolve , 500 ) ) ;
357- process . removeListener ( "unhandledRejection" , suppressDisposed ) ;
335+ // Verify both tools work before disconnect (sequential to avoid nondeterministic tool_call ordering)
336+ const stableResponse = await session1 . sendAndWait ( {
337+ prompt : "Use the stable_tool with input 'test1' and tell me the result." ,
338+ } ) ;
339+ expect ( stableResponse ?. data . content ) . toContain ( "STABLE_test1" ) ;
358340
359- // Recreate client2 for cleanup in afterAll (but don't rejoin the session)
360- client2 = new CopilotClient ( {
361- connection : RuntimeConnection . forUri ( `localhost:${ runtimePort } ` , {
362- connectionToken : tcpConnectionToken ,
363- } ) ,
364- } ) ;
341+ const ephemeralResponse = await session1 . sendAndWait ( {
342+ prompt : "Use the ephemeral_tool with input 'test2' and tell me the result." ,
343+ } ) ;
344+ expect ( ephemeralResponse ?. data . content ) . toContain ( "EPHEMERAL_test2" ) ;
345+
346+ // Disconnect client 2 without destroying the shared session.
347+ // Suppress "Connection is disposed" rejections that occur when the server
348+ // broadcasts events (e.g. tool_changed_notice) to the now-dead connection.
349+ const suppressDisposed = ( reason : unknown ) => {
350+ if ( reason instanceof Error && reason . message . includes ( "Connection is disposed" ) ) {
351+ return ;
352+ }
353+ throw reason ;
354+ } ;
355+ process . on ( "unhandledRejection" , suppressDisposed ) ;
356+ await client2 . stop ( ) ;
357+
358+ // Give the server time to process the connection close and remove tools
359+ await new Promise ( ( resolve ) => setTimeout ( resolve , 500 ) ) ;
360+ process . removeListener ( "unhandledRejection" , suppressDisposed ) ;
361+
362+ // Recreate client2 for cleanup in afterAll (but don't rejoin the session)
363+ client2 = new CopilotClient ( {
364+ connection : RuntimeConnection . forUri ( `localhost:${ runtimePort } ` , {
365+ connectionToken : tcpConnectionToken ,
366+ } ) ,
367+ } ) ;
365368
366- // Now only stable_tool should be available
367- const afterResponse = await session1 . sendAndWait ( {
368- prompt : "Use the stable_tool with input 'still_here'. Also try using ephemeral_tool if it is available." ,
369- } ) ;
370- expect ( afterResponse ?. data . content ) . toContain ( "STABLE_still_here" ) ;
371- // ephemeral_tool should NOT have produced a result
372- expect ( afterResponse ?. data . content ) . not . toContain ( "EPHEMERAL_" ) ;
373- } ) ;
369+ // Now only stable_tool should be available
370+ const afterResponse = await session1 . sendAndWait ( {
371+ prompt : "Use the stable_tool with input 'still_here'. Also try using ephemeral_tool if it is available." ,
372+ } ) ;
373+ expect ( afterResponse ?. data . content ) . toContain ( "STABLE_still_here" ) ;
374+ // ephemeral_tool should NOT have produced a result
375+ expect ( afterResponse ?. data . content ) . not . toContain ( "EPHEMERAL_" ) ;
376+ }
377+ ) ;
374378} ) ;
0 commit comments