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

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);
});
});