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