Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/test/e2e/scenarioLoader.ts
13388 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
6
import assert from 'assert';
7
import * as fs from 'fs';
8
import * as jsoncParser from 'jsonc-parser';
9
import * as path from 'path';
10
import type { ChatResponseFileTreePart, Command } from 'vscode';
11
import { Turn } from '../../src/extension/prompt/common/conversation';
12
import { deserializeWorkbenchState, IDeserializedWorkspaceState } from '../../src/platform/test/node/promptContextModel';
13
import { ITestingServicesAccessor } from '../../src/platform/test/node/services';
14
import { SimulationWorkspace } from '../../src/platform/test/node/simulationWorkspace';
15
16
export interface IConversationTestCase {
17
name: string;
18
question: string;
19
scenarioFolderPath: string;
20
stateFile?: string;
21
setupCase?: (accessor: ITestingServicesAccessor, workspace: SimulationWorkspace) => void | Promise<void>;
22
getState?: () => IDeserializedWorkspaceState;
23
answer?: string;
24
applyChatCodeBlocks?: boolean;
25
json?: any;
26
/**
27
* A map of all tools that should (`true`) and should not (`false`) be used in this request.
28
* For agent mode, these selections are layered on top of the default agent tools.
29
*/
30
tools?: Record<string, boolean>;
31
}
32
33
34
35
export type ScenarioEvaluator = (
36
accessor: ITestingServicesAccessor,
37
question: string,
38
userVisibleAnswer: string,
39
rawResponse: string,
40
turn: Turn | undefined,
41
scenarioIndex: number,
42
commands: Command[],
43
confirmations: { title: string; buttons?: string[] }[],
44
fileTrees: ChatResponseFileTreePart[]
45
) => Promise<{ success: boolean; errorMessage?: string }>;
46
47
export type Scenario = IConversationTestCase[];
48
49
function createTestNameFromPath(folderName: string, fileName: string): string {
50
// Test file is <number>.conversation.json
51
if (/^\d+\.conversation\.json$/.test(fileName)) {
52
return `${folderName}.${fileName}`;
53
}
54
// Test file contains scenario information
55
return fileName;
56
}
57
58
export function fetchConversationScenarios(scenarioFolderPath: string): Scenario[] {
59
// Test files are only in the root so don't have to worry about nested folders
60
const testFiles = fs.readdirSync(scenarioFolderPath).filter(f => f.endsWith('.conversation.json'));
61
62
const scenarios: Scenario[] = [];
63
64
for (const testFile of testFiles) {
65
66
const fileContents = fs.readFileSync(path.join(scenarioFolderPath, testFile), 'utf8');
67
const parsedFile = jsoncParser.parse(fileContents) as unknown;
68
69
assert(parsedFile instanceof Array, 'Expected an array of test cases');
70
assert(parsedFile.every((testCase: any) => typeof testCase === 'object' && typeof testCase.question === 'string'), 'Expected an array of objects with a question property');
71
assert(parsedFile.every((testCase: any) => !testCase.stateFile || typeof testCase.stateFile === 'string'), 'Expected an array of objects with a stateFile property of type string');
72
assert(parsedFile.every((testCase: any) => !testCase.applyChatCodeBlocks || typeof testCase.applyChatCodeBlocks === 'boolean'), 'Expected an array of objects with a applyChatCodeBlocks property of type boolean');
73
const scenario: Scenario = [];
74
for (const testCase of parsedFile) {
75
scenario.push({
76
question: testCase.question,
77
name: createTestNameFromPath(path.basename(scenarioFolderPath), testFile),
78
json: testCase,
79
scenarioFolderPath,
80
stateFile: testCase.stateFile,
81
applyChatCodeBlocks: testCase.applyChatCodeBlocks,
82
getState: testCase.stateFile
83
? () => deserializeWorkbenchState(scenarioFolderPath, path.join(scenarioFolderPath, testCase.stateFile))
84
: undefined,
85
});
86
}
87
scenarios.push(scenario);
88
}
89
return scenarios;
90
}
91
92
export function discoverScenarios(rootFolder: string): Scenario[] {
93
const rootFolderContents = fs.readdirSync(rootFolder, { withFileTypes: true });
94
const containsConversationFile = rootFolderContents.some(f => f.isFile() && f.name.endsWith('.conversation.json'));
95
96
if (containsConversationFile) {
97
return fetchConversationScenarios(rootFolder);
98
} else {
99
const foldersWithScenarios = rootFolderContents.filter(f => f.isDirectory()).map(f => path.join(rootFolder, f.name));
100
if (foldersWithScenarios.length === 0) {
101
return [];
102
}
103
const scenarios = foldersWithScenarios.map(f => discoverScenarios(f));
104
return scenarios.flat();
105
}
106
}
107
108
109
export interface IToolArgsTestCaseInput {
110
tool: string;
111
args?: any;
112
}
113
114
export interface IToolCallScenarioJson {
115
toolArgs: IToolArgsTestCaseInput;
116
name: string;
117
stateFile?: string;
118
outputPath?: string;
119
}
120
export interface IToolCallScenarioTestCase {
121
name: string;
122
scenarioFolderPath: string;
123
getState?: () => IDeserializedWorkspaceState;
124
json: IToolCallScenarioJson;
125
stateFilePath?: string;
126
}
127
128
129
function fetchToolCallScenarios(scenarioFolderPath: string): IToolCallScenarioTestCase[] {
130
const testFiles = fs.readdirSync(scenarioFolderPath).filter(f => f.endsWith('.toolcall.json'));
131
132
const scenarios: IToolCallScenarioTestCase[] = [];
133
134
for (const testFile of testFiles) {
135
const fileContents = fs.readFileSync(path.join(scenarioFolderPath, testFile), 'utf8');
136
const testCase: IToolCallScenarioJson = jsoncParser.parse(fileContents) as IToolCallScenarioJson;
137
assert(testCase instanceof Object, 'Expected an object with toolArgs property');
138
assert(typeof testCase.toolArgs === 'object', 'Expected toolArgs to be an object');
139
assert(typeof testCase.name === 'string', 'Expected name to be a string');
140
assert(testCase.toolArgs.tool, 'Expected toolArgs to have a tool property');
141
assert(typeof testCase.toolArgs.tool === 'string', 'Expected toolArgs.tool to be a string');
142
let stateFile = testCase.stateFile;
143
if (testCase.stateFile) {
144
assert(typeof testCase.stateFile === 'string', 'Expected stateFile to be a string');
145
stateFile = path.join(scenarioFolderPath, testCase.stateFile);
146
}
147
scenarios.push({
148
name: createTestNameFromPath(path.basename(scenarioFolderPath), testFile),
149
json: testCase,
150
scenarioFolderPath,
151
getState: stateFile
152
? () => deserializeWorkbenchState(scenarioFolderPath, stateFile)
153
: undefined,
154
stateFilePath: stateFile,
155
});
156
}
157
return scenarios;
158
}
159
160
export function discoverToolsCalls(rootFolder: string): IToolCallScenarioTestCase[] {
161
const rootFolderContents = fs.readdirSync(rootFolder, { withFileTypes: true });
162
const containsToolArgsFile = rootFolderContents.some(f => f.isFile() && f.name.endsWith('.toolcall.json'));
163
if (containsToolArgsFile) {
164
return fetchToolCallScenarios(rootFolder);
165
}
166
return [];
167
}
168
169