summaryrefslogtreecommitdiff
path: root/packages/core/src/tools/read-file.test.ts
diff options
context:
space:
mode:
authorjoshualitt <[email protected]>2025-08-06 10:50:02 -0700
committerGitHub <[email protected]>2025-08-06 17:50:02 +0000
commit6133bea388a2de69c71a6be6f1450707f2ce4dfb (patch)
tree367de1d618069ea80e47d7e86c4fb8f82ad032a7 /packages/core/src/tools/read-file.test.ts
parent882a97aff998b2f19731e9966d135f1db5a59914 (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.ts310
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);
});
});
});