Skip to main content
Glama
search-codebase-tool.test.ts14.3 kB
/** * Tests for Search Codebase Tool * * Tests the atomic search_codebase function with dependency injection. * No complex ESM mocking required - uses simple dependency injection. */ import { describe, it, expect, _jest, beforeEach } from 'vitest'; import { searchCodebase, searchCodebaseTool, type SearchCodebaseDependencies, } from '../../src/tools/search-codebase-tool.js'; // Mock dependencies with proper typing const createMockDeps = (): SearchCodebaseDependencies => { const mockReadFile = vi.fn(); return { fs: { readFile: mockReadFile, } satisfies Partial<SearchCodebaseDependencies['fs']>, } satisfies SearchCodebaseDependencies; }; describe('searchCodebase', () => { let mockDeps: SearchCodebaseDependencies; beforeEach(() => { mockDeps = createMockDeps(); }); describe('input validation', () => { it('should throw error when query is empty', async () => { await expect(searchCodebase({ query: '' }, mockDeps)).rejects.toThrow( 'Search query is required' ); }); it('should throw error when query is only whitespace', async () => { await expect(searchCodebase({ query: ' ' }, mockDeps)).rejects.toThrow( 'Search query is required' ); }); it('should accept valid query', async () => { const result = await searchCodebase( { query: 'Docker configuration', projectPath: process.cwd(), }, mockDeps ); expect(result).toBeDefined(); expect(result.keywords).toBeDefined(); expect(Array.isArray(result.matches)).toBe(true); }); }); describe('keyword extraction', () => { it('should extract keywords from query', async () => { const result = await searchCodebase( { query: 'How does the Docker configuration work?', projectPath: process.cwd(), }, mockDeps ); expect(result.keywords).toContain('docker'); expect(result.keywords).toContain('configuration'); expect(result.keywords).toContain('work'); // Stop words should be excluded expect(result.keywords).not.toContain('the'); expect(result.keywords).not.toContain('how'); expect(result.keywords).not.toContain('does'); }); it('should handle special characters in query', async () => { const result = await searchCodebase( { query: 'kubernetes deployment & CI/CD pipeline!', projectPath: process.cwd(), }, mockDeps ); // Keywords can be either individual words or compound words (e.g., "kubernetes-deployment") const hasKubernetes = result.keywords.some(k => k.includes('kubernetes')); const hasDeployment = result.keywords.some(k => k.includes('deployment')); const hasPipeline = result.keywords.some(k => k.includes('pipeline')); expect(hasKubernetes).toBe(true); expect(hasDeployment).toBe(true); expect(hasPipeline).toBe(true); }); it('should remove duplicate keywords', async () => { const result = await searchCodebase( { query: 'Docker docker DOCKER configuration', projectPath: process.cwd(), }, mockDeps ); const dockerCount = result.keywords.filter(k => k === 'docker').length; expect(dockerCount).toBe(1); }); }); describe('file discovery', () => { it('should discover files based on query intent', async () => { const result = await searchCodebase( { query: 'Docker configuration', projectPath: process.cwd(), }, mockDeps ); expect(result.totalFiles).toBeGreaterThanOrEqual(0); expect(result.matches).toBeDefined(); }); it('should respect maxFiles limit', async () => { const result = await searchCodebase( { query: 'TypeScript files', projectPath: process.cwd(), maxFiles: 5, }, mockDeps ); expect(result.matches.length).toBeLessThanOrEqual(5); }); it('should handle custom scope patterns', async () => { const result = await searchCodebase( { query: 'test files', projectPath: process.cwd(), scope: ['tests/**/*.ts', 'src/**/*.test.ts'], maxFiles: 10, }, mockDeps ); expect(result.totalFiles).toBeGreaterThanOrEqual(0); }); }); describe('relevance scoring', () => { it('should return matches with relevance scores', async () => { const result = await searchCodebase( { query: 'Jest testing framework', projectPath: process.cwd(), }, mockDeps ); if (result.matches.length > 0) { result.matches.forEach(match => { expect(match.relevance).toBeGreaterThanOrEqual(0); expect(match.relevance).toBeLessThanOrEqual(1); }); } }); it('should sort matches by relevance descending', async () => { const result = await searchCodebase( { query: 'TypeScript configuration', projectPath: process.cwd(), }, mockDeps ); if (result.matches.length > 1) { for (let i = 0; i < result.matches.length - 1; i++) { expect(result.matches[i]!.relevance).toBeGreaterThanOrEqual( result.matches[i + 1]!.relevance ); } } }); it('should filter out low relevance matches', async () => { const result = await searchCodebase( { query: 'quantum computing blockchain AI', projectPath: process.cwd(), }, mockDeps ); // All returned matches should have relevance > 0.2 result.matches.forEach(match => { expect(match.relevance).toBeGreaterThan(0.2); }); }); }); describe('content inclusion', () => { it('should not include content by default', async () => { const result = await searchCodebase( { query: 'package.json', projectPath: process.cwd(), }, mockDeps ); if (result.matches.length > 0) { expect(result.matches[0]!.content).toBeUndefined(); } }); it('should include content when requested', async () => { const result = await searchCodebase( { query: 'package.json', projectPath: process.cwd(), includeContent: true, }, mockDeps ); if (result.matches.length > 0) { const packageJsonMatch = result.matches.find(m => m.path.includes('package.json')); if (packageJsonMatch) { expect(packageJsonMatch.content).toBeDefined(); expect(typeof packageJsonMatch.content).toBe('string'); } } }); }); describe('tree-sitter analysis', () => { it('should work without tree-sitter when disabled', async () => { const result = await searchCodebase( { query: 'TypeScript files', projectPath: process.cwd(), enableTreeSitter: false, }, mockDeps ); expect(result.matches).toBeDefined(); // Parse analysis should not be present result.matches.forEach(match => { expect(match.parseAnalysis).toBeUndefined(); }); }); it('should include parse analysis when tree-sitter is enabled', async () => { const result = await searchCodebase( { query: 'TypeScript configuration', projectPath: process.cwd(), enableTreeSitter: true, maxFiles: 5, }, mockDeps ); if (result.matches.length > 0) { const tsFile = result.matches.find(m => m.path.endsWith('.ts')); if (tsFile && tsFile.parseAnalysis) { expect(tsFile.parseAnalysis.language).toBeDefined(); expect(typeof tsFile.parseAnalysis.functionCount).toBe('number'); expect(typeof tsFile.parseAnalysis.importCount).toBe('number'); } } }); }); describe('result structure', () => { it('should return complete CodebaseSearchResult structure', async () => { const result = await searchCodebase( { query: 'test query', projectPath: process.cwd(), }, mockDeps ); expect(result).toHaveProperty('matches'); expect(result).toHaveProperty('totalFiles'); expect(result).toHaveProperty('keywords'); expect(result).toHaveProperty('projectPath'); expect(result).toHaveProperty('duration'); expect(Array.isArray(result.matches)).toBe(true); expect(typeof result.totalFiles).toBe('number'); expect(Array.isArray(result.keywords)).toBe(true); expect(typeof result.projectPath).toBe('string'); expect(typeof result.duration).toBe('number'); expect(result.duration).toBeGreaterThan(0); }); it('should return FileMatch objects with correct structure', async () => { const result = await searchCodebase( { query: 'package.json', projectPath: process.cwd(), }, mockDeps ); if (result.matches.length > 0) { const match = result.matches[0]!; expect(match).toHaveProperty('path'); expect(match).toHaveProperty('relevance'); expect(typeof match.path).toBe('string'); expect(typeof match.relevance).toBe('number'); } }); }); describe('intent-based discovery', () => { it('should detect Docker-related queries', async () => { const result = await searchCodebase( { query: 'Docker container configuration', projectPath: process.cwd(), }, mockDeps ); // Should search for docker-related files expect(result.keywords).toContain('docker'); expect(result.totalFiles).toBeGreaterThanOrEqual(0); }); it('should detect Kubernetes-related queries', async () => { const result = await searchCodebase( { query: 'Kubernetes deployment and pods', projectPath: process.cwd(), }, mockDeps ); expect(result.keywords).toContain('kubernetes'); expect(result.keywords).toContain('deployment'); }); it('should detect test-related queries', async () => { const result = await searchCodebase( { query: 'testing framework and test files', projectPath: process.cwd(), }, mockDeps ); expect(result.keywords).toContain('testing'); expect(result.keywords).toContain('test'); expect(result.keywords).toContain('files'); }); it('should detect build/CI queries', async () => { const result = await searchCodebase( { query: 'CI/CD pipeline and build configuration', projectPath: process.cwd(), }, mockDeps ); expect(result.keywords).toContain('pipeline'); expect(result.keywords).toContain('build'); expect(result.keywords).toContain('configuration'); }); }); describe('error handling', () => { it('should handle non-existent project path gracefully', async () => { const result = await searchCodebase( { query: 'test query', projectPath: '/non/existent/path', }, mockDeps ); // Should return empty results for non-existent path expect(result.matches).toEqual([]); expect(result.totalFiles).toBe(0); }); it('should continue on file read errors', async () => { const failingDeps: SearchCodebaseDependencies = { fs: { readFile: vi.fn().mockRejectedValue(new Error('File read error')), } as any, }; const result = await searchCodebase( { query: 'test query', projectPath: process.cwd(), }, failingDeps ); // Should still return a valid result even if file reads fail expect(result).toBeDefined(); expect(result.matches).toBeDefined(); }); }); }); describe('searchCodebaseTool', () => { describe('MCP tool wrapper', () => { it('should return CallToolResult structure', async () => { const result = await searchCodebaseTool({ query: 'package.json', projectPath: process.cwd(), }); expect(result).toHaveProperty('content'); expect(Array.isArray(result.content)).toBe(true); expect(result.content[0]).toHaveProperty('type'); expect(result.content[0]).toHaveProperty('text'); expect(result.content[0]!.type).toBe('text'); }); it('should format output with search results', async () => { const result = await searchCodebaseTool({ query: 'TypeScript configuration', projectPath: process.cwd(), maxFiles: 3, }); const output = result.content[0]!.text; expect(output).toContain('Codebase Search Results'); expect(output).toContain('Query'); expect(output).toContain('Matches'); expect(output).toContain('Duration'); expect(output).toContain('Keywords'); }); it('should handle errors gracefully', async () => { const result = await searchCodebaseTool({ query: '', projectPath: process.cwd(), }); expect(result.isError).toBe(true); expect(result.content[0]!.text).toContain('Search failed'); }); it('should show "No files found" when no matches', async () => { const result = await searchCodebaseTool({ query: 'xyzabc123nonexistentquery999', projectPath: process.cwd(), }); if (!result.isError) { const output = result.content[0]!.text; expect(output).toContain('No files found matching the query'); } }); it('should include content preview when requested', async () => { const result = await searchCodebaseTool({ query: 'package.json', projectPath: process.cwd(), includeContent: true, maxFiles: 1, }); if (!result.isError) { const output = result.content[0]!.text; if (output.includes('package.json')) { expect(output).toContain('Content Preview'); } } }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/tosin2013/mcp-adr-analysis-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server