feat(core): Migrate read_many_files, shell, and web_fetch. (#6167)

This commit is contained in:
joshualitt
2025-08-13 12:27:09 -07:00
committed by GitHub
parent 904f4623b6
commit c0c0e9b7a0
6 changed files with 503 additions and 464 deletions

View File

@@ -121,66 +121,71 @@ describe('ReadManyFilesTool', () => {
}
});
describe('validateParams', () => {
it('should return null for valid relative paths within root', () => {
describe('build', () => {
it('should return an invocation for valid relative paths within root', () => {
const params = { paths: ['file1.txt', 'subdir/file2.txt'] };
expect(tool.validateParams(params)).toBeNull();
const invocation = tool.build(params);
expect(invocation).toBeDefined();
});
it('should return null for valid glob patterns within root', () => {
it('should return an invocation for valid glob patterns within root', () => {
const params = { paths: ['*.txt', 'subdir/**/*.js'] };
expect(tool.validateParams(params)).toBeNull();
const invocation = tool.build(params);
expect(invocation).toBeDefined();
});
it('should return null for paths trying to escape the root (e.g., ../) as execute handles this', () => {
it('should return an invocation for paths trying to escape the root (e.g., ../) as execute handles this', () => {
const params = { paths: ['../outside.txt'] };
expect(tool.validateParams(params)).toBeNull();
const invocation = tool.build(params);
expect(invocation).toBeDefined();
});
it('should return null for absolute paths as execute handles this', () => {
it('should return an invocation for absolute paths as execute handles this', () => {
const params = { paths: [path.join(tempDirOutsideRoot, 'absolute.txt')] };
expect(tool.validateParams(params)).toBeNull();
const invocation = tool.build(params);
expect(invocation).toBeDefined();
});
it('should return error if paths array is empty', () => {
it('should throw error if paths array is empty', () => {
const params = { paths: [] };
expect(tool.validateParams(params)).toBe(
expect(() => tool.build(params)).toThrow(
'params/paths must NOT have fewer than 1 items',
);
});
it('should return null for valid exclude and include patterns', () => {
it('should return an invocation for valid exclude and include patterns', () => {
const params = {
paths: ['src/**/*.ts'],
exclude: ['**/*.test.ts'],
include: ['src/utils/*.ts'],
};
expect(tool.validateParams(params)).toBeNull();
const invocation = tool.build(params);
expect(invocation).toBeDefined();
});
it('should return error if paths array contains an empty string', () => {
it('should throw error if paths array contains an empty string', () => {
const params = { paths: ['file1.txt', ''] };
expect(tool.validateParams(params)).toBe(
expect(() => tool.build(params)).toThrow(
'params/paths/1 must NOT have fewer than 1 characters',
);
});
it('should return error if include array contains non-string elements', () => {
it('should throw error if include array contains non-string elements', () => {
const params = {
paths: ['file1.txt'],
include: ['*.ts', 123] as string[],
};
expect(tool.validateParams(params)).toBe(
expect(() => tool.build(params)).toThrow(
'params/include/1 must be string',
);
});
it('should return error if exclude array contains non-string elements', () => {
it('should throw error if exclude array contains non-string elements', () => {
const params = {
paths: ['file1.txt'],
exclude: ['*.log', {}] as string[],
};
expect(tool.validateParams(params)).toBe(
expect(() => tool.build(params)).toThrow(
'params/exclude/1 must be string',
);
});
@@ -201,7 +206,8 @@ describe('ReadManyFilesTool', () => {
it('should read a single specified file', async () => {
createFile('file1.txt', 'Content of file1');
const params = { paths: ['file1.txt'] };
const result = await tool.execute(params, new AbortController().signal);
const invocation = tool.build(params);
const result = await invocation.execute(new AbortController().signal);
const expectedPath = path.join(tempRootDir, 'file1.txt');
expect(result.llmContent).toEqual([
`--- ${expectedPath} ---\n\nContent of file1\n\n`,
@@ -215,7 +221,8 @@ describe('ReadManyFilesTool', () => {
createFile('file1.txt', 'Content1');
createFile('subdir/file2.js', 'Content2');
const params = { paths: ['file1.txt', 'subdir/file2.js'] };
const result = await tool.execute(params, new AbortController().signal);
const invocation = tool.build(params);
const result = await invocation.execute(new AbortController().signal);
const content = result.llmContent as string[];
const expectedPath1 = path.join(tempRootDir, 'file1.txt');
const expectedPath2 = path.join(tempRootDir, 'subdir/file2.js');
@@ -239,7 +246,8 @@ describe('ReadManyFilesTool', () => {
createFile('another.txt', 'Another text');
createFile('sub/data.json', '{}');
const params = { paths: ['*.txt'] };
const result = await tool.execute(params, new AbortController().signal);
const invocation = tool.build(params);
const result = await invocation.execute(new AbortController().signal);
const content = result.llmContent as string[];
const expectedPath1 = path.join(tempRootDir, 'file.txt');
const expectedPath2 = path.join(tempRootDir, 'another.txt');
@@ -263,7 +271,8 @@ describe('ReadManyFilesTool', () => {
createFile('src/main.ts', 'Main content');
createFile('src/main.test.ts', 'Test content');
const params = { paths: ['src/**/*.ts'], exclude: ['**/*.test.ts'] };
const result = await tool.execute(params, new AbortController().signal);
const invocation = tool.build(params);
const result = await invocation.execute(new AbortController().signal);
const content = result.llmContent as string[];
const expectedPath = path.join(tempRootDir, 'src/main.ts');
expect(content).toEqual([`--- ${expectedPath} ---\n\nMain content\n\n`]);
@@ -277,7 +286,8 @@ describe('ReadManyFilesTool', () => {
it('should handle nonexistent specific files gracefully', async () => {
const params = { paths: ['nonexistent-file.txt'] };
const result = await tool.execute(params, new AbortController().signal);
const invocation = tool.build(params);
const result = await invocation.execute(new AbortController().signal);
expect(result.llmContent).toEqual([
'No files matching the criteria were found or all were skipped.',
]);
@@ -290,7 +300,8 @@ describe('ReadManyFilesTool', () => {
createFile('node_modules/some-lib/index.js', 'lib code');
createFile('src/app.js', 'app code');
const params = { paths: ['**/*.js'] };
const result = await tool.execute(params, new AbortController().signal);
const invocation = tool.build(params);
const result = await invocation.execute(new AbortController().signal);
const content = result.llmContent as string[];
const expectedPath = path.join(tempRootDir, 'src/app.js');
expect(content).toEqual([`--- ${expectedPath} ---\n\napp code\n\n`]);
@@ -306,7 +317,8 @@ describe('ReadManyFilesTool', () => {
createFile('node_modules/some-lib/index.js', 'lib code');
createFile('src/app.js', 'app code');
const params = { paths: ['**/*.js'], useDefaultExcludes: false };
const result = await tool.execute(params, new AbortController().signal);
const invocation = tool.build(params);
const result = await invocation.execute(new AbortController().signal);
const content = result.llmContent as string[];
const expectedPath1 = path.join(
tempRootDir,
@@ -334,7 +346,8 @@ describe('ReadManyFilesTool', () => {
Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
);
const params = { paths: ['*.png'] }; // Explicitly requesting .png
const result = await tool.execute(params, new AbortController().signal);
const invocation = tool.build(params);
const result = await invocation.execute(new AbortController().signal);
expect(result.llmContent).toEqual([
{
inlineData: {
@@ -356,7 +369,8 @@ describe('ReadManyFilesTool', () => {
Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
);
const params = { paths: ['myExactImage.png'] }; // Explicitly requesting by full name
const result = await tool.execute(params, new AbortController().signal);
const invocation = tool.build(params);
const result = await invocation.execute(new AbortController().signal);
expect(result.llmContent).toEqual([
{
inlineData: {
@@ -373,7 +387,8 @@ describe('ReadManyFilesTool', () => {
createBinaryFile('document.pdf', Buffer.from('%PDF-1.4...'));
createFile('notes.txt', 'text notes');
const params = { paths: ['*'] }; // Generic glob, not specific to .pdf
const result = await tool.execute(params, new AbortController().signal);
const invocation = tool.build(params);
const result = await invocation.execute(new AbortController().signal);
const content = result.llmContent as string[];
const expectedPath = path.join(tempRootDir, 'notes.txt');
expect(
@@ -392,7 +407,8 @@ describe('ReadManyFilesTool', () => {
it('should include PDF files as inlineData parts if explicitly requested by extension', async () => {
createBinaryFile('important.pdf', Buffer.from('%PDF-1.4...'));
const params = { paths: ['*.pdf'] }; // Explicitly requesting .pdf files
const result = await tool.execute(params, new AbortController().signal);
const invocation = tool.build(params);
const result = await invocation.execute(new AbortController().signal);
expect(result.llmContent).toEqual([
{
inlineData: {
@@ -406,7 +422,8 @@ describe('ReadManyFilesTool', () => {
it('should include PDF files as inlineData parts if explicitly requested by name', async () => {
createBinaryFile('report-final.pdf', Buffer.from('%PDF-1.4...'));
const params = { paths: ['report-final.pdf'] };
const result = await tool.execute(params, new AbortController().signal);
const invocation = tool.build(params);
const result = await invocation.execute(new AbortController().signal);
expect(result.llmContent).toEqual([
{
inlineData: {
@@ -422,7 +439,8 @@ describe('ReadManyFilesTool', () => {
createFile('bar.ts', '');
createFile('foo.quux', '');
const params = { paths: ['foo.bar', 'bar.ts', 'foo.quux'] };
const result = await tool.execute(params, new AbortController().signal);
const invocation = tool.build(params);
const result = await invocation.execute(new AbortController().signal);
expect(result.returnDisplay).not.toContain('foo.bar');
expect(result.returnDisplay).not.toContain('foo.quux');
expect(result.returnDisplay).toContain('bar.ts');
@@ -451,7 +469,8 @@ describe('ReadManyFilesTool', () => {
fs.writeFileSync(path.join(tempDir2, 'file2.txt'), 'Content2');
const params = { paths: ['*.txt'] };
const result = await tool.execute(params, new AbortController().signal);
const invocation = tool.build(params);
const result = await invocation.execute(new AbortController().signal);
const content = result.llmContent as string[];
if (!Array.isArray(content)) {
throw new Error(`llmContent is not an array: ${content}`);
@@ -486,7 +505,8 @@ describe('ReadManyFilesTool', () => {
createFile('large-file.txt', longContent);
const params = { paths: ['*.txt'] };
const result = await tool.execute(params, new AbortController().signal);
const invocation = tool.build(params);
const result = await invocation.execute(new AbortController().signal);
const content = result.llmContent as string[];
const normalFileContent = content.find((c) => c.includes('file1.txt'));
@@ -541,7 +561,8 @@ describe('ReadManyFilesTool', () => {
});
const params = { paths: files };
const result = await tool.execute(params, new AbortController().signal);
const invocation = tool.build(params);
const result = await invocation.execute(new AbortController().signal);
// Verify all files were processed
const content = result.llmContent as string[];
@@ -569,7 +590,8 @@ describe('ReadManyFilesTool', () => {
],
};
const result = await tool.execute(params, new AbortController().signal);
const invocation = tool.build(params);
const result = await invocation.execute(new AbortController().signal);
const content = result.llmContent as string[];
// Should successfully process valid files despite one failure
@@ -606,7 +628,8 @@ describe('ReadManyFilesTool', () => {
return 'text';
});
await tool.execute({ paths: files }, new AbortController().signal);
const invocation = tool.build({ paths: files });
await invocation.execute(new AbortController().signal);
console.log('Execution order:', executionOrder);