test(#1074): add additive per-file workflow size baseline guard (PR 1/3) (#1089)

* test(#1074): add additive per-file workflow size baseline guard (PR 1/3)

Introduces a committed per-file size baseline scheme alongside (not replacing)
the existing tier anti-creep tests. Green by construction — the baseline
records current sizes, so both schemes pass side by side during migration.

- scripts/lib/allowlist-ratchet.cjs: add assertFileBaseline (third pure helper,
  same injected-fail style) — per-file growth/shrink/add/remove diff vs baseline.
- scripts/workflow-size.cjs: single source of truth for LF-normalized byte
  counting (#683) + workflow enumeration, shared by the guard and the generator
  so they can never measure differently. Lives in scripts/ root (NOT scripts/lib/)
  because it is dev/CI-only tooling — scripts/lib/ is bundled into the installed
  runtime, scripts/ root is not, so this keeps it out of the shipped payload.
- scripts/update-size-baseline.cjs + npm run size:baseline: regenerate the
  snapshot (sorted keys, trailing newline, idempotent).
- tests/workflow-size-baseline.json: generated snapshot (88 workflows).
- tests/workflow-size-budget.test.cjs: import the shared counter (drops the
  duplicated local byteCount) and add the per-file baseline describe block.
- Tests for the helper, the shared module, and the generator (incl. round-trip
  and fault-injection cases).

Refs #1074. Part 1 of 3; PR 2 swaps enforcement, PR 3 covers the agent test.

* test(#1074): regenerate workflow baseline after Update-branch merge with next

The 'Update branch' merge (652a916b) pulled in next's update.md change (#1090)
without regenerating the snapshot, leaving the per-file baseline stale by one
file. Re-ran `npm run size:baseline` so the committed baseline matches the
merged workflow files.

Refs #1074.

---------

Co-authored-by: Tom Boucher <trekkie@nomorestars.com>
This commit is contained in:
Rezolv
2026-06-11 23:59:56 -04:00
committed by GitHub
parent b4a7eabaae
commit 74d7bc8239
9 changed files with 723 additions and 14 deletions

View File

@@ -14,6 +14,7 @@ const assert = require('node:assert/strict');
const {
assertWithinAllowlist,
assertTightCeiling,
assertFileBaseline,
} = require('../scripts/lib/allowlist-ratchet.cjs');
// ─── Fake fail helper ────────────────────────────────────────────────────────
@@ -295,3 +296,150 @@ describe('assertTightCeiling', () => {
assert.ok(calls[0].includes('my-special-guard'), 'label should appear in message');
});
});
// ─── assertFileBaseline ──────────────────────────────────────────────────────
describe('assertFileBaseline', () => {
test('exact match: current equals baseline — fail never called', () => {
const { fail, calls } = makeFail();
const result = assertFileBaseline({
label: 'workflow-size',
current: { 'a.md': 100, 'b.md': 200 },
baseline: { 'a.md': 100, 'b.md': 200 },
fail,
});
assert.strictEqual(calls.length, 0, 'fail should not be called on an exact match');
assert.deepStrictEqual(result.grown, []);
assert.deepStrictEqual(result.shrunk, []);
assert.deepStrictEqual(result.added, []);
assert.deepStrictEqual(result.removed, []);
});
test('growth: a file larger than baseline — fail called, delta reported', () => {
const { fail, calls } = makeFail();
const result = assertFileBaseline({
label: 'workflow-size',
current: { 'a.md': 154, 'b.md': 200 },
baseline: { 'a.md': 100, 'b.md': 200 },
fail,
});
assert.strictEqual(calls.length, 1, 'fail should be called once for growth');
assert.ok(calls[0].includes('grew') || calls[0].includes('grow'), 'message should describe growth');
assert.ok(calls[0].includes('a.md'), 'message should name the grown file');
assert.ok(calls[0].includes('100') && calls[0].includes('154'), 'message should show from → to');
assert.ok(calls[0].includes('54'), 'message should show the +delta');
assert.deepStrictEqual(result.grown.map((g) => g.name), ['a.md']);
assert.strictEqual(result.grown[0].delta, 54);
});
test('shrink: a file smaller than baseline — fail called as stale (auto-tighten)', () => {
const { fail, calls } = makeFail();
const result = assertFileBaseline({
label: 'workflow-size',
current: { 'a.md': 80, 'b.md': 200 },
baseline: { 'a.md': 100, 'b.md': 200 },
fail,
});
assert.strictEqual(calls.length, 1, 'fail should be called once for a stale (shrunk) baseline');
assert.ok(/stale|smaller|shrank|shrunk/i.test(calls[0]), 'message should flag a stale/shrunk baseline');
assert.ok(calls[0].includes('a.md'), 'message should name the shrunk file');
assert.deepStrictEqual(result.shrunk.map((s) => s.name), ['a.md']);
assert.strictEqual(result.shrunk[0].delta, 20);
});
test('added: a file absent from baseline — fail called', () => {
const { fail, calls } = makeFail();
const result = assertFileBaseline({
label: 'workflow-size',
current: { 'a.md': 100, 'new.md': 50 },
baseline: { 'a.md': 100 },
fail,
});
assert.strictEqual(calls.length, 1, 'fail should be called once for an unbaselined new file');
assert.ok(/not in the baseline|new|missing/i.test(calls[0]), 'message should flag the unbaselined file');
assert.ok(calls[0].includes('new.md'), 'message should name the new file');
assert.deepStrictEqual(result.added, ['new.md']);
});
test('removed: a baseline entry with no current file — fail called', () => {
const { fail, calls } = makeFail();
const result = assertFileBaseline({
label: 'workflow-size',
current: { 'a.md': 100 },
baseline: { 'a.md': 100, 'gone.md': 70 },
fail,
});
assert.strictEqual(calls.length, 1, 'fail should be called once for an orphaned baseline entry');
assert.ok(/no longer exist|removed|orphan/i.test(calls[0]), 'message should flag the orphaned entry');
assert.ok(calls[0].includes('gone.md'), 'message should name the orphaned entry');
assert.deepStrictEqual(result.removed, ['gone.md']);
});
test('multiple categories at once — one fail per non-empty category', () => {
const { fail, calls } = makeFail();
const result = assertFileBaseline({
label: 'workflow-size',
current: { 'grow.md': 150, 'shrink.md': 50, 'new.md': 10 },
baseline: { 'grow.md': 100, 'shrink.md': 100, 'gone.md': 30 },
fail,
});
// grown(1) + shrunk(1) + added(1) + removed(1) = 4 categories
assert.strictEqual(calls.length, 4, 'one fail per non-empty category');
assert.deepStrictEqual(result.grown.map((g) => g.name), ['grow.md']);
assert.deepStrictEqual(result.shrunk.map((s) => s.name), ['shrink.md']);
assert.deepStrictEqual(result.added, ['new.md']);
assert.deepStrictEqual(result.removed, ['gone.md']);
});
test('updateHint appears in every failure message', () => {
const { fail, calls } = makeFail();
assertFileBaseline({
label: 'workflow-size',
current: { 'grow.md': 150, 'new.md': 10 },
baseline: { 'grow.md': 100, 'gone.md': 30 },
fail,
updateHint: 'Run `npm run size:baseline`',
});
assert.ok(calls.length >= 2, 'multiple categories should each fail');
for (const msg of calls) {
assert.ok(msg.includes('Run `npm run size:baseline`'), 'every message should carry the updateHint');
}
});
test('empty inputs — fail never called', () => {
const { fail, calls } = makeFail();
const result = assertFileBaseline({
label: 'workflow-size',
current: {},
baseline: {},
fail,
});
assert.strictEqual(calls.length, 0);
assert.deepStrictEqual(result.grown, []);
assert.deepStrictEqual(result.shrunk, []);
assert.deepStrictEqual(result.added, []);
assert.deepStrictEqual(result.removed, []);
});
test('returned category lists are sorted by name', () => {
const { fail } = makeFail();
const result = assertFileBaseline({
label: 'workflow-size',
current: { 'z.md': 10, 'a.md': 10, 'm.md': 10 },
baseline: {},
fail,
});
assert.deepStrictEqual(result.added, ['a.md', 'm.md', 'z.md'], 'added should be sorted');
});
test('label appears in failure messages', () => {
const { fail, calls } = makeFail();
assertFileBaseline({
label: 'my-size-guard',
current: { 'a.md': 200 },
baseline: { 'a.md': 100 },
fail,
});
assert.ok(calls[0].includes('my-size-guard'), 'label should appear in message');
});
});