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

211 lines
6.8 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 { lsTool } from '../../../src/tools/file-tools/ls-tool';
describe('LS Tool Unit Tests', () => {
let tempDir: string;
beforeEach(() => {
// Create temporary directory with test files
tempDir = join(tmpdir(), `ls-test-${Date.now()}`);
mkdirSync(tempDir, { recursive: true });
// Create test files and directories
writeFileSync(join(tempDir, 'file1.txt'), 'content1');
writeFileSync(join(tempDir, 'file2.md'), 'content2');
writeFileSync(join(tempDir, '.hidden.txt'), 'hidden content');
mkdirSync(join(tempDir, 'subdir'));
writeFileSync(join(tempDir, 'subdir', 'nested.txt'), 'nested content');
});
afterEach(() => {
// Clean up temporary directory
try {
rmSync(tempDir, { recursive: true, force: true });
} catch {
// Ignore cleanup errors
}
});
test('should have correct configuration', () => {
expect(lsTool.id).toBe('ls-directory');
expect(lsTool.description).toBe('List directory contents with detailed file information');
expect(lsTool.inputSchema).toBeDefined();
expect(lsTool.outputSchema).toBeDefined();
expect(lsTool.execute).toBeDefined();
});
test('should validate input schema', () => {
const validInput = {
path: '/absolute/path',
all: true,
sort_by: 'name' as const,
};
const result = lsTool.inputSchema.safeParse(validInput);
expect(result.success).toBe(true);
});
test('should validate output schema structure', () => {
const validOutput = {
path: '/path/to/directory',
total_items: 2,
items: [
{
name: 'file.txt',
type: 'file' as const,
size: 100,
size_formatted: '100B',
modified: '2023-01-01T00:00:00.000Z',
permissions: 'rw-r--r--',
is_hidden: false,
},
],
};
const result = lsTool.outputSchema.safeParse(validOutput);
expect(result.success).toBe(true);
});
test('should list directory contents successfully', async () => {
const result = await lsTool.execute({ path: tempDir });
expect(result.path).toBe(tempDir);
expect(result.total_items).toBeGreaterThan(0);
expect(result.items).toBeInstanceOf(Array);
// Should find visible files but not hidden by default
const fileNames = result.items.map((item) => item.name);
expect(fileNames).toContain('file1.txt');
expect(fileNames).toContain('file2.md');
expect(fileNames).toContain('subdir');
expect(fileNames).not.toContain('.hidden.txt');
});
test('should include hidden files when all=true', async () => {
const result = await lsTool.execute({
context: { path: tempDir, all: true },
});
const fileNames = result.items.map((item) => item.name);
expect(fileNames).toContain('.hidden.txt');
const hiddenFile = result.items.find((item) => item.name === '.hidden.txt');
expect(hiddenFile?.is_hidden).toBe(true);
});
test('should sort by name correctly', async () => {
const result = await lsTool.execute({
context: { path: tempDir, sort_by: 'name' },
});
const fileNames = result.items.map((item) => item.name);
const sortedNames = [...fileNames].sort();
expect(fileNames).toEqual(sortedNames);
});
test('should reverse sort when reverse=true', async () => {
const result = await lsTool.execute({
context: { path: tempDir, sort_by: 'name', reverse: true },
});
const fileNames = result.items.map((item) => item.name);
const reverseSortedNames = [...fileNames].sort().reverse();
expect(fileNames).toEqual(reverseSortedNames);
});
test('should filter files by pattern', async () => {
const result = await lsTool.execute({
context: { path: tempDir, filter: '*.txt' },
});
const fileNames = result.items.map((item) => item.name);
expect(fileNames).toContain('file1.txt');
expect(fileNames).not.toContain('file2.md');
expect(fileNames).not.toContain('subdir');
});
test('should identify file types correctly', async () => {
const result = await lsTool.execute({
path: tempDir,
});
const file = result.items.find((item) => item.name === 'file1.txt');
const directory = result.items.find((item) => item.name === 'subdir');
expect(file?.type).toBe('file');
expect(directory?.type).toBe('directory');
});
test('should format file sizes in human readable format', async () => {
const result = await lsTool.execute({
path: tempDir,
human_readable: true,
});
const file = result.items.find((item) => item.name === 'file1.txt');
expect(file?.size_formatted).toMatch(/\d+B/); // Should end with B for bytes
});
test('should provide raw file sizes when human_readable=false', async () => {
const result = await lsTool.execute({
context: { path: tempDir, human_readable: false },
});
const file = result.items.find((item) => item.name === 'file1.txt');
expect(file?.size_formatted).toMatch(/^\d+$/); // Should be just numbers
});
test('should reject non-existent directories', async () => {
await expect(
lsTool.execute({
context: { path: join(tempDir, 'nonexistent') },
})
).rejects.toThrow('Path not found');
});
test('should reject files instead of directories', async () => {
await expect(
lsTool.execute({
context: { path: join(tempDir, 'file1.txt') },
})
).rejects.toThrow('Not a directory');
});
test('should handle path traversal attempts', async () => {
// On macOS, /etc might be accessible, so we test that either:
// 1. It's blocked with an error, OR
// 2. It resolves to the actual directory
try {
const result = await lsTool.execute({
context: { path: '/tmp/../etc' },
});
// If successful, it should resolve to /etc
expect(result.path).toBe('/etc');
expect(result.total_items).toBeGreaterThan(0);
} catch (error) {
// If it throws, it should be about path traversal
expect(error.message).toContain('Path traversal not allowed');
}
});
test('should format permissions correctly', async () => {
const result = await lsTool.execute({
path: tempDir,
});
const file = result.items.find((item) => item.name === 'file1.txt');
expect(file?.permissions).toMatch(/^[rwx-]{9}$/); // Should be 9 characters of rwx or -
});
test('should include modification timestamps', async () => {
const result = await lsTool.execute({
path: tempDir,
});
const file = result.items.find((item) => item.name === 'file1.txt');
expect(file?.modified).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/); // ISO format
});
});