Files
codegraph/__tests__/explore-path-pinning.test.ts
T
Colby McHenry 238dbc5cec fix(explore): accurately resolve query file paths and find camelCase symbols
Previously, `codegraph_explore` queries explicitly naming files by path (e.g., `src/routes/m/projects/[id]/runs/[runId]/+page.svelte`) were shredded. Bracketed path segments exploded into "named symbol" seeds, and FTS on fragments like `page` or `runs` admitted every sibling file, starving the user's intended target.

This change introduces:
- **Query path pinning:** File paths named in a query are now resolved against the index, "pinned," and stripped from the query. Pinned files are guaranteed inclusion, top ranking, and fair allocation. Unresolvable path-like spans are reported.
- **Segment vocabulary supplement:** Natural language query terms (e.g., "auto-scroll to bottom") can now reach camelCase identifiers (e.g., `pinFeedIfNearBottom`, `feedAtBottom`) by matching against their constituent segments.
- **Variable seeding:** `variable` and `constant` node kinds are now included in identifier seeding, improving recall for `$state`-style variables common in frameworks like Svelte.
2026-08-20 09:10:07 -07:00

106 lines
4.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* End-to-end gate for query-path pinning + the segment-vocab supplement +
* variable seeding, on the bug that motivated all three: an agent named a
* SvelteKit route file by exact path plus behavior words ("scrollToBottom,
* onscroll, atBottom tracking") and got back neither the file's scroll code
* nor the file itself at full weight — the bracketed path was tokenizer
* shrapnel (`runId` seeded as a named symbol, every sibling `+page` admitted)
* and the camelCase scroll symbols were FTS-opaque.
*
* The fixture mirrors that shape in plain TS (bracket/paren directories are
* the crux, not the language): a target file under
* `src/routes/m/projects/[id]/runs/[runId]/` holding `feedAtBottom` /
* `handleFeedScroll` / `pinFeedIfNearBottom`, a decoy chat-window page under
* a `(protected)` route group, and a runs-store decoy defining `runId` and
* `Scope` — the two symbols that headlined the original junk blast radius.
*/
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/index';
import { ToolHandler } from '../src/mcp/tools';
const FIXTURE = 'explore-path-pinning';
const TARGET = 'src/routes/m/projects/[id]/runs/[runId]/+page.ts';
const DECOY_CHAT = 'src/routes/(protected)/chat-window/+page.ts';
let dir: string;
let cg: CodeGraph;
async function explore(query: string): Promise<string> {
const res = await new ToolHandler(cg).execute('codegraph_explore', { query });
return res.content?.[0]?.text ?? '';
}
/** The response renders a source section for `file`. */
const hasSection = (response: string, file: string): boolean =>
response.includes('**`' + file + '`');
beforeAll(async () => {
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-path-pin-'));
fs.cpSync(path.join(__dirname, 'fixtures', FIXTURE), dir, { recursive: true });
fs.rmSync(path.join(dir, '.codegraph'), { recursive: true, force: true });
cg = CodeGraph.initSync(dir);
await cg.indexAll();
}, 180_000);
afterAll(() => {
cg?.destroy();
if (dir && fs.existsSync(dir)) fs.rmSync(dir, { recursive: true, force: true });
});
describe('fixture shape — if this rots, the gates below mean nothing', () => {
it('indexes the bracketed-path target with its scroll symbols', () => {
const names = cg.getNodesInFile(TARGET).map((n) => n.name);
expect(names).toContain('feedAtBottom');
expect(names).toContain('handleFeedScroll');
expect(names).toContain('pinFeedIfNearBottom');
});
});
describe('path pinning (fix 1)', () => {
it('a pure-path query renders the named file and says it was pinned', async () => {
const out = await explore(TARGET);
expect(hasSection(out, TARGET)).toBe(true);
expect(out).toContain('pinned from the query');
});
it('the original bug-shaped query renders the pinned file, not path shrapnel', async () => {
const out = await explore(
`run page auto-scroll to bottom logic in ${TARGET} — scrollToBottom, onscroll, atBottom tracking`,
);
expect(hasSection(out, TARGET)).toBe(true);
// The path fragments must not seed: `runId` (runs-store decoy) and the
// bracketed segment's namesakes headlined the original junk blast radius.
const blast = out.split('**Relationships**')[0]!;
expect(blast).not.toMatch(/`runId` \(src\/lib\/runs-store\.ts/);
// The chat decoy MAY render — it genuinely holds scroll-pinning code the
// segment supplement now finds — but the pinned file must rank first.
// (Pre-fix, `+page`/`runs` shrapnel admitted the siblings ABOVE the named
// file and the envelope truncated it.)
const decoyAt = out.indexOf('**`' + DECOY_CHAT + '`');
const targetAt = out.indexOf('**`' + TARGET + '`');
expect(targetAt).toBeGreaterThan(-1);
if (decoyAt !== -1) expect(targetAt).toBeLessThan(decoyAt);
});
it('an unresolvable path is reported, not silently dropped', async () => {
const out = await explore('crash in src/routes/gone/missing-page.ts on load');
expect(out).toContain('No indexed file uniquely matches');
expect(out).toContain('src/routes/gone/missing-page.ts');
});
});
describe('segment supplement + variable seeding (fixes 23)', () => {
it('word-level scroll terms reach the camelCase scroll code without a path', async () => {
const out = await explore('feed auto-scroll to bottom pinning behavior');
expect(hasSection(out, TARGET)).toBe(true);
});
it('a camel infix naming only $state-style variables still finds their file', async () => {
const out = await explore('where does the atBottom flag get reset');
expect(hasSection(out, TARGET)).toBe(true);
});
});