Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/test/e2e/scenarioTest.ts
13388 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import assert from 'assert';
7
import * as fs from 'fs';
8
import type { ChatErrorDetails, LanguageModelToolInformation, MappedEditsResponseStream, TextDocument } from 'vscode';
9
import { CodeBlocksMetadata } from '../../src/extension/codeBlocks/node/codeBlockProcessor';
10
import { agentsToCommands, Intent } from '../../src/extension/common/constants';
11
import '../../src/extension/intents/node/allIntents';
12
import { ChatParticipantRequestHandler } from '../../src/extension/prompt/node/chatParticipantRequestHandler';
13
import { IDocumentContext } from '../../src/extension/prompt/node/documentContext';
14
import { CodeMapper, ICodeMapperExistingDocument } from '../../src/extension/prompts/node/codeMapper/codeMapper';
15
import { getContributedToolName } from '../../src/extension/tools/common/toolNames';
16
import '../../src/extension/tools/node/allTools';
17
import { getChatParticipantIdFromName } from '../../src/platform/chat/common/chatAgents';
18
import { IConversationOptions } from '../../src/platform/chat/common/conversationOptions';
19
import { ITabsAndEditorsService } from '../../src/platform/tabs/common/tabsAndEditorsService';
20
import { isInExtensionHost } from '../../src/platform/test/node/isInExtensionHost';
21
import { isNotebook, SimulationWorkspace } from '../../src/platform/test/node/simulationWorkspace';
22
import { SpyChatResponseStream } from '../../src/util/common/test/mockChatResponseStream';
23
import { ChatRequestTurn, ChatResponseTurn } from '../../src/util/common/test/shims/chatTypes';
24
import { CancellationToken } from '../../src/util/vs/base/common/cancellation';
25
import { DisposableStore } from '../../src/util/vs/base/common/lifecycle';
26
import { IInstantiationService } from '../../src/util/vs/platform/instantiation/common/instantiation';
27
import { ChatLocation, ChatRequest, ChatResponseAnchorPart, ChatResponseMarkdownPart, Uri } from '../../src/vscodeTypes';
28
import { SimulationWorkspaceExtHost } from '../base/extHostContext/simulationWorkspaceExtHost';
29
import { ISimulationTestRuntime, SimulationTestFunction } from '../base/stest';
30
import { INLINE_CHANGED_DOC_TAG, INLINE_INITIAL_DOC_TAG, IWorkspaceStateFile } from '../simulation/shared/sharedTypes';
31
import { Scenario, ScenarioEvaluator } from './scenarioLoader';
32
import { parseQueryForScenarioTest } from './testHelper';
33
34
/**
35
* Grabs the default conversation options. Copied over from conversationFeature.ts
36
* TODO @lramos15, these should use the same code as conversationFeature.ts
37
*/
38
export function fetchConversationOptions() {
39
const maxResponseTokens = undefined;
40
const temperature = 0.1;
41
const topP = 1;
42
43
const options: IConversationOptions = {
44
_serviceBrand: undefined,
45
46
maxResponseTokens: maxResponseTokens,
47
temperature: temperature,
48
topP: topP,
49
rejectionMessage: 'Sorry, but I can only assist with programming related questions.',
50
};
51
return options;
52
}
53
54
export function generateScenarioTestRunner(scenario: Scenario, evaluator: ScenarioEvaluator): SimulationTestFunction {
55
return async function (testingServiceCollection) {
56
const disposables = new DisposableStore();
57
try {
58
testingServiceCollection.define(IConversationOptions, fetchConversationOptions());
59
const simulationWorkspace = disposables.add(isInExtensionHost ? new SimulationWorkspaceExtHost() : new SimulationWorkspace());
60
simulationWorkspace.setupServices(testingServiceCollection);
61
const accessor = testingServiceCollection.createTestingAccessor();
62
63
const testContext = accessor.get(ISimulationTestRuntime);
64
const log = (message: string, err?: any) => testContext.log(message, err);
65
66
const history: (ChatRequestTurn | ChatResponseTurn)[] = [];
67
for (let i = 0; i < scenario.length; i++) {
68
const testCase = scenario[i];
69
simulationWorkspace.resetFromDeserializedWorkspaceState(testCase.getState?.());
70
await testCase.setupCase?.(accessor, simulationWorkspace);
71
const mockProgressReporter = new SpyChatResponseStream();
72
log(`> Query "${testCase.question}"\n`);
73
74
const parsedQuery = await parseQueryForScenarioTest(accessor, testCase, simulationWorkspace);
75
const participantId = (parsedQuery.participantName && getChatParticipantIdFromName(parsedQuery.participantName)) ?? '';
76
const request: ChatRequest = { prompt: parsedQuery.query, references: parsedQuery.variables, command: parsedQuery.command, location: ChatLocation.Panel, location2: undefined, attempt: 0, enableCommandDetection: false, isParticipantDetected: false, toolReferences: parsedQuery.toolReferences, toolInvocationToken: undefined as never, model: null!, tools: new Map(), id: '1', sessionId: '1', sessionResource: Uri.parse('chat:/1'), hasHooksEnabled: false };
77
if (testCase.tools) {
78
for (const [toolName, shouldUse] of Object.entries(testCase.tools)) {
79
request.tools.set({ name: getContributedToolName(toolName) } as LanguageModelToolInformation, shouldUse);
80
}
81
}
82
const interactiveSession = accessor.get(IInstantiationService).createInstance(
83
ChatParticipantRequestHandler,
84
history,
85
request,
86
mockProgressReporter,
87
CancellationToken.None,
88
{
89
agentId: participantId,
90
agentName: parsedQuery.participantName || '',
91
intentId: (!parsedQuery.participantName && parsedQuery.command) ? parsedQuery.command :
92
parsedQuery.command ? agentsToCommands[parsedQuery.participantName as Intent]![parsedQuery.command] :
93
parsedQuery.participantName,
94
},
95
() => false,
96
undefined,
97
);
98
const result = await interactiveSession.getResult();
99
assert.ok(!result.errorDetails, result.errorDetails?.message);
100
101
history.push(new ChatRequestTurn(request.prompt, request.command, [...request.references], getChatParticipantIdFromName(participantId), []));
102
history.push(new ChatResponseTurn(mockProgressReporter.items.filter(x => x instanceof ChatResponseMarkdownPart || x instanceof ChatResponseAnchorPart), result, participantId, request.command));
103
104
testCase.answer = mockProgressReporter.currentProgress;
105
106
const turn = interactiveSession.conversation.getLatestTurn();
107
const fullResponse = turn?.responseMessage?.message ?? '';
108
109
accessor.get(ISimulationTestRuntime).setOutcome({
110
kind: 'answer',
111
content: fullResponse
112
});
113
114
// Use the evaluator passed to us to evaluate if the response is correct
115
log(`## Response:\n${fullResponse}\n`);
116
log(`## Commands:\n`);
117
const commands = mockProgressReporter.commandButtons;
118
for (const command of commands) {
119
log(`- ${JSON.stringify(command)}\n`);
120
}
121
122
if (scenario[i].applyChatCodeBlocks) {
123
const codeBlocks = turn?.getMetadata(CodeBlocksMetadata)?.codeBlocks ?? [];
124
const testRuntime = accessor.get(ISimulationTestRuntime);
125
126
if (codeBlocks.length !== 0) {
127
const codeMapper = accessor.get(IInstantiationService).createInstance(CodeMapper);
128
const changedDocs: Map<string, { document: TextDocument; originalContent: string; postContent: string }> = new Map();
129
130
// Apply Code Block Changes
131
let codeBlockApplyErrorDetails: ChatErrorDetails | undefined = undefined;
132
for (const codeBlock of codeBlocks) {
133
const prevDocument = simulationWorkspace.activeTextEditor?.document!;
134
// Set the active document if the code resource has a uri
135
if (codeBlock.resource) {
136
simulationWorkspace.setCurrentDocument(codeBlock.resource);
137
}
138
const editor = accessor.get(ITabsAndEditorsService).activeTextEditor!;
139
const codeMap = codeBlock.code;
140
const document = simulationWorkspace.activeTextEditor!.document;
141
const documentContext = IDocumentContext.fromEditor(editor);
142
const workspacePath = simulationWorkspace.getFilePath(document.uri);
143
144
const previousTextContent = document.getText();
145
const response: MappedEditsResponseStream = {
146
textEdit(target, edits) {
147
simulationWorkspace.applyEdits(target, Array.isArray(edits) ? edits : [edits]);
148
},
149
notebookEdit(target, edits) {
150
simulationWorkspace.applyNotebookEdits(target, Array.isArray(edits) ? edits : [edits]);
151
},
152
};
153
const input: ICodeMapperExistingDocument = { createNew: false, codeBlock: codeMap, uri: document.uri, markdownBeforeBlock: undefined, existingDocument: documentContext.document };
154
const result = await codeMapper.mapCode(input, response, undefined, CancellationToken.None);
155
156
if (!result) {
157
codeBlockApplyErrorDetails = {
158
message: `Code block changes failed to apply to ${document.uri.toString()}`,
159
};
160
break;
161
}
162
163
if (result.errorDetails) {
164
result.errorDetails.message = `Code block changes failed to apply to ${document.uri.toString()}:\n${result.errorDetails.message}`;
165
codeBlockApplyErrorDetails = result.errorDetails;
166
break;
167
}
168
169
const postEditTextContent = editor.document.getText();
170
if (previousTextContent !== postEditTextContent) {
171
const previousChange = changedDocs.get(workspacePath);
172
if (previousChange) {
173
previousChange.postContent = postEditTextContent;
174
changedDocs.set(workspacePath, previousChange);
175
} else {
176
changedDocs.set(workspacePath, { document, originalContent: previousTextContent, postContent: postEditTextContent });
177
}
178
}
179
180
if (prevDocument) {
181
simulationWorkspace.setCurrentDocument(prevDocument.uri);
182
}
183
}
184
185
// Log the changed files
186
const changedFilePaths: IWorkspaceStateFile[] = [];
187
if (!codeBlockApplyErrorDetails && changedDocs.size > 0) {
188
const seenDoc = new Set<string>();
189
for (const [workspacePath, changes] of changedDocs.entries()) {
190
if (seenDoc.has(workspacePath)) {
191
continue;
192
}
193
seenDoc.add(workspacePath);
194
195
if (isNotebook(changes.document.uri)) {
196
await testRuntime.writeFile(workspacePath + '.txt', changes.originalContent, INLINE_INITIAL_DOC_TAG); // using .txt instead of real file extension to avoid breaking automation scripts
197
198
changedFilePaths.push({
199
workspacePath,
200
relativeDiskPath: await testRuntime.writeFile(workspacePath, changes.postContent, INLINE_CHANGED_DOC_TAG),
201
languageId: changes.document.languageId
202
});
203
} else {
204
await testRuntime.writeFile(workspacePath + '.txt', changes.originalContent, INLINE_INITIAL_DOC_TAG); // using .txt instead of real file extension to avoid breaking automation scripts
205
206
changedFilePaths.push({
207
workspacePath,
208
relativeDiskPath: await testRuntime.writeFile(workspacePath, changes.postContent, INLINE_CHANGED_DOC_TAG),
209
languageId: changes.document.languageId
210
});
211
}
212
}
213
214
testRuntime.setOutcome({
215
kind: 'edit',
216
files: changedFilePaths.map(f => ({ srcUri: f.workspacePath, post: f.relativeDiskPath }))
217
});
218
} else if (codeBlockApplyErrorDetails) {
219
testRuntime.setOutcome({
220
kind: 'failed',
221
error: codeBlockApplyErrorDetails.message,
222
hitContentFilter: codeBlockApplyErrorDetails.responseIsFiltered ?? false,
223
critical: false
224
});
225
}
226
}
227
}
228
229
const evaluatedResponse = await evaluator(
230
accessor,
231
testCase.question,
232
mockProgressReporter.currentProgress,
233
fullResponse,
234
turn,
235
i,
236
commands,
237
mockProgressReporter.confirmations,
238
mockProgressReporter.fileTrees,
239
);
240
assert.ok(evaluatedResponse.success, evaluatedResponse.errorMessage);
241
}
242
} finally {
243
disposables.dispose();
244
}
245
};
246
}
247
248
export function shouldSkip(scenario: Scenario): boolean {
249
const workspaceFolderPath = scenario[0].getState?.().workspaceFolderPath;
250
try {
251
return !workspaceFolderPath || fs.readdirSync(workspaceFolderPath).length === 0;
252
} catch (e) {
253
return true;
254
}
255
}
256
257