mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-16 20:53:57 +08:00
4297b8e2ed
* docs: record regression audit and validation * test: cover audited call and extraction regressions * test: cover audited calls and selector guards * test: cover audited calls and selector guards * test: cover audited calls and selector guards * test: cover audited calls and selector guards * test: cover audited calls and selector guards * fix: retain qualified call sites and restore language-specific getters * fix: resolve typed calls and bound store actions * fix: retain qualified call sites in the native kernel * docs: explain restored call and Steps coverage * test: isolate watchdog and cover imported store aliases * refactor: expose import lookup through resolver context * refactor: remove import resolver cycle * test: canonicalize Claude config temp paths * docs: record review fixes and large TypeScript timings * test: cover store cache edits and document verification * perf: cache per-file store eligibility * fix: observe large benchmarks without a wall timeout * docs: explain completed indexing benchmarks docs: explain completed indexing benchmarks
103 lines
4.4 KiB
TypeScript
103 lines
4.4 KiB
TypeScript
/**
|
|
* The MCP `codegraph_callers` / `codegraph_callees` answers say when their
|
|
* `limit` cut the list (#1639, #1674). A capped list with no marker reads as
|
|
* the complete set, and an agent under-counts "who calls this" from it.
|
|
*/
|
|
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import * as os from 'os';
|
|
import { CodeGraph } from '../src';
|
|
import { ToolHandler } from '../src/mcp/tools';
|
|
import { initGrammars, loadAllGrammars } from '../src/extraction/grammars';
|
|
|
|
let tmpDir: string;
|
|
let cg: CodeGraph;
|
|
let handler: ToolHandler;
|
|
|
|
const text = async (tool: string, args: Record<string, unknown>): Promise<string> => {
|
|
const res = await handler.execute(tool, args);
|
|
return res.content?.[0]?.text ?? '';
|
|
};
|
|
|
|
const CALLERS = 25;
|
|
|
|
beforeAll(async () => {
|
|
await initGrammars();
|
|
await loadAllGrammars();
|
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-1674-'));
|
|
fs.mkdirSync(path.join(tmpDir, 'src'));
|
|
// `warm` lives in a file of another name: one definition, the flat list.
|
|
fs.writeFileSync(path.join(tmpDir, 'src', 'target.ts'), 'export function warm(n: number): number { return n; }\n');
|
|
fs.writeFileSync(
|
|
path.join(tmpDir, 'src', 'callers.ts'),
|
|
"import { warm } from './target';\n" +
|
|
Array.from({ length: CALLERS }, (_, i) => `export function caller${i}(): number { return warm(${i}); }`).join('\n') +
|
|
'\n'
|
|
);
|
|
// Two real `hot` functions exercise per-definition truncation. A filename
|
|
// is not an overload of its exact-named function (#1809).
|
|
fs.writeFileSync(path.join(tmpDir, 'src', 'hot.ts'), 'export function hot(n: number): number { return n; }\n');
|
|
fs.writeFileSync(
|
|
path.join(tmpDir, 'src', 'hot-callers.ts'),
|
|
"import { hot } from './hot';\n" +
|
|
Array.from({ length: CALLERS }, (_, i) => `export function hotCaller${i}(): number { return hot(${i}); }`).join('\n') +
|
|
'\n'
|
|
);
|
|
fs.writeFileSync(path.join(tmpDir, 'src', 'other-hot.ts'), 'export function hot(n: number): number { return n + 1; }\n');
|
|
fs.writeFileSync(path.join(tmpDir, 'src', 'other-hot-callers.ts'),
|
|
"import { hot } from './other-hot';\n" +
|
|
Array.from({ length: CALLERS }, (_, i) => `export function otherHotCaller${i}(): number { return hot(${i}); }`).join('\n') + '\n');
|
|
fs.writeFileSync(
|
|
path.join(tmpDir, 'src', 'fan.ts'),
|
|
Array.from({ length: CALLERS }, (_, i) => `export function helper${i}(): number { return ${i}; }`).join('\n') +
|
|
`\nexport function fanout(): number { return ${Array.from({ length: CALLERS }, (_, i) => `helper${i}()`).join(' + ')}; }\n`
|
|
);
|
|
cg = CodeGraph.initSync(tmpDir);
|
|
await cg.indexAll();
|
|
handler = new ToolHandler(cg);
|
|
});
|
|
|
|
afterAll(() => {
|
|
cg.destroy();
|
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
describe('codegraph_callers truncation', () => {
|
|
it('says how many callers the default limit hid', async () => {
|
|
const out = await text('codegraph_callers', { symbol: 'warm' });
|
|
// The importing file counts as a caller too, so the total is at least CALLERS.
|
|
const m = out.match(/Showing 20 of (\d+) callers; pass `limit`/);
|
|
expect(m).not.toBeNull();
|
|
expect(Number(m![1])).toBeGreaterThanOrEqual(CALLERS);
|
|
expect(out.match(/^- caller\d+ /gm)?.length).toBe(20);
|
|
});
|
|
|
|
it('is silent when the list is complete', async () => {
|
|
const out = await text('codegraph_callers', { symbol: 'warm', limit: 100 });
|
|
expect(out).not.toContain('Showing');
|
|
expect(out.match(/^- caller\d+ /gm)?.length).toBe(CALLERS);
|
|
});
|
|
|
|
it('marks the cut inside each per-definition section too', async () => {
|
|
const out = await text('codegraph_callers', { symbol: 'hot' });
|
|
expect(out).toContain('2 distinct definitions');
|
|
expect(out.match(/- … \+\d+ more \(pass `limit` to widen\)/g)).toHaveLength(2);
|
|
expect(await text('codegraph_callers', { symbol: 'hot', limit: 100 })).not.toContain('more (pass');
|
|
});
|
|
});
|
|
|
|
describe('codegraph_callees truncation', () => {
|
|
it('says how many callees the default limit hid', async () => {
|
|
const out = await text('codegraph_callees', { symbol: 'fanout' });
|
|
const m = out.match(/Showing 20 of (\d+) callees; pass `limit`/);
|
|
expect(m).not.toBeNull();
|
|
expect(Number(m![1])).toBe(CALLERS);
|
|
});
|
|
|
|
it('is silent when the list is complete', async () => {
|
|
const out = await text('codegraph_callees', { symbol: 'fanout', limit: 100 });
|
|
expect(out).not.toContain('Showing');
|
|
});
|
|
});
|