fix(test): review findings — Windows path normalization, full totals rebuild, ratchet coverage

Pre-landing review (5 specialists) found one critical: the ratchet test runs
in the curated Windows lane, where path.relative yields backslash skill names
that miss the test/ filter and mismatch every POSIX fixture key. Names are now
normalized once in buildRatchetBill (toPosixName) and the fixture filter is
tightened to test/fixtures/. All eight Bill.totals fields are rebuilt from the
filtered list (no fixture-polluted perInvocation/totalMd numbers for future
consumers). New coverage: Windows-separator normalization pins, a
captureContextBudget round-trip against tree-a (headroom math exact), a
stripFields regression pin (interactive/benefits-from absent from renders,
hooks/gbrain preserved), and the ceilings test no longer double-reports
stale-fixture entries.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-08-25 02:38:47 +00:00
co-authored by Claude Fable 5
parent 6d52318384
commit d80d3c3ed7
3 changed files with 85 additions and 5 deletions
+26 -4
View File
@@ -35,9 +35,19 @@ export const BUDGET_FIXTURE_PATH = path.join(REPO_ROOT, 'test', 'fixtures', 'con
export const ALWAYS_ON_HEADROOM = 1.05;
export const EAGER_HEADROOM = 1.10;
/**
* Skill names come from path.relative in buildBill, which yields backslash
* separators on Windows. The fixture keys are POSIX. Normalize once here so
* the filter, the fixture keys, and checkBudget's name matching agree on
* every platform (the ratchet test runs in the curated Windows lane).
*/
export function toPosixName(name: string): string {
return name.split(path.sep).join('/');
}
/** Skills that exist only as context-bill test data — never budgeted. */
export function isFixtureSkill(name: string): boolean {
return name.startsWith('test/');
return toPosixName(name).startsWith('test/fixtures/');
}
export interface ContextBudget {
@@ -46,20 +56,32 @@ export interface ContextBudget {
eagerPerInvocation: Record<string, number>;
}
/** The bill the ratchet grades: repo tree minus test-fixture skill dirs. */
/**
* The bill the ratchet grades: repo tree minus test-fixture skill dirs, with
* POSIX-normalized names and ALL totals rebuilt from the filtered list (a
* partially-updated totals object would hand fixture-polluted numbers to any
* future consumer of the perInvocation/totalMd fields).
*/
export function buildRatchetBill(root: string = REPO_ROOT): Bill {
const bill = buildBill(root);
const skills = bill.skills.filter((s) => !isFixtureSkill(s.name));
const skills = bill.skills
.map((s) => ({ ...s, name: toPosixName(s.name) }))
.filter((s) => !isFixtureSkill(s.name));
return {
...bill,
skills,
totals: {
...bill.totals,
skillCount: skills.length,
alwaysOnBytes: skills.reduce((n, s) => n + s.frontmatterBytes, 0),
alwaysOnTokens: skills.reduce((n, s) => n + s.frontmatterTokens, 0),
eagerBytesBySkill: Object.fromEntries(skills.map((s) => [s.name, s.eagerBytes])),
eagerTokensBySkill: Object.fromEntries(skills.map((s) => [s.name, Math.round(s.eagerTokens)])),
perInvocationBytesBySkill: Object.fromEntries(skills.map((s) => [s.name, s.perInvocationBytes])),
perInvocationTokensBySkill: Object.fromEntries(
skills.map((s) => [s.name, Math.round(s.perInvocationTokens)]),
),
totalMdBytes: skills.reduce((n, s) => n + s.totalMdBytes, 0),
totalMdTokens: skills.reduce((n, s) => n + s.totalMdTokens, 0),
},
};
}