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

327 lines
9.7 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 { globTool, multiGlobTool } from '../../../src/tools/file-tools/glob-tool';
describe('Glob Tool Unit Tests', () => {
let tempDir: string;
let sourceDir: string;
let testsDir: string;
beforeEach(() => {
// Create temporary directory structure for tests
tempDir = join(tmpdir(), `glob-test-${Date.now()}`);
sourceDir = join(tempDir, 'src');
testsDir = join(tempDir, 'tests');
mkdirSync(tempDir, { recursive: true });
mkdirSync(sourceDir, { recursive: true });
mkdirSync(testsDir, { recursive: true });
mkdirSync(join(sourceDir, 'components'), { recursive: true });
mkdirSync(join(sourceDir, 'utils'), { recursive: true });
// Create test files
writeFileSync(join(sourceDir, 'app.ts'), 'export default app;');
writeFileSync(join(sourceDir, 'index.js'), 'console.log("hello");');
writeFileSync(join(sourceDir, 'components', 'Button.tsx'), 'export const Button = () => {};');
writeFileSync(join(sourceDir, 'components', 'Modal.tsx'), 'export const Modal = () => {};');
writeFileSync(join(sourceDir, 'utils', 'helpers.ts'), 'export const help = () => {};');
writeFileSync(join(testsDir, 'app.test.ts'), 'test("app", () => {});');
writeFileSync(join(tempDir, 'README.md'), '# Project');
writeFileSync(join(tempDir, 'package.json'), '{}');
});
afterEach(() => {
// Clean up temporary directory
try {
rmSync(tempDir, { recursive: true, force: true });
} catch {
// Ignore cleanup errors
}
});
test('should have correct configuration', () => {
expect(globTool.id).toBe('glob-search');
expect(globTool.description).toBe('Find files matching glob patterns with advanced filtering');
expect(globTool.inputSchema).toBeDefined();
expect(globTool.outputSchema).toBeDefined();
expect(globTool.execute).toBeDefined();
});
test('should validate input schema', () => {
const validInput = {
pattern: '**/*.ts',
cwd: '/absolute/path',
};
const result = globTool.inputSchema.safeParse(validInput);
expect(result.success).toBe(true);
});
test('should validate output schema structure', () => {
const validOutput = {
pattern: '**/*.ts',
matches: ['/path/to/file.ts'],
count: 1,
truncated: false,
search_time_ms: 100,
};
const result = globTool.outputSchema.safeParse(validOutput);
expect(result.success).toBe(true);
});
test('should find TypeScript files with basic pattern', async () => {
const result = await globTool.execute({
context: {
pattern: '**/*.ts',
cwd: tempDir,
ignore: [],
},
});
expect(result.count).toBeGreaterThan(0);
expect(result.matches).toContain('src/app.ts');
expect(result.matches).toContain('src/utils/helpers.ts');
expect(result.matches).toContain('tests/app.test.ts');
expect(result.truncated).toBe(false);
expect(result.search_time_ms).toBeGreaterThan(0);
});
test('should find TypeScript React files with specific pattern', async () => {
const result = await globTool.execute({
context: {
pattern: '**/*.tsx',
cwd: tempDir,
},
});
expect(result.count).toBe(2);
expect(result.matches).toContain('src/components/Button.tsx');
expect(result.matches).toContain('src/components/Modal.tsx');
});
test('should find files in specific directory', async () => {
const result = await globTool.execute({
context: {
pattern: 'src/**/*.ts',
cwd: tempDir,
},
});
expect(result.matches).toContain('src/app.ts');
expect(result.matches).toContain('src/utils/helpers.ts');
expect(result.matches).not.toContain('tests/app.test.ts');
});
test('should handle absolute paths when requested', async () => {
const result = await globTool.execute({
context: {
pattern: '*.md',
cwd: tempDir,
absolute: true,
},
});
expect(result.count).toBe(1);
expect(result.matches[0]).toEqual(join(tempDir, 'README.md'));
});
test('should apply ignore patterns correctly', async () => {
// Create node_modules directory with files
const nodeModulesDir = join(tempDir, 'node_modules');
mkdirSync(nodeModulesDir, { recursive: true });
writeFileSync(join(nodeModulesDir, 'package.ts'), 'ignored file');
const result = await globTool.execute({
context: {
pattern: '**/*.ts',
cwd: tempDir,
ignore: ['**/node_modules/**'],
},
});
expect(result.matches).not.toContain('node_modules/package.ts');
expect(result.matches).toContain('src/app.ts');
});
test('should limit results when specified', async () => {
const result = await globTool.execute({
context: {
pattern: '**/*',
cwd: tempDir,
limit: 3,
},
});
expect(result.count).toBe(3);
expect(result.truncated).toBe(true);
expect(result.matches).toHaveLength(3);
});
test('should handle only_directories option', async () => {
const result = await globTool.execute({
context: {
pattern: '**/*',
cwd: tempDir,
only_files: false,
only_directories: true,
},
});
expect(result.matches).toContain('src');
expect(result.matches).toContain('tests');
expect(result.matches).toContain('src/components');
expect(result.matches).toContain('src/utils');
expect(result.matches).not.toContain('README.md');
expect(result.matches).not.toContain('package.json');
});
test('should handle max_depth option', async () => {
const result = await globTool.execute({
context: {
pattern: '**/*',
cwd: tempDir,
max_depth: 1,
},
});
expect(result.matches).toContain('README.md');
expect(result.matches).toContain('package.json');
expect(result.matches).not.toContain('src/app.ts');
expect(result.matches).not.toContain('src/components/Button.tsx');
});
test('should handle empty pattern error', async () => {
await expect(
globTool.execute({
context: {
pattern: '',
cwd: tempDir,
},
})
).rejects.toThrow('Pattern cannot be empty');
});
test('should handle invalid patterns gracefully', async () => {
await expect(
globTool.execute({
context: {
pattern: '[invalid',
cwd: tempDir,
},
})
).rejects.toThrow();
});
test('should handle non-absolute cwd paths', async () => {
await expect(
globTool.execute({
context: {
pattern: '*.ts',
cwd: 'relative/path',
},
})
).rejects.toThrow('Path must be absolute');
});
test('should handle path traversal attempts', async () => {
await expect(
globTool.execute({
context: {
pattern: '*.ts',
cwd: '/tmp/../etc',
},
})
).rejects.toThrow('Path traversal not allowed');
});
test('should handle access to sensitive directories', async () => {
await expect(
globTool.execute({
context: {
pattern: '*.conf',
cwd: '/etc',
},
})
).rejects.toThrow('Access denied to path');
});
describe('Multi-Glob Tool', () => {
test('should have correct configuration', () => {
expect(multiGlobTool.id).toBe('multi-glob-search');
expect(multiGlobTool.description).toBe('Search with multiple glob patterns simultaneously');
expect(multiGlobTool.inputSchema).toBeDefined();
expect(multiGlobTool.outputSchema).toBeDefined();
expect(multiGlobTool.execute).toBeDefined();
});
test('should search with multiple patterns', async () => {
const result = await multiGlobTool.execute({
context: {
patterns: ['**/*.ts', '**/*.tsx', '*.md'],
cwd: tempDir,
},
});
expect(result.total_matches).toBeGreaterThan(0);
// Find files that match multiple patterns
const tsxFiles = result.matches.filter((m: { path: string }) => m.path.endsWith('.tsx'));
expect(tsxFiles).toHaveLength(2);
// Find README.md
const readmeFiles = result.matches.filter((m: { path: string }) =>
m.path.endsWith('README.md')
);
expect(readmeFiles).toHaveLength(1);
expect(result.search_time_ms).toBeGreaterThan(0);
});
test('should track which patterns matched each file', async () => {
const result = await multiGlobTool.execute({
context: {
patterns: ['**/*.ts', 'src/**/*'],
cwd: tempDir,
},
});
// Find a TypeScript file in src directory
const srcTsFile = result.matches.find(
(m: { path: string }) => m.path.includes('src') && m.path.endsWith('.ts')
);
expect(srcTsFile).toBeDefined();
expect(srcTsFile!.matched_patterns).toContain('**/*.ts');
expect(srcTsFile!.matched_patterns).toContain('src/**/*');
});
test('should validate multi-glob input schema', () => {
const validInput = {
patterns: ['**/*.ts', '**/*.js'],
cwd: '/absolute/path',
};
const result = multiGlobTool.inputSchema.safeParse(validInput);
expect(result.success).toBe(true);
});
test('should validate multi-glob output schema', () => {
const validOutput = {
patterns: ['**/*.ts'],
matches: [
{
path: '/path/to/file.ts',
matched_patterns: ['**/*.ts'],
},
],
total_matches: 1,
search_time_ms: 100,
};
const result = multiGlobTool.outputSchema.safeParse(validOutput);
expect(result.success).toBe(true);
});
});
});