Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/intents/node/test/promptOverride.spec.ts
13405 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 { Raw } from '@vscode/prompt-tsx';
7
import { beforeEach, describe, expect, test, vi } from 'vitest';
8
import type { LanguageModelToolInformation } from 'vscode';
9
import { MockFileSystemService } from '../../../../platform/filesystem/node/test/mockFileSystemService';
10
import { TestLogService } from '../../../../platform/testing/common/testLogService';
11
import { URI } from '../../../../util/vs/base/common/uri';
12
import { applyConfiguredPromptOverrides, applyPromptOverrides, applyPromptOverridesFromString, resetPromptOverrideWarnings } from '../promptOverride';
13
14
function makeMessages(...specs: Array<{ role: Raw.ChatRole; content: string }>): Raw.ChatMessage[] {
15
return specs.map(s => ({
16
role: s.role,
17
content: [{ type: Raw.ChatCompletionContentPartKind.Text, text: s.content }],
18
})) as Raw.ChatMessage[];
19
}
20
21
function makeTools(...names: string[]): LanguageModelToolInformation[] {
22
return names.map(name => ({
23
name,
24
description: `Default description for ${name}`,
25
inputSchema: undefined,
26
tags: [],
27
source: undefined,
28
})) as LanguageModelToolInformation[];
29
}
30
31
describe('applyPromptOverrides', () => {
32
let logService: TestLogService;
33
let fileSystemService: MockFileSystemService;
34
35
beforeEach(() => {
36
logService = new TestLogService();
37
fileSystemService = new MockFileSystemService();
38
resetPromptOverrideWarnings();
39
});
40
41
test('returns unchanged and logs warning when file is not found', async () => {
42
const warnSpy = vi.spyOn(logService, 'warn');
43
const fileUri = URI.file('/nonexistent.yaml');
44
45
const messages = makeMessages({ role: Raw.ChatRole.System, content: 'original' });
46
const tools = makeTools('tool_a');
47
48
const result = await applyPromptOverrides(fileUri, messages, tools, fileSystemService, logService);
49
50
expect(result.messages).toEqual(messages);
51
expect(result.tools).toEqual(tools);
52
expect(warnSpy).toHaveBeenCalledOnce();
53
});
54
55
test('returns unchanged and logs warning on invalid YAML', async () => {
56
const warnSpy = vi.spyOn(logService, 'warn');
57
const fileUri = URI.file('/bad.yaml');
58
fileSystemService.mockFile(fileUri, '{{{{not valid yaml');
59
60
const messages = makeMessages({ role: Raw.ChatRole.System, content: 'original' });
61
const result = await applyPromptOverrides(fileUri, messages, makeTools(), fileSystemService, logService);
62
63
expect(result.messages).toEqual(messages);
64
expect(warnSpy).toHaveBeenCalledOnce();
65
});
66
67
test('replaces all system messages with systemPrompt override', async () => {
68
const fileUri = URI.file('/override.yaml');
69
fileSystemService.mockFile(fileUri, 'systemPrompt: "Custom system prompt"');
70
71
const messages = makeMessages(
72
{ role: Raw.ChatRole.System, content: 'System 1' },
73
{ role: Raw.ChatRole.System, content: 'System 2' },
74
{ role: Raw.ChatRole.User, content: 'Hello' },
75
{ role: Raw.ChatRole.Assistant, content: 'Hi' },
76
);
77
78
const result = await applyPromptOverrides(fileUri, messages, makeTools(), fileSystemService, logService);
79
80
expect(result.messages).toHaveLength(3);
81
expect(result.messages[0]).toEqual({
82
role: Raw.ChatRole.System,
83
content: [{ type: Raw.ChatCompletionContentPartKind.Text, text: 'Custom system prompt' }],
84
});
85
expect(result.messages[1]).toEqual({
86
role: Raw.ChatRole.User,
87
content: [{ type: Raw.ChatCompletionContentPartKind.Text, text: 'Hello' }],
88
});
89
expect(result.messages[2]).toEqual({
90
role: Raw.ChatRole.Assistant,
91
content: [{ type: Raw.ChatCompletionContentPartKind.Text, text: 'Hi' }],
92
});
93
});
94
95
test('overrides matching tool descriptions', async () => {
96
const fileUri = URI.file('/override.yaml');
97
fileSystemService.mockFile(fileUri, [
98
'toolDescriptions:',
99
' tool_a:',
100
' description: "Overridden A"',
101
].join('\n'));
102
103
const tools = makeTools('tool_a', 'tool_b');
104
105
const result = await applyPromptOverrides(fileUri, makeMessages(), tools, fileSystemService, logService);
106
107
expect(result.tools[0].description).toBe('Overridden A');
108
expect(result.tools[1].description).toBe('Default description for tool_b');
109
});
110
111
test('applies inline system prompt override', () => {
112
const result = applyPromptOverridesFromString(
113
'systemPrompt: "Inline system prompt"',
114
makeMessages(
115
{ role: Raw.ChatRole.System, content: 'Old system' },
116
{ role: Raw.ChatRole.User, content: 'Hello' },
117
),
118
makeTools('tool_a'),
119
logService,
120
);
121
122
expect(result.messages[0]).toEqual({
123
role: Raw.ChatRole.System,
124
content: [{ type: Raw.ChatCompletionContentPartKind.Text, text: 'Inline system prompt' }],
125
});
126
expect(result.messages[1]).toEqual({
127
role: Raw.ChatRole.User,
128
content: [{ type: Raw.ChatCompletionContentPartKind.Text, text: 'Hello' }],
129
});
130
});
131
132
test('applies inline tool description overrides', () => {
133
const result = applyPromptOverridesFromString(
134
[
135
'toolDescriptions:',
136
' tool_a:',
137
' description: "Inline description"',
138
].join('\n'),
139
makeMessages(),
140
makeTools('tool_a', 'tool_b'),
141
logService,
142
);
143
144
expect(result.tools[0].description).toBe('Inline description');
145
expect(result.tools[1].description).toBe('Default description for tool_b');
146
});
147
148
test('applies both system prompt and tool description overrides', async () => {
149
const fileUri = URI.file('/override.yaml');
150
fileSystemService.mockFile(fileUri, [
151
'systemPrompt: "New system"',
152
'toolDescriptions:',
153
' tool_x:',
154
' description: "New tool_x desc"',
155
].join('\n'));
156
157
const messages = makeMessages(
158
{ role: Raw.ChatRole.System, content: 'Old system' },
159
{ role: Raw.ChatRole.User, content: 'Hello' },
160
);
161
const tools = makeTools('tool_x');
162
163
const result = await applyPromptOverrides(fileUri, messages, tools, fileSystemService, logService);
164
165
expect(result.messages[0]).toEqual({
166
role: Raw.ChatRole.System,
167
content: [{ type: Raw.ChatCompletionContentPartKind.Text, text: 'New system' }],
168
});
169
expect(result.messages[1]).toEqual({
170
role: Raw.ChatRole.User,
171
content: [{ type: Raw.ChatCompletionContentPartKind.Text, text: 'Hello' }],
172
});
173
expect(result.tools[0].description).toBe('New tool_x desc');
174
});
175
176
test('returns unchanged for empty YAML file', async () => {
177
const fileUri = URI.file('/empty.yaml');
178
fileSystemService.mockFile(fileUri, '');
179
180
const messages = makeMessages({ role: Raw.ChatRole.System, content: 'original' });
181
const tools = makeTools('tool_a');
182
183
const result = await applyPromptOverrides(fileUri, messages, tools, fileSystemService, logService);
184
185
expect(result.messages).toEqual(messages);
186
expect(result.tools).toEqual(tools);
187
});
188
189
test('returns unchanged and logs warning on invalid inline YAML', () => {
190
const warnSpy = vi.spyOn(logService, 'warn');
191
const messages = makeMessages({ role: Raw.ChatRole.System, content: 'original' });
192
const tools = makeTools('tool_a');
193
194
const result = applyPromptOverridesFromString('{{{{not valid yaml', messages, tools, logService);
195
196
expect(result.messages).toEqual(messages);
197
expect(result.tools).toEqual(tools);
198
expect(warnSpy).toHaveBeenCalledOnce();
199
});
200
201
test('silently ignores tool names not found in available tools', async () => {
202
const fileUri = URI.file('/override.yaml');
203
fileSystemService.mockFile(fileUri, [
204
'toolDescriptions:',
205
' nonexistent_tool:',
206
' description: "Does not matter"',
207
].join('\n'));
208
209
const tools = makeTools('tool_a');
210
211
const result = await applyPromptOverrides(fileUri, makeMessages(), tools, fileSystemService, logService);
212
213
expect(result.tools[0].description).toBe('Default description for tool_a');
214
});
215
216
test('warns only once per file path, then uses trace for repeated failures', async () => {
217
const warnSpy = vi.spyOn(logService, 'warn');
218
const traceSpy = vi.spyOn(logService, 'trace');
219
const fileUri = URI.file('/missing.yaml');
220
221
const messages = makeMessages({ role: Raw.ChatRole.System, content: 'original' });
222
const tools = makeTools('tool_a');
223
224
// First call should warn
225
await applyPromptOverrides(fileUri, messages, tools, fileSystemService, logService);
226
expect(warnSpy).toHaveBeenCalledOnce();
227
228
// Second call should use trace instead
229
await applyPromptOverrides(fileUri, messages, tools, fileSystemService, logService);
230
expect(warnSpy).toHaveBeenCalledOnce(); // still only one warn
231
expect(traceSpy).toHaveBeenCalled();
232
});
233
234
test('re-warns after a successful read followed by a new failure', async () => {
235
const warnSpy = vi.spyOn(logService, 'warn');
236
const fileUri = URI.file('/flaky.yaml');
237
238
const messages = makeMessages({ role: Raw.ChatRole.System, content: 'original' });
239
const tools = makeTools('tool_a');
240
241
// First call fails — should warn
242
await applyPromptOverrides(fileUri, messages, tools, fileSystemService, logService);
243
expect(warnSpy).toHaveBeenCalledOnce();
244
245
// Now the file exists and succeeds — clears the warned state
246
fileSystemService.mockFile(fileUri, 'systemPrompt: "hello"');
247
await applyPromptOverrides(fileUri, messages, tools, fileSystemService, logService);
248
249
// Remove the file again — should warn again since previous read succeeded
250
fileSystemService.mockError(fileUri, new Error('ENOENT'));
251
await applyPromptOverrides(fileUri, messages, tools, fileSystemService, logService);
252
expect(warnSpy).toHaveBeenCalledTimes(2);
253
});
254
255
test('prefers inline prompt override text over prompt override file', async () => {
256
const fileUri = URI.file('/override.yaml');
257
fileSystemService.mockFile(fileUri, 'systemPrompt: "From file"');
258
const traceSpy = vi.spyOn(logService, 'trace');
259
260
const result = await applyConfiguredPromptOverrides(
261
'systemPrompt: "From inline"',
262
fileUri.fsPath,
263
makeMessages(
264
{ role: Raw.ChatRole.System, content: 'Old system' },
265
{ role: Raw.ChatRole.User, content: 'Hello' },
266
),
267
makeTools('tool_a'),
268
fileSystemService,
269
logService,
270
);
271
272
expect(result.messages[0]).toEqual({
273
role: Raw.ChatRole.System,
274
content: [{ type: Raw.ChatCompletionContentPartKind.Text, text: 'From inline' }],
275
});
276
expect(traceSpy).toHaveBeenCalledWith('[PromptOverride] Both inline prompt override text and prompt override file are configured; using inline prompt override text');
277
});
278
});
279
280