Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
331 changes: 331 additions & 0 deletions packages/core/src/map/__tests__/map.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ describe('Codebase Map', () => {
path: 'packages/cli/src/cli.ts',
type: 'function',
name: 'main',
signature: 'function main(args: string[]): Promise<void>',
startLine: 5,
endLine: 50,
language: 'typescript',
Expand Down Expand Up @@ -167,6 +168,30 @@ describe('Codebase Map', () => {
expect(nodeWithExports?.exports?.[0].name).toBeDefined();
});

it('should include signatures in exports when available', async () => {
const indexer = createMockIndexer();
const map = await generateCodebaseMap(indexer, { depth: 5, includeExports: true });

// Find any node with an export that has a signature
const findExportWithSignature = (
node: typeof map.root
): { name: string; signature?: string } | null => {
if (node.exports) {
const withSig = node.exports.find((e) => e.signature);
if (withSig) return withSig;
}
for (const child of node.children) {
const found = findExportWithSignature(child);
if (found) return found;
}
return null;
};

const exportWithSig = findExportWithSignature(map.root);
expect(exportWithSig).not.toBeNull();
expect(exportWithSig?.signature).toBe('function main(args: string[]): Promise<void>');
});

it('should not include exports when includeExports is false', async () => {
const indexer = createMockIndexer();
const map = await generateCodebaseMap(indexer, { depth: 5, includeExports: false });
Expand Down Expand Up @@ -259,6 +284,41 @@ describe('Codebase Map', () => {
expect(output).toContain('exports:');
});

it('should show signatures in exports when available', async () => {
const indexer = createMockIndexer();
const map = await generateCodebaseMap(indexer, { depth: 5, includeExports: true });
const output = formatCodebaseMap(map, { includeExports: true });

// The main function has a signature, should appear in output
expect(output).toContain('function main(args: string[]): Promise<void>');
});

it('should truncate long signatures', async () => {
const longSigResults: SearchResult[] = [
{
id: 'src/index.ts:longFunction:1',
score: 0.9,
metadata: {
path: 'src/index.ts',
type: 'function',
name: 'longFunction',
signature:
'function longFunction(param1: string, param2: number, param3: boolean, param4: object): Promise<ComplexReturnType>',
exported: true,
},
},
];

const indexer = createMockIndexer(longSigResults);
const map = await generateCodebaseMap(indexer, { depth: 5, includeExports: true });
const output = formatCodebaseMap(map, { includeExports: true });

// Should be truncated with ...
expect(output).toContain('...');
// Should not contain the full signature
expect(output).not.toContain('ComplexReturnType');
});

it('should show component counts', async () => {
const indexer = createMockIndexer();
const map = await generateCodebaseMap(indexer);
Expand All @@ -277,6 +337,277 @@ describe('Codebase Map', () => {
});
});

describe('Hot Paths', () => {
it('should compute hot paths from callers data', async () => {
const resultsWithCallers: SearchResult[] = [
{
id: 'src/core.ts:coreFunction:1',
score: 0.9,
metadata: {
path: 'src/core.ts',
type: 'function',
name: 'coreFunction',
exported: true,
callers: [
{ name: 'caller1', file: 'src/a.ts', startLine: 10 },
{ name: 'caller2', file: 'src/b.ts', startLine: 20 },
{ name: 'caller3', file: 'src/c.ts', startLine: 30 },
],
},
},
{
id: 'src/utils.ts:utilFunction:1',
score: 0.8,
metadata: {
path: 'src/utils.ts',
type: 'function',
name: 'utilFunction',
exported: true,
callers: [{ name: 'caller1', file: 'src/a.ts', startLine: 15 }],
},
},
];

const indexer = createMockIndexer(resultsWithCallers);
const map = await generateCodebaseMap(indexer, { includeHotPaths: true });

expect(map.hotPaths.length).toBeGreaterThan(0);
// coreFunction should be first (more callers)
expect(map.hotPaths[0].file).toBe('src/core.ts');
expect(map.hotPaths[0].incomingRefs).toBe(3);
});

it('should compute hot paths from callees data', async () => {
const resultsWithCallees: SearchResult[] = [
{
id: 'src/main.ts:main:1',
score: 0.9,
metadata: {
path: 'src/main.ts',
type: 'function',
name: 'main',
exported: true,
callees: [
{ name: 'helper', file: 'src/helpers.ts', line: 10 },
{ name: 'helper', file: 'src/helpers.ts', line: 10 },
],
},
},
{
id: 'src/other.ts:other:1',
score: 0.8,
metadata: {
path: 'src/other.ts',
type: 'function',
name: 'other',
exported: true,
callees: [{ name: 'helper', file: 'src/helpers.ts', line: 10 }],
},
},
];

const indexer = createMockIndexer(resultsWithCallees);
const map = await generateCodebaseMap(indexer, { includeHotPaths: true });

expect(map.hotPaths.length).toBeGreaterThan(0);
// helpers.ts should be referenced most
expect(map.hotPaths[0].file).toBe('src/helpers.ts');
expect(map.hotPaths[0].incomingRefs).toBe(3);
});

it('should limit hot paths to maxHotPaths', async () => {
const manyRefs: SearchResult[] = Array.from({ length: 20 }, (_, i) => ({
id: `src/file${i}.ts:fn:1`,
score: 0.9,
metadata: {
path: `src/file${i}.ts`,
type: 'function',
name: `fn${i}`,
exported: true,
callers: Array.from({ length: 20 - i }, (_, j) => ({
name: `caller${j}`,
file: `src/other${j}.ts`,
startLine: j * 10,
})),
},
}));

const indexer = createMockIndexer(manyRefs);
const map = await generateCodebaseMap(indexer, { includeHotPaths: true, maxHotPaths: 3 });

expect(map.hotPaths.length).toBe(3);
// Should be sorted by refs descending
expect(map.hotPaths[0].incomingRefs).toBeGreaterThanOrEqual(map.hotPaths[1].incomingRefs);
});

it('should not include hot paths when disabled', async () => {
const resultsWithCallers: SearchResult[] = [
{
id: 'src/core.ts:coreFunction:1',
score: 0.9,
metadata: {
path: 'src/core.ts',
type: 'function',
name: 'coreFunction',
exported: true,
callers: [{ name: 'caller1', file: 'src/a.ts', startLine: 10 }],
},
},
];

const indexer = createMockIndexer(resultsWithCallers);
const map = await generateCodebaseMap(indexer, { includeHotPaths: false });

expect(map.hotPaths.length).toBe(0);
});

it('should format hot paths in output', async () => {
const resultsWithCallers: SearchResult[] = [
{
id: 'src/core.ts:coreFunction:1',
score: 0.9,
metadata: {
path: 'src/core.ts',
type: 'function',
name: 'coreFunction',
exported: true,
callers: [
{ name: 'caller1', file: 'src/a.ts', startLine: 10 },
{ name: 'caller2', file: 'src/b.ts', startLine: 20 },
],
},
},
];

const indexer = createMockIndexer(resultsWithCallers);
const map = await generateCodebaseMap(indexer, { includeHotPaths: true });
const output = formatCodebaseMap(map, { includeHotPaths: true });

expect(output).toContain('## Hot Paths');
expect(output).toContain('src/core.ts');
expect(output).toContain('2 refs');
});
});

describe('Smart Depth', () => {
it('should expand dense directories when smartDepth is enabled', async () => {
// Create a structure with varying density
const mixedDensity: SearchResult[] = [
// Dense directory - 15 components
...Array.from({ length: 15 }, (_, i) => ({
id: `packages/core/src/dense/file${i}.ts:fn:1`,
score: 0.9,
metadata: {
path: `packages/core/src/dense/file${i}.ts`,
type: 'function',
name: `fn${i}`,
exported: true,
},
})),
// Sparse directory - 2 components
...Array.from({ length: 2 }, (_, i) => ({
id: `packages/core/src/sparse/file${i}.ts:fn:1`,
score: 0.9,
metadata: {
path: `packages/core/src/sparse/file${i}.ts`,
type: 'function',
name: `fn${i}`,
exported: true,
},
})),
];

const indexer = createMockIndexer(mixedDensity);
const map = await generateCodebaseMap(indexer, {
depth: 5,
smartDepth: true,
smartDepthThreshold: 10,
});

// Find the core node
const findNode = (node: typeof map.root, name: string): typeof map.root | null => {
if (node.name === name) return node;
for (const child of node.children) {
const found = findNode(child, name);
if (found) return found;
}
return null;
};

const srcNode = findNode(map.root, 'src');
expect(srcNode).not.toBeNull();

// Dense should be expanded (has children or is at leaf level)
const denseNode = srcNode?.children.find((c) => c.name === 'dense');
expect(denseNode).toBeDefined();
expect(denseNode?.componentCount).toBe(15);

// Sparse should also exist but may be collapsed
const sparseNode = srcNode?.children.find((c) => c.name === 'sparse');
expect(sparseNode).toBeDefined();
expect(sparseNode?.componentCount).toBe(2);
});

it('should always expand first 2 levels regardless of density', async () => {
const sparseResults: SearchResult[] = [
{
id: 'packages/tiny/src/file.ts:fn:1',
score: 0.9,
metadata: {
path: 'packages/tiny/src/file.ts',
type: 'function',
name: 'fn',
exported: true,
},
},
];

const indexer = createMockIndexer(sparseResults);
const map = await generateCodebaseMap(indexer, {
depth: 5,
smartDepth: true,
smartDepthThreshold: 100, // Very high threshold
});

// Should still show packages and tiny (first 2 levels)
const packagesNode = map.root.children.find((c) => c.name === 'packages');
expect(packagesNode).toBeDefined();
expect(packagesNode?.children.length).toBeGreaterThan(0);
});

it('should not use smart depth when disabled', async () => {
const results: SearchResult[] = Array.from({ length: 5 }, (_, i) => ({
id: `a/b/c/d/e/file${i}.ts:fn:1`,
score: 0.9,
metadata: {
path: `a/b/c/d/e/file${i}.ts`,
type: 'function',
name: `fn${i}`,
exported: true,
},
}));

const indexer = createMockIndexer(results);
const mapWithSmart = await generateCodebaseMap(indexer, {
depth: 3,
smartDepth: true,
smartDepthThreshold: 1,
});
const mapWithoutSmart = await generateCodebaseMap(indexer, {
depth: 3,
smartDepth: false,
});

// Without smart depth, should strictly follow depth limit
const countDepth = (node: typeof mapWithSmart.root, d = 0): number => {
if (node.children.length === 0) return d;
return Math.max(...node.children.map((c) => countDepth(c, d + 1)));
};

expect(countDepth(mapWithoutSmart.root)).toBeLessThanOrEqual(3);
});
});

describe('Edge Cases', () => {
it('should handle empty results', async () => {
const indexer = createMockIndexer([]);
Expand Down
Loading