Path: blob/main/extensions/copilot/test/e2e/fetchWebPageTool.stest.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 path from 'path';7import { ToolName } from '../../src/extension/tools/common/toolNames';8import { deserializeWorkbenchState } from '../../src/platform/test/node/promptContextModel';9import { ssuite, stest } from '../base/stest';10import { generateToolTestRunner } from './toolSimTest';11import { shouldSkipAgentTests } from './tools.stest';1213interface IFetchWebPageToolParams {14urls: string[];15query?: string;16}1718ssuite.optional(shouldSkipAgentTests, { title: 'fetchWebPageTool', subtitle: 'toolCalling', location: 'panel' }, () => {19const scenarioFolder = path.join(__dirname, '..', 'test/scenarios/test-tools');20const getState = () => deserializeWorkbenchState(scenarioFolder, path.join(scenarioFolder, 'tools.state.json'));2122stest('proper URL validation and query handling', generateToolTestRunner({23question: 'fetch information about React hooks from https://react.dev/reference/react',24scenarioFolderPath: '',25getState,26expectedToolCalls: ToolName.FetchWebPage,27tools: {28[ToolName.FetchWebPage]: true,29[ToolName.FindFiles]: true,30[ToolName.FindTextInFiles]: true,31[ToolName.ReadFile]: true,32[ToolName.EditFile]: true,33[ToolName.Codebase]: true,34[ToolName.ListDirectory]: true,35[ToolName.SearchWorkspaceSymbols]: true,36},37}, {38allowParallelToolCalls: false,39toolCallValidators: {40[ToolName.FetchWebPage]: async (toolCalls) => {41assert.strictEqual(toolCalls.length, 1, 'should make exactly one fetch webpage tool call');42const input = toolCalls[0].input as IFetchWebPageToolParams;4344// Should have exactly 1 URL45assert.ok(Array.isArray(input.urls), 'urls should be an array');46assert.strictEqual(input.urls.length, 1, 'should have exactly 1 URL');4748// Should be the exact URL from the question49const expectedUrl = 'https://react.dev/reference/react';50assert.strictEqual(input.urls[0], expectedUrl, `should have the exact URL: ${expectedUrl}`);5152// Validate query parameter if present53if (input.query !== undefined) {54assert.ok(typeof input.query === 'string', 'query should be a string if provided');55assert.ok(input.query.length > 0, 'query should not be empty if provided');56}57}58}59}));6061stest('multiple URLs handling', generateToolTestRunner({62question: 'get content from https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-0.html and https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function about async/await',63scenarioFolderPath: '',64getState,65expectedToolCalls: ToolName.FetchWebPage,66tools: {67[ToolName.FetchWebPage]: true,68[ToolName.FindFiles]: true,69[ToolName.FindTextInFiles]: true,70[ToolName.ReadFile]: true,71[ToolName.EditFile]: true,72[ToolName.Codebase]: true,73[ToolName.ListDirectory]: true,74[ToolName.SearchWorkspaceSymbols]: true,75},76}, {77allowParallelToolCalls: true,78toolCallValidators: {79[ToolName.FetchWebPage]: (toolCalls) => {80const expectedTypescriptUrl = 'https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-0.html';81const expectedMdnUrl = 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function';82const expectedUrls = [expectedTypescriptUrl, expectedMdnUrl];8384// Allow either 1 tool call with 2 URLs or 2 tool calls with 1 URL each85assert.ok(toolCalls.length === 1 || toolCalls.length === 2, 'should make either 1 or 2 fetch webpage tool calls');8687if (toolCalls.length === 1) {88// Single tool call with multiple URLs89const input = toolCalls[0].input as IFetchWebPageToolParams;9091// Should have exactly 2 URLs92assert.ok(Array.isArray(input.urls), 'urls should be an array');93assert.strictEqual(input.urls.length, 2, 'should have exactly 2 URLs');9495// Check that both expected URLs are present96assert.ok(input.urls.includes(expectedTypescriptUrl), `should include the TypeScript URL: ${expectedTypescriptUrl}`);97assert.ok(input.urls.includes(expectedMdnUrl), `should include the MDN URL: ${expectedMdnUrl}`);9899// Verify no unexpected URLs100input.urls.forEach(url => {101assert.ok(expectedUrls.includes(url), `unexpected URL found: ${url}`);102});103} else {104// Multiple parallel tool calls with one URL each105const allUrls: string[] = [];106toolCalls.forEach(toolCall => {107const input = toolCall.input as IFetchWebPageToolParams;108assert.ok(Array.isArray(input.urls), 'urls should be an array');109assert.strictEqual(input.urls.length, 1, 'each tool call should have exactly 1 URL');110allUrls.push(input.urls[0]);111});112113// Check that both expected URLs are present across all tool calls114assert.ok(allUrls.includes(expectedTypescriptUrl), `should include the TypeScript URL: ${expectedTypescriptUrl}`);115assert.ok(allUrls.includes(expectedMdnUrl), `should include the MDN URL: ${expectedMdnUrl}`);116117// Verify no unexpected URLs118allUrls.forEach(url => {119assert.ok(expectedUrls.includes(url), `unexpected URL found: ${url}`);120});121}122123// Check query parameter for any tool call that has it124toolCalls.forEach(toolCall => {125const input = toolCall.input as IFetchWebPageToolParams;126if (input.query) {127assert.ok(128input.query.toLowerCase().includes('async') || input.query.toLowerCase().includes('await'),129'query should relate to async/await when specifically requested'130);131}132});133}134}135}));136137stest('query parameter extraction', generateToolTestRunner({138question: 'find specific information about error handling patterns from https://nodejs.org/en/docs/guides/error-handling/',139scenarioFolderPath: '',140getState,141expectedToolCalls: ToolName.FetchWebPage,142tools: {143[ToolName.FetchWebPage]: true,144[ToolName.FindFiles]: true,145[ToolName.FindTextInFiles]: true,146[ToolName.ReadFile]: true,147[ToolName.EditFile]: true,148[ToolName.Codebase]: true,149[ToolName.ListDirectory]: true,150[ToolName.SearchWorkspaceSymbols]: true,151},152}, {153allowParallelToolCalls: false,154toolCallValidators: {155[ToolName.FetchWebPage]: async (toolCalls) => {156assert.strictEqual(toolCalls.length, 1, 'should make exactly one fetch webpage tool call');157const input = toolCalls[0].input as IFetchWebPageToolParams;158159// Should have exactly 1 URL160assert.ok(Array.isArray(input.urls), 'urls should be an array');161assert.strictEqual(input.urls.length, 1, 'should have exactly 1 URL');162163// Should be the exact URL from the question164const expectedUrl = 'https://nodejs.org/en/docs/guides/error-handling/';165assert.strictEqual(input.urls[0], expectedUrl, `should have the exact URL: ${expectedUrl}`);166167// Should extract meaningful query when user asks for specific information168assert.ok(input.query !== undefined, 'should include a query when user asks for specific information');169assert.ok(typeof input.query === 'string', 'query should be a string');170assert.ok(input.query.length > 0, 'query should not be empty');171172// Query should relate to error handling since that's what was requested173const queryLower = input.query.toLowerCase();174assert.ok(175queryLower.includes('error') || queryLower.includes('handling') || queryLower.includes('pattern'),176'query should relate to error handling patterns when specifically requested'177);178}179}180}));181182stest('multiple URLs boundary test with 6 URLs', generateToolTestRunner({183question: 'gather information from these documentation sources: https://react.dev/learn/hooks-overview, https://vuejs.org/guide/essentials/reactivity-fundamentals.html, https://angular.io/guide/component-interaction, https://svelte.dev/docs/introduction, https://solid-js.com/guides/getting-started, and https://lit.dev/docs/ about component state management',184scenarioFolderPath: '',185getState,186expectedToolCalls: ToolName.FetchWebPage,187tools: {188[ToolName.FetchWebPage]: true,189[ToolName.FindFiles]: true,190[ToolName.FindTextInFiles]: true,191[ToolName.ReadFile]: true,192[ToolName.EditFile]: true,193[ToolName.Codebase]: true,194[ToolName.ListDirectory]: true,195[ToolName.SearchWorkspaceSymbols]: true,196},197}, {198allowParallelToolCalls: true,199toolCallValidators: {200[ToolName.FetchWebPage]: (toolCalls) => {201const expectedUrls = [202'https://react.dev/learn/hooks-overview',203'https://vuejs.org/guide/essentials/reactivity-fundamentals.html',204'https://angular.io/guide/component-interaction',205'https://svelte.dev/docs/introduction',206'https://solid-js.com/guides/getting-started',207'https://lit.dev/docs/'208];209210// Allow anywhere from 1 to 6 tool calls211assert.ok(toolCalls.length >= 1 && toolCalls.length <= 6, `should make between 1 and 6 fetch webpage tool calls, but got ${toolCalls.length}`);212213// Collect all URLs from all tool calls214const allUrls: string[] = [];215let totalUrlCount = 0;216217toolCalls.forEach((toolCall, index) => {218const input = toolCall.input as IFetchWebPageToolParams;219assert.ok(Array.isArray(input.urls), `tool call ${index + 1}: urls should be an array`);220assert.ok(input.urls.length >= 1, `tool call ${index + 1}: should have at least 1 URL`);221222totalUrlCount += input.urls.length;223allUrls.push(...input.urls);224});225226// Should have exactly 6 URLs total across all tool calls227assert.strictEqual(totalUrlCount, 6, 'should have exactly 6 URLs total across all tool calls');228assert.strictEqual(allUrls.length, 6, 'collected URLs array should have exactly 6 URLs');229230// Check that all expected URLs are present231expectedUrls.forEach(expectedUrl => {232assert.ok(allUrls.includes(expectedUrl), `should include the URL: ${expectedUrl}`);233});234235// Verify no unexpected URLs236allUrls.forEach(url => {237assert.ok(expectedUrls.includes(url), `unexpected URL found: ${url}`);238});239240// Verify no duplicate URLs241const uniqueUrls = new Set(allUrls);242assert.strictEqual(uniqueUrls.size, 6, 'should not have duplicate URLs');243244// Check query parameter for any tool call that has it245toolCalls.forEach((toolCall, index) => {246const input = toolCall.input as IFetchWebPageToolParams;247assert.ok(input.query, `tool call ${index + 1}: query should be defined if provided`);248});249}250}251}));252});253254255