Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/editTelemetry/test/browser/aiContributionFeature.test.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 assert from 'assert';
7
import { DisposableStore } from '../../../../../base/common/lifecycle.js';
8
import { URI } from '../../../../../base/common/uri.js';
9
import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js';
10
import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js';
11
import { AnnotatedDocuments, UriVisibilityProvider } from '../../browser/helpers/annotatedDocuments.js';
12
import { StringEditWithReason } from '../../browser/helpers/observableWorkspace.js';
13
import { AiContributionFeature } from '../../browser/aiContributionFeature.js';
14
import { EditSources } from '../../../../../editor/common/textModelEditSource.js';
15
import { DiffService } from '../../browser/helpers/documentWithAnnotatedEdits.js';
16
import { computeStringDiff } from '../../../../../editor/common/services/editorWebWorker.js';
17
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
18
import { ILogService, NullLogService } from '../../../../../platform/log/common/log.js';
19
import { MutableObservableWorkspace } from './editTelemetry.test.js';
20
import { CommandsRegistry } from '../../../../../platform/commands/common/commands.js';
21
import { timeout } from '../../../../../base/common/async.js';
22
import { runWithFakedTimers } from '../../../../../base/test/common/timeTravelScheduler.js';
23
24
suite('AiContributionFeature', () => {
25
ensureNoDisposablesAreLeakedInTestSuite();
26
27
let disposables: DisposableStore;
28
let workspace: MutableObservableWorkspace;
29
30
const fileA = URI.parse('file:///a.ts');
31
const fileB = URI.parse('file:///b.ts');
32
33
const chatEdit = EditSources.chatApplyEdits({
34
languageId: 'plaintext',
35
modelId: undefined,
36
codeBlockSuggestionId: undefined,
37
extensionId: undefined,
38
mode: undefined,
39
requestId: undefined,
40
sessionId: undefined,
41
});
42
43
const userEdit = EditSources.cursor({ kind: 'type' });
44
45
const inlineCompletionEdit = EditSources.inlineCompletionAccept({
46
nes: false,
47
requestUuid: 'test-uuid',
48
languageId: 'plaintext',
49
correlationId: undefined,
50
});
51
52
function setup(): void {
53
disposables = new DisposableStore();
54
const instantiationService = disposables.add(new TestInstantiationService(new ServiceCollection(), false, undefined, true));
55
instantiationService.stubInstance(DiffService, { computeDiff: async (original, modified) => computeStringDiff(original, modified, { maxComputationTimeMs: 500 }, 'advanced') });
56
instantiationService.stubInstance(UriVisibilityProvider, { isVisible: () => true });
57
instantiationService.stub(ILogService, new NullLogService());
58
59
workspace = new MutableObservableWorkspace();
60
const annotatedDocuments = disposables.add(new AnnotatedDocuments(workspace, instantiationService));
61
disposables.add(instantiationService.createInstance(AiContributionFeature, annotatedDocuments));
62
}
63
64
function hasAiContributions(uris: URI[], level: 'chatAndAgent' | 'all'): boolean {
65
return CommandsRegistry.getCommand('_aiEdits.hasAiContributions')!.handler(undefined!, uris, level) as unknown as boolean;
66
}
67
68
function clearAiContributions(uris: URI[]): void {
69
CommandsRegistry.getCommand('_aiEdits.clearAiContributions')!.handler(undefined!, uris);
70
}
71
72
function clearAllAiContributions(): void {
73
CommandsRegistry.getCommand('_aiEdits.clearAllAiContributions')!.handler(undefined!);
74
}
75
76
test('no contributions initially', () => runWithFakedTimers({}, async () => {
77
setup();
78
const d = disposables.add(workspace.createDocument({ uri: fileA, initialValue: 'hello' }, undefined));
79
await timeout(1500);
80
assert.strictEqual(hasAiContributions([d.uri], 'all'), false);
81
assert.strictEqual(hasAiContributions([d.uri], 'chatAndAgent'), false);
82
disposables.dispose();
83
}));
84
85
test('detects chat AI edits', () => runWithFakedTimers({}, async () => {
86
setup();
87
const d = disposables.add(workspace.createDocument({ uri: fileA, initialValue: 'hello' }, undefined));
88
await timeout(1500);
89
90
d.applyEdit(StringEditWithReason.replace(d.findRange('hello'), 'world', chatEdit));
91
await timeout(1500);
92
93
assert.strictEqual(hasAiContributions([d.uri], 'all'), true);
94
assert.strictEqual(hasAiContributions([d.uri], 'chatAndAgent'), true);
95
disposables.dispose();
96
}));
97
98
test('detects inline completion AI edits at all level only', () => runWithFakedTimers({}, async () => {
99
setup();
100
const d = disposables.add(workspace.createDocument({ uri: fileA, initialValue: 'hello' }, undefined));
101
await timeout(1500);
102
103
d.applyEdit(StringEditWithReason.replace(d.findRange('hello'), 'world', inlineCompletionEdit));
104
await timeout(1500);
105
106
assert.strictEqual(hasAiContributions([d.uri], 'all'), true);
107
assert.strictEqual(hasAiContributions([d.uri], 'chatAndAgent'), false);
108
disposables.dispose();
109
}));
110
111
test('does not detect user edits as AI', () => runWithFakedTimers({}, async () => {
112
setup();
113
const d = disposables.add(workspace.createDocument({ uri: fileA, initialValue: 'hello' }, undefined));
114
await timeout(1500);
115
116
d.applyEdit(StringEditWithReason.replace(d.findRange('hello'), 'world', userEdit));
117
await timeout(1500);
118
119
assert.strictEqual(hasAiContributions([d.uri], 'all'), false);
120
assert.strictEqual(hasAiContributions([d.uri], 'chatAndAgent'), false);
121
disposables.dispose();
122
}));
123
124
test('clear resets contributions for specific resources', () => runWithFakedTimers({}, async () => {
125
setup();
126
const dA = disposables.add(workspace.createDocument({ uri: fileA, initialValue: 'hello' }, undefined));
127
const dB = disposables.add(workspace.createDocument({ uri: fileB, initialValue: 'world' }, undefined));
128
await timeout(1500);
129
130
dA.applyEdit(StringEditWithReason.replace(dA.findRange('hello'), 'foo', chatEdit));
131
dB.applyEdit(StringEditWithReason.replace(dB.findRange('world'), 'bar', chatEdit));
132
await timeout(1500);
133
134
assert.strictEqual(hasAiContributions([dA.uri], 'all'), true);
135
assert.strictEqual(hasAiContributions([dB.uri], 'all'), true);
136
137
clearAiContributions([dA.uri]);
138
139
assert.strictEqual(hasAiContributions([dA.uri], 'all'), false);
140
assert.strictEqual(hasAiContributions([dB.uri], 'all'), true);
141
disposables.dispose();
142
}));
143
144
test('clearAll resets all contributions', () => runWithFakedTimers({}, async () => {
145
setup();
146
const dA = disposables.add(workspace.createDocument({ uri: fileA, initialValue: 'hello' }, undefined));
147
const dB = disposables.add(workspace.createDocument({ uri: fileB, initialValue: 'world' }, undefined));
148
await timeout(1500);
149
150
dA.applyEdit(StringEditWithReason.replace(dA.findRange('hello'), 'foo', chatEdit));
151
dB.applyEdit(StringEditWithReason.replace(dB.findRange('world'), 'bar', chatEdit));
152
await timeout(1500);
153
154
clearAllAiContributions();
155
156
assert.strictEqual(hasAiContributions([dA.uri], 'all'), false);
157
assert.strictEqual(hasAiContributions([dB.uri], 'all'), false);
158
disposables.dispose();
159
}));
160
161
test('tracks new edits after clear', () => runWithFakedTimers({}, async () => {
162
setup();
163
const d = disposables.add(workspace.createDocument({ uri: fileA, initialValue: 'hello' }, undefined));
164
await timeout(1500);
165
166
d.applyEdit(StringEditWithReason.replace(d.findRange('hello'), 'world', chatEdit));
167
await timeout(1500);
168
169
clearAiContributions([d.uri]);
170
assert.strictEqual(hasAiContributions([d.uri], 'all'), false);
171
172
d.applyEdit(StringEditWithReason.replace(d.findRange('world'), 'again', chatEdit));
173
await timeout(1500);
174
175
assert.strictEqual(hasAiContributions([d.uri], 'all'), true);
176
disposables.dispose();
177
}));
178
179
test('cleans up tracker when document is closed', () => runWithFakedTimers({}, async () => {
180
setup();
181
const d = disposables.add(workspace.createDocument({ uri: fileA, initialValue: 'hello' }, undefined));
182
await timeout(1500);
183
184
d.applyEdit(StringEditWithReason.replace(d.findRange('hello'), 'world', chatEdit));
185
await timeout(1500);
186
187
assert.strictEqual(hasAiContributions([d.uri], 'all'), true);
188
189
d.dispose();
190
await timeout(1500);
191
192
assert.strictEqual(hasAiContributions([fileA], 'all'), false);
193
disposables.dispose();
194
}));
195
196
test('returns false for unknown URIs', () => runWithFakedTimers({}, async () => {
197
setup();
198
assert.strictEqual(hasAiContributions([URI.parse('file:///unknown.ts')], 'all'), false);
199
disposables.dispose();
200
}));
201
202
test('checks multiple resources', () => runWithFakedTimers({}, async () => {
203
setup();
204
const dA = disposables.add(workspace.createDocument({ uri: fileA, initialValue: 'hello' }, undefined));
205
disposables.add(workspace.createDocument({ uri: fileB, initialValue: 'world' }, undefined));
206
await timeout(1500);
207
208
dA.applyEdit(StringEditWithReason.replace(dA.findRange('hello'), 'foo', chatEdit));
209
await timeout(1500);
210
211
// Returns true if any of the resources has AI contributions
212
assert.strictEqual(hasAiContributions([fileA, fileB], 'all'), true);
213
assert.strictEqual(hasAiContributions([fileB, fileA], 'all'), true);
214
assert.strictEqual(hasAiContributions([fileB], 'all'), false);
215
disposables.dispose();
216
}));
217
});
218
219