Skip to content

Commit 8173922

Browse files
committed
test: add tests for codebase map generation and adapter
Add comprehensive test suites: - 17 tests for generateCodebaseMap and formatCodebaseMap in core - 22 tests for MapAdapter in mcp-server Tests cover: - Map structure and component counting - Depth limiting and focus filtering - Export extraction and limiting - Token budget and truncation - Validation of parameters - Output formatting Part of #81
1 parent bb8511c commit 8173922

File tree

2 files changed

+625
-0
lines changed

2 files changed

+625
-0
lines changed
Lines changed: 329 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,329 @@
1+
/**
2+
* Tests for Codebase Map Generation
3+
*/
4+
5+
import { describe, expect, it, vi } from 'vitest';
6+
import type { RepositoryIndexer } from '../../indexer';
7+
import type { SearchResult } from '../../vector/types';
8+
import { formatCodebaseMap, generateCodebaseMap } from '../index';
9+
10+
describe('Codebase Map', () => {
11+
// Mock search results representing indexed documents
12+
const mockSearchResults: SearchResult[] = [
13+
{
14+
id: 'packages/core/src/scanner/typescript.ts:TypeScriptScanner:19',
15+
score: 0.9,
16+
metadata: {
17+
path: 'packages/core/src/scanner/typescript.ts',
18+
type: 'class',
19+
name: 'TypeScriptScanner',
20+
startLine: 19,
21+
endLine: 100,
22+
language: 'typescript',
23+
exported: true,
24+
},
25+
},
26+
{
27+
id: 'packages/core/src/scanner/typescript.ts:scan:45',
28+
score: 0.85,
29+
metadata: {
30+
path: 'packages/core/src/scanner/typescript.ts',
31+
type: 'method',
32+
name: 'scan',
33+
startLine: 45,
34+
endLine: 70,
35+
language: 'typescript',
36+
exported: true,
37+
},
38+
},
39+
{
40+
id: 'packages/core/src/indexer/index.ts:RepositoryIndexer:10',
41+
score: 0.8,
42+
metadata: {
43+
path: 'packages/core/src/indexer/index.ts',
44+
type: 'class',
45+
name: 'RepositoryIndexer',
46+
startLine: 10,
47+
endLine: 200,
48+
language: 'typescript',
49+
exported: true,
50+
},
51+
},
52+
{
53+
id: 'packages/mcp-server/src/adapters/search-adapter.ts:SearchAdapter:35',
54+
score: 0.75,
55+
metadata: {
56+
path: 'packages/mcp-server/src/adapters/search-adapter.ts',
57+
type: 'class',
58+
name: 'SearchAdapter',
59+
startLine: 35,
60+
endLine: 150,
61+
language: 'typescript',
62+
exported: true,
63+
},
64+
},
65+
{
66+
id: 'packages/cli/src/cli.ts:main:5',
67+
score: 0.7,
68+
metadata: {
69+
path: 'packages/cli/src/cli.ts',
70+
type: 'function',
71+
name: 'main',
72+
startLine: 5,
73+
endLine: 50,
74+
language: 'typescript',
75+
exported: true,
76+
},
77+
},
78+
{
79+
id: 'packages/core/src/utils/helpers.ts:privateHelper:10',
80+
score: 0.65,
81+
metadata: {
82+
path: 'packages/core/src/utils/helpers.ts',
83+
type: 'function',
84+
name: 'privateHelper',
85+
startLine: 10,
86+
endLine: 20,
87+
language: 'typescript',
88+
exported: false, // Not exported
89+
},
90+
},
91+
];
92+
93+
// Create mock indexer
94+
function createMockIndexer(results: SearchResult[] = mockSearchResults): RepositoryIndexer {
95+
return {
96+
search: vi.fn().mockResolvedValue(results),
97+
} as unknown as RepositoryIndexer;
98+
}
99+
100+
describe('generateCodebaseMap', () => {
101+
it('should generate a map with correct structure', async () => {
102+
const indexer = createMockIndexer();
103+
const map = await generateCodebaseMap(indexer);
104+
105+
expect(map.root).toBeDefined();
106+
expect(map.root.name).toBe('root');
107+
expect(map.totalComponents).toBeGreaterThan(0);
108+
expect(map.totalDirectories).toBeGreaterThan(0);
109+
expect(map.generatedAt).toBeDefined();
110+
});
111+
112+
it('should count components correctly', async () => {
113+
const indexer = createMockIndexer();
114+
const map = await generateCodebaseMap(indexer);
115+
116+
// Should have all mock results counted (root includes all children)
117+
expect(map.totalComponents).toBeGreaterThanOrEqual(6);
118+
});
119+
120+
it('should build directory hierarchy', async () => {
121+
const indexer = createMockIndexer();
122+
const map = await generateCodebaseMap(indexer, { depth: 3 });
123+
124+
// Should have packages as a child of root
125+
const packagesNode = map.root.children.find((c) => c.name === 'packages');
126+
expect(packagesNode).toBeDefined();
127+
expect(packagesNode?.children.length).toBeGreaterThan(0);
128+
});
129+
130+
it('should respect depth limit', async () => {
131+
const indexer = createMockIndexer();
132+
const map = await generateCodebaseMap(indexer, { depth: 1 });
133+
134+
// At depth 1, should only have immediate children
135+
const packagesNode = map.root.children.find((c) => c.name === 'packages');
136+
expect(packagesNode?.children.length).toBe(0); // Pruned at depth 1
137+
});
138+
139+
it('should filter by focus directory', async () => {
140+
const indexer = createMockIndexer();
141+
const fullMap = await generateCodebaseMap(indexer);
142+
const focusedMap = await generateCodebaseMap(indexer, { focus: 'packages/core' });
143+
144+
// Focused map should have fewer components than full map
145+
expect(focusedMap.totalComponents).toBeLessThan(fullMap.totalComponents);
146+
147+
// Root should contain core-related content
148+
expect(focusedMap.totalComponents).toBeGreaterThan(0);
149+
});
150+
151+
it('should extract exports when includeExports is true', async () => {
152+
const indexer = createMockIndexer();
153+
const map = await generateCodebaseMap(indexer, { depth: 5, includeExports: true });
154+
155+
// Find a node with exports
156+
const findNodeWithExports = (node: typeof map.root): typeof map.root | null => {
157+
if (node.exports && node.exports.length > 0) return node;
158+
for (const child of node.children) {
159+
const found = findNodeWithExports(child);
160+
if (found) return found;
161+
}
162+
return null;
163+
};
164+
165+
const nodeWithExports = findNodeWithExports(map.root);
166+
expect(nodeWithExports).not.toBeNull();
167+
expect(nodeWithExports?.exports?.[0].name).toBeDefined();
168+
});
169+
170+
it('should not include exports when includeExports is false', async () => {
171+
const indexer = createMockIndexer();
172+
const map = await generateCodebaseMap(indexer, { depth: 5, includeExports: false });
173+
174+
// Check that no node has exports
175+
const hasExports = (node: typeof map.root): boolean => {
176+
if (node.exports && node.exports.length > 0) return true;
177+
return node.children.some(hasExports);
178+
};
179+
180+
expect(hasExports(map.root)).toBe(false);
181+
});
182+
183+
it('should limit exports per directory', async () => {
184+
// Create results with many exports in one directory
185+
const manyExports: SearchResult[] = Array.from({ length: 20 }, (_, i) => ({
186+
id: `packages/core/src/index.ts:export${i}:${i * 10}`,
187+
score: 0.9 - i * 0.01,
188+
metadata: {
189+
path: 'packages/core/src/index.ts',
190+
type: 'function',
191+
name: `export${i}`,
192+
startLine: i * 10,
193+
endLine: i * 10 + 5,
194+
language: 'typescript',
195+
exported: true,
196+
},
197+
}));
198+
199+
const indexer = createMockIndexer(manyExports);
200+
const map = await generateCodebaseMap(indexer, {
201+
depth: 5,
202+
includeExports: true,
203+
maxExportsPerDir: 5,
204+
});
205+
206+
// Find the src node
207+
const findNode = (node: typeof map.root, name: string): typeof map.root | null => {
208+
if (node.name === name) return node;
209+
for (const child of node.children) {
210+
const found = findNode(child, name);
211+
if (found) return found;
212+
}
213+
return null;
214+
};
215+
216+
const srcNode = findNode(map.root, 'src');
217+
expect(srcNode?.exports?.length).toBeLessThanOrEqual(5);
218+
});
219+
220+
it('should sort children alphabetically', async () => {
221+
const indexer = createMockIndexer();
222+
const map = await generateCodebaseMap(indexer, { depth: 3 });
223+
224+
const packagesNode = map.root.children.find((c) => c.name === 'packages');
225+
if (packagesNode && packagesNode.children.length > 1) {
226+
const names = packagesNode.children.map((c) => c.name);
227+
const sorted = [...names].sort();
228+
expect(names).toEqual(sorted);
229+
}
230+
});
231+
});
232+
233+
describe('formatCodebaseMap', () => {
234+
it('should format map as readable text', async () => {
235+
const indexer = createMockIndexer();
236+
const map = await generateCodebaseMap(indexer);
237+
const output = formatCodebaseMap(map);
238+
239+
expect(output).toContain('# Codebase Map');
240+
expect(output).toContain('components');
241+
expect(output).toContain('directories');
242+
});
243+
244+
it('should include tree structure with connectors', async () => {
245+
const indexer = createMockIndexer();
246+
const map = await generateCodebaseMap(indexer, { depth: 2 });
247+
const output = formatCodebaseMap(map);
248+
249+
// Should have tree connectors
250+
expect(output).toMatch(/[]/);
251+
expect(output).toMatch(//);
252+
});
253+
254+
it('should show exports when includeExports is true', async () => {
255+
const indexer = createMockIndexer();
256+
const map = await generateCodebaseMap(indexer, { depth: 5, includeExports: true });
257+
const output = formatCodebaseMap(map, { includeExports: true });
258+
259+
expect(output).toContain('exports:');
260+
});
261+
262+
it('should show component counts', async () => {
263+
const indexer = createMockIndexer();
264+
const map = await generateCodebaseMap(indexer);
265+
const output = formatCodebaseMap(map);
266+
267+
expect(output).toMatch(/\d+ components/);
268+
});
269+
270+
it('should show total summary', async () => {
271+
const indexer = createMockIndexer();
272+
const map = await generateCodebaseMap(indexer);
273+
const output = formatCodebaseMap(map);
274+
275+
expect(output).toContain('**Total:**');
276+
expect(output).toContain('indexed components');
277+
});
278+
});
279+
280+
describe('Edge Cases', () => {
281+
it('should handle empty results', async () => {
282+
const indexer = createMockIndexer([]);
283+
const map = await generateCodebaseMap(indexer);
284+
285+
expect(map.totalComponents).toBe(0);
286+
expect(map.root.children.length).toBe(0);
287+
});
288+
289+
it('should handle results with missing path', async () => {
290+
const resultsWithMissingPath: SearchResult[] = [
291+
{
292+
id: 'test:1',
293+
score: 0.9,
294+
metadata: {
295+
type: 'function',
296+
name: 'test',
297+
// No path field
298+
},
299+
},
300+
];
301+
302+
const indexer = createMockIndexer(resultsWithMissingPath);
303+
const map = await generateCodebaseMap(indexer);
304+
305+
// Should not crash, just skip the result
306+
expect(map.totalComponents).toBe(0);
307+
});
308+
309+
it('should handle deeply nested directories', async () => {
310+
const deepResults: SearchResult[] = [
311+
{
312+
id: 'a/b/c/d/e/f/g/file.ts:fn:1',
313+
score: 0.9,
314+
metadata: {
315+
path: 'a/b/c/d/e/f/g/file.ts',
316+
type: 'function',
317+
name: 'fn',
318+
exported: true,
319+
},
320+
},
321+
];
322+
323+
const indexer = createMockIndexer(deepResults);
324+
const map = await generateCodebaseMap(indexer, { depth: 10 });
325+
326+
expect(map.totalComponents).toBe(1);
327+
});
328+
});
329+
});

0 commit comments

Comments
 (0)