Files
msd-core/tests/allowlist-ratchet.test.cjs
Rezolv 74d7bc8239 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>
2026-06-11 23:59:56 -04:00

446 lines
16 KiB
JavaScript

'use strict';
/**
* Tests for scripts/lib/allowlist-ratchet.cjs
*
* Covers assertWithinAllowlist and assertTightCeiling.
* Uses a non-throwing fake `fail` that records messages into an array so we can
* assert on call count and message content without early-exit on first failure.
*/
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const {
assertWithinAllowlist,
assertTightCeiling,
assertFileBaseline,
} = require('../scripts/lib/allowlist-ratchet.cjs');
// ─── Fake fail helper ────────────────────────────────────────────────────────
/**
* Returns a { fail, calls } pair. `fail` records its message without throwing,
* so tests can observe every violation rather than stopping at the first.
*/
function makeFail() {
const calls = [];
return {
calls,
fail(msg) {
calls.push(msg);
},
};
}
// ─── assertWithinAllowlist ───────────────────────────────────────────────────
describe('assertWithinAllowlist', () => {
test('clean case: current subset of known, no stale entries — fail never called', () => {
const { fail, calls } = makeFail();
const result = assertWithinAllowlist({
label: 'test-guard',
current: ['a.ts', 'b.ts'],
known: ['a.ts', 'b.ts'],
fail,
});
assert.strictEqual(calls.length, 0, 'fail should not be called');
assert.deepStrictEqual(result.novel, []);
assert.deepStrictEqual(result.stale, []);
});
test('novel detected: id in current but not in known — fail called with that id', () => {
const { fail, calls } = makeFail();
const result = assertWithinAllowlist({
label: 'novel-guard',
current: ['a.ts', 'b.ts', 'c.ts'],
known: ['a.ts', 'b.ts'],
fail,
});
assert.strictEqual(calls.length, 1, 'fail should be called once for novel');
assert.ok(calls[0].includes('c.ts'), 'message should mention the novel id');
assert.ok(
calls[0].includes('fix at the source'),
'message should include fix-at-source guidance'
);
assert.deepStrictEqual(result.novel, ['c.ts']);
assert.deepStrictEqual(result.stale, []);
});
test('stale detected: id in known but not in current — fail called with that id', () => {
const { fail, calls } = makeFail();
const result = assertWithinAllowlist({
label: 'stale-guard',
current: ['a.ts'],
known: ['a.ts', 'b.ts'],
fail,
});
assert.strictEqual(calls.length, 1, 'fail should be called once for stale');
assert.ok(calls[0].includes('b.ts'), 'message should mention the stale id');
assert.ok(
calls[0].includes('ratchets toward zero'),
'message should include ratchet-toward-zero language'
);
assert.deepStrictEqual(result.novel, []);
assert.deepStrictEqual(result.stale, ['b.ts']);
});
test('stale message includes pruneHint when provided', () => {
const { fail, calls } = makeFail();
assertWithinAllowlist({
label: 'prune-guard',
current: ['a.ts'],
known: ['a.ts', 'b.ts'],
fail,
pruneHint: 'edit scripts/my-allowlist.json',
});
assert.ok(
calls[0].includes('edit scripts/my-allowlist.json'),
'message should include the pruneHint'
);
});
test('both novel and stale at once — fail called twice', () => {
const { fail, calls } = makeFail();
const result = assertWithinAllowlist({
label: 'both-guard',
current: ['a.ts', 'c.ts'], // c.ts is new, b.ts is fixed
known: ['a.ts', 'b.ts'],
fail,
});
assert.strictEqual(calls.length, 2, 'fail should be called once for novel and once for stale');
const allMessages = calls.join('\n');
assert.ok(allMessages.includes('c.ts'), 'should mention novel id c.ts');
assert.ok(allMessages.includes('b.ts'), 'should mention stale id b.ts');
assert.deepStrictEqual(result.novel, ['c.ts']);
assert.deepStrictEqual(result.stale, ['b.ts']);
});
test('empty inputs — fail never called', () => {
const { fail, calls } = makeFail();
const result = assertWithinAllowlist({
label: 'empty-guard',
current: [],
known: [],
fail,
});
assert.strictEqual(calls.length, 0);
assert.deepStrictEqual(result.novel, []);
assert.deepStrictEqual(result.stale, []);
});
test('order-independence: Sets and arrays produce the same result', () => {
const callsArr = makeFail();
const callsSet = makeFail();
const resultArr = assertWithinAllowlist({
label: 'order-array',
current: ['z.ts', 'a.ts', 'm.ts'],
known: ['a.ts', 'm.ts'],
fail: callsArr.fail,
});
const resultSet = assertWithinAllowlist({
label: 'order-set',
current: new Set(['z.ts', 'a.ts', 'm.ts']),
known: new Set(['a.ts', 'm.ts']),
fail: callsSet.fail,
});
assert.deepStrictEqual(resultArr.novel, resultSet.novel, 'novel should be identical regardless of input type');
assert.deepStrictEqual(resultArr.stale, resultSet.stale, 'stale should be identical regardless of input type');
assert.deepStrictEqual(resultArr.novel, ['z.ts'], 'novel should be sorted');
});
test('returned novel and stale arrays are sorted', () => {
const { fail } = makeFail();
const result = assertWithinAllowlist({
label: 'sort-guard',
current: ['z.ts', 'a.ts', 'm.ts', 'new.ts'],
known: ['z.ts', 'a.ts', 'm.ts', 'old.ts'],
fail,
});
assert.deepStrictEqual(result.novel, ['new.ts']);
assert.deepStrictEqual(result.stale, ['old.ts']);
});
test('current empty, known non-empty — all known are stale', () => {
const { fail, calls } = makeFail();
const result = assertWithinAllowlist({
label: 'all-stale',
current: [],
known: ['a.ts', 'b.ts'],
fail,
});
assert.strictEqual(calls.length, 1);
assert.deepStrictEqual(result.stale, ['a.ts', 'b.ts']);
assert.deepStrictEqual(result.novel, []);
});
test('known empty, current non-empty — all current are novel', () => {
const { fail, calls } = makeFail();
const result = assertWithinAllowlist({
label: 'all-novel',
current: ['a.ts', 'b.ts'],
known: [],
fail,
});
assert.strictEqual(calls.length, 1);
assert.deepStrictEqual(result.novel, ['a.ts', 'b.ts']);
assert.deepStrictEqual(result.stale, []);
});
});
// ─── assertTightCeiling ──────────────────────────────────────────────────────
describe('assertTightCeiling', () => {
test('actualMax under ceiling within grace — ok, fail never called', () => {
const { fail, calls } = makeFail();
const result = assertTightCeiling({
label: 'size-guard',
actualMax: 90,
ceiling: 100,
grace: 15,
fail,
});
assert.strictEqual(calls.length, 0, 'fail should not be called');
assert.strictEqual(result.ok, true);
assert.strictEqual(result.slack, 10);
});
test('actualMax over ceiling — fail called with regression message', () => {
const { fail, calls } = makeFail();
const result = assertTightCeiling({
label: 'size-guard',
actualMax: 110,
ceiling: 100,
grace: 5,
fail,
});
assert.strictEqual(calls.length, 1, 'fail should be called once');
assert.ok(calls[0].includes('Regression'), 'message should say Regression');
assert.ok(calls[0].includes('110'), 'message should include actualMax');
assert.ok(calls[0].includes('100'), 'message should include ceiling');
assert.strictEqual(result.ok, false);
assert.strictEqual(result.slack, -10);
});
test('ceiling too loose (slack > grace) — fail called with tighten message', () => {
const { fail, calls } = makeFail();
const result = assertTightCeiling({
label: 'loose-guard',
actualMax: 50,
ceiling: 100,
grace: 10,
fail,
});
assert.strictEqual(calls.length, 1, 'fail should be called once');
assert.ok(
calls[0].toLowerCase().includes('tighten') || calls[0].includes('too far'),
'message should mention tightening'
);
assert.ok(calls[0].includes('Budgets may only decrease'), 'message should include budget policy');
assert.strictEqual(result.ok, false);
assert.strictEqual(result.slack, 50);
});
test('boundary: slack === grace — ok (exactly at the grace limit)', () => {
const { fail, calls } = makeFail();
const result = assertTightCeiling({
label: 'boundary-guard',
actualMax: 90,
ceiling: 100,
grace: 10,
fail,
});
assert.strictEqual(calls.length, 0, 'fail should not be called at exact grace boundary');
assert.strictEqual(result.ok, true);
assert.strictEqual(result.slack, 10);
});
test('actualMax equals ceiling — ok, slack is zero', () => {
const { fail, calls } = makeFail();
const result = assertTightCeiling({
label: 'exact-guard',
actualMax: 100,
ceiling: 100,
grace: 0,
fail,
});
assert.strictEqual(calls.length, 0);
assert.strictEqual(result.ok, true);
assert.strictEqual(result.slack, 0);
});
test('grace 0: any slack triggers fail', () => {
const { fail, calls } = makeFail();
assertTightCeiling({
label: 'tight-guard',
actualMax: 99,
ceiling: 100,
grace: 0,
fail,
});
assert.strictEqual(calls.length, 1, 'any slack above 0 should fail when grace is 0');
});
test('label appears in failure messages', () => {
const { fail, calls } = makeFail();
assertTightCeiling({
label: 'my-special-guard',
actualMax: 200,
ceiling: 100,
grace: 5,
fail,
});
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');
});
});