Path: blob/main/src/vs/workbench/contrib/chat/test/browser/languageModelToolsService.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 * as assert from 'assert';6import { Barrier } from '../../../../../base/common/async.js';7import { VSBuffer } from '../../../../../base/common/buffer.js';8import { CancellationToken } from '../../../../../base/common/cancellation.js';9import { CancellationError, isCancellationError } from '../../../../../base/common/errors.js';10import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';11import { IAccessibilityService } from '../../../../../platform/accessibility/common/accessibility.js';12import { TestAccessibilityService } from '../../../../../platform/accessibility/test/common/testAccessibilityService.js';13import { AccessibilitySignal, IAccessibilitySignalService } from '../../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js';14import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js';15import { ContextKeyService } from '../../../../../platform/contextkey/browser/contextKeyService.js';16import { ContextKeyEqualsExpr, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';17import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';18import { workbenchInstantiationService } from '../../../../test/browser/workbenchTestServices.js';19import { LanguageModelToolsService } from '../../browser/languageModelToolsService.js';20import { IChatModel } from '../../common/chatModel.js';21import { IChatService, IChatToolInputInvocationData } from '../../common/chatService.js';22import { IToolData, IToolImpl, IToolInvocation, ToolDataSource } from '../../common/languageModelToolsService.js';23import { MockChatService } from '../common/mockChatService.js';24import { IConfigurationChangeEvent } from '../../../../../platform/configuration/common/configuration.js';2526// --- Test helpers to reduce repetition and improve readability ---2728class TestAccessibilitySignalService implements Partial<IAccessibilitySignalService> {29public signalPlayedCalls: { signal: AccessibilitySignal; options?: any }[] = [];3031async playSignal(signal: AccessibilitySignal, options?: any): Promise<void> {32this.signalPlayedCalls.push({ signal, options });33}3435reset() {36this.signalPlayedCalls = [];37}38}3940class TestTelemetryService implements Partial<ITelemetryService> {41public events: Array<{ eventName: string; data: any }> = [];4243publicLog2<E extends Record<string, any>, T extends Record<string, any>>(eventName: string, data?: E): void {44this.events.push({ eventName, data });45}4647reset() {48this.events = [];49}50}5152function registerToolForTest(service: LanguageModelToolsService, store: any, id: string, impl: IToolImpl, data?: Partial<IToolData>) {53const toolData: IToolData = {54id,55modelDescription: data?.modelDescription ?? 'Test Tool',56displayName: data?.displayName ?? 'Test Tool',57source: ToolDataSource.Internal,58...data,59};60store.add(service.registerTool(toolData, impl));61return {62id,63makeDto: (parameters: any, context?: { sessionId: string }, callId: string = '1'): IToolInvocation => ({64callId,65toolId: id,66tokenBudget: 100,67parameters,68context,69}),70};71}7273function stubGetSession(chatService: MockChatService, sessionId: string, options?: { requestId?: string; capture?: { invocation?: any } }): IChatModel {74const requestId = options?.requestId ?? 'requestId';75const capture = options?.capture;76const fakeModel = {77sessionId,78getRequests: () => [{ id: requestId, modelId: 'test-model' }],79acceptResponseProgress: (_req: any, progress: any) => { if (capture) { capture.invocation = progress; } },80} as IChatModel;81chatService.addSession(fakeModel);82return fakeModel;83}8485async function waitForPublishedInvocation(capture: { invocation?: any }, tries = 5): Promise<any> {86for (let i = 0; i < tries && !capture.invocation; i++) {87await Promise.resolve();88}89return capture.invocation;90}9192suite('LanguageModelToolsService', () => {93const store = ensureNoDisposablesAreLeakedInTestSuite();9495let contextKeyService: IContextKeyService;96let service: LanguageModelToolsService;97let chatService: MockChatService;98let configurationService: TestConfigurationService;99100setup(() => {101configurationService = new TestConfigurationService();102const instaService = workbenchInstantiationService({103contextKeyService: () => store.add(new ContextKeyService(configurationService)),104configurationService: () => configurationService105}, store);106contextKeyService = instaService.get(IContextKeyService);107chatService = new MockChatService();108instaService.stub(IChatService, chatService);109service = store.add(instaService.createInstance(LanguageModelToolsService));110});111112test('registerToolData', () => {113const toolData: IToolData = {114id: 'testTool',115modelDescription: 'Test Tool',116displayName: 'Test Tool',117source: ToolDataSource.Internal,118};119120const disposable = service.registerToolData(toolData);121assert.strictEqual(service.getTool('testTool')?.id, 'testTool');122disposable.dispose();123assert.strictEqual(service.getTool('testTool'), undefined);124});125126test('registerToolImplementation', () => {127const toolData: IToolData = {128id: 'testTool',129modelDescription: 'Test Tool',130displayName: 'Test Tool',131source: ToolDataSource.Internal,132};133134store.add(service.registerToolData(toolData));135136const toolImpl: IToolImpl = {137invoke: async () => ({ content: [{ kind: 'text', value: 'result' }] }),138};139140store.add(service.registerToolImplementation('testTool', toolImpl));141assert.strictEqual(service.getTool('testTool')?.id, 'testTool');142});143144test('getTools', () => {145contextKeyService.createKey('testKey', true);146const toolData1: IToolData = {147id: 'testTool1',148modelDescription: 'Test Tool 1',149when: ContextKeyEqualsExpr.create('testKey', false),150displayName: 'Test Tool',151source: ToolDataSource.Internal,152};153154const toolData2: IToolData = {155id: 'testTool2',156modelDescription: 'Test Tool 2',157when: ContextKeyEqualsExpr.create('testKey', true),158displayName: 'Test Tool',159source: ToolDataSource.Internal,160};161162const toolData3: IToolData = {163id: 'testTool3',164modelDescription: 'Test Tool 3',165displayName: 'Test Tool',166source: ToolDataSource.Internal,167};168169store.add(service.registerToolData(toolData1));170store.add(service.registerToolData(toolData2));171store.add(service.registerToolData(toolData3));172173const tools = Array.from(service.getTools());174assert.strictEqual(tools.length, 2);175assert.strictEqual(tools[0].id, 'testTool2');176assert.strictEqual(tools[1].id, 'testTool3');177});178179test('getToolByName', () => {180contextKeyService.createKey('testKey', true);181const toolData1: IToolData = {182id: 'testTool1',183toolReferenceName: 'testTool1',184modelDescription: 'Test Tool 1',185when: ContextKeyEqualsExpr.create('testKey', false),186displayName: 'Test Tool',187source: ToolDataSource.Internal,188};189190const toolData2: IToolData = {191id: 'testTool2',192toolReferenceName: 'testTool2',193modelDescription: 'Test Tool 2',194when: ContextKeyEqualsExpr.create('testKey', true),195displayName: 'Test Tool',196source: ToolDataSource.Internal,197};198199const toolData3: IToolData = {200id: 'testTool3',201toolReferenceName: 'testTool3',202modelDescription: 'Test Tool 3',203displayName: 'Test Tool',204source: ToolDataSource.Internal,205};206207store.add(service.registerToolData(toolData1));208store.add(service.registerToolData(toolData2));209store.add(service.registerToolData(toolData3));210211assert.strictEqual(service.getToolByName('testTool1'), undefined);212assert.strictEqual(service.getToolByName('testTool1', true)?.id, 'testTool1');213assert.strictEqual(service.getToolByName('testTool2')?.id, 'testTool2');214assert.strictEqual(service.getToolByName('testTool3')?.id, 'testTool3');215});216217test('invokeTool', async () => {218const toolData: IToolData = {219id: 'testTool',220modelDescription: 'Test Tool',221displayName: 'Test Tool',222source: ToolDataSource.Internal,223};224225store.add(service.registerToolData(toolData));226227const toolImpl: IToolImpl = {228invoke: async (invocation) => {229assert.strictEqual(invocation.callId, '1');230assert.strictEqual(invocation.toolId, 'testTool');231assert.deepStrictEqual(invocation.parameters, { a: 1 });232return { content: [{ kind: 'text', value: 'result' }] };233}234};235236store.add(service.registerToolImplementation('testTool', toolImpl));237238const dto: IToolInvocation = {239callId: '1',240toolId: 'testTool',241tokenBudget: 100,242parameters: {243a: 1244},245context: undefined,246};247248const result = await service.invokeTool(dto, async () => 0, CancellationToken.None);249assert.strictEqual(result.content[0].value, 'result');250});251252test('invocation parameters are overridden by input toolSpecificData', async () => {253const rawInput = { b: 2 };254const tool = registerToolForTest(service, store, 'testToolInputOverride', {255prepareToolInvocation: async () => ({256toolSpecificData: { kind: 'input', rawInput } satisfies IChatToolInputInvocationData,257confirmationMessages: {258title: 'a',259message: 'b',260}261}),262invoke: async (invocation) => {263// The service should replace parameters with rawInput and strip toolSpecificData264assert.deepStrictEqual(invocation.parameters, rawInput);265assert.strictEqual(invocation.toolSpecificData, undefined);266return { content: [{ kind: 'text', value: 'ok' }] };267},268});269270const sessionId = 'sessionId';271const capture: { invocation?: any } = {};272stubGetSession(chatService, sessionId, { requestId: 'requestId-io', capture });273const dto = tool.makeDto({ a: 1 }, { sessionId });274275const invokeP = service.invokeTool(dto, async () => 0, CancellationToken.None);276const published = await waitForPublishedInvocation(capture);277published.confirmed.complete(true);278const result = await invokeP;279assert.strictEqual(result.content[0].value, 'ok');280});281282test('chat invocation injects input toolSpecificData for confirmation when alwaysDisplayInputOutput', async () => {283const toolData: IToolData = {284id: 'testToolDisplayIO',285modelDescription: 'Test Tool',286displayName: 'Test Tool',287source: ToolDataSource.Internal,288alwaysDisplayInputOutput: true,289};290291const tool = registerToolForTest(service, store, 'testToolDisplayIO', {292prepareToolInvocation: async () => ({293confirmationMessages: { title: 'Confirm', message: 'Proceed?' }294}),295invoke: async () => ({ content: [{ kind: 'text', value: 'done' }] }),296}, toolData);297298const sessionId = 'sessionId-io';299const capture: { invocation?: any } = {};300stubGetSession(chatService, sessionId, { requestId: 'requestId-io', capture });301302const dto = tool.makeDto({ a: 1 }, { sessionId });303304const invokeP = service.invokeTool(dto, async () => 0, CancellationToken.None);305const published = await waitForPublishedInvocation(capture);306assert.ok(published, 'expected ChatToolInvocation to be published');307assert.strictEqual(published.toolId, tool.id);308// The service should have injected input toolSpecificData with the raw parameters309assert.strictEqual(published.toolSpecificData?.kind, 'input');310assert.deepStrictEqual(published.toolSpecificData?.rawInput, dto.parameters);311312// Confirm to let invoke proceed313published.confirmed.complete(true);314const result = await invokeP;315assert.strictEqual(result.content[0].value, 'done');316});317318test('chat invocation waits for user confirmation before invoking', async () => {319const toolData: IToolData = {320id: 'testToolConfirm',321modelDescription: 'Test Tool',322displayName: 'Test Tool',323source: ToolDataSource.Internal,324};325326let invoked = false;327const tool = registerToolForTest(service, store, toolData.id, {328prepareToolInvocation: async () => ({ confirmationMessages: { title: 'Confirm', message: 'Go?' } }),329invoke: async () => {330invoked = true;331return { content: [{ kind: 'text', value: 'ran' }] };332},333}, toolData);334335const sessionId = 'sessionId-confirm';336const capture: { invocation?: any } = {};337stubGetSession(chatService, sessionId, { requestId: 'requestId-confirm', capture });338339const dto = tool.makeDto({ x: 1 }, { sessionId });340341const promise = service.invokeTool(dto, async () => 0, CancellationToken.None);342const published = await waitForPublishedInvocation(capture);343assert.ok(published, 'expected ChatToolInvocation to be published');344assert.strictEqual(invoked, false, 'invoke should not run before confirmation');345346published.confirmed.complete(true);347const result = await promise;348assert.strictEqual(invoked, true, 'invoke should have run after confirmation');349assert.strictEqual(result.content[0].value, 'ran');350});351352test('cancel tool call', async () => {353const toolBarrier = new Barrier();354const tool = registerToolForTest(service, store, 'testTool', {355invoke: async (invocation, countTokens, progress, cancelToken) => {356assert.strictEqual(invocation.callId, '1');357assert.strictEqual(invocation.toolId, 'testTool');358assert.deepStrictEqual(invocation.parameters, { a: 1 });359await toolBarrier.wait();360if (cancelToken.isCancellationRequested) {361throw new CancellationError();362} else {363throw new Error('Tool call should be cancelled');364}365}366});367368const sessionId = 'sessionId';369const requestId = 'requestId';370const dto = tool.makeDto({ a: 1 }, { sessionId });371stubGetSession(chatService, sessionId, { requestId });372const toolPromise = service.invokeTool(dto, async () => 0, CancellationToken.None);373service.cancelToolCallsForRequest(requestId);374toolBarrier.open();375await assert.rejects(toolPromise, err => {376return isCancellationError(err);377}, 'Expected tool call to be cancelled');378});379380test('toToolEnablementMap', () => {381const toolData1: IToolData = {382id: 'tool1',383toolReferenceName: 'refTool1',384modelDescription: 'Test Tool 1',385displayName: 'Test Tool 1',386source: ToolDataSource.Internal,387};388389const toolData2: IToolData = {390id: 'tool2',391toolReferenceName: 'refTool2',392modelDescription: 'Test Tool 2',393displayName: 'Test Tool 2',394source: ToolDataSource.Internal,395};396397const toolData3: IToolData = {398id: 'tool3',399// No toolReferenceName400modelDescription: 'Test Tool 3',401displayName: 'Test Tool 3',402source: ToolDataSource.Internal,403};404405store.add(service.registerToolData(toolData1));406store.add(service.registerToolData(toolData2));407store.add(service.registerToolData(toolData3));408409// Test with enabled tools410const enabledToolNames = new Set(['refTool1']);411const result1 = service.toToolEnablementMap(enabledToolNames);412413assert.strictEqual(result1['tool1'], true, 'tool1 should be enabled');414assert.strictEqual(result1['tool2'], false, 'tool2 should be disabled');415assert.strictEqual(result1['tool3'], false, 'tool3 should be disabled (no reference name)');416417// Test with multiple enabled tools418const multipleEnabledToolNames = new Set(['refTool1', 'refTool2']);419const result2 = service.toToolEnablementMap(multipleEnabledToolNames);420421assert.strictEqual(result2['tool1'], true, 'tool1 should be enabled');422assert.strictEqual(result2['tool2'], true, 'tool2 should be enabled');423assert.strictEqual(result2['tool3'], false, 'tool3 should be disabled');424425// Test with no enabled tools426const noEnabledToolNames = new Set<string>();427const result3 = service.toToolEnablementMap(noEnabledToolNames);428429assert.strictEqual(result3['tool1'], false, 'tool1 should be disabled');430assert.strictEqual(result3['tool2'], false, 'tool2 should be disabled');431assert.strictEqual(result3['tool3'], false, 'tool3 should be disabled');432});433434test('toToolEnablementMap with tool sets', () => {435// Register individual tools436const toolData1: IToolData = {437id: 'tool1',438toolReferenceName: 'refTool1',439modelDescription: 'Test Tool 1',440displayName: 'Test Tool 1',441source: ToolDataSource.Internal,442};443444const toolData2: IToolData = {445id: 'tool2',446modelDescription: 'Test Tool 2',447displayName: 'Test Tool 2',448source: ToolDataSource.Internal,449};450451store.add(service.registerToolData(toolData1));452store.add(service.registerToolData(toolData2));453454// Create a tool set455const toolSet = store.add(service.createToolSet(456ToolDataSource.Internal,457'testToolSet',458'refToolSet',459{ description: 'Test Tool Set' }460));461462// Add tools to the tool set463const toolSetTool1: IToolData = {464id: 'toolSetTool1',465modelDescription: 'Tool Set Tool 1',466displayName: 'Tool Set Tool 1',467source: ToolDataSource.Internal,468};469470const toolSetTool2: IToolData = {471id: 'toolSetTool2',472modelDescription: 'Tool Set Tool 2',473displayName: 'Tool Set Tool 2',474source: ToolDataSource.Internal,475};476477store.add(service.registerToolData(toolSetTool1));478store.add(service.registerToolData(toolSetTool2));479store.add(toolSet.addTool(toolSetTool1));480store.add(toolSet.addTool(toolSetTool2));481482// Test enabling the tool set483const enabledNames = new Set(['refToolSet', 'refTool1']);484const result = service.toToolEnablementMap(enabledNames);485486assert.strictEqual(result['tool1'], true, 'individual tool should be enabled');487assert.strictEqual(result['tool2'], false);488assert.strictEqual(result['toolSetTool1'], true, 'tool set tool 1 should be enabled');489assert.strictEqual(result['toolSetTool2'], true, 'tool set tool 2 should be enabled');490});491492test('toToolEnablementMap with non-existent tool names', () => {493const toolData: IToolData = {494id: 'tool1',495toolReferenceName: 'refTool1',496modelDescription: 'Test Tool 1',497displayName: 'Test Tool 1',498source: ToolDataSource.Internal,499};500501store.add(service.registerToolData(toolData));502503// Test with non-existent tool names504const enabledNames = new Set(['nonExistentTool', 'refTool1']);505const result = service.toToolEnablementMap(enabledNames);506507assert.strictEqual(result['tool1'], true, 'existing tool should be enabled');508// Non-existent tools should not appear in the result map509assert.strictEqual(result['nonExistentTool'], undefined, 'non-existent tool should not be in result');510});511512test('accessibility signal for tool confirmation', async () => {513// Create a test configuration service with proper settings514const testConfigService = new TestConfigurationService();515testConfigService.setUserConfiguration('chat.tools.global.autoApprove', false);516testConfigService.setUserConfiguration('accessibility.signals.chatUserActionRequired', { sound: 'auto', announcement: 'auto' });517518// Create a test accessibility service that simulates screen reader being enabled519const testAccessibilityService = new class extends TestAccessibilityService {520override isScreenReaderOptimized(): boolean { return true; }521}();522523// Create a test accessibility signal service that tracks calls524const testAccessibilitySignalService = new TestAccessibilitySignalService();525526// Create a new service instance with the test services527const instaService = workbenchInstantiationService({528contextKeyService: () => store.add(new ContextKeyService(testConfigService)),529configurationService: () => testConfigService530}, store);531instaService.stub(IChatService, chatService);532instaService.stub(IAccessibilityService, testAccessibilityService);533instaService.stub(IAccessibilitySignalService, testAccessibilitySignalService as unknown as IAccessibilitySignalService);534const testService = store.add(instaService.createInstance(LanguageModelToolsService));535536const toolData: IToolData = {537id: 'testAccessibilityTool',538modelDescription: 'Test Accessibility Tool',539displayName: 'Test Accessibility Tool',540source: ToolDataSource.Internal,541};542543const tool = registerToolForTest(testService, store, toolData.id, {544prepareToolInvocation: async () => ({ confirmationMessages: { title: 'Accessibility Test', message: 'Testing accessibility signal' } }),545invoke: async () => ({ content: [{ kind: 'text', value: 'executed' }] }),546}, toolData);547548const sessionId = 'sessionId-accessibility';549const capture: { invocation?: any } = {};550stubGetSession(chatService, sessionId, { requestId: 'requestId-accessibility', capture });551552const dto = tool.makeDto({ param: 'value' }, { sessionId });553554const promise = testService.invokeTool(dto, async () => 0, CancellationToken.None);555const published = await waitForPublishedInvocation(capture);556557assert.ok(published, 'expected ChatToolInvocation to be published');558assert.ok(published.confirmationMessages, 'should have confirmation messages');559560// The accessibility signal should have been played561assert.strictEqual(testAccessibilitySignalService.signalPlayedCalls.length, 1, 'accessibility signal should have been played once');562const signalCall = testAccessibilitySignalService.signalPlayedCalls[0];563assert.strictEqual(signalCall.signal, AccessibilitySignal.chatUserActionRequired, 'correct signal should be played');564assert.ok(signalCall.options?.customAlertMessage.includes('Accessibility Test'), 'alert message should include tool title');565assert.ok(signalCall.options?.customAlertMessage.includes('Chat confirmation required'), 'alert message should include confirmation text');566567// Complete the invocation568published.confirmed.complete(true);569const result = await promise;570assert.strictEqual(result.content[0].value, 'executed');571});572573test('accessibility signal respects autoApprove configuration', async () => {574// Create a test configuration service with auto-approve enabled575const testConfigService = new TestConfigurationService();576testConfigService.setUserConfiguration('chat.tools.global.autoApprove', true);577testConfigService.setUserConfiguration('accessibility.signals.chatUserActionRequired', { sound: 'auto', announcement: 'auto' });578579// Create a test accessibility service that simulates screen reader being enabled580const testAccessibilityService = new class extends TestAccessibilityService {581override isScreenReaderOptimized(): boolean { return true; }582}();583584// Create a test accessibility signal service that tracks calls585const testAccessibilitySignalService = new TestAccessibilitySignalService();586587// Create a new service instance with the test services588const instaService = workbenchInstantiationService({589contextKeyService: () => store.add(new ContextKeyService(testConfigService)),590configurationService: () => testConfigService591}, store);592instaService.stub(IChatService, chatService);593instaService.stub(IAccessibilityService, testAccessibilityService);594instaService.stub(IAccessibilitySignalService, testAccessibilitySignalService as unknown as IAccessibilitySignalService);595const testService = store.add(instaService.createInstance(LanguageModelToolsService));596597const toolData: IToolData = {598id: 'testAutoApproveTool',599modelDescription: 'Test Auto Approve Tool',600displayName: 'Test Auto Approve Tool',601source: ToolDataSource.Internal,602};603604const tool = registerToolForTest(testService, store, toolData.id, {605prepareToolInvocation: async () => ({ confirmationMessages: { title: 'Auto Approve Test', message: 'Testing auto approve' } }),606invoke: async () => ({ content: [{ kind: 'text', value: 'auto approved' }] }),607}, toolData);608609const sessionId = 'sessionId-auto-approve';610const capture: { invocation?: any } = {};611stubGetSession(chatService, sessionId, { requestId: 'requestId-auto-approve', capture });612613const dto = tool.makeDto({ config: 'test' }, { sessionId });614615// When auto-approve is enabled, tool should complete without user intervention616const result = await testService.invokeTool(dto, async () => 0, CancellationToken.None);617618// Verify the tool completed and no accessibility signal was played619assert.strictEqual(result.content[0].value, 'auto approved');620assert.strictEqual(testAccessibilitySignalService.signalPlayedCalls.length, 0, 'accessibility signal should not be played when auto-approve is enabled');621});622623test('shouldAutoConfirm with basic configuration', async () => {624// Test basic shouldAutoConfirm behavior with simple configuration625const testConfigService = new TestConfigurationService();626testConfigService.setUserConfiguration('chat.tools.global.autoApprove', true); // Global enabled627628const instaService = workbenchInstantiationService({629contextKeyService: () => store.add(new ContextKeyService(testConfigService)),630configurationService: () => testConfigService631}, store);632instaService.stub(IChatService, chatService);633const testService = store.add(instaService.createInstance(LanguageModelToolsService));634635// Register a tool that should be auto-approved636const autoTool = registerToolForTest(testService, store, 'autoTool', {637prepareToolInvocation: async () => ({ confirmationMessages: { title: 'Test', message: 'Should auto-approve' } }),638invoke: async () => ({ content: [{ kind: 'text', value: 'auto approved' }] })639});640641const sessionId = 'test-basic-config';642stubGetSession(chatService, sessionId, { requestId: 'req1' });643644// Tool should be auto-approved (global config = true)645const result = await testService.invokeTool(646autoTool.makeDto({ test: 1 }, { sessionId }),647async () => 0,648CancellationToken.None649);650assert.strictEqual(result.content[0].value, 'auto approved');651});652653test('shouldAutoConfirm with per-tool configuration object', async () => {654// Test per-tool configuration: { toolId: true/false }655const testConfigService = new TestConfigurationService();656testConfigService.setUserConfiguration('chat.tools.global.autoApprove', {657'approvedTool': true,658'deniedTool': false659});660661const instaService = workbenchInstantiationService({662contextKeyService: () => store.add(new ContextKeyService(testConfigService)),663configurationService: () => testConfigService664}, store);665instaService.stub(IChatService, chatService);666const testService = store.add(instaService.createInstance(LanguageModelToolsService));667668// Tool explicitly approved669const approvedTool = registerToolForTest(testService, store, 'approvedTool', {670prepareToolInvocation: async () => ({ confirmationMessages: { title: 'Test', message: 'Should auto-approve' } }),671invoke: async () => ({ content: [{ kind: 'text', value: 'approved' }] })672});673674const sessionId = 'test-per-tool';675stubGetSession(chatService, sessionId, { requestId: 'req1' });676677// Approved tool should auto-approve678const approvedResult = await testService.invokeTool(679approvedTool.makeDto({ test: 1 }, { sessionId }),680async () => 0,681CancellationToken.None682);683assert.strictEqual(approvedResult.content[0].value, 'approved');684685// Test that non-specified tools require confirmation (default behavior)686const unspecifiedTool = registerToolForTest(testService, store, 'unspecifiedTool', {687prepareToolInvocation: async () => ({ confirmationMessages: { title: 'Test', message: 'Should require confirmation' } }),688invoke: async () => ({ content: [{ kind: 'text', value: 'unspecified' }] })689});690691const capture: { invocation?: any } = {};692stubGetSession(chatService, sessionId + '2', { requestId: 'req2', capture });693const unspecifiedPromise = testService.invokeTool(694unspecifiedTool.makeDto({ test: 2 }, { sessionId: sessionId + '2' }),695async () => 0,696CancellationToken.None697);698const published = await waitForPublishedInvocation(capture);699assert.ok(published?.confirmationMessages, 'unspecified tool should require confirmation');700701published.confirmed.complete(true);702const unspecifiedResult = await unspecifiedPromise;703assert.strictEqual(unspecifiedResult.content[0].value, 'unspecified');704});705706test('tool content formatting with alwaysDisplayInputOutput', async () => {707// Test ensureToolDetails, formatToolInput, and toolResultToIO708const toolData: IToolData = {709id: 'formatTool',710modelDescription: 'Format Test Tool',711displayName: 'Format Test Tool',712source: ToolDataSource.Internal,713alwaysDisplayInputOutput: true714};715716const tool = registerToolForTest(service, store, toolData.id, {717prepareToolInvocation: async () => ({}),718invoke: async (invocation) => ({719content: [720{ kind: 'text', value: 'Text result' },721{ kind: 'data', value: { data: VSBuffer.fromByteArray([1, 2, 3]), mimeType: 'application/octet-stream' } }722]723})724}, toolData);725726const input = { a: 1, b: 'test', c: [1, 2, 3] };727const result = await service.invokeTool(728tool.makeDto(input),729async () => 0,730CancellationToken.None731);732733// Should have tool result details because alwaysDisplayInputOutput = true734assert.ok(result.toolResultDetails, 'should have toolResultDetails');735const details = result.toolResultDetails as any; // Type assertion needed for test736737// Test formatToolInput - should be formatted JSON738const expectedInputJson = JSON.stringify(input, undefined, 2);739assert.strictEqual(details.input, expectedInputJson, 'input should be formatted JSON');740741// Test toolResultToIO - should convert different content types742assert.strictEqual(details.output.length, 2, 'should have 2 output items');743744// Text content745const textOutput = details.output[0];746assert.strictEqual(textOutput.type, 'embed');747assert.strictEqual(textOutput.isText, true);748assert.strictEqual(textOutput.value, 'Text result');749750// Data content (base64 encoded)751const dataOutput = details.output[1];752assert.strictEqual(dataOutput.type, 'embed');753assert.strictEqual(dataOutput.mimeType, 'application/octet-stream');754assert.strictEqual(dataOutput.value, 'AQID'); // base64 of [1,2,3]755});756757test('tool error handling and telemetry', async () => {758const testTelemetryService = new TestTelemetryService();759760const instaService = workbenchInstantiationService({761contextKeyService: () => store.add(new ContextKeyService(configurationService)),762configurationService: () => configurationService763}, store);764instaService.stub(IChatService, chatService);765instaService.stub(ITelemetryService, testTelemetryService as any);766const testService = store.add(instaService.createInstance(LanguageModelToolsService));767768// Test successful invocation telemetry769const successTool = registerToolForTest(testService, store, 'successTool', {770prepareToolInvocation: async () => ({}),771invoke: async () => ({ content: [{ kind: 'text', value: 'success' }] })772});773774const sessionId = 'telemetry-test';775stubGetSession(chatService, sessionId, { requestId: 'req1' });776777await testService.invokeTool(778successTool.makeDto({ test: 1 }, { sessionId }),779async () => 0,780CancellationToken.None781);782783// Check success telemetry784const successEvents = testTelemetryService.events.filter(e => e.eventName === 'languageModelToolInvoked');785assert.strictEqual(successEvents.length, 1, 'should have success telemetry event');786assert.strictEqual(successEvents[0].data.result, 'success');787assert.strictEqual(successEvents[0].data.toolId, 'successTool');788assert.strictEqual(successEvents[0].data.chatSessionId, sessionId);789790testTelemetryService.reset();791792// Test error telemetry793const errorTool = registerToolForTest(testService, store, 'errorTool', {794prepareToolInvocation: async () => ({}),795invoke: async () => { throw new Error('Tool error'); }796});797798stubGetSession(chatService, sessionId + '2', { requestId: 'req2' });799800try {801await testService.invokeTool(802errorTool.makeDto({ test: 2 }, { sessionId: sessionId + '2' }),803async () => 0,804CancellationToken.None805);806assert.fail('Should have thrown');807} catch (err) {808// Expected809}810811// Check error telemetry812const errorEvents = testTelemetryService.events.filter(e => e.eventName === 'languageModelToolInvoked');813assert.strictEqual(errorEvents.length, 1, 'should have error telemetry event');814assert.strictEqual(errorEvents[0].data.result, 'error');815assert.strictEqual(errorEvents[0].data.toolId, 'errorTool');816});817818test('call tracking and cleanup', async () => {819// Test that cancelToolCallsForRequest method exists and can be called820// (The detailed cancellation behavior is already tested in "cancel tool call" test)821const sessionId = 'tracking-session';822const requestId = 'tracking-request';823stubGetSession(chatService, sessionId, { requestId });824825// Just verify the method exists and doesn't throw826assert.doesNotThrow(() => {827service.cancelToolCallsForRequest(requestId);828}, 'cancelToolCallsForRequest should not throw');829830// Verify calling with non-existent request ID doesn't throw831assert.doesNotThrow(() => {832service.cancelToolCallsForRequest('non-existent-request');833}, 'cancelToolCallsForRequest with non-existent ID should not throw');834});835836test('accessibility signal with different settings combinations', async () => {837const testAccessibilitySignalService = new TestAccessibilitySignalService();838839// Test case 1: Sound enabled, announcement disabled, screen reader off840const testConfigService1 = new TestConfigurationService();841testConfigService1.setUserConfiguration('chat.tools.global.autoApprove', false);842testConfigService1.setUserConfiguration('accessibility.signals.chatUserActionRequired', { sound: 'on', announcement: 'off' });843844const testAccessibilityService1 = new class extends TestAccessibilityService {845override isScreenReaderOptimized(): boolean { return false; }846}();847848const instaService1 = workbenchInstantiationService({849contextKeyService: () => store.add(new ContextKeyService(testConfigService1)),850configurationService: () => testConfigService1851}, store);852instaService1.stub(IChatService, chatService);853instaService1.stub(IAccessibilityService, testAccessibilityService1);854instaService1.stub(IAccessibilitySignalService, testAccessibilitySignalService as unknown as IAccessibilitySignalService);855const testService1 = store.add(instaService1.createInstance(LanguageModelToolsService));856857const tool1 = registerToolForTest(testService1, store, 'soundOnlyTool', {858prepareToolInvocation: async () => ({ confirmationMessages: { title: 'Sound Test', message: 'Testing sound only' } }),859invoke: async () => ({ content: [{ kind: 'text', value: 'executed' }] })860});861862const sessionId1 = 'sound-test';863const capture1: { invocation?: any } = {};864stubGetSession(chatService, sessionId1, { requestId: 'req1', capture: capture1 });865866const promise1 = testService1.invokeTool(tool1.makeDto({ test: 1 }, { sessionId: sessionId1 }), async () => 0, CancellationToken.None);867const published1 = await waitForPublishedInvocation(capture1);868869// Signal should be played (sound=on, no screen reader requirement)870assert.strictEqual(testAccessibilitySignalService.signalPlayedCalls.length, 1, 'sound should be played when sound=on');871const call1 = testAccessibilitySignalService.signalPlayedCalls[0];872assert.strictEqual(call1.options?.modality, undefined, 'should use default modality for sound');873874published1.confirmed.complete(true);875await promise1;876877testAccessibilitySignalService.reset();878879// Test case 2: Sound auto, announcement auto, screen reader on880const testConfigService2 = new TestConfigurationService();881testConfigService2.setUserConfiguration('chat.tools.global.autoApprove', false);882testConfigService2.setUserConfiguration('accessibility.signals.chatUserActionRequired', { sound: 'auto', announcement: 'auto' });883884const testAccessibilityService2 = new class extends TestAccessibilityService {885override isScreenReaderOptimized(): boolean { return true; }886}();887888const instaService2 = workbenchInstantiationService({889contextKeyService: () => store.add(new ContextKeyService(testConfigService2)),890configurationService: () => testConfigService2891}, store);892instaService2.stub(IChatService, chatService);893instaService2.stub(IAccessibilityService, testAccessibilityService2);894instaService2.stub(IAccessibilitySignalService, testAccessibilitySignalService as unknown as IAccessibilitySignalService);895const testService2 = store.add(instaService2.createInstance(LanguageModelToolsService));896897const tool2 = registerToolForTest(testService2, store, 'autoScreenReaderTool', {898prepareToolInvocation: async () => ({ confirmationMessages: { title: 'Auto Test', message: 'Testing auto with screen reader' } }),899invoke: async () => ({ content: [{ kind: 'text', value: 'executed' }] })900});901902const sessionId2 = 'auto-sr-test';903const capture2: { invocation?: any } = {};904stubGetSession(chatService, sessionId2, { requestId: 'req2', capture: capture2 });905906const promise2 = testService2.invokeTool(tool2.makeDto({ test: 2 }, { sessionId: sessionId2 }), async () => 0, CancellationToken.None);907const published2 = await waitForPublishedInvocation(capture2);908909// Signal should be played (both sound and announcement enabled for screen reader)910assert.strictEqual(testAccessibilitySignalService.signalPlayedCalls.length, 1, 'signal should be played with screen reader optimization');911const call2 = testAccessibilitySignalService.signalPlayedCalls[0];912assert.ok(call2.options?.customAlertMessage, 'should have custom alert message');913assert.strictEqual(call2.options?.userGesture, true, 'should mark as user gesture');914915published2.confirmed.complete(true);916await promise2;917918testAccessibilitySignalService.reset();919920// Test case 3: Sound off, announcement off - no signal921const testConfigService3 = new TestConfigurationService();922testConfigService3.setUserConfiguration('chat.tools.global.autoApprove', false);923testConfigService3.setUserConfiguration('accessibility.signals.chatUserActionRequired', { sound: 'off', announcement: 'off' });924925const testAccessibilityService3 = new class extends TestAccessibilityService {926override isScreenReaderOptimized(): boolean { return true; }927}();928929const instaService3 = workbenchInstantiationService({930contextKeyService: () => store.add(new ContextKeyService(testConfigService3)),931configurationService: () => testConfigService3932}, store);933instaService3.stub(IChatService, chatService);934instaService3.stub(IAccessibilityService, testAccessibilityService3);935instaService3.stub(IAccessibilitySignalService, testAccessibilitySignalService as unknown as IAccessibilitySignalService);936const testService3 = store.add(instaService3.createInstance(LanguageModelToolsService));937938const tool3 = registerToolForTest(testService3, store, 'offTool', {939prepareToolInvocation: async () => ({ confirmationMessages: { title: 'Off Test', message: 'Testing off settings' } }),940invoke: async () => ({ content: [{ kind: 'text', value: 'executed' }] })941});942943const sessionId3 = 'off-test';944const capture3: { invocation?: any } = {};945stubGetSession(chatService, sessionId3, { requestId: 'req3', capture: capture3 });946947const promise3 = testService3.invokeTool(tool3.makeDto({ test: 3 }, { sessionId: sessionId3 }), async () => 0, CancellationToken.None);948const published3 = await waitForPublishedInvocation(capture3);949950// No signal should be played951assert.strictEqual(testAccessibilitySignalService.signalPlayedCalls.length, 0, 'no signal should be played when both sound and announcement are off');952953published3.confirmed.complete(true);954await promise3;955});956957test('setToolAutoConfirmation and getToolAutoConfirmation', () => {958const toolId = 'testAutoConfirmTool';959960// Initially should be 'never'961assert.strictEqual(service.getToolAutoConfirmation(toolId), 'never');962963// Set to workspace scope964service.setToolAutoConfirmation(toolId, 'workspace');965assert.strictEqual(service.getToolAutoConfirmation(toolId), 'workspace');966967// Set to profile scope968service.setToolAutoConfirmation(toolId, 'profile');969assert.strictEqual(service.getToolAutoConfirmation(toolId), 'profile');970971// Set to session scope972service.setToolAutoConfirmation(toolId, 'session');973assert.strictEqual(service.getToolAutoConfirmation(toolId), 'session');974975// Set back to never976service.setToolAutoConfirmation(toolId, 'never');977assert.strictEqual(service.getToolAutoConfirmation(toolId), 'never');978});979980test('resetToolAutoConfirmation', () => {981const toolId1 = 'testTool1';982const toolId2 = 'testTool2';983984// Set different auto-confirmations985service.setToolAutoConfirmation(toolId1, 'workspace');986service.setToolAutoConfirmation(toolId2, 'session');987988// Verify they're set989assert.strictEqual(service.getToolAutoConfirmation(toolId1), 'workspace');990assert.strictEqual(service.getToolAutoConfirmation(toolId2), 'session');991992// Reset all993service.resetToolAutoConfirmation();994995// Should all be back to 'never'996assert.strictEqual(service.getToolAutoConfirmation(toolId1), 'never');997assert.strictEqual(service.getToolAutoConfirmation(toolId2), 'never');998});9991000test('createToolSet and getToolSet', () => {1001const toolSet = store.add(service.createToolSet(1002ToolDataSource.Internal,1003'testToolSetId',1004'testToolSetName',1005{ icon: undefined, description: 'Test tool set' }1006));10071008// Should be able to retrieve by ID1009const retrieved = service.getToolSet('testToolSetId');1010assert.ok(retrieved);1011assert.strictEqual(retrieved.id, 'testToolSetId');1012assert.strictEqual(retrieved.referenceName, 'testToolSetName');10131014// Should not find non-existent tool set1015assert.strictEqual(service.getToolSet('nonExistentId'), undefined);10161017// Dispose should remove it1018toolSet.dispose();1019assert.strictEqual(service.getToolSet('testToolSetId'), undefined);1020});10211022test('getToolSetByName', () => {1023store.add(service.createToolSet(1024ToolDataSource.Internal,1025'toolSet1',1026'refName1'1027));10281029store.add(service.createToolSet(1030ToolDataSource.Internal,1031'toolSet2',1032'refName2'1033));10341035// Should find by reference name1036assert.strictEqual(service.getToolSetByName('refName1')?.id, 'toolSet1');1037assert.strictEqual(service.getToolSetByName('refName2')?.id, 'toolSet2');10381039// Should not find non-existent name1040assert.strictEqual(service.getToolSetByName('nonExistentName'), undefined);1041});10421043test('getTools with includeDisabled parameter', () => {1044// Test the includeDisabled parameter behavior with context keys1045contextKeyService.createKey('testKey', false);1046const disabledTool: IToolData = {1047id: 'disabledTool',1048modelDescription: 'Disabled Tool',1049displayName: 'Disabled Tool',1050source: ToolDataSource.Internal,1051when: ContextKeyEqualsExpr.create('testKey', true), // Will be disabled since testKey is false1052};10531054const enabledTool: IToolData = {1055id: 'enabledTool',1056modelDescription: 'Enabled Tool',1057displayName: 'Enabled Tool',1058source: ToolDataSource.Internal,1059};10601061store.add(service.registerToolData(disabledTool));1062store.add(service.registerToolData(enabledTool));10631064const enabledTools = Array.from(service.getTools());1065assert.strictEqual(enabledTools.length, 1, 'Should only return enabled tools');1066assert.strictEqual(enabledTools[0].id, 'enabledTool');10671068const allTools = Array.from(service.getTools(true));1069assert.strictEqual(allTools.length, 2, 'includeDisabled should return all tools');1070});10711072test('tool registration duplicate error', () => {1073const toolData: IToolData = {1074id: 'duplicateTool',1075modelDescription: 'Duplicate Tool',1076displayName: 'Duplicate Tool',1077source: ToolDataSource.Internal,1078};10791080// First registration should succeed1081store.add(service.registerToolData(toolData));10821083// Second registration should throw1084assert.throws(() => {1085service.registerToolData(toolData);1086}, /Tool "duplicateTool" is already registered/);1087});10881089test('tool implementation registration without data throws', () => {1090const toolImpl: IToolImpl = {1091invoke: async () => ({ content: [] }),1092};10931094// Should throw when registering implementation for non-existent tool1095assert.throws(() => {1096service.registerToolImplementation('nonExistentTool', toolImpl);1097}, /Tool "nonExistentTool" was not contributed/);1098});10991100test('tool implementation duplicate registration throws', () => {1101const toolData: IToolData = {1102id: 'testTool',1103modelDescription: 'Test Tool',1104displayName: 'Test Tool',1105source: ToolDataSource.Internal,1106};11071108const toolImpl1: IToolImpl = {1109invoke: async () => ({ content: [] }),1110};11111112const toolImpl2: IToolImpl = {1113invoke: async () => ({ content: [] }),1114};11151116store.add(service.registerToolData(toolData));1117store.add(service.registerToolImplementation('testTool', toolImpl1));11181119// Second implementation should throw1120assert.throws(() => {1121service.registerToolImplementation('testTool', toolImpl2);1122}, /Tool "testTool" already has an implementation/);1123});11241125test('invokeTool with unknown tool throws', async () => {1126const dto: IToolInvocation = {1127callId: '1',1128toolId: 'unknownTool',1129tokenBudget: 100,1130parameters: {},1131context: undefined,1132};11331134await assert.rejects(1135service.invokeTool(dto, async () => 0, CancellationToken.None),1136/Tool unknownTool was not contributed/1137);1138});11391140test('invokeTool without implementation activates extension and throws if still not found', async () => {1141const toolData: IToolData = {1142id: 'extensionActivationTool',1143modelDescription: 'Extension Tool',1144displayName: 'Extension Tool',1145source: ToolDataSource.Internal,1146};11471148store.add(service.registerToolData(toolData));11491150const dto: IToolInvocation = {1151callId: '1',1152toolId: 'extensionActivationTool',1153tokenBudget: 100,1154parameters: {},1155context: undefined,1156};11571158// Should throw after attempting extension activation1159await assert.rejects(1160service.invokeTool(dto, async () => 0, CancellationToken.None),1161/Tool extensionActivationTool does not have an implementation registered/1162);1163});11641165test('invokeTool without context (non-chat scenario)', async () => {1166const tool = registerToolForTest(service, store, 'nonChatTool', {1167invoke: async (invocation) => {1168assert.strictEqual(invocation.context, undefined);1169return { content: [{ kind: 'text', value: 'non-chat result' }] };1170}1171});11721173const dto = tool.makeDto({ test: 1 }); // No context11741175const result = await service.invokeTool(dto, async () => 0, CancellationToken.None);1176assert.strictEqual(result.content[0].value, 'non-chat result');1177});11781179test('invokeTool with unknown chat session throws', async () => {1180const tool = registerToolForTest(service, store, 'unknownSessionTool', {1181invoke: async () => ({ content: [{ kind: 'text', value: 'should not reach' }] })1182});11831184const dto = tool.makeDto({ test: 1 }, { sessionId: 'unknownSession' });11851186// Test that it throws, regardless of exact error message1187let threwError = false;1188try {1189await service.invokeTool(dto, async () => 0, CancellationToken.None);1190} catch (err) {1191threwError = true;1192// Verify it's one of the expected error types1193assert.ok(1194err instanceof Error && (1195err.message.includes('Tool called for unknown chat session') ||1196err.message.includes('getRequests is not a function')1197),1198`Unexpected error: ${err.message}`1199);1200}1201assert.strictEqual(threwError, true, 'Should have thrown an error');1202});12031204test('tool error with alwaysDisplayInputOutput includes details', async () => {1205const toolData: IToolData = {1206id: 'errorToolWithIO',1207modelDescription: 'Error Tool With IO',1208displayName: 'Error Tool With IO',1209source: ToolDataSource.Internal,1210alwaysDisplayInputOutput: true1211};12121213const tool = registerToolForTest(service, store, toolData.id, {1214invoke: async () => { throw new Error('Tool execution failed'); }1215}, toolData);12161217const input = { param: 'testValue' };12181219try {1220await service.invokeTool(1221tool.makeDto(input),1222async () => 0,1223CancellationToken.None1224);1225assert.fail('Should have thrown');1226} catch (err: any) {1227// The error should bubble up, but we need to check if toolResultError is set1228// This tests the internal error handling path1229assert.strictEqual(err.message, 'Tool execution failed');1230}1231});12321233test('context key changes trigger tool updates', async () => {1234let changeEventFired = false;1235const disposable = service.onDidChangeTools(() => {1236changeEventFired = true;1237});1238store.add(disposable);12391240// Create a tool with a context key dependency1241contextKeyService.createKey('dynamicKey', false);1242const toolData: IToolData = {1243id: 'contextTool',1244modelDescription: 'Context Tool',1245displayName: 'Context Tool',1246source: ToolDataSource.Internal,1247when: ContextKeyEqualsExpr.create('dynamicKey', true),1248};12491250store.add(service.registerToolData(toolData));12511252// Change the context key value1253contextKeyService.createKey('dynamicKey', true);12541255// Wait a bit for the scheduler1256await new Promise(resolve => setTimeout(resolve, 800));12571258assert.strictEqual(changeEventFired, true, 'onDidChangeTools should fire when context keys change');1259});12601261test('configuration changes trigger tool updates', async () => {1262let changeEventFired = false;1263const disposable = service.onDidChangeTools(() => {1264changeEventFired = true;1265});1266store.add(disposable);12671268// Change the correct configuration key1269configurationService.setUserConfiguration('chat.extensionTools.enabled', false);1270// Fire the configuration change event manually1271configurationService.onDidChangeConfigurationEmitter.fire({ affectsConfiguration: () => true, affectedKeys: new Set(['chat.extensionTools.enabled']) } as any as IConfigurationChangeEvent);12721273// Wait a bit for the scheduler1274await new Promise(resolve => setTimeout(resolve, 800));12751276assert.strictEqual(changeEventFired, true, 'onDidChangeTools should fire when configuration changes');1277});12781279test('toToolAndToolSetEnablementMap with MCP toolset enables contained tools', () => {1280// Create MCP toolset1281const mcpToolSet = store.add(service.createToolSet(1282{ type: 'mcp', label: 'testServer', serverLabel: 'testServer', instructions: undefined, collectionId: 'testCollection', definitionId: 'testDef' },1283'mcpSet',1284'mcpSetRef'1285));12861287const mcpTool: IToolData = {1288id: 'mcpTool',1289modelDescription: 'MCP Tool',1290displayName: 'MCP Tool',1291source: { type: 'mcp', label: 'testServer', serverLabel: 'testServer', instructions: undefined, collectionId: 'testCollection', definitionId: 'testDef' },1292canBeReferencedInPrompt: true,1293toolReferenceName: 'mcpToolRef'1294};12951296store.add(service.registerToolData(mcpTool));1297store.add(mcpToolSet.addTool(mcpTool));12981299// Enable the MCP toolset1300const result = service.toToolAndToolSetEnablementMap(['mcpSetRef']);13011302let toolSetEnabled = false;1303let toolEnabled = false;1304for (const [toolOrSet, enabled] of result) {1305if ('referenceName' in toolOrSet && toolOrSet.referenceName === 'mcpSetRef') {1306toolSetEnabled = enabled;1307}1308if ('id' in toolOrSet && toolOrSet.id === 'mcpTool') {1309toolEnabled = enabled;1310}1311}13121313assert.strictEqual(toolSetEnabled, true, 'MCP toolset should be enabled');1314assert.strictEqual(toolEnabled, true, 'MCP tool should be enabled when its toolset is enabled');1315});13161317test('shouldAutoConfirm with workspace-specific tool configuration', async () => {1318const testConfigService = new TestConfigurationService();1319// Configure per-tool settings at different scopes1320testConfigService.setUserConfiguration('chat.tools.global.autoApprove', { 'workspaceTool': true });13211322const instaService = workbenchInstantiationService({1323contextKeyService: () => store.add(new ContextKeyService(testConfigService)),1324configurationService: () => testConfigService1325}, store);1326instaService.stub(IChatService, chatService);1327const testService = store.add(instaService.createInstance(LanguageModelToolsService));13281329const workspaceTool = registerToolForTest(testService, store, 'workspaceTool', {1330prepareToolInvocation: async () => ({ confirmationMessages: { title: 'Test', message: 'Workspace tool' } }),1331invoke: async () => ({ content: [{ kind: 'text', value: 'workspace result' }] })1332}, { runsInWorkspace: true });13331334const sessionId = 'workspace-test';1335stubGetSession(chatService, sessionId, { requestId: 'req1' });13361337// Should auto-approve based on user configuration1338const result = await testService.invokeTool(1339workspaceTool.makeDto({ test: 1 }, { sessionId }),1340async () => 0,1341CancellationToken.None1342);1343assert.strictEqual(result.content[0].value, 'workspace result');1344});1345});134613471348