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