mirror of https://github.com/buster-so/buster.git
204 lines
6.0 KiB
TypeScript
204 lines
6.0 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 { writeFileTool } from '../../../src/tools/file-tools/write-file-tool';
|
||
|
|
||
|
describe('Write File Tool Unit Tests', () => {
|
||
|
let tempDir: string;
|
||
|
let testFile: string;
|
||
|
let existingFile: string;
|
||
|
|
||
|
beforeEach(() => {
|
||
|
// Create temporary directory for tests
|
||
|
tempDir = join(tmpdir(), `write-file-test-${Date.now()}`);
|
||
|
mkdirSync(tempDir, { recursive: true });
|
||
|
testFile = join(tempDir, 'test.txt');
|
||
|
existingFile = join(tempDir, 'existing.txt');
|
||
|
|
||
|
// Create an existing file
|
||
|
writeFileSync(existingFile, 'Original content');
|
||
|
});
|
||
|
|
||
|
afterEach(() => {
|
||
|
// Clean up temporary directory
|
||
|
try {
|
||
|
rmSync(tempDir, { recursive: true, force: true });
|
||
|
} catch {
|
||
|
// Ignore cleanup errors
|
||
|
}
|
||
|
});
|
||
|
|
||
|
test('should have correct configuration', () => {
|
||
|
expect(writeFileTool.id).toBe('write-file');
|
||
|
expect(writeFileTool.description).toBe(
|
||
|
'Write content to a file with atomic operations and safety checks'
|
||
|
);
|
||
|
expect(writeFileTool.inputSchema).toBeDefined();
|
||
|
expect(writeFileTool.outputSchema).toBeDefined();
|
||
|
expect(writeFileTool.execute).toBeDefined();
|
||
|
});
|
||
|
|
||
|
test('should validate input schema', () => {
|
||
|
const validInput = {
|
||
|
file_path: '/absolute/path/to/file.txt',
|
||
|
content: 'Hello, world!',
|
||
|
overwrite: true,
|
||
|
};
|
||
|
const result = writeFileTool.inputSchema.safeParse(validInput);
|
||
|
expect(result.success).toBe(true);
|
||
|
});
|
||
|
|
||
|
test('should validate output schema structure', () => {
|
||
|
const validOutput = {
|
||
|
success: true,
|
||
|
file_path: '/path/to/file.txt',
|
||
|
bytes_written: 100,
|
||
|
backup_path: '/path/to/file.txt.backup.2023-01-01T00-00-00-000Z',
|
||
|
created_directories: ['/path', '/path/to'],
|
||
|
};
|
||
|
|
||
|
const result = writeFileTool.outputSchema.safeParse(validOutput);
|
||
|
expect(result.success).toBe(true);
|
||
|
});
|
||
|
|
||
|
test('should write new file successfully', async () => {
|
||
|
const content = 'Hello, world!';
|
||
|
const result = await writeFileTool.execute({
|
||
|
context: {
|
||
|
file_path: testFile,
|
||
|
content,
|
||
|
overwrite: false,
|
||
|
},
|
||
|
});
|
||
|
|
||
|
expect(result.success).toBe(true);
|
||
|
expect(result.file_path).toBe(testFile);
|
||
|
expect(result.bytes_written).toBeGreaterThan(0);
|
||
|
expect(result.backup_path).toBeUndefined();
|
||
|
expect(existsSync(testFile)).toBe(true);
|
||
|
expect(readFileSync(testFile, 'utf-8')).toBe(content);
|
||
|
});
|
||
|
|
||
|
test('should overwrite existing file with backup', async () => {
|
||
|
const newContent = 'New content';
|
||
|
const result = await writeFileTool.execute({
|
||
|
context: {
|
||
|
file_path: existingFile,
|
||
|
content: newContent,
|
||
|
overwrite: true,
|
||
|
create_backup: true,
|
||
|
},
|
||
|
});
|
||
|
|
||
|
expect(result.success).toBe(true);
|
||
|
expect(result.backup_path).toBeDefined();
|
||
|
if (result.backup_path) {
|
||
|
expect(existsSync(result.backup_path)).toBe(true);
|
||
|
expect(readFileSync(result.backup_path, 'utf-8')).toBe('Original content');
|
||
|
}
|
||
|
expect(readFileSync(existingFile, 'utf-8')).toBe(newContent);
|
||
|
});
|
||
|
|
||
|
test('should create directories if they do not exist', async () => {
|
||
|
const nestedFile = join(tempDir, 'nested', 'deep', 'file.txt');
|
||
|
const result = await writeFileTool.execute({
|
||
|
context: {
|
||
|
file_path: nestedFile,
|
||
|
content: 'Nested content',
|
||
|
overwrite: false,
|
||
|
create_backup: true,
|
||
|
encoding: 'utf8',
|
||
|
},
|
||
|
});
|
||
|
|
||
|
expect(result.success).toBe(true);
|
||
|
expect(result.created_directories.length).toBeGreaterThan(0);
|
||
|
expect(existsSync(nestedFile)).toBe(true);
|
||
|
expect(readFileSync(nestedFile, 'utf-8')).toBe('Nested content');
|
||
|
});
|
||
|
|
||
|
test('should reject overwrite when overwrite=false and file exists', async () => {
|
||
|
await expect(
|
||
|
writeFileTool.execute({
|
||
|
context: {
|
||
|
file_path: existingFile,
|
||
|
content: 'New content',
|
||
|
overwrite: false,
|
||
|
},
|
||
|
})
|
||
|
).rejects.toThrow('File already exists');
|
||
|
});
|
||
|
|
||
|
test('should reject non-absolute paths', async () => {
|
||
|
await expect(
|
||
|
writeFileTool.execute({
|
||
|
context: {
|
||
|
file_path: 'relative/path.txt',
|
||
|
content: 'content',
|
||
|
},
|
||
|
})
|
||
|
).rejects.toThrow('File path must be absolute');
|
||
|
});
|
||
|
|
||
|
test('should reject path traversal attempts', async () => {
|
||
|
await expect(
|
||
|
writeFileTool.execute({
|
||
|
context: {
|
||
|
file_path: '/tmp/../etc/passwd',
|
||
|
content: 'malicious content',
|
||
|
overwrite: false,
|
||
|
create_backup: true,
|
||
|
encoding: 'utf8',
|
||
|
},
|
||
|
})
|
||
|
).rejects.toThrow(/Write access denied to system directory|Path traversal not allowed/);
|
||
|
});
|
||
|
|
||
|
test('should reject writes to system directories', async () => {
|
||
|
await expect(
|
||
|
writeFileTool.execute({
|
||
|
context: {
|
||
|
file_path: '/etc/malicious.txt',
|
||
|
content: 'malicious content',
|
||
|
overwrite: false,
|
||
|
create_backup: true,
|
||
|
encoding: 'utf8',
|
||
|
},
|
||
|
})
|
||
|
).rejects.toThrow('Write access denied to system directory');
|
||
|
});
|
||
|
|
||
|
test('should support different encodings', async () => {
|
||
|
const content = 'ASCII content';
|
||
|
const result = await writeFileTool.execute({
|
||
|
context: {
|
||
|
file_path: testFile,
|
||
|
content,
|
||
|
overwrite: false,
|
||
|
create_backup: true,
|
||
|
encoding: 'ascii',
|
||
|
},
|
||
|
});
|
||
|
|
||
|
expect(result.success).toBe(true);
|
||
|
expect(readFileSync(testFile, 'ascii')).toBe(content);
|
||
|
});
|
||
|
|
||
|
test('should handle write without backup when file exists', async () => {
|
||
|
const newContent = 'No backup content';
|
||
|
const result = await writeFileTool.execute({
|
||
|
context: {
|
||
|
file_path: existingFile,
|
||
|
content: newContent,
|
||
|
overwrite: true,
|
||
|
create_backup: false,
|
||
|
},
|
||
|
});
|
||
|
|
||
|
expect(result.success).toBe(true);
|
||
|
expect(result.backup_path).toBeUndefined();
|
||
|
expect(readFileSync(existingFile, 'utf-8')).toBe(newContent);
|
||
|
});
|
||
|
});
|