1
0

explore-result-count.test.ts 4.1 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
  1. /**
  2. * codegraph_explore — the "Found N symbols across M files." header reflects the
  3. * CURATED answer actually rendered, not the raw candidate gather (#1046).
  4. *
  5. * A broad natural-language query FTS-matches a huge pool of symbols ("status",
  6. * "publish", "api" hit a large fraction of any API-heavy repo), but only a
  7. * handful of files clear the relevance gate + budget and render with source.
  8. * The header used to report `subgraph.nodes.size` / `fileGroups.size` — the raw
  9. * pool (260 symbols / 124 files on a 636-file repo) — which read as "wade
  10. * through 260 results" even though the correctly-ranked answer was the few files
  11. * below. It now reports only the files whose source survives in the output.
  12. *
  13. * The locked invariant: the header's file count EQUALS the number of rendered
  14. * `**`<path>`**` source sections. Pre-fix that failed whenever the gather
  15. * exceeded what rendered (here: 8 disconnected "noise" files are gathered but
  16. * gated out), so this fixture discriminates the fix from the old behaviour.
  17. */
  18. import { describe, it, expect, beforeEach, afterEach } from 'vitest';
  19. import * as fs from 'fs';
  20. import * as path from 'path';
  21. import * as os from 'os';
  22. import CodeGraph from '../src/index';
  23. import { ToolHandler } from '../src/mcp/tools';
  24. /** Files explore rendered as ``**`<path>`**`` source sections (issue #778: bold
  25. * labels, not ATX headings). */
  26. function renderedSourceFiles(text: string): string[] {
  27. const out: string[] = [];
  28. for (const line of text.split('\n')) {
  29. const m = line.match(/^\*\*`(.+?)`\*\*/);
  30. if (m) out.push(m[1].trim());
  31. }
  32. return out;
  33. }
  34. function headerFileCount(text: string): number | null {
  35. const m = text.match(/Found \d+ symbols? across (\d+) files?\./);
  36. return m ? parseInt(m[1], 10) : null;
  37. }
  38. describe('codegraph_explore — curated result count (#1046)', () => {
  39. let testDir: string;
  40. let cg: CodeGraph;
  41. let handler: ToolHandler;
  42. beforeEach(async () => {
  43. testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-count-'));
  44. // The real, connected flow — its symbols call each other, so it clears the
  45. // relevance gate and renders. snake_case so FTS tokenizes "status" out of
  46. // the names (camelCase would leave one unmatchable token).
  47. fs.writeFileSync(path.join(testDir, 'flow.ts'),
  48. `export function publish_status() { return build_status(); }\n` +
  49. `export function build_status() { return send_status(); }\n` +
  50. `export function send_status() { return 'ok'; }\n`);
  51. // Disconnected "noise" files: each defines ONE symbol that text-matches the
  52. // query word "status" but calls nothing in the flow. They ARE gathered into
  53. // the subgraph by FTS (so the OLD header counted them), but score too low to
  54. // render — exactly the breadth that inflated the count.
  55. for (let i = 0; i < 8; i++) {
  56. fs.writeFileSync(path.join(testDir, `status_widget_${i}.ts`),
  57. `export function status_widget_${i}() { return ${i}; }\n`);
  58. }
  59. cg = CodeGraph.initSync(testDir, { config: { include: ['**/*.ts'], exclude: [] } });
  60. await cg.indexAll();
  61. handler = new ToolHandler(cg);
  62. });
  63. afterEach(() => {
  64. if (cg) cg.destroy();
  65. if (fs.existsSync(testDir)) fs.rmSync(testDir, { recursive: true, force: true });
  66. });
  67. it('header file count equals the number of rendered source sections', async () => {
  68. const res = await handler.execute('codegraph_explore', { query: 'publish status' });
  69. const text = res.content[0].text;
  70. const headerFiles = headerFileCount(text);
  71. const rendered = renderedSourceFiles(text);
  72. expect(headerFiles).not.toBeNull();
  73. // The core honesty invariant — the header counts what's shown, not the gather.
  74. expect(headerFiles).toBe(rendered.length);
  75. // The flow file is the answer and must be among the rendered files.
  76. expect(rendered).toContain('flow.ts');
  77. // Curation actually happened: far fewer than the 9 gathered files (1 flow +
  78. // 8 noise) are reported. Pre-fix this was the inflated gather count.
  79. expect(headerFiles!).toBeLessThan(5);
  80. // And the sentinel placeholder never leaks into the rendered header.
  81. expect(text).not.toContain('codegraph-explore-summary');
  82. });
  83. });