Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/test/common/chatModel.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 { MarkdownString } from '../../../../../base/common/htmlContent.js';
8
import { URI } from '../../../../../base/common/uri.js';
9
import { assertSnapshot } from '../../../../../base/test/common/snapshot.js';
10
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
11
import { Range } from '../../../../../editor/common/core/range.js';
12
import { OffsetRange } from '../../../../../editor/common/core/ranges/offsetRange.js';
13
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
14
import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js';
15
import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';
16
import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js';
17
import { MockContextKeyService } from '../../../../../platform/keybinding/test/common/mockKeybindingService.js';
18
import { ILogService, NullLogService } from '../../../../../platform/log/common/log.js';
19
import { IStorageService } from '../../../../../platform/storage/common/storage.js';
20
import { IExtensionService } from '../../../../services/extensions/common/extensions.js';
21
import { TestExtensionService, TestStorageService } from '../../../../test/common/workbenchTestServices.js';
22
import { ChatAgentService, IChatAgentService } from '../../common/chatAgents.js';
23
import { ChatModel, ISerializableChatData1, ISerializableChatData2, ISerializableChatData3, normalizeSerializableChatData, Response } from '../../common/chatModel.js';
24
import { ChatRequestTextPart } from '../../common/chatParserTypes.js';
25
import { ChatAgentLocation } from '../../common/constants.js';
26
27
suite('ChatModel', () => {
28
const testDisposables = ensureNoDisposablesAreLeakedInTestSuite();
29
30
let instantiationService: TestInstantiationService;
31
32
setup(async () => {
33
instantiationService = testDisposables.add(new TestInstantiationService());
34
instantiationService.stub(IStorageService, testDisposables.add(new TestStorageService()));
35
instantiationService.stub(ILogService, new NullLogService());
36
instantiationService.stub(IExtensionService, new TestExtensionService());
37
instantiationService.stub(IContextKeyService, new MockContextKeyService());
38
instantiationService.stub(IChatAgentService, testDisposables.add(instantiationService.createInstance(ChatAgentService)));
39
instantiationService.stub(IConfigurationService, new TestConfigurationService());
40
});
41
42
test('removeRequest', async () => {
43
const model = testDisposables.add(instantiationService.createInstance(ChatModel, undefined, ChatAgentLocation.Panel));
44
45
const text = 'hello';
46
model.addRequest({ text, parts: [new ChatRequestTextPart(new OffsetRange(0, text.length), new Range(1, text.length, 1, text.length), text)] }, { variables: [] }, 0);
47
const requests = model.getRequests();
48
assert.strictEqual(requests.length, 1);
49
50
model.removeRequest(requests[0].id);
51
assert.strictEqual(model.getRequests().length, 0);
52
});
53
54
test('adoptRequest', async function () {
55
const model1 = testDisposables.add(instantiationService.createInstance(ChatModel, undefined, ChatAgentLocation.Editor));
56
const model2 = testDisposables.add(instantiationService.createInstance(ChatModel, undefined, ChatAgentLocation.Panel));
57
58
const text = 'hello';
59
const request1 = model1.addRequest({ text, parts: [new ChatRequestTextPart(new OffsetRange(0, text.length), new Range(1, text.length, 1, text.length), text)] }, { variables: [] }, 0);
60
61
assert.strictEqual(model1.getRequests().length, 1);
62
assert.strictEqual(model2.getRequests().length, 0);
63
assert.ok(request1.session === model1);
64
assert.ok(request1.response?.session === model1);
65
66
model2.adoptRequest(request1);
67
68
assert.strictEqual(model1.getRequests().length, 0);
69
assert.strictEqual(model2.getRequests().length, 1);
70
assert.ok(request1.session === model2);
71
assert.ok(request1.response?.session === model2);
72
73
model2.acceptResponseProgress(request1, { content: new MarkdownString('Hello'), kind: 'markdownContent' });
74
75
assert.strictEqual(request1.response.response.toString(), 'Hello');
76
});
77
78
test('addCompleteRequest', async function () {
79
const model1 = testDisposables.add(instantiationService.createInstance(ChatModel, undefined, ChatAgentLocation.Panel));
80
81
const text = 'hello';
82
const request1 = model1.addRequest({ text, parts: [new ChatRequestTextPart(new OffsetRange(0, text.length), new Range(1, text.length, 1, text.length), text)] }, { variables: [] }, 0, undefined, undefined, undefined, undefined, undefined, undefined, true);
83
84
assert.strictEqual(request1.isCompleteAddedRequest, true);
85
assert.strictEqual(request1.response!.isCompleteAddedRequest, true);
86
assert.strictEqual(request1.shouldBeRemovedOnSend, undefined);
87
assert.strictEqual(request1.response!.shouldBeRemovedOnSend, undefined);
88
});
89
});
90
91
suite('Response', () => {
92
const store = ensureNoDisposablesAreLeakedInTestSuite();
93
94
test('mergeable markdown', async () => {
95
const response = store.add(new Response([]));
96
response.updateContent({ content: new MarkdownString('markdown1'), kind: 'markdownContent' });
97
response.updateContent({ content: new MarkdownString('markdown2'), kind: 'markdownContent' });
98
await assertSnapshot(response.value);
99
100
assert.strictEqual(response.toString(), 'markdown1markdown2');
101
});
102
103
test('not mergeable markdown', async () => {
104
const response = store.add(new Response([]));
105
const md1 = new MarkdownString('markdown1');
106
md1.supportHtml = true;
107
response.updateContent({ content: md1, kind: 'markdownContent' });
108
response.updateContent({ content: new MarkdownString('markdown2'), kind: 'markdownContent' });
109
await assertSnapshot(response.value);
110
});
111
112
test('inline reference', async () => {
113
const response = store.add(new Response([]));
114
response.updateContent({ content: new MarkdownString('text before '), kind: 'markdownContent' });
115
response.updateContent({ inlineReference: URI.parse('https://microsoft.com/'), kind: 'inlineReference' });
116
response.updateContent({ content: new MarkdownString(' text after'), kind: 'markdownContent' });
117
await assertSnapshot(response.value);
118
119
assert.strictEqual(response.toString(), 'text before https://microsoft.com/ text after');
120
121
});
122
123
test('consolidated edit summary', async () => {
124
const response = store.add(new Response([]));
125
response.updateContent({ content: new MarkdownString('Some content before edits'), kind: 'markdownContent' });
126
response.updateContent({ kind: 'textEditGroup', uri: URI.parse('file:///file1.ts'), edits: [], state: undefined, done: true });
127
response.updateContent({ kind: 'textEditGroup', uri: URI.parse('file:///file2.ts'), edits: [], state: undefined, done: true });
128
response.updateContent({ content: new MarkdownString('Some content after edits'), kind: 'markdownContent' });
129
130
// Should have single "Made changes." at the end instead of multiple entries
131
const responseString = response.toString();
132
const madeChangesCount = (responseString.match(/Made changes\./g) || []).length;
133
assert.strictEqual(madeChangesCount, 1, 'Should have exactly one "Made changes." message');
134
assert.ok(responseString.includes('Some content before edits'), 'Should include content before edits');
135
assert.ok(responseString.includes('Some content after edits'), 'Should include content after edits');
136
assert.ok(responseString.endsWith('Made changes.'), 'Should end with "Made changes."');
137
});
138
139
test('no edit summary when no edits', async () => {
140
const response = store.add(new Response([]));
141
response.updateContent({ content: new MarkdownString('Some content'), kind: 'markdownContent' });
142
response.updateContent({ content: new MarkdownString('More content'), kind: 'markdownContent' });
143
144
// Should not have "Made changes." when there are no edit groups
145
const responseString = response.toString();
146
assert.ok(!responseString.includes('Made changes.'), 'Should not include "Made changes." when no edits present');
147
assert.strictEqual(responseString, 'Some contentMore content');
148
});
149
150
test('consolidated edit summary with clear operation', async () => {
151
const response = store.add(new Response([]));
152
response.updateContent({ content: new MarkdownString('Initial content'), kind: 'markdownContent' });
153
response.updateContent({ kind: 'textEditGroup', uri: URI.parse('file:///file1.ts'), edits: [], state: undefined, done: true });
154
response.updateContent({ kind: 'clearToPreviousToolInvocation', reason: 1 });
155
response.updateContent({ content: new MarkdownString('Content after clear'), kind: 'markdownContent' });
156
response.updateContent({ kind: 'textEditGroup', uri: URI.parse('file:///file2.ts'), edits: [], state: undefined, done: true });
157
158
// Should only show "Made changes." for edits after the clear operation
159
const responseString = response.toString();
160
const madeChangesCount = (responseString.match(/Made changes\./g) || []).length;
161
assert.strictEqual(madeChangesCount, 1, 'Should have exactly one "Made changes." message after clear');
162
assert.ok(responseString.includes('Content after clear'), 'Should include content after clear');
163
assert.ok(!responseString.includes('Initial content'), 'Should not include content before clear');
164
assert.ok(responseString.endsWith('Made changes.'), 'Should end with "Made changes."');
165
});
166
});
167
168
suite('normalizeSerializableChatData', () => {
169
ensureNoDisposablesAreLeakedInTestSuite();
170
171
test('v1', () => {
172
const v1Data: ISerializableChatData1 = {
173
creationDate: Date.now(),
174
initialLocation: undefined,
175
isImported: false,
176
requesterAvatarIconUri: undefined,
177
requesterUsername: 'me',
178
requests: [],
179
responderAvatarIconUri: undefined,
180
responderUsername: 'bot',
181
sessionId: 'session1',
182
};
183
184
const newData = normalizeSerializableChatData(v1Data);
185
assert.strictEqual(newData.creationDate, v1Data.creationDate);
186
assert.strictEqual(newData.lastMessageDate, v1Data.creationDate);
187
assert.strictEqual(newData.version, 3);
188
assert.ok('customTitle' in newData);
189
});
190
191
test('v2', () => {
192
const v2Data: ISerializableChatData2 = {
193
version: 2,
194
creationDate: 100,
195
lastMessageDate: Date.now(),
196
initialLocation: undefined,
197
isImported: false,
198
requesterAvatarIconUri: undefined,
199
requesterUsername: 'me',
200
requests: [],
201
responderAvatarIconUri: undefined,
202
responderUsername: 'bot',
203
sessionId: 'session1',
204
computedTitle: 'computed title'
205
};
206
207
const newData = normalizeSerializableChatData(v2Data);
208
assert.strictEqual(newData.version, 3);
209
assert.strictEqual(newData.creationDate, v2Data.creationDate);
210
assert.strictEqual(newData.lastMessageDate, v2Data.lastMessageDate);
211
assert.strictEqual(newData.customTitle, v2Data.computedTitle);
212
});
213
214
test('old bad data', () => {
215
const v1Data: ISerializableChatData1 = {
216
// Testing the scenario where these are missing
217
sessionId: undefined!,
218
creationDate: undefined!,
219
220
initialLocation: undefined,
221
isImported: false,
222
requesterAvatarIconUri: undefined,
223
requesterUsername: 'me',
224
requests: [],
225
responderAvatarIconUri: undefined,
226
responderUsername: 'bot',
227
};
228
229
const newData = normalizeSerializableChatData(v1Data);
230
assert.strictEqual(newData.version, 3);
231
assert.ok(newData.creationDate > 0);
232
assert.ok(newData.lastMessageDate > 0);
233
assert.ok(newData.sessionId);
234
});
235
236
test('v3 with bug', () => {
237
const v3Data: ISerializableChatData3 = {
238
// Test case where old data was wrongly normalized and these fields were missing
239
creationDate: undefined!,
240
lastMessageDate: undefined!,
241
242
version: 3,
243
initialLocation: undefined,
244
isImported: false,
245
requesterAvatarIconUri: undefined,
246
requesterUsername: 'me',
247
requests: [],
248
responderAvatarIconUri: undefined,
249
responderUsername: 'bot',
250
sessionId: 'session1',
251
customTitle: 'computed title'
252
};
253
254
const newData = normalizeSerializableChatData(v3Data);
255
assert.strictEqual(newData.version, 3);
256
assert.ok(newData.creationDate > 0);
257
assert.ok(newData.lastMessageDate > 0);
258
assert.ok(newData.sessionId);
259
});
260
});
261
262