Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/api/test/browser/mainThreadChatSessions.test.ts
5236 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 * as sinon from 'sinon';
8
import { CancellationToken } from '../../../../base/common/cancellation.js';
9
import { DisposableStore } from '../../../../base/common/lifecycle.js';
10
import { URI } from '../../../../base/common/uri.js';
11
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';
12
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
13
import { TestConfigurationService } from '../../../../platform/configuration/test/common/testConfigurationService.js';
14
import { ContextKeyService } from '../../../../platform/contextkey/browser/contextKeyService.js';
15
import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
16
import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js';
17
import { TestInstantiationService } from '../../../../platform/instantiation/test/common/instantiationServiceMock.js';
18
import { ILogService, NullLogService } from '../../../../platform/log/common/log.js';
19
import { ChatSessionsService } from '../../../contrib/chat/browser/chatSessions/chatSessions.contribution.js';
20
import { IChatAgentRequest } from '../../../contrib/chat/common/participants/chatAgents.js';
21
import { IChatProgress, IChatProgressMessage, IChatService } from '../../../contrib/chat/common/chatService/chatService.js';
22
import { IChatSessionProviderOptionGroup, IChatSessionsService } from '../../../contrib/chat/common/chatSessionsService.js';
23
import { LocalChatSessionUri } from '../../../contrib/chat/common/model/chatUri.js';
24
import { ChatAgentLocation } from '../../../contrib/chat/common/constants.js';
25
import { IEditorService } from '../../../services/editor/common/editorService.js';
26
import { IExtHostContext } from '../../../services/extensions/common/extHostCustomers.js';
27
import { ExtensionHostKind } from '../../../services/extensions/common/extensionHostKind.js';
28
import { IExtensionService } from '../../../services/extensions/common/extensions.js';
29
import { IViewsService } from '../../../services/views/common/viewsService.js';
30
import { mock, TestExtensionService } from '../../../test/common/workbenchTestServices.js';
31
import { MainThreadChatSessions, ObservableChatSession } from '../../browser/mainThreadChatSessions.js';
32
import { ExtHostChatSessionsShape, IChatProgressDto, IChatSessionProviderOptions } from '../../common/extHost.protocol.js';
33
import { ILabelService } from '../../../../platform/label/common/label.js';
34
import { MockChatService } from '../../../contrib/chat/test/common/chatService/mockChatService.js';
35
import { IAgentSessionsService } from '../../../contrib/chat/browser/agentSessions/agentSessionsService.js';
36
import { IAgentSessionsModel } from '../../../contrib/chat/browser/agentSessions/agentSessionsModel.js';
37
import { Event } from '../../../../base/common/event.js';
38
39
suite('ObservableChatSession', function () {
40
let disposables: DisposableStore;
41
let logService: ILogService;
42
let dialogService: IDialogService;
43
let proxy: ExtHostChatSessionsShape;
44
45
setup(function () {
46
disposables = new DisposableStore();
47
logService = new NullLogService();
48
49
dialogService = new class extends mock<IDialogService>() {
50
override async confirm() {
51
return { confirmed: true };
52
}
53
};
54
55
proxy = {
56
$provideChatSessionContent: sinon.stub(),
57
$provideChatSessionProviderOptions: sinon.stub<[providerHandle: number, token: CancellationToken], Promise<IChatSessionProviderOptions | undefined>>().resolves(undefined),
58
$provideHandleOptionsChange: sinon.stub(),
59
$invokeOptionGroupSearch: sinon.stub().resolves([]),
60
$interruptChatSessionActiveResponse: sinon.stub(),
61
$invokeChatSessionRequestHandler: sinon.stub(),
62
$disposeChatSessionContent: sinon.stub(),
63
$provideChatSessionItems: sinon.stub(),
64
$onDidChangeChatSessionItemState: sinon.stub(),
65
};
66
});
67
68
teardown(function () {
69
disposables.dispose();
70
sinon.restore();
71
});
72
73
ensureNoDisposablesAreLeakedInTestSuite();
74
75
function createSessionContent(options: {
76
id?: string;
77
history?: any[];
78
hasActiveResponseCallback?: boolean;
79
hasRequestHandler?: boolean;
80
} = {}) {
81
return {
82
id: options.id || 'test-id',
83
history: options.history || [],
84
hasActiveResponseCallback: options.hasActiveResponseCallback || false,
85
hasRequestHandler: options.hasRequestHandler || false
86
};
87
}
88
89
async function createInitializedSession(sessionContent: any, sessionId = 'test-id'): Promise<ObservableChatSession> {
90
const resource = LocalChatSessionUri.forSession(sessionId);
91
const session = new ObservableChatSession(resource, 1, proxy, logService, dialogService);
92
(proxy.$provideChatSessionContent as sinon.SinonStub).resolves(sessionContent);
93
await session.initialize(CancellationToken.None);
94
return session;
95
}
96
97
test('constructor creates session with proper initial state', function () {
98
const sessionId = 'test-id';
99
const resource = LocalChatSessionUri.forSession(sessionId);
100
const session = disposables.add(new ObservableChatSession(resource, 1, proxy, logService, dialogService));
101
102
assert.strictEqual(session.providerHandle, 1);
103
assert.deepStrictEqual(session.history, []);
104
assert.ok(session.progressObs);
105
assert.ok(session.isCompleteObs);
106
107
// Initial state should be inactive and incomplete
108
assert.deepStrictEqual(session.progressObs.get(), []);
109
assert.strictEqual(session.isCompleteObs.get(), false);
110
});
111
112
test('session queues progress before initialization and processes it after', async function () {
113
const sessionId = 'test-id';
114
const resource = LocalChatSessionUri.forSession(sessionId);
115
const session = disposables.add(new ObservableChatSession(resource, 1, proxy, logService, dialogService));
116
117
const progress1: IChatProgress = { kind: 'progressMessage', content: { value: 'Hello', isTrusted: false } };
118
const progress2: IChatProgress = { kind: 'progressMessage', content: { value: 'World', isTrusted: false } };
119
120
// Add progress before initialization - should be queued
121
session.handleProgressChunk('req1', [progress1]);
122
session.handleProgressChunk('req1', [progress2]);
123
124
// Progress should be queued, not visible yet
125
assert.deepStrictEqual(session.progressObs.get(), []);
126
127
// Initialize the session
128
const sessionContent = createSessionContent();
129
(proxy.$provideChatSessionContent as sinon.SinonStub).resolves(sessionContent);
130
await session.initialize(CancellationToken.None);
131
132
// Now progress should be visible
133
assert.strictEqual(session.progressObs.get().length, 2);
134
assert.deepStrictEqual(session.progressObs.get(), [progress1, progress2]);
135
assert.strictEqual(session.isCompleteObs.get(), true); // Should be complete for sessions without active response callback or request handler
136
});
137
138
test('initialization loads session history and sets up capabilities', async function () {
139
const sessionHistory = [
140
{ type: 'request', prompt: 'Previous question' },
141
{ type: 'response', parts: [{ kind: 'progressMessage', content: { value: 'Previous answer', isTrusted: false } }] }
142
];
143
144
const sessionContent = createSessionContent({
145
history: sessionHistory,
146
hasActiveResponseCallback: true,
147
hasRequestHandler: true
148
});
149
150
const session = disposables.add(await createInitializedSession(sessionContent));
151
152
// Verify history was loaded
153
assert.strictEqual(session.history.length, 2);
154
assert.strictEqual(session.history[0].type, 'request');
155
assert.strictEqual(session.history[0].prompt, 'Previous question');
156
assert.strictEqual(session.history[1].type, 'response');
157
158
// Verify capabilities were set up
159
assert.ok(session.interruptActiveResponseCallback);
160
assert.ok(session.requestHandler);
161
});
162
163
test('initialization is idempotent and returns same promise', async function () {
164
const sessionId = 'test-id';
165
const resource = LocalChatSessionUri.forSession(sessionId);
166
const session = disposables.add(new ObservableChatSession(resource, 1, proxy, logService, dialogService));
167
168
const sessionContent = createSessionContent();
169
(proxy.$provideChatSessionContent as sinon.SinonStub).resolves(sessionContent);
170
171
const promise1 = session.initialize(CancellationToken.None);
172
const promise2 = session.initialize(CancellationToken.None);
173
174
assert.strictEqual(promise1, promise2);
175
await promise1;
176
177
// Should only call proxy once even though initialize was called twice
178
assert.ok((proxy.$provideChatSessionContent as sinon.SinonStub).calledOnce);
179
});
180
181
test('progress handling works correctly after initialization', async function () {
182
const sessionContent = createSessionContent();
183
const session = disposables.add(await createInitializedSession(sessionContent));
184
185
const progress: IChatProgress = { kind: 'progressMessage', content: { value: 'New progress', isTrusted: false } };
186
187
// Add progress after initialization
188
session.handleProgressChunk('req1', [progress]);
189
190
assert.deepStrictEqual(session.progressObs.get(), [progress]);
191
// Session with no capabilities should remain complete
192
assert.strictEqual(session.isCompleteObs.get(), true);
193
});
194
195
test('progress completion updates session state correctly', async function () {
196
const sessionContent = createSessionContent();
197
const session = disposables.add(await createInitializedSession(sessionContent));
198
199
// Add some progress first
200
const progress: IChatProgress = { kind: 'progressMessage', content: { value: 'Processing...', isTrusted: false } };
201
session.handleProgressChunk('req1', [progress]);
202
203
// Session with no capabilities should already be complete
204
assert.strictEqual(session.isCompleteObs.get(), true);
205
session.handleProgressComplete('req1');
206
assert.strictEqual(session.isCompleteObs.get(), true);
207
});
208
209
test('session with active response callback becomes active when progress is added', async function () {
210
const sessionContent = createSessionContent({ hasActiveResponseCallback: true });
211
const session = disposables.add(await createInitializedSession(sessionContent));
212
213
// Session should start inactive and incomplete (has capabilities but no active progress)
214
assert.strictEqual(session.isCompleteObs.get(), false);
215
216
const progress: IChatProgress = { kind: 'progressMessage', content: { value: 'Processing...', isTrusted: false } };
217
session.handleProgressChunk('req1', [progress]);
218
219
assert.strictEqual(session.isCompleteObs.get(), false);
220
session.handleProgressComplete('req1');
221
222
assert.strictEqual(session.isCompleteObs.get(), true);
223
});
224
225
test('request handler forwards requests to proxy', async function () {
226
const sessionContent = createSessionContent({ hasRequestHandler: true });
227
const session = disposables.add(await createInitializedSession(sessionContent));
228
229
assert.ok(session.requestHandler);
230
231
const request: IChatAgentRequest = {
232
requestId: 'req1',
233
sessionResource: LocalChatSessionUri.forSession('test-session'),
234
agentId: 'test-agent',
235
message: 'Test prompt',
236
location: ChatAgentLocation.Chat,
237
variables: { variables: [] }
238
};
239
const progressCallback = sinon.stub();
240
241
await session.requestHandler!(request, progressCallback, [], CancellationToken.None);
242
243
assert.ok((proxy.$invokeChatSessionRequestHandler as sinon.SinonStubbedMember<typeof proxy.$invokeChatSessionRequestHandler>).calledOnceWith(1, session.sessionResource, request, [], CancellationToken.None));
244
});
245
246
test('request handler forwards progress updates to external callback', async function () {
247
const sessionContent = createSessionContent({ hasRequestHandler: true });
248
const session = disposables.add(await createInitializedSession(sessionContent));
249
250
assert.ok(session.requestHandler);
251
252
const request: IChatAgentRequest = {
253
requestId: 'req1',
254
sessionResource: LocalChatSessionUri.forSession('test-session'),
255
agentId: 'test-agent',
256
message: 'Test prompt',
257
location: ChatAgentLocation.Chat,
258
variables: { variables: [] }
259
};
260
const progressCallback = sinon.stub();
261
262
let resolveRequest: () => void;
263
const requestPromise = new Promise<void>(resolve => {
264
resolveRequest = resolve;
265
});
266
267
(proxy.$invokeChatSessionRequestHandler as sinon.SinonStub).returns(requestPromise);
268
269
const requestHandlerPromise = session.requestHandler!(request, progressCallback, [], CancellationToken.None);
270
271
const progress1: IChatProgress = { kind: 'progressMessage', content: { value: 'Progress 1', isTrusted: false } };
272
const progress2: IChatProgress = { kind: 'progressMessage', content: { value: 'Progress 2', isTrusted: false } };
273
274
session.handleProgressChunk('req1', [progress1]);
275
session.handleProgressChunk('req1', [progress2]);
276
277
// Wait a bit for autorun to trigger
278
await new Promise(resolve => setTimeout(resolve, 0));
279
280
assert.ok(progressCallback.calledTwice);
281
assert.deepStrictEqual(progressCallback.firstCall.args[0], [progress1]);
282
assert.deepStrictEqual(progressCallback.secondCall.args[0], [progress2]);
283
284
// Complete the request
285
resolveRequest!();
286
await requestHandlerPromise;
287
288
assert.strictEqual(session.isCompleteObs.get(), true);
289
});
290
291
test('dispose properly cleans up resources and notifies listeners', function () {
292
const sessionId = 'test-id';
293
const resource = LocalChatSessionUri.forSession(sessionId);
294
const session = disposables.add(new ObservableChatSession(resource, 1, proxy, logService, dialogService));
295
296
let disposeEventFired = false;
297
const disposable = session.onWillDispose(() => {
298
disposeEventFired = true;
299
});
300
301
session.dispose();
302
303
assert.ok(disposeEventFired);
304
assert.ok((proxy.$disposeChatSessionContent as sinon.SinonStubbedMember<typeof proxy.$disposeChatSessionContent>).calledOnceWith(1, resource));
305
306
disposable.dispose();
307
});
308
309
test('session with multiple request/response pairs in history', async function () {
310
const sessionHistory = [
311
{ type: 'request', prompt: 'First question' },
312
{ type: 'response', parts: [{ kind: 'progressMessage', content: { value: 'First answer', isTrusted: false } }] },
313
{ type: 'request', prompt: 'Second question' },
314
{ type: 'response', parts: [{ kind: 'progressMessage', content: { value: 'Second answer', isTrusted: false } }] }
315
];
316
317
const sessionContent = createSessionContent({
318
history: sessionHistory,
319
hasActiveResponseCallback: false,
320
hasRequestHandler: false
321
});
322
323
const session = disposables.add(await createInitializedSession(sessionContent));
324
325
// Verify all history was loaded correctly
326
assert.strictEqual(session.history.length, 4);
327
assert.strictEqual(session.history[0].type, 'request');
328
assert.strictEqual(session.history[0].prompt, 'First question');
329
assert.strictEqual(session.history[1].type, 'response');
330
assert.strictEqual((session.history[1].parts[0] as IChatProgressMessage).content.value, 'First answer');
331
assert.strictEqual(session.history[2].type, 'request');
332
assert.strictEqual(session.history[2].prompt, 'Second question');
333
assert.strictEqual(session.history[3].type, 'response');
334
assert.strictEqual((session.history[3].parts[0] as IChatProgressMessage).content.value, 'Second answer');
335
336
// Session should be complete since it has no capabilities
337
assert.strictEqual(session.isCompleteObs.get(), true);
338
});
339
});
340
341
suite('MainThreadChatSessions', function () {
342
let instantiationService: TestInstantiationService;
343
let mainThread: MainThreadChatSessions;
344
let proxy: ExtHostChatSessionsShape;
345
let chatSessionsService: IChatSessionsService;
346
let disposables: DisposableStore;
347
348
setup(function () {
349
disposables = new DisposableStore();
350
instantiationService = new TestInstantiationService();
351
352
proxy = {
353
$provideChatSessionContent: sinon.stub(),
354
$provideChatSessionProviderOptions: sinon.stub<[providerHandle: number, token: CancellationToken], Promise<IChatSessionProviderOptions | undefined>>().resolves(undefined),
355
$provideHandleOptionsChange: sinon.stub(),
356
$invokeOptionGroupSearch: sinon.stub().resolves([]),
357
$interruptChatSessionActiveResponse: sinon.stub(),
358
$invokeChatSessionRequestHandler: sinon.stub(),
359
$disposeChatSessionContent: sinon.stub(),
360
$provideChatSessionItems: sinon.stub(),
361
$onDidChangeChatSessionItemState: sinon.stub(),
362
};
363
364
const extHostContext = new class implements IExtHostContext {
365
remoteAuthority = '';
366
extensionHostKind = ExtensionHostKind.LocalProcess;
367
dispose() { }
368
assertRegistered() { }
369
set(v: any): any { return null; }
370
getProxy(): any { return proxy; }
371
drain(): any { return null; }
372
};
373
374
instantiationService.stub(IConfigurationService, new TestConfigurationService());
375
instantiationService.stub(IContextKeyService, disposables.add(instantiationService.createInstance(ContextKeyService)));
376
instantiationService.stub(ILogService, new NullLogService());
377
instantiationService.stub(IEditorService, new class extends mock<IEditorService>() { });
378
instantiationService.stub(IExtensionService, new TestExtensionService());
379
instantiationService.stub(IViewsService, new class extends mock<IViewsService>() {
380
override async openView() { return null; }
381
});
382
instantiationService.stub(IDialogService, new class extends mock<IDialogService>() {
383
override async confirm() {
384
return { confirmed: true };
385
}
386
});
387
instantiationService.stub(ILabelService, new class extends mock<ILabelService>() {
388
override registerFormatter() {
389
return {
390
dispose: () => { }
391
};
392
}
393
});
394
instantiationService.stub(IChatService, new MockChatService());
395
instantiationService.stub(IAgentSessionsService, new class extends mock<IAgentSessionsService>() {
396
override get model(): IAgentSessionsModel {
397
return new class extends mock<IAgentSessionsModel>() {
398
override onDidChangeSessionArchivedState = Event.None;
399
};
400
}
401
402
});
403
404
chatSessionsService = disposables.add(instantiationService.createInstance(ChatSessionsService));
405
instantiationService.stub(IChatSessionsService, chatSessionsService);
406
mainThread = disposables.add(instantiationService.createInstance(MainThreadChatSessions, extHostContext));
407
});
408
409
teardown(function () {
410
disposables.dispose();
411
instantiationService.dispose();
412
sinon.restore();
413
});
414
415
ensureNoDisposablesAreLeakedInTestSuite();
416
417
test('provideChatSessionContent creates and initializes session', async function () {
418
const sessionScheme = 'test-session-type';
419
mainThread.$registerChatSessionContentProvider(1, sessionScheme);
420
421
const sessionContent = {
422
id: 'test-session',
423
history: [],
424
hasActiveResponseCallback: false,
425
hasRequestHandler: false
426
};
427
428
const resource = URI.parse(`${sessionScheme}:/test-session`);
429
430
(proxy.$provideChatSessionContent as sinon.SinonStub).resolves(sessionContent);
431
const session1 = await chatSessionsService.getOrCreateChatSession(resource, CancellationToken.None);
432
433
assert.ok(session1);
434
435
const session2 = await chatSessionsService.getOrCreateChatSession(resource, CancellationToken.None);
436
assert.strictEqual(session1, session2);
437
438
assert.ok((proxy.$provideChatSessionContent as sinon.SinonStub).calledOnce);
439
mainThread.$unregisterChatSessionContentProvider(1);
440
});
441
442
test('$handleProgressChunk routes to correct session', async function () {
443
const sessionScheme = 'test-session-type';
444
445
mainThread.$registerChatSessionContentProvider(1, sessionScheme);
446
447
const sessionContent = {
448
id: 'test-session',
449
history: [],
450
hasActiveResponseCallback: false,
451
hasRequestHandler: false
452
};
453
454
(proxy.$provideChatSessionContent as sinon.SinonStub).resolves(sessionContent);
455
456
const resource = URI.parse(`${sessionScheme}:/test-session`);
457
const session = await chatSessionsService.getOrCreateChatSession(resource, CancellationToken.None) as ObservableChatSession;
458
459
const progressDto: IChatProgressDto = { kind: 'progressMessage', content: { value: 'Test', isTrusted: false } };
460
await mainThread.$handleProgressChunk(1, resource, 'req1', [progressDto]);
461
462
assert.strictEqual(session.progressObs.get().length, 1);
463
assert.strictEqual(session.progressObs.get()[0].kind, 'progressMessage');
464
465
mainThread.$unregisterChatSessionContentProvider(1);
466
});
467
468
test('$handleProgressComplete marks session complete', async function () {
469
const sessionScheme = 'test-session-type';
470
mainThread.$registerChatSessionContentProvider(1, sessionScheme);
471
472
const sessionContent = {
473
id: 'test-session',
474
history: [],
475
hasActiveResponseCallback: false,
476
hasRequestHandler: false
477
};
478
479
(proxy.$provideChatSessionContent as sinon.SinonStub).resolves(sessionContent);
480
481
const resource = URI.parse(`${sessionScheme}:/test-session`);
482
const session = await chatSessionsService.getOrCreateChatSession(resource, CancellationToken.None) as ObservableChatSession;
483
484
const progressDto: IChatProgressDto = { kind: 'progressMessage', content: { value: 'Test', isTrusted: false } };
485
await mainThread.$handleProgressChunk(1, resource, 'req1', [progressDto]);
486
mainThread.$handleProgressComplete(1, resource, 'req1');
487
488
assert.strictEqual(session.isCompleteObs.get(), true);
489
490
mainThread.$unregisterChatSessionContentProvider(1);
491
});
492
493
test('integration with multiple request/response pairs', async function () {
494
const sessionScheme = 'test-session-type';
495
mainThread.$registerChatSessionContentProvider(1, sessionScheme);
496
497
const sessionContent = {
498
id: 'multi-turn-session',
499
history: [
500
{ type: 'request', prompt: 'First question' },
501
{ type: 'response', parts: [{ kind: 'progressMessage', content: { value: 'First answer', isTrusted: false } }] },
502
{ type: 'request', prompt: 'Second question' },
503
{ type: 'response', parts: [{ kind: 'progressMessage', content: { value: 'Second answer', isTrusted: false } }] }
504
],
505
hasActiveResponseCallback: false,
506
hasRequestHandler: false
507
};
508
509
(proxy.$provideChatSessionContent as sinon.SinonStub).resolves(sessionContent);
510
511
const resource = URI.parse(`${sessionScheme}:/multi-turn-session`);
512
const session = await chatSessionsService.getOrCreateChatSession(resource, CancellationToken.None) as ObservableChatSession;
513
514
// Verify the session loaded correctly
515
assert.ok(session);
516
assert.strictEqual(session.history.length, 4);
517
518
// Verify all history items are correctly loaded
519
assert.strictEqual(session.history[0].type, 'request');
520
assert.strictEqual(session.history[0].prompt, 'First question');
521
assert.strictEqual(session.history[1].type, 'response');
522
assert.strictEqual(session.history[2].type, 'request');
523
assert.strictEqual(session.history[2].prompt, 'Second question');
524
assert.strictEqual(session.history[3].type, 'response');
525
526
// Session should be complete since it has no active capabilities
527
assert.strictEqual(session.isCompleteObs.get(), true);
528
529
mainThread.$unregisterChatSessionContentProvider(1);
530
});
531
532
test('$onDidChangeChatSessionProviderOptions refreshes option groups', async function () {
533
const sessionScheme = 'test-session-type';
534
const handle = 1;
535
536
const optionGroups1: IChatSessionProviderOptionGroup[] = [{
537
id: 'models',
538
name: 'Models',
539
items: [{ id: 'modelA', name: 'Model A' }]
540
}];
541
const optionGroups2: IChatSessionProviderOptionGroup[] = [{
542
id: 'models',
543
name: 'Models',
544
items: [{ id: 'modelB', name: 'Model B' }]
545
}];
546
547
const provideOptionsStub = proxy.$provideChatSessionProviderOptions as sinon.SinonStub;
548
provideOptionsStub.onFirstCall().resolves({ optionGroups: optionGroups1 } as IChatSessionProviderOptions);
549
provideOptionsStub.onSecondCall().resolves({ optionGroups: optionGroups2 } as IChatSessionProviderOptions);
550
551
mainThread.$registerChatSessionContentProvider(handle, sessionScheme);
552
553
// Wait for initial options fetch triggered on registration
554
await new Promise(resolve => setTimeout(resolve, 0));
555
556
let storedGroups = chatSessionsService.getOptionGroupsForSessionType(sessionScheme);
557
assert.ok(storedGroups);
558
assert.strictEqual(storedGroups![0].items[0].id, 'modelA');
559
560
// Simulate extension signaling that provider options have changed
561
mainThread.$onDidChangeChatSessionProviderOptions(handle);
562
await new Promise(resolve => setTimeout(resolve, 0));
563
564
storedGroups = chatSessionsService.getOptionGroupsForSessionType(sessionScheme);
565
assert.ok(storedGroups);
566
assert.strictEqual(storedGroups![0].items[0].id, 'modelB');
567
568
mainThread.$unregisterChatSessionContentProvider(handle);
569
});
570
571
test('getSessionOption returns undefined for unset options', async function () {
572
const sessionScheme = 'test-session-type';
573
mainThread.$registerChatSessionContentProvider(1, sessionScheme);
574
575
const sessionContent = {
576
id: 'test-session',
577
history: [],
578
hasActiveResponseCallback: false,
579
hasRequestHandler: false,
580
// No options provided
581
};
582
583
(proxy.$provideChatSessionContent as sinon.SinonStub).resolves(sessionContent);
584
585
const resource = URI.parse(`${sessionScheme}:/test-session`);
586
await chatSessionsService.getOrCreateChatSession(resource, CancellationToken.None);
587
588
// getSessionOption should return undefined for unset options
589
assert.strictEqual(chatSessionsService.getSessionOption(resource, 'models'), undefined);
590
assert.strictEqual(chatSessionsService.getSessionOption(resource, 'anyOption'), undefined);
591
592
mainThread.$unregisterChatSessionContentProvider(1);
593
});
594
595
test('getSessionOption returns value for explicitly set options', async function () {
596
const sessionScheme = 'test-session-type';
597
mainThread.$registerChatSessionContentProvider(1, sessionScheme);
598
599
const sessionContent = {
600
id: 'test-session',
601
history: [],
602
hasActiveResponseCallback: false,
603
hasRequestHandler: false,
604
options: {
605
'models': 'gpt-4',
606
'region': { id: 'us-east', name: 'US East' }
607
}
608
};
609
610
(proxy.$provideChatSessionContent as sinon.SinonStub).resolves(sessionContent);
611
612
const resource = URI.parse(`${sessionScheme}:/test-session`);
613
await chatSessionsService.getOrCreateChatSession(resource, CancellationToken.None);
614
615
// getSessionOption should return the configured values
616
assert.strictEqual(chatSessionsService.getSessionOption(resource, 'models'), 'gpt-4');
617
assert.deepStrictEqual(chatSessionsService.getSessionOption(resource, 'region'), { id: 'us-east', name: 'US East' });
618
619
// getSessionOption should return undefined for options not in the session
620
assert.strictEqual(chatSessionsService.getSessionOption(resource, 'notConfigured'), undefined);
621
622
mainThread.$unregisterChatSessionContentProvider(1);
623
});
624
625
test('option change notifications are sent to the extension', async function () {
626
const sessionScheme = 'test-session-type';
627
const handle = 1;
628
629
mainThread.$registerChatSessionContentProvider(handle, sessionScheme);
630
631
const sessionContent = {
632
id: 'test-session',
633
history: [],
634
hasActiveResponseCallback: false,
635
hasRequestHandler: false,
636
options: {
637
'models': 'gpt-4'
638
}
639
};
640
641
(proxy.$provideChatSessionContent as sinon.SinonStub).resolves(sessionContent);
642
643
const resource = URI.parse(`${sessionScheme}:/test-session`);
644
await chatSessionsService.getOrCreateChatSession(resource, CancellationToken.None);
645
646
// Clear the stub call history
647
(proxy.$provideHandleOptionsChange as sinon.SinonStub).resetHistory();
648
649
// Simulate an option change
650
await chatSessionsService.notifySessionOptionsChange(resource, [
651
{ optionId: 'models', value: 'gpt-4-turbo' }
652
]);
653
654
// Verify the extension was notified
655
assert.ok((proxy.$provideHandleOptionsChange as sinon.SinonStub).calledOnce);
656
const call = (proxy.$provideHandleOptionsChange as sinon.SinonStub).firstCall;
657
assert.strictEqual(call.args[0], handle);
658
assert.deepStrictEqual(call.args[1], resource);
659
assert.deepStrictEqual(call.args[2], [{ optionId: 'models', value: 'gpt-4-turbo' }]);
660
661
mainThread.$unregisterChatSessionContentProvider(handle);
662
});
663
664
test('option change notifications fail silently when provider not registered', async function () {
665
const sessionScheme = 'unregistered-session-type';
666
667
// Do NOT register a content provider for this scheme
668
669
const resource = URI.parse(`${sessionScheme}:/test-session`);
670
671
// Clear any previous calls
672
(proxy.$provideHandleOptionsChange as sinon.SinonStub).resetHistory();
673
674
// Attempt to notify option change for an unregistered scheme
675
// This should not throw, but also should not call the proxy
676
await chatSessionsService.notifySessionOptionsChange(resource, [
677
{ optionId: 'models', value: 'gpt-4-turbo' }
678
]);
679
680
// Verify the extension was NOT notified (no provider registered)
681
assert.strictEqual((proxy.$provideHandleOptionsChange as sinon.SinonStub).callCount, 0);
682
});
683
684
test('setSessionOption updates option and getSessionOption reflects change', async function () {
685
const sessionScheme = 'test-session-type';
686
mainThread.$registerChatSessionContentProvider(1, sessionScheme);
687
688
const sessionContent = {
689
id: 'test-session',
690
history: [],
691
hasActiveResponseCallback: false,
692
hasRequestHandler: false,
693
// Start with no options
694
};
695
696
(proxy.$provideChatSessionContent as sinon.SinonStub).resolves(sessionContent);
697
698
const resource = URI.parse(`${sessionScheme}:/test-session`);
699
await chatSessionsService.getOrCreateChatSession(resource, CancellationToken.None);
700
701
// Initially no options set
702
assert.strictEqual(chatSessionsService.getSessionOption(resource, 'models'), undefined);
703
704
// Set an option
705
chatSessionsService.setSessionOption(resource, 'models', 'gpt-4');
706
707
// Now getSessionOption should return the value
708
assert.strictEqual(chatSessionsService.getSessionOption(resource, 'models'), 'gpt-4');
709
710
mainThread.$unregisterChatSessionContentProvider(1);
711
});
712
713
test('hasAnySessionOptions returns correct values', async function () {
714
const sessionScheme = 'test-session-type';
715
mainThread.$registerChatSessionContentProvider(1, sessionScheme);
716
717
// Session with options
718
const sessionContentWithOptions = {
719
id: 'session-with-options',
720
history: [],
721
hasActiveResponseCallback: false,
722
hasRequestHandler: false,
723
options: { 'models': 'gpt-4' }
724
};
725
726
// Session without options
727
const sessionContentWithoutOptions = {
728
id: 'session-without-options',
729
history: [],
730
hasActiveResponseCallback: false,
731
hasRequestHandler: false,
732
};
733
734
(proxy.$provideChatSessionContent as sinon.SinonStub)
735
.onFirstCall().resolves(sessionContentWithOptions)
736
.onSecondCall().resolves(sessionContentWithoutOptions);
737
738
const resourceWithOptions = URI.parse(`${sessionScheme}:/session-with-options`);
739
const resourceWithoutOptions = URI.parse(`${sessionScheme}:/session-without-options`);
740
741
await chatSessionsService.getOrCreateChatSession(resourceWithOptions, CancellationToken.None);
742
await chatSessionsService.getOrCreateChatSession(resourceWithoutOptions, CancellationToken.None);
743
744
assert.strictEqual(chatSessionsService.hasAnySessionOptions(resourceWithOptions), true);
745
assert.strictEqual(chatSessionsService.hasAnySessionOptions(resourceWithoutOptions), false);
746
747
mainThread.$unregisterChatSessionContentProvider(1);
748
});
749
});
750
751