Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/test/browser/chatEditingService.test.ts
3296 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 { CancellationToken } from '../../../../../base/common/cancellation.js';
8
import { Disposable, DisposableStore, IDisposable } from '../../../../../base/common/lifecycle.js';
9
import { waitForState } from '../../../../../base/common/observable.js';
10
import { isEqual } from '../../../../../base/common/resources.js';
11
import { assertType } from '../../../../../base/common/types.js';
12
import { URI } from '../../../../../base/common/uri.js';
13
import { mock } from '../../../../../base/test/common/mock.js';
14
import { assertThrowsAsync, ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
15
import { Range } from '../../../../../editor/common/core/range.js';
16
import { IModelService } from '../../../../../editor/common/services/model.js';
17
import { ITextModelService } from '../../../../../editor/common/services/resolverService.js';
18
import { SyncDescriptor } from '../../../../../platform/instantiation/common/descriptors.js';
19
import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js';
20
import { IWorkbenchAssignmentService } from '../../../../services/assignment/common/assignmentService.js';
21
import { NullWorkbenchAssignmentService } from '../../../../services/assignment/test/common/nullAssignmentService.js';
22
import { nullExtensionDescription } from '../../../../services/extensions/common/extensions.js';
23
import { workbenchInstantiationService } from '../../../../test/browser/workbenchTestServices.js';
24
import { IMultiDiffSourceResolver, IMultiDiffSourceResolverService } from '../../../multiDiffEditor/browser/multiDiffSourceResolverService.js';
25
import { NotebookTextModel } from '../../../notebook/common/model/notebookTextModel.js';
26
import { INotebookService } from '../../../notebook/common/notebookService.js';
27
import { ChatEditingService } from '../../browser/chatEditing/chatEditingServiceImpl.js';
28
import { ChatAgentService, IChatAgentData, IChatAgentImplementation, IChatAgentService } from '../../common/chatAgents.js';
29
import { ChatEditingSessionState, IChatEditingService, IChatEditingSession, ModifiedFileEntryState } from '../../common/chatEditingService.js';
30
import { IChatService } from '../../common/chatService.js';
31
import { ChatService } from '../../common/chatServiceImpl.js';
32
import { IChatSlashCommandService } from '../../common/chatSlashCommands.js';
33
import { ChatTransferService, IChatTransferService } from '../../common/chatTransferService.js';
34
import { IChatVariablesService } from '../../common/chatVariables.js';
35
import { ChatAgentLocation, ChatModeKind } from '../../common/constants.js';
36
import { ILanguageModelsService } from '../../common/languageModels.js';
37
import { NullLanguageModelsService } from '../common/languageModels.js';
38
import { MockChatVariablesService } from '../common/mockChatVariables.js';
39
import { IEditorWorkerService } from '../../../../../editor/common/services/editorWorker.js';
40
import { TestWorkerService } from '../../../inlineChat/test/browser/testWorkerService.js';
41
import { EditOperation } from '../../../../../editor/common/core/editOperation.js';
42
import { Position } from '../../../../../editor/common/core/position.js';
43
import { ChatModel } from '../../common/chatModel.js';
44
import { TextEdit } from '../../../../../editor/common/languages.js';
45
import { IMcpService } from '../../../mcp/common/mcpTypes.js';
46
import { TestMcpService } from '../../../mcp/test/common/testMcpService.js';
47
import { IChatSessionsService } from '../../common/chatSessionsService.js';
48
import { ChatSessionsService } from '../../browser/chatSessions.contribution.js';
49
50
function getAgentData(id: string): IChatAgentData {
51
return {
52
name: id,
53
id: id,
54
extensionId: nullExtensionDescription.identifier,
55
extensionVersion: undefined,
56
extensionPublisherId: '',
57
publisherDisplayName: '',
58
extensionDisplayName: '',
59
locations: [ChatAgentLocation.Panel],
60
modes: [ChatModeKind.Ask],
61
metadata: {},
62
slashCommands: [],
63
disambiguation: [],
64
};
65
}
66
67
suite('ChatEditingService', function () {
68
69
const store = new DisposableStore();
70
let editingService: ChatEditingService;
71
let chatService: IChatService;
72
let textModelService: ITextModelService;
73
74
setup(function () {
75
const collection = new ServiceCollection();
76
collection.set(IWorkbenchAssignmentService, new NullWorkbenchAssignmentService());
77
collection.set(IChatAgentService, new SyncDescriptor(ChatAgentService));
78
collection.set(IChatVariablesService, new MockChatVariablesService());
79
collection.set(IChatSlashCommandService, new class extends mock<IChatSlashCommandService>() { });
80
collection.set(IChatTransferService, new SyncDescriptor(ChatTransferService));
81
collection.set(IChatSessionsService, new SyncDescriptor(ChatSessionsService));
82
collection.set(IChatEditingService, new SyncDescriptor(ChatEditingService));
83
collection.set(IEditorWorkerService, new SyncDescriptor(TestWorkerService));
84
collection.set(IChatService, new SyncDescriptor(ChatService));
85
collection.set(IMcpService, new TestMcpService());
86
collection.set(ILanguageModelsService, new SyncDescriptor(NullLanguageModelsService));
87
collection.set(IMultiDiffSourceResolverService, new class extends mock<IMultiDiffSourceResolverService>() {
88
override registerResolver(_resolver: IMultiDiffSourceResolver): IDisposable {
89
return Disposable.None;
90
}
91
});
92
collection.set(INotebookService, new class extends mock<INotebookService>() {
93
override getNotebookTextModel(_uri: URI): NotebookTextModel | undefined {
94
return undefined;
95
}
96
override hasSupportedNotebooks(_resource: URI): boolean {
97
return false;
98
}
99
});
100
const insta = store.add(store.add(workbenchInstantiationService(undefined, store)).createChild(collection));
101
store.add(insta.get(IEditorWorkerService) as TestWorkerService);
102
const value = insta.get(IChatEditingService);
103
assert.ok(value instanceof ChatEditingService);
104
editingService = value;
105
106
chatService = insta.get(IChatService);
107
108
store.add(insta.get(IChatSessionsService) as ChatSessionsService); // Needs to be disposed in between test runs to clear extensionPoint contribution
109
110
const chatAgentService = insta.get(IChatAgentService);
111
112
const agent: IChatAgentImplementation = {
113
async invoke(request, progress, history, token) {
114
return {};
115
},
116
};
117
store.add(chatAgentService.registerAgent('testAgent', { ...getAgentData('testAgent'), isDefault: true }));
118
store.add(chatAgentService.registerAgentImplementation('testAgent', agent));
119
120
textModelService = insta.get(ITextModelService);
121
122
const modelService = insta.get(IModelService);
123
124
store.add(textModelService.registerTextModelContentProvider('test', {
125
async provideTextContent(resource) {
126
return store.add(modelService.createModel(resource.path.repeat(10), null, resource, false));
127
},
128
}));
129
});
130
131
teardown(() => {
132
store.clear();
133
});
134
135
ensureNoDisposablesAreLeakedInTestSuite();
136
137
test('create session', async function () {
138
assert.ok(editingService);
139
140
const model = chatService.startSession(ChatAgentLocation.Panel, CancellationToken.None);
141
const session = await editingService.createEditingSession(model, true);
142
143
assert.strictEqual(session.chatSessionId, model.sessionId);
144
assert.strictEqual(session.isGlobalEditingSession, true);
145
146
await assertThrowsAsync(async () => {
147
// DUPE not allowed
148
await editingService.createEditingSession(model);
149
});
150
151
session.dispose();
152
model.dispose();
153
});
154
155
test('create session, file entry & isCurrentlyBeingModifiedBy', async function () {
156
assert.ok(editingService);
157
158
const uri = URI.from({ scheme: 'test', path: 'HelloWorld' });
159
160
const model = chatService.startSession(ChatAgentLocation.Panel, CancellationToken.None);
161
const session = await model.editingSessionObs?.promise;
162
if (!session) {
163
assert.fail('session not created');
164
}
165
166
const chatRequest = model?.addRequest({ text: '', parts: [] }, { variables: [] }, 0);
167
assertType(chatRequest.response);
168
chatRequest.response.updateContent({ kind: 'textEdit', uri, edits: [], done: false });
169
chatRequest.response.updateContent({ kind: 'textEdit', uri, edits: [{ range: new Range(1, 1, 1, 1), text: 'FarBoo\n' }], done: false });
170
chatRequest.response.updateContent({ kind: 'textEdit', uri, edits: [], done: true });
171
172
const entry = await waitForState(session.entries.map(value => value.find(a => isEqual(a.modifiedURI, uri))));
173
174
assert.ok(isEqual(entry.modifiedURI, uri));
175
176
await waitForState(entry.isCurrentlyBeingModifiedBy.map(value => value === chatRequest.response));
177
assert.ok(entry.isCurrentlyBeingModifiedBy.get() === chatRequest.response);
178
179
const unset = waitForState(entry.isCurrentlyBeingModifiedBy.map(res => res === undefined));
180
181
chatRequest.response.complete();
182
183
await unset;
184
185
await entry.reject();
186
187
model.dispose();
188
});
189
190
async function idleAfterEdit(session: IChatEditingSession, model: ChatModel, uri: URI, edits: TextEdit[]) {
191
const isStreaming = waitForState(session.state.map(s => s === ChatEditingSessionState.StreamingEdits), Boolean);
192
193
const chatRequest = model.addRequest({ text: '', parts: [] }, { variables: [] }, 0);
194
assertType(chatRequest.response);
195
196
chatRequest.response.updateContent({ kind: 'textEdit', uri, edits, done: true });
197
198
const entry = await waitForState(session.entries.map(value => value.find(a => isEqual(a.modifiedURI, uri))));
199
200
assert.ok(isEqual(entry.modifiedURI, uri));
201
202
chatRequest.response.complete();
203
204
await isStreaming;
205
206
const isIdle = waitForState(session.state.map(s => s === ChatEditingSessionState.Idle), Boolean);
207
await isIdle;
208
209
return entry;
210
}
211
212
test('mirror typing outside -> accept', async function () {
213
assert.ok(editingService);
214
215
const uri = URI.from({ scheme: 'test', path: 'abc\n' });
216
217
const model = store.add(chatService.startSession(ChatAgentLocation.Panel, CancellationToken.None));
218
const session = await model.editingSessionObs?.promise;
219
assertType(session, 'session not created');
220
221
const entry = await idleAfterEdit(session, model, uri, [{ range: new Range(1, 1, 1, 1), text: 'FarBoo\n' }]);
222
const original = store.add(await textModelService.createModelReference(entry.originalURI)).object.textEditorModel;
223
const modified = store.add(await textModelService.createModelReference(entry.modifiedURI)).object.textEditorModel;
224
225
assert.strictEqual(entry.state.get(), ModifiedFileEntryState.Modified);
226
227
assert.strictEqual(original.getValue(), 'abc\n'.repeat(10));
228
assert.strictEqual(modified.getValue(), 'FarBoo\n' + 'abc\n'.repeat(10));
229
230
modified.pushEditOperations(null, [EditOperation.insert(new Position(3, 1), 'USER_TYPE\n')], () => null);
231
232
assert.ok(modified.getValue().includes('USER_TYPE'));
233
assert.ok(original.getValue().includes('USER_TYPE'));
234
235
await entry.accept();
236
assert.strictEqual(modified.getValue(), original.getValue());
237
assert.strictEqual(entry.state.get(), ModifiedFileEntryState.Accepted);
238
239
assert.ok(modified.getValue().includes('FarBoo'));
240
assert.ok(original.getValue().includes('FarBoo'));
241
});
242
243
test('mirror typing outside -> reject', async function () {
244
assert.ok(editingService);
245
246
const uri = URI.from({ scheme: 'test', path: 'abc\n' });
247
248
const model = store.add(chatService.startSession(ChatAgentLocation.Panel, CancellationToken.None));
249
const session = await model.editingSessionObs?.promise;
250
assertType(session, 'session not created');
251
252
const entry = await idleAfterEdit(session, model, uri, [{ range: new Range(1, 1, 1, 1), text: 'FarBoo\n' }]);
253
const original = store.add(await textModelService.createModelReference(entry.originalURI)).object.textEditorModel;
254
const modified = store.add(await textModelService.createModelReference(entry.modifiedURI)).object.textEditorModel;
255
256
assert.strictEqual(entry.state.get(), ModifiedFileEntryState.Modified);
257
258
assert.strictEqual(original.getValue(), 'abc\n'.repeat(10));
259
assert.strictEqual(modified.getValue(), 'FarBoo\n' + 'abc\n'.repeat(10));
260
261
modified.pushEditOperations(null, [EditOperation.insert(new Position(3, 1), 'USER_TYPE\n')], () => null);
262
263
assert.ok(modified.getValue().includes('USER_TYPE'));
264
assert.ok(original.getValue().includes('USER_TYPE'));
265
266
await entry.reject();
267
assert.strictEqual(modified.getValue(), original.getValue());
268
assert.strictEqual(entry.state.get(), ModifiedFileEntryState.Rejected);
269
270
assert.ok(!modified.getValue().includes('FarBoo'));
271
assert.ok(!original.getValue().includes('FarBoo'));
272
});
273
274
test('NO mirror typing inside -> accept', async function () {
275
assert.ok(editingService);
276
277
const uri = URI.from({ scheme: 'test', path: 'abc\n' });
278
279
const model = store.add(chatService.startSession(ChatAgentLocation.Panel, CancellationToken.None));
280
const session = await model.editingSessionObs?.promise;
281
assertType(session, 'session not created');
282
283
const entry = await idleAfterEdit(session, model, uri, [{ range: new Range(1, 1, 1, 1), text: 'FarBoo\n' }]);
284
const original = store.add(await textModelService.createModelReference(entry.originalURI)).object.textEditorModel;
285
const modified = store.add(await textModelService.createModelReference(entry.modifiedURI)).object.textEditorModel;
286
287
assert.strictEqual(entry.state.get(), ModifiedFileEntryState.Modified);
288
289
assert.strictEqual(original.getValue(), 'abc\n'.repeat(10));
290
assert.strictEqual(modified.getValue(), 'FarBoo\n' + 'abc\n'.repeat(10));
291
292
modified.pushEditOperations(null, [EditOperation.replace(new Range(1, 2, 1, 7), 'ooBar')], () => null);
293
294
assert.ok(modified.getValue().includes('FooBar'));
295
assert.ok(!original.getValue().includes('FooBar')); // typed in the AI edits, DO NOT transpose
296
297
await entry.accept();
298
assert.strictEqual(modified.getValue(), original.getValue());
299
assert.strictEqual(entry.state.get(), ModifiedFileEntryState.Accepted);
300
301
assert.ok(modified.getValue().includes('FooBar'));
302
assert.ok(original.getValue().includes('FooBar'));
303
});
304
305
});
306
307