Skip to content
Open
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
103 changes: 103 additions & 0 deletions __tests__/context.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ describe('Context Builder', () => {
// Create a sample codebase
const srcDir = path.join(testDir, 'src');
fs.mkdirSync(srcDir);
const evalDir = path.join(testDir, '__tests__', 'evaluation');
fs.mkdirSync(evalDir, { recursive: true });

// Create a payment service file
fs.writeFileSync(
Expand Down Expand Up @@ -135,6 +137,46 @@ export function validateEmail(email: string): boolean {
`
);

fs.writeFileSync(
path.join(srcDir, 'affected.ts'),
`
export function getFileDependents(filePath: string): string[] {
return [filePath];
}

export function affected(filePath: string): string[] {
return getFileDependents(filePath);
}
`
);

fs.writeFileSync(
path.join(srcDir, 'cache.ts'),
`
export class LRUCache {
private values = new Map<string, string>();

get(key: string): string | undefined {
return this.values.get(key);
}

getDb(): Map<string, string> {
return this.values;
}
}
`
);

fs.writeFileSync(
path.join(evalDir, 'test-cases.ts'),
`
export const testCases = [
{ id: 'search-class-exact', query: 'PaymentService' },
{ id: 'context-affected', query: 'affected tests' },
];
`
);

// Initialize CodeGraph
cg = CodeGraph.initSync(testDir, {
config: {
Expand Down Expand Up @@ -194,6 +236,22 @@ export function validateEmail(email: string): boolean {
).toBe(true);
});

it('should avoid generic split-term entry points when an exact compound symbol matches', async () => {
const result = await cg.findRelevantContext('getFileDependents affected tests', {
searchLimit: 3,
maxNodes: 12,
});

const entryNames = result.roots
.map((id) => result.nodes.get(id)?.name)
.filter(Boolean);

expect(entryNames).toContain('getFileDependents');
expect(entryNames).not.toContain('get');
expect(entryNames).not.toContain('getDb');
expect(entryNames).not.toContain('testCases');
});

it('should include edges in the result', async () => {
const result = await cg.findRelevantContext('checkout', {
traversalDepth: 2,
Expand Down Expand Up @@ -267,6 +325,51 @@ export function validateEmail(email: string): boolean {
expect(markdown).toContain('```typescript');
});

it('should avoid related class code blocks for method-focused context', async () => {
const result = await cg.buildContext('processCheckout', {
format: 'json',
includeCode: true,
maxCodeBlocks: 10,
traversalDepth: 2,
});

const parsed = JSON.parse(result as string);
const codeBlocks = parsed.codeBlocks as Array<{
nodeName: string;
nodeKind: string;
filePath: string;
startLine: number;
}>;
const entryKeys = new Set(
parsed.entryPoints.map((node: { name: string; filePath: string; startLine: number }) =>
`${node.name}:${node.filePath}:${node.startLine}`
)
);
const relatedClassBlocks = codeBlocks.filter((block) =>
block.nodeKind === 'class' &&
!entryKeys.has(`${block.nodeName}:${block.filePath}:${block.startLine}`)
);

expect(codeBlocks.some((block) => block.nodeName === 'processCheckout')).toBe(true);
expect(relatedClassBlocks).toHaveLength(0);
});

it('should include a compact snippet when a focus term shares a file with the symbol', async () => {
const result = await cg.buildContext('getFileDependents affected tests', {
format: 'markdown',
includeCode: true,
maxCodeBlocks: 2,
});

const markdown = result as string;

expect(markdown).toContain('#### getFileDependents');
expect(markdown).toContain('#### Snippet');
expect(markdown).toContain('function affected');
expect(markdown).toContain('return getFileDependents(filePath)');
expect(markdown).not.toContain('class LRUCache');
});

it('should exclude code blocks when requested', async () => {
const result = await cg.buildContext('payment', {
format: 'markdown',
Expand Down
35 changes: 35 additions & 0 deletions __tests__/graph.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,17 @@ export { main };
`
);

fs.writeFileSync(
path.join(srcDir, 'utils.test.ts'),
`
import { processValue } from './utils';

export function testProcessValue(): boolean {
return processValue(2) === 2;
}
`
);

// Initialize and index
cg = CodeGraph.initSync(testDir, {
config: {
Expand Down Expand Up @@ -281,6 +292,19 @@ export { main };
expect(Array.isArray(callers)).toBe(true);
});

it('should get instantiating callers of a class', () => {
const nodes = cg.getNodesByKind('class');
const derivedClass = nodes.find((n) => n.name === 'DerivedClass');

if (!derivedClass) {
return;
}

const callers = cg.getCallers(derivedClass.id);

expect(callers.some((c) => c.node.name === 'main' && c.edge.kind === 'instantiates')).toBe(true);
});

it('should get callees of a function', () => {
const nodes = cg.getNodesByKind('function');
const processValue = nodes.find((n) => n.name === 'processValue');
Expand Down Expand Up @@ -379,12 +403,23 @@ export { main };
const deps = cg.getFileDependencies('src/main.ts');

expect(Array.isArray(deps)).toBe(true);
expect(deps).toContain('src/derived.ts');
expect(deps).toContain('src/utils.ts');
});

it('should get file dependents', () => {
const dependents = cg.getFileDependents('src/utils.ts');

expect(Array.isArray(dependents)).toBe(true);
expect(dependents).toContain('src/main.ts');
expect(dependents).toContain('src/utils.test.ts');
});

it('should normalize Windows-style file paths for dependents', () => {
const dependents = cg.getFileDependents('src\\utils.ts');

expect(dependents).toContain('src/main.ts');
expect(dependents).toContain('src/utils.test.ts');
});
});

Expand Down
7 changes: 5 additions & 2 deletions src/bin/codegraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import * as fs from 'fs';
import { getCodeGraphDir, isInitialized } from '../directory';
import { createShimmerProgress } from '../ui/shimmer-progress';
import { getGlyphs } from '../ui/glyphs';
import { normalizePath } from '../utils';

import { buildNode25BlockBanner, buildNodeTooOldBanner, MIN_NODE_MAJOR } from './node-version-check';
import { relaunchWithWasmRuntimeFlagsIfNeeded } from '../extraction/wasm-runtime-flags';
Expand Down Expand Up @@ -1085,7 +1086,7 @@ program
.description('Build context for a task (outputs markdown)')
.option('-p, --path <path>', 'Project path')
.option('-n, --max-nodes <number>', 'Maximum nodes to include', '50')
.option('-c, --max-code <number>', 'Maximum code blocks', '10')
.option('-c, --max-code <number>', 'Maximum code blocks', '2')
.option('--no-code', 'Exclude code blocks')
.option('-f, --format <format>', 'Output format (markdown, json)', 'markdown')
.action(async (task: string, options: {
Expand All @@ -1108,7 +1109,7 @@ program

const context = await cg.buildContext(task, {
maxNodes: parseInt(options.maxNodes || '50', 10),
maxCodeBlocks: parseInt(options.maxCode || '10', 10),
maxCodeBlocks: parseInt(options.maxCode || '2', 10),
includeCode: options.code !== false,
format: options.format as 'markdown' | 'json',
});
Expand Down Expand Up @@ -1505,6 +1506,8 @@ program
changedFiles.push(...stdinFiles);
}

changedFiles = changedFiles.map(normalizePath);

if (changedFiles.length === 0) {
if (!options.quiet) info('No files provided. Use file arguments or --stdin.');
process.exit(0);
Expand Down
2 changes: 1 addition & 1 deletion src/context/formatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export function formatContextAsMarkdown(context: TaskContext): string {
if (context.codeBlocks.length > 0) {
lines.push('### Code\n');
for (const block of context.codeBlocks) {
const nodeName = block.node?.name ?? 'Unknown';
const nodeName = block.node?.name ?? 'Snippet';
lines.push(`#### ${nodeName} (${block.filePath}:${block.startLine})\n`);
lines.push('```' + block.language);
lines.push(block.content);
Expand Down
Loading