Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/test/electron-browser/fetchPageTool.test.ts
3296 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 * as assert from 'assert';
7
import { CancellationToken } from '../../../../../base/common/cancellation.js';
8
import { VSBuffer } from '../../../../../base/common/buffer.js';
9
import { URI } from '../../../../../base/common/uri.js';
10
import { ResourceMap } from '../../../../../base/common/map.js';
11
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
12
import { IFileContent, IReadFileOptions } from '../../../../../platform/files/common/files.js';
13
import { IWebContentExtractorService } from '../../../../../platform/webContentExtractor/common/webContentExtractor.js';
14
import { FetchWebPageTool } from '../../electron-browser/tools/fetchPageTool.js';
15
import { TestFileService } from '../../../../test/browser/workbenchTestServices.js';
16
17
class TestWebContentExtractorService implements IWebContentExtractorService {
18
_serviceBrand: undefined;
19
20
constructor(private uriToContentMap: ResourceMap<string>) { }
21
22
async extract(uris: URI[]): Promise<string[]> {
23
return uris.map(uri => {
24
const content = this.uriToContentMap.get(uri);
25
if (content === undefined) {
26
throw new Error(`No content configured for URI: ${uri.toString()}`);
27
}
28
return content;
29
});
30
}
31
}
32
33
class ExtendedTestFileService extends TestFileService {
34
constructor(private uriToContentMap: ResourceMap<string | VSBuffer>) {
35
super();
36
}
37
38
override async readFile(resource: URI, options?: IReadFileOptions | undefined): Promise<IFileContent> {
39
const content = this.uriToContentMap.get(resource);
40
if (content === undefined) {
41
throw new Error(`File not found: ${resource.toString()}`);
42
}
43
44
const buffer = typeof content === 'string' ? VSBuffer.fromString(content) : content;
45
return {
46
resource,
47
value: buffer,
48
name: '',
49
size: buffer.byteLength,
50
etag: '',
51
mtime: 0,
52
ctime: 0,
53
readonly: false,
54
locked: false
55
};
56
}
57
58
override async stat(resource: URI) {
59
// Check if the resource exists in our map
60
if (!this.uriToContentMap.has(resource)) {
61
throw new Error(`File not found: ${resource.toString()}`);
62
}
63
64
return super.stat(resource);
65
}
66
}
67
68
suite('FetchWebPageTool', () => {
69
ensureNoDisposablesAreLeakedInTestSuite();
70
71
test('should handle http/https via web content extractor and other schemes via file service', async () => {
72
const webContentMap = new ResourceMap<string>([
73
[URI.parse('https://example.com'), 'HTTPS content'],
74
[URI.parse('http://example.com'), 'HTTP content']
75
]);
76
77
const fileContentMap = new ResourceMap<string | VSBuffer>([
78
[URI.parse('test://static/resource/50'), 'MCP resource content'],
79
[URI.parse('mcp-resource://746573742D736572766572/custom/hello/world.txt'), 'Custom MCP content']
80
]);
81
82
const tool = new FetchWebPageTool(
83
new TestWebContentExtractorService(webContentMap),
84
new ExtendedTestFileService(fileContentMap)
85
);
86
87
const testUrls = [
88
'https://example.com',
89
'http://example.com',
90
'test://static/resource/50',
91
'mcp-resource://746573742D736572766572/custom/hello/world.txt',
92
'file:///path/to/nonexistent',
93
'ftp://example.com',
94
'invalid-url'
95
];
96
97
const result = await tool.invoke(
98
{ callId: 'test-call-1', toolId: 'fetch-page', parameters: { urls: testUrls }, context: undefined },
99
() => Promise.resolve(0),
100
{ report: () => { } },
101
CancellationToken.None
102
);
103
104
// Should have 7 results (one for each input URL)
105
assert.strictEqual(result.content.length, 7, 'Should have result for each input URL');
106
107
// HTTP and HTTPS URLs should have their content from web extractor
108
assert.strictEqual(result.content[0].value, 'HTTPS content', 'HTTPS URL should return content');
109
assert.strictEqual(result.content[1].value, 'HTTP content', 'HTTP URL should return content');
110
111
// MCP resources should have their content from file service
112
assert.strictEqual(result.content[2].value, 'MCP resource content', 'test:// URL should return content from file service');
113
assert.strictEqual(result.content[3].value, 'Custom MCP content', 'mcp-resource:// URL should return content from file service');
114
115
// Nonexistent file should be marked as invalid
116
assert.strictEqual(result.content[4].value, 'Invalid URL', 'Nonexistent file should be invalid');
117
118
// Unsupported scheme (ftp) should be marked as invalid since file service can't handle it
119
assert.strictEqual(result.content[5].value, 'Invalid URL', 'ftp:// URL should be invalid');
120
121
// Invalid URL should be marked as invalid
122
assert.strictEqual(result.content[6].value, 'Invalid URL', 'Invalid URL should be invalid');
123
124
// All successfully fetched URLs should be in toolResultDetails
125
assert.strictEqual(Array.isArray(result.toolResultDetails) ? result.toolResultDetails.length : 0, 4, 'Should have 4 valid URLs in toolResultDetails');
126
});
127
128
test('should handle empty and undefined URLs', async () => {
129
const tool = new FetchWebPageTool(
130
new TestWebContentExtractorService(new ResourceMap<string>()),
131
new ExtendedTestFileService(new ResourceMap<string | VSBuffer>())
132
);
133
134
// Test empty array
135
const emptyResult = await tool.invoke(
136
{ callId: 'test-call-2', toolId: 'fetch-page', parameters: { urls: [] }, context: undefined },
137
() => Promise.resolve(0),
138
{ report: () => { } },
139
CancellationToken.None
140
);
141
assert.strictEqual(emptyResult.content.length, 1, 'Empty array should return single message');
142
assert.strictEqual(emptyResult.content[0].value, 'No valid URLs provided.', 'Should indicate no valid URLs');
143
144
// Test undefined
145
const undefinedResult = await tool.invoke(
146
{ callId: 'test-call-3', toolId: 'fetch-page', parameters: {}, context: undefined },
147
() => Promise.resolve(0),
148
{ report: () => { } },
149
CancellationToken.None
150
);
151
assert.strictEqual(undefinedResult.content.length, 1, 'Undefined URLs should return single message');
152
assert.strictEqual(undefinedResult.content[0].value, 'No valid URLs provided.', 'Should indicate no valid URLs');
153
154
// Test array with invalid URLs
155
const invalidResult = await tool.invoke(
156
{ callId: 'test-call-4', toolId: 'fetch-page', parameters: { urls: ['', ' ', 'invalid-scheme-that-fileservice-cannot-handle://test'] }, context: undefined },
157
() => Promise.resolve(0),
158
{ report: () => { } },
159
CancellationToken.None
160
);
161
assert.strictEqual(invalidResult.content.length, 3, 'Should have result for each invalid URL');
162
assert.strictEqual(invalidResult.content[0].value, 'Invalid URL', 'Empty string should be invalid');
163
assert.strictEqual(invalidResult.content[1].value, 'Invalid URL', 'Space-only string should be invalid');
164
assert.strictEqual(invalidResult.content[2].value, 'Invalid URL', 'Unhandleable scheme should be invalid');
165
});
166
167
test('should provide correct past tense messages for mixed valid/invalid URLs', async () => {
168
const webContentMap = new ResourceMap<string>([
169
[URI.parse('https://valid.com'), 'Valid content']
170
]);
171
172
const fileContentMap = new ResourceMap<string | VSBuffer>([
173
[URI.parse('test://valid/resource'), 'Valid MCP content']
174
]);
175
176
const tool = new FetchWebPageTool(
177
new TestWebContentExtractorService(webContentMap),
178
new ExtendedTestFileService(fileContentMap)
179
);
180
181
const preparation = await tool.prepareToolInvocation(
182
{ parameters: { urls: ['https://valid.com', 'test://valid/resource', 'invalid://invalid'] } },
183
CancellationToken.None
184
);
185
186
assert.ok(preparation, 'Should return prepared invocation');
187
assert.ok(preparation.pastTenseMessage, 'Should have past tense message');
188
const messageText = typeof preparation.pastTenseMessage === 'string' ? preparation.pastTenseMessage : preparation.pastTenseMessage!.value;
189
assert.ok(messageText.includes('Fetched'), 'Should mention fetched resources');
190
assert.ok(messageText.includes('invalid://invalid'), 'Should mention invalid URL');
191
});
192
193
test('should return message for binary files indicating they are not supported', async () => {
194
// Create binary content (a simple PNG-like header with null bytes)
195
const binaryContent = new Uint8Array([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D]);
196
const binaryBuffer = VSBuffer.wrap(binaryContent);
197
198
const fileContentMap = new ResourceMap<string | VSBuffer>([
199
[URI.parse('file:///path/to/binary.dat'), binaryBuffer],
200
[URI.parse('file:///path/to/text.txt'), 'This is text content']
201
]);
202
203
const tool = new FetchWebPageTool(
204
new TestWebContentExtractorService(new ResourceMap<string>()),
205
new ExtendedTestFileService(fileContentMap)
206
);
207
208
const result = await tool.invoke(
209
{
210
callId: 'test-call-binary',
211
toolId: 'fetch-page',
212
parameters: { urls: ['file:///path/to/binary.dat', 'file:///path/to/text.txt'] },
213
context: undefined
214
},
215
() => Promise.resolve(0),
216
{ report: () => { } },
217
CancellationToken.None
218
);
219
220
// Should have 2 results
221
assert.strictEqual(result.content.length, 2, 'Should have 2 results');
222
223
// First result should be a text part with binary not supported message
224
assert.strictEqual(result.content[0].kind, 'text', 'Binary file should return text part');
225
if (result.content[0].kind === 'text') {
226
assert.strictEqual(result.content[0].value, 'Binary files are not supported at the moment.', 'Should return not supported message');
227
}
228
229
// Second result should be a text part for the text file
230
assert.strictEqual(result.content[1].kind, 'text', 'Text file should return text part');
231
if (result.content[1].kind === 'text') {
232
assert.strictEqual(result.content[1].value, 'This is text content', 'Should return text content');
233
}
234
235
// Both files should be in toolResultDetails since they were successfully fetched
236
assert.strictEqual(Array.isArray(result.toolResultDetails) ? result.toolResultDetails.length : 0, 2, 'Should have 2 valid URLs in toolResultDetails');
237
});
238
239
test('PNG files are now supported as image data parts (regression test)', async () => {
240
// This test ensures that PNG files that previously returned "not supported"
241
// messages now return proper image data parts
242
const binaryContent = new Uint8Array([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D]);
243
const binaryBuffer = VSBuffer.wrap(binaryContent);
244
245
const fileContentMap = new ResourceMap<string | VSBuffer>([
246
[URI.parse('file:///path/to/image.png'), binaryBuffer]
247
]);
248
249
const tool = new FetchWebPageTool(
250
new TestWebContentExtractorService(new ResourceMap<string>()),
251
new ExtendedTestFileService(fileContentMap)
252
);
253
254
const result = await tool.invoke(
255
{
256
callId: 'test-png-support',
257
toolId: 'fetch-page',
258
parameters: { urls: ['file:///path/to/image.png'] },
259
context: undefined
260
},
261
() => Promise.resolve(0),
262
{ report: () => { } },
263
CancellationToken.None
264
);
265
266
// Should have 1 result
267
assert.strictEqual(result.content.length, 1, 'Should have 1 result');
268
269
// PNG file should now be returned as a data part, not a "not supported" message
270
assert.strictEqual(result.content[0].kind, 'data', 'PNG file should return data part');
271
if (result.content[0].kind === 'data') {
272
assert.strictEqual(result.content[0].value.mimeType, 'image/png', 'Should have PNG MIME type');
273
assert.strictEqual(result.content[0].value.data, binaryBuffer, 'Should have correct binary data');
274
}
275
});
276
277
test('should correctly distinguish between binary and text content', async () => {
278
// Create content that might be ambiguous
279
const jsonData = '{"name": "test", "value": 123}';
280
// Create definitely binary data - some random bytes with null bytes that don't follow UTF-16 pattern
281
const realBinaryData = new Uint8Array([0x89, 0x50, 0x4E, 0x47, 0x00, 0x00, 0x00, 0x0D, 0xFF, 0x00, 0xAB]); // More clearly binary
282
283
const fileContentMap = new ResourceMap<string | VSBuffer>([
284
[URI.parse('file:///data.json'), jsonData], // Should be detected as text
285
[URI.parse('file:///binary.dat'), VSBuffer.wrap(realBinaryData)] // Should be detected as binary
286
]);
287
288
const tool = new FetchWebPageTool(
289
new TestWebContentExtractorService(new ResourceMap<string>()),
290
new ExtendedTestFileService(fileContentMap)
291
);
292
293
const result = await tool.invoke(
294
{
295
callId: 'test-distinguish',
296
toolId: 'fetch-page',
297
parameters: { urls: ['file:///data.json', 'file:///binary.dat'] },
298
context: undefined
299
},
300
() => Promise.resolve(0),
301
{ report: () => { } },
302
CancellationToken.None
303
);
304
305
// JSON should be returned as text
306
assert.strictEqual(result.content[0].kind, 'text', 'JSON should be detected as text');
307
if (result.content[0].kind === 'text') {
308
assert.strictEqual(result.content[0].value, jsonData, 'Should return JSON as text');
309
}
310
311
// Binary data should be returned as not supported message
312
assert.strictEqual(result.content[1].kind, 'text', 'Binary content should return text part with message');
313
if (result.content[1].kind === 'text') {
314
assert.strictEqual(result.content[1].value, 'Binary files are not supported at the moment.', 'Should return not supported message');
315
}
316
});
317
318
test('Supported image files are returned as data parts', async () => {
319
// Test data for different supported image formats
320
const pngData = VSBuffer.fromString('fake PNG data');
321
const jpegData = VSBuffer.fromString('fake JPEG data');
322
const gifData = VSBuffer.fromString('fake GIF data');
323
const webpData = VSBuffer.fromString('fake WebP data');
324
const bmpData = VSBuffer.fromString('fake BMP data');
325
326
const fileContentMap = new ResourceMap<string | VSBuffer>();
327
fileContentMap.set(URI.parse('file:///image.png'), pngData);
328
fileContentMap.set(URI.parse('file:///photo.jpg'), jpegData);
329
fileContentMap.set(URI.parse('file:///animation.gif'), gifData);
330
fileContentMap.set(URI.parse('file:///modern.webp'), webpData);
331
fileContentMap.set(URI.parse('file:///bitmap.bmp'), bmpData);
332
333
const tool = new FetchWebPageTool(
334
new TestWebContentExtractorService(new ResourceMap<string>()),
335
new ExtendedTestFileService(fileContentMap)
336
);
337
338
const result = await tool.invoke(
339
{
340
callId: 'test-images',
341
toolId: 'fetch-page',
342
parameters: { urls: ['file:///image.png', 'file:///photo.jpg', 'file:///animation.gif', 'file:///modern.webp', 'file:///bitmap.bmp'] },
343
context: undefined
344
},
345
() => Promise.resolve(0),
346
{ report: () => { } },
347
CancellationToken.None
348
);
349
350
// All images should be returned as data parts
351
assert.strictEqual(result.content.length, 5, 'Should have 5 results');
352
353
// Check PNG
354
assert.strictEqual(result.content[0].kind, 'data', 'PNG should be data part');
355
if (result.content[0].kind === 'data') {
356
assert.strictEqual(result.content[0].value.mimeType, 'image/png', 'PNG should have correct MIME type');
357
assert.strictEqual(result.content[0].value.data, pngData, 'PNG should have correct data');
358
}
359
360
// Check JPEG
361
assert.strictEqual(result.content[1].kind, 'data', 'JPEG should be data part');
362
if (result.content[1].kind === 'data') {
363
assert.strictEqual(result.content[1].value.mimeType, 'image/jpeg', 'JPEG should have correct MIME type');
364
assert.strictEqual(result.content[1].value.data, jpegData, 'JPEG should have correct data');
365
}
366
367
// Check GIF
368
assert.strictEqual(result.content[2].kind, 'data', 'GIF should be data part');
369
if (result.content[2].kind === 'data') {
370
assert.strictEqual(result.content[2].value.mimeType, 'image/gif', 'GIF should have correct MIME type');
371
assert.strictEqual(result.content[2].value.data, gifData, 'GIF should have correct data');
372
}
373
374
// Check WebP
375
assert.strictEqual(result.content[3].kind, 'data', 'WebP should be data part');
376
if (result.content[3].kind === 'data') {
377
assert.strictEqual(result.content[3].value.mimeType, 'image/webp', 'WebP should have correct MIME type');
378
assert.strictEqual(result.content[3].value.data, webpData, 'WebP should have correct data');
379
}
380
381
// Check BMP
382
assert.strictEqual(result.content[4].kind, 'data', 'BMP should be data part');
383
if (result.content[4].kind === 'data') {
384
assert.strictEqual(result.content[4].value.mimeType, 'image/bmp', 'BMP should have correct MIME type');
385
assert.strictEqual(result.content[4].value.data, bmpData, 'BMP should have correct data');
386
}
387
});
388
389
test('Mixed image and text files work correctly', async () => {
390
const textData = 'This is some text content';
391
const imageData = VSBuffer.fromString('fake image data');
392
393
const fileContentMap = new ResourceMap<string | VSBuffer>();
394
fileContentMap.set(URI.parse('file:///text.txt'), textData);
395
fileContentMap.set(URI.parse('file:///image.png'), imageData);
396
397
const tool = new FetchWebPageTool(
398
new TestWebContentExtractorService(new ResourceMap<string>()),
399
new ExtendedTestFileService(fileContentMap)
400
);
401
402
const result = await tool.invoke(
403
{
404
callId: 'test-mixed',
405
toolId: 'fetch-page',
406
parameters: { urls: ['file:///text.txt', 'file:///image.png'] },
407
context: undefined
408
},
409
() => Promise.resolve(0),
410
{ report: () => { } },
411
CancellationToken.None
412
);
413
414
// Text should be returned as text part
415
assert.strictEqual(result.content[0].kind, 'text', 'Text file should be text part');
416
if (result.content[0].kind === 'text') {
417
assert.strictEqual(result.content[0].value, textData, 'Text should have correct content');
418
}
419
420
// Image should be returned as data part
421
assert.strictEqual(result.content[1].kind, 'data', 'Image file should be data part');
422
if (result.content[1].kind === 'data') {
423
assert.strictEqual(result.content[1].value.mimeType, 'image/png', 'Image should have correct MIME type');
424
assert.strictEqual(result.content[1].value.data, imageData, 'Image should have correct data');
425
}
426
});
427
428
test('Case insensitive image extensions work', async () => {
429
const imageData = VSBuffer.fromString('fake image data');
430
431
const fileContentMap = new ResourceMap<string | VSBuffer>();
432
fileContentMap.set(URI.parse('file:///image.PNG'), imageData);
433
fileContentMap.set(URI.parse('file:///photo.JPEG'), imageData);
434
435
const tool = new FetchWebPageTool(
436
new TestWebContentExtractorService(new ResourceMap<string>()),
437
new ExtendedTestFileService(fileContentMap)
438
);
439
440
const result = await tool.invoke(
441
{
442
callId: 'test-case',
443
toolId: 'fetch-page',
444
parameters: { urls: ['file:///image.PNG', 'file:///photo.JPEG'] },
445
context: undefined
446
},
447
() => Promise.resolve(0),
448
{ report: () => { } },
449
CancellationToken.None
450
);
451
452
// Both should be returned as data parts despite uppercase extensions
453
assert.strictEqual(result.content[0].kind, 'data', 'PNG with uppercase extension should be data part');
454
if (result.content[0].kind === 'data') {
455
assert.strictEqual(result.content[0].value.mimeType, 'image/png', 'Should have correct MIME type');
456
}
457
458
assert.strictEqual(result.content[1].kind, 'data', 'JPEG with uppercase extension should be data part');
459
if (result.content[1].kind === 'data') {
460
assert.strictEqual(result.content[1].value.mimeType, 'image/jpeg', 'Should have correct MIME type');
461
}
462
});
463
464
// Comprehensive tests for toolResultDetails
465
suite('toolResultDetails', () => {
466
test('should include only successfully fetched URIs in correct order', async () => {
467
const webContentMap = new ResourceMap<string>([
468
[URI.parse('https://success1.com'), 'Content 1'],
469
[URI.parse('https://success2.com'), 'Content 2']
470
]);
471
472
const fileContentMap = new ResourceMap<string | VSBuffer>([
473
[URI.parse('file:///success.txt'), 'File content'],
474
[URI.parse('mcp-resource://server/file.txt'), 'MCP content']
475
]);
476
477
const tool = new FetchWebPageTool(
478
new TestWebContentExtractorService(webContentMap),
479
new ExtendedTestFileService(fileContentMap)
480
);
481
482
const testUrls = [
483
'https://success1.com', // index 0 - should be in toolResultDetails
484
'invalid-url', // index 1 - should NOT be in toolResultDetails
485
'file:///success.txt', // index 2 - should be in toolResultDetails
486
'https://success2.com', // index 3 - should be in toolResultDetails
487
'file:///nonexistent.txt', // index 4 - should NOT be in toolResultDetails
488
'mcp-resource://server/file.txt' // index 5 - should be in toolResultDetails
489
];
490
491
const result = await tool.invoke(
492
{ callId: 'test-details', toolId: 'fetch-page', parameters: { urls: testUrls }, context: undefined },
493
() => Promise.resolve(0),
494
{ report: () => { } },
495
CancellationToken.None
496
);
497
498
// Verify toolResultDetails contains exactly the successful URIs
499
assert.ok(Array.isArray(result.toolResultDetails), 'toolResultDetails should be an array');
500
assert.strictEqual(result.toolResultDetails.length, 4, 'Should have 4 successful URIs');
501
502
// Check that all entries are URI objects
503
const uriDetails = result.toolResultDetails as URI[];
504
assert.ok(uriDetails.every(uri => uri instanceof URI), 'All toolResultDetails entries should be URI objects');
505
506
// Check specific URIs are included (web URIs first, then successful file URIs)
507
const expectedUris = [
508
'https://success1.com/',
509
'https://success2.com/',
510
'file:///success.txt',
511
'mcp-resource://server/file.txt'
512
];
513
514
const actualUriStrings = uriDetails.map(uri => uri.toString());
515
assert.deepStrictEqual(actualUriStrings.sort(), expectedUris.sort(), 'Should contain exactly the expected successful URIs');
516
517
// Verify content array matches input order (including failures)
518
assert.strictEqual(result.content.length, 6, 'Content should have result for each input URL');
519
assert.strictEqual(result.content[0].value, 'Content 1', 'First web URI content');
520
assert.strictEqual(result.content[1].value, 'Invalid URL', 'Invalid URL marked as invalid');
521
assert.strictEqual(result.content[2].value, 'File content', 'File URI content');
522
assert.strictEqual(result.content[3].value, 'Content 2', 'Second web URI content');
523
assert.strictEqual(result.content[4].value, 'Invalid URL', 'Nonexistent file marked as invalid');
524
assert.strictEqual(result.content[5].value, 'MCP content', 'MCP resource content');
525
});
526
527
test('should exclude failed web requests from toolResultDetails', async () => {
528
// Set up web content extractor that will throw for some URIs
529
const webContentMap = new ResourceMap<string>([
530
[URI.parse('https://success.com'), 'Success content']
531
// https://failure.com not in map - will throw error
532
]);
533
534
const tool = new FetchWebPageTool(
535
new TestWebContentExtractorService(webContentMap),
536
new ExtendedTestFileService(new ResourceMap<string | VSBuffer>())
537
);
538
539
const testUrls = [
540
'https://success.com', // Should succeed
541
'https://failure.com' // Should fail (not in content map)
542
];
543
544
try {
545
await tool.invoke(
546
{ callId: 'test-web-failure', toolId: 'fetch-page', parameters: { urls: testUrls }, context: undefined },
547
() => Promise.resolve(0),
548
{ report: () => { } },
549
CancellationToken.None
550
);
551
552
// If the web extractor throws, it should be handled gracefully
553
// But in this test setup, the TestWebContentExtractorService throws for missing content
554
assert.fail('Expected test web content extractor to throw for missing URI');
555
} catch (error) {
556
// This is expected behavior with the current test setup
557
// The TestWebContentExtractorService throws when content is not found
558
assert.ok(error.message.includes('No content configured for URI'), 'Should throw for unconfigured URI');
559
}
560
});
561
562
test('should exclude failed file reads from toolResultDetails', async () => {
563
const fileContentMap = new ResourceMap<string | VSBuffer>([
564
[URI.parse('file:///existing.txt'), 'File exists']
565
// file:///missing.txt not in map - will throw error
566
]);
567
568
const tool = new FetchWebPageTool(
569
new TestWebContentExtractorService(new ResourceMap<string>()),
570
new ExtendedTestFileService(fileContentMap)
571
);
572
573
const testUrls = [
574
'file:///existing.txt', // Should succeed
575
'file:///missing.txt' // Should fail (not in file map)
576
];
577
578
const result = await tool.invoke(
579
{ callId: 'test-file-failure', toolId: 'fetch-page', parameters: { urls: testUrls }, context: undefined },
580
() => Promise.resolve(0),
581
{ report: () => { } },
582
CancellationToken.None
583
);
584
585
// Verify only successful file URI is in toolResultDetails
586
assert.ok(Array.isArray(result.toolResultDetails), 'toolResultDetails should be an array');
587
assert.strictEqual(result.toolResultDetails.length, 1, 'Should have only 1 successful URI');
588
589
const uriDetails = result.toolResultDetails as URI[];
590
assert.strictEqual(uriDetails[0].toString(), 'file:///existing.txt', 'Should contain only the successful file URI');
591
592
// Verify content reflects both attempts
593
assert.strictEqual(result.content.length, 2, 'Should have results for both input URLs');
594
assert.strictEqual(result.content[0].value, 'File exists', 'First file should have content');
595
assert.strictEqual(result.content[1].value, 'Invalid URL', 'Second file should be marked invalid');
596
});
597
598
test('should handle mixed success and failure scenarios', async () => {
599
const webContentMap = new ResourceMap<string>([
600
[URI.parse('https://web-success.com'), 'Web success']
601
]);
602
603
const fileContentMap = new ResourceMap<string | VSBuffer>([
604
[URI.parse('file:///file-success.txt'), 'File success'],
605
[URI.parse('mcp-resource://good/file.txt'), VSBuffer.fromString('MCP binary content')]
606
]);
607
608
const tool = new FetchWebPageTool(
609
new TestWebContentExtractorService(webContentMap),
610
new ExtendedTestFileService(fileContentMap)
611
);
612
613
const testUrls = [
614
'invalid-scheme://bad', // Invalid URI
615
'https://web-success.com', // Web success
616
'file:///file-missing.txt', // File failure
617
'file:///file-success.txt', // File success
618
'completely-invalid-url', // Invalid URL format
619
'mcp-resource://good/file.txt' // MCP success
620
];
621
622
const result = await tool.invoke(
623
{ callId: 'test-mixed', toolId: 'fetch-page', parameters: { urls: testUrls }, context: undefined },
624
() => Promise.resolve(0),
625
{ report: () => { } },
626
CancellationToken.None
627
);
628
629
// Should have 3 successful URIs: web-success, file-success, mcp-success
630
assert.ok(Array.isArray(result.toolResultDetails), 'toolResultDetails should be an array');
631
assert.strictEqual((result.toolResultDetails as URI[]).length, 3, 'Should have 3 successful URIs');
632
633
const uriDetails = result.toolResultDetails as URI[];
634
const actualUriStrings = uriDetails.map(uri => uri.toString());
635
const expectedSuccessful = [
636
'https://web-success.com/',
637
'file:///file-success.txt',
638
'mcp-resource://good/file.txt'
639
];
640
641
assert.deepStrictEqual(actualUriStrings.sort(), expectedSuccessful.sort(), 'Should contain exactly the successful URIs');
642
643
// Verify content array reflects all inputs in original order
644
assert.strictEqual(result.content.length, 6, 'Should have results for all input URLs');
645
assert.strictEqual(result.content[0].value, 'Invalid URL', 'Invalid scheme marked as invalid');
646
assert.strictEqual(result.content[1].value, 'Web success', 'Web success content');
647
assert.strictEqual(result.content[2].value, 'Invalid URL', 'Missing file marked as invalid');
648
assert.strictEqual(result.content[3].value, 'File success', 'File success content');
649
assert.strictEqual(result.content[4].value, 'Invalid URL', 'Invalid URL marked as invalid');
650
assert.strictEqual(result.content[5].value, 'MCP binary content', 'MCP success content');
651
});
652
653
test('should return empty toolResultDetails when all requests fail', async () => {
654
const tool = new FetchWebPageTool(
655
new TestWebContentExtractorService(new ResourceMap<string>()), // Empty - all web requests fail
656
new ExtendedTestFileService(new ResourceMap<string | VSBuffer>()) // Empty - all file requests fail
657
);
658
659
const testUrls = [
660
'https://nonexistent.com',
661
'file:///missing.txt',
662
'invalid-url',
663
'bad://scheme'
664
];
665
666
try {
667
const result = await tool.invoke(
668
{ callId: 'test-all-fail', toolId: 'fetch-page', parameters: { urls: testUrls }, context: undefined },
669
() => Promise.resolve(0),
670
{ report: () => { } },
671
CancellationToken.None
672
);
673
674
// If web extractor doesn't throw, check the results
675
assert.ok(Array.isArray(result.toolResultDetails), 'toolResultDetails should be an array');
676
assert.strictEqual((result.toolResultDetails as URI[]).length, 0, 'Should have no successful URIs');
677
assert.strictEqual(result.content.length, 4, 'Should have results for all input URLs');
678
assert.ok(result.content.every(content => content.value === 'Invalid URL'), 'All content should be marked as invalid');
679
} catch (error) {
680
// Expected with TestWebContentExtractorService when no content is configured
681
assert.ok(error.message.includes('No content configured for URI'), 'Should throw for unconfigured URI');
682
}
683
});
684
685
test('should handle empty URL array', async () => {
686
const tool = new FetchWebPageTool(
687
new TestWebContentExtractorService(new ResourceMap<string>()),
688
new ExtendedTestFileService(new ResourceMap<string | VSBuffer>())
689
);
690
691
const result = await tool.invoke(
692
{ callId: 'test-empty', toolId: 'fetch-page', parameters: { urls: [] }, context: undefined },
693
() => Promise.resolve(0),
694
{ report: () => { } },
695
CancellationToken.None
696
);
697
698
assert.strictEqual(result.content.length, 1, 'Should have one content item for empty URLs');
699
assert.strictEqual(result.content[0].value, 'No valid URLs provided.', 'Should indicate no valid URLs');
700
assert.ok(!result.toolResultDetails, 'toolResultDetails should not be present for empty URLs');
701
});
702
703
test('should handle image files in toolResultDetails', async () => {
704
const imageBuffer = VSBuffer.fromString('fake-png-data');
705
const fileContentMap = new ResourceMap<string | VSBuffer>([
706
[URI.parse('file:///image.png'), imageBuffer],
707
[URI.parse('file:///document.txt'), 'Text content']
708
]);
709
710
const tool = new FetchWebPageTool(
711
new TestWebContentExtractorService(new ResourceMap<string>()),
712
new ExtendedTestFileService(fileContentMap)
713
);
714
715
const result = await tool.invoke(
716
{ callId: 'test-images', toolId: 'fetch-page', parameters: { urls: ['file:///image.png', 'file:///document.txt'] }, context: undefined },
717
() => Promise.resolve(0),
718
{ report: () => { } },
719
CancellationToken.None
720
);
721
722
// Both files should be successful and in toolResultDetails
723
assert.ok(Array.isArray(result.toolResultDetails), 'toolResultDetails should be an array');
724
assert.strictEqual((result.toolResultDetails as URI[]).length, 2, 'Should have 2 successful file URIs');
725
726
const uriDetails = result.toolResultDetails as URI[];
727
assert.strictEqual(uriDetails[0].toString(), 'file:///image.png', 'Should include image file');
728
assert.strictEqual(uriDetails[1].toString(), 'file:///document.txt', 'Should include text file');
729
730
// Check content types
731
assert.strictEqual(result.content[0].kind, 'data', 'Image should be data part');
732
assert.strictEqual(result.content[1].kind, 'text', 'Text file should be text part');
733
});
734
});
735
});
736
737