Path: blob/main/src/vs/workbench/api/test/browser/mainThreadChatSessions.test.ts
5236 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import assert from 'assert';6import * as sinon from 'sinon';7import { CancellationToken } from '../../../../base/common/cancellation.js';8import { DisposableStore } from '../../../../base/common/lifecycle.js';9import { URI } from '../../../../base/common/uri.js';10import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';11import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';12import { TestConfigurationService } from '../../../../platform/configuration/test/common/testConfigurationService.js';13import { ContextKeyService } from '../../../../platform/contextkey/browser/contextKeyService.js';14import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';15import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js';16import { TestInstantiationService } from '../../../../platform/instantiation/test/common/instantiationServiceMock.js';17import { ILogService, NullLogService } from '../../../../platform/log/common/log.js';18import { ChatSessionsService } from '../../../contrib/chat/browser/chatSessions/chatSessions.contribution.js';19import { IChatAgentRequest } from '../../../contrib/chat/common/participants/chatAgents.js';20import { IChatProgress, IChatProgressMessage, IChatService } from '../../../contrib/chat/common/chatService/chatService.js';21import { IChatSessionProviderOptionGroup, IChatSessionsService } from '../../../contrib/chat/common/chatSessionsService.js';22import { LocalChatSessionUri } from '../../../contrib/chat/common/model/chatUri.js';23import { ChatAgentLocation } from '../../../contrib/chat/common/constants.js';24import { IEditorService } from '../../../services/editor/common/editorService.js';25import { IExtHostContext } from '../../../services/extensions/common/extHostCustomers.js';26import { ExtensionHostKind } from '../../../services/extensions/common/extensionHostKind.js';27import { IExtensionService } from '../../../services/extensions/common/extensions.js';28import { IViewsService } from '../../../services/views/common/viewsService.js';29import { mock, TestExtensionService } from '../../../test/common/workbenchTestServices.js';30import { MainThreadChatSessions, ObservableChatSession } from '../../browser/mainThreadChatSessions.js';31import { ExtHostChatSessionsShape, IChatProgressDto, IChatSessionProviderOptions } from '../../common/extHost.protocol.js';32import { ILabelService } from '../../../../platform/label/common/label.js';33import { MockChatService } from '../../../contrib/chat/test/common/chatService/mockChatService.js';34import { IAgentSessionsService } from '../../../contrib/chat/browser/agentSessions/agentSessionsService.js';35import { IAgentSessionsModel } from '../../../contrib/chat/browser/agentSessions/agentSessionsModel.js';36import { Event } from '../../../../base/common/event.js';3738suite('ObservableChatSession', function () {39let disposables: DisposableStore;40let logService: ILogService;41let dialogService: IDialogService;42let proxy: ExtHostChatSessionsShape;4344setup(function () {45disposables = new DisposableStore();46logService = new NullLogService();4748dialogService = new class extends mock<IDialogService>() {49override async confirm() {50return { confirmed: true };51}52};5354proxy = {55$provideChatSessionContent: sinon.stub(),56$provideChatSessionProviderOptions: sinon.stub<[providerHandle: number, token: CancellationToken], Promise<IChatSessionProviderOptions | undefined>>().resolves(undefined),57$provideHandleOptionsChange: sinon.stub(),58$invokeOptionGroupSearch: sinon.stub().resolves([]),59$interruptChatSessionActiveResponse: sinon.stub(),60$invokeChatSessionRequestHandler: sinon.stub(),61$disposeChatSessionContent: sinon.stub(),62$provideChatSessionItems: sinon.stub(),63$onDidChangeChatSessionItemState: sinon.stub(),64};65});6667teardown(function () {68disposables.dispose();69sinon.restore();70});7172ensureNoDisposablesAreLeakedInTestSuite();7374function createSessionContent(options: {75id?: string;76history?: any[];77hasActiveResponseCallback?: boolean;78hasRequestHandler?: boolean;79} = {}) {80return {81id: options.id || 'test-id',82history: options.history || [],83hasActiveResponseCallback: options.hasActiveResponseCallback || false,84hasRequestHandler: options.hasRequestHandler || false85};86}8788async function createInitializedSession(sessionContent: any, sessionId = 'test-id'): Promise<ObservableChatSession> {89const resource = LocalChatSessionUri.forSession(sessionId);90const session = new ObservableChatSession(resource, 1, proxy, logService, dialogService);91(proxy.$provideChatSessionContent as sinon.SinonStub).resolves(sessionContent);92await session.initialize(CancellationToken.None);93return session;94}9596test('constructor creates session with proper initial state', function () {97const sessionId = 'test-id';98const resource = LocalChatSessionUri.forSession(sessionId);99const session = disposables.add(new ObservableChatSession(resource, 1, proxy, logService, dialogService));100101assert.strictEqual(session.providerHandle, 1);102assert.deepStrictEqual(session.history, []);103assert.ok(session.progressObs);104assert.ok(session.isCompleteObs);105106// Initial state should be inactive and incomplete107assert.deepStrictEqual(session.progressObs.get(), []);108assert.strictEqual(session.isCompleteObs.get(), false);109});110111test('session queues progress before initialization and processes it after', async function () {112const sessionId = 'test-id';113const resource = LocalChatSessionUri.forSession(sessionId);114const session = disposables.add(new ObservableChatSession(resource, 1, proxy, logService, dialogService));115116const progress1: IChatProgress = { kind: 'progressMessage', content: { value: 'Hello', isTrusted: false } };117const progress2: IChatProgress = { kind: 'progressMessage', content: { value: 'World', isTrusted: false } };118119// Add progress before initialization - should be queued120session.handleProgressChunk('req1', [progress1]);121session.handleProgressChunk('req1', [progress2]);122123// Progress should be queued, not visible yet124assert.deepStrictEqual(session.progressObs.get(), []);125126// Initialize the session127const sessionContent = createSessionContent();128(proxy.$provideChatSessionContent as sinon.SinonStub).resolves(sessionContent);129await session.initialize(CancellationToken.None);130131// Now progress should be visible132assert.strictEqual(session.progressObs.get().length, 2);133assert.deepStrictEqual(session.progressObs.get(), [progress1, progress2]);134assert.strictEqual(session.isCompleteObs.get(), true); // Should be complete for sessions without active response callback or request handler135});136137test('initialization loads session history and sets up capabilities', async function () {138const sessionHistory = [139{ type: 'request', prompt: 'Previous question' },140{ type: 'response', parts: [{ kind: 'progressMessage', content: { value: 'Previous answer', isTrusted: false } }] }141];142143const sessionContent = createSessionContent({144history: sessionHistory,145hasActiveResponseCallback: true,146hasRequestHandler: true147});148149const session = disposables.add(await createInitializedSession(sessionContent));150151// Verify history was loaded152assert.strictEqual(session.history.length, 2);153assert.strictEqual(session.history[0].type, 'request');154assert.strictEqual(session.history[0].prompt, 'Previous question');155assert.strictEqual(session.history[1].type, 'response');156157// Verify capabilities were set up158assert.ok(session.interruptActiveResponseCallback);159assert.ok(session.requestHandler);160});161162test('initialization is idempotent and returns same promise', async function () {163const sessionId = 'test-id';164const resource = LocalChatSessionUri.forSession(sessionId);165const session = disposables.add(new ObservableChatSession(resource, 1, proxy, logService, dialogService));166167const sessionContent = createSessionContent();168(proxy.$provideChatSessionContent as sinon.SinonStub).resolves(sessionContent);169170const promise1 = session.initialize(CancellationToken.None);171const promise2 = session.initialize(CancellationToken.None);172173assert.strictEqual(promise1, promise2);174await promise1;175176// Should only call proxy once even though initialize was called twice177assert.ok((proxy.$provideChatSessionContent as sinon.SinonStub).calledOnce);178});179180test('progress handling works correctly after initialization', async function () {181const sessionContent = createSessionContent();182const session = disposables.add(await createInitializedSession(sessionContent));183184const progress: IChatProgress = { kind: 'progressMessage', content: { value: 'New progress', isTrusted: false } };185186// Add progress after initialization187session.handleProgressChunk('req1', [progress]);188189assert.deepStrictEqual(session.progressObs.get(), [progress]);190// Session with no capabilities should remain complete191assert.strictEqual(session.isCompleteObs.get(), true);192});193194test('progress completion updates session state correctly', async function () {195const sessionContent = createSessionContent();196const session = disposables.add(await createInitializedSession(sessionContent));197198// Add some progress first199const progress: IChatProgress = { kind: 'progressMessage', content: { value: 'Processing...', isTrusted: false } };200session.handleProgressChunk('req1', [progress]);201202// Session with no capabilities should already be complete203assert.strictEqual(session.isCompleteObs.get(), true);204session.handleProgressComplete('req1');205assert.strictEqual(session.isCompleteObs.get(), true);206});207208test('session with active response callback becomes active when progress is added', async function () {209const sessionContent = createSessionContent({ hasActiveResponseCallback: true });210const session = disposables.add(await createInitializedSession(sessionContent));211212// Session should start inactive and incomplete (has capabilities but no active progress)213assert.strictEqual(session.isCompleteObs.get(), false);214215const progress: IChatProgress = { kind: 'progressMessage', content: { value: 'Processing...', isTrusted: false } };216session.handleProgressChunk('req1', [progress]);217218assert.strictEqual(session.isCompleteObs.get(), false);219session.handleProgressComplete('req1');220221assert.strictEqual(session.isCompleteObs.get(), true);222});223224test('request handler forwards requests to proxy', async function () {225const sessionContent = createSessionContent({ hasRequestHandler: true });226const session = disposables.add(await createInitializedSession(sessionContent));227228assert.ok(session.requestHandler);229230const request: IChatAgentRequest = {231requestId: 'req1',232sessionResource: LocalChatSessionUri.forSession('test-session'),233agentId: 'test-agent',234message: 'Test prompt',235location: ChatAgentLocation.Chat,236variables: { variables: [] }237};238const progressCallback = sinon.stub();239240await session.requestHandler!(request, progressCallback, [], CancellationToken.None);241242assert.ok((proxy.$invokeChatSessionRequestHandler as sinon.SinonStubbedMember<typeof proxy.$invokeChatSessionRequestHandler>).calledOnceWith(1, session.sessionResource, request, [], CancellationToken.None));243});244245test('request handler forwards progress updates to external callback', async function () {246const sessionContent = createSessionContent({ hasRequestHandler: true });247const session = disposables.add(await createInitializedSession(sessionContent));248249assert.ok(session.requestHandler);250251const request: IChatAgentRequest = {252requestId: 'req1',253sessionResource: LocalChatSessionUri.forSession('test-session'),254agentId: 'test-agent',255message: 'Test prompt',256location: ChatAgentLocation.Chat,257variables: { variables: [] }258};259const progressCallback = sinon.stub();260261let resolveRequest: () => void;262const requestPromise = new Promise<void>(resolve => {263resolveRequest = resolve;264});265266(proxy.$invokeChatSessionRequestHandler as sinon.SinonStub).returns(requestPromise);267268const requestHandlerPromise = session.requestHandler!(request, progressCallback, [], CancellationToken.None);269270const progress1: IChatProgress = { kind: 'progressMessage', content: { value: 'Progress 1', isTrusted: false } };271const progress2: IChatProgress = { kind: 'progressMessage', content: { value: 'Progress 2', isTrusted: false } };272273session.handleProgressChunk('req1', [progress1]);274session.handleProgressChunk('req1', [progress2]);275276// Wait a bit for autorun to trigger277await new Promise(resolve => setTimeout(resolve, 0));278279assert.ok(progressCallback.calledTwice);280assert.deepStrictEqual(progressCallback.firstCall.args[0], [progress1]);281assert.deepStrictEqual(progressCallback.secondCall.args[0], [progress2]);282283// Complete the request284resolveRequest!();285await requestHandlerPromise;286287assert.strictEqual(session.isCompleteObs.get(), true);288});289290test('dispose properly cleans up resources and notifies listeners', function () {291const sessionId = 'test-id';292const resource = LocalChatSessionUri.forSession(sessionId);293const session = disposables.add(new ObservableChatSession(resource, 1, proxy, logService, dialogService));294295let disposeEventFired = false;296const disposable = session.onWillDispose(() => {297disposeEventFired = true;298});299300session.dispose();301302assert.ok(disposeEventFired);303assert.ok((proxy.$disposeChatSessionContent as sinon.SinonStubbedMember<typeof proxy.$disposeChatSessionContent>).calledOnceWith(1, resource));304305disposable.dispose();306});307308test('session with multiple request/response pairs in history', async function () {309const sessionHistory = [310{ type: 'request', prompt: 'First question' },311{ type: 'response', parts: [{ kind: 'progressMessage', content: { value: 'First answer', isTrusted: false } }] },312{ type: 'request', prompt: 'Second question' },313{ type: 'response', parts: [{ kind: 'progressMessage', content: { value: 'Second answer', isTrusted: false } }] }314];315316const sessionContent = createSessionContent({317history: sessionHistory,318hasActiveResponseCallback: false,319hasRequestHandler: false320});321322const session = disposables.add(await createInitializedSession(sessionContent));323324// Verify all history was loaded correctly325assert.strictEqual(session.history.length, 4);326assert.strictEqual(session.history[0].type, 'request');327assert.strictEqual(session.history[0].prompt, 'First question');328assert.strictEqual(session.history[1].type, 'response');329assert.strictEqual((session.history[1].parts[0] as IChatProgressMessage).content.value, 'First answer');330assert.strictEqual(session.history[2].type, 'request');331assert.strictEqual(session.history[2].prompt, 'Second question');332assert.strictEqual(session.history[3].type, 'response');333assert.strictEqual((session.history[3].parts[0] as IChatProgressMessage).content.value, 'Second answer');334335// Session should be complete since it has no capabilities336assert.strictEqual(session.isCompleteObs.get(), true);337});338});339340suite('MainThreadChatSessions', function () {341let instantiationService: TestInstantiationService;342let mainThread: MainThreadChatSessions;343let proxy: ExtHostChatSessionsShape;344let chatSessionsService: IChatSessionsService;345let disposables: DisposableStore;346347setup(function () {348disposables = new DisposableStore();349instantiationService = new TestInstantiationService();350351proxy = {352$provideChatSessionContent: sinon.stub(),353$provideChatSessionProviderOptions: sinon.stub<[providerHandle: number, token: CancellationToken], Promise<IChatSessionProviderOptions | undefined>>().resolves(undefined),354$provideHandleOptionsChange: sinon.stub(),355$invokeOptionGroupSearch: sinon.stub().resolves([]),356$interruptChatSessionActiveResponse: sinon.stub(),357$invokeChatSessionRequestHandler: sinon.stub(),358$disposeChatSessionContent: sinon.stub(),359$provideChatSessionItems: sinon.stub(),360$onDidChangeChatSessionItemState: sinon.stub(),361};362363const extHostContext = new class implements IExtHostContext {364remoteAuthority = '';365extensionHostKind = ExtensionHostKind.LocalProcess;366dispose() { }367assertRegistered() { }368set(v: any): any { return null; }369getProxy(): any { return proxy; }370drain(): any { return null; }371};372373instantiationService.stub(IConfigurationService, new TestConfigurationService());374instantiationService.stub(IContextKeyService, disposables.add(instantiationService.createInstance(ContextKeyService)));375instantiationService.stub(ILogService, new NullLogService());376instantiationService.stub(IEditorService, new class extends mock<IEditorService>() { });377instantiationService.stub(IExtensionService, new TestExtensionService());378instantiationService.stub(IViewsService, new class extends mock<IViewsService>() {379override async openView() { return null; }380});381instantiationService.stub(IDialogService, new class extends mock<IDialogService>() {382override async confirm() {383return { confirmed: true };384}385});386instantiationService.stub(ILabelService, new class extends mock<ILabelService>() {387override registerFormatter() {388return {389dispose: () => { }390};391}392});393instantiationService.stub(IChatService, new MockChatService());394instantiationService.stub(IAgentSessionsService, new class extends mock<IAgentSessionsService>() {395override get model(): IAgentSessionsModel {396return new class extends mock<IAgentSessionsModel>() {397override onDidChangeSessionArchivedState = Event.None;398};399}400401});402403chatSessionsService = disposables.add(instantiationService.createInstance(ChatSessionsService));404instantiationService.stub(IChatSessionsService, chatSessionsService);405mainThread = disposables.add(instantiationService.createInstance(MainThreadChatSessions, extHostContext));406});407408teardown(function () {409disposables.dispose();410instantiationService.dispose();411sinon.restore();412});413414ensureNoDisposablesAreLeakedInTestSuite();415416test('provideChatSessionContent creates and initializes session', async function () {417const sessionScheme = 'test-session-type';418mainThread.$registerChatSessionContentProvider(1, sessionScheme);419420const sessionContent = {421id: 'test-session',422history: [],423hasActiveResponseCallback: false,424hasRequestHandler: false425};426427const resource = URI.parse(`${sessionScheme}:/test-session`);428429(proxy.$provideChatSessionContent as sinon.SinonStub).resolves(sessionContent);430const session1 = await chatSessionsService.getOrCreateChatSession(resource, CancellationToken.None);431432assert.ok(session1);433434const session2 = await chatSessionsService.getOrCreateChatSession(resource, CancellationToken.None);435assert.strictEqual(session1, session2);436437assert.ok((proxy.$provideChatSessionContent as sinon.SinonStub).calledOnce);438mainThread.$unregisterChatSessionContentProvider(1);439});440441test('$handleProgressChunk routes to correct session', async function () {442const sessionScheme = 'test-session-type';443444mainThread.$registerChatSessionContentProvider(1, sessionScheme);445446const sessionContent = {447id: 'test-session',448history: [],449hasActiveResponseCallback: false,450hasRequestHandler: false451};452453(proxy.$provideChatSessionContent as sinon.SinonStub).resolves(sessionContent);454455const resource = URI.parse(`${sessionScheme}:/test-session`);456const session = await chatSessionsService.getOrCreateChatSession(resource, CancellationToken.None) as ObservableChatSession;457458const progressDto: IChatProgressDto = { kind: 'progressMessage', content: { value: 'Test', isTrusted: false } };459await mainThread.$handleProgressChunk(1, resource, 'req1', [progressDto]);460461assert.strictEqual(session.progressObs.get().length, 1);462assert.strictEqual(session.progressObs.get()[0].kind, 'progressMessage');463464mainThread.$unregisterChatSessionContentProvider(1);465});466467test('$handleProgressComplete marks session complete', async function () {468const sessionScheme = 'test-session-type';469mainThread.$registerChatSessionContentProvider(1, sessionScheme);470471const sessionContent = {472id: 'test-session',473history: [],474hasActiveResponseCallback: false,475hasRequestHandler: false476};477478(proxy.$provideChatSessionContent as sinon.SinonStub).resolves(sessionContent);479480const resource = URI.parse(`${sessionScheme}:/test-session`);481const session = await chatSessionsService.getOrCreateChatSession(resource, CancellationToken.None) as ObservableChatSession;482483const progressDto: IChatProgressDto = { kind: 'progressMessage', content: { value: 'Test', isTrusted: false } };484await mainThread.$handleProgressChunk(1, resource, 'req1', [progressDto]);485mainThread.$handleProgressComplete(1, resource, 'req1');486487assert.strictEqual(session.isCompleteObs.get(), true);488489mainThread.$unregisterChatSessionContentProvider(1);490});491492test('integration with multiple request/response pairs', async function () {493const sessionScheme = 'test-session-type';494mainThread.$registerChatSessionContentProvider(1, sessionScheme);495496const sessionContent = {497id: 'multi-turn-session',498history: [499{ type: 'request', prompt: 'First question' },500{ type: 'response', parts: [{ kind: 'progressMessage', content: { value: 'First answer', isTrusted: false } }] },501{ type: 'request', prompt: 'Second question' },502{ type: 'response', parts: [{ kind: 'progressMessage', content: { value: 'Second answer', isTrusted: false } }] }503],504hasActiveResponseCallback: false,505hasRequestHandler: false506};507508(proxy.$provideChatSessionContent as sinon.SinonStub).resolves(sessionContent);509510const resource = URI.parse(`${sessionScheme}:/multi-turn-session`);511const session = await chatSessionsService.getOrCreateChatSession(resource, CancellationToken.None) as ObservableChatSession;512513// Verify the session loaded correctly514assert.ok(session);515assert.strictEqual(session.history.length, 4);516517// Verify all history items are correctly loaded518assert.strictEqual(session.history[0].type, 'request');519assert.strictEqual(session.history[0].prompt, 'First question');520assert.strictEqual(session.history[1].type, 'response');521assert.strictEqual(session.history[2].type, 'request');522assert.strictEqual(session.history[2].prompt, 'Second question');523assert.strictEqual(session.history[3].type, 'response');524525// Session should be complete since it has no active capabilities526assert.strictEqual(session.isCompleteObs.get(), true);527528mainThread.$unregisterChatSessionContentProvider(1);529});530531test('$onDidChangeChatSessionProviderOptions refreshes option groups', async function () {532const sessionScheme = 'test-session-type';533const handle = 1;534535const optionGroups1: IChatSessionProviderOptionGroup[] = [{536id: 'models',537name: 'Models',538items: [{ id: 'modelA', name: 'Model A' }]539}];540const optionGroups2: IChatSessionProviderOptionGroup[] = [{541id: 'models',542name: 'Models',543items: [{ id: 'modelB', name: 'Model B' }]544}];545546const provideOptionsStub = proxy.$provideChatSessionProviderOptions as sinon.SinonStub;547provideOptionsStub.onFirstCall().resolves({ optionGroups: optionGroups1 } as IChatSessionProviderOptions);548provideOptionsStub.onSecondCall().resolves({ optionGroups: optionGroups2 } as IChatSessionProviderOptions);549550mainThread.$registerChatSessionContentProvider(handle, sessionScheme);551552// Wait for initial options fetch triggered on registration553await new Promise(resolve => setTimeout(resolve, 0));554555let storedGroups = chatSessionsService.getOptionGroupsForSessionType(sessionScheme);556assert.ok(storedGroups);557assert.strictEqual(storedGroups![0].items[0].id, 'modelA');558559// Simulate extension signaling that provider options have changed560mainThread.$onDidChangeChatSessionProviderOptions(handle);561await new Promise(resolve => setTimeout(resolve, 0));562563storedGroups = chatSessionsService.getOptionGroupsForSessionType(sessionScheme);564assert.ok(storedGroups);565assert.strictEqual(storedGroups![0].items[0].id, 'modelB');566567mainThread.$unregisterChatSessionContentProvider(handle);568});569570test('getSessionOption returns undefined for unset options', async function () {571const sessionScheme = 'test-session-type';572mainThread.$registerChatSessionContentProvider(1, sessionScheme);573574const sessionContent = {575id: 'test-session',576history: [],577hasActiveResponseCallback: false,578hasRequestHandler: false,579// No options provided580};581582(proxy.$provideChatSessionContent as sinon.SinonStub).resolves(sessionContent);583584const resource = URI.parse(`${sessionScheme}:/test-session`);585await chatSessionsService.getOrCreateChatSession(resource, CancellationToken.None);586587// getSessionOption should return undefined for unset options588assert.strictEqual(chatSessionsService.getSessionOption(resource, 'models'), undefined);589assert.strictEqual(chatSessionsService.getSessionOption(resource, 'anyOption'), undefined);590591mainThread.$unregisterChatSessionContentProvider(1);592});593594test('getSessionOption returns value for explicitly set options', async function () {595const sessionScheme = 'test-session-type';596mainThread.$registerChatSessionContentProvider(1, sessionScheme);597598const sessionContent = {599id: 'test-session',600history: [],601hasActiveResponseCallback: false,602hasRequestHandler: false,603options: {604'models': 'gpt-4',605'region': { id: 'us-east', name: 'US East' }606}607};608609(proxy.$provideChatSessionContent as sinon.SinonStub).resolves(sessionContent);610611const resource = URI.parse(`${sessionScheme}:/test-session`);612await chatSessionsService.getOrCreateChatSession(resource, CancellationToken.None);613614// getSessionOption should return the configured values615assert.strictEqual(chatSessionsService.getSessionOption(resource, 'models'), 'gpt-4');616assert.deepStrictEqual(chatSessionsService.getSessionOption(resource, 'region'), { id: 'us-east', name: 'US East' });617618// getSessionOption should return undefined for options not in the session619assert.strictEqual(chatSessionsService.getSessionOption(resource, 'notConfigured'), undefined);620621mainThread.$unregisterChatSessionContentProvider(1);622});623624test('option change notifications are sent to the extension', async function () {625const sessionScheme = 'test-session-type';626const handle = 1;627628mainThread.$registerChatSessionContentProvider(handle, sessionScheme);629630const sessionContent = {631id: 'test-session',632history: [],633hasActiveResponseCallback: false,634hasRequestHandler: false,635options: {636'models': 'gpt-4'637}638};639640(proxy.$provideChatSessionContent as sinon.SinonStub).resolves(sessionContent);641642const resource = URI.parse(`${sessionScheme}:/test-session`);643await chatSessionsService.getOrCreateChatSession(resource, CancellationToken.None);644645// Clear the stub call history646(proxy.$provideHandleOptionsChange as sinon.SinonStub).resetHistory();647648// Simulate an option change649await chatSessionsService.notifySessionOptionsChange(resource, [650{ optionId: 'models', value: 'gpt-4-turbo' }651]);652653// Verify the extension was notified654assert.ok((proxy.$provideHandleOptionsChange as sinon.SinonStub).calledOnce);655const call = (proxy.$provideHandleOptionsChange as sinon.SinonStub).firstCall;656assert.strictEqual(call.args[0], handle);657assert.deepStrictEqual(call.args[1], resource);658assert.deepStrictEqual(call.args[2], [{ optionId: 'models', value: 'gpt-4-turbo' }]);659660mainThread.$unregisterChatSessionContentProvider(handle);661});662663test('option change notifications fail silently when provider not registered', async function () {664const sessionScheme = 'unregistered-session-type';665666// Do NOT register a content provider for this scheme667668const resource = URI.parse(`${sessionScheme}:/test-session`);669670// Clear any previous calls671(proxy.$provideHandleOptionsChange as sinon.SinonStub).resetHistory();672673// Attempt to notify option change for an unregistered scheme674// This should not throw, but also should not call the proxy675await chatSessionsService.notifySessionOptionsChange(resource, [676{ optionId: 'models', value: 'gpt-4-turbo' }677]);678679// Verify the extension was NOT notified (no provider registered)680assert.strictEqual((proxy.$provideHandleOptionsChange as sinon.SinonStub).callCount, 0);681});682683test('setSessionOption updates option and getSessionOption reflects change', async function () {684const sessionScheme = 'test-session-type';685mainThread.$registerChatSessionContentProvider(1, sessionScheme);686687const sessionContent = {688id: 'test-session',689history: [],690hasActiveResponseCallback: false,691hasRequestHandler: false,692// Start with no options693};694695(proxy.$provideChatSessionContent as sinon.SinonStub).resolves(sessionContent);696697const resource = URI.parse(`${sessionScheme}:/test-session`);698await chatSessionsService.getOrCreateChatSession(resource, CancellationToken.None);699700// Initially no options set701assert.strictEqual(chatSessionsService.getSessionOption(resource, 'models'), undefined);702703// Set an option704chatSessionsService.setSessionOption(resource, 'models', 'gpt-4');705706// Now getSessionOption should return the value707assert.strictEqual(chatSessionsService.getSessionOption(resource, 'models'), 'gpt-4');708709mainThread.$unregisterChatSessionContentProvider(1);710});711712test('hasAnySessionOptions returns correct values', async function () {713const sessionScheme = 'test-session-type';714mainThread.$registerChatSessionContentProvider(1, sessionScheme);715716// Session with options717const sessionContentWithOptions = {718id: 'session-with-options',719history: [],720hasActiveResponseCallback: false,721hasRequestHandler: false,722options: { 'models': 'gpt-4' }723};724725// Session without options726const sessionContentWithoutOptions = {727id: 'session-without-options',728history: [],729hasActiveResponseCallback: false,730hasRequestHandler: false,731};732733(proxy.$provideChatSessionContent as sinon.SinonStub)734.onFirstCall().resolves(sessionContentWithOptions)735.onSecondCall().resolves(sessionContentWithoutOptions);736737const resourceWithOptions = URI.parse(`${sessionScheme}:/session-with-options`);738const resourceWithoutOptions = URI.parse(`${sessionScheme}:/session-without-options`);739740await chatSessionsService.getOrCreateChatSession(resourceWithOptions, CancellationToken.None);741await chatSessionsService.getOrCreateChatSession(resourceWithoutOptions, CancellationToken.None);742743assert.strictEqual(chatSessionsService.hasAnySessionOptions(resourceWithOptions), true);744assert.strictEqual(chatSessionsService.hasAnySessionOptions(resourceWithoutOptions), false);745746mainThread.$unregisterChatSessionContentProvider(1);747});748});749750751