diff options
| author | joshualitt <[email protected]> | 2025-08-06 10:50:02 -0700 |
|---|---|---|
| committer | GitHub <[email protected]> | 2025-08-06 17:50:02 +0000 |
| commit | 6133bea388a2de69c71a6be6f1450707f2ce4dfb (patch) | |
| tree | 367de1d618069ea80e47d7e86c4fb8f82ad032a7 /packages/core/src/tools/read-file.test.ts | |
| parent | 882a97aff998b2f19731e9966d135f1db5a59914 (diff) | |
feat(core): Introduce `DeclarativeTool` and `ToolInvocation`. (#5613)
Diffstat (limited to 'packages/core/src/tools/read-file.test.ts')
| -rw-r--r-- | packages/core/src/tools/read-file.test.ts | 310 |
1 files changed, 163 insertions, 147 deletions
diff --git a/packages/core/src/tools/read-file.test.ts b/packages/core/src/tools/read-file.test.ts index fa1e458c..bb9317fd 100644 --- a/packages/core/src/tools/read-file.test.ts +++ b/packages/core/src/tools/read-file.test.ts @@ -13,6 +13,7 @@ import fsp from 'fs/promises'; import { Config } from '../config/config.js'; import { FileDiscoveryService } from '../services/fileDiscoveryService.js'; import { createMockWorkspaceContext } from '../test-utils/mockWorkspaceContext.js'; +import { ToolInvocation, ToolResult } from './tools.js'; describe('ReadFileTool', () => { let tempRootDir: string; @@ -40,57 +41,62 @@ describe('ReadFileTool', () => { } }); - describe('validateToolParams', () => { - it('should return null for valid params (absolute path within root)', () => { + describe('build', () => { + it('should return an invocation for valid params (absolute path within root)', () => { const params: ReadFileToolParams = { absolute_path: path.join(tempRootDir, 'test.txt'), }; - expect(tool.validateToolParams(params)).toBeNull(); + const result = tool.build(params); + expect(result).not.toBeTypeOf('string'); + expect(typeof result).toBe('object'); + expect( + (result as ToolInvocation<ReadFileToolParams, ToolResult>).params, + ).toEqual(params); }); - it('should return null for valid params with offset and limit', () => { + it('should return an invocation for valid params with offset and limit', () => { const params: ReadFileToolParams = { absolute_path: path.join(tempRootDir, 'test.txt'), offset: 0, limit: 10, }; - expect(tool.validateToolParams(params)).toBeNull(); + const result = tool.build(params); + expect(result).not.toBeTypeOf('string'); }); - it('should return error for relative path', () => { + it('should throw error for relative path', () => { const params: ReadFileToolParams = { absolute_path: 'test.txt' }; - expect(tool.validateToolParams(params)).toBe( + expect(() => tool.build(params)).toThrow( `File path must be absolute, but was relative: test.txt. You must provide an absolute path.`, ); }); - it('should return error for path outside root', () => { + it('should throw error for path outside root', () => { const outsidePath = path.resolve(os.tmpdir(), 'outside-root.txt'); const params: ReadFileToolParams = { absolute_path: outsidePath }; - const error = tool.validateToolParams(params); - expect(error).toContain( + expect(() => tool.build(params)).toThrow( 'File path must be within one of the workspace directories', ); }); - it('should return error for negative offset', () => { + it('should throw error for negative offset', () => { const params: ReadFileToolParams = { absolute_path: path.join(tempRootDir, 'test.txt'), offset: -1, limit: 10, }; - expect(tool.validateToolParams(params)).toBe( + expect(() => tool.build(params)).toThrow( 'Offset must be a non-negative number', ); }); - it('should return error for non-positive limit', () => { + it('should throw error for non-positive limit', () => { const paramsZero: ReadFileToolParams = { absolute_path: path.join(tempRootDir, 'test.txt'), offset: 0, limit: 0, }; - expect(tool.validateToolParams(paramsZero)).toBe( + expect(() => tool.build(paramsZero)).toThrow( 'Limit must be a positive number', ); const paramsNegative: ReadFileToolParams = { @@ -98,168 +104,182 @@ describe('ReadFileTool', () => { offset: 0, limit: -5, }; - expect(tool.validateToolParams(paramsNegative)).toBe( + expect(() => tool.build(paramsNegative)).toThrow( 'Limit must be a positive number', ); }); - it('should return error for schema validation failure (e.g. missing path)', () => { + it('should throw error for schema validation failure (e.g. missing path)', () => { const params = { offset: 0 } as unknown as ReadFileToolParams; - expect(tool.validateToolParams(params)).toBe( + expect(() => tool.build(params)).toThrow( `params must have required property 'absolute_path'`, ); }); }); - describe('getDescription', () => { - it('should return a shortened, relative path', () => { - const filePath = path.join(tempRootDir, 'sub', 'dir', 'file.txt'); - const params: ReadFileToolParams = { absolute_path: filePath }; - expect(tool.getDescription(params)).toBe( - path.join('sub', 'dir', 'file.txt'), - ); - }); - - it('should return . if path is the root directory', () => { - const params: ReadFileToolParams = { absolute_path: tempRootDir }; - expect(tool.getDescription(params)).toBe('.'); - }); - }); + describe('ToolInvocation', () => { + describe('getDescription', () => { + it('should return a shortened, relative path', () => { + const filePath = path.join(tempRootDir, 'sub', 'dir', 'file.txt'); + const params: ReadFileToolParams = { absolute_path: filePath }; + const invocation = tool.build(params); + expect(typeof invocation).not.toBe('string'); + expect( + ( + invocation as ToolInvocation<ReadFileToolParams, ToolResult> + ).getDescription(), + ).toBe(path.join('sub', 'dir', 'file.txt')); + }); - describe('execute', () => { - it('should return validation error if params are invalid', async () => { - const params: ReadFileToolParams = { - absolute_path: 'relative/path.txt', - }; - expect(await tool.execute(params, abortSignal)).toEqual({ - llmContent: - 'Error: Invalid parameters provided. Reason: File path must be absolute, but was relative: relative/path.txt. You must provide an absolute path.', - returnDisplay: - 'File path must be absolute, but was relative: relative/path.txt. You must provide an absolute path.', + it('should return . if path is the root directory', () => { + const params: ReadFileToolParams = { absolute_path: tempRootDir }; + const invocation = tool.build(params); + expect(typeof invocation).not.toBe('string'); + expect( + ( + invocation as ToolInvocation<ReadFileToolParams, ToolResult> + ).getDescription(), + ).toBe('.'); }); }); - it('should return error if file does not exist', async () => { - const filePath = path.join(tempRootDir, 'nonexistent.txt'); - const params: ReadFileToolParams = { absolute_path: filePath }; + describe('execute', () => { + it('should return error if file does not exist', async () => { + const filePath = path.join(tempRootDir, 'nonexistent.txt'); + const params: ReadFileToolParams = { absolute_path: filePath }; + const invocation = tool.build(params) as ToolInvocation< + ReadFileToolParams, + ToolResult + >; - expect(await tool.execute(params, abortSignal)).toEqual({ - llmContent: `File not found: ${filePath}`, - returnDisplay: 'File not found.', + expect(await invocation.execute(abortSignal)).toEqual({ + llmContent: `File not found: ${filePath}`, + returnDisplay: 'File not found.', + }); }); - }); - it('should return success result for a text file', async () => { - const filePath = path.join(tempRootDir, 'textfile.txt'); - const fileContent = 'This is a test file.'; - await fsp.writeFile(filePath, fileContent, 'utf-8'); - const params: ReadFileToolParams = { absolute_path: filePath }; + it('should return success result for a text file', async () => { + const filePath = path.join(tempRootDir, 'textfile.txt'); + const fileContent = 'This is a test file.'; + await fsp.writeFile(filePath, fileContent, 'utf-8'); + const params: ReadFileToolParams = { absolute_path: filePath }; + const invocation = tool.build(params) as ToolInvocation< + ReadFileToolParams, + ToolResult + >; - expect(await tool.execute(params, abortSignal)).toEqual({ - llmContent: fileContent, - returnDisplay: '', + expect(await invocation.execute(abortSignal)).toEqual({ + llmContent: fileContent, + returnDisplay: '', + }); }); - }); - it('should return success result for an image file', async () => { - // A minimal 1x1 transparent PNG file. - const pngContent = Buffer.from([ - 137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, - 1, 0, 0, 0, 1, 8, 6, 0, 0, 0, 31, 21, 196, 137, 0, 0, 0, 10, 73, 68, 65, - 84, 120, 156, 99, 0, 1, 0, 0, 5, 0, 1, 13, 10, 45, 180, 0, 0, 0, 0, 73, - 69, 78, 68, 174, 66, 96, 130, - ]); - const filePath = path.join(tempRootDir, 'image.png'); - await fsp.writeFile(filePath, pngContent); - const params: ReadFileToolParams = { absolute_path: filePath }; + it('should return success result for an image file', async () => { + // A minimal 1x1 transparent PNG file. + const pngContent = Buffer.from([ + 137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, + 1, 0, 0, 0, 1, 8, 6, 0, 0, 0, 31, 21, 196, 137, 0, 0, 0, 10, 73, 68, + 65, 84, 120, 156, 99, 0, 1, 0, 0, 5, 0, 1, 13, 10, 45, 180, 0, 0, 0, + 0, 73, 69, 78, 68, 174, 66, 96, 130, + ]); + const filePath = path.join(tempRootDir, 'image.png'); + await fsp.writeFile(filePath, pngContent); + const params: ReadFileToolParams = { absolute_path: filePath }; + const invocation = tool.build(params) as ToolInvocation< + ReadFileToolParams, + ToolResult + >; - expect(await tool.execute(params, abortSignal)).toEqual({ - llmContent: { - inlineData: { - mimeType: 'image/png', - data: pngContent.toString('base64'), + expect(await invocation.execute(abortSignal)).toEqual({ + llmContent: { + inlineData: { + mimeType: 'image/png', + data: pngContent.toString('base64'), + }, }, - }, - returnDisplay: `Read image file: image.png`, + returnDisplay: `Read image file: image.png`, + }); }); - }); - it('should treat a non-image file with image extension as an image', async () => { - const filePath = path.join(tempRootDir, 'fake-image.png'); - const fileContent = 'This is not a real png.'; - await fsp.writeFile(filePath, fileContent, 'utf-8'); - const params: ReadFileToolParams = { absolute_path: filePath }; + it('should treat a non-image file with image extension as an image', async () => { + const filePath = path.join(tempRootDir, 'fake-image.png'); + const fileContent = 'This is not a real png.'; + await fsp.writeFile(filePath, fileContent, 'utf-8'); + const params: ReadFileToolParams = { absolute_path: filePath }; + const invocation = tool.build(params) as ToolInvocation< + ReadFileToolParams, + ToolResult + >; - expect(await tool.execute(params, abortSignal)).toEqual({ - llmContent: { - inlineData: { - mimeType: 'image/png', - data: Buffer.from(fileContent).toString('base64'), + expect(await invocation.execute(abortSignal)).toEqual({ + llmContent: { + inlineData: { + mimeType: 'image/png', + data: Buffer.from(fileContent).toString('base64'), + }, }, - }, - returnDisplay: `Read image file: fake-image.png`, + returnDisplay: `Read image file: fake-image.png`, + }); }); - }); - it('should pass offset and limit to read a slice of a text file', async () => { - const filePath = path.join(tempRootDir, 'paginated.txt'); - const fileContent = Array.from( - { length: 20 }, - (_, i) => `Line ${i + 1}`, - ).join('\n'); - await fsp.writeFile(filePath, fileContent, 'utf-8'); + it('should pass offset and limit to read a slice of a text file', async () => { + const filePath = path.join(tempRootDir, 'paginated.txt'); + const fileContent = Array.from( + { length: 20 }, + (_, i) => `Line ${i + 1}`, + ).join('\n'); + await fsp.writeFile(filePath, fileContent, 'utf-8'); - const params: ReadFileToolParams = { - absolute_path: filePath, - offset: 5, // Start from line 6 - limit: 3, - }; + const params: ReadFileToolParams = { + absolute_path: filePath, + offset: 5, // Start from line 6 + limit: 3, + }; + const invocation = tool.build(params) as ToolInvocation< + ReadFileToolParams, + ToolResult + >; - expect(await tool.execute(params, abortSignal)).toEqual({ - llmContent: [ - '[File content truncated: showing lines 6-8 of 20 total lines. Use offset/limit parameters to view more.]', - 'Line 6', - 'Line 7', - 'Line 8', - ].join('\n'), - returnDisplay: 'Read lines 6-8 of 20 from paginated.txt', + expect(await invocation.execute(abortSignal)).toEqual({ + llmContent: [ + '[File content truncated: showing lines 6-8 of 20 total lines. Use offset/limit parameters to view more.]', + 'Line 6', + 'Line 7', + 'Line 8', + ].join('\n'), + returnDisplay: 'Read lines 6-8 of 20 from paginated.txt', + }); }); - }); - describe('with .geminiignore', () => { - beforeEach(async () => { - await fsp.writeFile( - path.join(tempRootDir, '.geminiignore'), - ['foo.*', 'ignored/'].join('\n'), - ); - }); + describe('with .geminiignore', () => { + beforeEach(async () => { + await fsp.writeFile( + path.join(tempRootDir, '.geminiignore'), + ['foo.*', 'ignored/'].join('\n'), + ); + }); - it('should return error if path is ignored by a .geminiignore pattern', async () => { - const ignoredFilePath = path.join(tempRootDir, 'foo.bar'); - await fsp.writeFile(ignoredFilePath, 'content', 'utf-8'); - const params: ReadFileToolParams = { - absolute_path: ignoredFilePath, - }; - const expectedError = `File path '${ignoredFilePath}' is ignored by .geminiignore pattern(s).`; - expect(await tool.execute(params, abortSignal)).toEqual({ - llmContent: `Error: Invalid parameters provided. Reason: ${expectedError}`, - returnDisplay: expectedError, + it('should throw error if path is ignored by a .geminiignore pattern', async () => { + const ignoredFilePath = path.join(tempRootDir, 'foo.bar'); + await fsp.writeFile(ignoredFilePath, 'content', 'utf-8'); + const params: ReadFileToolParams = { + absolute_path: ignoredFilePath, + }; + const expectedError = `File path '${ignoredFilePath}' is ignored by .geminiignore pattern(s).`; + expect(() => tool.build(params)).toThrow(expectedError); }); - }); - it('should return error if path is in an ignored directory', async () => { - const ignoredDirPath = path.join(tempRootDir, 'ignored'); - await fsp.mkdir(ignoredDirPath); - const filePath = path.join(ignoredDirPath, 'somefile.txt'); - await fsp.writeFile(filePath, 'content', 'utf-8'); + it('should throw error if path is in an ignored directory', async () => { + const ignoredDirPath = path.join(tempRootDir, 'ignored'); + await fsp.mkdir(ignoredDirPath); + const filePath = path.join(ignoredDirPath, 'somefile.txt'); + await fsp.writeFile(filePath, 'content', 'utf-8'); - const params: ReadFileToolParams = { - absolute_path: filePath, - }; - const expectedError = `File path '${filePath}' is ignored by .geminiignore pattern(s).`; - expect(await tool.execute(params, abortSignal)).toEqual({ - llmContent: `Error: Invalid parameters provided. Reason: ${expectedError}`, - returnDisplay: expectedError, + const params: ReadFileToolParams = { + absolute_path: filePath, + }; + const expectedError = `File path '${filePath}' is ignored by .geminiignore pattern(s).`; + expect(() => tool.build(params)).toThrow(expectedError); }); }); }); @@ -270,18 +290,16 @@ describe('ReadFileTool', () => { const params: ReadFileToolParams = { absolute_path: path.join(tempRootDir, 'file.txt'), }; - expect(tool.validateToolParams(params)).toBeNull(); + expect(() => tool.build(params)).not.toThrow(); }); it('should reject paths outside workspace root', () => { const params: ReadFileToolParams = { absolute_path: '/etc/passwd', }; - const error = tool.validateToolParams(params); - expect(error).toContain( + expect(() => tool.build(params)).toThrow( 'File path must be within one of the workspace directories', ); - expect(error).toContain(tempRootDir); }); it('should provide clear error message with workspace directories', () => { @@ -289,11 +307,9 @@ describe('ReadFileTool', () => { const params: ReadFileToolParams = { absolute_path: outsidePath, }; - const error = tool.validateToolParams(params); - expect(error).toContain( + expect(() => tool.build(params)).toThrow( 'File path must be within one of the workspace directories', ); - expect(error).toContain(tempRootDir); }); }); }); |
