Extends the #1074 scheme to tests/agent-size-budget.test.cjs, which used the same assertTightCeiling tier ratchet but was still line-based (never rebased in #717). Completes the migration — the last part of the #1074 epic. - Rebase agent sizing from lines to LF-normalized bytes (#717/#683). - Delete the 'SIZE: tier anti-creep' describe (3 assertTightCeiling tests); add a per-agent baseline (tests/agent-size-baseline.json) as the primary anti-creep, and byte hard caps (XL 56 KiB / LARGE 48 KiB / DEFAULT 24 KiB), each above its tier high-water with real headroom. No separate new-file cap: a net-new agent is DEFAULT-tier, already bounded by the DEFAULT cap. - Keep the agent-classification tests verbatim. - scripts/workflow-size.cjs: add generic measureMdFiles(dir, predicate) (workflows + agents share one byte-measurement path); measureWorkflows now delegates to it. - scripts/update-size-baseline.cjs: one 'npm run size:baseline' now regenerates BOTH the workflow and agent baselines (gsd-* filter for agents). Rebased onto next after PR 2/3 (#1096) merged: replicate the scripts/lib/workflow-size.cjs -> scripts/workflow-size.cjs move (PR 1/3) across the generator and the agent test's require; regenerate the agent baseline against current agents (a uniform +170 B preamble drift on all 33 since authoring). Addresses the #1097 review (trek-e): - BLOCKER (acceptance criterion 5): document the agent contract in CONTEXT.md. Adds RULESET.AGENT_SIZE_BUDGET (caps 57344/49152/24576, per-agent baseline, dual size:baseline, shared measureMdFiles seam) and disambiguates it from the separate DEFECT.AGENT-FILE-SIZE-CAP-BREACH 45K-CHAR guard (two units, two purposes). - Docs: now that #1096's docs/TESTING-SUITES.md "Workflow size budget" section is in next, fold in the agent coverage here (renamed to "Workflow & agent size budget"): agent caps + per-agent baseline + the how-to + reference rows, and the disambiguation from the 45K-char guard. - Minor (negative proof): add a boundary-fixture test exercising the hard-cap comparison at cap-1/cap/cap+1 through the real lfByteCount path, so a future threshold/operator edit can't silently neuter a cap. - Nit: align the tier test name wording ("stays within") with the <= operator. Negative proof on a real tracked agent (gsd-planner): baseline catches +10 B; XL hard cap catches 57,516 > 57,344 with the baseline current. Closes #1095 (PR 3/3 child); landing this completes the #1074 epic.
145 lines
5.2 KiB
JavaScript
145 lines
5.2 KiB
JavaScript
'use strict';
|
|
|
|
/**
|
|
* Tests for scripts/update-size-baseline.cjs — the per-file workflow size
|
|
* baseline generator (issue #1074).
|
|
*/
|
|
|
|
const { test, describe, beforeEach, afterEach } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('fs');
|
|
const os = require('node:os');
|
|
const path = require('path');
|
|
|
|
const {
|
|
generateBaseline,
|
|
serializeBaseline,
|
|
} = require('../scripts/update-size-baseline.cjs');
|
|
const { assertFileBaseline } = require('../scripts/lib/allowlist-ratchet.cjs');
|
|
const { measureWorkflows } = require('../scripts/workflow-size.cjs');
|
|
const { cleanup } = require('./helpers.cjs');
|
|
|
|
describe('serializeBaseline', () => {
|
|
test('keys are sorted and output ends with a trailing newline', () => {
|
|
const out = serializeBaseline({ 'z.md': 3, 'a.md': 1, 'm.md': 2 });
|
|
assert.ok(out.endsWith('\n'), 'must end with a trailing newline');
|
|
const keys = Object.keys(JSON.parse(out));
|
|
assert.deepStrictEqual(keys, ['a.md', 'm.md', 'z.md'], 'keys must be sorted');
|
|
});
|
|
|
|
test('is stable: same input serializes identically (minimal-diff artifact)', () => {
|
|
const input = { 'b.md': 2, 'a.md': 1 };
|
|
assert.strictEqual(serializeBaseline(input), serializeBaseline({ ...input }));
|
|
});
|
|
});
|
|
|
|
describe('generateBaseline', () => {
|
|
let dir;
|
|
let outPath;
|
|
beforeEach(() => {
|
|
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-gen-baseline-'));
|
|
outPath = path.join(dir, 'baseline.json');
|
|
});
|
|
afterEach(() => cleanup(dir));
|
|
|
|
test('writes a baseline matching the measured workflow sizes', () => {
|
|
const wfDir = path.join(dir, 'workflows');
|
|
fs.mkdirSync(wfDir);
|
|
fs.writeFileSync(path.join(wfDir, 'one.md'), 'hello\n');
|
|
fs.writeFileSync(path.join(wfDir, 'two.md'), 'a longer body here\n');
|
|
|
|
const result = generateBaseline({ dir: wfDir, outPath });
|
|
assert.strictEqual(result.count, 2);
|
|
|
|
const written = JSON.parse(fs.readFileSync(outPath, 'utf-8'));
|
|
assert.deepStrictEqual(written, measureWorkflows(wfDir));
|
|
});
|
|
|
|
test('idempotent: a second run with no changes produces an identical file', () => {
|
|
const wfDir = path.join(dir, 'workflows');
|
|
fs.mkdirSync(wfDir);
|
|
fs.writeFileSync(path.join(wfDir, 'one.md'), 'hello\n');
|
|
|
|
generateBaseline({ dir: wfDir, outPath });
|
|
const first = fs.readFileSync(outPath, 'utf-8');
|
|
generateBaseline({ dir: wfDir, outPath });
|
|
const second = fs.readFileSync(outPath, 'utf-8');
|
|
assert.strictEqual(first, second, 'a no-op regeneration must not churn the file');
|
|
});
|
|
|
|
test('round-trip: a freshly generated baseline satisfies assertFileBaseline', () => {
|
|
const wfDir = path.join(dir, 'workflows');
|
|
fs.mkdirSync(wfDir);
|
|
fs.writeFileSync(path.join(wfDir, 'one.md'), 'hello\n');
|
|
fs.writeFileSync(path.join(wfDir, 'two.md'), 'world body\n');
|
|
|
|
generateBaseline({ dir: wfDir, outPath });
|
|
const baseline = JSON.parse(fs.readFileSync(outPath, 'utf-8'));
|
|
|
|
const calls = [];
|
|
assertFileBaseline({
|
|
label: 'roundtrip',
|
|
current: measureWorkflows(wfDir),
|
|
baseline,
|
|
fail: (m) => calls.push(m),
|
|
});
|
|
assert.deepStrictEqual(calls, [], 'a just-generated baseline must pass the guard with zero failures');
|
|
});
|
|
|
|
test('regeneration records growth after a workflow file grows', () => {
|
|
const wfDir = path.join(dir, 'workflows');
|
|
fs.mkdirSync(wfDir);
|
|
const wf = path.join(wfDir, 'one.md');
|
|
fs.writeFileSync(wf, 'small\n');
|
|
generateBaseline({ dir: wfDir, outPath });
|
|
|
|
// File grows; the OLD baseline should now flag growth...
|
|
fs.writeFileSync(wf, 'small\nplus several more bytes\n');
|
|
const oldBaseline = JSON.parse(fs.readFileSync(outPath, 'utf-8'));
|
|
const beforeRegen = [];
|
|
assertFileBaseline({
|
|
label: 'grow',
|
|
current: measureWorkflows(wfDir),
|
|
baseline: oldBaseline,
|
|
fail: (m) => beforeRegen.push(m),
|
|
});
|
|
assert.strictEqual(beforeRegen.length, 1, 'old baseline must flag the growth');
|
|
|
|
// ...and regenerating clears it.
|
|
generateBaseline({ dir: wfDir, outPath });
|
|
const newBaseline = JSON.parse(fs.readFileSync(outPath, 'utf-8'));
|
|
const afterRegen = [];
|
|
assertFileBaseline({
|
|
label: 'grow',
|
|
current: measureWorkflows(wfDir),
|
|
baseline: newBaseline,
|
|
fail: (m) => afterRegen.push(m),
|
|
});
|
|
assert.deepStrictEqual(afterRegen, [], 'regenerated baseline must pass');
|
|
});
|
|
|
|
test('throws when the workflows directory does not exist', () => {
|
|
assert.throws(
|
|
() => generateBaseline({ dir: path.join(dir, 'missing'), outPath }),
|
|
/ENOENT/
|
|
);
|
|
});
|
|
|
|
test('predicate filters which files are baselined (agent path)', () => {
|
|
const agentDir = path.join(dir, 'agents');
|
|
fs.mkdirSync(agentDir);
|
|
fs.writeFileSync(path.join(agentDir, 'gsd-one.md'), 'a\n');
|
|
fs.writeFileSync(path.join(agentDir, 'gsd-two.md'), 'bb\n');
|
|
fs.writeFileSync(path.join(agentDir, 'README.md'), 'not an agent\n');
|
|
|
|
const result = generateBaseline({
|
|
dir: agentDir,
|
|
outPath,
|
|
predicate: (f) => f.startsWith('gsd-'),
|
|
});
|
|
assert.strictEqual(result.count, 2, 'only gsd-*.md files should be baselined');
|
|
const written = JSON.parse(fs.readFileSync(outPath, 'utf-8'));
|
|
assert.deepStrictEqual(Object.keys(written), ['gsd-one.md', 'gsd-two.md']);
|
|
});
|
|
});
|