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