Files
codegraph/__tests__/explore-reservation-invariant.test.ts
T
Colby McHenryandClaude Opus 5 7cbde95ce2 fix(explore): pay every admitted file on every render path (CG-26)
The invariant this closes: every admitted file receives at least its
reservation before any file draws on carry-forward slack. CG-30 bounded an
oversize cluster member and CG-31 gave the cluster path a displacement guard;
three holes were left, and each one starved a file that had been admitted,
reserved and — in the worst case — rendered.

1. The whole-file arms had no displacement guard. BUY's fit test read
   `renderCeiling - totalChars` (everyone's room) while its source-space
   sibling refused the same trade, and GRACE was not fit-tested at all.
   okhttp's CallServerInterceptor.kt shipped 8,499 chars on a 5,964 funded
   ceiling and the rank-6 file below it delivered nothing. Both arms now test
   the render they actually produce against `fundedHeadroom`, and a whole
   render that does not fit falls through to clustering instead of skipping
   the file.

2. Every section was charged a flat 200 chars while a real header runs
   300-500. The loop believed it had room it did not have — okhttp allocated
   26,601 against a 24,400 ceiling — so the final truncation threw a
   fully-rendered section away. Sections are charged their real cost now, the
   owed-below arithmetic uses a per-file overhead estimated from the file's own
   symbols, and a marginal overrun trims the weakest cluster (or windows the
   last one into the room that is left) rather than skipping the file over a
   rounding difference.

3. `owedPayableBelow` held all-or-nothing. When the last admitted file's FULL
   reservation no longer fit, nothing was held for it: on the precise-query
   fixture the rank-5 file took 4,134 chars against a 2,948 reservation while
   rank 6 — admitted, reserved 2,539 — was left 4 chars and skipped. It now
   holds the remainder while that remainder is still worth a section
   (MIN_CHARS).

And the epilogue is budgeted instead of discarded. The flat 600-char margin was
neither the epilogue's size (1,064 gin, 1,788 django, 2,231 excalidraw) nor a
bound on it, so four of six suite repos shipped with no pointer list and no
reminders at all. The loop now reserves the epilogue's FLOOR — the one line
that says an uncovered area exists, plus a pointer for every file whose bytes
were deliberately withheld (CG-12) — and the rest is fitted to the room that
actually remains, in priority order, entry by entry. Sized from the real
strings; no constant was swept against the suite.

Deterministic, same clean-rebuilt indexes, baseline = CG-31 tip:

  repo         base source   new source   files      ceiling
  django            20,791       20,878   6 -> 6     was discarding its epilogue
  tokio             21,521       21,607   5 -> 5     was discarding its epilogue
  okhttp            19,034       18,870   5 -> 6     +1 file delivered
  excalidraw        20,204       19,652   8 -> 8     keeps its pointer list
  gin               10,776       10,776   4 -> 4     byte-identical
  alamofire         11,662       11,662   2 -> 2     byte-identical

No repo truncates any more and none loses a file. okhttp and excalidraw trade
164 and 552 source chars on their LAST-ranked file for the pointer list naming
what the response could not cover — bytes the CG-31 tip only had because it
over-filled a ceiling it mis-measured and then discarded the epilogue whole.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 03:45:28 -05:00

266 lines
12 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.
/**
* Regression fixture for CG-26 — the end-to-end reservation invariant.
*
* Every admitted file receives at least its reservation before any file draws
* on carry-forward slack.
*
* CG-30 bounded how far an oversize cluster member may overshoot and CG-31 gave
* the cluster path a displacement guard. This pins the invariant they jointly
* satisfy across EVERY render path — cluster, whole-file grace, whole-file BUY —
* and in BOTH directions: the top-ranked file when the files below it overspend,
* and an admitted lower-ranked file when the top one does.
*
* Two things CG-26 fixed are pinned here because nothing else can see them:
*
* - The whole-file arms were fit-tested against raw room before the ceiling,
* never against what was still owed below. A grace-sized file could take a
* pending file's reservation on its way to the ceiling; okhttp's
* `CallServerInterceptor.kt` shipped 8,499 chars on a 5,964 funded ceiling
* and the rank-6 file below it delivered nothing.
* - Every section was charged a flat 200 chars of overhead while a real header
* runs 300500. The loop believed it had room it did not have (okhttp
* rendered 26,601 chars against a 24,400 ceiling), so the final truncation
* threw a fully-rendered section away — the same starvation, arriving after
* the guard had done its work.
*
* Shares the `displacement-ts` fixture: four pipeline stages competing for one
* envelope, the first a single ~20K function, padded past 500 indexed files so
* the response sits on the 24K tier where reservations genuinely saturate the
* ceiling.
*/
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';
import { attributeSourceBytes } from '../src/mcp/explore-diagnostics';
import type { ExploreDiagnosticReport, ExploreDiagnosticFile } from '../src/mcp/explore-diagnostics';
const FIXTURE_SRC = path.join(__dirname, 'fixtures', 'displacement-ts');
const FILLER_FILES = 520;
/** The giant: one ~20K function. Ranks #1 under the spread query. */
const GIANT = 'src/pipeline/ingest.ts';
/**
* Three shapes, so the invariant is tested from both sides:
* spread — every stage named; the giant ranks #1 and overspends downwards.
* tail — the stages BELOW the giant named; something small ranks #1 while
* the giant competes from underneath. This is the direction CG-31's
* fixture could not reach.
* precise — one symbol. The concentration case the guard must not flatten.
*/
const QUERIES = {
spread: 'ingestRecords normalizeRecords enrichRecords publishRecords',
tail: 'publishRecords sinkRecord PipelineRecord ingestRecords',
precise: 'ingestRecords',
} as const;
type Shape = keyof typeof QUERIES;
interface Probe {
response: string;
report: ExploreDiagnosticReport;
bytes: Map<string, number>;
}
describe('CG-26 — no admitted file is starved, on any render path', () => {
let testDir: string;
let cg: CodeGraph;
const probes = {} as Record<Shape, Probe>;
/** Admitted = the allocator reserved bytes for it. */
const admitted = (probe: Probe): ExploreDiagnosticFile[] =>
probe.report.files.filter((f) => (f.allowance ?? 0) > 0);
const all = (): Probe[] => Object.values(probes);
beforeAll(async () => {
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-cg26-'));
fs.cpSync(FIXTURE_SRC, testDir, { recursive: true });
fs.rmSync(path.join(testDir, '.codegraph'), { recursive: true, force: true });
const filler = path.join(testDir, 'src', 'generated');
fs.mkdirSync(filler, { recursive: true });
for (let i = 0; i < FILLER_FILES; i++) {
fs.writeFileSync(
path.join(filler, `unit${i}.ts`),
`export const seed${i} = ${i};\n`
+ `export function widget${i}(n: number): number {\n return n * ${i + 1} + seed${i};\n}\n`,
);
}
cg = CodeGraph.initSync(testDir);
await cg.indexAll();
const sidecar = path.join(testDir, 'explore-diag.jsonl');
const previous = process.env.CODEGRAPH_EXPLORE_DEBUG;
process.env.CODEGRAPH_EXPLORE_DEBUG = sidecar;
try {
const handler = new ToolHandler(cg);
for (const [shape, query] of Object.entries(QUERIES) as [Shape, string][]) {
const result = await handler.execute('codegraph_explore', { query });
const response = result.content?.[0]?.text ?? '';
const written = fs.readFileSync(sidecar, 'utf-8').trim().split('\n').filter(Boolean);
probes[shape] = {
response,
report: JSON.parse(written[written.length - 1]!) as ExploreDiagnosticReport,
bytes: attributeSourceBytes(response),
};
}
} finally {
if (previous === undefined) delete process.env.CODEGRAPH_EXPLORE_DEBUG;
else process.env.CODEGRAPH_EXPLORE_DEBUG = previous;
}
}, 180_000);
afterAll(() => {
if (cg) cg.destroy();
if (testDir && fs.existsSync(testDir)) fs.rmSync(testDir, { recursive: true, force: true });
});
// ── Fixture shape — if these rot, the gates below mean nothing ─────────────
describe('fixture shape', () => {
it('sits on the 24K tier, where the reservations saturate the ceiling', () => {
expect(cg.getStats().fileCount).toBeGreaterThanOrEqual(500);
for (const probe of all()) expect(probe.report.budget.maxOutputChars).toBe(24000);
});
it('exercises both directions — the giant ranks #1 in one shape and lower in another', () => {
// Which shape puts it where is the ranker's business and may move; that
// it lands on BOTH sides across the three is what makes the gates below
// test the invariant rather than one arrangement of it.
const ranks = all().map((p) => p.report.files.find((f) => f.path === GIANT)?.rank ?? -1);
expect(ranks).toContain(1);
expect(ranks.some((r) => r > 1)).toBe(true);
});
it('exercises both render paths — something ships whole, something clusters', () => {
const modes = new Set(all().flatMap((p) => p.report.files.map((f) => f.render)));
expect(modes).toContain('clusters');
expect(modes).toContain('whole');
});
});
// ── The invariant ─────────────────────────────────────────────────────────
describe('the reservation invariant', () => {
it('CG-26 GATE: no file on ANY render path emits past what was still free', () => {
// CG-31 pinned this for `clusters` only. The whole-file arms were fit-
// tested against `renderCeiling - totalChars`, which is everyone's room,
// not this file's — so a whole render could spend a reservation the loop
// had already promised further down.
for (const [shape, probe] of Object.entries(probes) as [Shape, Probe][]) {
const over = probe.report.files
.filter((f) => f.render !== null && f.render !== 'dropped' && f.funded !== null)
// +1 for the render loop's own rounding on a windowed cut.
.filter((f) => f.emittedChars > f.funded! + 1)
.map((f) => `${shape}/${f.path}: ${f.emittedChars} emitted of ${f.funded} funded (${f.render})`);
expect(over).toEqual([]);
}
});
it('CG-26 GATE: every admitted file is delivered, whatever its rank', () => {
for (const [shape, probe] of Object.entries(probes) as [Shape, Probe][]) {
for (const rec of admitted(probe)) {
expect(rec.skipped, `${shape}/${rec.path} skipped`).toBeNull();
expect(probe.bytes.get(rec.path) ?? 0, `${shape}/${rec.path} bytes`).toBeGreaterThan(0);
}
}
});
it('CG-26 GATE: the rank-#1 file gets its reservation even when a file below overspends', () => {
// The direction CG-31's fixture could not reach: under `tail` the giant
// ranks below a small file and draws far past its own reservation from
// carry-forward slack. Rank #1 must still receive what it was promised
// (or its whole file, if that is less).
for (const [shape, probe] of Object.entries(probes) as [Shape, Probe][]) {
const top = admitted(probe).sort((a, b) => a.rank - b.rank)[0];
if (!top) continue;
const onDisk = fs.statSync(path.join(testDir, top.path)).size;
expect(probe.bytes.get(top.path) ?? 0, `${shape}/${top.path}`)
.toBeGreaterThanOrEqual(Math.min(top.allowance!, onDisk) * 0.9);
}
});
it('and the gate above is not vacuous — a lower-ranked file does overspend', () => {
const overspenders = (probe: Probe) => admitted(probe)
.filter((f) => f.rank > 1 && f.emittedChars > f.allowance!);
expect(overspenders(probes.tail).length).toBeGreaterThan(0);
});
});
// ── What the ceiling must no longer do ────────────────────────────────────
describe('the hard ceiling never throws a rendered section away', () => {
it('the render loop spends what it counts — nothing is allocated past the ceiling', () => {
// Sections used to be charged a flat 200 chars against a header that runs
// 300500, so the loop over-filled and the final truncation dropped whole
// sections. `allocatedChars` is the pre-truncation length: it staying
// under the ceiling IS the accounting being exact.
for (const [shape, probe] of Object.entries(probes) as [Shape, Probe][]) {
expect(probe.report.envelope.allocatedChars, shape)
.toBeLessThanOrEqual(probe.report.budget.hardCeiling);
expect(probe.report.envelope.truncated, shape).toBe(false);
}
});
it('no file is rendered and then dropped', () => {
for (const probe of all()) {
expect(probe.report.files.filter((f) => f.render === 'dropped')).toEqual([]);
}
});
it('keeps the response inside the hard ceiling', () => {
for (const probe of all()) {
expect(probe.report.envelope.chars).toBeLessThanOrEqual(probe.report.budget.hardCeiling);
}
});
});
// ── The epilogue is budgeted, not discarded ───────────────────────────────
describe('the epilogue the loop budgeted for is the epilogue it emits', () => {
it('a response that withheld files still says so, and says to explore not Read', () => {
// The flat 600-char margin was neither the epilogue's size nor a bound on
// it, so a saturated response shipped with no pointer list and no
// reminders at all. Whatever else is traded away, the agent must be told
// an uncovered area exists and that another explore reaches it.
for (const [shape, probe] of Object.entries(probes) as [Shape, Probe][]) {
const withheld = probe.report.files.some(
(f) => f.render === null || (probe.bytes.get(f.path) ?? 0) === 0);
if (!withheld) continue;
expect(
/Not shown above|omitted for size|codegraph_explore/.test(probe.response),
`${shape} withheld files without saying where to look`,
).toBe(true);
}
});
it('never steers the agent to Read', () => {
for (const probe of all()) {
expect(/use (the )?Read|fall back to Read(?!ing those files)/i.test(probe.response)).toBe(false);
}
});
});
// ── The thing the invariant must NOT become ───────────────────────────────
describe('concentration survives', () => {
it('a precise symbol query still puts the most source in the named file', () => {
const mine = probes.precise.bytes.get(GIANT) ?? 0;
expect(mine).toBeGreaterThan(0);
for (const [p, n] of probes.precise.bytes) {
if (p === GIANT) continue;
expect(mine, `${GIANT} vs ${p}`).toBeGreaterThan(n);
}
});
it('is not an even split — the named file outspends its equal share', () => {
const rec = probes.precise.report.files.find((f) => f.path === GIANT)!;
const even = probes.precise.report.budget.maxOutputChars / admitted(probes.precise).length;
expect(rec.emittedChars).toBeGreaterThan(even);
});
});
});