Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/prompts/node/agent/test/agentPrompt.spec.tsx
13406 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 { afterAll, beforeAll, beforeEach, expect, suite, test } from 'vitest';
7
import { IChatMLFetcher } from '../../../../../platform/chat/common/chatMLFetcher';
8
import { ChatLocation } from '../../../../../platform/chat/common/commonTypes';
9
import { StaticChatMLFetcher } from '../../../../../platform/chat/test/common/staticChatMLFetcher';
10
import { CodeGenerationTextInstruction, ConfigKey, IConfigurationService } from '../../../../../platform/configuration/common/configurationService';
11
import { MockEndpoint } from '../../../../../platform/endpoint/test/node/mockEndpoint';
12
import { messageToMarkdown } from '../../../../../platform/log/common/messageStringify';
13
import { IResponseDelta } from '../../../../../platform/networking/common/fetch';
14
import { ITestingServicesAccessor } from '../../../../../platform/test/node/services';
15
import { TestWorkspaceService } from '../../../../../platform/test/node/testWorkspaceService';
16
import { IWorkspaceService } from '../../../../../platform/workspace/common/workspaceService';
17
import { createTextDocumentData } from '../../../../../util/common/test/shims/textDocument';
18
import { URI } from '../../../../../util/vs/base/common/uri';
19
import { SyncDescriptor } from '../../../../../util/vs/platform/instantiation/common/descriptors';
20
import { IInstantiationService } from '../../../../../util/vs/platform/instantiation/common/instantiation';
21
import { ChatRequestEditedFileEventKind, LanguageModelTextPart, LanguageModelToolResult } from '../../../../../vscodeTypes';
22
import { addCacheBreakpoints } from '../../../../intents/node/cacheBreakpoints';
23
import { ChatVariablesCollection } from '../../../../prompt/common/chatVariablesCollection';
24
import { Conversation, ICopilotChatResultIn, Turn, TurnStatus } from '../../../../prompt/common/conversation';
25
import { IBuildPromptContext, IToolCall } from '../../../../prompt/common/intents';
26
import { ToolCallRound } from '../../../../prompt/common/toolCallRound';
27
import { createExtensionUnitTestingServices } from '../../../../test/node/services';
28
import { ToolName } from '../../../../tools/common/toolNames';
29
import { IToolsService } from '../../../../tools/common/toolsService';
30
import { PromptRenderer } from '../../base/promptRenderer';
31
import { AgentPrompt, AgentPromptProps } from '../agentPrompt';
32
import { PromptRegistry } from '../promptRegistry';
33
34
const testFamilies = [
35
'default',
36
'gpt-4.1',
37
'gpt-5',
38
'gpt-5-mini',
39
'gpt-5-codex',
40
'gpt-5.1',
41
'gpt-5.1-codex',
42
'gpt-5.1-codex-mini',
43
'claude-haiku-4.5',
44
'claude-sonnet-4.5',
45
'claude-opus-4.5',
46
'claude-sonnet-4.6',
47
'claude-opus-4.6',
48
'gemini-2.0-flash',
49
'grok-code-fast-1'
50
];
51
52
testFamilies.forEach(family => {
53
suite(`AgentPrompt - ${family}`, () => {
54
let accessor: ITestingServicesAccessor;
55
let chatResponse: (string | IResponseDelta[])[] = [];
56
const fileTsUri = URI.file('/workspace/file.ts');
57
58
function getSnapshotFile(name: string): string {
59
return `./__snapshots__/agentPrompts-${family}/${name}.spec.snap`;
60
}
61
62
let conversation: Conversation;
63
64
beforeAll(() => {
65
const testDoc = createTextDocumentData(fileTsUri, 'line 1\nline 2\n\nline 4\nline 5', 'ts').document;
66
67
const services = createExtensionUnitTestingServices();
68
services.define(IWorkspaceService, new SyncDescriptor(
69
TestWorkspaceService,
70
[
71
[URI.file('/workspace')],
72
[testDoc]
73
]
74
));
75
chatResponse = [];
76
services.define(IChatMLFetcher, new StaticChatMLFetcher(chatResponse));
77
accessor = services.createTestingAccessor();
78
accessor.get(IConfigurationService).setConfig(ConfigKey.CodeGenerationInstructions, [{
79
text: 'This is a test custom instruction file',
80
} satisfies CodeGenerationTextInstruction]);
81
});
82
83
beforeEach(() => {
84
const turn = new Turn('turnId', { type: 'user', message: 'hello' });
85
conversation = new Conversation('sessionId', [turn]);
86
});
87
88
afterAll(() => {
89
accessor.dispose();
90
});
91
92
async function agentPromptToString(accessor: ITestingServicesAccessor, promptContext: IBuildPromptContext, otherProps?: Partial<AgentPromptProps>): Promise<string> {
93
const instaService = accessor.get(IInstantiationService);
94
const endpoint = family === 'default'
95
? instaService.createInstance(MockEndpoint, undefined)
96
: instaService.createInstance(MockEndpoint, family);
97
if (!promptContext.conversation) {
98
promptContext = { ...promptContext, conversation };
99
}
100
101
const customizations = await PromptRegistry.resolveAllCustomizations(instaService, endpoint);
102
const baseProps = {
103
priority: 1,
104
endpoint,
105
location: ChatLocation.Panel,
106
promptContext,
107
...otherProps,
108
customizations
109
};
110
111
const props: AgentPromptProps = baseProps;
112
const renderer = PromptRenderer.create(instaService, endpoint, AgentPrompt, props);
113
114
const r = await renderer.render();
115
addCacheBreakpoints(r.messages);
116
return r.messages
117
.map(m => messageToMarkdown(m))
118
.join('\n\n')
119
.replace(/\\+/g, '/')
120
.replace(/The current date is.*/g, '(Date removed from snapshot)');
121
}
122
123
function createEditFileToolCall(idx: number): IToolCall {
124
return {
125
id: `tooluse_${idx}`,
126
name: ToolName.EditFile,
127
arguments: JSON.stringify({
128
filePath: fileTsUri.fsPath, code: `// existing code...\nconsole.log('hi')`
129
})
130
};
131
}
132
133
function createEditFileToolResult(...idxs: number[]): Record<string, LanguageModelToolResult> {
134
const result: Record<string, LanguageModelToolResult> = {};
135
for (const idx of idxs) {
136
result[`tooluse_${idx}`] = new LanguageModelToolResult([new LanguageModelTextPart('success')]);
137
}
138
return result;
139
}
140
141
test('simple case', async () => {
142
await expect(await agentPromptToString(accessor, {
143
chatVariables: new ChatVariablesCollection(),
144
history: [],
145
query: 'hello',
146
}, undefined)).toMatchFileSnapshot(getSnapshotFile('simple_case'));
147
});
148
149
test('all tools', async () => {
150
const toolsService = accessor.get(IToolsService);
151
await expect(await agentPromptToString(accessor, {
152
chatVariables: new ChatVariablesCollection(),
153
history: [],
154
query: 'hello',
155
tools: {
156
availableTools: toolsService.tools,
157
toolInvocationToken: null as never,
158
toolReferences: [],
159
}
160
}, undefined)).toMatchFileSnapshot(getSnapshotFile('all_tools'));
161
});
162
163
test('all non-edit tools', async () => {
164
const toolsService = accessor.get(IToolsService);
165
const editTools: Set<string> = new Set([ToolName.ApplyPatch, ToolName.EditFile, ToolName.ReplaceString, ToolName.MultiReplaceString]);
166
await expect(await agentPromptToString(accessor, {
167
chatVariables: new ChatVariablesCollection(),
168
history: [],
169
query: 'hello',
170
tools: {
171
availableTools: toolsService.tools.filter(t => !editTools.has(t.name)),
172
toolInvocationToken: null as never,
173
toolReferences: [],
174
}
175
}, undefined)).toMatchFileSnapshot(getSnapshotFile('all_non_edit_tools'));
176
});
177
178
test('one attachment', async () => {
179
await expect(await agentPromptToString(accessor, {
180
chatVariables: new ChatVariablesCollection([{ id: 'vscode.file', name: 'file', value: fileTsUri }]),
181
history: [],
182
query: 'hello',
183
}, undefined)).toMatchFileSnapshot(getSnapshotFile('one_attachment'));
184
});
185
186
const tools: IBuildPromptContext['tools'] = {
187
availableTools: [],
188
toolInvocationToken: null as never,
189
toolReferences: [],
190
};
191
192
test('tool use', async () => {
193
await expect(await agentPromptToString(
194
accessor,
195
{
196
chatVariables: new ChatVariablesCollection([{ id: 'vscode.file', name: 'file', value: fileTsUri }]),
197
history: [],
198
query: 'edit this file',
199
toolCallRounds: [
200
new ToolCallRound('ok', [createEditFileToolCall(1)]),
201
],
202
toolCallResults: createEditFileToolResult(1),
203
tools,
204
}, undefined)).toMatchFileSnapshot(getSnapshotFile('tool_use'));
205
});
206
207
test('cache BPs', async () => {
208
await expect(await agentPromptToString(
209
accessor,
210
{
211
chatVariables: new ChatVariablesCollection([{ id: 'vscode.file', name: 'file', value: fileTsUri }]),
212
history: [],
213
query: 'edit this file',
214
},
215
{
216
enableCacheBreakpoints: true,
217
})).toMatchFileSnapshot(getSnapshotFile('cache_BPs'));
218
});
219
220
test('cache BPs with multi tool call rounds', async () => {
221
let toolIdx = 0;
222
const previousTurn = new Turn('id', { type: 'user', message: 'previous turn' });
223
const previousTurnResult: ICopilotChatResultIn = {
224
metadata: {
225
toolCallRounds: [
226
new ToolCallRound('response', [
227
createEditFileToolCall(toolIdx++),
228
createEditFileToolCall(toolIdx++),
229
], undefined, 'toolCallRoundId1'),
230
new ToolCallRound('response 2', [
231
createEditFileToolCall(toolIdx++),
232
createEditFileToolCall(toolIdx++),
233
], undefined, 'toolCallRoundId1'),
234
],
235
toolCallResults: createEditFileToolResult(0, 1, 2, 3),
236
}
237
};
238
previousTurn.setResponse(TurnStatus.Success, { type: 'user', message: 'response' }, 'responseId', previousTurnResult);
239
240
await expect(await agentPromptToString(
241
accessor,
242
{
243
chatVariables: new ChatVariablesCollection([]),
244
history: [previousTurn],
245
query: 'edit this file',
246
toolCallRounds: [
247
new ToolCallRound('ok', [
248
createEditFileToolCall(toolIdx++),
249
createEditFileToolCall(toolIdx++),
250
]),
251
new ToolCallRound('ok', [
252
createEditFileToolCall(toolIdx++),
253
createEditFileToolCall(toolIdx++),
254
]),
255
],
256
toolCallResults: createEditFileToolResult(4, 5, 6, 7),
257
tools,
258
},
259
{
260
enableCacheBreakpoints: true,
261
})).toMatchFileSnapshot(getSnapshotFile('cache_BPs_multi_round'));
262
});
263
264
test('custom instructions not in system message', async () => {
265
accessor.get(IConfigurationService).setConfig(ConfigKey.CustomInstructionsInSystemMessage, false);
266
await expect(await agentPromptToString(accessor, {
267
chatVariables: new ChatVariablesCollection(),
268
history: [],
269
query: 'hello',
270
modeInstructions: { name: 'Plan', content: 'custom mode instructions' },
271
}, undefined)).toMatchFileSnapshot(getSnapshotFile('custom_instructions_not_in_system_message'));
272
});
273
274
test('omit base agent instructions', async () => {
275
accessor.get(IConfigurationService).setConfig(ConfigKey.Advanced.OmitBaseAgentInstructions, true);
276
await expect(await agentPromptToString(accessor, {
277
chatVariables: new ChatVariablesCollection(),
278
history: [],
279
query: 'hello',
280
}, undefined)).toMatchFileSnapshot(getSnapshotFile('omit_base_agent_instructions'));
281
});
282
283
test('edited file events are grouped by kind', async () => {
284
const otherUri = URI.file('/workspace/other.ts');
285
286
await expect((await agentPromptToString(accessor, {
287
chatVariables: new ChatVariablesCollection(),
288
history: [],
289
query: 'hello',
290
editedFileEvents: [
291
{ eventKind: ChatRequestEditedFileEventKind.Undo, uri: fileTsUri },
292
{ eventKind: ChatRequestEditedFileEventKind.UserModification, uri: otherUri },
293
// duplicate to ensure deduplication within a group
294
{ eventKind: ChatRequestEditedFileEventKind.Undo, uri: fileTsUri },
295
],
296
}, undefined))).toMatchFileSnapshot(getSnapshotFile('edited_file_events_grouped_by_kind'));
297
});
298
});
299
});
300
301