Path: blob/main/src/vs/workbench/contrib/chat/test/common/chatModeService.test.ts
5262 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 { timeout } from '../../../../../base/common/async.js';7import { Emitter } from '../../../../../base/common/event.js';8import { URI } from '../../../../../base/common/uri.js';9import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';10import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';11import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';12import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js';13import { MockContextKeyService } from '../../../../../platform/keybinding/test/common/mockKeybindingService.js';14import { ILogService, NullLogService } from '../../../../../platform/log/common/log.js';15import { IStorageService } from '../../../../../platform/storage/common/storage.js';16import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js';17import { TestStorageService } from '../../../../test/common/workbenchTestServices.js';18import { IChatAgentService } from '../../common/participants/chatAgents.js';19import { ChatMode, ChatModeService } from '../../common/chatModes.js';20import { ChatModeKind } from '../../common/constants.js';21import { IAgentSource, ICustomAgent, IPromptsService, PromptsStorage, Target } from '../../common/promptSyntax/service/promptsService.js';22import { MockPromptsService } from './promptSyntax/service/mockPromptsService.js';2324class TestChatAgentService implements Partial<IChatAgentService> {25_serviceBrand: undefined;2627private _hasToolsAgent = true;28private readonly _onDidChangeAgents = new Emitter<any>();2930get hasToolsAgent(): boolean {31return this._hasToolsAgent;32}3334setHasToolsAgent(value: boolean): void {35this._hasToolsAgent = value;36this._onDidChangeAgents.fire(undefined);37}3839readonly onDidChangeAgents = this._onDidChangeAgents.event;40}4142suite('ChatModeService', () => {43const testDisposables = ensureNoDisposablesAreLeakedInTestSuite();4445const workspaceSource: IAgentSource = { storage: PromptsStorage.local };4647let instantiationService: TestInstantiationService;48let promptsService: MockPromptsService;49let chatAgentService: TestChatAgentService;50let storageService: TestStorageService;51let configurationService: TestConfigurationService;52let chatModeService: ChatModeService;5354setup(async () => {55instantiationService = testDisposables.add(new TestInstantiationService());56promptsService = new MockPromptsService();57chatAgentService = new TestChatAgentService();58storageService = testDisposables.add(new TestStorageService());59configurationService = new TestConfigurationService();6061instantiationService.stub(IPromptsService, promptsService);62instantiationService.stub(IChatAgentService, chatAgentService);63instantiationService.stub(IStorageService, storageService);64instantiationService.stub(ILogService, new NullLogService());65instantiationService.stub(IContextKeyService, new MockContextKeyService());66instantiationService.stub(IConfigurationService, configurationService);6768chatModeService = testDisposables.add(instantiationService.createInstance(ChatModeService));69});7071test('should return builtin modes', () => {72const modes = chatModeService.getModes();7374assert.strictEqual(modes.builtin.length, 3);75assert.strictEqual(modes.custom.length, 0);7677// Check that Ask mode is always present78const askMode = modes.builtin.find(mode => mode.id === ChatModeKind.Ask);79assert.ok(askMode);80assert.strictEqual(askMode.label.get(), 'Ask');81assert.strictEqual(askMode.name.get(), 'ask');82assert.strictEqual(askMode.kind, ChatModeKind.Ask);83});8485test('should adjust builtin modes based on tools agent availability', () => {86// Agent mode should always be present regardless of tools agent availability87chatAgentService.setHasToolsAgent(true);88let agents = chatModeService.getModes();89assert.ok(agents.builtin.find(agent => agent.id === ChatModeKind.Agent));9091// Without tools agent - Agent mode should not be present92chatAgentService.setHasToolsAgent(false);93agents = chatModeService.getModes();94assert.strictEqual(agents.builtin.find(agent => agent.id === ChatModeKind.Agent), undefined);9596// Ask and Edit modes should always be present97assert.ok(agents.builtin.find(agent => agent.id === ChatModeKind.Ask));98assert.ok(agents.builtin.find(agent => agent.id === ChatModeKind.Edit));99});100101test('should find builtin modes by id', () => {102const agentMode = chatModeService.findModeById(ChatModeKind.Agent);103assert.ok(agentMode);104assert.strictEqual(agentMode.id, ChatMode.Agent.id);105assert.strictEqual(agentMode.kind, ChatModeKind.Agent);106});107108test('should return undefined for non-existent mode', () => {109const mode = chatModeService.findModeById('non-existent-mode');110assert.strictEqual(mode, undefined);111});112113test('should handle custom modes from prompts service', async () => {114const customMode: ICustomAgent = {115uri: URI.parse('file:///test/custom-mode.md'),116name: 'Test Mode',117description: 'A test custom mode',118tools: ['tool1', 'tool2'],119agentInstructions: { content: 'Custom mode body', toolReferences: [] },120source: workspaceSource,121target: Target.Undefined,122visibility: { userInvokable: true, agentInvokable: true }123};124125promptsService.setCustomModes([customMode]);126127// Wait for the service to refresh128await timeout(0);129130const modes = chatModeService.getModes();131assert.strictEqual(modes.custom.length, 1);132133const testMode = modes.custom[0];134assert.strictEqual(testMode.id, customMode.uri.toString());135assert.strictEqual(testMode.name.get(), customMode.name);136assert.strictEqual(testMode.label.get(), customMode.name);137assert.strictEqual(testMode.description.get(), customMode.description);138assert.strictEqual(testMode.kind, ChatModeKind.Agent);139assert.deepStrictEqual(testMode.customTools?.get(), customMode.tools);140assert.deepStrictEqual(testMode.modeInstructions?.get(), customMode.agentInstructions);141assert.deepStrictEqual(testMode.handOffs?.get(), customMode.handOffs);142assert.strictEqual(testMode.uri?.get().toString(), customMode.uri.toString());143assert.deepStrictEqual(testMode.source, workspaceSource);144});145146test('should fire change event when custom modes are updated', async () => {147let eventFired = false;148testDisposables.add(chatModeService.onDidChangeChatModes(() => {149eventFired = true;150}));151152const customMode: ICustomAgent = {153uri: URI.parse('file:///test/custom-mode.md'),154name: 'Test Mode',155description: 'A test custom mode',156tools: [],157agentInstructions: { content: 'Custom mode body', toolReferences: [] },158source: workspaceSource,159target: Target.Undefined,160visibility: { userInvokable: true, agentInvokable: true }161};162163promptsService.setCustomModes([customMode]);164165// Wait for the event to fire166await timeout(0);167168assert.ok(eventFired);169});170171test('should find custom modes by id', async () => {172const customMode: ICustomAgent = {173uri: URI.parse('file:///test/findable-mode.md'),174name: 'Findable Mode',175description: 'A findable custom mode',176tools: [],177agentInstructions: { content: 'Findable mode body', toolReferences: [] },178source: workspaceSource,179target: Target.Undefined,180visibility: { userInvokable: true, agentInvokable: true }181};182183promptsService.setCustomModes([customMode]);184185// Wait for the service to refresh186await timeout(0);187188const foundMode = chatModeService.findModeById(customMode.uri.toString());189assert.ok(foundMode);190assert.strictEqual(foundMode.id, customMode.uri.toString());191assert.strictEqual(foundMode.name.get(), customMode.name);192assert.strictEqual(foundMode.label.get(), customMode.name);193});194195test('should update existing custom mode instances when data changes', async () => {196const uri = URI.parse('file:///test/updateable-mode.md');197const initialMode: ICustomAgent = {198uri,199name: 'Initial Mode',200description: 'Initial description',201tools: ['tool1'],202agentInstructions: { content: 'Initial body', toolReferences: [] },203model: ['gpt-4'],204source: workspaceSource,205target: Target.Undefined,206visibility: { userInvokable: true, agentInvokable: true }207};208209promptsService.setCustomModes([initialMode]);210await timeout(0);211212const initialModes = chatModeService.getModes();213const initialCustomMode = initialModes.custom[0];214assert.strictEqual(initialCustomMode.description.get(), 'Initial description');215216// Update the mode data217const updatedMode: ICustomAgent = {218...initialMode,219description: 'Updated description',220tools: ['tool1', 'tool2'],221agentInstructions: { content: 'Updated body', toolReferences: [] },222model: ['Updated model']223};224225promptsService.setCustomModes([updatedMode]);226await timeout(0);227228const updatedModes = chatModeService.getModes();229const updatedCustomMode = updatedModes.custom[0];230231// The instance should be the same (reused)232assert.strictEqual(initialCustomMode, updatedCustomMode);233234// But the observable properties should be updated235assert.strictEqual(updatedCustomMode.description.get(), 'Updated description');236assert.deepStrictEqual(updatedCustomMode.customTools?.get(), ['tool1', 'tool2']);237assert.deepStrictEqual(updatedCustomMode.modeInstructions?.get(), { content: 'Updated body', toolReferences: [] });238assert.deepStrictEqual(updatedCustomMode.model?.get(), ['Updated model']);239assert.deepStrictEqual(updatedCustomMode.source, workspaceSource);240});241242test('should remove custom modes that no longer exist', async () => {243const mode1: ICustomAgent = {244uri: URI.parse('file:///test/mode1.md'),245name: 'Mode 1',246description: 'First mode',247tools: [],248agentInstructions: { content: 'Mode 1 body', toolReferences: [] },249source: workspaceSource,250target: Target.Undefined,251visibility: { userInvokable: true, agentInvokable: true }252};253254const mode2: ICustomAgent = {255uri: URI.parse('file:///test/mode2.md'),256name: 'Mode 2',257description: 'Second mode',258tools: [],259agentInstructions: { content: 'Mode 2 body', toolReferences: [] },260source: workspaceSource,261target: Target.Undefined,262visibility: { userInvokable: true, agentInvokable: true }263};264265// Add both modes266promptsService.setCustomModes([mode1, mode2]);267await timeout(0);268269let modes = chatModeService.getModes();270assert.strictEqual(modes.custom.length, 2);271272// Remove one mode273promptsService.setCustomModes([mode1]);274await timeout(0);275276modes = chatModeService.getModes();277assert.strictEqual(modes.custom.length, 1);278assert.strictEqual(modes.custom[0].id, mode1.uri.toString());279});280281});282283284