summaryrefslogtreecommitdiff
path: root/packages/core/src/tools/read-file.test.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/core/src/tools/read-file.test.ts')
-rw-r--r--packages/core/src/tools/read-file.test.ts228
1 files changed, 228 insertions, 0 deletions
diff --git a/packages/core/src/tools/read-file.test.ts b/packages/core/src/tools/read-file.test.ts
new file mode 100644
index 00000000..8ea42134
--- /dev/null
+++ b/packages/core/src/tools/read-file.test.ts
@@ -0,0 +1,228 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { vi, describe, it, expect, beforeEach, afterEach, Mock } from 'vitest';
+import { ReadFileTool, ReadFileToolParams } from './read-file.js';
+import * as fileUtils from '../utils/fileUtils.js';
+import path from 'path';
+import os from 'os';
+import fs from 'fs'; // For actual fs operations in setup
+
+// Mock fileUtils.processSingleFileContent
+vi.mock('../utils/fileUtils', async () => {
+ const actualFileUtils =
+ await vi.importActual<typeof fileUtils>('../utils/fileUtils');
+ return {
+ ...actualFileUtils, // Spread actual implementations
+ processSingleFileContent: vi.fn(), // Mock specific function
+ };
+});
+
+const mockProcessSingleFileContent = fileUtils.processSingleFileContent as Mock;
+
+describe('ReadFileTool', () => {
+ let tempRootDir: string;
+ let tool: ReadFileTool;
+ const abortSignal = new AbortController().signal;
+
+ beforeEach(() => {
+ // Create a unique temporary root directory for each test run
+ tempRootDir = fs.mkdtempSync(
+ path.join(os.tmpdir(), 'read-file-tool-root-'),
+ );
+ tool = new ReadFileTool(tempRootDir);
+ mockProcessSingleFileContent.mockReset();
+ });
+
+ afterEach(() => {
+ // Clean up the temporary root directory
+ if (fs.existsSync(tempRootDir)) {
+ fs.rmSync(tempRootDir, { recursive: true, force: true });
+ }
+ });
+
+ describe('validateToolParams', () => {
+ it('should return null for valid params (absolute path within root)', () => {
+ const params: ReadFileToolParams = {
+ path: path.join(tempRootDir, 'test.txt'),
+ };
+ expect(tool.validateToolParams(params)).toBeNull();
+ });
+
+ it('should return null for valid params with offset and limit', () => {
+ const params: ReadFileToolParams = {
+ path: path.join(tempRootDir, 'test.txt'),
+ offset: 0,
+ limit: 10,
+ };
+ expect(tool.validateToolParams(params)).toBeNull();
+ });
+
+ it('should return error for relative path', () => {
+ const params: ReadFileToolParams = { path: 'test.txt' };
+ expect(tool.validateToolParams(params)).toMatch(
+ /File path must be absolute/,
+ );
+ });
+
+ it('should return error for path outside root', () => {
+ const outsidePath = path.resolve(os.tmpdir(), 'outside-root.txt');
+ const params: ReadFileToolParams = { path: outsidePath };
+ expect(tool.validateToolParams(params)).toMatch(
+ /File path must be within the root directory/,
+ );
+ });
+
+ it('should return error for negative offset', () => {
+ const params: ReadFileToolParams = {
+ path: path.join(tempRootDir, 'test.txt'),
+ offset: -1,
+ limit: 10,
+ };
+ expect(tool.validateToolParams(params)).toBe(
+ 'Offset must be a non-negative number',
+ );
+ });
+
+ it('should return error for non-positive limit', () => {
+ const paramsZero: ReadFileToolParams = {
+ path: path.join(tempRootDir, 'test.txt'),
+ offset: 0,
+ limit: 0,
+ };
+ expect(tool.validateToolParams(paramsZero)).toBe(
+ 'Limit must be a positive number',
+ );
+ const paramsNegative: ReadFileToolParams = {
+ path: path.join(tempRootDir, 'test.txt'),
+ offset: 0,
+ limit: -5,
+ };
+ expect(tool.validateToolParams(paramsNegative)).toBe(
+ 'Limit must be a positive number',
+ );
+ });
+
+ it('should return error for schema validation failure (e.g. missing path)', () => {
+ const params = { offset: 0 } as unknown as ReadFileToolParams;
+ expect(tool.validateToolParams(params)).toBe(
+ 'Parameters failed schema validation.',
+ );
+ });
+ });
+
+ describe('getDescription', () => {
+ it('should return a shortened, relative path', () => {
+ const filePath = path.join(tempRootDir, 'sub', 'dir', 'file.txt');
+ const params: ReadFileToolParams = { path: filePath };
+ // Assuming tempRootDir is something like /tmp/read-file-tool-root-XXXXXX
+ // The relative path would be sub/dir/file.txt
+ expect(tool.getDescription(params)).toBe('sub/dir/file.txt');
+ });
+
+ it('should return . if path is the root directory', () => {
+ const params: ReadFileToolParams = { path: tempRootDir };
+ expect(tool.getDescription(params)).toBe('.');
+ });
+ });
+
+ describe('execute', () => {
+ it('should return validation error if params are invalid', async () => {
+ const params: ReadFileToolParams = { path: 'relative/path.txt' };
+ const result = await tool.execute(params, abortSignal);
+ expect(result.llmContent).toMatch(/Error: Invalid parameters provided/);
+ expect(result.returnDisplay).toMatch(/File path must be absolute/);
+ });
+
+ it('should return error from processSingleFileContent if it fails', async () => {
+ const filePath = path.join(tempRootDir, 'error.txt');
+ const params: ReadFileToolParams = { path: filePath };
+ const errorMessage = 'Simulated read error';
+ mockProcessSingleFileContent.mockResolvedValue({
+ llmContent: `Error reading file ${filePath}: ${errorMessage}`,
+ returnDisplay: `Error reading file ${filePath}: ${errorMessage}`,
+ error: errorMessage,
+ });
+
+ const result = await tool.execute(params, abortSignal);
+ expect(mockProcessSingleFileContent).toHaveBeenCalledWith(
+ filePath,
+ tempRootDir,
+ undefined,
+ undefined,
+ );
+ expect(result.llmContent).toContain(errorMessage);
+ expect(result.returnDisplay).toContain(errorMessage);
+ });
+
+ it('should return success result for a text file', async () => {
+ const filePath = path.join(tempRootDir, 'textfile.txt');
+ const fileContent = 'This is a test file.';
+ const params: ReadFileToolParams = { path: filePath };
+ mockProcessSingleFileContent.mockResolvedValue({
+ llmContent: fileContent,
+ returnDisplay: `Read text file: ${path.basename(filePath)}`,
+ });
+
+ const result = await tool.execute(params, abortSignal);
+ expect(mockProcessSingleFileContent).toHaveBeenCalledWith(
+ filePath,
+ tempRootDir,
+ undefined,
+ undefined,
+ );
+ expect(result.llmContent).toBe(fileContent);
+ expect(result.returnDisplay).toBe(
+ `Read text file: ${path.basename(filePath)}`,
+ );
+ });
+
+ it('should return success result for an image file', async () => {
+ const filePath = path.join(tempRootDir, 'image.png');
+ const imageData = {
+ inlineData: { mimeType: 'image/png', data: 'base64...' },
+ };
+ const params: ReadFileToolParams = { path: filePath };
+ mockProcessSingleFileContent.mockResolvedValue({
+ llmContent: imageData,
+ returnDisplay: `Read image file: ${path.basename(filePath)}`,
+ });
+
+ const result = await tool.execute(params, abortSignal);
+ expect(mockProcessSingleFileContent).toHaveBeenCalledWith(
+ filePath,
+ tempRootDir,
+ undefined,
+ undefined,
+ );
+ expect(result.llmContent).toEqual(imageData);
+ expect(result.returnDisplay).toBe(
+ `Read image file: ${path.basename(filePath)}`,
+ );
+ });
+
+ it('should pass offset and limit to processSingleFileContent', async () => {
+ const filePath = path.join(tempRootDir, 'paginated.txt');
+ const params: ReadFileToolParams = {
+ path: filePath,
+ offset: 10,
+ limit: 5,
+ };
+ mockProcessSingleFileContent.mockResolvedValue({
+ llmContent: 'some lines',
+ returnDisplay: 'Read text file (paginated)',
+ });
+
+ await tool.execute(params, abortSignal);
+ expect(mockProcessSingleFileContent).toHaveBeenCalledWith(
+ filePath,
+ tempRootDir,
+ 10,
+ 5,
+ );
+ });
+ });
+});