explore-elided-symbol-names.test.ts 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. /**
  2. * Regression for #1711 — when codegraph_explore trims a file, elided symbols
  3. * must be named (gap markers + header bias), not left as a bare `... (gap) ...`
  4. * while the footer asks for "exact names" the model was never given.
  5. */
  6. import { describe, it, expect, beforeAll, afterAll } from 'vitest';
  7. import * as fs from 'fs';
  8. import * as path from 'path';
  9. import * as os from 'os';
  10. import CodeGraph from '../src/index';
  11. import {
  12. ToolHandler,
  13. formatGapMarker,
  14. symbolsBetweenRanges,
  15. biasHeaderSymbols,
  16. joinPartsWithNamedGaps,
  17. } from '../src/mcp/tools';
  18. describe('#1711 helpers — name what a trim dropped', () => {
  19. it('formatGapMarker stays bare when the hole has no symbols', () => {
  20. expect(formatGapMarker('a.ts', [])).toBe('\n\n... (gap) ...\n\n');
  21. });
  22. it('formatGapMarker lists name (file:line) for elided symbols', () => {
  23. const marker = formatGapMarker('src/obs.ts', [
  24. { name: 'syncStateNow', kind: 'method', startLine: 1913 },
  25. { name: 'performHeavyDraftSync', kind: 'method', startLine: 1867 },
  26. ]);
  27. expect(marker).toContain('syncStateNow (src/obs.ts:1913)');
  28. expect(marker).toContain('performHeavyDraftSync (src/obs.ts:1867)');
  29. expect(marker).toMatch(/\.\.\. \(gap: .+\) \.\.\./);
  30. });
  31. it('symbolsBetweenRanges only returns defs that start in the hole', () => {
  32. const nodes = [
  33. { name: 'keep', kind: 'method', startLine: 10, endLine: 20 },
  34. { name: 'elided', kind: 'method', startLine: 30, endLine: 40 },
  35. { name: 'also', kind: 'method', startLine: 45, endLine: 50 },
  36. { name: 'later', kind: 'method', startLine: 60, endLine: 70 },
  37. { name: 'imp', kind: 'import', startLine: 35, endLine: 35 },
  38. ];
  39. const hit = symbolsBetweenRanges(nodes, 20, 60);
  40. expect(hit.map((h) => h.name)).toEqual(['elided', 'also']);
  41. });
  42. it('biasHeaderSymbols prefers elided labels over frequency alone', () => {
  43. const { shown } = biasHeaderSymbols(
  44. [
  45. 'imports0(method)', 'imports0(method)', 'imports0(method)',
  46. 'imports1(method)', 'imports1(method)',
  47. 'noise(method)',
  48. ],
  49. [{ name: 'syncStateNow', kind: 'method', startLine: 100 }],
  50. 3,
  51. );
  52. expect(shown[0]).toBe('syncStateNow(method)');
  53. expect(shown).toContain('imports0(method)');
  54. });
  55. it('joinPartsWithNamedGaps annotates the hole between parts', () => {
  56. const text = joinPartsWithNamedGaps(
  57. 'f.ts',
  58. [
  59. { range: { start: 1, end: 5 }, text: 'ONE' },
  60. { range: { start: 40, end: 45 }, text: 'TWO' },
  61. ],
  62. [{ name: 'mid', kind: 'function', startLine: 20, endLine: 25 }],
  63. );
  64. expect(text).toContain('ONE');
  65. expect(text).toContain('TWO');
  66. expect(text).toContain('mid (f.ts:20)');
  67. });
  68. });
  69. describe('#1711 explore — trimmed file names its elisions', () => {
  70. let dir: string;
  71. let cg: CodeGraph;
  72. let response: string;
  73. beforeAll(async () => {
  74. dir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-1711-'));
  75. fs.writeFileSync(path.join(dir, 'package.json'), '{"name":"cg1711","version":"1.0.0"}\n');
  76. const srcDir = path.join(dir, 'src');
  77. fs.mkdirSync(srcDir);
  78. // One large observer + noise files so the budget trims rather than shipping whole.
  79. const lines: string[] = ['export class EspnDraftObserver {'];
  80. for (let i = 0; i < 40; i++) {
  81. lines.push(` imports${i}() { return ${i}; }`, '');
  82. }
  83. const big = (name: string, next: string | null, n: number) => {
  84. lines.push(` ${name}() {`);
  85. lines.push(` const marker = "${name}_MARKER";`);
  86. for (let j = 0; j < n; j++) lines.push(` const x${j} = ${j} + marker.length;`);
  87. lines.push(next ? ` return this.${next}();` : ' return marker;');
  88. lines.push(' }', '');
  89. };
  90. big('persistDraftState', null, 60);
  91. big('performHeavyDraftSync', 'persistDraftState', 60);
  92. big('syncStateNow', 'performHeavyDraftSync', 60);
  93. big('scrapeFullDraftState', 'syncStateNow', 60);
  94. for (let i = 0; i < 30; i++) {
  95. lines.push(` calls${i}() { return ${i}; }`, '');
  96. }
  97. lines.push('}', '');
  98. fs.writeFileSync(path.join(srcDir, 'espn-draft-observer.ts'), lines.join('\n'));
  99. for (let i = 1; i <= 20; i++) {
  100. fs.writeFileSync(path.join(srcDir, `noise${i}.ts`), `export const n${i} = ${i};\n`);
  101. }
  102. cg = CodeGraph.initSync(dir);
  103. await cg.indexAll();
  104. const result = await new ToolHandler(cg).execute('codegraph_explore', {
  105. query:
  106. 'In this repos ESPN draft observer (espn-draft-observer.ts), name in order the chain of methods from scrapeFullDraftState to the method that calls storage.saveDraftState. One line.',
  107. });
  108. response = result.content?.[0]?.text ?? '';
  109. }, 120_000);
  110. afterAll(() => {
  111. cg?.destroy();
  112. if (dir && fs.existsSync(dir)) fs.rmSync(dir, { recursive: true, force: true });
  113. });
  114. it('still renders the observer file (ranker chooses the right file)', () => {
  115. expect(response).toContain('espn-draft-observer.ts');
  116. });
  117. it('names elided symbols inside gap markers as name (file:line)', () => {
  118. // Match only in-fence gap markers that list at least one path:line ref.
  119. const namedGaps = response.match(/\.\.\. \(gap: [^\n]*?\([^\n]+?:\d+\)[^\n]*?\) \.\.\./g) ?? [];
  120. expect(namedGaps.length, `response head:\n${response.slice(0, 2000)}`).toBeGreaterThan(0);
  121. for (const g of namedGaps) {
  122. expect(g).toMatch(/\w+ \([^\s)]+:\d+\)/);
  123. }
  124. });
  125. it('footer points at named gaps / header instead of asking for unknown names', () => {
  126. if (!response.includes('trimmed for size')) return;
  127. expect(response).toMatch(/preferred in the file header|named inside gap markers/);
  128. });
  129. it('biases the file header away from filler-only when symbols were elided', () => {
  130. const header = response.split('\n').find((l) => l.includes('**`src/espn-draft-observer.ts`**'));
  131. expect(header).toBeDefined();
  132. // Either the header names a chain method, or a named gap does — never
  133. // neither while the footer asks for exact names.
  134. const namesAnswer = /syncStateNow|performHeavyDraftSync|persistDraftState|scrapeFullDraftState/;
  135. const namedSomewhere =
  136. namesAnswer.test(header!) ||
  137. namesAnswer.test(response);
  138. expect(namedSomewhere).toBe(true);
  139. });
  140. });