mirror of https://github.com/buster-so/buster.git
301 lines
8.5 KiB
TypeScript
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');
|
||
|
});
|
||
|
});
|