@@ -63,6 +63,24 @@ vi.mock('../copilotCLITerminalIntegration', () => {
6363 } ;
6464} ) ;
6565
66+ // Mock vscode.commands.executeCommand so we can control delegation behavior in tests.
67+ // By default it throws (simulating commands API not being available), which causes
68+ // createCLISessionAndSubmitRequest to fall into its catch block and call handleRequest directly.
69+ // The workaround tests override this to simulate the full VS Code core round-trip.
70+ const { mockExecuteCommand } = vi . hoisted ( ( ) => ( {
71+ mockExecuteCommand : vi . fn ( )
72+ } ) ) ;
73+
74+ vi . mock ( 'vscode' , async ( importOriginal ) => {
75+ const actual = await import ( '../../../../vscodeTypes' ) ;
76+ return {
77+ ...actual ,
78+ commands : {
79+ executeCommand : mockExecuteCommand
80+ }
81+ } ;
82+ } ) ;
83+
6684class FakeToolsService extends mock < IToolsService > ( ) {
6785 nextConfirmationButton : string | undefined = undefined ;
6886 override invokeTool = vi . fn ( async ( name : string , _options : unknown , _token : unknown ) => {
@@ -212,6 +230,9 @@ describe('CopilotCLIChatSessionParticipant.handleRequest', () => {
212230
213231 beforeEach ( async ( ) => {
214232 cliSessions . length = 0 ;
233+ // Reset executeCommand to throw by default — existing delegation tests rely on this
234+ // falling into the catch block of createCLISessionAndSubmitRequest.
235+ mockExecuteCommand . mockImplementation ( ( ) => { throw new Error ( 'vscode.commands.executeCommand not available in test' ) ; } ) ;
215236 const sdk = {
216237 getPackage : vi . fn ( async ( ) => ( { internal : { LocalSessionManager : MockCliSdkSessionManager } } ) )
217238 } as unknown as ICopilotCLISDK ;
@@ -1286,78 +1307,129 @@ describe('CopilotCLIChatSessionParticipant.handleRequest', () => {
12861307 } ) ;
12871308
12881309 describe ( 'chatSessionContext lost workaround (core bug)' , ( ) => {
1289- // Tests for the workaround at lines 863-884 in copilotCLIChatSessionsContribution.ts.
1290- // When delegating from another chat, VS Code core may drop chatSessionContext before the
1291- // request handler runs. Without the workaround the handler would re-enter delegation,
1292- // creating an infinite loop. The fix detects a copilotcli:// sessionResource with stored
1293- // contextForRequest data and reconstructs a synthetic chatSessionContext.
1294-
1295- it ( 'reconstructs context and reuses session when chatSessionContext is lost but contextForRequest has stored data' , async ( ) => {
1296- const sessionId = 'existing-delegated-123' ;
1297- const sdkSession = new MockCliSdkSession ( sessionId , new Date ( ) ) ;
1298- manager . sessions . set ( sessionId , sdkSession ) ;
1299-
1300- // Pre-populate contextForRequest to simulate a prior delegation via createCLISessionAndSubmitRequest
1301- const storedPrompt = 'stored prompt from delegation' ;
1302- const storedAttachments : Attachment [ ] = [ ] ;
1303- ( participant as any ) . contextForRequest . set ( sessionId , { prompt : storedPrompt , attachments : storedAttachments } ) ;
1304-
1305- const request = new TestChatRequest ( 'request prompt that should be ignored' ) ;
1306- // Override sessionResource to be a copilotcli:// URI matching the existing session
1307- request . sessionResource = vscode . Uri . from ( { scheme : 'copilotcli' , path : `/${ sessionId } ` } ) as any ;
1310+ // Full end-to-end tests for the delegation → executeCommand → workaround round-trip.
1311+ //
1312+ // When delegating from another chat:
1313+ // 1. handleRequest is called with chatSessionContext=undefined → triggers handleDelegationFromAnotherChat
1314+ // 2. createCLISessionAndSubmitRequest creates a session, stores prompt in contextForRequest,
1315+ // then calls vscode.commands.executeCommand('workbench.action.chat.openSessionWithPrompt.copilotcli', ...)
1316+ // 3. VS Code core opens the new session and calls handleRequest again with the copilotcli:// resource,
1317+ // but due to a core bug chatSessionContext may be undefined
1318+ // 4. The workaround detects the copilotcli:// scheme + stored contextForRequest data and
1319+ // reconstructs a synthetic chatSessionContext, so the session is reused with the stored prompt.
1320+
1321+ beforeEach ( ( ) => {
1322+ // Override the default throwing behavior to simulate VS Code core
1323+ // calling handleRequest again with the copilotcli:// resource but with chatSessionContext lost.
1324+ mockExecuteCommand . mockImplementation ( async ( command : string , args : any ) => {
1325+ if ( command === 'workbench.action.chat.openSessionWithPrompt.copilotcli' ) {
1326+ // Simulate VS Code core: it opens the session and fires handleRequest,
1327+ // but the core bug means chatSessionContext is undefined.
1328+ const callbackRequest = new TestChatRequest ( args . prompt ) ;
1329+ callbackRequest . sessionResource = args . resource ;
1330+ const callbackContext = { chatSessionContext : undefined } as vscode . ChatContext ;
1331+ const callbackStream = new MockChatResponseStream ( ) ;
1332+ const callbackToken = disposables . add ( new CancellationTokenSource ( ) ) . token ;
1333+ await participant . createHandler ( ) ( callbackRequest , callbackContext , callbackStream , callbackToken ) ;
1334+ }
1335+ } ) ;
1336+ } ) ;
13081337
1309- // chatSessionContext is undefined — this is the core bug condition
1338+ it ( 'full delegation round-trip: executeCommand triggers callback that uses workaround to reconstruct context and reuse session' , async ( ) => {
1339+ // Start delegation: call handleRequest with no chatSessionContext.
1340+ // This triggers handleDelegationFromAnotherChat → createCLISessionAndSubmitRequest
1341+ // which creates a session, stores prompt/attachments, calls executeCommand.
1342+ // The mock executeCommand simulates VS Code calling handleRequest again with
1343+ // the copilotcli:// resource but chatSessionContext=undefined (the core bug).
1344+ // The workaround reconstructs context and reuses the session.
1345+ const request = new TestChatRequest ( 'Build feature X' ) ;
13101346 const context = { chatSessionContext : undefined } as vscode . ChatContext ;
13111347 const stream = new MockChatResponseStream ( ) ;
13121348 const token = disposables . add ( new CancellationTokenSource ( ) ) . token ;
13131349
13141350 await participant . createHandler ( ) ( request , context , stream , token ) ;
13151351
1316- // Should reuse the existing session instead of creating a new delegation session
1352+ // executeCommand should have been called with the correct command and args
1353+ expect ( mockExecuteCommand ) . toHaveBeenCalledWith (
1354+ 'workbench.action.chat.openSessionWithPrompt.copilotcli' ,
1355+ expect . objectContaining ( {
1356+ resource : expect . objectContaining ( { scheme : 'copilotcli' } ) ,
1357+ prompt : 'Build feature X' ,
1358+ } )
1359+ ) ;
1360+
1361+ // Only one session should have been created (the delegation creates it,
1362+ // and the callback reuses it via the workaround — no second session).
13171363 expect ( cliSessions . length ) . toBe ( 1 ) ;
1318- expect ( cliSessions [ 0 ] . sessionId ) . toBe ( sessionId ) ;
13191364
1320- // Should use the stored prompt from contextForRequest, not request.prompt
1365+ // The session's handleRequest should have been called exactly once,
1366+ // using the stored prompt from contextForRequest (set during delegation).
13211367 expect ( cliSessions [ 0 ] . requests . length ) . toBe ( 1 ) ;
1322- expect ( cliSessions [ 0 ] . requests [ 0 ] . input ) . toEqual ( { prompt : storedPrompt } ) ;
1323- expect ( cliSessions [ 0 ] . requests [ 0 ] . attachments ) . toEqual ( storedAttachments ) ;
1368+ expect ( cliSessions [ 0 ] . requests [ 0 ] . input ) . toEqual (
1369+ expect . objectContaining ( { prompt : expect . stringContaining ( 'Build feature X' ) } )
1370+ ) ;
1371+
1372+ // contextForRequest should have been consumed (cleaned up after use)
1373+ expect ( ( participant as any ) . contextForRequest . size ) . toBe ( 0 ) ;
13241374 } ) ;
13251375
1326- it ( 'falls through to delegation when copilotcli resource has no stored context' , async ( ) => {
1327- const sessionId = 'no-stored-context-456' ;
1328- // Do NOT populate contextForRequest — simulates a genuinely missing context
1329- // without the prior delegation step having stored data
1376+ it ( 'falls through to new delegation when executeCommand callback has a different session id with no stored context' , async ( ) => {
1377+ // Override the mock ONCE: the first callback uses a DIFFERENT copilotcli:// session id
1378+ // that has nothing in contextForRequest. The workaround should NOT activate for that id,
1379+ // and instead it falls through to a new delegation creating another session.
1380+ // The second executeCommand call (from that inner delegation) falls back to the
1381+ // default mock which correctly passes args.resource, activating the workaround.
1382+ mockExecuteCommand . mockImplementationOnce ( async ( command : string , args : any ) => {
1383+ if ( command === 'workbench.action.chat.openSessionWithPrompt.copilotcli' ) {
1384+ const callbackRequest = new TestChatRequest ( args . prompt ) ;
1385+ // Use a different session id than the one created by the delegation
1386+ callbackRequest . sessionResource = vscode . Uri . from ( { scheme : 'copilotcli' , path : '/unknown-session-999' } ) as any ;
1387+ const callbackContext = { chatSessionContext : undefined } as vscode . ChatContext ;
1388+ const callbackStream = new MockChatResponseStream ( ) ;
1389+ const callbackToken = disposables . add ( new CancellationTokenSource ( ) ) . token ;
1390+ await participant . createHandler ( ) ( callbackRequest , callbackContext , callbackStream , callbackToken ) ;
1391+ }
1392+ } ) ;
13301393
13311394 const request = new TestChatRequest ( 'delegate this prompt' ) ;
1332- request . sessionResource = vscode . Uri . from ( { scheme : 'copilotcli' , path : `/${ sessionId } ` } ) as any ;
1333-
13341395 const context = { chatSessionContext : undefined } as vscode . ChatContext ;
13351396 const stream = new MockChatResponseStream ( ) ;
13361397 const token = disposables . add ( new CancellationTokenSource ( ) ) . token ;
13371398
13381399 await participant . createHandler ( ) ( request , context , stream , token ) ;
13391400
1340- // Should fall through to delegation path and create a new session
1341- expect ( cliSessions . length ) . toBe ( 1 ) ;
1342- expect ( cliSessions [ 0 ] . requests . length ) . toBe ( 1 ) ;
1343- expect ( cliSessions [ 0 ] . requests [ 0 ] . input ) . toEqual (
1401+ // Two sessions should exist: the first from the initial delegation,
1402+ // and a second created when the callback fell through to delegation
1403+ // (because the workaround did not activate for the unknown session id).
1404+ // The second session's executeCommand call used the default mock which
1405+ // correctly passed the resource, allowing the workaround to activate.
1406+ expect ( cliSessions . length ) . toBe ( 2 ) ;
1407+ // The second session should have had its handleRequest called (via the workaround)
1408+ expect ( cliSessions [ 1 ] . requests . length ) . toBe ( 1 ) ;
1409+ expect ( cliSessions [ 1 ] . requests [ 0 ] . input ) . toEqual (
13441410 expect . objectContaining ( { prompt : expect . stringContaining ( 'delegate this prompt' ) } )
13451411 ) ;
13461412 } ) ;
13471413
1348- it ( 'does not attempt workaround for non-copilotcli resource even if contextForRequest has data' , async ( ) => {
1349- // Pre-populate contextForRequest with an unrelated ID
1350- ( participant as any ) . contextForRequest . set ( 'some-id' , { prompt : 'stored' , attachments : [ ] } ) ;
1351-
1414+ it ( 'does not attempt workaround for non-copilotcli resource and proceeds with normal delegation' , async ( ) => {
13521415 const request = new TestChatRequest ( 'do some work' ) ;
1353- // Default sessionResource is test://session/... (not copilotcli scheme)
1416+ // Default sessionResource is test://session/... (not copilotcli scheme),
1417+ // so the workaround check at the top of handleRequest is skipped entirely.
13541418 const context = { chatSessionContext : undefined } as vscode . ChatContext ;
13551419 const stream = new MockChatResponseStream ( ) ;
13561420 const token = disposables . add ( new CancellationTokenSource ( ) ) . token ;
13571421
13581422 await participant . createHandler ( ) ( request , context , stream , token ) ;
13591423
1360- // Should create a new session via the delegation path (not the workaround)
1424+ // executeCommand should have been called (delegation creates a session and calls it)
1425+ expect ( mockExecuteCommand ) . toHaveBeenCalledWith (
1426+ 'workbench.action.chat.openSessionWithPrompt.copilotcli' ,
1427+ expect . objectContaining ( {
1428+ prompt : 'do some work' ,
1429+ } )
1430+ ) ;
1431+
1432+ // A session should have been created via the delegation path
13611433 expect ( cliSessions . length ) . toBe ( 1 ) ;
13621434 expect ( cliSessions [ 0 ] . requests . length ) . toBe ( 1 ) ;
13631435 expect ( cliSessions [ 0 ] . requests [ 0 ] . input ) . toEqual (
0 commit comments