@theia/ai-ide 1.70.0-next.21 → 1.70.0-next.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/browser/workspace-functions.d.ts +7 -0
- package/lib/browser/workspace-functions.d.ts.map +1 -1
- package/lib/browser/workspace-functions.js +181 -13
- package/lib/browser/workspace-functions.js.map +1 -1
- package/lib/browser/workspace-functions.spec.js +342 -0
- package/lib/browser/workspace-functions.spec.js.map +1 -1
- package/lib/common/workspace-preferences.d.ts +1 -0
- package/lib/common/workspace-preferences.d.ts.map +1 -1
- package/lib/common/workspace-preferences.js +11 -1
- package/lib/common/workspace-preferences.js.map +1 -1
- package/package.json +22 -22
- package/src/browser/workspace-functions.spec.ts +415 -0
- package/src/browser/workspace-functions.ts +195 -17
- package/src/common/workspace-preferences.ts +11 -0
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
import { ToolInvocationContext } from '@theia/ai-core';
|
|
33
33
|
import { Container } from '@theia/core/shared/inversify';
|
|
34
34
|
import { FileService } from '@theia/filesystem/lib/browser/file-service';
|
|
35
|
+
import { FileOperationError, FileOperationResult } from '@theia/filesystem/lib/common/files';
|
|
35
36
|
import { URI } from '@theia/core/lib/common/uri';
|
|
36
37
|
import { WorkspaceService } from '@theia/workspace/lib/browser';
|
|
37
38
|
import { ProblemManager } from '@theia/markers/lib/browser';
|
|
@@ -254,6 +255,420 @@ describe('FileContentFunction.getArgumentsShortLabel', () => {
|
|
|
254
255
|
const result = getArgumentsShortLabel(JSON.stringify({ path: 'src/index.ts' }));
|
|
255
256
|
expect(result).to.be.undefined;
|
|
256
257
|
});
|
|
258
|
+
|
|
259
|
+
it('returns hasMore true when offset is provided', () => {
|
|
260
|
+
const result = getArgumentsShortLabel(JSON.stringify({ file: 'src/index.ts', offset: 10 }));
|
|
261
|
+
expect(result).to.deep.equal({ label: 'src/index.ts', hasMore: true });
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('returns hasMore true when limit is provided', () => {
|
|
265
|
+
const result = getArgumentsShortLabel(JSON.stringify({ file: 'src/index.ts', limit: 50 }));
|
|
266
|
+
expect(result).to.deep.equal({ label: 'src/index.ts', hasMore: true });
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('returns hasMore true when both offset and limit are provided', () => {
|
|
270
|
+
const result = getArgumentsShortLabel(JSON.stringify({ file: 'src/index.ts', offset: 10, limit: 50 }));
|
|
271
|
+
expect(result).to.deep.equal({ label: 'src/index.ts', hasMore: true });
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
describe('FileContentFunction handler', () => {
|
|
276
|
+
let container: Container;
|
|
277
|
+
let fileContentFunction: FileContentFunction;
|
|
278
|
+
// Mutable delegates — tests reassign these directly instead of casting the mock object.
|
|
279
|
+
let mockResolve: () => Promise<unknown>;
|
|
280
|
+
let mockRead: () => Promise<unknown>;
|
|
281
|
+
let mockReadStream: () => Promise<unknown>;
|
|
282
|
+
let mockMonacoWorkspace: MonacoWorkspace;
|
|
283
|
+
let mockPreferenceService: { get: <T>(path: string, defaultValue: T) => T };
|
|
284
|
+
|
|
285
|
+
const makeMockStream = (content: string) => {
|
|
286
|
+
const handlers: Record<string, Function> = {};
|
|
287
|
+
// Use setTimeout so the macro-task fires after all pending microtasks
|
|
288
|
+
// (including the await continuation that registers the listeners).
|
|
289
|
+
setTimeout(() => {
|
|
290
|
+
handlers['data']?.(content);
|
|
291
|
+
handlers['end']?.();
|
|
292
|
+
}, 0);
|
|
293
|
+
return {
|
|
294
|
+
on(event: string, cb: Function): void { handlers[event] = cb; },
|
|
295
|
+
pause(): void { },
|
|
296
|
+
resume(): void { },
|
|
297
|
+
destroy(): void { },
|
|
298
|
+
removeListener(): void { }
|
|
299
|
+
};
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
let disableJSDOMInner: () => void;
|
|
303
|
+
before(() => {
|
|
304
|
+
disableJSDOMInner = enableJSDOM();
|
|
305
|
+
});
|
|
306
|
+
after(() => {
|
|
307
|
+
disableJSDOMInner();
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
beforeEach(() => {
|
|
311
|
+
container = new Container();
|
|
312
|
+
|
|
313
|
+
const mockWorkspaceService = {
|
|
314
|
+
roots: [{ resource: new URI('file:///workspace') }]
|
|
315
|
+
} as unknown as WorkspaceService;
|
|
316
|
+
|
|
317
|
+
mockResolve = async () => ({
|
|
318
|
+
isFile: true,
|
|
319
|
+
isDirectory: false,
|
|
320
|
+
size: 1024,
|
|
321
|
+
resource: new URI('file:///workspace/test.txt')
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
mockRead = async () => ({ value: 'line1\nline2\nline3\nline4\nline5' });
|
|
325
|
+
|
|
326
|
+
mockReadStream = async () => ({ value: makeMockStream('line1\nline2\nline3\nline4\nline5') });
|
|
327
|
+
|
|
328
|
+
// The mock object is stable across a test; individual methods delegate to
|
|
329
|
+
// the mutable variables above so tests can substitute behaviour without
|
|
330
|
+
// the fragile `(obj as unknown as {…}).method = …` double-cast pattern.
|
|
331
|
+
const mockFileService = {
|
|
332
|
+
exists: async () => true,
|
|
333
|
+
resolve: () => mockResolve(),
|
|
334
|
+
read: () => mockRead(),
|
|
335
|
+
readStream: () => mockReadStream(),
|
|
336
|
+
} as unknown as FileService;
|
|
337
|
+
|
|
338
|
+
mockPreferenceService = {
|
|
339
|
+
get: <T>(_path: string, defaultValue: T) => defaultValue
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
mockMonacoWorkspace = {
|
|
343
|
+
getTextDocument: () => undefined
|
|
344
|
+
} as unknown as MonacoWorkspace;
|
|
345
|
+
|
|
346
|
+
container.bind(WorkspaceService).toConstantValue(mockWorkspaceService);
|
|
347
|
+
container.bind(FileService).toConstantValue(mockFileService);
|
|
348
|
+
container.bind(PreferenceService).toConstantValue(mockPreferenceService);
|
|
349
|
+
container.bind(MonacoWorkspace).toConstantValue(mockMonacoWorkspace);
|
|
350
|
+
container.bind(WorkspaceFunctionScope).toSelf();
|
|
351
|
+
container.bind(FileContentFunction).toSelf();
|
|
352
|
+
|
|
353
|
+
fileContentFunction = container.get(FileContentFunction);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it('returns file content when file is within size limit', async () => {
|
|
357
|
+
const handler = fileContentFunction.getTool().handler;
|
|
358
|
+
const result = await handler(JSON.stringify({ file: 'test.txt' }), undefined);
|
|
359
|
+
expect(result).to.equal('line1\nline2\nline3\nline4\nline5');
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it('rejects without reading when on-disk size exceeds limit', async () => {
|
|
363
|
+
// Stat reports 512 KB; default limit is 256 KB
|
|
364
|
+
let readCalled = false;
|
|
365
|
+
mockResolve = async () => ({
|
|
366
|
+
isFile: true,
|
|
367
|
+
isDirectory: false,
|
|
368
|
+
size: 512 * 1024,
|
|
369
|
+
resource: new URI('file:///workspace/big.txt')
|
|
370
|
+
});
|
|
371
|
+
mockRead = async () => {
|
|
372
|
+
readCalled = true;
|
|
373
|
+
return { value: 'should not be read' };
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
const handler = fileContentFunction.getTool().handler;
|
|
377
|
+
const result = await handler(JSON.stringify({ file: 'big.txt' }), undefined);
|
|
378
|
+
|
|
379
|
+
const parsed = JSON.parse(result as string);
|
|
380
|
+
expect(parsed.error).to.include('size limit');
|
|
381
|
+
expect(parsed.sizeKB).to.equal(512);
|
|
382
|
+
expect(readCalled).to.be.false;
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it('returns editor content when file is open in editor and within size limit', async () => {
|
|
386
|
+
let resolveCalled = false;
|
|
387
|
+
mockResolve = async () => {
|
|
388
|
+
resolveCalled = true;
|
|
389
|
+
return { isFile: true, isDirectory: false, size: 1024, resource: new URI('file:///workspace/open.txt') };
|
|
390
|
+
};
|
|
391
|
+
mockMonacoWorkspace.getTextDocument = () => ({
|
|
392
|
+
getText: () => 'editor content'
|
|
393
|
+
} as unknown as ReturnType<MonacoWorkspace['getTextDocument']>);
|
|
394
|
+
|
|
395
|
+
const handler = fileContentFunction.getTool().handler;
|
|
396
|
+
const result = await handler(JSON.stringify({ file: 'open.txt' }), undefined);
|
|
397
|
+
|
|
398
|
+
expect(result).to.equal('editor content');
|
|
399
|
+
expect(resolveCalled).to.be.false;
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it('rejects editor content when it exceeds the size limit', async () => {
|
|
403
|
+
const bigContent = 'x'.repeat(512 * 1024);
|
|
404
|
+
mockMonacoWorkspace.getTextDocument = () => ({
|
|
405
|
+
getText: () => bigContent
|
|
406
|
+
} as unknown as ReturnType<MonacoWorkspace['getTextDocument']>);
|
|
407
|
+
|
|
408
|
+
const handler = fileContentFunction.getTool().handler;
|
|
409
|
+
const result = await handler(JSON.stringify({ file: 'open.txt' }), undefined);
|
|
410
|
+
|
|
411
|
+
const parsed = JSON.parse(result as string);
|
|
412
|
+
expect(parsed.error).to.include('size limit');
|
|
413
|
+
expect(parsed.sizeKB).to.equal(512);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it('returns sliced content with header when offset and limit are provided', async () => {
|
|
417
|
+
const handler = fileContentFunction.getTool().handler;
|
|
418
|
+
const result = await handler(JSON.stringify({ file: 'test.txt', offset: 1, limit: 2 }), undefined);
|
|
419
|
+
|
|
420
|
+
expect(result).to.include('[Lines 2\u20133 of 5 total.');
|
|
421
|
+
expect(result).to.include('line2\nline3');
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
it('returns sliced editor content with header when file is open and offset/limit are provided', async () => {
|
|
425
|
+
mockMonacoWorkspace.getTextDocument = () => ({
|
|
426
|
+
getText: () => 'alpha\nbeta\ngamma\ndelta\nepsilon'
|
|
427
|
+
} as unknown as ReturnType<MonacoWorkspace['getTextDocument']>);
|
|
428
|
+
|
|
429
|
+
const handler = fileContentFunction.getTool().handler;
|
|
430
|
+
const result = await handler(JSON.stringify({ file: 'open.txt', offset: 1, limit: 2 }), undefined);
|
|
431
|
+
|
|
432
|
+
expect(result).to.include('[Lines 2\u20133 of 5 total.');
|
|
433
|
+
expect(result).to.include('beta\ngamma');
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it('does not call resolve() or read() for paginated disk reads, uses readStream instead', async () => {
|
|
437
|
+
// Stat would report huge file, but the streaming path bypasses both stat and read
|
|
438
|
+
let resolveCalled = false;
|
|
439
|
+
let readCalled = false;
|
|
440
|
+
mockResolve = async () => {
|
|
441
|
+
resolveCalled = true;
|
|
442
|
+
return {
|
|
443
|
+
isFile: true,
|
|
444
|
+
isDirectory: false,
|
|
445
|
+
size: 512 * 1024,
|
|
446
|
+
resource: new URI('file:///workspace/big.txt')
|
|
447
|
+
};
|
|
448
|
+
};
|
|
449
|
+
mockRead = async () => {
|
|
450
|
+
readCalled = true;
|
|
451
|
+
return { value: 'should not be read' };
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
const handler = fileContentFunction.getTool().handler;
|
|
455
|
+
const result = await handler(JSON.stringify({ file: 'big.txt', offset: 0, limit: 3 }), undefined);
|
|
456
|
+
|
|
457
|
+
// resolve and read are NOT called in the streaming path
|
|
458
|
+
expect(resolveCalled).to.be.false;
|
|
459
|
+
expect(readCalled).to.be.false;
|
|
460
|
+
expect(result).to.include('line1\nline2\nline3');
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
it('rejects when the requested slice itself exceeds the size limit', async () => {
|
|
464
|
+
const bigLine = 'x'.repeat(1024);
|
|
465
|
+
const bigContent = Array.from({ length: 300 }, () => bigLine).join('\n');
|
|
466
|
+
mockReadStream = async () => ({ value: makeMockStream(bigContent) });
|
|
467
|
+
|
|
468
|
+
const handler = fileContentFunction.getTool().handler;
|
|
469
|
+
// Reading all 300 lines × 1 KB each = ~300 KB, over the 256 KB default limit
|
|
470
|
+
const result = await handler(JSON.stringify({ file: 'big.txt', offset: 0, limit: 300 }), undefined);
|
|
471
|
+
|
|
472
|
+
const parsed = JSON.parse(result as string);
|
|
473
|
+
expect(parsed.error).to.include('size limit');
|
|
474
|
+
expect(parsed.resultSizeKB).to.be.greaterThan(256);
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
it('returns File not found error when file does not exist', async () => {
|
|
478
|
+
mockResolve = async () => { throw new Error('File not found'); };
|
|
479
|
+
mockRead = async () => { throw new Error('File not found'); };
|
|
480
|
+
mockReadStream = async () => { throw new Error('File not found'); };
|
|
481
|
+
|
|
482
|
+
const handler = fileContentFunction.getTool().handler;
|
|
483
|
+
const result = await handler(JSON.stringify({ file: 'nonexistent.txt' }), undefined);
|
|
484
|
+
|
|
485
|
+
const parsed = JSON.parse(result as string);
|
|
486
|
+
expect(parsed.error).to.equal('File not found');
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
it('rejects negative offset', async () => {
|
|
490
|
+
const handler = fileContentFunction.getTool().handler;
|
|
491
|
+
const result = await handler(JSON.stringify({ file: 'test.txt', offset: -1 }), undefined);
|
|
492
|
+
|
|
493
|
+
const parsed = JSON.parse(result as string);
|
|
494
|
+
expect(parsed.error).to.include('non-negative integer');
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
it('rejects fractional offset', async () => {
|
|
498
|
+
const handler = fileContentFunction.getTool().handler;
|
|
499
|
+
const result = await handler(JSON.stringify({ file: 'test.txt', offset: 1.5 }), undefined);
|
|
500
|
+
|
|
501
|
+
const parsed = JSON.parse(result as string);
|
|
502
|
+
expect(parsed.error).to.include('non-negative integer');
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
it('rejects negative limit', async () => {
|
|
506
|
+
const handler = fileContentFunction.getTool().handler;
|
|
507
|
+
const result = await handler(JSON.stringify({ file: 'test.txt', limit: -1 }), undefined);
|
|
508
|
+
|
|
509
|
+
const parsed = JSON.parse(result as string);
|
|
510
|
+
expect(parsed.error).to.include('positive integer');
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
it('rejects zero limit', async () => {
|
|
514
|
+
const handler = fileContentFunction.getTool().handler;
|
|
515
|
+
const result = await handler(JSON.stringify({ file: 'test.txt', limit: 0 }), undefined);
|
|
516
|
+
|
|
517
|
+
const parsed = JSON.parse(result as string);
|
|
518
|
+
expect(parsed.error).to.include('positive integer');
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
it('returns content from offset to end when only offset is provided', async () => {
|
|
522
|
+
const handler = fileContentFunction.getTool().handler;
|
|
523
|
+
const result = await handler(JSON.stringify({ file: 'test.txt', offset: 2 }), undefined);
|
|
524
|
+
|
|
525
|
+
expect(result).to.include('[Lines 3\u20135 of 5 total.');
|
|
526
|
+
expect(result).to.include('line3\nline4\nline5');
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
it('returns last line when offset is at boundary', async () => {
|
|
530
|
+
const handler = fileContentFunction.getTool().handler;
|
|
531
|
+
const result = await handler(JSON.stringify({ file: 'test.txt', offset: 4, limit: 1 }), undefined);
|
|
532
|
+
|
|
533
|
+
expect(result).to.include('[Lines 5\u20135 of 5 total.');
|
|
534
|
+
expect(result).to.include('line5');
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
it('returns empty content when offset is beyond end of file', async () => {
|
|
538
|
+
const handler = fileContentFunction.getTool().handler;
|
|
539
|
+
const result = await handler(JSON.stringify({ file: 'test.txt', offset: 100, limit: 5 }), undefined);
|
|
540
|
+
|
|
541
|
+
// slice beyond end returns empty array → empty joined string
|
|
542
|
+
expect(result).to.include('[Lines 101\u2013100 of 5 total.');
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
it('uses custom preference value for size limit', async () => {
|
|
546
|
+
// Set a very small limit of 1 KB
|
|
547
|
+
mockPreferenceService.get = <T>(_path: string, _defaultValue: T) => 1 as unknown as T;
|
|
548
|
+
|
|
549
|
+
const content = 'x'.repeat(2 * 1024); // 2 KB
|
|
550
|
+
mockRead = async () => ({ value: content });
|
|
551
|
+
mockResolve = async () => ({
|
|
552
|
+
isFile: true,
|
|
553
|
+
isDirectory: false,
|
|
554
|
+
size: 2 * 1024,
|
|
555
|
+
resource: new URI('file:///workspace/test.txt')
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
const handler = fileContentFunction.getTool().handler;
|
|
559
|
+
const result = await handler(JSON.stringify({ file: 'test.txt' }), undefined);
|
|
560
|
+
|
|
561
|
+
const parsed = JSON.parse(result as string);
|
|
562
|
+
expect(parsed.error).to.include('size limit');
|
|
563
|
+
expect(parsed.maxSizeKB).to.equal(1);
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
it('falls back to streaming when stat.size is undefined and file is within limit', async () => {
|
|
567
|
+
// stat does not include a size — the code must not treat this as "0 KB"
|
|
568
|
+
// but instead stream the file and succeed when content is small.
|
|
569
|
+
let readCalled = false;
|
|
570
|
+
mockResolve = async () => ({
|
|
571
|
+
isFile: true,
|
|
572
|
+
isDirectory: false,
|
|
573
|
+
size: undefined,
|
|
574
|
+
resource: new URI('file:///workspace/test.txt')
|
|
575
|
+
});
|
|
576
|
+
mockRead = async () => {
|
|
577
|
+
readCalled = true;
|
|
578
|
+
return { value: 'should not be used' };
|
|
579
|
+
};
|
|
580
|
+
// mockReadStream already returns the small 5-line fixture from beforeEach
|
|
581
|
+
|
|
582
|
+
const handler = fileContentFunction.getTool().handler;
|
|
583
|
+
const result = await handler(JSON.stringify({ file: 'test.txt' }), undefined);
|
|
584
|
+
|
|
585
|
+
expect(readCalled).to.be.false;
|
|
586
|
+
expect(result).to.include('line1');
|
|
587
|
+
// Full-file streaming fallback must NOT include the [Lines...] header
|
|
588
|
+
expect(result).to.not.include('[Lines');
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
it('returns size-limit error (not "File not found") when stat.size is undefined and streamed content exceeds limit', async () => {
|
|
592
|
+
// stat does not include a size; the streamed content is larger than the limit.
|
|
593
|
+
mockResolve = async () => ({
|
|
594
|
+
isFile: true,
|
|
595
|
+
isDirectory: false,
|
|
596
|
+
size: undefined,
|
|
597
|
+
resource: new URI('file:///workspace/big.txt')
|
|
598
|
+
});
|
|
599
|
+
const bigLine = 'x'.repeat(1024);
|
|
600
|
+
const bigContent = Array.from({ length: 300 }, () => bigLine).join('\n'); // ~300 KB
|
|
601
|
+
mockReadStream = async () => ({ value: makeMockStream(bigContent) });
|
|
602
|
+
|
|
603
|
+
const handler = fileContentFunction.getTool().handler;
|
|
604
|
+
const result = await handler(JSON.stringify({ file: 'big.txt' }), undefined);
|
|
605
|
+
|
|
606
|
+
const parsed = JSON.parse(result as string);
|
|
607
|
+
expect(parsed.error).to.include('size limit');
|
|
608
|
+
expect(parsed.error).to.include('offset');
|
|
609
|
+
expect(parsed.error).not.to.equal('File not found');
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
it('returns size-limit error (not "File not found") when fileService.read throws FILE_TOO_LARGE', async () => {
|
|
613
|
+
// Simulate a file system provider that enforces its own hard size limit below maxSizeKB.
|
|
614
|
+
// stat.size is present and within our configured limit, but read() still throws.
|
|
615
|
+
mockResolve = async () => ({
|
|
616
|
+
isFile: true,
|
|
617
|
+
isDirectory: false,
|
|
618
|
+
size: 100 * 1024, // 100 KB — under the 256 KB default limit
|
|
619
|
+
resource: new URI('file:///workspace/test.txt')
|
|
620
|
+
});
|
|
621
|
+
mockRead = async () => {
|
|
622
|
+
throw new FileOperationError('File too large', FileOperationResult.FILE_TOO_LARGE);
|
|
623
|
+
};
|
|
624
|
+
|
|
625
|
+
const handler = fileContentFunction.getTool().handler;
|
|
626
|
+
const result = await handler(JSON.stringify({ file: 'test.txt' }), undefined);
|
|
627
|
+
|
|
628
|
+
const parsed = JSON.parse(result as string);
|
|
629
|
+
expect(parsed.error).to.include('size limit');
|
|
630
|
+
expect(parsed.error).to.include('offset');
|
|
631
|
+
expect(parsed.maxSizeKB).to.equal(256);
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
it('returns size-limit error (not "File not found") when fileService.read throws FILE_EXCEEDS_MEMORY_LIMIT', async () => {
|
|
635
|
+
mockResolve = async () => ({
|
|
636
|
+
isFile: true,
|
|
637
|
+
isDirectory: false,
|
|
638
|
+
size: 100 * 1024,
|
|
639
|
+
resource: new URI('file:///workspace/test.txt')
|
|
640
|
+
});
|
|
641
|
+
mockRead = async () => {
|
|
642
|
+
throw new FileOperationError('Exceeds memory limit', FileOperationResult.FILE_EXCEEDS_MEMORY_LIMIT);
|
|
643
|
+
};
|
|
644
|
+
|
|
645
|
+
const handler = fileContentFunction.getTool().handler;
|
|
646
|
+
const result = await handler(JSON.stringify({ file: 'test.txt' }), undefined);
|
|
647
|
+
|
|
648
|
+
const parsed = JSON.parse(result as string);
|
|
649
|
+
expect(parsed.error).to.include('size limit');
|
|
650
|
+
expect(parsed.error).to.include('offset');
|
|
651
|
+
expect(parsed.maxSizeKB).to.equal(256);
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
it('returns size-limit error (not "File not found") when readStream throws FILE_TOO_LARGE for paginated read', async () => {
|
|
655
|
+
// This is the key scenario: files.maxFileSizeMB is lower than the file size,
|
|
656
|
+
// but the caller is trying to read a chunk with offset/limit.
|
|
657
|
+
// Before the fix, readStream would throw FILE_TOO_LARGE and the catch block
|
|
658
|
+
// would return "File not found".
|
|
659
|
+
mockReadStream = async () => {
|
|
660
|
+
throw new FileOperationError('File too large', FileOperationResult.FILE_TOO_LARGE);
|
|
661
|
+
};
|
|
662
|
+
|
|
663
|
+
const handler = fileContentFunction.getTool().handler;
|
|
664
|
+
const result = await handler(JSON.stringify({ file: 'big.txt', offset: 0, limit: 50 }), undefined);
|
|
665
|
+
|
|
666
|
+
const parsed = JSON.parse(result as string);
|
|
667
|
+
expect(parsed.error).to.include('size limit');
|
|
668
|
+
expect(parsed.error).to.include('offset');
|
|
669
|
+
expect(parsed.error).not.to.equal('File not found');
|
|
670
|
+
expect(parsed.maxSizeKB).to.equal(256);
|
|
671
|
+
});
|
|
257
672
|
});
|
|
258
673
|
|
|
259
674
|
describe('FindFilesByPattern.getArgumentsShortLabel', () => {
|