mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
fix: properly report tool errors in telemetry (#5688)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { ReadFileTool, ReadFileToolParams } from './read-file.js';
|
||||
import { ToolErrorType } from './tool-error.js';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
import fs from 'fs';
|
||||
@@ -47,276 +48,390 @@ describe('ReadFileTool', () => {
|
||||
absolute_path: path.join(tempRootDir, 'test.txt'),
|
||||
};
|
||||
const result = tool.build(params);
|
||||
expect(result).not.toBeTypeOf('string');
|
||||
expect(typeof result).toBe('object');
|
||||
expect(
|
||||
(result as ToolInvocation<ReadFileToolParams, ToolResult>).params,
|
||||
).toEqual(params);
|
||||
expect(typeof result).not.toBe('string');
|
||||
});
|
||||
|
||||
it('should return an invocation for valid params with offset and limit', () => {
|
||||
it('should throw error if file path is relative', () => {
|
||||
const params: ReadFileToolParams = {
|
||||
absolute_path: path.join(tempRootDir, 'test.txt'),
|
||||
offset: 0,
|
||||
limit: 10,
|
||||
absolute_path: 'relative/path.txt',
|
||||
};
|
||||
const result = tool.build(params);
|
||||
expect(result).not.toBeTypeOf('string');
|
||||
});
|
||||
|
||||
it('should throw error for relative path', () => {
|
||||
const params: ReadFileToolParams = { absolute_path: 'test.txt' };
|
||||
expect(() => tool.build(params)).toThrow(
|
||||
`File path must be absolute, but was relative: test.txt. You must provide an absolute path.`,
|
||||
'File path must be absolute, but was relative: relative/path.txt. You must provide an absolute path.',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error for path outside root', () => {
|
||||
const outsidePath = path.resolve(os.tmpdir(), 'outside-root.txt');
|
||||
const params: ReadFileToolParams = { absolute_path: outsidePath };
|
||||
it('should throw error if path is outside root', () => {
|
||||
const params: ReadFileToolParams = {
|
||||
absolute_path: '/outside/root.txt',
|
||||
};
|
||||
expect(() => tool.build(params)).toThrow(
|
||||
'File path must be within one of the workspace directories',
|
||||
/File path must be within one of the workspace directories/,
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error for negative offset', () => {
|
||||
it('should throw error if offset is negative', () => {
|
||||
const params: ReadFileToolParams = {
|
||||
absolute_path: path.join(tempRootDir, 'test.txt'),
|
||||
offset: -1,
|
||||
limit: 10,
|
||||
};
|
||||
expect(() => tool.build(params)).toThrow(
|
||||
'Offset must be a non-negative number',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error for non-positive limit', () => {
|
||||
const paramsZero: ReadFileToolParams = {
|
||||
it('should throw error if limit is zero or negative', () => {
|
||||
const params: ReadFileToolParams = {
|
||||
absolute_path: path.join(tempRootDir, 'test.txt'),
|
||||
offset: 0,
|
||||
limit: 0,
|
||||
};
|
||||
expect(() => tool.build(paramsZero)).toThrow(
|
||||
'Limit must be a positive number',
|
||||
);
|
||||
const paramsNegative: ReadFileToolParams = {
|
||||
absolute_path: path.join(tempRootDir, 'test.txt'),
|
||||
offset: 0,
|
||||
limit: -5,
|
||||
};
|
||||
expect(() => tool.build(paramsNegative)).toThrow(
|
||||
'Limit must be a positive number',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error for schema validation failure (e.g. missing path)', () => {
|
||||
const params = { offset: 0 } as unknown as ReadFileToolParams;
|
||||
expect(() => tool.build(params)).toThrow(
|
||||
`params must have required property 'absolute_path'`,
|
||||
'Limit must be a positive number',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
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('getDescription', () => {
|
||||
it('should return relative path without limit/offset', () => {
|
||||
const subDir = path.join(tempRootDir, 'sub', 'dir');
|
||||
const params: ReadFileToolParams = {
|
||||
absolute_path: path.join(subDir, 'file.txt'),
|
||||
};
|
||||
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'));
|
||||
});
|
||||
|
||||
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 shortened path when file path is deep', () => {
|
||||
const deepPath = path.join(
|
||||
tempRootDir,
|
||||
'very',
|
||||
'deep',
|
||||
'directory',
|
||||
'structure',
|
||||
'that',
|
||||
'exceeds',
|
||||
'the',
|
||||
'normal',
|
||||
'limit',
|
||||
'file.txt',
|
||||
);
|
||||
const params: ReadFileToolParams = { absolute_path: deepPath };
|
||||
const invocation = tool.build(params);
|
||||
expect(typeof invocation).not.toBe('string');
|
||||
const desc = (
|
||||
invocation as ToolInvocation<ReadFileToolParams, ToolResult>
|
||||
).getDescription();
|
||||
expect(desc).toContain('...');
|
||||
expect(desc).toContain('file.txt');
|
||||
});
|
||||
|
||||
it('should handle non-normalized file paths correctly', () => {
|
||||
const subDir = path.join(tempRootDir, 'sub', 'dir');
|
||||
const params: ReadFileToolParams = {
|
||||
absolute_path: path.join(subDir, '..', 'dir', 'file.txt'),
|
||||
};
|
||||
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'));
|
||||
});
|
||||
|
||||
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('.');
|
||||
});
|
||||
});
|
||||
|
||||
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
|
||||
>;
|
||||
|
||||
const result = await invocation.execute(abortSignal);
|
||||
expect(result).toEqual({
|
||||
llmContent:
|
||||
'Could not read file because no file was found at the specified path.',
|
||||
returnDisplay: 'File not found.',
|
||||
error: {
|
||||
message: `File not found: ${filePath}`,
|
||||
type: ToolErrorType.FILE_NOT_FOUND,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
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
|
||||
>;
|
||||
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 invocation.execute(abortSignal)).toEqual({
|
||||
llmContent: `File not found: ${filePath}`,
|
||||
returnDisplay: 'File not found.',
|
||||
});
|
||||
expect(await invocation.execute(abortSignal)).toEqual({
|
||||
llmContent: fileContent,
|
||||
returnDisplay: '',
|
||||
});
|
||||
});
|
||||
|
||||
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
|
||||
>;
|
||||
it('should return error if path is a directory', async () => {
|
||||
const dirPath = path.join(tempRootDir, 'directory');
|
||||
await fsp.mkdir(dirPath);
|
||||
const params: ReadFileToolParams = { absolute_path: dirPath };
|
||||
const invocation = tool.build(params) as ToolInvocation<
|
||||
ReadFileToolParams,
|
||||
ToolResult
|
||||
>;
|
||||
|
||||
expect(await invocation.execute(abortSignal)).toEqual({
|
||||
llmContent: fileContent,
|
||||
returnDisplay: '',
|
||||
});
|
||||
const result = await invocation.execute(abortSignal);
|
||||
expect(result).toEqual({
|
||||
llmContent:
|
||||
'Could not read file because the provided path is a directory, not a file.',
|
||||
returnDisplay: 'Path is a directory.',
|
||||
error: {
|
||||
message: `Path is a directory, not a file: ${dirPath}`,
|
||||
type: ToolErrorType.INVALID_TOOL_PARAMS,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
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
|
||||
>;
|
||||
it('should return error for a file that is too large', async () => {
|
||||
const filePath = path.join(tempRootDir, 'largefile.txt');
|
||||
// 21MB of content exceeds 20MB limit
|
||||
const largeContent = 'x'.repeat(21 * 1024 * 1024);
|
||||
await fsp.writeFile(filePath, largeContent, 'utf-8');
|
||||
const params: ReadFileToolParams = { absolute_path: filePath };
|
||||
const invocation = tool.build(params) as ToolInvocation<
|
||||
ReadFileToolParams,
|
||||
ToolResult
|
||||
>;
|
||||
|
||||
expect(await invocation.execute(abortSignal)).toEqual({
|
||||
llmContent: {
|
||||
inlineData: {
|
||||
mimeType: 'image/png',
|
||||
data: pngContent.toString('base64'),
|
||||
},
|
||||
},
|
||||
returnDisplay: `Read image file: image.png`,
|
||||
});
|
||||
const result = await invocation.execute(abortSignal);
|
||||
expect(result).toHaveProperty('error');
|
||||
expect(result.error?.type).toBe(ToolErrorType.FILE_TOO_LARGE);
|
||||
expect(result.error?.message).toContain(
|
||||
'File size exceeds the 20MB limit',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle text file with lines exceeding maximum length', async () => {
|
||||
const filePath = path.join(tempRootDir, 'longlines.txt');
|
||||
const longLine = 'a'.repeat(2500); // Exceeds MAX_LINE_LENGTH_TEXT_FILE (2000)
|
||||
const fileContent = `Short line\n${longLine}\nAnother short line`;
|
||||
await fsp.writeFile(filePath, fileContent, 'utf-8');
|
||||
const params: ReadFileToolParams = { absolute_path: filePath };
|
||||
const invocation = tool.build(params) as ToolInvocation<
|
||||
ReadFileToolParams,
|
||||
ToolResult
|
||||
>;
|
||||
|
||||
const result = await invocation.execute(abortSignal);
|
||||
expect(result.llmContent).toContain(
|
||||
'IMPORTANT: The file content has been truncated',
|
||||
);
|
||||
expect(result.llmContent).toContain('--- FILE CONTENT (truncated) ---');
|
||||
expect(result.returnDisplay).toContain('some lines were shortened');
|
||||
});
|
||||
|
||||
it('should handle image file and return appropriate content', async () => {
|
||||
const imagePath = path.join(tempRootDir, 'image.png');
|
||||
// Minimal PNG header
|
||||
const pngHeader = Buffer.from([
|
||||
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
|
||||
]);
|
||||
await fsp.writeFile(imagePath, pngHeader);
|
||||
const params: ReadFileToolParams = { absolute_path: imagePath };
|
||||
const invocation = tool.build(params) as ToolInvocation<
|
||||
ReadFileToolParams,
|
||||
ToolResult
|
||||
>;
|
||||
|
||||
const result = await invocation.execute(abortSignal);
|
||||
expect(result.llmContent).toEqual({
|
||||
inlineData: {
|
||||
data: pngHeader.toString('base64'),
|
||||
mimeType: 'image/png',
|
||||
},
|
||||
});
|
||||
expect(result.returnDisplay).toBe('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 };
|
||||
const invocation = tool.build(params) as ToolInvocation<
|
||||
ReadFileToolParams,
|
||||
ToolResult
|
||||
>;
|
||||
it('should handle PDF file and return appropriate content', async () => {
|
||||
const pdfPath = path.join(tempRootDir, 'document.pdf');
|
||||
// Minimal PDF header
|
||||
const pdfHeader = Buffer.from('%PDF-1.4');
|
||||
await fsp.writeFile(pdfPath, pdfHeader);
|
||||
const params: ReadFileToolParams = { absolute_path: pdfPath };
|
||||
const invocation = tool.build(params) as ToolInvocation<
|
||||
ReadFileToolParams,
|
||||
ToolResult
|
||||
>;
|
||||
|
||||
expect(await invocation.execute(abortSignal)).toEqual({
|
||||
llmContent: {
|
||||
inlineData: {
|
||||
mimeType: 'image/png',
|
||||
data: Buffer.from(fileContent).toString('base64'),
|
||||
},
|
||||
},
|
||||
returnDisplay: `Read image file: fake-image.png`,
|
||||
});
|
||||
const result = await invocation.execute(abortSignal);
|
||||
expect(result.llmContent).toEqual({
|
||||
inlineData: {
|
||||
data: pdfHeader.toString('base64'),
|
||||
mimeType: 'application/pdf',
|
||||
},
|
||||
});
|
||||
expect(result.returnDisplay).toBe('Read pdf file: document.pdf');
|
||||
});
|
||||
|
||||
it('should return a structured message when a slice of a text file is read', 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 handle binary file and skip content', async () => {
|
||||
const binPath = path.join(tempRootDir, 'binary.bin');
|
||||
// Binary data with null bytes
|
||||
const binaryData = Buffer.from([0x00, 0xff, 0x00, 0xff]);
|
||||
await fsp.writeFile(binPath, binaryData);
|
||||
const params: ReadFileToolParams = { absolute_path: binPath };
|
||||
const invocation = tool.build(params) as ToolInvocation<
|
||||
ReadFileToolParams,
|
||||
ToolResult
|
||||
>;
|
||||
|
||||
const params: ReadFileToolParams = {
|
||||
absolute_path: filePath,
|
||||
offset: 5, // Start from line 6
|
||||
limit: 3,
|
||||
};
|
||||
const invocation = tool.build(params) as ToolInvocation<
|
||||
ReadFileToolParams,
|
||||
ToolResult
|
||||
>;
|
||||
const result = await invocation.execute(abortSignal);
|
||||
expect(result.llmContent).toBe(
|
||||
'Cannot display content of binary file: binary.bin',
|
||||
);
|
||||
expect(result.returnDisplay).toBe('Skipped binary file: binary.bin');
|
||||
});
|
||||
|
||||
const result = await invocation.execute(abortSignal);
|
||||
it('should handle SVG file as text', async () => {
|
||||
const svgPath = path.join(tempRootDir, 'image.svg');
|
||||
const svgContent = '<svg><circle cx="50" cy="50" r="40"/></svg>';
|
||||
await fsp.writeFile(svgPath, svgContent, 'utf-8');
|
||||
const params: ReadFileToolParams = { absolute_path: svgPath };
|
||||
const invocation = tool.build(params) as ToolInvocation<
|
||||
ReadFileToolParams,
|
||||
ToolResult
|
||||
>;
|
||||
|
||||
const expectedLlmContent = `
|
||||
IMPORTANT: The file content has been truncated.
|
||||
Status: Showing lines 6-8 of 20 total lines.
|
||||
Action: To read more of the file, you can use the 'offset' and 'limit' parameters in a subsequent 'read_file' call. For example, to read the next section of the file, use offset: 8.
|
||||
const result = await invocation.execute(abortSignal);
|
||||
expect(result.llmContent).toBe(svgContent);
|
||||
expect(result.returnDisplay).toBe('Read SVG as text: image.svg');
|
||||
});
|
||||
|
||||
--- FILE CONTENT (truncated) ---
|
||||
Line 6
|
||||
Line 7
|
||||
Line 8`;
|
||||
it('should handle large SVG file', async () => {
|
||||
const svgPath = path.join(tempRootDir, 'large.svg');
|
||||
// Create SVG content larger than 1MB
|
||||
const largeContent = '<svg>' + 'x'.repeat(1024 * 1024 + 1) + '</svg>';
|
||||
await fsp.writeFile(svgPath, largeContent, 'utf-8');
|
||||
const params: ReadFileToolParams = { absolute_path: svgPath };
|
||||
const invocation = tool.build(params) as ToolInvocation<
|
||||
ReadFileToolParams,
|
||||
ToolResult
|
||||
>;
|
||||
|
||||
expect(result.llmContent).toEqual(expectedLlmContent);
|
||||
expect(result.returnDisplay).toBe(
|
||||
'Read lines 6-8 of 20 from paginated.txt',
|
||||
const result = await invocation.execute(abortSignal);
|
||||
expect(result.llmContent).toBe(
|
||||
'Cannot display content of SVG file larger than 1MB: large.svg',
|
||||
);
|
||||
expect(result.returnDisplay).toBe(
|
||||
'Skipped large SVG file (>1MB): large.svg',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle empty file', async () => {
|
||||
const emptyPath = path.join(tempRootDir, 'empty.txt');
|
||||
await fsp.writeFile(emptyPath, '', 'utf-8');
|
||||
const params: ReadFileToolParams = { absolute_path: emptyPath };
|
||||
const invocation = tool.build(params) as ToolInvocation<
|
||||
ReadFileToolParams,
|
||||
ToolResult
|
||||
>;
|
||||
|
||||
const result = await invocation.execute(abortSignal);
|
||||
expect(result.llmContent).toBe('');
|
||||
expect(result.returnDisplay).toBe('');
|
||||
});
|
||||
|
||||
it('should support offset and limit for text files', async () => {
|
||||
const filePath = path.join(tempRootDir, 'paginated.txt');
|
||||
const lines = Array.from({ length: 20 }, (_, i) => `Line ${i + 1}`);
|
||||
const fileContent = lines.join('\n');
|
||||
await fsp.writeFile(filePath, fileContent, 'utf-8');
|
||||
|
||||
const params: ReadFileToolParams = {
|
||||
absolute_path: filePath,
|
||||
offset: 5, // Start from line 6
|
||||
limit: 3,
|
||||
};
|
||||
const invocation = tool.build(params) as ToolInvocation<
|
||||
ReadFileToolParams,
|
||||
ToolResult
|
||||
>;
|
||||
|
||||
const result = await invocation.execute(abortSignal);
|
||||
expect(result.llmContent).toContain(
|
||||
'IMPORTANT: The file content has been truncated',
|
||||
);
|
||||
expect(result.llmContent).toContain(
|
||||
'Status: Showing lines 6-8 of 20 total lines',
|
||||
);
|
||||
expect(result.llmContent).toContain('Line 6');
|
||||
expect(result.llmContent).toContain('Line 7');
|
||||
expect(result.llmContent).toContain('Line 8');
|
||||
expect(result.returnDisplay).toBe(
|
||||
'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 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 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 throw error if file is in an ignored directory', async () => {
|
||||
const ignoredDirPath = path.join(tempRootDir, 'ignored');
|
||||
await fsp.mkdir(ignoredDirPath, { recursive: true });
|
||||
const ignoredFilePath = path.join(ignoredDirPath, 'file.txt');
|
||||
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 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(() => tool.build(params)).toThrow(expectedError);
|
||||
});
|
||||
it('should allow reading non-ignored files', async () => {
|
||||
const allowedFilePath = path.join(tempRootDir, 'allowed.txt');
|
||||
await fsp.writeFile(allowedFilePath, 'content', 'utf-8');
|
||||
const params: ReadFileToolParams = {
|
||||
absolute_path: allowedFilePath,
|
||||
};
|
||||
const invocation = tool.build(params);
|
||||
expect(typeof invocation).not.toBe('string');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('workspace boundary validation', () => {
|
||||
it('should validate paths are within workspace root', () => {
|
||||
const params: ReadFileToolParams = {
|
||||
absolute_path: path.join(tempRootDir, 'file.txt'),
|
||||
};
|
||||
expect(() => tool.build(params)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should reject paths outside workspace root', () => {
|
||||
const params: ReadFileToolParams = {
|
||||
absolute_path: '/etc/passwd',
|
||||
};
|
||||
expect(() => tool.build(params)).toThrow(
|
||||
'File path must be within one of the workspace directories',
|
||||
);
|
||||
});
|
||||
|
||||
it('should provide clear error message with workspace directories', () => {
|
||||
const outsidePath = path.join(os.tmpdir(), 'outside-workspace.txt');
|
||||
const params: ReadFileToolParams = {
|
||||
absolute_path: outsidePath,
|
||||
};
|
||||
expect(() => tool.build(params)).toThrow(
|
||||
'File path must be within one of the workspace directories',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user