Path: blob/main/extensions/copilot/test/simulation/tools/toolcall.stest.ts
13394 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*--------------------------------------------------------------------------------------------*/4import { assert } from 'console';5import * as fs from 'fs';6import * as path from 'path';7import { CopilotToolMode } from '../../../src/extension/tools/common/toolsRegistry';8import { IToolsService } from '../../../src/extension/tools/common/toolsService';9import { IConversationOptions } from '../../../src/platform/chat/common/conversationOptions';10import { isInExtensionHost } from '../../../src/platform/test/node/isInExtensionHost';11import { ITestingServicesAccessor, TestingServiceCollection } from '../../../src/platform/test/node/services';12import { SimulationWorkspace } from '../../../src/platform/test/node/simulationWorkspace';13import { SpyChatResponseStream } from '../../../src/util/common/test/mockChatResponseStream';14import { SimulationWorkspaceExtHost } from '../../base/extHostContext/simulationWorkspaceExtHost';15import { ssuite, stest } from '../../base/stest';16import { discoverToolsCalls } from '../../e2e/scenarioLoader';17import { fetchConversationOptions } from '../../e2e/scenarioTest';181920type ArgsPreprocessor = (accessor: ITestingServicesAccessor, args: any, workspaceFoldersFilePaths?: string[]) => Promise<any> | any;212223const toolArgsPreprocessors: Record<string, ArgsPreprocessor> = {24'get_errors': (_accessor: ITestingServicesAccessor, args: any, workspaceFoldersFilePaths?: string[]) => {25const filePaths = (args.filePaths ?? []).map((filePath: string) => {26if (path.isAbsolute(filePath)) {27return filePath;28}29// Use the first workspace folder as base path if available30return workspaceFoldersFilePaths && workspaceFoldersFilePaths.length > 031? path.resolve(workspaceFoldersFilePaths[0], filePath)32: filePath;33});3435return {36...args,37filePaths38};39},40'read_file': (_accessor: ITestingServicesAccessor, args: any, workspaceFoldersFilePaths?: string[]) => {41assert(args.filePath, 'read_file tool requires a file path to read');42const filePath = args.filePath;4344// Convert to absolute path if it's relative and we have workspace folders45const resolvedFilePath = path.isAbsolute(filePath) || !workspaceFoldersFilePaths || workspaceFoldersFilePaths.length === 046? filePath47: path.resolve(workspaceFoldersFilePaths[0], filePath);4849return {50...args,51filePath: resolvedFilePath52};53},54// Add more tool-specific preprocessors here as needed55// 'another_tool': (args: any, runtime: ISimulationTestRuntime) => { ... }56};575859ssuite({ title: 'tooltest', subtitle: 'toolcall', location: 'panel' }, (inputPath) => {60// This test suite simulates the execution of tools in a controlled environment61if (!inputPath) {62return;63}6465const toolCallsFolder = inputPath;66const scenarios = discoverToolsCalls(toolCallsFolder);67for (const scenario of scenarios) {68let outputFilePath: string | undefined;69if (scenario.json.outputPath) {70outputFilePath = path.resolve(toolCallsFolder, scenario.json.outputPath);71}7273stest({ description: scenario.name }, async (testingServiceCollection: TestingServiceCollection) => {74try {75const input = scenario.json.toolArgs;7677testingServiceCollection.define(IConversationOptions, fetchConversationOptions());78const simulationWorkspace = isInExtensionHost ? new SimulationWorkspaceExtHost() : new SimulationWorkspace();79simulationWorkspace.setupServices(testingServiceCollection);808182const accessor = testingServiceCollection.createTestingAccessor();83simulationWorkspace.resetFromDeserializedWorkspaceState(scenario.getState?.());8485let workspaceFoldersFilePaths: string[] | undefined;86if (scenario.stateFilePath) {87const stateJson = await fs.promises.readFile(scenario.stateFilePath, 'utf-8');88const state = JSON.parse(stateJson);89const stateFileDir = path.dirname(scenario.stateFilePath);90if (state.workspaceFoldersFilePaths) {91workspaceFoldersFilePaths = state.workspaceFoldersFilePaths.map((folder: string) => {92if (path.isAbsolute(folder)) {93return folder;94}95return path.resolve(stateFileDir, folder);96});97}98}99100const result = await invokeTool(accessor, input.tool, input.args || {}, workspaceFoldersFilePaths);101102const output = {103toolName: input.tool,104args: input.args || {},105result106};107108if (outputFilePath) {109await writeOutputFile(outputFilePath, output);110} else {111console.log('Tool output:', JSON.stringify(output, null, 2));112}113} catch (error) {114const errorOutput = {115error: {116message: error.message,117stack: error.stack118}119};120121if (outputFilePath) {122await writeOutputFile(outputFilePath, errorOutput);123}124throw error;125}126});127}128129130async function writeOutputFile(filePath: string, content: any): Promise<void> {131try {132await fs.promises.writeFile(filePath, JSON.stringify(content, null, 2), 'utf-8');133} catch (error) {134throw new Error(`Failed to write output file: ${error.message}`);135}136}137138async function invokeTool(139accessor: ITestingServicesAccessor,140toolName: string,141args: any,142workspaceFoldersFilePaths?: string[]143) {144const token = {145isCancellationRequested: false,146onCancellationRequested: () => ({ dispose: () => { } })147};148149150151const toolsService = accessor.get(IToolsService);152const tool = toolsService.getCopilotTool(toolName);153154if (!tool) {155throw new Error(`Tool not found: ${toolName}`);156}157158let processedArgs = args;159if (toolArgsPreprocessors[toolName]) {160processedArgs = await toolArgsPreprocessors[toolName](accessor, args, workspaceFoldersFilePaths);161}162163if (tool.resolveInput) {164const context = { stream: new SpyChatResponseStream() } as any;165processedArgs = await tool.resolveInput(processedArgs, context, CopilotToolMode.FullContext);166}167168return await toolsService.invokeTool(169toolName,170{171input: processedArgs || {},172toolInvocationToken: undefined173},174token175);176}177});178179180