Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/chatSessions/claude/node/test/resolvePromptToContentBlocks.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 Anthropic from '@anthropic-ai/sdk';
7
import { describe, expect, it } from 'vitest';
8
import type * as vscode from 'vscode';
9
import { URI } from '../../../../../util/vs/base/common/uri';
10
import { ChatReferenceBinaryData } from '../../../../../vscodeTypes';
11
import { TestChatRequest } from '../../../../test/node/testHelpers';
12
import { resolvePromptToContentBlocks } from '../claudePromptResolver';
13
14
// #region Test Helpers
15
16
function makeRef(
17
value: vscode.ChatPromptReference['value'],
18
range?: [number, number],
19
): vscode.ChatPromptReference {
20
return { id: 'ref', name: 'ref', value, range } as vscode.ChatPromptReference;
21
}
22
23
function makeLocationRef(
24
uri: URI,
25
startLine: number,
26
range?: [number, number],
27
): vscode.ChatPromptReference {
28
const location = { uri, range: { start: { line: startLine, character: 0 }, end: { line: startLine, character: 0 } } };
29
return { id: 'loc', name: 'loc', value: location, range } as vscode.ChatPromptReference;
30
}
31
32
function textBlocks(blocks: Anthropic.ContentBlockParam[]): Anthropic.TextBlockParam[] {
33
return blocks.filter(b => b.type === 'text') as Anthropic.TextBlockParam[];
34
}
35
36
function imageBlocks(blocks: Anthropic.ContentBlockParam[]): Anthropic.ImageBlockParam[] {
37
return blocks.filter(b => b.type === 'image') as Anthropic.ImageBlockParam[];
38
}
39
40
// #endregion
41
42
describe('resolvePromptToContentBlocks', () => {
43
it('returns plain text for a simple prompt', async () => {
44
const request = new TestChatRequest('Hello world');
45
const blocks = await resolvePromptToContentBlocks(request);
46
47
expect(blocks).toHaveLength(1);
48
expect(textBlocks(blocks)[0].text).toBe('Hello world');
49
});
50
51
it('passes through slash-command prompts unmodified', async () => {
52
const request = new TestChatRequest('/help me with something');
53
const blocks = await resolvePromptToContentBlocks(request);
54
55
expect(blocks).toHaveLength(1);
56
expect(textBlocks(blocks)[0].text).toBe('/help me with something');
57
});
58
59
it('prefixes command name when request.command is set', async () => {
60
const request = new TestChatRequest('fix this bug');
61
request.command = 'fix';
62
const blocks = await resolvePromptToContentBlocks(request);
63
64
expect(blocks).toHaveLength(1);
65
expect(textBlocks(blocks)[0].text).toBe('/fix fix this bug');
66
});
67
68
// #region Inline References (ref.range)
69
70
it('substitutes an inline URI reference at the correct range', async () => {
71
const fileUri = URI.file('/src/app.ts');
72
// Simulate "fix #file:app.ts please" where #file:app.ts occupies indices 4..16
73
const prompt = 'fix #file:app.ts please';
74
const ref = makeRef(fileUri, [4, 16]);
75
const request = new TestChatRequest(prompt, [ref]);
76
77
const blocks = await resolvePromptToContentBlocks(request);
78
79
expect(blocks).toHaveLength(1);
80
expect(textBlocks(blocks)[0].text).toBe(`fix ${fileUri.fsPath} please`);
81
});
82
83
it('substitutes an inline Location reference with line number', async () => {
84
const fileUri = URI.file('/src/utils.ts');
85
// "look at #ref here" — #ref is at [8, 12]
86
const prompt = 'look at #ref here';
87
const ref = makeLocationRef(fileUri, 41, [8, 12]);
88
const request = new TestChatRequest(prompt, [ref]);
89
90
const blocks = await resolvePromptToContentBlocks(request);
91
92
expect(blocks).toHaveLength(1);
93
// Location refs include `:lineNumber` (1-indexed)
94
expect(textBlocks(blocks)[0].text).toBe(`look at ${fileUri.fsPath}:42 here`);
95
});
96
97
it('substitutes multiple inline references correctly', async () => {
98
const uri1 = URI.file('/a.ts');
99
const uri2 = URI.file('/b.ts');
100
// "compare #ref1 and #ref2" — refs at [8, 12] and [17, 21]
101
const prompt = 'compare #rf1 and #rf2';
102
const ref1 = makeRef(uri1, [8, 12]);
103
const ref2 = makeRef(uri2, [17, 21]);
104
const request = new TestChatRequest(prompt, [ref1, ref2]);
105
106
const blocks = await resolvePromptToContentBlocks(request);
107
108
expect(blocks).toHaveLength(1);
109
const text = textBlocks(blocks)[0].text;
110
expect(text).toContain(uri1.fsPath);
111
expect(text).toContain(uri2.fsPath);
112
});
113
114
// #endregion
115
116
// #region Non-Inline References (system-reminder block)
117
118
it('appends non-inline URI references as a system-reminder block', async () => {
119
const fileUri = URI.file('/src/main.ts');
120
const ref = makeRef(fileUri);
121
const request = new TestChatRequest('explain this', [ref]);
122
123
const blocks = await resolvePromptToContentBlocks(request);
124
125
expect(blocks).toHaveLength(2);
126
expect(textBlocks(blocks)[0].text).toBe('explain this');
127
expect(textBlocks(blocks)[1].text).toContain('<system-reminder>');
128
expect(textBlocks(blocks)[1].text).toContain(fileUri.fsPath);
129
});
130
131
it('includes multiple non-inline references in a single system-reminder block', async () => {
132
const uri1 = URI.file('/a.ts');
133
const uri2 = URI.file('/b.ts');
134
const request = new TestChatRequest('check these', [makeRef(uri1), makeRef(uri2)]);
135
136
const blocks = await resolvePromptToContentBlocks(request);
137
138
const reminderBlocks = textBlocks(blocks).filter(b => b.text.includes('<system-reminder>'));
139
expect(reminderBlocks).toHaveLength(1);
140
expect(reminderBlocks[0].text).toContain(uri1.fsPath);
141
expect(reminderBlocks[0].text).toContain(uri2.fsPath);
142
});
143
144
// #endregion
145
146
// #region Image References
147
148
it('converts a PNG binary reference to an image content block', async () => {
149
const imageData = new Uint8Array([0x89, 0x50, 0x4E, 0x47]);
150
const ref = makeRef(new ChatReferenceBinaryData('image/png', () => Promise.resolve(imageData)));
151
const request = new TestChatRequest('describe this', [ref]);
152
153
const blocks = await resolvePromptToContentBlocks(request);
154
155
expect(imageBlocks(blocks)).toHaveLength(1);
156
const img = imageBlocks(blocks)[0];
157
expect(img.source.type).toBe('base64');
158
expect((img.source as Anthropic.Base64ImageSource).media_type).toBe('image/png');
159
expect((img.source as Anthropic.Base64ImageSource).data).toBe(Buffer.from(imageData).toString('base64'));
160
});
161
162
it('normalizes image/jpg to image/jpeg', async () => {
163
const ref = makeRef(new ChatReferenceBinaryData('image/jpg', () => Promise.resolve(new Uint8Array([0xFF, 0xD8]))));
164
const request = new TestChatRequest('check', [ref]);
165
166
const blocks = await resolvePromptToContentBlocks(request);
167
168
const img = imageBlocks(blocks)[0];
169
expect((img.source as Anthropic.Base64ImageSource).media_type).toBe('image/jpeg');
170
});
171
172
it('skips unsupported image MIME types', async () => {
173
const ref = makeRef(new ChatReferenceBinaryData('image/bmp', () => Promise.resolve(new Uint8Array([0x42, 0x4D]))));
174
const request = new TestChatRequest('check', [ref]);
175
176
const blocks = await resolvePromptToContentBlocks(request);
177
178
expect(imageBlocks(blocks)).toHaveLength(0);
179
});
180
181
it('falls back to reference URI when ChatReferenceBinaryData has unsupported MIME but has a reference', async () => {
182
const fileUri = URI.file('/img/photo.bmp');
183
const binaryData = Object.assign(
184
new ChatReferenceBinaryData('image/bmp', () => Promise.resolve(new Uint8Array([]))),
185
{ reference: fileUri },
186
);
187
const ref = makeRef(binaryData);
188
const request = new TestChatRequest('check', [ref]);
189
190
const blocks = await resolvePromptToContentBlocks(request);
191
192
expect(imageBlocks(blocks)).toHaveLength(0);
193
// URI should appear in system-reminder instead
194
expect(textBlocks(blocks).some(b => b.text.includes(fileUri.fsPath))).toBe(true);
195
});
196
197
// #endregion
198
199
// #region Mixed References
200
201
it('handles a mix of inline, non-inline, and image references', async () => {
202
const inlineUri = URI.file('/inline.ts');
203
const extraUri = URI.file('/extra.ts');
204
const imageData = new Uint8Array([0x89]);
205
206
const prompt = 'fix #ref and check';
207
const inlineRef = makeRef(inlineUri, [4, 8]);
208
const extraRef = makeRef(extraUri);
209
const imageRef = makeRef(new ChatReferenceBinaryData('image/png', () => Promise.resolve(imageData)));
210
211
const request = new TestChatRequest(prompt, [inlineRef, extraRef, imageRef]);
212
const blocks = await resolvePromptToContentBlocks(request);
213
214
// Text block with inline substitution
215
expect(textBlocks(blocks)[0].text).toContain(inlineUri.fsPath);
216
expect(textBlocks(blocks)[0].text).toContain('and check');
217
218
// Image block
219
expect(imageBlocks(blocks)).toHaveLength(1);
220
221
// System-reminder block for the non-inline ref
222
const reminderBlocks = textBlocks(blocks).filter(b => b.text.includes('<system-reminder>'));
223
expect(reminderBlocks).toHaveLength(1);
224
expect(reminderBlocks[0].text).toContain(extraUri.fsPath);
225
});
226
227
it('does not add system-reminder block when there are no non-inline references', async () => {
228
const uri = URI.file('/file.ts');
229
const prompt = 'fix #ref please';
230
const ref = makeRef(uri, [4, 8]);
231
const request = new TestChatRequest(prompt, [ref]);
232
233
const blocks = await resolvePromptToContentBlocks(request);
234
235
const reminderBlocks = textBlocks(blocks).filter(b => b.text.includes('<system-reminder>'));
236
expect(reminderBlocks).toHaveLength(0);
237
});
238
239
it('handles empty references array', async () => {
240
const request = new TestChatRequest('just text', []);
241
const blocks = await resolvePromptToContentBlocks(request);
242
243
expect(blocks).toHaveLength(1);
244
expect(textBlocks(blocks)[0].text).toBe('just text');
245
});
246
247
// #endregion
248
});
249
250