explore-path-pinning.test.ts 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
  1. /**
  2. * End-to-end gate for query-path pinning + the segment-vocab supplement +
  3. * variable seeding, on the bug that motivated all three: an agent named a
  4. * SvelteKit route file by exact path plus behavior words ("scrollToBottom,
  5. * onscroll, atBottom tracking") and got back neither the file's scroll code
  6. * nor the file itself at full weight — the bracketed path was tokenizer
  7. * shrapnel (`runId` seeded as a named symbol, every sibling `+page` admitted)
  8. * and the camelCase scroll symbols were FTS-opaque.
  9. *
  10. * The fixture mirrors that shape in plain TS (bracket/paren directories are
  11. * the crux, not the language): a target file under
  12. * `src/routes/m/projects/[id]/runs/[runId]/` holding `feedAtBottom` /
  13. * `handleFeedScroll` / `pinFeedIfNearBottom`, a decoy chat-window page under
  14. * a `(protected)` route group, and a runs-store decoy defining `runId` and
  15. * `Scope` — the two symbols that headlined the original junk blast radius.
  16. */
  17. import { describe, it, expect, beforeAll, afterAll } from 'vitest';
  18. import * as fs from 'fs';
  19. import * as path from 'path';
  20. import * as os from 'os';
  21. import CodeGraph from '../src/index';
  22. import { ToolHandler } from '../src/mcp/tools';
  23. const FIXTURE = 'explore-path-pinning';
  24. const TARGET = 'src/routes/m/projects/[id]/runs/[runId]/+page.ts';
  25. const DECOY_CHAT = 'src/routes/(protected)/chat-window/+page.ts';
  26. let dir: string;
  27. let cg: CodeGraph;
  28. async function explore(query: string): Promise<string> {
  29. const res = await new ToolHandler(cg).execute('codegraph_explore', { query });
  30. return res.content?.[0]?.text ?? '';
  31. }
  32. /** The response renders a source section for `file`. */
  33. const hasSection = (response: string, file: string): boolean =>
  34. response.includes('**`' + file + '`');
  35. beforeAll(async () => {
  36. dir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-path-pin-'));
  37. fs.cpSync(path.join(__dirname, 'fixtures', FIXTURE), dir, { recursive: true });
  38. fs.rmSync(path.join(dir, '.codegraph'), { recursive: true, force: true });
  39. cg = CodeGraph.initSync(dir);
  40. await cg.indexAll();
  41. }, 180_000);
  42. afterAll(() => {
  43. cg?.destroy();
  44. if (dir && fs.existsSync(dir)) fs.rmSync(dir, { recursive: true, force: true });
  45. });
  46. describe('fixture shape — if this rots, the gates below mean nothing', () => {
  47. it('indexes the bracketed-path target with its scroll symbols', () => {
  48. const names = cg.getNodesInFile(TARGET).map((n) => n.name);
  49. expect(names).toContain('feedAtBottom');
  50. expect(names).toContain('handleFeedScroll');
  51. expect(names).toContain('pinFeedIfNearBottom');
  52. });
  53. });
  54. describe('path pinning (fix 1)', () => {
  55. it('a pure-path query renders the named file and says it was pinned', async () => {
  56. const out = await explore(TARGET);
  57. expect(hasSection(out, TARGET)).toBe(true);
  58. expect(out).toContain('pinned from the query');
  59. });
  60. it('the original bug-shaped query renders the pinned file, not path shrapnel', async () => {
  61. const out = await explore(
  62. `run page auto-scroll to bottom logic in ${TARGET} — scrollToBottom, onscroll, atBottom tracking`,
  63. );
  64. expect(hasSection(out, TARGET)).toBe(true);
  65. // The path fragments must not seed: `runId` (runs-store decoy) and the
  66. // bracketed segment's namesakes headlined the original junk blast radius.
  67. const blast = out.split('**Relationships**')[0]!;
  68. expect(blast).not.toMatch(/`runId` \(src\/lib\/runs-store\.ts/);
  69. // The chat decoy MAY render — it genuinely holds scroll-pinning code the
  70. // segment supplement now finds — but the pinned file must rank first.
  71. // (Pre-fix, `+page`/`runs` shrapnel admitted the siblings ABOVE the named
  72. // file and the envelope truncated it.)
  73. const decoyAt = out.indexOf('**`' + DECOY_CHAT + '`');
  74. const targetAt = out.indexOf('**`' + TARGET + '`');
  75. expect(targetAt).toBeGreaterThan(-1);
  76. if (decoyAt !== -1) expect(targetAt).toBeLessThan(decoyAt);
  77. });
  78. it('an unresolvable path is reported, not silently dropped', async () => {
  79. const out = await explore('crash in src/routes/gone/missing-page.ts on load');
  80. expect(out).toContain('No indexed file uniquely matches');
  81. expect(out).toContain('src/routes/gone/missing-page.ts');
  82. });
  83. });
  84. describe('extension-less kebab basenames (the amnisphere gap)', () => {
  85. const KEBAB_TARGET = 'src/lib/background-image-table.ts';
  86. it('a bare kebab basename — no slash, no extension — pins and renders its file', async () => {
  87. // Pre-fix this query never opened the path gate; FTS shredded the token
  88. // into `background`/`image`/`table` and served the fragment decoy instead.
  89. const out = await explore('background-image-table Source column');
  90. expect(hasSection(out, KEBAB_TARGET)).toBe(true);
  91. expect(out).toContain('pinned from the query');
  92. });
  93. it('kebab prose that names no file is not reported as an unresolved path', async () => {
  94. const out = await explore('how does cross-call dedup interact with feed scroll pinning');
  95. expect(out).not.toContain('No indexed file uniquely matches');
  96. });
  97. });
  98. describe('segment supplement + variable seeding (fixes 2–3)', () => {
  99. it('word-level scroll terms reach the camelCase scroll code without a path', async () => {
  100. const out = await explore('feed auto-scroll to bottom pinning behavior');
  101. expect(hasSection(out, TARGET)).toBe(true);
  102. });
  103. it('a camel infix naming only $state-style variables still finds their file', async () => {
  104. const out = await explore('where does the atBottom flag get reset');
  105. expect(hasSection(out, TARGET)).toBe(true);
  106. });
  107. });