Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/test/common/chatService.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 { Event } from '../../../../../base/common/event.js';
9
import { MarkdownString } from '../../../../../base/common/htmlContent.js';
10
import { Disposable } from '../../../../../base/common/lifecycle.js';
11
import { URI } from '../../../../../base/common/uri.js';
12
import { assertSnapshot } from '../../../../../base/test/common/snapshot.js';
13
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
14
import { Range } from '../../../../../editor/common/core/range.js';
15
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
16
import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js';
17
import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';
18
import { IEnvironmentService } from '../../../../../platform/environment/common/environment.js';
19
import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js';
20
import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js';
21
import { MockContextKeyService } from '../../../../../platform/keybinding/test/common/mockKeybindingService.js';
22
import { ILogService, NullLogService } from '../../../../../platform/log/common/log.js';
23
import { IStorageService } from '../../../../../platform/storage/common/storage.js';
24
import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';
25
import { NullTelemetryService } from '../../../../../platform/telemetry/common/telemetryUtils.js';
26
import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js';
27
import { IWorkbenchAssignmentService } from '../../../../services/assignment/common/assignmentService.js';
28
import { NullWorkbenchAssignmentService } from '../../../../services/assignment/test/common/nullAssignmentService.js';
29
import { IExtensionService, nullExtensionDescription } from '../../../../services/extensions/common/extensions.js';
30
import { ILifecycleService } from '../../../../services/lifecycle/common/lifecycle.js';
31
import { IViewsService } from '../../../../services/views/common/viewsService.js';
32
import { mock, TestContextService, TestExtensionService, TestStorageService } from '../../../../test/common/workbenchTestServices.js';
33
import { ChatAgentService, IChatAgent, IChatAgentData, IChatAgentImplementation, IChatAgentService } from '../../common/chatAgents.js';
34
import { IChatEditingService, IChatEditingSession } from '../../common/chatEditingService.js';
35
import { IChatModel, ISerializableChatData } from '../../common/chatModel.js';
36
import { IChatFollowup, IChatService } from '../../common/chatService.js';
37
import { ChatService } from '../../common/chatServiceImpl.js';
38
import { ChatSlashCommandService, IChatSlashCommandService } from '../../common/chatSlashCommands.js';
39
import { IChatVariablesService } from '../../common/chatVariables.js';
40
import { ChatAgentLocation, ChatModeKind } from '../../common/constants.js';
41
import { MockChatService } from './mockChatService.js';
42
import { MockChatVariablesService } from './mockChatVariables.js';
43
import { IMcpService } from '../../../mcp/common/mcpTypes.js';
44
import { TestMcpService } from '../../../mcp/test/common/testMcpService.js';
45
46
const chatAgentWithUsedContextId = 'ChatProviderWithUsedContext';
47
const chatAgentWithUsedContext: IChatAgent = {
48
id: chatAgentWithUsedContextId,
49
name: chatAgentWithUsedContextId,
50
extensionId: nullExtensionDescription.identifier,
51
extensionVersion: undefined,
52
publisherDisplayName: '',
53
extensionPublisherId: '',
54
extensionDisplayName: '',
55
locations: [ChatAgentLocation.Panel],
56
modes: [ChatModeKind.Ask],
57
metadata: {},
58
slashCommands: [],
59
disambiguation: [],
60
async invoke(request, progress, history, token) {
61
progress([{
62
documents: [
63
{
64
uri: URI.file('/test/path/to/file'),
65
version: 3,
66
ranges: [
67
new Range(1, 1, 2, 2)
68
]
69
}
70
],
71
kind: 'usedContext'
72
}]);
73
74
return { metadata: { metadataKey: 'value' } };
75
},
76
async provideFollowups(sessionId, token) {
77
return [{ kind: 'reply', message: 'Something else', agentId: '', tooltip: 'a tooltip' } satisfies IChatFollowup];
78
},
79
};
80
81
const chatAgentWithMarkdownId = 'ChatProviderWithMarkdown';
82
const chatAgentWithMarkdown: IChatAgent = {
83
id: chatAgentWithMarkdownId,
84
name: chatAgentWithMarkdownId,
85
extensionId: nullExtensionDescription.identifier,
86
extensionVersion: undefined,
87
publisherDisplayName: '',
88
extensionPublisherId: '',
89
extensionDisplayName: '',
90
locations: [ChatAgentLocation.Panel],
91
modes: [ChatModeKind.Ask],
92
metadata: {},
93
slashCommands: [],
94
disambiguation: [],
95
async invoke(request, progress, history, token) {
96
progress([{ kind: 'markdownContent', content: new MarkdownString('test') }]);
97
return { metadata: { metadataKey: 'value' } };
98
},
99
async provideFollowups(sessionId, token) {
100
return [];
101
},
102
};
103
104
function getAgentData(id: string): IChatAgentData {
105
return {
106
name: id,
107
id: id,
108
extensionId: nullExtensionDescription.identifier,
109
extensionVersion: undefined,
110
extensionPublisherId: '',
111
publisherDisplayName: '',
112
extensionDisplayName: '',
113
locations: [ChatAgentLocation.Panel],
114
modes: [ChatModeKind.Ask],
115
metadata: {},
116
slashCommands: [],
117
disambiguation: [],
118
};
119
}
120
121
suite('ChatService', () => {
122
const testDisposables = ensureNoDisposablesAreLeakedInTestSuite();
123
124
let storageService: IStorageService;
125
let instantiationService: TestInstantiationService;
126
127
let chatAgentService: IChatAgentService;
128
129
setup(async () => {
130
instantiationService = testDisposables.add(new TestInstantiationService(new ServiceCollection(
131
[IChatVariablesService, new MockChatVariablesService()],
132
[IWorkbenchAssignmentService, new NullWorkbenchAssignmentService()],
133
[IMcpService, new TestMcpService()],
134
)));
135
instantiationService.stub(IStorageService, storageService = testDisposables.add(new TestStorageService()));
136
instantiationService.stub(ILogService, new NullLogService());
137
instantiationService.stub(ITelemetryService, NullTelemetryService);
138
instantiationService.stub(IExtensionService, new TestExtensionService());
139
instantiationService.stub(IContextKeyService, new MockContextKeyService());
140
instantiationService.stub(IViewsService, new TestExtensionService());
141
instantiationService.stub(IWorkspaceContextService, new TestContextService());
142
instantiationService.stub(IChatSlashCommandService, testDisposables.add(instantiationService.createInstance(ChatSlashCommandService)));
143
instantiationService.stub(IConfigurationService, new TestConfigurationService());
144
instantiationService.stub(IChatService, new MockChatService());
145
instantiationService.stub(IEnvironmentService, { workspaceStorageHome: URI.file('/test/path/to/workspaceStorage') });
146
instantiationService.stub(ILifecycleService, { onWillShutdown: Event.None });
147
instantiationService.stub(IChatEditingService, new class extends mock<IChatEditingService>() {
148
override startOrContinueGlobalEditingSession(): Promise<IChatEditingSession> {
149
return Promise.resolve(Disposable.None as IChatEditingSession);
150
}
151
});
152
153
chatAgentService = testDisposables.add(instantiationService.createInstance(ChatAgentService));
154
instantiationService.stub(IChatAgentService, chatAgentService);
155
156
const agent: IChatAgentImplementation = {
157
async invoke(request, progress, history, token) {
158
return {};
159
},
160
};
161
testDisposables.add(chatAgentService.registerAgent('testAgent', { ...getAgentData('testAgent'), isDefault: true }));
162
testDisposables.add(chatAgentService.registerAgent(chatAgentWithUsedContextId, getAgentData(chatAgentWithUsedContextId)));
163
testDisposables.add(chatAgentService.registerAgent(chatAgentWithMarkdownId, getAgentData(chatAgentWithMarkdownId)));
164
testDisposables.add(chatAgentService.registerAgentImplementation('testAgent', agent));
165
chatAgentService.updateAgent('testAgent', { requester: { name: 'test' } });
166
});
167
168
test('retrieveSession', async () => {
169
const testService = testDisposables.add(instantiationService.createInstance(ChatService));
170
const session1 = testDisposables.add(testService.startSession(ChatAgentLocation.Panel, CancellationToken.None));
171
session1.addRequest({ parts: [], text: 'request 1' }, { variables: [] }, 0);
172
173
const session2 = testDisposables.add(testService.startSession(ChatAgentLocation.Panel, CancellationToken.None));
174
session2.addRequest({ parts: [], text: 'request 2' }, { variables: [] }, 0);
175
176
storageService.flush();
177
const testService2 = testDisposables.add(instantiationService.createInstance(ChatService));
178
const retrieved1 = testDisposables.add((await testService2.getOrRestoreSession(session1.sessionId))!);
179
const retrieved2 = testDisposables.add((await testService2.getOrRestoreSession(session2.sessionId))!);
180
assert.deepStrictEqual(retrieved1.getRequests()[0]?.message.text, 'request 1');
181
assert.deepStrictEqual(retrieved2.getRequests()[0]?.message.text, 'request 2');
182
});
183
184
test('addCompleteRequest', async () => {
185
const testService = testDisposables.add(instantiationService.createInstance(ChatService));
186
187
const model = testDisposables.add(testService.startSession(ChatAgentLocation.Panel, CancellationToken.None));
188
assert.strictEqual(model.getRequests().length, 0);
189
190
await testService.addCompleteRequest(model.sessionId, 'test request', undefined, 0, { message: 'test response' });
191
assert.strictEqual(model.getRequests().length, 1);
192
assert.ok(model.getRequests()[0].response);
193
assert.strictEqual(model.getRequests()[0].response?.response.toString(), 'test response');
194
});
195
196
test('sendRequest fails', async () => {
197
const testService = testDisposables.add(instantiationService.createInstance(ChatService));
198
199
const model = testDisposables.add(testService.startSession(ChatAgentLocation.Panel, CancellationToken.None));
200
const response = await testService.sendRequest(model.sessionId, `@${chatAgentWithUsedContextId} test request`);
201
assert(response);
202
await response.responseCompletePromise;
203
204
await assertSnapshot(toSnapshotExportData(model));
205
});
206
207
test('history', async () => {
208
const historyLengthAgent: IChatAgentImplementation = {
209
async invoke(request, progress, history, token) {
210
return {
211
metadata: { historyLength: history.length }
212
};
213
},
214
};
215
216
testDisposables.add(chatAgentService.registerAgent('defaultAgent', { ...getAgentData('defaultAgent'), isDefault: true }));
217
testDisposables.add(chatAgentService.registerAgent('agent2', getAgentData('agent2')));
218
testDisposables.add(chatAgentService.registerAgentImplementation('defaultAgent', historyLengthAgent));
219
testDisposables.add(chatAgentService.registerAgentImplementation('agent2', historyLengthAgent));
220
221
const testService = testDisposables.add(instantiationService.createInstance(ChatService));
222
const model = testDisposables.add(testService.startSession(ChatAgentLocation.Panel, CancellationToken.None));
223
224
// Send a request to default agent
225
const response = await testService.sendRequest(model.sessionId, `test request`, { agentId: 'defaultAgent' });
226
assert(response);
227
await response.responseCompletePromise;
228
assert.strictEqual(model.getRequests().length, 1);
229
assert.strictEqual(model.getRequests()[0].response?.result?.metadata?.historyLength, 0);
230
231
// Send a request to agent2- it can't see the default agent's message
232
const response2 = await testService.sendRequest(model.sessionId, `test request`, { agentId: 'agent2' });
233
assert(response2);
234
await response2.responseCompletePromise;
235
assert.strictEqual(model.getRequests().length, 2);
236
assert.strictEqual(model.getRequests()[1].response?.result?.metadata?.historyLength, 0);
237
238
// Send a request to defaultAgent - the default agent can see agent2's message
239
const response3 = await testService.sendRequest(model.sessionId, `test request`, { agentId: 'defaultAgent' });
240
assert(response3);
241
await response3.responseCompletePromise;
242
assert.strictEqual(model.getRequests().length, 3);
243
assert.strictEqual(model.getRequests()[2].response?.result?.metadata?.historyLength, 2);
244
});
245
246
test('can serialize', async () => {
247
testDisposables.add(chatAgentService.registerAgentImplementation(chatAgentWithUsedContextId, chatAgentWithUsedContext));
248
chatAgentService.updateAgent(chatAgentWithUsedContextId, { requester: { name: 'test' } });
249
const testService = testDisposables.add(instantiationService.createInstance(ChatService));
250
251
const model = testDisposables.add(testService.startSession(ChatAgentLocation.Panel, CancellationToken.None));
252
assert.strictEqual(model.getRequests().length, 0);
253
254
await assertSnapshot(toSnapshotExportData(model));
255
256
const response = await testService.sendRequest(model.sessionId, `@${chatAgentWithUsedContextId} test request`);
257
assert(response);
258
await response.responseCompletePromise;
259
assert.strictEqual(model.getRequests().length, 1);
260
261
const response2 = await testService.sendRequest(model.sessionId, `test request 2`);
262
assert(response2);
263
await response2.responseCompletePromise;
264
assert.strictEqual(model.getRequests().length, 2);
265
266
await assertSnapshot(toSnapshotExportData(model));
267
});
268
269
test('can deserialize', async () => {
270
let serializedChatData: ISerializableChatData;
271
testDisposables.add(chatAgentService.registerAgentImplementation(chatAgentWithUsedContextId, chatAgentWithUsedContext));
272
273
// create the first service, send request, get response, and serialize the state
274
{ // serapate block to not leak variables in outer scope
275
const testService = testDisposables.add(instantiationService.createInstance(ChatService));
276
277
const chatModel1 = testDisposables.add(testService.startSession(ChatAgentLocation.Panel, CancellationToken.None));
278
assert.strictEqual(chatModel1.getRequests().length, 0);
279
280
const response = await testService.sendRequest(chatModel1.sessionId, `@${chatAgentWithUsedContextId} test request`);
281
assert(response);
282
283
await response.responseCompletePromise;
284
285
serializedChatData = JSON.parse(JSON.stringify(chatModel1));
286
}
287
288
// try deserializing the state into a new service
289
290
const testService2 = testDisposables.add(instantiationService.createInstance(ChatService));
291
292
const chatModel2 = testService2.loadSessionFromContent(serializedChatData);
293
assert(chatModel2);
294
295
await assertSnapshot(toSnapshotExportData(chatModel2));
296
chatModel2.dispose();
297
});
298
299
test('can deserialize with response', async () => {
300
let serializedChatData: ISerializableChatData;
301
testDisposables.add(chatAgentService.registerAgentImplementation(chatAgentWithMarkdownId, chatAgentWithMarkdown));
302
303
{
304
const testService = testDisposables.add(instantiationService.createInstance(ChatService));
305
306
const chatModel1 = testDisposables.add(testService.startSession(ChatAgentLocation.Panel, CancellationToken.None));
307
assert.strictEqual(chatModel1.getRequests().length, 0);
308
309
const response = await testService.sendRequest(chatModel1.sessionId, `@${chatAgentWithUsedContextId} test request`);
310
assert(response);
311
312
await response.responseCompletePromise;
313
314
serializedChatData = JSON.parse(JSON.stringify(chatModel1));
315
}
316
317
// try deserializing the state into a new service
318
319
const testService2 = testDisposables.add(instantiationService.createInstance(ChatService));
320
321
const chatModel2 = testService2.loadSessionFromContent(serializedChatData);
322
assert(chatModel2);
323
324
await assertSnapshot(toSnapshotExportData(chatModel2));
325
chatModel2.dispose();
326
});
327
});
328
329
330
function toSnapshotExportData(model: IChatModel) {
331
const exp = model.toExport();
332
return {
333
...exp,
334
requests: exp.requests.map(r => {
335
return {
336
...r,
337
timestamp: undefined,
338
requestId: undefined, // id contains a random part
339
responseId: undefined, // id contains a random part
340
};
341
})
342
};
343
}
344
345