Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/prompts/node/panel/test/fileVariable.spec.ts
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 { type JSONTree, OutputMode, Raw } from '@vscode/prompt-tsx';
7
import { beforeAll, describe, expect, test } from 'vitest';
8
import { IEndpointProvider } from '../../../../../platform/endpoint/common/endpointProvider';
9
import { IFileSystemService } from '../../../../../platform/filesystem/common/fileSystemService';
10
import { MockFileSystemService } from '../../../../../platform/filesystem/node/test/mockFileSystemService';
11
import type { IChatEndpoint } from '../../../../../platform/networking/common/networking';
12
import { ITestingServicesAccessor } from '../../../../../platform/test/node/services';
13
import { TestWorkspaceService } from '../../../../../platform/test/node/testWorkspaceService';
14
import { IWorkspaceService } from '../../../../../platform/workspace/common/workspaceService';
15
import { createTextDocumentData } from '../../../../../util/common/test/shims/textDocument';
16
import { ITokenizer, TokenizerType } from '../../../../../util/common/tokenizer';
17
import { Event } from '../../../../../util/vs/base/common/event';
18
import { IInstantiationService } from '../../../../../util/vs/platform/instantiation/common/instantiation';
19
import { Uri } from '../../../../../vscodeTypes';
20
import { createExtensionUnitTestingServices } from '../../../../test/node/services';
21
import { PromptRenderer, renderPromptElementJSON } from '../../base/promptRenderer';
22
import { FileVariable } from '../fileVariable';
23
24
// PromptNodeType enum values from @vscode/prompt-tsx (const enum values are erased at runtime)
25
const PromptNodeType = {
26
Piece: 1,
27
Text: 2,
28
} as const;
29
30
function jsonTreeToString(node: JSONTree.PromptNodeJSON): string {
31
if (node.type === PromptNodeType.Text) {
32
return (node as JSONTree.TextJSON).text;
33
} else if (node.type === PromptNodeType.Piece) {
34
return (node as JSONTree.PieceJSON).children.map(jsonTreeToString).join('');
35
}
36
return '';
37
}
38
39
function hasDocumentContentPart(messages: Raw.ChatMessage[]): boolean {
40
return messages.some(msg =>
41
msg.content.some(part => part.type === Raw.ChatCompletionContentPartKind.Document)
42
);
43
}
44
45
function createMockEndpoint(overrides: { family?: string; supportsVision?: boolean; model?: string } = {}): IChatEndpoint {
46
return {
47
family: overrides.family ?? 'gpt-4.1',
48
model: overrides.model ?? 'gpt-4.1',
49
supportsVision: overrides.supportsVision ?? true,
50
modelMaxPromptTokens: 128000,
51
maxOutputTokens: 4096,
52
name: 'test-model',
53
version: '1.0',
54
modelProvider: 'test',
55
supportsToolCalls: true,
56
supportsPrediction: false,
57
showInModelPicker: false,
58
isFallback: false,
59
tokenizer: TokenizerType.O200K,
60
urlOrRequestMetadata: '',
61
acquireTokenizer: (): ITokenizer => ({
62
mode: OutputMode.Raw,
63
tokenLength: async () => 0,
64
countMessageTokens: async () => 0,
65
countMessagesTokens: async () => 0,
66
countToolTokens: async () => 0,
67
}),
68
} as IChatEndpoint;
69
}
70
71
class MockEndpointProvider implements IEndpointProvider {
72
declare readonly _serviceBrand: undefined;
73
constructor(private readonly endpoint: IChatEndpoint) { }
74
readonly onDidModelsRefresh = Event.None;
75
async getChatEndpoint(): Promise<IChatEndpoint> { return this.endpoint; }
76
async getEmbeddingsEndpoint(): Promise<never> { throw new Error('not implemented'); }
77
async getAllChatEndpoints(): Promise<IChatEndpoint[]> { return [this.endpoint]; }
78
async getAllCompletionModels(): Promise<never[]> { return []; }
79
}
80
81
describe('FileVariable', () => {
82
let accessor: ITestingServicesAccessor;
83
84
beforeAll(() => {
85
const testingServiceCollection = createExtensionUnitTestingServices();
86
accessor = testingServiceCollection.createTestingAccessor();
87
});
88
89
test('does not include unknown untitled file', async () => {
90
const result = await renderPromptElementJSON(
91
accessor.get(IInstantiationService),
92
FileVariable,
93
{
94
variableName: '',
95
variableValue: Uri.parse('untitled:Untitled-1'),
96
});
97
expect(jsonTreeToString(result.node)).toMatchSnapshot();
98
});
99
100
test('does include known untitled file', async () => {
101
const untitledUri = Uri.parse('untitled:Untitled-1');
102
const untitledDoc = createTextDocumentData(untitledUri, 'test!', 'python').document;
103
104
const testingServiceCollection = createExtensionUnitTestingServices();
105
testingServiceCollection.define(IWorkspaceService, new TestWorkspaceService(undefined, [untitledDoc]));
106
107
accessor = testingServiceCollection.createTestingAccessor();
108
109
const result = await renderPromptElementJSON(
110
accessor.get(IInstantiationService),
111
FileVariable,
112
{
113
variableName: '',
114
variableValue: Uri.parse('untitled:Untitled-1'),
115
});
116
expect(jsonTreeToString(result.node)).toMatchSnapshot();
117
});
118
119
test('omits file contents when omitContents is true', async () => {
120
const untitledUri = Uri.parse('untitled:Untitled-1');
121
const untitledDoc = createTextDocumentData(untitledUri, 'file contents that should be omitted', 'python').document;
122
123
const testingServiceCollection = createExtensionUnitTestingServices();
124
testingServiceCollection.define(IWorkspaceService, new TestWorkspaceService(undefined, [untitledDoc]));
125
126
accessor = testingServiceCollection.createTestingAccessor();
127
128
const result = await renderPromptElementJSON(
129
accessor.get(IInstantiationService),
130
FileVariable,
131
{
132
variableName: 'myfile',
133
variableValue: Uri.parse('untitled:Untitled-1'),
134
omitContents: true,
135
});
136
expect(jsonTreeToString(result.node)).toMatchSnapshot();
137
});
138
});
139
140
describe('FileVariable PDF support', () => {
141
142
// Valid PDF magic bytes: %PDF (\x25\x50\x44\x46) followed by version
143
const VALID_PDF_CONTENT = '%PDF-1.4\n1 0 obj\n<</Type /Catalog>>\nendobj';
144
const INVALID_PDF_CONTENT = 'This is not a PDF file at all';
145
146
function createPdfTestServices(options: { family: string; supportsVision: boolean }) {
147
const testingServiceCollection = createExtensionUnitTestingServices();
148
const mockEndpoint = createMockEndpoint({
149
family: options.family,
150
supportsVision: options.supportsVision,
151
model: `${options.family}-test`,
152
});
153
testingServiceCollection.define(IEndpointProvider, new MockEndpointProvider(mockEndpoint));
154
return { testingServiceCollection, mockEndpoint };
155
}
156
157
test('renders PDF document for Anthropic model with vision', async () => {
158
const { testingServiceCollection, mockEndpoint } = createPdfTestServices({ family: 'claude-3.5-sonnet', supportsVision: true });
159
const mockFs = new MockFileSystemService();
160
const pdfUri = Uri.parse('file:///workspace/doc.pdf');
161
mockFs.mockFile(pdfUri, VALID_PDF_CONTENT);
162
testingServiceCollection.define(IFileSystemService, mockFs);
163
164
const accessor = testingServiceCollection.createTestingAccessor();
165
const renderer = PromptRenderer.create(
166
accessor.get(IInstantiationService),
167
mockEndpoint,
168
FileVariable,
169
{
170
variableName: 'doc',
171
variableValue: pdfUri,
172
});
173
const { messages } = await renderer.render();
174
175
// Should contain a Document content part in the rendered messages
176
expect(hasDocumentContentPart(messages)).toBe(true);
177
});
178
179
test('shows omitted reference for non-Anthropic model', async () => {
180
const { testingServiceCollection, mockEndpoint } = createPdfTestServices({ family: 'gpt-4.1', supportsVision: true });
181
const mockFs = new MockFileSystemService();
182
const pdfUri = Uri.parse('file:///workspace/doc.pdf');
183
mockFs.mockFile(pdfUri, VALID_PDF_CONTENT);
184
testingServiceCollection.define(IFileSystemService, mockFs);
185
186
const accessor = testingServiceCollection.createTestingAccessor();
187
const renderer = PromptRenderer.create(
188
accessor.get(IInstantiationService),
189
mockEndpoint,
190
FileVariable,
191
{
192
variableName: 'doc',
193
variableValue: pdfUri,
194
});
195
const { messages } = await renderer.render();
196
197
// Non-Anthropic model should not produce a Document content part
198
expect(hasDocumentContentPart(messages)).toBe(false);
199
});
200
201
test('shows omitted reference for model without vision', async () => {
202
const { testingServiceCollection, mockEndpoint } = createPdfTestServices({ family: 'claude-3.5-sonnet', supportsVision: false });
203
const mockFs = new MockFileSystemService();
204
const pdfUri = Uri.parse('file:///workspace/doc.pdf');
205
mockFs.mockFile(pdfUri, VALID_PDF_CONTENT);
206
testingServiceCollection.define(IFileSystemService, mockFs);
207
208
const accessor = testingServiceCollection.createTestingAccessor();
209
const renderer = PromptRenderer.create(
210
accessor.get(IInstantiationService),
211
mockEndpoint,
212
FileVariable,
213
{
214
variableName: 'doc',
215
variableValue: pdfUri,
216
});
217
const { messages } = await renderer.render();
218
219
// Model without vision should not produce a Document content part
220
expect(hasDocumentContentPart(messages)).toBe(false);
221
});
222
223
test('shows omitted reference for invalid PDF (bad magic bytes)', async () => {
224
const { testingServiceCollection, mockEndpoint } = createPdfTestServices({ family: 'claude-3.5-sonnet', supportsVision: true });
225
const mockFs = new MockFileSystemService();
226
const pdfUri = Uri.parse('file:///workspace/fake.pdf');
227
mockFs.mockFile(pdfUri, INVALID_PDF_CONTENT);
228
testingServiceCollection.define(IFileSystemService, mockFs);
229
230
const accessor = testingServiceCollection.createTestingAccessor();
231
const renderer = PromptRenderer.create(
232
accessor.get(IInstantiationService),
233
mockEndpoint,
234
FileVariable,
235
{
236
variableName: 'fake',
237
variableValue: pdfUri,
238
});
239
const { messages } = await renderer.render();
240
241
// Invalid PDF should not produce a Document content part
242
expect(hasDocumentContentPart(messages)).toBe(false);
243
});
244
245
test('shows omitted reference when file read fails', async () => {
246
const { testingServiceCollection, mockEndpoint } = createPdfTestServices({ family: 'claude-3.5-sonnet', supportsVision: true });
247
const mockFs = new MockFileSystemService();
248
const pdfUri = Uri.parse('file:///workspace/missing.pdf');
249
mockFs.mockError(pdfUri, new Error('ENOENT'));
250
testingServiceCollection.define(IFileSystemService, mockFs);
251
252
const accessor = testingServiceCollection.createTestingAccessor();
253
const renderer = PromptRenderer.create(
254
accessor.get(IInstantiationService),
255
mockEndpoint,
256
FileVariable,
257
{
258
variableName: 'missing',
259
variableValue: pdfUri,
260
});
261
const { messages } = await renderer.render();
262
263
// File read error should not produce a Document content part
264
expect(hasDocumentContentPart(messages)).toBe(false);
265
});
266
267
test('returns empty for unsupported model when omitReferences is true', async () => {
268
const { testingServiceCollection, mockEndpoint } = createPdfTestServices({ family: 'gpt-4.1', supportsVision: true });
269
const mockFs = new MockFileSystemService();
270
const pdfUri = Uri.parse('file:///workspace/doc.pdf');
271
mockFs.mockFile(pdfUri, VALID_PDF_CONTENT);
272
testingServiceCollection.define(IFileSystemService, mockFs);
273
274
const accessor = testingServiceCollection.createTestingAccessor();
275
const renderer = PromptRenderer.create(
276
accessor.get(IInstantiationService),
277
mockEndpoint,
278
FileVariable,
279
{
280
variableName: 'doc',
281
variableValue: pdfUri,
282
omitReferences: true,
283
});
284
const { messages } = await renderer.render();
285
286
// Unsupported model with omitReferences should not produce a Document content part
287
expect(hasDocumentContentPart(messages)).toBe(false);
288
});
289
});
290