| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153 |
- /**
- * Regression for #1711 — when codegraph_explore trims a file, elided symbols
- * must be named (gap markers + header bias), not left as a bare `... (gap) ...`
- * while the footer asks for "exact names" the model was never given.
- */
- 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,
- formatGapMarker,
- symbolsBetweenRanges,
- biasHeaderSymbols,
- joinPartsWithNamedGaps,
- } from '../src/mcp/tools';
- describe('#1711 helpers — name what a trim dropped', () => {
- it('formatGapMarker stays bare when the hole has no symbols', () => {
- expect(formatGapMarker('a.ts', [])).toBe('\n\n... (gap) ...\n\n');
- });
- it('formatGapMarker lists name (file:line) for elided symbols', () => {
- const marker = formatGapMarker('src/obs.ts', [
- { name: 'syncStateNow', kind: 'method', startLine: 1913 },
- { name: 'performHeavyDraftSync', kind: 'method', startLine: 1867 },
- ]);
- expect(marker).toContain('syncStateNow (src/obs.ts:1913)');
- expect(marker).toContain('performHeavyDraftSync (src/obs.ts:1867)');
- expect(marker).toMatch(/\.\.\. \(gap: .+\) \.\.\./);
- });
- it('symbolsBetweenRanges only returns defs that start in the hole', () => {
- const nodes = [
- { name: 'keep', kind: 'method', startLine: 10, endLine: 20 },
- { name: 'elided', kind: 'method', startLine: 30, endLine: 40 },
- { name: 'also', kind: 'method', startLine: 45, endLine: 50 },
- { name: 'later', kind: 'method', startLine: 60, endLine: 70 },
- { name: 'imp', kind: 'import', startLine: 35, endLine: 35 },
- ];
- const hit = symbolsBetweenRanges(nodes, 20, 60);
- expect(hit.map((h) => h.name)).toEqual(['elided', 'also']);
- });
- it('biasHeaderSymbols prefers elided labels over frequency alone', () => {
- const { shown } = biasHeaderSymbols(
- [
- 'imports0(method)', 'imports0(method)', 'imports0(method)',
- 'imports1(method)', 'imports1(method)',
- 'noise(method)',
- ],
- [{ name: 'syncStateNow', kind: 'method', startLine: 100 }],
- 3,
- );
- expect(shown[0]).toBe('syncStateNow(method)');
- expect(shown).toContain('imports0(method)');
- });
- it('joinPartsWithNamedGaps annotates the hole between parts', () => {
- const text = joinPartsWithNamedGaps(
- 'f.ts',
- [
- { range: { start: 1, end: 5 }, text: 'ONE' },
- { range: { start: 40, end: 45 }, text: 'TWO' },
- ],
- [{ name: 'mid', kind: 'function', startLine: 20, endLine: 25 }],
- );
- expect(text).toContain('ONE');
- expect(text).toContain('TWO');
- expect(text).toContain('mid (f.ts:20)');
- });
- });
- describe('#1711 explore — trimmed file names its elisions', () => {
- let dir: string;
- let cg: CodeGraph;
- let response: string;
- beforeAll(async () => {
- dir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-1711-'));
- fs.writeFileSync(path.join(dir, 'package.json'), '{"name":"cg1711","version":"1.0.0"}\n');
- const srcDir = path.join(dir, 'src');
- fs.mkdirSync(srcDir);
- // One large observer + noise files so the budget trims rather than shipping whole.
- const lines: string[] = ['export class EspnDraftObserver {'];
- for (let i = 0; i < 40; i++) {
- lines.push(` imports${i}() { return ${i}; }`, '');
- }
- const big = (name: string, next: string | null, n: number) => {
- lines.push(` ${name}() {`);
- lines.push(` const marker = "${name}_MARKER";`);
- for (let j = 0; j < n; j++) lines.push(` const x${j} = ${j} + marker.length;`);
- lines.push(next ? ` return this.${next}();` : ' return marker;');
- lines.push(' }', '');
- };
- big('persistDraftState', null, 60);
- big('performHeavyDraftSync', 'persistDraftState', 60);
- big('syncStateNow', 'performHeavyDraftSync', 60);
- big('scrapeFullDraftState', 'syncStateNow', 60);
- for (let i = 0; i < 30; i++) {
- lines.push(` calls${i}() { return ${i}; }`, '');
- }
- lines.push('}', '');
- fs.writeFileSync(path.join(srcDir, 'espn-draft-observer.ts'), lines.join('\n'));
- for (let i = 1; i <= 20; i++) {
- fs.writeFileSync(path.join(srcDir, `noise${i}.ts`), `export const n${i} = ${i};\n`);
- }
- cg = CodeGraph.initSync(dir);
- await cg.indexAll();
- const result = await new ToolHandler(cg).execute('codegraph_explore', {
- query:
- '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.',
- });
- response = result.content?.[0]?.text ?? '';
- }, 120_000);
- afterAll(() => {
- cg?.destroy();
- if (dir && fs.existsSync(dir)) fs.rmSync(dir, { recursive: true, force: true });
- });
- it('still renders the observer file (ranker chooses the right file)', () => {
- expect(response).toContain('espn-draft-observer.ts');
- });
- it('names elided symbols inside gap markers as name (file:line)', () => {
- // Match only in-fence gap markers that list at least one path:line ref.
- const namedGaps = response.match(/\.\.\. \(gap: [^\n]*?\([^\n]+?:\d+\)[^\n]*?\) \.\.\./g) ?? [];
- expect(namedGaps.length, `response head:\n${response.slice(0, 2000)}`).toBeGreaterThan(0);
- for (const g of namedGaps) {
- expect(g).toMatch(/\w+ \([^\s)]+:\d+\)/);
- }
- });
- it('footer points at named gaps / header instead of asking for unknown names', () => {
- if (!response.includes('trimmed for size')) return;
- expect(response).toMatch(/preferred in the file header|named inside gap markers/);
- });
- it('biases the file header away from filler-only when symbols were elided', () => {
- const header = response.split('\n').find((l) => l.includes('**`src/espn-draft-observer.ts`**'));
- expect(header).toBeDefined();
- // Either the header names a chain method, or a named gap does — never
- // neither while the footer asks for exact names.
- const namesAnswer = /syncStateNow|performHeavyDraftSync|persistDraftState|scrapeFullDraftState/;
- const namedSomewhere =
- namesAnswer.test(header!) ||
- namesAnswer.test(response);
- expect(namedSomewhere).toBe(true);
- });
- });
|