Path: blob/main/extensions/copilot/src/extension/agents/vscode-node/test/askAgentProvider.spec.ts
13405 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 'chai';6import * as os from 'os';7import * as path from 'path';8import { afterEach, beforeEach, suite, test } from 'vitest';9import * as vscode from 'vscode';10import { ConfigKey, IConfigurationService } from '../../../../platform/configuration/common/configurationService';11import { InMemoryConfigurationService } from '../../../../platform/configuration/test/common/inMemoryConfigurationService';12import { IVSCodeExtensionContext } from '../../../../platform/extContext/common/extensionContext';13import { IFileSystemService } from '../../../../platform/filesystem/common/fileSystemService';14import { MockExtensionContext } from '../../../../platform/test/node/extensionContext';15import { ITestingServicesAccessor } from '../../../../platform/test/node/services';16import { DisposableStore } from '../../../../util/vs/base/common/lifecycle';17import { SyncDescriptor } from '../../../../util/vs/platform/instantiation/common/descriptors';18import { IInstantiationService } from '../../../../util/vs/platform/instantiation/common/instantiation';19import { createExtensionUnitTestingServices } from '../../../test/node/services';20import { AskAgentProvider } from '../askAgentProvider';2122suite('AskAgentProvider', () => {23let disposables: DisposableStore;24let mockConfigurationService: InMemoryConfigurationService;25let fileSystemService: IFileSystemService;26let accessor: ITestingServicesAccessor;27let instantiationService: IInstantiationService;2829beforeEach(() => {30disposables = new DisposableStore();3132const testingServiceCollection = createExtensionUnitTestingServices(disposables);33const globalStoragePath = path.join(os.tmpdir(), 'ask-agent-test-' + Date.now());34testingServiceCollection.define(IVSCodeExtensionContext, new SyncDescriptor(MockExtensionContext, [globalStoragePath]));35accessor = testingServiceCollection.createTestingAccessor();36disposables.add(accessor);37instantiationService = accessor.get(IInstantiationService);3839mockConfigurationService = accessor.get(IConfigurationService) as InMemoryConfigurationService;40fileSystemService = accessor.get(IFileSystemService);41});4243afterEach(() => {44disposables.dispose();45});4647function createProvider() {48const provider = instantiationService.createInstance(AskAgentProvider);49disposables.add(provider);50return provider;51}5253async function getAgentContent(agent: vscode.ChatResource): Promise<string> {54const content = await fileSystemService.readFile(agent.uri);55return new TextDecoder().decode(content);56}5758test('provideCustomAgents() returns an Ask agent with correct structure', async () => {59const provider = createProvider();6061const agents = await provider.provideCustomAgents({}, {} as any);6263assert.equal(agents.length, 1);64assert.ok(agents[0].uri, 'Agent should have a URI');65assert.ok(agents[0].uri.path.endsWith('.agent.md'), 'Agent URI should end with .agent.md');66});6768test('returns agent content with base frontmatter when no settings configured', async () => {69const provider = createProvider();7071const agents = await provider.provideCustomAgents({}, {} as any);7273assert.equal(agents.length, 1);74const content = await getAgentContent(agents[0]);7576// Should contain base read-only tools77assert.ok(content.includes('search'));78assert.ok(content.includes('read'));79assert.ok(content.includes('web'));80assert.ok(content.includes('github/issue_read'));8182// Should NOT contain editing tools8384assert.ok(!content.includes('\'edit'), 'Should not have edit or edit/... tools');85assert.ok(!content.includes('\'execute/run'), 'Should not have any execute/run... tool');8687// Should have correct metadata88assert.ok(content.includes('name: Ask'));89assert.ok(content.includes('description: Answers questions without making changes'));90});9192test('merges additionalTools setting with base tools', async () => {93await mockConfigurationService.setConfig(ConfigKey.AskAgentAdditionalTools, ['customTool1', 'customTool2']);9495const provider = createProvider();96const agents = await provider.provideCustomAgents({}, {} as any);9798assert.equal(agents.length, 1);99const content = await getAgentContent(agents[0]);100101// Should contain base tools102assert.ok(content.includes('search'));103assert.ok(content.includes('read'));104105// Should contain additional tools106assert.ok(content.includes('customTool1'));107assert.ok(content.includes('customTool2'));108});109110test('deduplicates tools when additionalTools overlaps with base tools', async () => {111await mockConfigurationService.setConfig(ConfigKey.AskAgentAdditionalTools, ['search', 'newTool']);112113const provider = createProvider();114const agents = await provider.provideCustomAgents({}, {} as any);115116assert.equal(agents.length, 1);117const content = await getAgentContent(agents[0]);118119// Count occurrences of 'search' in tools list120const toolsMatch = content.match(/tools: \[([^\]]+)\]/);121assert.ok(toolsMatch, 'Tools list not found in agent content');122const toolsSection = toolsMatch[1];123const searchCount = (toolsSection.match(/'search'/g) || []).length;124assert.equal(searchCount, 1, 'search tool should appear only once after deduplication');125126// Should contain new tool127assert.ok(content.includes('newTool'));128});129130test('applies model override from settings', async () => {131await mockConfigurationService.setConfig(ConfigKey.AskAgentModel, 'Claude Haiku 4.5 (copilot)');132133const provider = createProvider();134const agents = await provider.provideCustomAgents({}, {} as any);135136assert.equal(agents.length, 1);137const content = await getAgentContent(agents[0]);138139assert.ok(content.includes('model: Claude Haiku 4.5 (copilot)'));140});141142test('applies both additionalTools and model settings together', async () => {143await mockConfigurationService.setConfig(ConfigKey.AskAgentAdditionalTools, ['extraTool']);144await mockConfigurationService.setConfig(ConfigKey.AskAgentModel, 'claude-3-sonnet');145146const provider = createProvider();147const agents = await provider.provideCustomAgents({}, {} as any);148149assert.equal(agents.length, 1);150const content = await getAgentContent(agents[0]);151152assert.ok(content.includes('extraTool'));153assert.ok(content.includes('model: claude-3-sonnet'));154});155156test('fires onDidChangeCustomAgents when additionalTools setting changes', async () => {157const provider = createProvider();158159let eventFired = false;160provider.onDidChangeCustomAgents(() => {161eventFired = true;162});163164await mockConfigurationService.setConfig(ConfigKey.AskAgentAdditionalTools, ['newTool']);165166assert.equal(eventFired, true);167});168169test('fires onDidChangeCustomAgents when model setting changes', async () => {170const provider = createProvider();171172let eventFired = false;173provider.onDidChangeCustomAgents(() => {174eventFired = true;175});176177await mockConfigurationService.setConfig(ConfigKey.AskAgentModel, 'new-model');178179assert.equal(eventFired, true);180});181182test('does not fire onDidChangeCustomAgents for unrelated setting changes', async () => {183const provider = createProvider();184185let eventFired = false;186provider.onDidChangeCustomAgents(() => {187eventFired = true;188});189190await mockConfigurationService.setConfig(ConfigKey.Advanced.FeedbackOnChange, true);191192assert.equal(eventFired, false);193});194195test('always includes askQuestions tool in generated content', async () => {196const provider = createProvider();197const agents = await provider.provideCustomAgents({}, {} as any);198199assert.equal(agents.length, 1);200const content = await getAgentContent(agents[0]);201202assert.ok(content.includes('vscode/askQuestions'));203});204205test('has correct label property', () => {206const provider = createProvider();207assert.ok(provider.label.includes('Ask'));208});209210test('preserves body content after frontmatter when applying settings', async () => {211await mockConfigurationService.setConfig(ConfigKey.AskAgentModel, 'test-model');212213const provider = createProvider();214const agents = await provider.provideCustomAgents({}, {} as any);215216const content = await getAgentContent(agents[0]);217218assert.ok(content.includes('You are an ASK AGENT'));219assert.ok(content.includes('NEVER modify files or run commands that change state'));220});221222test('handles empty additionalTools array gracefully', async () => {223await mockConfigurationService.setConfig(ConfigKey.AskAgentAdditionalTools, []);224225const provider = createProvider();226const agents = await provider.provideCustomAgents({}, {} as any);227228assert.equal(agents.length, 1);229const content = await getAgentContent(agents[0]);230231// Should have base tools only232assert.ok(content.includes('search'));233assert.ok(content.includes('read'));234});235236test('handles empty model string gracefully', async () => {237await mockConfigurationService.setConfig(ConfigKey.AskAgentModel, '');238239const provider = createProvider();240const agents = await provider.provideCustomAgents({}, {} as any);241242assert.equal(agents.length, 1);243const content = await getAgentContent(agents[0]);244245assert.ok(!content.includes('model:'));246});247248test('does not include handoffs section', async () => {249const provider = createProvider();250const agents = await provider.provideCustomAgents({}, {} as any);251252const content = await getAgentContent(agents[0]);253254assert.ok(!content.includes('handoffs:'), 'Ask agent should not have handoffs');255});256257test('body content instructs not to edit files', async () => {258const provider = createProvider();259const agents = await provider.provideCustomAgents({}, {} as any);260261const content = await getAgentContent(agents[0]);262263assert.ok(content.includes('NEVER modify files'));264assert.ok(content.includes('NEVER use file editing tools'));265});266});267268269