buster/packages/ai/tests/tools/unit/read-file-tool.test.ts

159 lines
5.2 KiB
TypeScript

import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterEach, beforeEach, describe, expect, test } from 'vitest';
import { readFileTool } from '../../../src/tools/file-tools/read-file-tool';
describe('Read File Tool Unit Tests', () => {
let tempDir: string;
let testFile: string;
let multiTestFile: string;
beforeEach(() => {
// Create temporary directory for tests
tempDir = join(tmpdir(), `read-file-test-${Date.now()}`);
mkdirSync(tempDir, { recursive: true });
testFile = join(tempDir, 'test.txt');
multiTestFile = join(tempDir, 'multi.txt');
// Create test files
writeFileSync(testFile, 'Line 1\nLine 2\nLine 3\nLine 4\nLine 5');
writeFileSync(multiTestFile, 'Multi line 1\nMulti line 2\nMulti line 3');
});
afterEach(() => {
// Clean up temporary directory
try {
rmSync(tempDir, { recursive: true, force: true });
} catch {
// Ignore cleanup errors
}
});
test('should have correct configuration', () => {
expect(readFileTool.id).toBe('read-file');
expect(readFileTool.description).toBe(
'Reads files from the local filesystem with optional line offset and limit'
);
expect(readFileTool.inputSchema).toBeDefined();
expect(readFileTool.outputSchema).toBeDefined();
expect(readFileTool.execute).toBeDefined();
});
test('should validate single file input schema', () => {
const validInput = { file_path: '/absolute/path/to/file.txt' };
const result = readFileTool.inputSchema.safeParse(validInput);
expect(result.success).toBe(true);
});
test('should validate multiple files input schema', () => {
const validInput = {
file_paths: ['/absolute/path/to/file1.txt', '/absolute/path/to/file2.txt'],
};
const result = readFileTool.inputSchema.safeParse(validInput);
expect(result.success).toBe(true);
});
test('should validate output schema structure', () => {
const validOutput = {
content: 'file content',
files_read: 1,
total_lines: 10,
truncated: false,
};
const result = readFileTool.outputSchema.safeParse(validOutput);
expect(result.success).toBe(true);
});
test('should read single file successfully', async () => {
const result = await readFileTool.execute({
context: { file_path: testFile },
});
expect(result.files_read).toBe(1);
expect(result.content).toContain('Line 1');
expect(result.content).toContain('Line 5');
expect(result.truncated).toBe(false);
});
test('should read multiple files successfully', async () => {
const result = await readFileTool.execute({
context: { file_paths: [testFile, multiTestFile] },
});
expect(result.files_read).toBe(2);
expect(result.content).toContain(`==> ${testFile} <==`);
expect(result.content).toContain(`==> ${multiTestFile} <==`);
expect(result.content).toContain('Line 1');
expect(result.content).toContain('Multi line 1');
});
test('should apply offset and limit correctly', async () => {
const result = await readFileTool.execute({
context: { file_path: testFile, offset: 1, limit: 2 },
});
expect(result.content).toContain(' 2 Line 2');
expect(result.content).toContain(' 3 Line 3');
expect(result.content).not.toContain('Line 1');
expect(result.content).not.toContain('Line 4');
expect(result.truncated).toBe(true);
});
test('should handle non-absolute paths with error message', async () => {
const result = await readFileTool.execute({
context: { file_path: 'relative/path.txt' },
});
expect(result.content).toContain('Error reading relative/path.txt');
expect(result.content).toContain('File path must be absolute');
});
test('should handle non-existent files with error message', async () => {
const nonExistentFile = join(tempDir, 'nonexistent.txt');
const result = await readFileTool.execute({
context: { file_path: nonExistentFile },
});
expect(result.content).toContain('Error reading');
expect(result.content).toContain('File does not exist');
});
test('should handle path traversal attempts with error message', async () => {
const result = await readFileTool.execute({
context: { file_path: '/tmp/../etc/passwd' },
});
expect(result.content).toContain('Error reading');
expect(result.content).toContain('Access denied to path');
});
test('should handle access to sensitive directories with error message', async () => {
const result = await readFileTool.execute({
context: { file_path: '/etc/passwd' },
});
expect(result.content).toContain('Error reading');
expect(result.content).toContain('Access denied to path');
});
test('should handle missing file_path and file_paths', async () => {
await expect(
readFileTool.execute({
context: {},
})
).rejects.toThrow('Must provide either file_path or file_paths');
});
test('should format line numbers correctly', async () => {
const result = await readFileTool.execute({
context: { file_path: testFile },
});
expect(result.content).toMatch(/\s+1 Line 1/);
expect(result.content).toMatch(/\s+2 Line 2/);
expect(result.content).toMatch(/\s+5 Line 5/);
});
});