Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/inlineEdits/test/node/nextEditProviderSpeculative.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 { afterEach, assert, beforeEach, describe, expect, it } from 'vitest';
7
import { ConfigKey } from '../../../../platform/configuration/common/configurationService';
8
import { DefaultsOnlyConfigurationService } from '../../../../platform/configuration/common/defaultsOnlyConfigurationService';
9
import { InMemoryConfigurationService } from '../../../../platform/configuration/test/common/inMemoryConfigurationService';
10
import { IGitExtensionService } from '../../../../platform/git/common/gitExtensionService';
11
import { NullGitExtensionService } from '../../../../platform/git/common/nullGitExtensionService';
12
import { DocumentId } from '../../../../platform/inlineEdits/common/dataTypes/documentId';
13
import { SpeculativeRequestsAutoExpandEditWindowLines, SpeculativeRequestsEnablement } from '../../../../platform/inlineEdits/common/dataTypes/xtabPromptOptions';
14
import { InlineEditRequestLogContext } from '../../../../platform/inlineEdits/common/inlineEditLogContext';
15
import { ObservableGit } from '../../../../platform/inlineEdits/common/observableGit';
16
import { MutableObservableWorkspace } from '../../../../platform/inlineEdits/common/observableWorkspace';
17
import { EditStreamingWithTelemetry, IStatelessNextEditProvider, NoNextEditReason, RequestEditWindow, RequestEditWindowWithCursorJump, StatelessNextEditRequest, StatelessNextEditTelemetryBuilder, WithStatelessProviderTelemetry } from '../../../../platform/inlineEdits/common/statelessNextEditProvider';
18
import { NesHistoryContextProvider } from '../../../../platform/inlineEdits/common/workspaceEditTracker/nesHistoryContextProvider';
19
import { NesXtabHistoryTracker } from '../../../../platform/inlineEdits/common/workspaceEditTracker/nesXtabHistoryTracker';
20
import { ILogger, ILogService, LogServiceImpl } from '../../../../platform/log/common/logService';
21
import { IRequestLogger } from '../../../../platform/requestLogger/common/requestLogger';
22
import { NullRequestLogger } from '../../../../platform/requestLogger/node/nullRequestLogger';
23
import { ISnippyService, NullSnippyService } from '../../../../platform/snippy/common/snippyService';
24
import { IExperimentationService, NullExperimentationService } from '../../../../platform/telemetry/common/nullExperimentationService';
25
import { mockNotebookService } from '../../../../platform/test/common/testNotebookService';
26
import { TestWorkspaceService } from '../../../../platform/test/node/testWorkspaceService';
27
import { IWorkspaceService } from '../../../../platform/workspace/common/workspaceService';
28
import { Result } from '../../../../util/common/result';
29
import { DeferredPromise } from '../../../../util/vs/base/common/async';
30
import { CancellationToken } from '../../../../util/vs/base/common/cancellation';
31
import { DisposableStore } from '../../../../util/vs/base/common/lifecycle';
32
import { URI } from '../../../../util/vs/base/common/uri';
33
import { generateUuid } from '../../../../util/vs/base/common/uuid';
34
import { LineReplacement } from '../../../../util/vs/editor/common/core/edits/lineEdit';
35
import { StringEdit } from '../../../../util/vs/editor/common/core/edits/stringEdit';
36
import { LineRange } from '../../../../util/vs/editor/common/core/ranges/lineRange';
37
import { OffsetRange } from '../../../../util/vs/editor/common/core/ranges/offsetRange';
38
import { NESInlineCompletionContext, NextEditProvider } from '../../node/nextEditProvider';
39
import { ILlmNESTelemetry, NextEditProviderTelemetryBuilder, ReusedRequestKind } from '../../node/nextEditProviderTelemetry';
40
41
interface ICallRecord {
42
readonly request: StatelessNextEditRequest;
43
readonly cancellationRequested: DeferredPromise<void>;
44
readonly completed: DeferredPromise<void>;
45
wasCancelled: boolean;
46
}
47
48
type ProviderBehavior =
49
| {
50
kind: 'yieldEditThenNoSuggestions';
51
edit: LineReplacement;
52
}
53
| {
54
kind: 'yieldEditThenWait';
55
edit: LineReplacement;
56
continueSignal: DeferredPromise<void>;
57
}
58
| {
59
kind: 'yieldEditThenWaitThenYieldEditsThenNoSuggestions';
60
firstEdit: LineReplacement;
61
continueSignal: DeferredPromise<void>;
62
remainingEdits: readonly LineReplacement[];
63
}
64
| {
65
kind: 'waitForCancellation';
66
};
67
68
class TestStatelessNextEditProvider implements IStatelessNextEditProvider {
69
public readonly ID = 'TestStatelessNextEditProvider';
70
71
private readonly _behaviors: ProviderBehavior[] = [];
72
public readonly calls: ICallRecord[] = [];
73
private readonly _callDeferreds: DeferredPromise<void>[] = [];
74
75
/**
76
* When set, each `provideNextEdit` call will assign this to `request.requestEditWindow`
77
* (mirroring how the real XtabProvider sets the edit window early in its execution).
78
*/
79
public editWindow: RequestEditWindow | RequestEditWindowWithCursorJump | undefined;
80
81
public enqueueBehavior(behavior: ProviderBehavior): void {
82
this._behaviors.push(behavior);
83
}
84
85
/** Returns a promise that resolves when the Nth call (1-based) arrives. */
86
public waitForCall(callNumber: number): Promise<void> {
87
if (this.calls.length >= callNumber) {
88
return Promise.resolve();
89
}
90
while (this._callDeferreds.length < callNumber) {
91
this._callDeferreds.push(new DeferredPromise<void>());
92
}
93
return this._callDeferreds[callNumber - 1].p;
94
}
95
96
private _resolveCallDeferred(): void {
97
const callIdx = this.calls.length - 1;
98
if (callIdx < this._callDeferreds.length) {
99
this._callDeferreds[callIdx].complete();
100
}
101
}
102
103
public async *provideNextEdit(request: StatelessNextEditRequest, _logger: ILogger, _logContext: InlineEditRequestLogContext, cancellationToken: CancellationToken): EditStreamingWithTelemetry {
104
const behavior = this._behaviors.shift();
105
if (!behavior) {
106
throw new Error('Missing provider behavior');
107
}
108
109
if (this.editWindow) {
110
request.requestEditWindow = this.editWindow;
111
}
112
113
const streamedEditWindow = this.editWindow?.window;
114
const streamedOriginalWindow = this.editWindow instanceof RequestEditWindowWithCursorJump ? this.editWindow.originalWindow : undefined;
115
const telemetryBuilder = new StatelessNextEditTelemetryBuilder(request.headerRequestId);
116
const activeDocId = request.getActiveDocument().id;
117
const cancellationRequested = new DeferredPromise<void>();
118
const completed = new DeferredPromise<void>();
119
const call: ICallRecord = {
120
request,
121
cancellationRequested,
122
completed,
123
wasCancelled: false,
124
};
125
126
this.calls.push(call);
127
this._resolveCallDeferred();
128
const cancellationDisposable = cancellationToken.onCancellationRequested(() => {
129
call.wasCancelled = true;
130
if (!cancellationRequested.isSettled) {
131
cancellationRequested.complete();
132
}
133
});
134
135
try {
136
if (behavior.kind === 'waitForCancellation') {
137
await cancellationRequested.p;
138
const cancelled = new NoNextEditReason.GotCancelled('testCancellation');
139
return new WithStatelessProviderTelemetry(cancelled, telemetryBuilder.build(Result.error(cancelled)));
140
}
141
142
if (behavior.kind === 'yieldEditThenWaitThenYieldEditsThenNoSuggestions') {
143
yield new WithStatelessProviderTelemetry({ edit: behavior.firstEdit, isFromCursorJump: false, targetDocument: activeDocId, window: streamedEditWindow, originalWindow: streamedOriginalWindow }, telemetryBuilder.build(Result.ok(undefined)));
144
await Promise.race([behavior.continueSignal.p, cancellationRequested.p]);
145
if (!call.wasCancelled) {
146
for (const edit of behavior.remainingEdits) {
147
yield new WithStatelessProviderTelemetry({ edit, isFromCursorJump: false, targetDocument: activeDocId, window: streamedEditWindow, originalWindow: streamedOriginalWindow }, telemetryBuilder.build(Result.ok(undefined)));
148
}
149
}
150
const noSuggestions = new NoNextEditReason.NoSuggestions(request.documentBeforeEdits, streamedEditWindow);
151
return new WithStatelessProviderTelemetry(noSuggestions, telemetryBuilder.build(Result.error(noSuggestions)));
152
}
153
154
yield new WithStatelessProviderTelemetry({ edit: behavior.edit, isFromCursorJump: false, targetDocument: activeDocId, window: streamedEditWindow, originalWindow: streamedOriginalWindow }, telemetryBuilder.build(Result.ok(undefined)));
155
156
if (behavior.kind === 'yieldEditThenWait') {
157
await Promise.race([behavior.continueSignal.p, cancellationRequested.p]);
158
}
159
160
const noSuggestions = new NoNextEditReason.NoSuggestions(request.documentBeforeEdits, streamedEditWindow);
161
return new WithStatelessProviderTelemetry(noSuggestions, telemetryBuilder.build(Result.error(noSuggestions)));
162
} finally {
163
cancellationDisposable.dispose();
164
if (!completed.isSettled) {
165
completed.complete();
166
}
167
}
168
}
169
}
170
171
function createInlineContext(): NESInlineCompletionContext {
172
return {
173
triggerKind: 1,
174
selectedCompletionInfo: undefined,
175
requestUuid: generateUuid(),
176
requestIssuedDateTime: Date.now(),
177
earliestShownDateTime: Date.now(),
178
enforceCacheDelay: false,
179
};
180
}
181
182
async function flushMicrotasks(ticks = 20): Promise<void> {
183
for (let i = 0; i < ticks; i++) {
184
await Promise.resolve();
185
}
186
}
187
188
function lineReplacement(lineNumberOneBased: number, newLine: string): LineReplacement {
189
return new LineReplacement(new LineRange(lineNumberOneBased, lineNumberOneBased + 1), [newLine]);
190
}
191
192
describe('NextEditProvider speculative requests', () => {
193
let disposables: DisposableStore;
194
let configService: InMemoryConfigurationService;
195
let snippyService: ISnippyService;
196
let gitExtensionService: IGitExtensionService;
197
let logService: ILogService;
198
let expService: IExperimentationService;
199
let workspaceService: IWorkspaceService;
200
let requestLogger: IRequestLogger;
201
202
beforeEach(() => {
203
disposables = new DisposableStore();
204
workspaceService = disposables.add(new TestWorkspaceService());
205
configService = new InMemoryConfigurationService(new DefaultsOnlyConfigurationService());
206
snippyService = new NullSnippyService();
207
gitExtensionService = new NullGitExtensionService();
208
logService = new LogServiceImpl([]);
209
expService = new NullExperimentationService();
210
requestLogger = new NullRequestLogger();
211
});
212
213
afterEach(() => {
214
disposables.dispose();
215
});
216
217
function createProviderAndWorkspace(statelessProvider: IStatelessNextEditProvider): { nextEditProvider: NextEditProvider; workspace: MutableObservableWorkspace } {
218
const workspace = new MutableObservableWorkspace();
219
const git = new ObservableGit(gitExtensionService);
220
const nextEditProvider = new NextEditProvider(
221
workspace,
222
statelessProvider,
223
new NesHistoryContextProvider(workspace, git),
224
new NesXtabHistoryTracker(workspace, undefined, configService, expService),
225
undefined,
226
configService,
227
snippyService,
228
logService,
229
expService,
230
requestLogger,
231
);
232
return { nextEditProvider, workspace };
233
}
234
235
async function getNextEdit(nextEditProvider: NextEditProvider, docId: DocumentId) {
236
const context = createInlineContext();
237
const logContext = new InlineEditRequestLogContext(docId.toString(), 1, context);
238
const telemetryBuilder = new NextEditProviderTelemetryBuilder(gitExtensionService, mockNotebookService, workspaceService, nextEditProvider.ID, undefined);
239
try {
240
return await nextEditProvider.getNextEdit(docId, context, logContext, CancellationToken.None, telemetryBuilder.nesBuilder);
241
} finally {
242
telemetryBuilder.dispose();
243
}
244
}
245
246
async function getNextEditWithTelemetry(nextEditProvider: NextEditProvider, docId: DocumentId): Promise<{ suggestion: Awaited<ReturnType<typeof getNextEdit>>; telemetry: ILlmNESTelemetry }> {
247
const context = createInlineContext();
248
const logContext = new InlineEditRequestLogContext(docId.toString(), 1, context);
249
const telemetryBuilder = new NextEditProviderTelemetryBuilder(gitExtensionService, mockNotebookService, workspaceService, nextEditProvider.ID, undefined);
250
try {
251
const suggestion = await nextEditProvider.getNextEdit(docId, context, logContext, CancellationToken.None, telemetryBuilder.nesBuilder);
252
const telemetry = telemetryBuilder.nesBuilder.build(false);
253
return { suggestion, telemetry };
254
} finally {
255
telemetryBuilder.dispose();
256
}
257
}
258
259
it('does not trigger speculative request when feature is off', async () => {
260
await configService.setConfig(ConfigKey.TeamInternal.InlineEditsSpeculativeRequests, SpeculativeRequestsEnablement.Off);
261
262
const statelessProvider = new TestStatelessNextEditProvider();
263
statelessProvider.enqueueBehavior({ kind: 'yieldEditThenNoSuggestions', edit: lineReplacement(1, 'const value = 2;') });
264
const { nextEditProvider, workspace } = createProviderAndWorkspace(statelessProvider);
265
266
const doc = workspace.addDocument({
267
id: DocumentId.create(URI.file('/test/spec-off.ts').toString()),
268
initialValue: 'const value = 1;\nconsole.log(value);',
269
});
270
doc.setSelection([new OffsetRange(0, 0)], undefined);
271
272
const suggestion = await getNextEdit(nextEditProvider, doc.id);
273
assert(suggestion.result?.edit);
274
275
nextEditProvider.handleShown(suggestion);
276
await flushMicrotasks();
277
278
expect(statelessProvider.calls.length).toBe(1);
279
});
280
281
it('triggers speculative request when feature is on', async () => {
282
await configService.setConfig(ConfigKey.TeamInternal.InlineEditsSpeculativeRequests, SpeculativeRequestsEnablement.On);
283
284
const statelessProvider = new TestStatelessNextEditProvider();
285
statelessProvider.enqueueBehavior({ kind: 'yieldEditThenNoSuggestions', edit: lineReplacement(1, 'const value = 2;') });
286
statelessProvider.enqueueBehavior({ kind: 'waitForCancellation' });
287
const { nextEditProvider, workspace } = createProviderAndWorkspace(statelessProvider);
288
289
const doc = workspace.addDocument({
290
id: DocumentId.create(URI.file('/test/spec-on.ts').toString()),
291
initialValue: 'const value = 1;\nconsole.log(value);',
292
});
293
doc.setSelection([new OffsetRange(0, 0)], undefined);
294
295
const suggestion = await getNextEdit(nextEditProvider, doc.id);
296
assert(suggestion.result?.edit);
297
298
nextEditProvider.handleShown(suggestion);
299
await statelessProvider.waitForCall(2);
300
301
expect(statelessProvider.calls.length).toBe(2);
302
nextEditProvider.handleRejection(doc.id, suggestion);
303
await statelessProvider.calls[1].completed.p;
304
});
305
306
it('reuses speculative request after acceptance without creating a third request', async () => {
307
await configService.setConfig(ConfigKey.TeamInternal.InlineEditsSpeculativeRequests, SpeculativeRequestsEnablement.On);
308
309
const statelessProvider = new TestStatelessNextEditProvider();
310
statelessProvider.enqueueBehavior({ kind: 'yieldEditThenNoSuggestions', edit: lineReplacement(1, 'const value = 2;') });
311
statelessProvider.enqueueBehavior({ kind: 'yieldEditThenNoSuggestions', edit: lineReplacement(2, 'console.log(value + 1);') });
312
const { nextEditProvider, workspace } = createProviderAndWorkspace(statelessProvider);
313
314
const doc = workspace.addDocument({
315
id: DocumentId.create(URI.file('/test/spec-reuse.ts').toString()),
316
initialValue: 'const value = 1;\nconsole.log(value);',
317
});
318
doc.setSelection([new OffsetRange(0, 0)], undefined);
319
320
const firstSuggestion = await getNextEdit(nextEditProvider, doc.id);
321
assert(firstSuggestion.result?.edit);
322
nextEditProvider.handleShown(firstSuggestion);
323
await statelessProvider.waitForCall(2);
324
await statelessProvider.calls[1].completed.p;
325
326
nextEditProvider.handleAcceptance(doc.id, firstSuggestion);
327
doc.applyEdit(firstSuggestion.result.edit.toEdit());
328
329
const secondSuggestion = await getNextEdit(nextEditProvider, doc.id);
330
assert(secondSuggestion.result?.edit);
331
332
expect(statelessProvider.calls.length).toBe(2);
333
expect(secondSuggestion.result.edit.newText).toBe('console.log(value + 1);');
334
});
335
336
it('cancels speculative request on rejection', async () => {
337
await configService.setConfig(ConfigKey.TeamInternal.InlineEditsSpeculativeRequests, SpeculativeRequestsEnablement.On);
338
339
const statelessProvider = new TestStatelessNextEditProvider();
340
statelessProvider.enqueueBehavior({ kind: 'yieldEditThenNoSuggestions', edit: lineReplacement(1, 'const value = 2;') });
341
statelessProvider.enqueueBehavior({ kind: 'waitForCancellation' });
342
const { nextEditProvider, workspace } = createProviderAndWorkspace(statelessProvider);
343
344
const doc = workspace.addDocument({
345
id: DocumentId.create(URI.file('/test/spec-reject.ts').toString()),
346
initialValue: 'const value = 1;\nconsole.log(value);',
347
});
348
doc.setSelection([new OffsetRange(0, 0)], undefined);
349
350
const suggestion = await getNextEdit(nextEditProvider, doc.id);
351
assert(suggestion.result?.edit);
352
nextEditProvider.handleShown(suggestion);
353
await statelessProvider.waitForCall(2);
354
355
nextEditProvider.handleRejection(doc.id, suggestion);
356
await statelessProvider.calls[1].cancellationRequested.p;
357
358
expect(statelessProvider.calls[1].wasCancelled).toBe(true);
359
});
360
361
it('cancels speculative request on ignored when suggestion was shown and not superseded', async () => {
362
await configService.setConfig(ConfigKey.TeamInternal.InlineEditsSpeculativeRequests, SpeculativeRequestsEnablement.On);
363
364
const statelessProvider = new TestStatelessNextEditProvider();
365
statelessProvider.enqueueBehavior({ kind: 'yieldEditThenNoSuggestions', edit: lineReplacement(1, 'const value = 2;') });
366
statelessProvider.enqueueBehavior({ kind: 'waitForCancellation' });
367
const { nextEditProvider, workspace } = createProviderAndWorkspace(statelessProvider);
368
369
const doc = workspace.addDocument({
370
id: DocumentId.create(URI.file('/test/spec-ignored.ts').toString()),
371
initialValue: 'const value = 1;\nconsole.log(value);',
372
});
373
doc.setSelection([new OffsetRange(0, 0)], undefined);
374
375
const suggestion = await getNextEdit(nextEditProvider, doc.id);
376
assert(suggestion.result?.edit);
377
nextEditProvider.handleShown(suggestion);
378
await statelessProvider.waitForCall(2);
379
380
nextEditProvider.handleIgnored(doc.id, suggestion, undefined);
381
await statelessProvider.calls[1].cancellationRequested.p;
382
383
expect(statelessProvider.calls[1].wasCancelled).toBe(true);
384
});
385
386
it('does not cancel speculative request on unrelated open-document changes', async () => {
387
await configService.setConfig(ConfigKey.TeamInternal.InlineEditsSpeculativeRequests, SpeculativeRequestsEnablement.On);
388
389
const statelessProvider = new TestStatelessNextEditProvider();
390
statelessProvider.enqueueBehavior({ kind: 'yieldEditThenNoSuggestions', edit: lineReplacement(1, 'const value = 2;') });
391
statelessProvider.enqueueBehavior({ kind: 'waitForCancellation' });
392
const { nextEditProvider, workspace } = createProviderAndWorkspace(statelessProvider);
393
394
const activeDoc = workspace.addDocument({
395
id: DocumentId.create(URI.file('/test/spec-active.ts').toString()),
396
initialValue: 'const value = 1;\nconsole.log(value);',
397
});
398
activeDoc.setSelection([new OffsetRange(0, 0)], undefined);
399
400
const unrelatedDoc = workspace.addDocument({
401
id: DocumentId.create(URI.file('/test/spec-other.ts').toString()),
402
initialValue: 'export const other = 1;',
403
});
404
unrelatedDoc.setSelection([new OffsetRange(0, 0)], undefined);
405
406
const suggestion = await getNextEdit(nextEditProvider, activeDoc.id);
407
assert(suggestion.result?.edit);
408
nextEditProvider.handleShown(suggestion);
409
await statelessProvider.waitForCall(2);
410
411
unrelatedDoc.applyEdit(StringEdit.insert(0, '// unrelated change\n'));
412
await flushMicrotasks();
413
414
expect(statelessProvider.calls[1].wasCancelled).toBe(false);
415
416
nextEditProvider.handleRejection(activeDoc.id, suggestion);
417
await statelessProvider.calls[1].completed.p;
418
});
419
420
it.skip('cancels speculative request when active document edit moves off the type-through trajectory', async () => {
421
await configService.setConfig(ConfigKey.TeamInternal.InlineEditsSpeculativeRequests, SpeculativeRequestsEnablement.On);
422
423
const statelessProvider = new TestStatelessNextEditProvider();
424
statelessProvider.enqueueBehavior({ kind: 'yieldEditThenNoSuggestions', edit: lineReplacement(1, 'const value = 2;') });
425
statelessProvider.enqueueBehavior({ kind: 'waitForCancellation' });
426
const { nextEditProvider, workspace } = createProviderAndWorkspace(statelessProvider);
427
428
const doc = workspace.addDocument({
429
id: DocumentId.create(URI.file('/test/spec-diverge.ts').toString()),
430
initialValue: 'const value = 1;\nconsole.log(value);',
431
});
432
doc.setSelection([new OffsetRange(0, 0)], undefined);
433
434
const suggestion = await getNextEdit(nextEditProvider, doc.id);
435
assert(suggestion.result?.edit);
436
nextEditProvider.handleShown(suggestion);
437
await statelessProvider.waitForCall(2);
438
439
// Inserting at the start of the document breaks the trajectory's prefix
440
// (the doc no longer starts with `pre[0..editStart]`). The speculative
441
// can no longer be reached via type-through-then-accept — cancel.
442
doc.applyEdit(StringEdit.insert(0, '/* diverged */\n'));
443
await statelessProvider.calls[1].cancellationRequested.p;
444
445
expect(statelessProvider.calls[1].wasCancelled).toBe(true);
446
});
447
448
it.skip('keeps speculative alive while user types characters of the suggestion (type-through)', async () => {
449
await configService.setConfig(ConfigKey.TeamInternal.InlineEditsSpeculativeRequests, SpeculativeRequestsEnablement.On);
450
451
const statelessProvider = new TestStatelessNextEditProvider();
452
// Suggestion inserts `'barbaz'` between `'foo'` and `'();'`.
453
// Resulting precise edit: replace [3, 3) with 'barbaz' (a pure insertion).
454
statelessProvider.enqueueBehavior({ kind: 'yieldEditThenNoSuggestions', edit: lineReplacement(1, 'foobarbaz();') });
455
statelessProvider.enqueueBehavior({ kind: 'waitForCancellation' });
456
const { nextEditProvider, workspace } = createProviderAndWorkspace(statelessProvider);
457
458
const doc = workspace.addDocument({
459
id: DocumentId.create(URI.file('/test/spec-typing.ts').toString()),
460
initialValue: 'foo();\nconsole.log();',
461
});
462
doc.setSelection([new OffsetRange(0, 0)], undefined);
463
464
const suggestion = await getNextEdit(nextEditProvider, doc.id);
465
assert(suggestion.result?.edit);
466
nextEditProvider.handleShown(suggestion);
467
await statelessProvider.waitForCall(2);
468
469
// User types characters of the suggestion at the edit position — each
470
// keystroke keeps the document on a type-through trajectory toward
471
// `postEditContent`, so the speculative must NOT be cancelled.
472
doc.applyEdit(StringEdit.insert(3, 'b'));
473
await flushMicrotasks();
474
expect(statelessProvider.calls[1].wasCancelled).toBe(false);
475
476
doc.applyEdit(StringEdit.insert(4, 'a'));
477
await flushMicrotasks();
478
expect(statelessProvider.calls[1].wasCancelled).toBe(false);
479
480
doc.applyEdit(StringEdit.insert(5, 'r'));
481
await flushMicrotasks();
482
expect(statelessProvider.calls[1].wasCancelled).toBe(false);
483
484
// Now the user types a character that doesn't match the suggestion's
485
// next character (`'b'` would be expected; they typed `'X'`). The
486
// trajectory is broken — cancel.
487
doc.applyEdit(StringEdit.insert(6, 'X'));
488
await statelessProvider.calls[1].cancellationRequested.p;
489
490
expect(statelessProvider.calls[1].wasCancelled).toBe(true);
491
});
492
493
it('cancels mismatched speculative request when starting a request for another document', async () => {
494
await configService.setConfig(ConfigKey.TeamInternal.InlineEditsSpeculativeRequests, SpeculativeRequestsEnablement.On);
495
496
const statelessProvider = new TestStatelessNextEditProvider();
497
statelessProvider.enqueueBehavior({ kind: 'yieldEditThenNoSuggestions', edit: lineReplacement(1, 'const value = 2;') });
498
statelessProvider.enqueueBehavior({ kind: 'waitForCancellation' });
499
statelessProvider.enqueueBehavior({ kind: 'yieldEditThenNoSuggestions', edit: lineReplacement(1, 'export const second = 2;') });
500
const { nextEditProvider, workspace } = createProviderAndWorkspace(statelessProvider);
501
502
const doc1 = workspace.addDocument({
503
id: DocumentId.create(URI.file('/test/spec-cross-doc-1.ts').toString()),
504
initialValue: 'const value = 1;\nconsole.log(value);',
505
});
506
doc1.setSelection([new OffsetRange(0, 0)], undefined);
507
508
const doc2 = workspace.addDocument({
509
id: DocumentId.create(URI.file('/test/spec-cross-doc-2.ts').toString()),
510
initialValue: 'export const second = 1;\nconsole.log(second);',
511
});
512
doc2.setSelection([new OffsetRange(0, 0)], undefined);
513
514
const suggestion = await getNextEdit(nextEditProvider, doc1.id);
515
assert(suggestion.result?.edit);
516
nextEditProvider.handleShown(suggestion);
517
await statelessProvider.waitForCall(2);
518
519
const secondDocSuggestion = await getNextEdit(nextEditProvider, doc2.id);
520
assert(secondDocSuggestion.result?.edit);
521
await statelessProvider.calls[1].cancellationRequested.p;
522
523
expect(statelessProvider.calls[1].wasCancelled).toBe(true);
524
expect(statelessProvider.calls.length).toBe(3);
525
});
526
527
describe('telemetry', () => {
528
it('fresh request has normal headerRequestId and no reusedRequest', async () => {
529
const statelessProvider = new TestStatelessNextEditProvider();
530
statelessProvider.enqueueBehavior({ kind: 'yieldEditThenNoSuggestions', edit: lineReplacement(1, 'const value = 2;') });
531
const { nextEditProvider, workspace } = createProviderAndWorkspace(statelessProvider);
532
533
const doc = workspace.addDocument({
534
id: DocumentId.create(URI.file('/test/telemetry-fresh.ts').toString()),
535
initialValue: 'const value = 1;\nconsole.log(value);',
536
});
537
doc.setSelection([new OffsetRange(0, 0)], undefined);
538
539
const { suggestion, telemetry } = await getNextEditWithTelemetry(nextEditProvider, doc.id);
540
assert(suggestion.result?.edit);
541
542
expect(telemetry.headerRequestId).toBeDefined();
543
expect(telemetry.headerRequestId!.startsWith('sp-')).toBe(false);
544
expect(telemetry.isFromCache).toBe(false);
545
expect(telemetry.reusedRequest).toBeUndefined();
546
});
547
548
it('reused speculative request has sp- headerRequestId and reusedRequest=speculative', async () => {
549
await configService.setConfig(ConfigKey.TeamInternal.InlineEditsSpeculativeRequests, SpeculativeRequestsEnablement.On);
550
551
const statelessProvider = new TestStatelessNextEditProvider();
552
statelessProvider.enqueueBehavior({ kind: 'yieldEditThenNoSuggestions', edit: lineReplacement(1, 'const value = 2;') });
553
// The speculative request yields an edit but stays in-flight until we signal it,
554
// so the second getNextEdit joins the pending speculative request rather than hitting cache.
555
const specContinue = new DeferredPromise<void>();
556
statelessProvider.enqueueBehavior({ kind: 'yieldEditThenWait', edit: lineReplacement(2, 'console.log(value + 1);'), continueSignal: specContinue });
557
const { nextEditProvider, workspace } = createProviderAndWorkspace(statelessProvider);
558
559
const doc = workspace.addDocument({
560
id: DocumentId.create(URI.file('/test/telemetry-spec-reuse.ts').toString()),
561
initialValue: 'const value = 1;\nconsole.log(value);',
562
});
563
doc.setSelection([new OffsetRange(0, 0)], undefined);
564
565
// First request: fresh
566
const firstSuggestion = await getNextEdit(nextEditProvider, doc.id);
567
assert(firstSuggestion.result?.edit);
568
nextEditProvider.handleShown(firstSuggestion);
569
await statelessProvider.waitForCall(2);
570
// Speculative request is now in-flight (yielded edit but waiting on continueSignal)
571
572
// Accept and apply the edit
573
nextEditProvider.handleAcceptance(doc.id, firstSuggestion);
574
doc.applyEdit(firstSuggestion.result.edit.toEdit());
575
576
// Second request: should join the still-in-flight speculative request
577
const { suggestion: secondSuggestion, telemetry } = await getNextEditWithTelemetry(nextEditProvider, doc.id);
578
assert(secondSuggestion.result?.edit);
579
580
expect(telemetry.headerRequestId).toBeDefined();
581
expect(telemetry.headerRequestId!.startsWith('sp-')).toBe(true);
582
expect(telemetry.isFromCache).toBe(false);
583
expect(telemetry.reusedRequest).toBe(ReusedRequestKind.Speculative);
584
585
// Clean up: let the speculative request finish
586
specContinue.complete();
587
await statelessProvider.calls[1].completed.p;
588
});
589
590
it('skips cache delay for edits from speculative requests even when enforceCacheDelay is true', async () => {
591
const CACHE_DELAY_MS = 5_000;
592
await configService.setConfig(ConfigKey.TeamInternal.InlineEditsSpeculativeRequests, SpeculativeRequestsEnablement.On);
593
await configService.setConfig(ConfigKey.TeamInternal.InlineEditsCacheDelay, CACHE_DELAY_MS);
594
await configService.setConfig(ConfigKey.TeamInternal.InlineEditsSpeculativeRequestDelay, 0);
595
596
const statelessProvider = new TestStatelessNextEditProvider();
597
statelessProvider.enqueueBehavior({ kind: 'yieldEditThenNoSuggestions', edit: lineReplacement(1, 'const value = 2;') });
598
const specContinue = new DeferredPromise<void>();
599
statelessProvider.enqueueBehavior({ kind: 'yieldEditThenWait', edit: lineReplacement(2, 'console.log(value + 1);'), continueSignal: specContinue });
600
const { nextEditProvider, workspace } = createProviderAndWorkspace(statelessProvider);
601
602
const doc = workspace.addDocument({
603
id: DocumentId.create(URI.file('/test/spec-skip-delay.ts').toString()),
604
initialValue: 'const value = 1;\nconsole.log(value);',
605
});
606
doc.setSelection([new OffsetRange(0, 0)], undefined);
607
608
// First request (fresh, no cache delay since enforceCacheDelay=false)
609
const firstSuggestion = await getNextEdit(nextEditProvider, doc.id);
610
assert(firstSuggestion.result?.edit);
611
nextEditProvider.handleShown(firstSuggestion);
612
await statelessProvider.waitForCall(2);
613
614
// Accept and apply the suggestion — doc now matches speculative request's postEditContent
615
nextEditProvider.handleAcceptance(doc.id, firstSuggestion);
616
doc.applyEdit(firstSuggestion.result.edit.toEdit());
617
618
// Second request with enforceCacheDelay=true — should still return fast because the result
619
// comes from a speculative request, which uses speculativeRequestDelay (0) instead of cacheDelay (5000)
620
const context: NESInlineCompletionContext = {
621
triggerKind: 1,
622
selectedCompletionInfo: undefined,
623
requestUuid: generateUuid(),
624
requestIssuedDateTime: Date.now(),
625
earliestShownDateTime: Date.now(),
626
enforceCacheDelay: true,
627
};
628
const logContext = new InlineEditRequestLogContext(doc.id.toString(), 1, context);
629
const telemetryBuilder = new NextEditProviderTelemetryBuilder(gitExtensionService, mockNotebookService, workspaceService, nextEditProvider.ID, undefined);
630
const start = Date.now();
631
try {
632
const secondSuggestion = await nextEditProvider.getNextEdit(doc.id, context, logContext, CancellationToken.None, telemetryBuilder.nesBuilder);
633
const elapsed = Date.now() - start;
634
assert(secondSuggestion.result?.edit);
635
expect(elapsed).toBeLessThan(100);
636
} finally {
637
telemetryBuilder.dispose();
638
specContinue.complete();
639
await statelessProvider.calls[1].completed.p;
640
}
641
});
642
643
it('cached speculative result has sp- headerRequestId and isFromCache=true', async () => {
644
await configService.setConfig(ConfigKey.TeamInternal.InlineEditsSpeculativeRequests, SpeculativeRequestsEnablement.On);
645
646
const statelessProvider = new TestStatelessNextEditProvider();
647
statelessProvider.enqueueBehavior({ kind: 'yieldEditThenNoSuggestions', edit: lineReplacement(1, 'const value = 2;') });
648
statelessProvider.enqueueBehavior({ kind: 'yieldEditThenNoSuggestions', edit: lineReplacement(2, 'console.log(value + 1);') });
649
const { nextEditProvider, workspace } = createProviderAndWorkspace(statelessProvider);
650
651
const doc = workspace.addDocument({
652
id: DocumentId.create(URI.file('/test/telemetry-spec-cache.ts').toString()),
653
initialValue: 'const value = 1;\nconsole.log(value);',
654
});
655
doc.setSelection([new OffsetRange(0, 0)], undefined);
656
657
// First request: fresh
658
const firstSuggestion = await getNextEdit(nextEditProvider, doc.id);
659
assert(firstSuggestion.result?.edit);
660
nextEditProvider.handleShown(firstSuggestion);
661
await statelessProvider.waitForCall(2);
662
await statelessProvider.calls[1].completed.p;
663
664
// Accept and apply (speculative result is now cached)
665
nextEditProvider.handleAcceptance(doc.id, firstSuggestion);
666
doc.applyEdit(firstSuggestion.result.edit.toEdit());
667
668
// Clear the speculative pending request by requesting once (consumes it from pending)
669
const consumeResult = await getNextEdit(nextEditProvider, doc.id);
670
assert(consumeResult.result?.edit);
671
672
// Now the result is in cache. Request again at same document state.
673
const { suggestion: cachedSuggestion, telemetry } = await getNextEditWithTelemetry(nextEditProvider, doc.id);
674
assert(cachedSuggestion.result?.edit);
675
676
expect(telemetry.headerRequestId).toBeDefined();
677
expect(telemetry.headerRequestId!.startsWith('sp-')).toBe(true);
678
expect(telemetry.isFromCache).toBe(true);
679
expect(telemetry.reusedRequest).toBeUndefined();
680
});
681
});
682
683
describe('isSpeculative and isSubsequentEdit flags', () => {
684
it('normal request result has isSpeculative = false and isSubsequentEdit = false', async () => {
685
const statelessProvider = new TestStatelessNextEditProvider();
686
statelessProvider.enqueueBehavior({ kind: 'yieldEditThenNoSuggestions', edit: lineReplacement(1, 'const value = 2;') });
687
const { nextEditProvider, workspace } = createProviderAndWorkspace(statelessProvider);
688
689
const doc = workspace.addDocument({
690
id: DocumentId.create(URI.file('/test/flags-normal.ts').toString()),
691
initialValue: 'const value = 1;\nconsole.log(value);',
692
});
693
doc.setSelection([new OffsetRange(0, 0)], undefined);
694
695
const suggestion = await getNextEdit(nextEditProvider, doc.id);
696
assert(suggestion.result?.edit);
697
698
expect(suggestion.source.isSpeculative).toBe(false);
699
expect(suggestion.result.isSubsequentEdit).toBe(false);
700
});
701
702
it('reused speculative result has isSpeculative = true on source', async () => {
703
await configService.setConfig(ConfigKey.TeamInternal.InlineEditsSpeculativeRequests, SpeculativeRequestsEnablement.On);
704
705
const statelessProvider = new TestStatelessNextEditProvider();
706
statelessProvider.enqueueBehavior({ kind: 'yieldEditThenNoSuggestions', edit: lineReplacement(1, 'const value = 2;') });
707
statelessProvider.enqueueBehavior({ kind: 'yieldEditThenNoSuggestions', edit: lineReplacement(2, 'console.log(value + 1);') });
708
const { nextEditProvider, workspace } = createProviderAndWorkspace(statelessProvider);
709
710
const doc = workspace.addDocument({
711
id: DocumentId.create(URI.file('/test/flags-speculative.ts').toString()),
712
initialValue: 'const value = 1;\nconsole.log(value);',
713
});
714
doc.setSelection([new OffsetRange(0, 0)], undefined);
715
716
const firstSuggestion = await getNextEdit(nextEditProvider, doc.id);
717
assert(firstSuggestion.result?.edit);
718
expect(firstSuggestion.source.isSpeculative).toBe(false);
719
720
nextEditProvider.handleShown(firstSuggestion);
721
await statelessProvider.waitForCall(2);
722
await statelessProvider.calls[1].completed.p;
723
724
nextEditProvider.handleAcceptance(doc.id, firstSuggestion);
725
doc.applyEdit(firstSuggestion.result.edit.toEdit());
726
727
const secondSuggestion = await getNextEdit(nextEditProvider, doc.id);
728
assert(secondSuggestion.result?.edit);
729
730
expect(secondSuggestion.source.isSpeculative).toBe(true);
731
});
732
});
733
734
describe('SpeculativeRequestsAutoExpandEditWindowLines', () => {
735
it('Off: speculative request has expandedEditWindowNLines = undefined', async () => {
736
await configService.setConfig(ConfigKey.TeamInternal.InlineEditsSpeculativeRequests, SpeculativeRequestsEnablement.On);
737
await configService.setConfig(ConfigKey.TeamInternal.InlineEditsSpeculativeRequestsAutoExpandEditWindowLines, SpeculativeRequestsAutoExpandEditWindowLines.Off);
738
739
const statelessProvider = new TestStatelessNextEditProvider();
740
statelessProvider.enqueueBehavior({ kind: 'yieldEditThenNoSuggestions', edit: lineReplacement(1, 'const value = 2;') });
741
statelessProvider.enqueueBehavior({ kind: 'waitForCancellation' });
742
const { nextEditProvider, workspace } = createProviderAndWorkspace(statelessProvider);
743
744
const doc = workspace.addDocument({
745
id: DocumentId.create(URI.file('/test/expand-off.ts').toString()),
746
initialValue: 'const value = 1;\nconsole.log(value);',
747
});
748
doc.setSelection([new OffsetRange(0, 0)], undefined);
749
750
const suggestion = await getNextEdit(nextEditProvider, doc.id);
751
assert(suggestion.result?.edit);
752
753
nextEditProvider.handleShown(suggestion);
754
await statelessProvider.waitForCall(2);
755
756
expect(statelessProvider.calls[1].request.expandedEditWindowNLines).toBeUndefined();
757
758
nextEditProvider.handleRejection(doc.id, suggestion);
759
await statelessProvider.calls[1].completed.p;
760
});
761
762
it('Always: speculative request has expandedEditWindowNLines from base config', async () => {
763
await configService.setConfig(ConfigKey.TeamInternal.InlineEditsSpeculativeRequests, SpeculativeRequestsEnablement.On);
764
await configService.setConfig(ConfigKey.TeamInternal.InlineEditsSpeculativeRequestsAutoExpandEditWindowLines, SpeculativeRequestsAutoExpandEditWindowLines.Always);
765
await configService.setConfig(ConfigKey.TeamInternal.InlineEditsAutoExpandEditWindowLines, 20);
766
767
const statelessProvider = new TestStatelessNextEditProvider();
768
statelessProvider.enqueueBehavior({ kind: 'yieldEditThenNoSuggestions', edit: lineReplacement(1, 'const value = 2;') });
769
statelessProvider.enqueueBehavior({ kind: 'waitForCancellation' });
770
const { nextEditProvider, workspace } = createProviderAndWorkspace(statelessProvider);
771
772
const doc = workspace.addDocument({
773
id: DocumentId.create(URI.file('/test/expand-always.ts').toString()),
774
initialValue: 'const value = 1;\nconsole.log(value);',
775
});
776
doc.setSelection([new OffsetRange(0, 0)], undefined);
777
778
const suggestion = await getNextEdit(nextEditProvider, doc.id);
779
assert(suggestion.result?.edit);
780
781
nextEditProvider.handleShown(suggestion);
782
await statelessProvider.waitForCall(2);
783
784
expect(statelessProvider.calls[1].request.expandedEditWindowNLines).toBe(20);
785
786
nextEditProvider.handleRejection(doc.id, suggestion);
787
await statelessProvider.calls[1].completed.p;
788
});
789
790
it('Smart: expandedEditWindowNLines is undefined for first non-speculative edit', async () => {
791
await configService.setConfig(ConfigKey.TeamInternal.InlineEditsSpeculativeRequests, SpeculativeRequestsEnablement.On);
792
await configService.setConfig(ConfigKey.TeamInternal.InlineEditsSpeculativeRequestsAutoExpandEditWindowLines, SpeculativeRequestsAutoExpandEditWindowLines.Smart);
793
await configService.setConfig(ConfigKey.TeamInternal.InlineEditsAutoExpandEditWindowLines, 20);
794
795
const statelessProvider = new TestStatelessNextEditProvider();
796
statelessProvider.enqueueBehavior({ kind: 'yieldEditThenNoSuggestions', edit: lineReplacement(1, 'const value = 2;') });
797
statelessProvider.enqueueBehavior({ kind: 'waitForCancellation' });
798
const { nextEditProvider, workspace } = createProviderAndWorkspace(statelessProvider);
799
800
const doc = workspace.addDocument({
801
id: DocumentId.create(URI.file('/test/expand-smart-first.ts').toString()),
802
initialValue: 'const value = 1;\nconsole.log(value);',
803
});
804
doc.setSelection([new OffsetRange(0, 0)], undefined);
805
806
// First suggestion is from a normal (non-speculative) request
807
const suggestion = await getNextEdit(nextEditProvider, doc.id);
808
assert(suggestion.result?.edit);
809
810
nextEditProvider.handleShown(suggestion);
811
await statelessProvider.waitForCall(2);
812
813
// The speculative request triggered from a non-speculative first edit
814
// should NOT expand the edit window in Smart mode
815
expect(statelessProvider.calls[1].request.expandedEditWindowNLines).toBeUndefined();
816
817
nextEditProvider.handleRejection(doc.id, suggestion);
818
await statelessProvider.calls[1].completed.p;
819
});
820
821
it('Smart: expandedEditWindowNLines uses base config when triggered by speculative chain', async () => {
822
await configService.setConfig(ConfigKey.TeamInternal.InlineEditsSpeculativeRequests, SpeculativeRequestsEnablement.On);
823
await configService.setConfig(ConfigKey.TeamInternal.InlineEditsSpeculativeRequestsAutoExpandEditWindowLines, SpeculativeRequestsAutoExpandEditWindowLines.Smart);
824
await configService.setConfig(ConfigKey.TeamInternal.InlineEditsAutoExpandEditWindowLines, 20);
825
826
const statelessProvider = new TestStatelessNextEditProvider();
827
// First normal request
828
statelessProvider.enqueueBehavior({ kind: 'yieldEditThenNoSuggestions', edit: lineReplacement(1, 'const value = 2;') });
829
// Speculative request after first edit
830
statelessProvider.enqueueBehavior({ kind: 'yieldEditThenNoSuggestions', edit: lineReplacement(2, 'console.log(value + 1);') });
831
// Speculative request after second (speculative-sourced) edit
832
statelessProvider.enqueueBehavior({ kind: 'waitForCancellation' });
833
const { nextEditProvider, workspace } = createProviderAndWorkspace(statelessProvider);
834
835
const doc = workspace.addDocument({
836
id: DocumentId.create(URI.file('/test/expand-smart-chain.ts').toString()),
837
initialValue: 'const value = 1;\nconsole.log(value);',
838
});
839
doc.setSelection([new OffsetRange(0, 0)], undefined);
840
841
// Step 1: Get first edit (normal, non-speculative)
842
const firstSuggestion = await getNextEdit(nextEditProvider, doc.id);
843
assert(firstSuggestion.result?.edit);
844
845
// Step 2: Show → triggers speculative request (call 2)
846
nextEditProvider.handleShown(firstSuggestion);
847
await statelessProvider.waitForCall(2);
848
await statelessProvider.calls[1].completed.p;
849
850
// Step 3: Accept and apply → doc matches speculative post-edit state
851
nextEditProvider.handleAcceptance(doc.id, firstSuggestion);
852
doc.applyEdit(firstSuggestion.result.edit.toEdit());
853
854
// Step 4: Get second edit → reuses speculative result (source.isSpeculative = true)
855
const secondSuggestion = await getNextEdit(nextEditProvider, doc.id);
856
assert(secondSuggestion.result?.edit);
857
assert(secondSuggestion.source.isSpeculative);
858
859
// Step 5: Show second suggestion → triggers another speculative request (call 3)
860
nextEditProvider.handleShown(secondSuggestion);
861
await statelessProvider.waitForCall(3);
862
863
// The 3rd call is a speculative request triggered by a speculative-sourced edit,
864
// so in Smart mode, isModelOnRightTrack = true and edit window should be expanded
865
expect(statelessProvider.calls[2].request.expandedEditWindowNLines).toBe(20);
866
867
nextEditProvider.handleRejection(doc.id, secondSuggestion);
868
await statelessProvider.calls[2].completed.p;
869
});
870
});
871
872
describe('scheduled speculative requests for multi-edit streams', () => {
873
it('does not trigger speculative when shown edit is not the last in a multi-edit stream', async () => {
874
await configService.setConfig(ConfigKey.TeamInternal.InlineEditsSpeculativeRequests, SpeculativeRequestsEnablement.On);
875
876
const statelessProvider = new TestStatelessNextEditProvider();
877
const continueSignal = new DeferredPromise<void>();
878
statelessProvider.enqueueBehavior({
879
kind: 'yieldEditThenWaitThenYieldEditsThenNoSuggestions',
880
firstEdit: lineReplacement(1, 'const value = 2;'),
881
continueSignal,
882
remainingEdits: [lineReplacement(2, 'console.log(value + 1);')],
883
});
884
const { nextEditProvider, workspace } = createProviderAndWorkspace(statelessProvider);
885
886
const doc = workspace.addDocument({
887
id: DocumentId.create(URI.file('/test/spec-multi-not-last.ts').toString()),
888
initialValue: 'const value = 1;\nconsole.log(value);',
889
});
890
doc.setSelection([new OffsetRange(0, 0)], undefined);
891
892
// Get first edit (E0) — stream is paused on continueSignal
893
const suggestion = await getNextEdit(nextEditProvider, doc.id);
894
assert(suggestion.result?.edit);
895
896
// Show E0 — stream still running → speculative is scheduled (not fired)
897
nextEditProvider.handleShown(suggestion);
898
899
// Resume stream — E1 arrives → clears the scheduled speculative
900
continueSignal.complete();
901
await statelessProvider.calls[0].completed.p;
902
await flushMicrotasks();
903
904
// Only the original request was made — no speculative request
905
expect(statelessProvider.calls.length).toBe(1);
906
});
907
908
it('triggers speculative after stream completes when shown edit is the last one', async () => {
909
await configService.setConfig(ConfigKey.TeamInternal.InlineEditsSpeculativeRequests, SpeculativeRequestsEnablement.On);
910
911
const statelessProvider = new TestStatelessNextEditProvider();
912
const continueSignal = new DeferredPromise<void>();
913
statelessProvider.enqueueBehavior({
914
kind: 'yieldEditThenWait',
915
edit: lineReplacement(1, 'const value = 2;'),
916
continueSignal,
917
});
918
statelessProvider.enqueueBehavior({ kind: 'waitForCancellation' });
919
const { nextEditProvider, workspace } = createProviderAndWorkspace(statelessProvider);
920
921
const doc = workspace.addDocument({
922
id: DocumentId.create(URI.file('/test/spec-last-edit.ts').toString()),
923
initialValue: 'const value = 1;\nconsole.log(value);',
924
});
925
doc.setSelection([new OffsetRange(0, 0)], undefined);
926
927
// Get first edit (E0) — stream paused on continueSignal
928
const suggestion = await getNextEdit(nextEditProvider, doc.id);
929
assert(suggestion.result?.edit);
930
931
// Show E0 — stream still running → speculative is scheduled
932
nextEditProvider.handleShown(suggestion);
933
934
// Resume stream — no more edits → stream ends → scheduled speculative fires
935
continueSignal.complete();
936
// The speculative fires from handleStreamEnd (background IIFE), so we need
937
// microtasks to propagate through the async chain before the call arrives.
938
await flushMicrotasks();
939
940
expect(statelessProvider.calls.length).toBe(2);
941
expect(statelessProvider.calls[1].request.isSpeculative).toBe(true);
942
943
nextEditProvider.handleRejection(doc.id, suggestion);
944
await statelessProvider.calls[1].completed.p;
945
});
946
947
it('clears scheduled speculative on rejection before stream completes', async () => {
948
await configService.setConfig(ConfigKey.TeamInternal.InlineEditsSpeculativeRequests, SpeculativeRequestsEnablement.On);
949
950
const statelessProvider = new TestStatelessNextEditProvider();
951
const continueSignal = new DeferredPromise<void>();
952
statelessProvider.enqueueBehavior({
953
kind: 'yieldEditThenWait',
954
edit: lineReplacement(1, 'const value = 2;'),
955
continueSignal,
956
});
957
const { nextEditProvider, workspace } = createProviderAndWorkspace(statelessProvider);
958
959
const doc = workspace.addDocument({
960
id: DocumentId.create(URI.file('/test/spec-reject-before-end.ts').toString()),
961
initialValue: 'const value = 1;\nconsole.log(value);',
962
});
963
doc.setSelection([new OffsetRange(0, 0)], undefined);
964
965
// Get E0, stream paused
966
const suggestion = await getNextEdit(nextEditProvider, doc.id);
967
assert(suggestion.result?.edit);
968
969
// Show → schedules speculative
970
nextEditProvider.handleShown(suggestion);
971
972
// Reject before stream completes → clears schedule
973
nextEditProvider.handleRejection(doc.id, suggestion);
974
975
// Let stream finish
976
continueSignal.complete();
977
await statelessProvider.calls[0].completed.p;
978
await flushMicrotasks();
979
980
// No speculative request was created
981
expect(statelessProvider.calls.length).toBe(1);
982
});
983
984
it('clears scheduled speculative on handleIgnored (shown, not superseded) before stream completes', async () => {
985
await configService.setConfig(ConfigKey.TeamInternal.InlineEditsSpeculativeRequests, SpeculativeRequestsEnablement.On);
986
987
const statelessProvider = new TestStatelessNextEditProvider();
988
const continueSignal = new DeferredPromise<void>();
989
statelessProvider.enqueueBehavior({
990
kind: 'yieldEditThenWait',
991
edit: lineReplacement(1, 'const value = 2;'),
992
continueSignal,
993
});
994
const { nextEditProvider, workspace } = createProviderAndWorkspace(statelessProvider);
995
996
const doc = workspace.addDocument({
997
id: DocumentId.create(URI.file('/test/spec-ignored-before-end.ts').toString()),
998
initialValue: 'const value = 1;\nconsole.log(value);',
999
});
1000
doc.setSelection([new OffsetRange(0, 0)], undefined);
1001
1002
const suggestion = await getNextEdit(nextEditProvider, doc.id);
1003
assert(suggestion.result?.edit);
1004
1005
// Show → schedules speculative
1006
nextEditProvider.handleShown(suggestion);
1007
1008
// Ignored (shown, not superseded) before stream completes → clears schedule
1009
nextEditProvider.handleIgnored(doc.id, suggestion, undefined);
1010
1011
// Let stream finish
1012
continueSignal.complete();
1013
await statelessProvider.calls[0].completed.p;
1014
await flushMicrotasks();
1015
1016
// No speculative request was created
1017
expect(statelessProvider.calls.length).toBe(1);
1018
});
1019
1020
it('fires speculative immediately when stream already completed before handleShown', async () => {
1021
await configService.setConfig(ConfigKey.TeamInternal.InlineEditsSpeculativeRequests, SpeculativeRequestsEnablement.On);
1022
1023
const statelessProvider = new TestStatelessNextEditProvider();
1024
statelessProvider.enqueueBehavior({ kind: 'yieldEditThenNoSuggestions', edit: lineReplacement(1, 'const value = 2;') });
1025
statelessProvider.enqueueBehavior({ kind: 'waitForCancellation' });
1026
const { nextEditProvider, workspace } = createProviderAndWorkspace(statelessProvider);
1027
1028
const doc = workspace.addDocument({
1029
id: DocumentId.create(URI.file('/test/spec-stream-done.ts').toString()),
1030
initialValue: 'const value = 1;\nconsole.log(value);',
1031
});
1032
doc.setSelection([new OffsetRange(0, 0)], undefined);
1033
1034
const suggestion = await getNextEdit(nextEditProvider, doc.id);
1035
assert(suggestion.result?.edit);
1036
1037
// Ensure the background IIFE has completed (stream is done, pending request cleared)
1038
await flushMicrotasks();
1039
1040
// Now handleShown sees no pending request → fires immediately (not scheduled)
1041
nextEditProvider.handleShown(suggestion);
1042
await statelessProvider.waitForCall(2);
1043
1044
expect(statelessProvider.calls.length).toBe(2);
1045
expect(statelessProvider.calls[1].request.isSpeculative).toBe(true);
1046
1047
nextEditProvider.handleRejection(doc.id, suggestion);
1048
await statelessProvider.calls[1].completed.p;
1049
});
1050
1051
it('clears scheduled speculative when a new getNextEdit supersedes the originating stream', async () => {
1052
await configService.setConfig(ConfigKey.TeamInternal.InlineEditsSpeculativeRequests, SpeculativeRequestsEnablement.On);
1053
1054
const statelessProvider = new TestStatelessNextEditProvider();
1055
// Stream A: yields E0, then waits (simulating a paused multi-edit stream)
1056
const streamAContinue = new DeferredPromise<void>();
1057
statelessProvider.enqueueBehavior({
1058
kind: 'yieldEditThenWait',
1059
edit: lineReplacement(1, 'const value = 2;'),
1060
continueSignal: streamAContinue,
1061
});
1062
// Stream B: the new request that supersedes stream A
1063
statelessProvider.enqueueBehavior({ kind: 'yieldEditThenNoSuggestions', edit: lineReplacement(1, 'const value = 3;') });
1064
const { nextEditProvider, workspace } = createProviderAndWorkspace(statelessProvider);
1065
1066
const doc = workspace.addDocument({
1067
id: DocumentId.create(URI.file('/test/spec-stale-schedule.ts').toString()),
1068
initialValue: 'const value = 1;\nconsole.log(value);',
1069
});
1070
doc.setSelection([new OffsetRange(0, 0)], undefined);
1071
1072
// 1. Get E0 from stream A — stream is paused on streamAContinue
1073
const suggestionA = await getNextEdit(nextEditProvider, doc.id);
1074
assert(suggestionA.result?.edit);
1075
1076
// 2. handleShown(E0) → speculative is scheduled (not fired, stream A still running)
1077
nextEditProvider.handleShown(suggestionA);
1078
1079
// 3. A new getNextEdit supersedes stream A — should clear the scheduled speculative
1080
const suggestionB = await getNextEdit(nextEditProvider, doc.id);
1081
assert(suggestionB.result?.edit);
1082
1083
// 4. Let stream A's background IIFE finish (after cancellation).
1084
// Without the fix, handleStreamEnd would see the stale scheduled speculative
1085
// and fire _triggerSpeculativeRequest for stream A's E0.
1086
streamAContinue.complete();
1087
await statelessProvider.calls[0].completed.p;
1088
await flushMicrotasks();
1089
1090
// Only 2 calls: stream A and stream B. No stale speculative request fired.
1091
expect(statelessProvider.calls.length).toBe(2);
1092
});
1093
1094
it('second handleShown replaces a previously scheduled speculative', async () => {
1095
await configService.setConfig(ConfigKey.TeamInternal.InlineEditsSpeculativeRequests, SpeculativeRequestsEnablement.On);
1096
1097
const statelessProvider = new TestStatelessNextEditProvider();
1098
const continueSignal = new DeferredPromise<void>();
1099
statelessProvider.enqueueBehavior({
1100
kind: 'yieldEditThenWait',
1101
edit: lineReplacement(1, 'const value = 2;'),
1102
continueSignal,
1103
});
1104
statelessProvider.enqueueBehavior({ kind: 'waitForCancellation' });
1105
const { nextEditProvider, workspace } = createProviderAndWorkspace(statelessProvider);
1106
1107
const doc = workspace.addDocument({
1108
id: DocumentId.create(URI.file('/test/spec-replace-schedule.ts').toString()),
1109
initialValue: 'const value = 1;\nconsole.log(value);',
1110
});
1111
doc.setSelection([new OffsetRange(0, 0)], undefined);
1112
1113
const suggestion = await getNextEdit(nextEditProvider, doc.id);
1114
assert(suggestion.result?.edit);
1115
1116
// First handleShown → schedules speculative (stream still running)
1117
nextEditProvider.handleShown(suggestion);
1118
1119
// Second handleShown for the same suggestion → clears the previous schedule
1120
// and sets a new one for the same headerRequestId
1121
nextEditProvider.handleShown(suggestion);
1122
1123
// Resume stream → stream ends → the (second) scheduled speculative fires
1124
continueSignal.complete();
1125
await flushMicrotasks();
1126
1127
// Exactly one speculative request was created (not two)
1128
expect(statelessProvider.calls.length).toBe(2);
1129
expect(statelessProvider.calls[1].request.isSpeculative).toBe(true);
1130
1131
nextEditProvider.handleRejection(doc.id, suggestion);
1132
await statelessProvider.calls[1].completed.p;
1133
});
1134
});
1135
1136
describe('edit window cursor check for request reuse', () => {
1137
it('does not reuse in-flight request when cursor moves outside edit window', async () => {
1138
const statelessProvider = new TestStatelessNextEditProvider();
1139
// Edit window covers offsets 0–20 of the document
1140
statelessProvider.editWindow = new RequestEditWindow(new OffsetRange(0, 20));
1141
const continueSignal1 = new DeferredPromise<void>();
1142
const continueSignal2 = new DeferredPromise<void>();
1143
statelessProvider.enqueueBehavior({ kind: 'yieldEditThenWait', edit: lineReplacement(1, 'const value = 2;'), continueSignal: continueSignal1 });
1144
statelessProvider.enqueueBehavior({ kind: 'yieldEditThenWait', edit: lineReplacement(1, 'const value = 3;'), continueSignal: continueSignal2 });
1145
const { nextEditProvider, workspace } = createProviderAndWorkspace(statelessProvider);
1146
1147
const doc = workspace.addDocument({
1148
id: DocumentId.create(URI.file('/test/ew-outside.ts').toString()),
1149
initialValue: 'const value = 1;\nconsole.log(value);\nconst other = 3;\n',
1150
});
1151
doc.setSelection([new OffsetRange(0, 0)], undefined);
1152
1153
// First request — yields first edit, stream still running in background
1154
const firstSuggestion = await getNextEdit(nextEditProvider, doc.id);
1155
assert(firstSuggestion.result?.edit);
1156
expect(statelessProvider.calls.length).toBe(1);
1157
1158
// Move cursor far outside the edit window (offset 40)
1159
doc.setSelection([new OffsetRange(40, 40)], undefined);
1160
1161
// Second request — should NOT reuse the in-flight request because cursor is outside edit window
1162
// The first request's stream is still running, but cursor is outside its edit window, so a new request is made
1163
const secondSuggestion = await getNextEdit(nextEditProvider, doc.id);
1164
assert(secondSuggestion.result?.edit);
1165
1166
// Two separate provider calls were made
1167
expect(statelessProvider.calls.length).toBe(2);
1168
1169
// Clean up
1170
continueSignal1.complete();
1171
continueSignal2.complete();
1172
await statelessProvider.calls[0].completed.p;
1173
await statelessProvider.calls[1].completed.p;
1174
});
1175
1176
it('reuses in-flight request when cursor stays within edit window', async () => {
1177
const statelessProvider = new TestStatelessNextEditProvider();
1178
// Edit window covers offsets 0–50 (whole document)
1179
statelessProvider.editWindow = new RequestEditWindow(new OffsetRange(0, 50));
1180
const continueSignal = new DeferredPromise<void>();
1181
statelessProvider.enqueueBehavior({ kind: 'yieldEditThenWait', edit: lineReplacement(1, 'const value = 2;'), continueSignal });
1182
const { nextEditProvider, workspace } = createProviderAndWorkspace(statelessProvider);
1183
1184
const doc = workspace.addDocument({
1185
id: DocumentId.create(URI.file('/test/ew-inside.ts').toString()),
1186
initialValue: 'const value = 1;\nconsole.log(value);\n',
1187
});
1188
doc.setSelection([new OffsetRange(0, 0)], undefined);
1189
1190
// First request — yields first edit, stream still running
1191
const firstSuggestion = await getNextEdit(nextEditProvider, doc.id);
1192
assert(firstSuggestion.result?.edit);
1193
expect(statelessProvider.calls.length).toBe(1);
1194
1195
// Move cursor but still within the edit window (offset 10)
1196
doc.setSelection([new OffsetRange(10, 10)], undefined);
1197
1198
// Second request — should reuse the in-flight request
1199
const secondSuggestion = await getNextEdit(nextEditProvider, doc.id);
1200
assert(secondSuggestion.result?.edit);
1201
1202
// Only one provider call was made (reused)
1203
expect(statelessProvider.calls.length).toBe(1);
1204
1205
// Clean up
1206
continueSignal.complete();
1207
await statelessProvider.calls[0].completed.p;
1208
});
1209
1210
it('reuses in-flight request when editWindow is undefined (graceful fallback)', async () => {
1211
const statelessProvider = new TestStatelessNextEditProvider();
1212
// No editWindow set — should allow reuse
1213
const continueSignal = new DeferredPromise<void>();
1214
statelessProvider.enqueueBehavior({ kind: 'yieldEditThenWait', edit: lineReplacement(1, 'const value = 2;'), continueSignal });
1215
const { nextEditProvider, workspace } = createProviderAndWorkspace(statelessProvider);
1216
1217
const doc = workspace.addDocument({
1218
id: DocumentId.create(URI.file('/test/ew-undefined.ts').toString()),
1219
initialValue: 'const value = 1;\nconsole.log(value);\nconst other = 3;\n',
1220
});
1221
doc.setSelection([new OffsetRange(0, 0)], undefined);
1222
1223
// First request — yields first edit, stream still running
1224
const firstSuggestion = await getNextEdit(nextEditProvider, doc.id);
1225
assert(firstSuggestion.result?.edit);
1226
expect(statelessProvider.calls.length).toBe(1);
1227
1228
// Move cursor far away — but editWindow is undefined so reuse is allowed
1229
doc.setSelection([new OffsetRange(40, 40)], undefined);
1230
1231
// Second request — should reuse (no edit window to check)
1232
const secondSuggestion = await getNextEdit(nextEditProvider, doc.id);
1233
assert(secondSuggestion.result?.edit);
1234
1235
expect(statelessProvider.calls.length).toBe(1);
1236
1237
// Clean up
1238
continueSignal.complete();
1239
await statelessProvider.calls[0].completed.p;
1240
});
1241
1242
it('does not reuse speculative request when cursor moves outside edit window', async () => {
1243
await configService.setConfig(ConfigKey.TeamInternal.InlineEditsSpeculativeRequests, SpeculativeRequestsEnablement.On);
1244
1245
const statelessProvider = new TestStatelessNextEditProvider();
1246
// Edit window covers offsets 0–20
1247
statelessProvider.editWindow = new RequestEditWindow(new OffsetRange(0, 20));
1248
statelessProvider.enqueueBehavior({ kind: 'yieldEditThenNoSuggestions', edit: lineReplacement(1, 'const value = 2;') });
1249
statelessProvider.enqueueBehavior({ kind: 'yieldEditThenNoSuggestions', edit: lineReplacement(2, 'console.log(value + 1);') });
1250
// Third behavior for the new request that will be needed since speculative won't be reused
1251
statelessProvider.enqueueBehavior({ kind: 'yieldEditThenNoSuggestions', edit: lineReplacement(2, 'console.log(value + 1);') });
1252
const { nextEditProvider, workspace } = createProviderAndWorkspace(statelessProvider);
1253
1254
const doc = workspace.addDocument({
1255
id: DocumentId.create(URI.file('/test/ew-spec-outside.ts').toString()),
1256
initialValue: 'const value = 1;\nconsole.log(value);\nconst other = 3;\n',
1257
});
1258
doc.setSelection([new OffsetRange(0, 0)], undefined);
1259
1260
const firstSuggestion = await getNextEdit(nextEditProvider, doc.id);
1261
assert(firstSuggestion.result?.edit);
1262
nextEditProvider.handleShown(firstSuggestion);
1263
await statelessProvider.waitForCall(2);
1264
await statelessProvider.calls[1].completed.p;
1265
1266
// Accept and apply the edit
1267
nextEditProvider.handleAcceptance(doc.id, firstSuggestion);
1268
doc.applyEdit(firstSuggestion.result.edit.toEdit());
1269
1270
// Move cursor outside the speculative request's edit window
1271
doc.setSelection([new OffsetRange(40, 40)], undefined);
1272
1273
// This should NOT reuse the speculative request (cursor is outside)
1274
await getNextEdit(nextEditProvider, doc.id);
1275
1276
// Three calls: original, speculative, and a new one (speculative was not reused)
1277
expect(statelessProvider.calls.length).toBe(3);
1278
});
1279
1280
it('reuses in-flight request when cursor is within originalWindow of cursor jump edit window', async () => {
1281
const statelessProvider = new TestStatelessNextEditProvider();
1282
// Cursor jump: new window is at 30–50, original window is at 0–20
1283
statelessProvider.editWindow = new RequestEditWindowWithCursorJump(new OffsetRange(30, 50), new OffsetRange(0, 20));
1284
const continueSignal = new DeferredPromise<void>();
1285
statelessProvider.enqueueBehavior({ kind: 'yieldEditThenWait', edit: lineReplacement(1, 'const value = 2;'), continueSignal });
1286
const { nextEditProvider, workspace } = createProviderAndWorkspace(statelessProvider);
1287
1288
const doc = workspace.addDocument({
1289
id: DocumentId.create(URI.file('/test/ew-cursorjump.ts').toString()),
1290
initialValue: 'const value = 1;\nconsole.log(value);\nconst other = 3;\nconst extra = 4;\n',
1291
});
1292
doc.setSelection([new OffsetRange(0, 0)], undefined);
1293
1294
// First request — yields first edit, stream still running
1295
const firstSuggestion = await getNextEdit(nextEditProvider, doc.id);
1296
assert(firstSuggestion.result?.edit);
1297
expect(statelessProvider.calls.length).toBe(1);
1298
1299
// Move cursor to offset 10 — inside originalWindow (0–20) but outside jump target (30–50)
1300
doc.setSelection([new OffsetRange(10, 10)], undefined);
1301
1302
// Second request — should reuse because cursor is in originalWindow
1303
const secondSuggestion = await getNextEdit(nextEditProvider, doc.id);
1304
assert(secondSuggestion.result?.edit);
1305
1306
expect(statelessProvider.calls.length).toBe(1);
1307
1308
// Clean up
1309
continueSignal.complete();
1310
await statelessProvider.calls[0].completed.p;
1311
});
1312
});
1313
1314
describe('cached speculative result delay', () => {
1315
it('uses speculativeRequestDelay (not cacheDelay) when speculative result is served from cache', async () => {
1316
const CACHE_DELAY_MS = 5_000;
1317
await configService.setConfig(ConfigKey.TeamInternal.InlineEditsSpeculativeRequests, SpeculativeRequestsEnablement.On);
1318
await configService.setConfig(ConfigKey.TeamInternal.InlineEditsCacheDelay, CACHE_DELAY_MS);
1319
await configService.setConfig(ConfigKey.TeamInternal.InlineEditsSpeculativeRequestDelay, 0);
1320
1321
const statelessProvider = new TestStatelessNextEditProvider();
1322
statelessProvider.enqueueBehavior({ kind: 'yieldEditThenNoSuggestions', edit: lineReplacement(1, 'const value = 2;') });
1323
statelessProvider.enqueueBehavior({ kind: 'yieldEditThenNoSuggestions', edit: lineReplacement(2, 'console.log(value + 1);') });
1324
const { nextEditProvider, workspace } = createProviderAndWorkspace(statelessProvider);
1325
1326
const doc = workspace.addDocument({
1327
id: DocumentId.create(URI.file('/test/spec-cache-delay.ts').toString()),
1328
initialValue: 'const value = 1;\nconsole.log(value);',
1329
});
1330
doc.setSelection([new OffsetRange(0, 0)], undefined);
1331
1332
// First request (fresh)
1333
const firstSuggestion = await getNextEdit(nextEditProvider, doc.id);
1334
assert(firstSuggestion.result?.edit);
1335
1336
// Show → triggers speculative request; wait for it to complete and cache
1337
nextEditProvider.handleShown(firstSuggestion);
1338
await statelessProvider.waitForCall(2);
1339
await statelessProvider.calls[1].completed.p;
1340
1341
// Accept and apply — doc now matches speculative request's postEditContent
1342
nextEditProvider.handleAcceptance(doc.id, firstSuggestion);
1343
doc.applyEdit(firstSuggestion.result.edit.toEdit());
1344
1345
// Next getNextEdit hits the cache path (speculative result already cached).
1346
// With enforceCacheDelay=true, it should use speculativeRequestDelay (0ms),
1347
// NOT the normal cacheDelay (5000ms).
1348
const context: NESInlineCompletionContext = {
1349
triggerKind: 1,
1350
selectedCompletionInfo: undefined,
1351
requestUuid: generateUuid(),
1352
requestIssuedDateTime: Date.now(),
1353
earliestShownDateTime: Date.now(),
1354
enforceCacheDelay: true,
1355
};
1356
const logContext = new InlineEditRequestLogContext(doc.id.toString(), 1, context);
1357
const telemetryBuilder = new NextEditProviderTelemetryBuilder(gitExtensionService, mockNotebookService, workspaceService, nextEditProvider.ID, undefined);
1358
const start = Date.now();
1359
try {
1360
const cachedSuggestion = await nextEditProvider.getNextEdit(doc.id, context, logContext, CancellationToken.None, telemetryBuilder.nesBuilder);
1361
const elapsed = Date.now() - start;
1362
assert(cachedSuggestion.result?.edit);
1363
1364
// The result comes from a speculative request's cache, so it should
1365
// use the speculative delay (0ms) rather than the cache delay (5000ms)
1366
expect(elapsed).toBeLessThan(100);
1367
} finally {
1368
telemetryBuilder.dispose();
1369
}
1370
});
1371
});
1372
1373
describe('lifecycle cancellation', () => {
1374
it('cancels in-flight speculative when clearCache() is called', async () => {
1375
await configService.setConfig(ConfigKey.TeamInternal.InlineEditsSpeculativeRequests, SpeculativeRequestsEnablement.On);
1376
1377
const statelessProvider = new TestStatelessNextEditProvider();
1378
statelessProvider.enqueueBehavior({ kind: 'yieldEditThenNoSuggestions', edit: lineReplacement(1, 'const value = 2;') });
1379
statelessProvider.enqueueBehavior({ kind: 'waitForCancellation' });
1380
const { nextEditProvider, workspace } = createProviderAndWorkspace(statelessProvider);
1381
1382
const doc = workspace.addDocument({
1383
id: DocumentId.create(URI.file('/test/spec-clear-cache.ts').toString()),
1384
initialValue: 'const value = 1;\nconsole.log(value);',
1385
});
1386
doc.setSelection([new OffsetRange(0, 0)], undefined);
1387
1388
const suggestion = await getNextEdit(nextEditProvider, doc.id);
1389
assert(suggestion.result?.edit);
1390
nextEditProvider.handleShown(suggestion);
1391
await statelessProvider.waitForCall(2);
1392
1393
nextEditProvider.clearCache();
1394
await statelessProvider.calls[1].cancellationRequested.p;
1395
1396
expect(statelessProvider.calls[1].wasCancelled).toBe(true);
1397
});
1398
1399
it('cancels in-flight speculative when its target document is closed', async () => {
1400
await configService.setConfig(ConfigKey.TeamInternal.InlineEditsSpeculativeRequests, SpeculativeRequestsEnablement.On);
1401
1402
const statelessProvider = new TestStatelessNextEditProvider();
1403
statelessProvider.enqueueBehavior({ kind: 'yieldEditThenNoSuggestions', edit: lineReplacement(1, 'const value = 2;') });
1404
statelessProvider.enqueueBehavior({ kind: 'waitForCancellation' });
1405
const { nextEditProvider, workspace } = createProviderAndWorkspace(statelessProvider);
1406
1407
const doc = workspace.addDocument({
1408
id: DocumentId.create(URI.file('/test/spec-doc-close.ts').toString()),
1409
initialValue: 'const value = 1;\nconsole.log(value);',
1410
});
1411
doc.setSelection([new OffsetRange(0, 0)], undefined);
1412
1413
const suggestion = await getNextEdit(nextEditProvider, doc.id);
1414
assert(suggestion.result?.edit);
1415
nextEditProvider.handleShown(suggestion);
1416
await statelessProvider.waitForCall(2);
1417
1418
// Closing the document removes it from openDocuments — the speculative's
1419
// cached result would never be hit again, so cancel it.
1420
doc.dispose();
1421
await statelessProvider.calls[1].cancellationRequested.p;
1422
1423
expect(statelessProvider.calls[1].wasCancelled).toBe(true);
1424
});
1425
1426
it('cancels in-flight speculative when the provider is disposed', async () => {
1427
await configService.setConfig(ConfigKey.TeamInternal.InlineEditsSpeculativeRequests, SpeculativeRequestsEnablement.On);
1428
1429
const statelessProvider = new TestStatelessNextEditProvider();
1430
statelessProvider.enqueueBehavior({ kind: 'yieldEditThenNoSuggestions', edit: lineReplacement(1, 'const value = 2;') });
1431
statelessProvider.enqueueBehavior({ kind: 'waitForCancellation' });
1432
const { nextEditProvider, workspace } = createProviderAndWorkspace(statelessProvider);
1433
1434
const doc = workspace.addDocument({
1435
id: DocumentId.create(URI.file('/test/spec-provider-dispose.ts').toString()),
1436
initialValue: 'const value = 1;\nconsole.log(value);',
1437
});
1438
doc.setSelection([new OffsetRange(0, 0)], undefined);
1439
1440
const suggestion = await getNextEdit(nextEditProvider, doc.id);
1441
assert(suggestion.result?.edit);
1442
nextEditProvider.handleShown(suggestion);
1443
await statelessProvider.waitForCall(2);
1444
1445
nextEditProvider.dispose();
1446
await statelessProvider.calls[1].cancellationRequested.p;
1447
1448
expect(statelessProvider.calls[1].wasCancelled).toBe(true);
1449
});
1450
});
1451
});
1452
1453