Skip to content

Commit b35e6a5

Browse files
committed
Update tests
1 parent 88289e2 commit b35e6a5

File tree

1 file changed

+113
-41
lines changed

1 file changed

+113
-41
lines changed

src/extension/chatSessions/vscode-node/test/copilotCLIChatSessionParticipant.spec.ts

Lines changed: 113 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
6684
class 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

Comments
 (0)