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

301 lines
8.5 KiB
TypeScript

import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterEach, beforeEach, describe, expect, test } from 'vitest';
import { editFileTool } from '../../../src/tools/file-tools/edit-file-tool';
describe('Edit File Tool Unit Tests', () => {
let tempDir: string;
let testFile: string;
beforeEach(() => {
// Create temporary directory for tests
tempDir = join(tmpdir(), `edit-file-test-${Date.now()}`);
mkdirSync(tempDir, { recursive: true });
testFile = join(tempDir, 'test.txt');
});
afterEach(() => {
// Clean up temporary directory
try {
rmSync(tempDir, { recursive: true, force: true });
} catch {
// Ignore cleanup errors
}
});
test('should have correct configuration', () => {
expect(editFileTool.id).toBe('edit-file');
expect(editFileTool.description).toBe(
'Perform exact string replacements in files with occurrence validation'
);
expect(editFileTool.inputSchema).toBeDefined();
expect(editFileTool.outputSchema).toBeDefined();
expect(editFileTool.execute).toBeDefined();
});
test('should validate input schema', () => {
const validInput = {
file_path: '/absolute/path/to/file.txt',
old_string: 'old text',
new_string: 'new text',
};
const result = editFileTool.inputSchema.safeParse(validInput);
expect(result.success).toBe(true);
});
test('should validate output schema structure', () => {
const validOutput = {
success: true,
replacements_made: 2,
file_path: '/path/to/file.txt',
backup_path: '/path/to/file.txt.backup.2024-01-01T00-00-00-000Z',
line_changes: [
{
line_number: 1,
old_line: 'old line',
new_line: 'new line',
},
],
};
const result = editFileTool.outputSchema.safeParse(validOutput);
expect(result.success).toBe(true);
});
test('should perform simple string replacement', async () => {
writeFileSync(testFile, 'Hello world\nGoodbye world\nHello again');
const result = await editFileTool.execute({
context: {
file_path: testFile,
old_string: 'world',
new_string: 'universe',
create_backup: false,
},
});
expect(result.success).toBe(true);
expect(result.replacements_made).toBe(2);
expect(result.line_changes).toHaveLength(2);
expect(result.line_changes[0].line_number).toBe(1);
expect(result.line_changes[0].old_line).toBe('Hello world');
expect(result.line_changes[0].new_line).toBe('Hello universe');
expect(result.line_changes[1].line_number).toBe(2);
expect(result.line_changes[1].old_line).toBe('Goodbye world');
expect(result.line_changes[1].new_line).toBe('Goodbye universe');
const updatedContent = readFileSync(testFile, 'utf8');
expect(updatedContent).toBe('Hello universe\nGoodbye universe\nHello again');
});
test('should create backup when enabled', async () => {
writeFileSync(testFile, 'original content');
const result = await editFileTool.execute({
context: {
file_path: testFile,
old_string: 'original',
new_string: 'modified',
create_backup: true,
},
});
expect(result.success).toBe(true);
expect(result.backup_path).toBeDefined();
expect(existsSync(result.backup_path!)).toBe(true);
const backupContent = readFileSync(result.backup_path!, 'utf8');
expect(backupContent).toBe('original content');
});
test('should validate expected occurrences', async () => {
writeFileSync(testFile, 'test test test');
const result = await editFileTool.execute({
context: {
file_path: testFile,
old_string: 'test',
new_string: 'exam',
expected_occurrences: 3,
create_backup: false,
},
});
expect(result.success).toBe(true);
expect(result.replacements_made).toBe(3);
});
test('should fail when expected occurrences mismatch', async () => {
writeFileSync(testFile, 'test test');
await expect(
editFileTool.execute({
context: {
file_path: testFile,
old_string: 'test',
new_string: 'exam',
expected_occurrences: 3,
create_backup: false,
},
})
).rejects.toThrow('Expected 3 occurrences but found 2');
});
test('should fail when string not found', async () => {
writeFileSync(testFile, 'Hello world');
await expect(
editFileTool.execute({
context: {
file_path: testFile,
old_string: 'missing',
new_string: 'found',
create_backup: false,
},
})
).rejects.toThrow('String not found in file: "missing"');
});
test('should fail when old_string equals new_string', async () => {
writeFileSync(testFile, 'Hello world');
await expect(
editFileTool.execute({
context: {
file_path: testFile,
old_string: 'same',
new_string: 'same',
create_backup: false,
},
})
).rejects.toThrow('old_string and new_string cannot be the same');
});
test('should fail when old_string is empty', async () => {
writeFileSync(testFile, 'Hello world');
await expect(
editFileTool.execute({
context: {
file_path: testFile,
old_string: '',
new_string: 'something',
create_backup: false,
},
})
).rejects.toThrow('old_string cannot be empty');
});
test('should handle non-absolute paths', async () => {
await expect(
editFileTool.execute({
context: {
file_path: 'relative/path.txt',
old_string: 'old',
new_string: 'new',
create_backup: false,
},
})
).rejects.toThrow('File path must be absolute');
});
test('should handle non-existent files', async () => {
const nonExistentFile = join(tempDir, 'nonexistent.txt');
await expect(
editFileTool.execute({
context: {
file_path: nonExistentFile,
old_string: 'old',
new_string: 'new',
create_backup: false,
},
})
).rejects.toThrow(`File not found: ${nonExistentFile}`);
});
test('should handle path traversal attempts', async () => {
await expect(
editFileTool.execute({
context: {
file_path: '/tmp/../etc/passwd',
old_string: 'old',
new_string: 'new',
create_backup: false,
},
})
).rejects.toThrow('Path traversal not allowed');
});
test('should handle access to sensitive directories', async () => {
await expect(
editFileTool.execute({
context: {
file_path: '/etc/passwd',
old_string: 'old',
new_string: 'new',
create_backup: false,
},
})
).rejects.toThrow('Access denied to path');
});
test('should preserve line endings when enabled', async () => {
const contentWithCrlf = 'line1\r\nline2\r\nline3';
writeFileSync(testFile, contentWithCrlf);
await editFileTool.execute({
context: {
file_path: testFile,
old_string: 'line2',
new_string: 'modified',
create_backup: false,
preserve_line_endings: true,
},
});
const updatedContent = readFileSync(testFile, 'utf8');
expect(updatedContent).toBe('line1\r\nmodified\r\nline3');
});
test('should handle complex replacements with regex characters', async () => {
writeFileSync(testFile, 'Price: $10.99\nDiscount: $5.00');
const result = await editFileTool.execute({
context: {
file_path: testFile,
old_string: '$10.99',
new_string: '$15.99',
create_backup: false,
},
});
expect(result.success).toBe(true);
expect(result.replacements_made).toBe(1);
const updatedContent = readFileSync(testFile, 'utf8');
expect(updatedContent).toBe('Price: $15.99\nDiscount: $5.00');
});
test('should handle multiline replacements', async () => {
writeFileSync(testFile, 'Line 1\nLine 2\nLine 3\nLine 2 again');
const result = await editFileTool.execute({
context: {
file_path: testFile,
old_string: 'Line 2',
new_string: 'Modified Line',
create_backup: false,
},
});
expect(result.success).toBe(true);
expect(result.replacements_made).toBe(2);
expect(result.line_changes).toHaveLength(2);
const updatedContent = readFileSync(testFile, 'utf8');
expect(updatedContent).toBe('Line 1\nModified Line\nLine 3\nModified Line again');
});
});