Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/test/node/pseudoStartStopConversationCallback.spec.ts
13399 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 sinon from 'sinon';
8
import { afterEach, beforeEach, suite, test } from 'vitest';
9
import type { ChatToolInvocationStreamData, ChatVulnerability } from 'vscode';
10
import { IResponsePart } from '../../../platform/chat/common/chatMLFetcher';
11
import { IResponseDelta } from '../../../platform/networking/common/fetch';
12
import { createPlatformServices } from '../../../platform/test/node/services';
13
import { ChatResponseStreamImpl } from '../../../util/common/chatResponseStreamImpl';
14
import { SpyChatResponseStream } from '../../../util/common/test/mockChatResponseStream';
15
import { AsyncIterableSource } from '../../../util/vs/base/common/async';
16
import { CancellationToken, CancellationTokenSource } from '../../../util/vs/base/common/cancellation';
17
import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation';
18
import { ChatResponseMarkdownPart, ChatResponseMarkdownWithVulnerabilitiesPart } from '../../../vscodeTypes';
19
import { PseudoStopStartResponseProcessor } from '../../prompt/node/pseudoStartStopConversationCallback';
20
21
22
suite('Post Report Conversation Callback', () => {
23
const postReportFn = (deltas: IResponseDelta[]) => {
24
return ['<processed>', ...deltas.map(d => d.text), '</processed>'];
25
};
26
const annotations = [{ id: 123, details: { type: 'type', description: 'description' } }, { id: 456, details: { type: 'type2', description: 'description2' } }];
27
28
let instaService: IInstantiationService;
29
30
beforeEach(() => {
31
const accessor = createPlatformServices().createTestingAccessor();
32
instaService = accessor.get(IInstantiationService);
33
});
34
35
test('Simple post-report', async () => {
36
const responseSource = new AsyncIterableSource<IResponsePart>();
37
const stream = new SpyChatResponseStream();
38
const testObj = instaService.createInstance(PseudoStopStartResponseProcessor,
39
[{
40
start: 'end',
41
stop: 'start'
42
}],
43
postReportFn);
44
45
responseSource.emitOne({ delta: { text: 'one' } });
46
responseSource.emitOne({ delta: { text: ' start ' } });
47
responseSource.emitOne({ delta: { text: 'two' } });
48
responseSource.emitOne({ delta: { text: ' end' } });
49
responseSource.resolve();
50
51
await testObj.doProcessResponse(responseSource.asyncIterable, stream, CancellationToken.None);
52
53
assert.deepStrictEqual(
54
stream.items.map(p => (p as ChatResponseMarkdownPart).value.value),
55
['one', ' ', '<processed>', ' ', 'two', ' ', '</processed>']);
56
});
57
58
test('Partial stop word with extra text before', async () => {
59
const responseSource = new AsyncIterableSource<IResponsePart>();
60
const stream = new SpyChatResponseStream();
61
const testObj = instaService.createInstance(PseudoStopStartResponseProcessor,
62
[{
63
start: 'end',
64
stop: 'start'
65
}],
66
postReportFn);
67
68
responseSource.emitOne({ delta: { text: 'one sta' } });
69
responseSource.emitOne({ delta: { text: 'rt' } });
70
responseSource.emitOne({ delta: { text: ' two end' } });
71
responseSource.resolve();
72
73
await testObj.doProcessResponse(responseSource.asyncIterable, stream, CancellationToken.None);
74
assert.deepStrictEqual(
75
stream.items.map(p => (p as ChatResponseMarkdownPart).value.value),
76
['one ', '<processed>', ' two ', '</processed>']
77
);
78
});
79
80
test('Partial stop word with extra text after', async () => {
81
const responseSource = new AsyncIterableSource<IResponsePart>();
82
const stream = new SpyChatResponseStream();
83
const testObj = instaService.createInstance(PseudoStopStartResponseProcessor,
84
[{
85
start: 'end',
86
stop: 'start'
87
}],
88
postReportFn);
89
90
responseSource.emitOne({ delta: { text: 'one ', codeVulnAnnotations: annotations } });
91
responseSource.emitOne({ delta: { text: 'sta' } });
92
responseSource.emitOne({ delta: { text: 'rt two' } });
93
responseSource.emitOne({ delta: { text: ' end' } });
94
responseSource.resolve();
95
96
await testObj.doProcessResponse(responseSource.asyncIterable, stream, CancellationToken.None);
97
assert.deepStrictEqual((stream.items[0] as ChatResponseMarkdownWithVulnerabilitiesPart).vulnerabilities, annotations.map(a => ({ title: a.details.type, description: a.details.description } satisfies ChatVulnerability)));
98
99
assert.deepStrictEqual(
100
stream.items.map(p => (p as ChatResponseMarkdownPart).value.value),
101
['one ', '<processed>', ' two', ' ', '</processed>']);
102
});
103
104
test('no second stop word', async () => {
105
const responseSource = new AsyncIterableSource<IResponsePart>();
106
const stream = new SpyChatResponseStream();
107
const testObj = instaService.createInstance(PseudoStopStartResponseProcessor,
108
[{
109
start: 'end',
110
stop: 'start'
111
}],
112
postReportFn,
113
);
114
115
responseSource.emitOne({ delta: { text: 'one' } });
116
responseSource.emitOne({ delta: { text: ' start ' } });
117
responseSource.emitOne({ delta: { text: 'two' } });
118
responseSource.emitOne({ delta: { text: ' ' } });
119
responseSource.resolve();
120
121
await testObj.doProcessResponse(responseSource.asyncIterable, stream, CancellationToken.None);
122
assert.deepStrictEqual(
123
stream.items.map(p => (p as ChatResponseMarkdownPart).value.value),
124
['one', ' ']);
125
});
126
127
test('Text on same line as start', async () => {
128
const responseSource = new AsyncIterableSource<IResponsePart>();
129
const stream = new SpyChatResponseStream();
130
const testObj = instaService.createInstance(PseudoStopStartResponseProcessor,
131
[
132
{
133
start: 'end',
134
stop: 'start'
135
}
136
],
137
postReportFn);
138
139
responseSource.emitOne({ delta: { text: 'this is test text\n\n' } });
140
responseSource.emitOne({ delta: { text: 'eeep start\n\n' } });
141
responseSource.emitOne({ delta: { text: 'test test test test 123456' } });
142
responseSource.emitOne({ delta: { text: 'end\n\nhello' } });
143
responseSource.resolve();
144
145
await testObj.doProcessResponse(responseSource.asyncIterable, stream, CancellationToken.None);
146
assert.deepStrictEqual(
147
stream.items.map(p => (p as ChatResponseMarkdownPart).value.value),
148
['this is test text\n\n', 'eeep ', '<processed>', '\n\n', 'test test test test 123456', '</processed>', '\n\nhello']);
149
});
150
151
152
test('Start word without a stop word', async () => {
153
const responseSource = new AsyncIterableSource<IResponsePart>();
154
155
const stream = new SpyChatResponseStream();
156
const testObj = instaService.createInstance(PseudoStopStartResponseProcessor,
157
[{
158
start: '[RESPONSE END]',
159
stop: '[RESPONSE START]'
160
}],
161
postReportFn);
162
163
164
responseSource.emitOne({ delta: { text: `I'm sorry, but as an AI programming assistant, I'm here to provide assistance with software development topics, specifically related to Visual Studio Code. I'm not equipped to provide a definition of a computer. [RESPONSE END]` } });
165
responseSource.resolve();
166
167
await testObj.doProcessResponse(responseSource.asyncIterable, stream, CancellationToken.None);
168
assert.strictEqual((stream.items[0] as ChatResponseMarkdownPart).value.value, `I'm sorry, but as an AI programming assistant, I'm here to provide assistance with software development topics, specifically related to Visual Studio Code. I'm not equipped to provide a definition of a computer. [RESPONSE END]`);
169
});
170
171
afterEach(() => sinon.restore());
172
});
173
174
suite('Tool stream throttling', () => {
175
let clock: sinon.SinonFakeTimers;
176
let updateCalls: { toolCallId: string; streamData: ChatToolInvocationStreamData }[];
177
let stream: ChatResponseStreamImpl;
178
179
beforeEach(() => {
180
clock = sinon.useFakeTimers({ now: 1000, toFake: ['Date'] });
181
updateCalls = [];
182
stream = new ChatResponseStreamImpl(
183
() => { },
184
() => { },
185
undefined,
186
undefined,
187
(toolCallId, streamData) => updateCalls.push({ toolCallId, streamData }),
188
);
189
});
190
191
afterEach(() => {
192
clock.restore();
193
sinon.restore();
194
});
195
196
test('first update is emitted immediately', async () => {
197
const responseSource = new AsyncIterableSource<IResponsePart>();
198
const processor = new PseudoStopStartResponseProcessor([], undefined);
199
200
responseSource.emitOne({ delta: { text: '', copilotToolCallStreamUpdates: [{ id: 'tool1', name: 'myTool', arguments: '{"a":1}' }] } });
201
responseSource.resolve();
202
203
await processor.doProcessResponse(responseSource.asyncIterable, stream, CancellationToken.None);
204
205
assert.strictEqual(updateCalls.length, 1);
206
assert.strictEqual(updateCalls[0].toolCallId, 'tool1');
207
});
208
209
test('rapid updates within throttle window are throttled', async () => {
210
const responseSource = new AsyncIterableSource<IResponsePart>();
211
const processor = new PseudoStopStartResponseProcessor([], undefined);
212
213
// First update goes through immediately
214
responseSource.emitOne({ delta: { text: '', copilotToolCallStreamUpdates: [{ id: 'tool1', name: 'myTool', arguments: '{"a":1}' }] } });
215
// These arrive within the 100ms throttle window — should be buffered
216
responseSource.emitOne({ delta: { text: '', copilotToolCallStreamUpdates: [{ id: 'tool1', name: 'myTool', arguments: '{"a":2}' }] } });
217
responseSource.emitOne({ delta: { text: '', copilotToolCallStreamUpdates: [{ id: 'tool1', name: 'myTool', arguments: '{"a":3}' }] } });
218
responseSource.resolve();
219
220
await processor.doProcessResponse(responseSource.asyncIterable, stream, CancellationToken.None);
221
222
// 1 immediate + 1 flush of the last buffered update = 2 total
223
assert.strictEqual(updateCalls.length, 2);
224
assert.strictEqual(updateCalls[0].toolCallId, 'tool1');
225
assert.deepStrictEqual(updateCalls[1].streamData.partialInput, { a: 3 });
226
});
227
228
test('update after throttle window elapses is emitted immediately', async () => {
229
const responseSource = new AsyncIterableSource<IResponsePart>();
230
const processor = new PseudoStopStartResponseProcessor([], undefined);
231
232
responseSource.emitOne({ delta: { text: '', copilotToolCallStreamUpdates: [{ id: 'tool1', name: 'myTool', arguments: '{"a":1}' }] } });
233
clock.tick(100);
234
responseSource.emitOne({ delta: { text: '', copilotToolCallStreamUpdates: [{ id: 'tool1', name: 'myTool', arguments: '{"a":2}' }] } });
235
responseSource.resolve();
236
237
await processor.doProcessResponse(responseSource.asyncIterable, stream, CancellationToken.None);
238
239
// Both emitted immediately (no pending flush needed)
240
assert.strictEqual(updateCalls.length, 2);
241
assert.deepStrictEqual(updateCalls[0].streamData.partialInput, { a: 1 });
242
assert.deepStrictEqual(updateCalls[1].streamData.partialInput, { a: 2 });
243
});
244
245
test('different tool IDs are throttled independently', async () => {
246
const responseSource = new AsyncIterableSource<IResponsePart>();
247
const processor = new PseudoStopStartResponseProcessor([], undefined);
248
249
responseSource.emitOne({ delta: { text: '', copilotToolCallStreamUpdates: [{ id: 'tool1', name: 'myTool', arguments: '{"a":1}' }] } });
250
responseSource.emitOne({ delta: { text: '', copilotToolCallStreamUpdates: [{ id: 'tool2', name: 'myTool', arguments: '{"b":1}' }] } });
251
// These are within the throttle window for their respective tools
252
responseSource.emitOne({ delta: { text: '', copilotToolCallStreamUpdates: [{ id: 'tool1', name: 'myTool', arguments: '{"a":2}' }] } });
253
responseSource.emitOne({ delta: { text: '', copilotToolCallStreamUpdates: [{ id: 'tool2', name: 'myTool', arguments: '{"b":2}' }] } });
254
responseSource.resolve();
255
256
await processor.doProcessResponse(responseSource.asyncIterable, stream, CancellationToken.None);
257
258
// 2 immediate (one per tool) + 2 flushed (one per tool) = 4
259
assert.strictEqual(updateCalls.length, 4);
260
});
261
262
test('pending updates are not flushed on cancellation', async () => {
263
const cts = new CancellationTokenSource();
264
const responseSource = new AsyncIterableSource<IResponsePart>();
265
const processor = new PseudoStopStartResponseProcessor([], undefined);
266
267
// Start processing, then emit items so the for-await loop consumes them
268
const promise = processor.doProcessResponse(responseSource.asyncIterable, stream, cts.token);
269
270
responseSource.emitOne({ delta: { text: '', copilotToolCallStreamUpdates: [{ id: 'tool1', name: 'myTool', arguments: '{"a":1}' }] } });
271
await new Promise(r => setTimeout(r, 0));
272
responseSource.emitOne({ delta: { text: '', copilotToolCallStreamUpdates: [{ id: 'tool1', name: 'myTool', arguments: '{"a":2}' }] } });
273
await new Promise(r => setTimeout(r, 0));
274
275
// Cancel after items are processed but before stream ends
276
cts.cancel();
277
responseSource.resolve();
278
279
await promise;
280
281
// Only the first immediate update — buffered update should NOT be flushed
282
assert.strictEqual(updateCalls.length, 1);
283
assert.strictEqual(updateCalls[0].toolCallId, 'tool1');
284
});
285
286
test('retry clears pending throttle state', async () => {
287
const responseSource = new AsyncIterableSource<IResponsePart>();
288
const clearCalls: number[] = [];
289
const clearStream = new ChatResponseStreamImpl(
290
() => { },
291
() => clearCalls.push(1),
292
undefined,
293
undefined,
294
(toolCallId, streamData) => updateCalls.push({ toolCallId, streamData }),
295
);
296
const processor = new PseudoStopStartResponseProcessor([], undefined);
297
298
// Buffer a pending update
299
responseSource.emitOne({ delta: { text: '', copilotToolCallStreamUpdates: [{ id: 'tool1', name: 'myTool', arguments: '{"a":1}' }] } });
300
responseSource.emitOne({ delta: { text: '', copilotToolCallStreamUpdates: [{ id: 'tool1', name: 'myTool', arguments: '{"a":2}' }] } });
301
// Retry clears everything
302
responseSource.emitOne({ delta: { text: '', retryReason: 'network_error' } });
303
// New update after retry should go through immediately
304
clock.tick(100);
305
responseSource.emitOne({ delta: { text: '', copilotToolCallStreamUpdates: [{ id: 'tool1', name: 'myTool', arguments: '{"a":3}' }] } });
306
responseSource.resolve();
307
308
await processor.doProcessResponse(responseSource.asyncIterable, clearStream, CancellationToken.None);
309
310
// 1 immediate before retry + 1 immediate after retry = 2
311
// The buffered {"a":2} should have been cleared by retry, not flushed
312
assert.strictEqual(updateCalls.length, 2);
313
assert.deepStrictEqual(updateCalls[0].streamData.partialInput, { a: 1 });
314
assert.deepStrictEqual(updateCalls[1].streamData.partialInput, { a: 3 });
315
});
316
317
test('updates without name are skipped', async () => {
318
const responseSource = new AsyncIterableSource<IResponsePart>();
319
const processor = new PseudoStopStartResponseProcessor([], undefined);
320
321
responseSource.emitOne({ delta: { text: '', copilotToolCallStreamUpdates: [{ id: 'tool1', name: undefined as any, arguments: '{"a":1}' }] } });
322
responseSource.resolve();
323
324
await processor.doProcessResponse(responseSource.asyncIterable, stream, CancellationToken.None);
325
326
assert.strictEqual(updateCalls.length, 0);
327
});
328
});
329
330