Path: blob/main/src/vs/workbench/contrib/chat/test/common/chatService.test.ts
3296 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 { CancellationToken } from '../../../../../base/common/cancellation.js';7import { Event } from '../../../../../base/common/event.js';8import { MarkdownString } from '../../../../../base/common/htmlContent.js';9import { Disposable } from '../../../../../base/common/lifecycle.js';10import { URI } from '../../../../../base/common/uri.js';11import { assertSnapshot } from '../../../../../base/test/common/snapshot.js';12import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';13import { Range } from '../../../../../editor/common/core/range.js';14import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';15import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js';16import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';17import { IEnvironmentService } from '../../../../../platform/environment/common/environment.js';18import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js';19import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js';20import { MockContextKeyService } from '../../../../../platform/keybinding/test/common/mockKeybindingService.js';21import { ILogService, NullLogService } from '../../../../../platform/log/common/log.js';22import { IStorageService } from '../../../../../platform/storage/common/storage.js';23import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';24import { NullTelemetryService } from '../../../../../platform/telemetry/common/telemetryUtils.js';25import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js';26import { IWorkbenchAssignmentService } from '../../../../services/assignment/common/assignmentService.js';27import { NullWorkbenchAssignmentService } from '../../../../services/assignment/test/common/nullAssignmentService.js';28import { IExtensionService, nullExtensionDescription } from '../../../../services/extensions/common/extensions.js';29import { ILifecycleService } from '../../../../services/lifecycle/common/lifecycle.js';30import { IViewsService } from '../../../../services/views/common/viewsService.js';31import { mock, TestContextService, TestExtensionService, TestStorageService } from '../../../../test/common/workbenchTestServices.js';32import { ChatAgentService, IChatAgent, IChatAgentData, IChatAgentImplementation, IChatAgentService } from '../../common/chatAgents.js';33import { IChatEditingService, IChatEditingSession } from '../../common/chatEditingService.js';34import { IChatModel, ISerializableChatData } from '../../common/chatModel.js';35import { IChatFollowup, IChatService } from '../../common/chatService.js';36import { ChatService } from '../../common/chatServiceImpl.js';37import { ChatSlashCommandService, IChatSlashCommandService } from '../../common/chatSlashCommands.js';38import { IChatVariablesService } from '../../common/chatVariables.js';39import { ChatAgentLocation, ChatModeKind } from '../../common/constants.js';40import { MockChatService } from './mockChatService.js';41import { MockChatVariablesService } from './mockChatVariables.js';42import { IMcpService } from '../../../mcp/common/mcpTypes.js';43import { TestMcpService } from '../../../mcp/test/common/testMcpService.js';4445const chatAgentWithUsedContextId = 'ChatProviderWithUsedContext';46const chatAgentWithUsedContext: IChatAgent = {47id: chatAgentWithUsedContextId,48name: chatAgentWithUsedContextId,49extensionId: nullExtensionDescription.identifier,50extensionVersion: undefined,51publisherDisplayName: '',52extensionPublisherId: '',53extensionDisplayName: '',54locations: [ChatAgentLocation.Panel],55modes: [ChatModeKind.Ask],56metadata: {},57slashCommands: [],58disambiguation: [],59async invoke(request, progress, history, token) {60progress([{61documents: [62{63uri: URI.file('/test/path/to/file'),64version: 3,65ranges: [66new Range(1, 1, 2, 2)67]68}69],70kind: 'usedContext'71}]);7273return { metadata: { metadataKey: 'value' } };74},75async provideFollowups(sessionId, token) {76return [{ kind: 'reply', message: 'Something else', agentId: '', tooltip: 'a tooltip' } satisfies IChatFollowup];77},78};7980const chatAgentWithMarkdownId = 'ChatProviderWithMarkdown';81const chatAgentWithMarkdown: IChatAgent = {82id: chatAgentWithMarkdownId,83name: chatAgentWithMarkdownId,84extensionId: nullExtensionDescription.identifier,85extensionVersion: undefined,86publisherDisplayName: '',87extensionPublisherId: '',88extensionDisplayName: '',89locations: [ChatAgentLocation.Panel],90modes: [ChatModeKind.Ask],91metadata: {},92slashCommands: [],93disambiguation: [],94async invoke(request, progress, history, token) {95progress([{ kind: 'markdownContent', content: new MarkdownString('test') }]);96return { metadata: { metadataKey: 'value' } };97},98async provideFollowups(sessionId, token) {99return [];100},101};102103function getAgentData(id: string): IChatAgentData {104return {105name: id,106id: id,107extensionId: nullExtensionDescription.identifier,108extensionVersion: undefined,109extensionPublisherId: '',110publisherDisplayName: '',111extensionDisplayName: '',112locations: [ChatAgentLocation.Panel],113modes: [ChatModeKind.Ask],114metadata: {},115slashCommands: [],116disambiguation: [],117};118}119120suite('ChatService', () => {121const testDisposables = ensureNoDisposablesAreLeakedInTestSuite();122123let storageService: IStorageService;124let instantiationService: TestInstantiationService;125126let chatAgentService: IChatAgentService;127128setup(async () => {129instantiationService = testDisposables.add(new TestInstantiationService(new ServiceCollection(130[IChatVariablesService, new MockChatVariablesService()],131[IWorkbenchAssignmentService, new NullWorkbenchAssignmentService()],132[IMcpService, new TestMcpService()],133)));134instantiationService.stub(IStorageService, storageService = testDisposables.add(new TestStorageService()));135instantiationService.stub(ILogService, new NullLogService());136instantiationService.stub(ITelemetryService, NullTelemetryService);137instantiationService.stub(IExtensionService, new TestExtensionService());138instantiationService.stub(IContextKeyService, new MockContextKeyService());139instantiationService.stub(IViewsService, new TestExtensionService());140instantiationService.stub(IWorkspaceContextService, new TestContextService());141instantiationService.stub(IChatSlashCommandService, testDisposables.add(instantiationService.createInstance(ChatSlashCommandService)));142instantiationService.stub(IConfigurationService, new TestConfigurationService());143instantiationService.stub(IChatService, new MockChatService());144instantiationService.stub(IEnvironmentService, { workspaceStorageHome: URI.file('/test/path/to/workspaceStorage') });145instantiationService.stub(ILifecycleService, { onWillShutdown: Event.None });146instantiationService.stub(IChatEditingService, new class extends mock<IChatEditingService>() {147override startOrContinueGlobalEditingSession(): Promise<IChatEditingSession> {148return Promise.resolve(Disposable.None as IChatEditingSession);149}150});151152chatAgentService = testDisposables.add(instantiationService.createInstance(ChatAgentService));153instantiationService.stub(IChatAgentService, chatAgentService);154155const agent: IChatAgentImplementation = {156async invoke(request, progress, history, token) {157return {};158},159};160testDisposables.add(chatAgentService.registerAgent('testAgent', { ...getAgentData('testAgent'), isDefault: true }));161testDisposables.add(chatAgentService.registerAgent(chatAgentWithUsedContextId, getAgentData(chatAgentWithUsedContextId)));162testDisposables.add(chatAgentService.registerAgent(chatAgentWithMarkdownId, getAgentData(chatAgentWithMarkdownId)));163testDisposables.add(chatAgentService.registerAgentImplementation('testAgent', agent));164chatAgentService.updateAgent('testAgent', { requester: { name: 'test' } });165});166167test('retrieveSession', async () => {168const testService = testDisposables.add(instantiationService.createInstance(ChatService));169const session1 = testDisposables.add(testService.startSession(ChatAgentLocation.Panel, CancellationToken.None));170session1.addRequest({ parts: [], text: 'request 1' }, { variables: [] }, 0);171172const session2 = testDisposables.add(testService.startSession(ChatAgentLocation.Panel, CancellationToken.None));173session2.addRequest({ parts: [], text: 'request 2' }, { variables: [] }, 0);174175storageService.flush();176const testService2 = testDisposables.add(instantiationService.createInstance(ChatService));177const retrieved1 = testDisposables.add((await testService2.getOrRestoreSession(session1.sessionId))!);178const retrieved2 = testDisposables.add((await testService2.getOrRestoreSession(session2.sessionId))!);179assert.deepStrictEqual(retrieved1.getRequests()[0]?.message.text, 'request 1');180assert.deepStrictEqual(retrieved2.getRequests()[0]?.message.text, 'request 2');181});182183test('addCompleteRequest', async () => {184const testService = testDisposables.add(instantiationService.createInstance(ChatService));185186const model = testDisposables.add(testService.startSession(ChatAgentLocation.Panel, CancellationToken.None));187assert.strictEqual(model.getRequests().length, 0);188189await testService.addCompleteRequest(model.sessionId, 'test request', undefined, 0, { message: 'test response' });190assert.strictEqual(model.getRequests().length, 1);191assert.ok(model.getRequests()[0].response);192assert.strictEqual(model.getRequests()[0].response?.response.toString(), 'test response');193});194195test('sendRequest fails', async () => {196const testService = testDisposables.add(instantiationService.createInstance(ChatService));197198const model = testDisposables.add(testService.startSession(ChatAgentLocation.Panel, CancellationToken.None));199const response = await testService.sendRequest(model.sessionId, `@${chatAgentWithUsedContextId} test request`);200assert(response);201await response.responseCompletePromise;202203await assertSnapshot(toSnapshotExportData(model));204});205206test('history', async () => {207const historyLengthAgent: IChatAgentImplementation = {208async invoke(request, progress, history, token) {209return {210metadata: { historyLength: history.length }211};212},213};214215testDisposables.add(chatAgentService.registerAgent('defaultAgent', { ...getAgentData('defaultAgent'), isDefault: true }));216testDisposables.add(chatAgentService.registerAgent('agent2', getAgentData('agent2')));217testDisposables.add(chatAgentService.registerAgentImplementation('defaultAgent', historyLengthAgent));218testDisposables.add(chatAgentService.registerAgentImplementation('agent2', historyLengthAgent));219220const testService = testDisposables.add(instantiationService.createInstance(ChatService));221const model = testDisposables.add(testService.startSession(ChatAgentLocation.Panel, CancellationToken.None));222223// Send a request to default agent224const response = await testService.sendRequest(model.sessionId, `test request`, { agentId: 'defaultAgent' });225assert(response);226await response.responseCompletePromise;227assert.strictEqual(model.getRequests().length, 1);228assert.strictEqual(model.getRequests()[0].response?.result?.metadata?.historyLength, 0);229230// Send a request to agent2- it can't see the default agent's message231const response2 = await testService.sendRequest(model.sessionId, `test request`, { agentId: 'agent2' });232assert(response2);233await response2.responseCompletePromise;234assert.strictEqual(model.getRequests().length, 2);235assert.strictEqual(model.getRequests()[1].response?.result?.metadata?.historyLength, 0);236237// Send a request to defaultAgent - the default agent can see agent2's message238const response3 = await testService.sendRequest(model.sessionId, `test request`, { agentId: 'defaultAgent' });239assert(response3);240await response3.responseCompletePromise;241assert.strictEqual(model.getRequests().length, 3);242assert.strictEqual(model.getRequests()[2].response?.result?.metadata?.historyLength, 2);243});244245test('can serialize', async () => {246testDisposables.add(chatAgentService.registerAgentImplementation(chatAgentWithUsedContextId, chatAgentWithUsedContext));247chatAgentService.updateAgent(chatAgentWithUsedContextId, { requester: { name: 'test' } });248const testService = testDisposables.add(instantiationService.createInstance(ChatService));249250const model = testDisposables.add(testService.startSession(ChatAgentLocation.Panel, CancellationToken.None));251assert.strictEqual(model.getRequests().length, 0);252253await assertSnapshot(toSnapshotExportData(model));254255const response = await testService.sendRequest(model.sessionId, `@${chatAgentWithUsedContextId} test request`);256assert(response);257await response.responseCompletePromise;258assert.strictEqual(model.getRequests().length, 1);259260const response2 = await testService.sendRequest(model.sessionId, `test request 2`);261assert(response2);262await response2.responseCompletePromise;263assert.strictEqual(model.getRequests().length, 2);264265await assertSnapshot(toSnapshotExportData(model));266});267268test('can deserialize', async () => {269let serializedChatData: ISerializableChatData;270testDisposables.add(chatAgentService.registerAgentImplementation(chatAgentWithUsedContextId, chatAgentWithUsedContext));271272// create the first service, send request, get response, and serialize the state273{ // serapate block to not leak variables in outer scope274const testService = testDisposables.add(instantiationService.createInstance(ChatService));275276const chatModel1 = testDisposables.add(testService.startSession(ChatAgentLocation.Panel, CancellationToken.None));277assert.strictEqual(chatModel1.getRequests().length, 0);278279const response = await testService.sendRequest(chatModel1.sessionId, `@${chatAgentWithUsedContextId} test request`);280assert(response);281282await response.responseCompletePromise;283284serializedChatData = JSON.parse(JSON.stringify(chatModel1));285}286287// try deserializing the state into a new service288289const testService2 = testDisposables.add(instantiationService.createInstance(ChatService));290291const chatModel2 = testService2.loadSessionFromContent(serializedChatData);292assert(chatModel2);293294await assertSnapshot(toSnapshotExportData(chatModel2));295chatModel2.dispose();296});297298test('can deserialize with response', async () => {299let serializedChatData: ISerializableChatData;300testDisposables.add(chatAgentService.registerAgentImplementation(chatAgentWithMarkdownId, chatAgentWithMarkdown));301302{303const testService = testDisposables.add(instantiationService.createInstance(ChatService));304305const chatModel1 = testDisposables.add(testService.startSession(ChatAgentLocation.Panel, CancellationToken.None));306assert.strictEqual(chatModel1.getRequests().length, 0);307308const response = await testService.sendRequest(chatModel1.sessionId, `@${chatAgentWithUsedContextId} test request`);309assert(response);310311await response.responseCompletePromise;312313serializedChatData = JSON.parse(JSON.stringify(chatModel1));314}315316// try deserializing the state into a new service317318const testService2 = testDisposables.add(instantiationService.createInstance(ChatService));319320const chatModel2 = testService2.loadSessionFromContent(serializedChatData);321assert(chatModel2);322323await assertSnapshot(toSnapshotExportData(chatModel2));324chatModel2.dispose();325});326});327328329function toSnapshotExportData(model: IChatModel) {330const exp = model.toExport();331return {332...exp,333requests: exp.requests.map(r => {334return {335...r,336timestamp: undefined,337requestId: undefined, // id contains a random part338responseId: undefined, // id contains a random part339};340})341};342}343344345