Path: blob/main/src/vs/workbench/contrib/chat/test/electron-browser/fetchPageTool.test.ts
3296 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 * as assert from 'assert';6import { CancellationToken } from '../../../../../base/common/cancellation.js';7import { VSBuffer } from '../../../../../base/common/buffer.js';8import { URI } from '../../../../../base/common/uri.js';9import { ResourceMap } from '../../../../../base/common/map.js';10import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';11import { IFileContent, IReadFileOptions } from '../../../../../platform/files/common/files.js';12import { IWebContentExtractorService } from '../../../../../platform/webContentExtractor/common/webContentExtractor.js';13import { FetchWebPageTool } from '../../electron-browser/tools/fetchPageTool.js';14import { TestFileService } from '../../../../test/browser/workbenchTestServices.js';1516class TestWebContentExtractorService implements IWebContentExtractorService {17_serviceBrand: undefined;1819constructor(private uriToContentMap: ResourceMap<string>) { }2021async extract(uris: URI[]): Promise<string[]> {22return uris.map(uri => {23const content = this.uriToContentMap.get(uri);24if (content === undefined) {25throw new Error(`No content configured for URI: ${uri.toString()}`);26}27return content;28});29}30}3132class ExtendedTestFileService extends TestFileService {33constructor(private uriToContentMap: ResourceMap<string | VSBuffer>) {34super();35}3637override async readFile(resource: URI, options?: IReadFileOptions | undefined): Promise<IFileContent> {38const content = this.uriToContentMap.get(resource);39if (content === undefined) {40throw new Error(`File not found: ${resource.toString()}`);41}4243const buffer = typeof content === 'string' ? VSBuffer.fromString(content) : content;44return {45resource,46value: buffer,47name: '',48size: buffer.byteLength,49etag: '',50mtime: 0,51ctime: 0,52readonly: false,53locked: false54};55}5657override async stat(resource: URI) {58// Check if the resource exists in our map59if (!this.uriToContentMap.has(resource)) {60throw new Error(`File not found: ${resource.toString()}`);61}6263return super.stat(resource);64}65}6667suite('FetchWebPageTool', () => {68ensureNoDisposablesAreLeakedInTestSuite();6970test('should handle http/https via web content extractor and other schemes via file service', async () => {71const webContentMap = new ResourceMap<string>([72[URI.parse('https://example.com'), 'HTTPS content'],73[URI.parse('http://example.com'), 'HTTP content']74]);7576const fileContentMap = new ResourceMap<string | VSBuffer>([77[URI.parse('test://static/resource/50'), 'MCP resource content'],78[URI.parse('mcp-resource://746573742D736572766572/custom/hello/world.txt'), 'Custom MCP content']79]);8081const tool = new FetchWebPageTool(82new TestWebContentExtractorService(webContentMap),83new ExtendedTestFileService(fileContentMap)84);8586const testUrls = [87'https://example.com',88'http://example.com',89'test://static/resource/50',90'mcp-resource://746573742D736572766572/custom/hello/world.txt',91'file:///path/to/nonexistent',92'ftp://example.com',93'invalid-url'94];9596const result = await tool.invoke(97{ callId: 'test-call-1', toolId: 'fetch-page', parameters: { urls: testUrls }, context: undefined },98() => Promise.resolve(0),99{ report: () => { } },100CancellationToken.None101);102103// Should have 7 results (one for each input URL)104assert.strictEqual(result.content.length, 7, 'Should have result for each input URL');105106// HTTP and HTTPS URLs should have their content from web extractor107assert.strictEqual(result.content[0].value, 'HTTPS content', 'HTTPS URL should return content');108assert.strictEqual(result.content[1].value, 'HTTP content', 'HTTP URL should return content');109110// MCP resources should have their content from file service111assert.strictEqual(result.content[2].value, 'MCP resource content', 'test:// URL should return content from file service');112assert.strictEqual(result.content[3].value, 'Custom MCP content', 'mcp-resource:// URL should return content from file service');113114// Nonexistent file should be marked as invalid115assert.strictEqual(result.content[4].value, 'Invalid URL', 'Nonexistent file should be invalid');116117// Unsupported scheme (ftp) should be marked as invalid since file service can't handle it118assert.strictEqual(result.content[5].value, 'Invalid URL', 'ftp:// URL should be invalid');119120// Invalid URL should be marked as invalid121assert.strictEqual(result.content[6].value, 'Invalid URL', 'Invalid URL should be invalid');122123// All successfully fetched URLs should be in toolResultDetails124assert.strictEqual(Array.isArray(result.toolResultDetails) ? result.toolResultDetails.length : 0, 4, 'Should have 4 valid URLs in toolResultDetails');125});126127test('should handle empty and undefined URLs', async () => {128const tool = new FetchWebPageTool(129new TestWebContentExtractorService(new ResourceMap<string>()),130new ExtendedTestFileService(new ResourceMap<string | VSBuffer>())131);132133// Test empty array134const emptyResult = await tool.invoke(135{ callId: 'test-call-2', toolId: 'fetch-page', parameters: { urls: [] }, context: undefined },136() => Promise.resolve(0),137{ report: () => { } },138CancellationToken.None139);140assert.strictEqual(emptyResult.content.length, 1, 'Empty array should return single message');141assert.strictEqual(emptyResult.content[0].value, 'No valid URLs provided.', 'Should indicate no valid URLs');142143// Test undefined144const undefinedResult = await tool.invoke(145{ callId: 'test-call-3', toolId: 'fetch-page', parameters: {}, context: undefined },146() => Promise.resolve(0),147{ report: () => { } },148CancellationToken.None149);150assert.strictEqual(undefinedResult.content.length, 1, 'Undefined URLs should return single message');151assert.strictEqual(undefinedResult.content[0].value, 'No valid URLs provided.', 'Should indicate no valid URLs');152153// Test array with invalid URLs154const invalidResult = await tool.invoke(155{ callId: 'test-call-4', toolId: 'fetch-page', parameters: { urls: ['', ' ', 'invalid-scheme-that-fileservice-cannot-handle://test'] }, context: undefined },156() => Promise.resolve(0),157{ report: () => { } },158CancellationToken.None159);160assert.strictEqual(invalidResult.content.length, 3, 'Should have result for each invalid URL');161assert.strictEqual(invalidResult.content[0].value, 'Invalid URL', 'Empty string should be invalid');162assert.strictEqual(invalidResult.content[1].value, 'Invalid URL', 'Space-only string should be invalid');163assert.strictEqual(invalidResult.content[2].value, 'Invalid URL', 'Unhandleable scheme should be invalid');164});165166test('should provide correct past tense messages for mixed valid/invalid URLs', async () => {167const webContentMap = new ResourceMap<string>([168[URI.parse('https://valid.com'), 'Valid content']169]);170171const fileContentMap = new ResourceMap<string | VSBuffer>([172[URI.parse('test://valid/resource'), 'Valid MCP content']173]);174175const tool = new FetchWebPageTool(176new TestWebContentExtractorService(webContentMap),177new ExtendedTestFileService(fileContentMap)178);179180const preparation = await tool.prepareToolInvocation(181{ parameters: { urls: ['https://valid.com', 'test://valid/resource', 'invalid://invalid'] } },182CancellationToken.None183);184185assert.ok(preparation, 'Should return prepared invocation');186assert.ok(preparation.pastTenseMessage, 'Should have past tense message');187const messageText = typeof preparation.pastTenseMessage === 'string' ? preparation.pastTenseMessage : preparation.pastTenseMessage!.value;188assert.ok(messageText.includes('Fetched'), 'Should mention fetched resources');189assert.ok(messageText.includes('invalid://invalid'), 'Should mention invalid URL');190});191192test('should return message for binary files indicating they are not supported', async () => {193// Create binary content (a simple PNG-like header with null bytes)194const binaryContent = new Uint8Array([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D]);195const binaryBuffer = VSBuffer.wrap(binaryContent);196197const fileContentMap = new ResourceMap<string | VSBuffer>([198[URI.parse('file:///path/to/binary.dat'), binaryBuffer],199[URI.parse('file:///path/to/text.txt'), 'This is text content']200]);201202const tool = new FetchWebPageTool(203new TestWebContentExtractorService(new ResourceMap<string>()),204new ExtendedTestFileService(fileContentMap)205);206207const result = await tool.invoke(208{209callId: 'test-call-binary',210toolId: 'fetch-page',211parameters: { urls: ['file:///path/to/binary.dat', 'file:///path/to/text.txt'] },212context: undefined213},214() => Promise.resolve(0),215{ report: () => { } },216CancellationToken.None217);218219// Should have 2 results220assert.strictEqual(result.content.length, 2, 'Should have 2 results');221222// First result should be a text part with binary not supported message223assert.strictEqual(result.content[0].kind, 'text', 'Binary file should return text part');224if (result.content[0].kind === 'text') {225assert.strictEqual(result.content[0].value, 'Binary files are not supported at the moment.', 'Should return not supported message');226}227228// Second result should be a text part for the text file229assert.strictEqual(result.content[1].kind, 'text', 'Text file should return text part');230if (result.content[1].kind === 'text') {231assert.strictEqual(result.content[1].value, 'This is text content', 'Should return text content');232}233234// Both files should be in toolResultDetails since they were successfully fetched235assert.strictEqual(Array.isArray(result.toolResultDetails) ? result.toolResultDetails.length : 0, 2, 'Should have 2 valid URLs in toolResultDetails');236});237238test('PNG files are now supported as image data parts (regression test)', async () => {239// This test ensures that PNG files that previously returned "not supported"240// messages now return proper image data parts241const binaryContent = new Uint8Array([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D]);242const binaryBuffer = VSBuffer.wrap(binaryContent);243244const fileContentMap = new ResourceMap<string | VSBuffer>([245[URI.parse('file:///path/to/image.png'), binaryBuffer]246]);247248const tool = new FetchWebPageTool(249new TestWebContentExtractorService(new ResourceMap<string>()),250new ExtendedTestFileService(fileContentMap)251);252253const result = await tool.invoke(254{255callId: 'test-png-support',256toolId: 'fetch-page',257parameters: { urls: ['file:///path/to/image.png'] },258context: undefined259},260() => Promise.resolve(0),261{ report: () => { } },262CancellationToken.None263);264265// Should have 1 result266assert.strictEqual(result.content.length, 1, 'Should have 1 result');267268// PNG file should now be returned as a data part, not a "not supported" message269assert.strictEqual(result.content[0].kind, 'data', 'PNG file should return data part');270if (result.content[0].kind === 'data') {271assert.strictEqual(result.content[0].value.mimeType, 'image/png', 'Should have PNG MIME type');272assert.strictEqual(result.content[0].value.data, binaryBuffer, 'Should have correct binary data');273}274});275276test('should correctly distinguish between binary and text content', async () => {277// Create content that might be ambiguous278const jsonData = '{"name": "test", "value": 123}';279// Create definitely binary data - some random bytes with null bytes that don't follow UTF-16 pattern280const realBinaryData = new Uint8Array([0x89, 0x50, 0x4E, 0x47, 0x00, 0x00, 0x00, 0x0D, 0xFF, 0x00, 0xAB]); // More clearly binary281282const fileContentMap = new ResourceMap<string | VSBuffer>([283[URI.parse('file:///data.json'), jsonData], // Should be detected as text284[URI.parse('file:///binary.dat'), VSBuffer.wrap(realBinaryData)] // Should be detected as binary285]);286287const tool = new FetchWebPageTool(288new TestWebContentExtractorService(new ResourceMap<string>()),289new ExtendedTestFileService(fileContentMap)290);291292const result = await tool.invoke(293{294callId: 'test-distinguish',295toolId: 'fetch-page',296parameters: { urls: ['file:///data.json', 'file:///binary.dat'] },297context: undefined298},299() => Promise.resolve(0),300{ report: () => { } },301CancellationToken.None302);303304// JSON should be returned as text305assert.strictEqual(result.content[0].kind, 'text', 'JSON should be detected as text');306if (result.content[0].kind === 'text') {307assert.strictEqual(result.content[0].value, jsonData, 'Should return JSON as text');308}309310// Binary data should be returned as not supported message311assert.strictEqual(result.content[1].kind, 'text', 'Binary content should return text part with message');312if (result.content[1].kind === 'text') {313assert.strictEqual(result.content[1].value, 'Binary files are not supported at the moment.', 'Should return not supported message');314}315});316317test('Supported image files are returned as data parts', async () => {318// Test data for different supported image formats319const pngData = VSBuffer.fromString('fake PNG data');320const jpegData = VSBuffer.fromString('fake JPEG data');321const gifData = VSBuffer.fromString('fake GIF data');322const webpData = VSBuffer.fromString('fake WebP data');323const bmpData = VSBuffer.fromString('fake BMP data');324325const fileContentMap = new ResourceMap<string | VSBuffer>();326fileContentMap.set(URI.parse('file:///image.png'), pngData);327fileContentMap.set(URI.parse('file:///photo.jpg'), jpegData);328fileContentMap.set(URI.parse('file:///animation.gif'), gifData);329fileContentMap.set(URI.parse('file:///modern.webp'), webpData);330fileContentMap.set(URI.parse('file:///bitmap.bmp'), bmpData);331332const tool = new FetchWebPageTool(333new TestWebContentExtractorService(new ResourceMap<string>()),334new ExtendedTestFileService(fileContentMap)335);336337const result = await tool.invoke(338{339callId: 'test-images',340toolId: 'fetch-page',341parameters: { urls: ['file:///image.png', 'file:///photo.jpg', 'file:///animation.gif', 'file:///modern.webp', 'file:///bitmap.bmp'] },342context: undefined343},344() => Promise.resolve(0),345{ report: () => { } },346CancellationToken.None347);348349// All images should be returned as data parts350assert.strictEqual(result.content.length, 5, 'Should have 5 results');351352// Check PNG353assert.strictEqual(result.content[0].kind, 'data', 'PNG should be data part');354if (result.content[0].kind === 'data') {355assert.strictEqual(result.content[0].value.mimeType, 'image/png', 'PNG should have correct MIME type');356assert.strictEqual(result.content[0].value.data, pngData, 'PNG should have correct data');357}358359// Check JPEG360assert.strictEqual(result.content[1].kind, 'data', 'JPEG should be data part');361if (result.content[1].kind === 'data') {362assert.strictEqual(result.content[1].value.mimeType, 'image/jpeg', 'JPEG should have correct MIME type');363assert.strictEqual(result.content[1].value.data, jpegData, 'JPEG should have correct data');364}365366// Check GIF367assert.strictEqual(result.content[2].kind, 'data', 'GIF should be data part');368if (result.content[2].kind === 'data') {369assert.strictEqual(result.content[2].value.mimeType, 'image/gif', 'GIF should have correct MIME type');370assert.strictEqual(result.content[2].value.data, gifData, 'GIF should have correct data');371}372373// Check WebP374assert.strictEqual(result.content[3].kind, 'data', 'WebP should be data part');375if (result.content[3].kind === 'data') {376assert.strictEqual(result.content[3].value.mimeType, 'image/webp', 'WebP should have correct MIME type');377assert.strictEqual(result.content[3].value.data, webpData, 'WebP should have correct data');378}379380// Check BMP381assert.strictEqual(result.content[4].kind, 'data', 'BMP should be data part');382if (result.content[4].kind === 'data') {383assert.strictEqual(result.content[4].value.mimeType, 'image/bmp', 'BMP should have correct MIME type');384assert.strictEqual(result.content[4].value.data, bmpData, 'BMP should have correct data');385}386});387388test('Mixed image and text files work correctly', async () => {389const textData = 'This is some text content';390const imageData = VSBuffer.fromString('fake image data');391392const fileContentMap = new ResourceMap<string | VSBuffer>();393fileContentMap.set(URI.parse('file:///text.txt'), textData);394fileContentMap.set(URI.parse('file:///image.png'), imageData);395396const tool = new FetchWebPageTool(397new TestWebContentExtractorService(new ResourceMap<string>()),398new ExtendedTestFileService(fileContentMap)399);400401const result = await tool.invoke(402{403callId: 'test-mixed',404toolId: 'fetch-page',405parameters: { urls: ['file:///text.txt', 'file:///image.png'] },406context: undefined407},408() => Promise.resolve(0),409{ report: () => { } },410CancellationToken.None411);412413// Text should be returned as text part414assert.strictEqual(result.content[0].kind, 'text', 'Text file should be text part');415if (result.content[0].kind === 'text') {416assert.strictEqual(result.content[0].value, textData, 'Text should have correct content');417}418419// Image should be returned as data part420assert.strictEqual(result.content[1].kind, 'data', 'Image file should be data part');421if (result.content[1].kind === 'data') {422assert.strictEqual(result.content[1].value.mimeType, 'image/png', 'Image should have correct MIME type');423assert.strictEqual(result.content[1].value.data, imageData, 'Image should have correct data');424}425});426427test('Case insensitive image extensions work', async () => {428const imageData = VSBuffer.fromString('fake image data');429430const fileContentMap = new ResourceMap<string | VSBuffer>();431fileContentMap.set(URI.parse('file:///image.PNG'), imageData);432fileContentMap.set(URI.parse('file:///photo.JPEG'), imageData);433434const tool = new FetchWebPageTool(435new TestWebContentExtractorService(new ResourceMap<string>()),436new ExtendedTestFileService(fileContentMap)437);438439const result = await tool.invoke(440{441callId: 'test-case',442toolId: 'fetch-page',443parameters: { urls: ['file:///image.PNG', 'file:///photo.JPEG'] },444context: undefined445},446() => Promise.resolve(0),447{ report: () => { } },448CancellationToken.None449);450451// Both should be returned as data parts despite uppercase extensions452assert.strictEqual(result.content[0].kind, 'data', 'PNG with uppercase extension should be data part');453if (result.content[0].kind === 'data') {454assert.strictEqual(result.content[0].value.mimeType, 'image/png', 'Should have correct MIME type');455}456457assert.strictEqual(result.content[1].kind, 'data', 'JPEG with uppercase extension should be data part');458if (result.content[1].kind === 'data') {459assert.strictEqual(result.content[1].value.mimeType, 'image/jpeg', 'Should have correct MIME type');460}461});462463// Comprehensive tests for toolResultDetails464suite('toolResultDetails', () => {465test('should include only successfully fetched URIs in correct order', async () => {466const webContentMap = new ResourceMap<string>([467[URI.parse('https://success1.com'), 'Content 1'],468[URI.parse('https://success2.com'), 'Content 2']469]);470471const fileContentMap = new ResourceMap<string | VSBuffer>([472[URI.parse('file:///success.txt'), 'File content'],473[URI.parse('mcp-resource://server/file.txt'), 'MCP content']474]);475476const tool = new FetchWebPageTool(477new TestWebContentExtractorService(webContentMap),478new ExtendedTestFileService(fileContentMap)479);480481const testUrls = [482'https://success1.com', // index 0 - should be in toolResultDetails483'invalid-url', // index 1 - should NOT be in toolResultDetails484'file:///success.txt', // index 2 - should be in toolResultDetails485'https://success2.com', // index 3 - should be in toolResultDetails486'file:///nonexistent.txt', // index 4 - should NOT be in toolResultDetails487'mcp-resource://server/file.txt' // index 5 - should be in toolResultDetails488];489490const result = await tool.invoke(491{ callId: 'test-details', toolId: 'fetch-page', parameters: { urls: testUrls }, context: undefined },492() => Promise.resolve(0),493{ report: () => { } },494CancellationToken.None495);496497// Verify toolResultDetails contains exactly the successful URIs498assert.ok(Array.isArray(result.toolResultDetails), 'toolResultDetails should be an array');499assert.strictEqual(result.toolResultDetails.length, 4, 'Should have 4 successful URIs');500501// Check that all entries are URI objects502const uriDetails = result.toolResultDetails as URI[];503assert.ok(uriDetails.every(uri => uri instanceof URI), 'All toolResultDetails entries should be URI objects');504505// Check specific URIs are included (web URIs first, then successful file URIs)506const expectedUris = [507'https://success1.com/',508'https://success2.com/',509'file:///success.txt',510'mcp-resource://server/file.txt'511];512513const actualUriStrings = uriDetails.map(uri => uri.toString());514assert.deepStrictEqual(actualUriStrings.sort(), expectedUris.sort(), 'Should contain exactly the expected successful URIs');515516// Verify content array matches input order (including failures)517assert.strictEqual(result.content.length, 6, 'Content should have result for each input URL');518assert.strictEqual(result.content[0].value, 'Content 1', 'First web URI content');519assert.strictEqual(result.content[1].value, 'Invalid URL', 'Invalid URL marked as invalid');520assert.strictEqual(result.content[2].value, 'File content', 'File URI content');521assert.strictEqual(result.content[3].value, 'Content 2', 'Second web URI content');522assert.strictEqual(result.content[4].value, 'Invalid URL', 'Nonexistent file marked as invalid');523assert.strictEqual(result.content[5].value, 'MCP content', 'MCP resource content');524});525526test('should exclude failed web requests from toolResultDetails', async () => {527// Set up web content extractor that will throw for some URIs528const webContentMap = new ResourceMap<string>([529[URI.parse('https://success.com'), 'Success content']530// https://failure.com not in map - will throw error531]);532533const tool = new FetchWebPageTool(534new TestWebContentExtractorService(webContentMap),535new ExtendedTestFileService(new ResourceMap<string | VSBuffer>())536);537538const testUrls = [539'https://success.com', // Should succeed540'https://failure.com' // Should fail (not in content map)541];542543try {544await tool.invoke(545{ callId: 'test-web-failure', toolId: 'fetch-page', parameters: { urls: testUrls }, context: undefined },546() => Promise.resolve(0),547{ report: () => { } },548CancellationToken.None549);550551// If the web extractor throws, it should be handled gracefully552// But in this test setup, the TestWebContentExtractorService throws for missing content553assert.fail('Expected test web content extractor to throw for missing URI');554} catch (error) {555// This is expected behavior with the current test setup556// The TestWebContentExtractorService throws when content is not found557assert.ok(error.message.includes('No content configured for URI'), 'Should throw for unconfigured URI');558}559});560561test('should exclude failed file reads from toolResultDetails', async () => {562const fileContentMap = new ResourceMap<string | VSBuffer>([563[URI.parse('file:///existing.txt'), 'File exists']564// file:///missing.txt not in map - will throw error565]);566567const tool = new FetchWebPageTool(568new TestWebContentExtractorService(new ResourceMap<string>()),569new ExtendedTestFileService(fileContentMap)570);571572const testUrls = [573'file:///existing.txt', // Should succeed574'file:///missing.txt' // Should fail (not in file map)575];576577const result = await tool.invoke(578{ callId: 'test-file-failure', toolId: 'fetch-page', parameters: { urls: testUrls }, context: undefined },579() => Promise.resolve(0),580{ report: () => { } },581CancellationToken.None582);583584// Verify only successful file URI is in toolResultDetails585assert.ok(Array.isArray(result.toolResultDetails), 'toolResultDetails should be an array');586assert.strictEqual(result.toolResultDetails.length, 1, 'Should have only 1 successful URI');587588const uriDetails = result.toolResultDetails as URI[];589assert.strictEqual(uriDetails[0].toString(), 'file:///existing.txt', 'Should contain only the successful file URI');590591// Verify content reflects both attempts592assert.strictEqual(result.content.length, 2, 'Should have results for both input URLs');593assert.strictEqual(result.content[0].value, 'File exists', 'First file should have content');594assert.strictEqual(result.content[1].value, 'Invalid URL', 'Second file should be marked invalid');595});596597test('should handle mixed success and failure scenarios', async () => {598const webContentMap = new ResourceMap<string>([599[URI.parse('https://web-success.com'), 'Web success']600]);601602const fileContentMap = new ResourceMap<string | VSBuffer>([603[URI.parse('file:///file-success.txt'), 'File success'],604[URI.parse('mcp-resource://good/file.txt'), VSBuffer.fromString('MCP binary content')]605]);606607const tool = new FetchWebPageTool(608new TestWebContentExtractorService(webContentMap),609new ExtendedTestFileService(fileContentMap)610);611612const testUrls = [613'invalid-scheme://bad', // Invalid URI614'https://web-success.com', // Web success615'file:///file-missing.txt', // File failure616'file:///file-success.txt', // File success617'completely-invalid-url', // Invalid URL format618'mcp-resource://good/file.txt' // MCP success619];620621const result = await tool.invoke(622{ callId: 'test-mixed', toolId: 'fetch-page', parameters: { urls: testUrls }, context: undefined },623() => Promise.resolve(0),624{ report: () => { } },625CancellationToken.None626);627628// Should have 3 successful URIs: web-success, file-success, mcp-success629assert.ok(Array.isArray(result.toolResultDetails), 'toolResultDetails should be an array');630assert.strictEqual((result.toolResultDetails as URI[]).length, 3, 'Should have 3 successful URIs');631632const uriDetails = result.toolResultDetails as URI[];633const actualUriStrings = uriDetails.map(uri => uri.toString());634const expectedSuccessful = [635'https://web-success.com/',636'file:///file-success.txt',637'mcp-resource://good/file.txt'638];639640assert.deepStrictEqual(actualUriStrings.sort(), expectedSuccessful.sort(), 'Should contain exactly the successful URIs');641642// Verify content array reflects all inputs in original order643assert.strictEqual(result.content.length, 6, 'Should have results for all input URLs');644assert.strictEqual(result.content[0].value, 'Invalid URL', 'Invalid scheme marked as invalid');645assert.strictEqual(result.content[1].value, 'Web success', 'Web success content');646assert.strictEqual(result.content[2].value, 'Invalid URL', 'Missing file marked as invalid');647assert.strictEqual(result.content[3].value, 'File success', 'File success content');648assert.strictEqual(result.content[4].value, 'Invalid URL', 'Invalid URL marked as invalid');649assert.strictEqual(result.content[5].value, 'MCP binary content', 'MCP success content');650});651652test('should return empty toolResultDetails when all requests fail', async () => {653const tool = new FetchWebPageTool(654new TestWebContentExtractorService(new ResourceMap<string>()), // Empty - all web requests fail655new ExtendedTestFileService(new ResourceMap<string | VSBuffer>()) // Empty - all file requests fail656);657658const testUrls = [659'https://nonexistent.com',660'file:///missing.txt',661'invalid-url',662'bad://scheme'663];664665try {666const result = await tool.invoke(667{ callId: 'test-all-fail', toolId: 'fetch-page', parameters: { urls: testUrls }, context: undefined },668() => Promise.resolve(0),669{ report: () => { } },670CancellationToken.None671);672673// If web extractor doesn't throw, check the results674assert.ok(Array.isArray(result.toolResultDetails), 'toolResultDetails should be an array');675assert.strictEqual((result.toolResultDetails as URI[]).length, 0, 'Should have no successful URIs');676assert.strictEqual(result.content.length, 4, 'Should have results for all input URLs');677assert.ok(result.content.every(content => content.value === 'Invalid URL'), 'All content should be marked as invalid');678} catch (error) {679// Expected with TestWebContentExtractorService when no content is configured680assert.ok(error.message.includes('No content configured for URI'), 'Should throw for unconfigured URI');681}682});683684test('should handle empty URL array', async () => {685const tool = new FetchWebPageTool(686new TestWebContentExtractorService(new ResourceMap<string>()),687new ExtendedTestFileService(new ResourceMap<string | VSBuffer>())688);689690const result = await tool.invoke(691{ callId: 'test-empty', toolId: 'fetch-page', parameters: { urls: [] }, context: undefined },692() => Promise.resolve(0),693{ report: () => { } },694CancellationToken.None695);696697assert.strictEqual(result.content.length, 1, 'Should have one content item for empty URLs');698assert.strictEqual(result.content[0].value, 'No valid URLs provided.', 'Should indicate no valid URLs');699assert.ok(!result.toolResultDetails, 'toolResultDetails should not be present for empty URLs');700});701702test('should handle image files in toolResultDetails', async () => {703const imageBuffer = VSBuffer.fromString('fake-png-data');704const fileContentMap = new ResourceMap<string | VSBuffer>([705[URI.parse('file:///image.png'), imageBuffer],706[URI.parse('file:///document.txt'), 'Text content']707]);708709const tool = new FetchWebPageTool(710new TestWebContentExtractorService(new ResourceMap<string>()),711new ExtendedTestFileService(fileContentMap)712);713714const result = await tool.invoke(715{ callId: 'test-images', toolId: 'fetch-page', parameters: { urls: ['file:///image.png', 'file:///document.txt'] }, context: undefined },716() => Promise.resolve(0),717{ report: () => { } },718CancellationToken.None719);720721// Both files should be successful and in toolResultDetails722assert.ok(Array.isArray(result.toolResultDetails), 'toolResultDetails should be an array');723assert.strictEqual((result.toolResultDetails as URI[]).length, 2, 'Should have 2 successful file URIs');724725const uriDetails = result.toolResultDetails as URI[];726assert.strictEqual(uriDetails[0].toString(), 'file:///image.png', 'Should include image file');727assert.strictEqual(uriDetails[1].toString(), 'file:///document.txt', 'Should include text file');728729// Check content types730assert.strictEqual(result.content[0].kind, 'data', 'Image should be data part');731assert.strictEqual(result.content[1].kind, 'text', 'Text file should be text part');732});733});734});735736737