Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/test/simulation/tools/toolcall.stest.ts
13394 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
import { assert } from 'console';
6
import * as fs from 'fs';
7
import * as path from 'path';
8
import { CopilotToolMode } from '../../../src/extension/tools/common/toolsRegistry';
9
import { IToolsService } from '../../../src/extension/tools/common/toolsService';
10
import { IConversationOptions } from '../../../src/platform/chat/common/conversationOptions';
11
import { isInExtensionHost } from '../../../src/platform/test/node/isInExtensionHost';
12
import { ITestingServicesAccessor, TestingServiceCollection } from '../../../src/platform/test/node/services';
13
import { SimulationWorkspace } from '../../../src/platform/test/node/simulationWorkspace';
14
import { SpyChatResponseStream } from '../../../src/util/common/test/mockChatResponseStream';
15
import { SimulationWorkspaceExtHost } from '../../base/extHostContext/simulationWorkspaceExtHost';
16
import { ssuite, stest } from '../../base/stest';
17
import { discoverToolsCalls } from '../../e2e/scenarioLoader';
18
import { fetchConversationOptions } from '../../e2e/scenarioTest';
19
20
21
type ArgsPreprocessor = (accessor: ITestingServicesAccessor, args: any, workspaceFoldersFilePaths?: string[]) => Promise<any> | any;
22
23
24
const toolArgsPreprocessors: Record<string, ArgsPreprocessor> = {
25
'get_errors': (_accessor: ITestingServicesAccessor, args: any, workspaceFoldersFilePaths?: string[]) => {
26
const filePaths = (args.filePaths ?? []).map((filePath: string) => {
27
if (path.isAbsolute(filePath)) {
28
return filePath;
29
}
30
// Use the first workspace folder as base path if available
31
return workspaceFoldersFilePaths && workspaceFoldersFilePaths.length > 0
32
? path.resolve(workspaceFoldersFilePaths[0], filePath)
33
: filePath;
34
});
35
36
return {
37
...args,
38
filePaths
39
};
40
},
41
'read_file': (_accessor: ITestingServicesAccessor, args: any, workspaceFoldersFilePaths?: string[]) => {
42
assert(args.filePath, 'read_file tool requires a file path to read');
43
const filePath = args.filePath;
44
45
// Convert to absolute path if it's relative and we have workspace folders
46
const resolvedFilePath = path.isAbsolute(filePath) || !workspaceFoldersFilePaths || workspaceFoldersFilePaths.length === 0
47
? filePath
48
: path.resolve(workspaceFoldersFilePaths[0], filePath);
49
50
return {
51
...args,
52
filePath: resolvedFilePath
53
};
54
},
55
// Add more tool-specific preprocessors here as needed
56
// 'another_tool': (args: any, runtime: ISimulationTestRuntime) => { ... }
57
};
58
59
60
ssuite({ title: 'tooltest', subtitle: 'toolcall', location: 'panel' }, (inputPath) => {
61
// This test suite simulates the execution of tools in a controlled environment
62
if (!inputPath) {
63
return;
64
}
65
66
const toolCallsFolder = inputPath;
67
const scenarios = discoverToolsCalls(toolCallsFolder);
68
for (const scenario of scenarios) {
69
let outputFilePath: string | undefined;
70
if (scenario.json.outputPath) {
71
outputFilePath = path.resolve(toolCallsFolder, scenario.json.outputPath);
72
}
73
74
stest({ description: scenario.name }, async (testingServiceCollection: TestingServiceCollection) => {
75
try {
76
const input = scenario.json.toolArgs;
77
78
testingServiceCollection.define(IConversationOptions, fetchConversationOptions());
79
const simulationWorkspace = isInExtensionHost ? new SimulationWorkspaceExtHost() : new SimulationWorkspace();
80
simulationWorkspace.setupServices(testingServiceCollection);
81
82
83
const accessor = testingServiceCollection.createTestingAccessor();
84
simulationWorkspace.resetFromDeserializedWorkspaceState(scenario.getState?.());
85
86
let workspaceFoldersFilePaths: string[] | undefined;
87
if (scenario.stateFilePath) {
88
const stateJson = await fs.promises.readFile(scenario.stateFilePath, 'utf-8');
89
const state = JSON.parse(stateJson);
90
const stateFileDir = path.dirname(scenario.stateFilePath);
91
if (state.workspaceFoldersFilePaths) {
92
workspaceFoldersFilePaths = state.workspaceFoldersFilePaths.map((folder: string) => {
93
if (path.isAbsolute(folder)) {
94
return folder;
95
}
96
return path.resolve(stateFileDir, folder);
97
});
98
}
99
}
100
101
const result = await invokeTool(accessor, input.tool, input.args || {}, workspaceFoldersFilePaths);
102
103
const output = {
104
toolName: input.tool,
105
args: input.args || {},
106
result
107
};
108
109
if (outputFilePath) {
110
await writeOutputFile(outputFilePath, output);
111
} else {
112
console.log('Tool output:', JSON.stringify(output, null, 2));
113
}
114
} catch (error) {
115
const errorOutput = {
116
error: {
117
message: error.message,
118
stack: error.stack
119
}
120
};
121
122
if (outputFilePath) {
123
await writeOutputFile(outputFilePath, errorOutput);
124
}
125
throw error;
126
}
127
});
128
}
129
130
131
async function writeOutputFile(filePath: string, content: any): Promise<void> {
132
try {
133
await fs.promises.writeFile(filePath, JSON.stringify(content, null, 2), 'utf-8');
134
} catch (error) {
135
throw new Error(`Failed to write output file: ${error.message}`);
136
}
137
}
138
139
async function invokeTool(
140
accessor: ITestingServicesAccessor,
141
toolName: string,
142
args: any,
143
workspaceFoldersFilePaths?: string[]
144
) {
145
const token = {
146
isCancellationRequested: false,
147
onCancellationRequested: () => ({ dispose: () => { } })
148
};
149
150
151
152
const toolsService = accessor.get(IToolsService);
153
const tool = toolsService.getCopilotTool(toolName);
154
155
if (!tool) {
156
throw new Error(`Tool not found: ${toolName}`);
157
}
158
159
let processedArgs = args;
160
if (toolArgsPreprocessors[toolName]) {
161
processedArgs = await toolArgsPreprocessors[toolName](accessor, args, workspaceFoldersFilePaths);
162
}
163
164
if (tool.resolveInput) {
165
const context = { stream: new SpyChatResponseStream() } as any;
166
processedArgs = await tool.resolveInput(processedArgs, context, CopilotToolMode.FullContext);
167
}
168
169
return await toolsService.invokeTool(
170
toolName,
171
{
172
input: processedArgs || {},
173
toolInvocationToken: undefined
174
},
175
token
176
);
177
}
178
});
179
180