Files
msd-core/tests/exit-code-registry.test.cjs
Tom Boucher 12f9d1d9a0 enhance(#3913): docs, and the guards come down (#3994)
ADR-3889 terminal phase. Generated docs/reference/exit-codes.md from the exit-code declaration with a --check drift arm; deleted the inert soft-error-exit-zero oracle; promoted untyped-success from SMELL to VIOLATION so it can fail a build; pruned all 5 smell-baseline entries.

Fixed inline: two mis-scoped oracles (routing-validity, value-hygiene), a second source behind the band table, unescaped declaration strings reaching Markdown, and a pre-existing Windows 8.3 short-name path-comparison defect.

Guard ledger corrected from a claimed net -4 to a measured net -1.

Closes #3913
2026-08-28 13:10:20 -04:00

1055 lines
49 KiB
JavaScript
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.
'use strict';
/**
* tests/exit-code-registry.test.cjs
*
* ADR-3889 ("One exit-code registry — 0 and 1 are free, everything else is
* allocated") Phase 1 (#3905): behavioral tests for the allocator —
* gsd-core/bin/shared/exit-codes.json (declaration), scripts/gen-exit-code-registry.cjs
* (generator + validator), and the generated gsd-core/bin/lib/exit-code-registry.cjs
* artifact (`EXIT_CODES`, `exitCodeFor`, `nameForExitCode`).
*
* Every test that needs a mutated declaration or artifact operates on a
* temp-dir copy driven via --declaration/--out — the real repo files under
* gsd-core/bin/shared and gsd-core/bin/lib are never mutated, since test
* files in this repo run in parallel.
*
* fast-check is confirmed present in package.json devDependencies (^4.8.0);
* property tests below pin { seed: 2704, numRuns: 200 } per-call so a
* failure replays deterministically regardless of this suite's global fc
* default.
*/
const { test, describe, before, after } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const { runNode } = require('./helpers/process-seam.cjs');
const { createTempDir, cleanup } = require('./helpers.cjs');
const { PROBE_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
const fc = require('./helpers/fast-check-setup.cjs');
const { splitLines } = require('../gsd-core/bin/lib/text-lines.cjs');
const REPO_ROOT = path.resolve(__dirname, '..');
const GEN_SCRIPT = path.join(REPO_ROOT, 'scripts', 'gen-exit-code-registry.cjs');
const REAL_DECLARATION_PATH = path.join(REPO_ROOT, 'gsd-core', 'bin', 'shared', 'exit-codes.json');
const REAL_ARTIFACT_PATH = path.join(REPO_ROOT, 'gsd-core', 'bin', 'lib', 'exit-code-registry.cjs');
const REAL_SCRIPTS_ARTIFACT_PATH = path.join(REPO_ROOT, 'scripts', 'lib', 'exit-code-registry.cjs');
const REAL_HOOKS_ARTIFACT_PATH = path.join(REPO_ROOT, 'hooks', 'lib', 'exit-code-registry.js');
const REAL_DTS_ARTIFACT_PATH = path.join(REPO_ROOT, 'src', 'exit-code-registry.d.cts');
const REAL_SH_ARTIFACT_PATH = path.join(REPO_ROOT, 'gsd-core', 'bin', 'shared', 'exit-codes.sh');
const generator = require(GEN_SCRIPT);
const registry = require(REAL_ARTIFACT_PATH);
const REGISTERED_NAMES = new Set(registry.EXIT_CODES.map((e) => e.name));
/** A minimal, otherwise-valid entry template, overridable per field. */
function makeEntry(overrides) {
return {
code: 64,
name: 'T_ENTRY',
meaning: 'a test meaning',
owner: 'generic',
authorizedBy: 'ADR-3889',
...overrides,
};
}
/**
* #3906 (ADR-3889 Phase 2): the generator now emits FIVE artifacts — a
* primary (gsd-core/bin/lib), a secondary (scripts/lib), and the ambient
* `.d.cts` type declaration (src/exit-code-registry.d.cts). #3908 (Phase 4)
* added a FOURTH: the shell-sourceable fragment (gsd-core/bin/shared/
* exit-codes.sh). #3911 (Phase 7) added a FIFTH: the hooks/lib/ copy
* (hooks/lib/exit-code-registry.js). Every existing call site below only
* overrides the PRIMARY path via `--out`; without matching
* `--scripts-out`/`--hooks-out`/`--dts-out`/`--sh-out` overrides, a
* `--write` here would clobber the real committed
* `scripts/lib/exit-code-registry.cjs`, `hooks/lib/exit-code-registry.js`,
* `src/exit-code-registry.d.cts`, and `gsd-core/bin/shared/exit-codes.sh` —
* dangerous since test files in this repo run in parallel. Rather than
* touch every call site, this single seam derives co-located,
* per-call-unique secondary/hooks/dts/sh paths from whatever `--out` value
* the test already supplies, whenever the caller has not already supplied
* its own `--scripts-out`/`--hooks-out`/`--dts-out`/`--sh-out`. Calls with
* no explicit `--out` (the "real committed set" checks) are left untouched.
*/
function ensureScriptsOut(args) {
const outIdx = args.indexOf('--out');
if (outIdx === -1) return args;
const outValue = args[outIdx + 1];
const extra = [];
if (!args.includes('--scripts-out')) extra.push('--scripts-out', `${outValue}.secondary.cjs`);
if (!args.includes('--hooks-out')) extra.push('--hooks-out', `${outValue}.hooks.js`);
if (!args.includes('--dts-out')) extra.push('--dts-out', `${outValue}.d.cts`);
if (!args.includes('--sh-out')) extra.push('--sh-out', `${outValue}.sh`);
return extra.length === 0 ? args : [...args, ...extra];
}
function runGen(args, opts = {}) {
return runNode([GEN_SCRIPT, ...ensureScriptsOut(args)], { timeoutMs: PROBE_TIMEOUT_MS, ...opts });
}
/**
* Run the generator CLI with `--json` and parse its single stdout JSON
* report. Per CONTRIBUTING.md's "Prohibited: Raw Text Matching on Test
* Outputs", CLI-subprocess assertions in this suite key off this structured
* `{ok, reason, context, detail?}` report — never a regex against human-readable
* stdout/stderr prose.
* @returns {{result: object, report: {ok:boolean, reason:string, detail?:string}}}
*/
function runGenJson(args, opts = {}) {
const result = runGen(['--json', ...args], opts);
let report;
try {
report = JSON.parse(result.stdout);
} catch (err) {
throw new Error(`runGenJson: stdout did not parse as JSON: ${err.message}\nstdout: ${result.stdout}\nstderr: ${result.stderr}`);
}
return { result, report };
}
// ── exitCodeFor / nameForExitCode ─────────────────────────────────────────────
describe('exit-code-registry: exitCodeFor', () => {
test('resolves each of the 5 registered names to its code', () => {
assert.equal(registry.exitCodeFor('HOOK_DENY'), 2);
assert.equal(registry.exitCodeFor('USAGE'), 64);
assert.equal(registry.exitCodeFor('NO_INPUT'), 66);
assert.equal(registry.exitCodeFor('UNAVAILABLE'), 69);
assert.equal(registry.exitCodeFor('INTERNAL'), 70);
});
const badNames = [
['unknown name', 'NOT_A_REAL_NAME'],
['empty string', ''],
['null', null],
['undefined', undefined],
['number 0', 0],
['plain object', {}],
['array', []],
['wrong case', 'usage'],
['untrimmed', ' USAGE '],
['__proto__', '__proto__'],
['constructor', 'constructor'],
['toString', 'toString'],
];
for (const [label, value] of badNames) {
test(`throws for ${label}`, () => {
assert.throws(() => registry.exitCodeFor(value));
});
}
});
describe('exit-code-registry: nameForExitCode', () => {
test('resolves each of the 5 registered codes to its name', () => {
assert.equal(registry.nameForExitCode(2), 'HOOK_DENY');
assert.equal(registry.nameForExitCode(64), 'USAGE');
assert.equal(registry.nameForExitCode(66), 'NO_INPUT');
assert.equal(registry.nameForExitCode(69), 'UNAVAILABLE');
assert.equal(registry.nameForExitCode(70), 'INTERNAL');
});
const badCodes = [
['unregistered code', 999],
['0 (free, unregistered)', 0],
['1 (free, unregistered)', 1],
['negative', -1],
['string', '64'],
['null', null],
['undefined', undefined],
];
for (const [label, value] of badCodes) {
test(`throws for ${label}`, () => {
assert.throws(() => registry.nameForExitCode(value));
});
}
});
describe('exit-code-registry: shipped table invariants', () => {
test('EXIT_CODES is frozen and every entry is frozen', () => {
assert.ok(Object.isFrozen(registry.EXIT_CODES));
for (const entry of registry.EXIT_CODES) {
assert.ok(Object.isFrozen(entry), `entry ${JSON.stringify(entry)} must be frozen`);
}
});
test('every shipped code is non-zero and inside an allocatable band', () => {
assert.ok(registry.EXIT_CODES.length > 0);
for (const entry of registry.EXIT_CODES) {
assert.ok(Number.isInteger(entry.code));
assert.notEqual(entry.code, 0);
assert.ok(
generator.isAllocatableCode(entry.code),
`code ${entry.code} (${entry.name}) must be inside an allocatable band`,
);
}
});
test('code 2 is owned only by hook-adapter in the shipped table', () => {
const hookDeny = registry.EXIT_CODES.find((e) => e.code === 2);
assert.ok(hookDeny);
assert.equal(hookDeny.owner, 'hook-adapter');
});
test('generic owns four distinct codes in the shipped table (ACCEPTED negative-space case)', () => {
const genericCodes = registry.EXIT_CODES.filter((e) => e.owner === 'generic').map((e) => e.code);
assert.equal(genericCodes.length, 4);
assert.equal(new Set(genericCodes).size, 4);
});
});
// ── Generator: REASON ─────────────────────────────────────────────────────────
describe('gen-exit-code-registry: REASON', () => {
const expectedKeys = [
'OK', 'DRIFTED', 'USAGE', 'MISSING_DECLARATION', 'MALFORMED_DECLARATION',
'NOT_AN_ARRAY', 'EMPTY_DECLARATION', 'INVALID_ENTRY', 'DUPLICATE_CODE',
'DUPLICATE_NAME', 'RESERVED_CODE', 'FORBIDDEN_OWNER', 'MISSING_ARTIFACT',
'INVALID_CHARACTERS',
];
test('is frozen', () => {
assert.ok(Object.isFrozen(generator.REASON));
});
test('key set matches exactly', () => {
assert.deepEqual(Object.keys(generator.REASON).sort(), [...expectedKeys].sort());
});
});
// ── Generator: per-entry band validation (limit-1/limit/limit+1 for every edge) ──
describe('gen-exit-code-registry: band validation', () => {
const cases = [
[0, 'RESERVED_CODE'],
[1, 'RESERVED_CODE'],
[2, 'OK'],
[3, 'RESERVED_CODE'],
[13, 'RESERVED_CODE'],
[14, 'RESERVED_CODE'],
[63, 'RESERVED_CODE'],
[64, 'OK'],
[78, 'OK'],
[79, 'RESERVED_CODE'],
[80, 'OK'],
[125, 'OK'],
[126, 'RESERVED_CODE'],
[127, 'RESERVED_CODE'],
[128, 'RESERVED_CODE'],
[-1, 'INVALID_ENTRY'],
[1.5, 'INVALID_ENTRY'],
['64', 'INVALID_ENTRY'],
[NaN, 'INVALID_ENTRY'],
[Infinity, 'INVALID_ENTRY'],
];
for (const [code, expected] of cases) {
test(`code ${String(code)} -> ${expected}`, () => {
const entry = makeEntry({
code,
name: `T_${String(code).replace(/[^A-Za-z0-9]/g, '_').toUpperCase()}`,
// code 2 is only accepted with owner hook-adapter; every other
// fixture code here uses 'generic' and is unaffected by that rule.
owner: code === 2 ? 'hook-adapter' : 'generic',
});
const result = generator.validateEntry(entry, 0);
if (expected === 'OK') {
assert.deepEqual(result, { ok: true });
} else {
assert.equal(result.ok, false);
assert.equal(result.reason, generator.REASON[expected]);
}
});
}
});
describe('gen-exit-code-registry: forbidden owner for code 2', () => {
test('code 2 with owner "hook-adapter" is accepted', () => {
const result = generator.validateEntry(makeEntry({ code: 2, name: 'HOOK_DENY_2', owner: 'hook-adapter' }), 0);
assert.deepEqual(result, { ok: true });
});
test('code 2 with any other owner is FORBIDDEN_OWNER', () => {
const result = generator.validateEntry(makeEntry({ code: 2, name: 'HOOK_DENY_2', owner: 'generic' }), 0);
assert.equal(result.ok, false);
assert.equal(result.reason, generator.REASON.FORBIDDEN_OWNER);
});
});
describe('gen-exit-code-registry: required string fields', () => {
const fields = ['meaning', 'owner', 'authorizedBy'];
const badValues = [undefined, '', ' '];
for (const field of fields) {
for (const bad of badValues) {
test(`missing/empty/whitespace "${field}" (${JSON.stringify(bad)}) -> INVALID_ENTRY`, () => {
const entry = makeEntry({ [field]: bad });
const result = generator.validateEntry(entry, 0);
assert.equal(result.ok, false);
assert.equal(result.reason, generator.REASON.INVALID_ENTRY);
});
}
}
test('non-SCREAMING_SNAKE_CASE name -> INVALID_ENTRY', () => {
const result = generator.validateEntry(makeEntry({ name: 'not_screaming' }), 0);
assert.equal(result.ok, false);
assert.equal(result.reason, generator.REASON.INVALID_ENTRY);
});
test('empty name -> INVALID_ENTRY', () => {
const result = generator.validateEntry(makeEntry({ name: '' }), 0);
assert.equal(result.ok, false);
assert.equal(result.reason, generator.REASON.INVALID_ENTRY);
});
});
// #3913 P9 SEC-3: a declaration string field carrying a `|`, CR, LF, or other
// control character breaks the Markdown table gen-exit-code-docs.cjs
// interpolates it into (a `|` splits the row; a `\n` can forge an entire
// extra row, including a fake Markdown heading). Rejected at the shared
// validator (validateEntry), not the renderer, so both generators inherit
// the fix.
describe('gen-exit-code-registry: forbidden characters in declaration string fields (#3913 P9 SEC-3)', () => {
const fields = ['meaning', 'owner', 'authorizedBy'];
const badValues = [
['a literal pipe', 'contains | a pipe'],
['a CR', 'contains\ra CR'],
['a LF', 'contains\na LF'],
['a CRLF', 'contains\r\na CRLF'],
['a NUL byte', 'contains\x00a NUL'],
['a DEL byte', 'contains\x7fa DEL'],
];
for (const field of fields) {
for (const [label, bad] of badValues) {
test(`"${field}" containing ${label} -> INVALID_CHARACTERS`, () => {
// F3a: failing-first against the pre-fix validator, this entry would
// have passed validateEntry entirely (no character check existed).
const entry = makeEntry({ [field]: bad });
const result = generator.validateEntry(entry, 0);
assert.equal(result.ok, false);
assert.equal(result.reason, generator.REASON.INVALID_CHARACTERS);
});
}
}
test('a clean value with none of the forbidden characters is accepted', () => {
const result = generator.validateEntry(makeEntry({ meaning: 'a perfectly normal meaning, with commas' }), 0);
assert.deepEqual(result, { ok: true });
});
test('hasForbiddenDeclarationChar is the exact predicate validateEntry uses (no drift)', () => {
assert.equal(generator.hasForbiddenDeclarationChar('clean'), false);
assert.equal(generator.hasForbiddenDeclarationChar('a | pipe'), true);
assert.equal(generator.hasForbiddenDeclarationChar('a\nnewline'), true);
assert.equal(generator.hasForbiddenDeclarationChar('a\rreturn'), true);
});
});
describe('gen-exit-code-registry: cross-entry invariants', () => {
test('duplicate code -> DUPLICATE_CODE, context carries the code and both names', () => {
const entries = [
makeEntry({ code: 64, name: 'FIRST_NAME' }),
makeEntry({ code: 64, name: 'SECOND_NAME' }),
];
const result = generator.validateEntries(entries);
assert.equal(result.ok, false);
assert.equal(result.reason, generator.REASON.DUPLICATE_CODE);
assert.deepEqual(result.context, { code: 64, names: ['FIRST_NAME', 'SECOND_NAME'] });
});
test('duplicate name -> DUPLICATE_NAME, context carries the name and both codes', () => {
const entries = [
makeEntry({ code: 64, name: 'SAME_NAME' }),
makeEntry({ code: 70, name: 'SAME_NAME' }),
];
const result = generator.validateEntries(entries);
assert.equal(result.ok, false);
assert.equal(result.reason, generator.REASON.DUPLICATE_NAME);
assert.deepEqual(result.context, { name: 'SAME_NAME', codes: [64, 70] });
});
test('same owner, different codes -> ACCEPTED', () => {
const entries = [
makeEntry({ code: 64, name: 'OWNER_A', owner: 'generic' }),
makeEntry({ code: 70, name: 'OWNER_B', owner: 'generic' }),
];
const result = generator.validateEntries(entries);
assert.deepEqual(result, { ok: true });
});
});
// ── Generator: declaration-file handling (pure loadDeclaration, temp files) ──
describe('gen-exit-code-registry: declaration file handling', () => {
let tmpDir;
before(() => {
tmpDir = createTempDir('gsd-exit-code-decl-');
});
after(() => {
cleanup(tmpDir);
});
test('absent declaration -> MISSING_DECLARATION', () => {
const missing = path.join(tmpDir, 'does-not-exist.json');
const result = generator.loadDeclaration(missing);
assert.equal(result.ok, false);
assert.equal(result.reason, generator.REASON.MISSING_DECLARATION);
});
test('unparseable JSON -> MALFORMED_DECLARATION', () => {
const bad = path.join(tmpDir, 'malformed.json');
fs.writeFileSync(bad, '{ this is not json', 'utf8');
const result = generator.loadDeclaration(bad);
assert.equal(result.ok, false);
assert.equal(result.reason, generator.REASON.MALFORMED_DECLARATION);
});
const notArrayCases = [
['object', '{}'],
['string', '"s"'],
['number', '0'],
['null', 'null'],
];
for (const [label, json] of notArrayCases) {
test(`valid JSON but not an array (${label}) -> NOT_AN_ARRAY`, () => {
const p = path.join(tmpDir, `not-array-${label}.json`);
fs.writeFileSync(p, json, 'utf8');
const result = generator.loadDeclaration(p);
assert.equal(result.ok, false);
assert.equal(result.reason, generator.REASON.NOT_AN_ARRAY);
});
}
test('empty array -> EMPTY_DECLARATION', () => {
const p = path.join(tmpDir, 'empty.json');
fs.writeFileSync(p, '[]', 'utf8');
const result = generator.loadDeclaration(p);
assert.equal(result.ok, false);
assert.equal(result.reason, generator.REASON.EMPTY_DECLARATION);
});
});
// ── Generator CLI ──────────────────────────────────────────────────────────────
describe('gen-exit-code-registry: CLI', () => {
let tmpDir;
before(() => {
tmpDir = createTempDir('gsd-exit-code-cli-');
});
after(() => {
cleanup(tmpDir);
});
function validDeclarationPath(dir, filename = 'exit-codes.json') {
const p = path.join(dir, filename);
fs.copyFileSync(REAL_DECLARATION_PATH, p);
return p;
}
test('--check is in sync against the real committed set (both .cjs artifacts and the .d.cts)', () => {
const result = runGen(['--check']);
assert.equal(result.exitCode, 0, result.stderr);
});
test('--write then --check on temp paths both exit 0', () => {
const decl = validDeclarationPath(tmpDir, 'a-decl.json');
const out = path.join(tmpDir, 'a-out.cjs');
const write = runGen(['--write', '--declaration', decl, '--out', out]);
assert.equal(write.exitCode, 0, write.stderr);
const check = runGen(['--check', '--declaration', decl, '--out', out]);
assert.equal(check.exitCode, 0, check.stderr);
});
test('--write is idempotent (byte-identical on a second run)', () => {
const decl = validDeclarationPath(tmpDir, 'b-decl.json');
const out = path.join(tmpDir, 'b-out.cjs');
const first = runGen(['--write', '--declaration', decl, '--out', out]);
assert.equal(first.exitCode, 0, first.stderr);
const firstBytes = fs.readFileSync(out, 'utf8');
const second = runGen(['--write', '--declaration', decl, '--out', out]);
assert.equal(second.exitCode, 0, second.stderr);
const secondBytes = fs.readFileSync(out, 'utf8');
assert.equal(secondBytes, firstBytes);
});
test('--check on a hand-edited artifact -> DRIFTED', () => {
const decl = validDeclarationPath(tmpDir, 'c-decl.json');
const out = path.join(tmpDir, 'c-out.cjs');
assert.equal(runGen(['--write', '--declaration', decl, '--out', out]).exitCode, 0);
fs.appendFileSync(out, '\n// hand-edited, drifts from generated content\n');
const { result, report } = runGenJson(['--check', '--declaration', decl, '--out', out]);
assert.equal(result.exitCode, 1);
assert.equal(report.ok, false);
assert.equal(report.reason, generator.REASON.DRIFTED);
});
// Review finding (#3906 follow-up): the ambient .d.cts was hand-maintained
// with no gate verifying it against serializeRegistry()'s actual shape.
// These pin the SAME write/check/drift contract already proven for the two
// .cjs artifacts above, but for the .d.cts specifically.
test('--write emits a matching .d.cts artifact', () => {
const decl = validDeclarationPath(tmpDir, 'dts-decl.json');
const out = path.join(tmpDir, 'dts-out.cjs');
const dtsOut = path.join(tmpDir, 'dts-out.d.cts');
const write = runGen(['--write', '--declaration', decl, '--out', out, '--dts-out', dtsOut]);
assert.equal(write.exitCode, 0, write.stderr);
assert.ok(fs.existsSync(dtsOut), 'expected the .d.cts artifact to be written');
const dtsContent = fs.readFileSync(dtsOut, 'utf8');
assert.ok(dtsContent.includes('export interface ExitCodeEntry'));
assert.ok(dtsContent.includes('export = exitCodeRegistry;'));
const check = runGen(['--check', '--declaration', decl, '--out', out, '--dts-out', dtsOut]);
assert.equal(check.exitCode, 0, check.stderr);
});
test('--check on a hand-edited .d.cts artifact -> DRIFTED', () => {
const decl = validDeclarationPath(tmpDir, 'dts-drift-decl.json');
const out = path.join(tmpDir, 'dts-drift-out.cjs');
const dtsOut = path.join(tmpDir, 'dts-drift-out.d.cts');
assert.equal(runGen(['--write', '--declaration', decl, '--out', out, '--dts-out', dtsOut]).exitCode, 0);
fs.appendFileSync(dtsOut, '\n// hand-edited, drifts from generated content\n');
const { result, report } = runGenJson(['--check', '--declaration', decl, '--out', out, '--dts-out', dtsOut]);
assert.equal(result.exitCode, 1);
assert.equal(report.ok, false);
assert.equal(report.reason, generator.REASON.DRIFTED);
assert.equal(report.context.artifact, 'dts');
});
test('--check with the .d.cts artifact absent -> MISSING_ARTIFACT', () => {
const decl = validDeclarationPath(tmpDir, 'dts-missing-decl.json');
const out = path.join(tmpDir, 'dts-missing-out.cjs');
const dtsOut = path.join(tmpDir, 'dts-missing-out.d.cts');
const { result, report } = runGenJson(['--check', '--declaration', decl, '--out', out, '--dts-out', dtsOut]);
assert.equal(result.exitCode, 1);
assert.equal(report.ok, false);
assert.equal(report.reason, generator.REASON.MISSING_ARTIFACT);
assert.equal(fs.existsSync(dtsOut), false);
});
test('--check with a stale artifact (declaration changed after write) -> DRIFTED', () => {
const decl = validDeclarationPath(tmpDir, 'd-decl.json');
const out = path.join(tmpDir, 'd-out.cjs');
assert.equal(runGen(['--write', '--declaration', decl, '--out', out]).exitCode, 0);
const entries = JSON.parse(fs.readFileSync(decl, 'utf8'));
// 81, not 80: the real declaration already allocates 80 to DEGRADED, and
// this fixture copies the REAL declaration (validDeclarationPath) — an
// appended entry must pick a code neither of the two committed entries
// already own, or the generator correctly reports fail_duplicate_code
// instead of the DRIFTED this test means to exercise.
entries.push({ code: 81, name: 'DOMAIN_X', meaning: 'm', owner: 'domain-x', authorizedBy: 'ADR-3889' });
fs.writeFileSync(decl, JSON.stringify(entries, null, 2), 'utf8');
const { result, report } = runGenJson(['--check', '--declaration', decl, '--out', out]);
assert.equal(result.exitCode, 1);
assert.equal(report.ok, false);
assert.equal(report.reason, generator.REASON.DRIFTED);
});
test('--check with the artifact absent -> MISSING_ARTIFACT', () => {
const decl = validDeclarationPath(tmpDir, 'e-decl.json');
const out = path.join(tmpDir, 'e-out-absent.cjs');
const { result, report } = runGenJson(['--check', '--declaration', decl, '--out', out]);
assert.equal(result.exitCode, 1);
assert.equal(report.ok, false);
assert.equal(report.reason, generator.REASON.MISSING_ARTIFACT);
assert.equal(fs.existsSync(out), false);
});
test('unknown flag -> USAGE, exit 1, artifact unchanged on disk', () => {
const decl = validDeclarationPath(tmpDir, 'f-decl.json');
const out = path.join(tmpDir, 'f-out.cjs');
assert.equal(runGen(['--write', '--declaration', decl, '--out', out]).exitCode, 0);
const before = fs.readFileSync(out, 'utf8');
const { result, report } = runGenJson(['--bogus-flag', '--declaration', decl, '--out', out]);
assert.equal(result.exitCode, 1);
assert.equal(report.ok, false);
assert.equal(report.reason, generator.REASON.USAGE);
const after = fs.readFileSync(out, 'utf8');
assert.equal(after, before);
});
test('second positional argument -> USAGE', () => {
const decl = validDeclarationPath(tmpDir, 'g-decl.json');
const out = path.join(tmpDir, 'g-out.cjs');
const { result, report } = runGenJson(['--check', 'extra-positional', '--declaration', decl, '--out', out]);
assert.equal(result.exitCode, 1);
assert.equal(report.ok, false);
assert.equal(report.reason, generator.REASON.USAGE);
});
test('missing declaration -> MISSING_DECLARATION, exit 1', () => {
const decl = path.join(tmpDir, 'does-not-exist-h.json');
const out = path.join(tmpDir, 'h-out.cjs');
const { result, report } = runGenJson(['--write', '--declaration', decl, '--out', out]);
assert.equal(result.exitCode, 1);
assert.equal(report.ok, false);
assert.equal(report.reason, generator.REASON.MISSING_DECLARATION);
});
test('malformed JSON declaration -> MALFORMED_DECLARATION, exit 1', () => {
const decl = path.join(tmpDir, 'i-decl.json');
fs.writeFileSync(decl, '{ not json', 'utf8');
const out = path.join(tmpDir, 'i-out.cjs');
const { result, report } = runGenJson(['--write', '--declaration', decl, '--out', out]);
assert.equal(result.exitCode, 1);
assert.equal(report.ok, false);
assert.equal(report.reason, generator.REASON.MALFORMED_DECLARATION);
});
test('valid JSON, not an array -> NOT_AN_ARRAY, exit 1', () => {
const decl = path.join(tmpDir, 'j-decl.json');
fs.writeFileSync(decl, '{}', 'utf8');
const out = path.join(tmpDir, 'j-out.cjs');
const { result, report } = runGenJson(['--write', '--declaration', decl, '--out', out]);
assert.equal(result.exitCode, 1);
assert.equal(report.ok, false);
assert.equal(report.reason, generator.REASON.NOT_AN_ARRAY);
});
test('empty array declaration -> EMPTY_DECLARATION, exit 1', () => {
const decl = path.join(tmpDir, 'k-decl.json');
fs.writeFileSync(decl, '[]', 'utf8');
const out = path.join(tmpDir, 'k-out.cjs');
const { result, report } = runGenJson(['--write', '--declaration', decl, '--out', out]);
assert.equal(result.exitCode, 1);
assert.equal(report.ok, false);
assert.equal(report.reason, generator.REASON.EMPTY_DECLARATION);
});
// Regression (#3911 follow-up): ensureScriptsOut derived --scripts-out/
// --dts-out/--sh-out from --out but did not derive --hooks-out, so any
// --write test here silently clobbered the real committed
// hooks/lib/exit-code-registry.js. Assert over ALL FIVE committed
// artifacts so the next added target is covered by construction.
test('a --write run redirected to a tmpdir leaves every committed artifact untouched', () => {
const before = {
out: fs.readFileSync(REAL_ARTIFACT_PATH, 'utf8'),
scripts: fs.readFileSync(REAL_SCRIPTS_ARTIFACT_PATH, 'utf8'),
hooks: fs.readFileSync(REAL_HOOKS_ARTIFACT_PATH, 'utf8'),
dts: fs.readFileSync(REAL_DTS_ARTIFACT_PATH, 'utf8'),
sh: fs.readFileSync(REAL_SH_ARTIFACT_PATH, 'utf8'),
};
const decl = validDeclarationPath(tmpDir, 'l-decl.json');
const out = path.join(tmpDir, 'l-out.cjs');
const write = runGen(['--write', '--declaration', decl, '--out', out]);
assert.equal(write.exitCode, 0, write.stderr);
assert.equal(fs.readFileSync(REAL_ARTIFACT_PATH, 'utf8'), before.out, 'primary artifact must be untouched');
assert.equal(fs.readFileSync(REAL_SCRIPTS_ARTIFACT_PATH, 'utf8'), before.scripts, 'scripts artifact must be untouched');
assert.equal(fs.readFileSync(REAL_HOOKS_ARTIFACT_PATH, 'utf8'), before.hooks, 'hooks artifact must be untouched');
assert.equal(fs.readFileSync(REAL_DTS_ARTIFACT_PATH, 'utf8'), before.dts, '.d.cts artifact must be untouched');
assert.equal(fs.readFileSync(REAL_SH_ARTIFACT_PATH, 'utf8'), before.sh, '.sh artifact must be untouched');
assert.ok(fs.existsSync(`${out}.hooks.js`), 'expected the redirected hooks copy to land in the tmpdir');
assert.ok(fs.existsSync(`${out}.secondary.cjs`), 'expected the redirected scripts copy to land in the tmpdir');
assert.ok(fs.existsSync(`${out}.d.cts`), 'expected the redirected .d.cts copy to land in the tmpdir');
assert.ok(fs.existsSync(`${out}.sh`), 'expected the redirected .sh copy to land in the tmpdir');
});
});
// ── Generator CLI: positive controls (each guard actually FAILS the build) ────
describe('gen-exit-code-registry: CLI positive controls for the ten guard rows', () => {
let tmpDir;
before(() => {
tmpDir = createTempDir('gsd-exit-code-positive-');
});
after(() => {
cleanup(tmpDir);
});
function writeFixture(name, entries) {
const decl = path.join(tmpDir, `${name}.json`);
fs.writeFileSync(decl, JSON.stringify(entries, null, 2), 'utf8');
return decl;
}
const validBase = () => ({ meaning: 'm', owner: 'generic', authorizedBy: 'ADR-3889' });
const rows = [
['duplicate code', () => [
{ ...validBase(), code: 64, name: 'DUP_A' },
{ ...validBase(), code: 64, name: 'DUP_B' },
], 'DUPLICATE_CODE'],
['duplicate name', () => [
{ ...validBase(), code: 64, name: 'SAME' },
{ ...validBase(), code: 70, name: 'SAME' },
], 'DUPLICATE_NAME'],
['code 2 wrong owner', () => [
{ ...validBase(), code: 2, name: 'HOOK_DENY', owner: 'not-hook-adapter' },
], 'FORBIDDEN_OWNER'],
['code 0', () => [{ ...validBase(), code: 0, name: 'ZERO' }], 'RESERVED_CODE'],
['code 13', () => [{ ...validBase(), code: 13, name: 'THIRTEEN' }], 'RESERVED_CODE'],
['code 79', () => [{ ...validBase(), code: 79, name: 'SEVENTYNINE' }], 'RESERVED_CODE'],
['code 126', () => [{ ...validBase(), code: 126, name: 'ONETWENTYSIX' }], 'RESERVED_CODE'],
['code "64" (string)', () => [{ ...validBase(), code: '64', name: 'STRCODE' }], 'INVALID_ENTRY'],
['missing meaning', () => [{ code: 64, name: 'NO_MEANING', owner: 'generic', authorizedBy: 'ADR-3889' }], 'INVALID_ENTRY'],
['[] empty declaration', () => [], 'EMPTY_DECLARATION'],
// F3a: a `meaning` carrying a `|` or a newline must be REJECTED with a non-zero exit —
// failing-first against the pre-fix validator (#3913 P9 SEC-3).
['meaning with a pipe', () => [{ ...validBase(), code: 64, name: 'PIPE_MEANING', meaning: 'a | pipe breaks the table' }], 'INVALID_CHARACTERS'],
['meaning with a newline', () => [{ ...validBase(), code: 64, name: 'NEWLINE_MEANING', meaning: 'a\nforged heading' }], 'INVALID_CHARACTERS'],
];
for (const [label, buildEntries, expectedReasonKey] of rows) {
test(`${label} -> ${expectedReasonKey}, exit 1`, () => {
const decl = writeFixture(label.replace(/[^a-z0-9]+/gi, '-'), buildEntries());
const out = path.join(tmpDir, `${label.replace(/[^a-z0-9]+/gi, '-')}-out.cjs`);
const { result, report } = runGenJson(['--write', '--declaration', decl, '--out', out]);
assert.equal(result.exitCode, 1, `expected exit 1 for ${label}, got stderr: ${result.stderr}`);
assert.equal(report.ok, false);
assert.equal(report.reason, generator.REASON[expectedReasonKey]);
});
}
test('duplicate-code fixture: --json payload carries a structured context, not just the reason', () => {
const decl = writeFixture('json-duplicate-code', [
{ ...validBase(), code: 64, name: 'DUP_A' },
{ ...validBase(), code: 64, name: 'DUP_B' },
]);
const out = path.join(tmpDir, 'json-duplicate-code-out.cjs');
const { result, report } = runGenJson(['--write', '--declaration', decl, '--out', out]);
assert.equal(result.exitCode, 1, result.stderr);
assert.equal(report.ok, false);
assert.equal(report.reason, generator.REASON.DUPLICATE_CODE);
// A --json consumer must be able to learn WHICH code collided and WHICH
// names collided without parsing the `detail` prose string.
assert.deepEqual(report.context, { code: 64, names: ['DUP_A', 'DUP_B'] });
});
});
// ── gen-exit-code-docs.cjs: docs/reference/exit-codes.md (P9, #3913, matrix F) ──
describe('gen-exit-code-docs: generated exit-code reference page (matrix F1-F4)', () => {
const DOCS_GEN_SCRIPT = path.join(REPO_ROOT, 'scripts', 'gen-exit-code-docs.cjs');
const REAL_DOC_PATH = path.join(REPO_ROOT, 'docs', 'reference', 'exit-codes.md');
const README_PATH = path.join(REPO_ROOT, 'docs', 'README.md');
const docsGenerator = require(DOCS_GEN_SCRIPT);
function runDocsGen(args, opts = {}) {
return runNode([DOCS_GEN_SCRIPT, ...args], { timeoutMs: PROBE_TIMEOUT_MS, ...opts });
}
// F1: every code in the registry appears in the generated page with its
// name — asserted over the ENUMERATED registry, so a newly allocated code
// fails until documented.
test('F1: every registered code appears in the generated page with its name', () => {
const md = fs.readFileSync(REAL_DOC_PATH, 'utf8');
assert.ok(registry.EXIT_CODES.length > 0, 'precondition: the registry is non-empty');
for (const entry of registry.EXIT_CODES) {
const row = md.split(/\r?\n/).find((l) => l.startsWith(`| ${entry.code} |`));
assert.ok(row, `code ${entry.code} (${entry.name}) must appear as a row in the generated page`);
assert.ok(row.includes(`\`${entry.name}\``), `code ${entry.code}'s row must carry its name ${entry.name}`);
}
});
// F2: `--check` exits non-zero when the committed page diverges from a
// fresh render. Proven by mutating a COPY (via --declaration/--out
// redirection to a tmpdir, never the real committed file — test files in
// this repo run in parallel) and observing the non-zero exit — not by
// reading the code.
describe('F2: --check catches drift (mutate-then-observe, not read-the-code)', () => {
let tmpDir;
before(() => {
tmpDir = createTempDir('gsd-exit-code-docs-f2-');
});
after(() => {
cleanup(tmpDir);
});
test('a freshly generated copy of the real committed page passes --check', () => {
const decl = path.join(tmpDir, 'decl.json');
fs.copyFileSync(REAL_DECLARATION_PATH, decl);
const out = path.join(tmpDir, 'exit-codes.md');
const write = runDocsGen(['--write', '--declaration', decl, '--out', out]);
assert.equal(write.exitCode, 0, write.stderr);
const check = runDocsGen(['--check', '--declaration', decl, '--out', out]);
assert.equal(check.exitCode, 0, check.stderr);
});
test('mutating the generated page then running --check exits non-zero', () => {
const decl = path.join(tmpDir, 'decl-mutate.json');
fs.copyFileSync(REAL_DECLARATION_PATH, decl);
const out = path.join(tmpDir, 'exit-codes-mutate.md');
assert.equal(runDocsGen(['--write', '--declaration', decl, '--out', out]).exitCode, 0);
// Mutate the generated copy — e.g. a hand-edit drifting from the
// generator's own output — then observe the ACTUAL exit code.
fs.appendFileSync(out, '\n<!-- hand-edited, drifts from generated content -->\n');
const check = runDocsGen(['--check', '--declaration', decl, '--out', out]);
assert.notEqual(check.exitCode, 0, 'a drifted page must fail --check, not pass it');
});
});
// F3: `--check` exits 0 on the committed tree (the generator is idempotent).
test('F3: --check exits 0 against the real committed page', () => {
const result = runDocsGen(['--check']);
assert.equal(result.exitCode, 0, result.stderr);
});
// F4: the page is reachable from docs/README.md.
test('F4: docs/README.md links to the generated exit-code reference page', () => {
const readme = fs.readFileSync(README_PATH, 'utf8');
assert.ok(readme.includes('reference/exit-codes.md'), 'docs/README.md must index docs/reference/exit-codes.md');
});
// Real parity assertion (replaces a near-tautological pair of `.includes()`
// checks — `md.includes('3')` matches "ADR-3889", not the Node-reserved
// band). Enumerates the FULL scanned code space and asserts the rendered
// "Reserved bands" table's ranges and row grouping are exactly what
// computeBandRanges/classifyBand — themselves composed only from
// isAllocatableCode/bandFor — return today. This fails the moment the
// generator's band table is re-hardcoded as a literal that stops tracking
// gen-exit-code-registry.cjs's own band logic.
test('parity: every rendered band range and status is DERIVED from isAllocatableCode/bandFor, not retyped', () => {
const md = fs.readFileSync(REAL_DOC_PATH, 'utf8');
const ranges = generator.computeBandRanges(500);
for (const { category, ranges: subRanges } of ranges) {
for (const range of subRanges) {
// Every individual code in every derived range must actually
// classify into that category right now — i.e. the derivation is
// self-consistent over the enumerated space, not just internally
// coherent by construction.
for (let code = range.start; code <= range.end; code += 1) {
assert.equal(generator.classifyBand(code), category, `code ${code} must classify as ${category}`);
}
// And the rendered page must actually contain a band-table token
// for this range's boundary (its start or its formatted label),
// so a hand-edited/stale table (the #1 defect: a literal that
// never changed when the band logic did) is caught here too.
const label = range.openEnded ? `${range.start}+` : (range.start === range.end ? `${range.start}` : `${range.start}\`–\`${range.end}`);
assert.ok(md.includes(`\`${label}\``), `rendered page must contain the derived band label for ${category}: \`${label}\``);
}
}
});
// Regression for #1: proves the band table is genuinely DERIVED from
// isAllocatableCode/bandFor rather than a second hand-typed literal. Widens
// the band logic in a SCRATCH COPY of both generator scripts (never the
// real committed files) so that 14-63 becomes allocatable, then runs
// `--check` against the REAL committed page with that widened logic. If
// the band table were still hand-typed (the pre-fix defect), --check would
// stay green because the template string never changed; with the fix, the
// freshly-rendered table for the widened logic diverges from the committed
// page's band table and --check must exit non-zero.
test('band table is DERIVED: widening isAllocatableCode without regenerating fails --check (regression, #1)', () => {
const tmp = createTempDir('gsd-exit-code-docs-band-parity-');
try {
const registrySrc = fs.readFileSync(GEN_SCRIPT, 'utf8');
// Widen the SAME BANDS table isAllocatableCode/bandFor/classifyBand are
// all derived from — this is the realistic "someone widens a band"
// edit the reviewer demonstrated, not a change to a derived function.
const NEEDLE = "{ category: 'generic', allocatable: true, test: (code) => code >= 64 && code <= 78 }";
assert.ok(registrySrc.includes(NEEDLE), 'precondition: gen-exit-code-registry.cjs must still contain the BANDS entry this test widens');
const widened = registrySrc.replace(
NEEDLE,
"{ category: 'generic', allocatable: true, test: (code) => (code >= 64 && code <= 78) || (code >= 14 && code <= 63) }",
);
assert.notEqual(widened, registrySrc, 'precondition: the widen replacement must actually change the source');
fs.writeFileSync(path.join(tmp, 'gen-exit-code-registry.cjs'), widened, 'utf8');
const docsSrc = fs.readFileSync(DOCS_GEN_SCRIPT, 'utf8');
fs.writeFileSync(path.join(tmp, 'gen-exit-code-docs.cjs'), docsSrc, 'utf8');
fs.mkdirSync(path.join(tmp, 'lib'), { recursive: true });
fs.copyFileSync(path.join(REPO_ROOT, 'scripts', 'lib', 'cli-exit.cjs'), path.join(tmp, 'lib', 'cli-exit.cjs'));
const check = runNode([path.join(tmp, 'gen-exit-code-docs.cjs'), '--check'], { timeoutMs: PROBE_TIMEOUT_MS });
assert.notEqual(check.exitCode, 0, 'a widened band (14-63 admitted) must invalidate the committed page — if this passes, the band table is a hand-typed literal again, not derived from isAllocatableCode/bandFor');
} finally {
cleanup(tmp);
}
});
// T1 (#3913 P9 review follow-up): a BANDS category present in neither
// CATEGORY_ROW_ORDER nor CATEGORY_MERGE_INTO must make generation THROW —
// not silently omit the new band from the rendered table while --check
// stays green (the exact defect assertBandCategoriesConsistent in
// gen-exit-code-docs.cjs closes). Driven via a scratch copy of both
// generator scripts, mirroring the widen-a-band regression test above —
// never the real committed files.
// Every T1-T3 scratch copy needs the same three sibling files
// gen-exit-code-docs.cjs's own `require`s resolve relative to itself:
// gen-exit-code-registry.cjs (source of BANDS), lib/cli-exit.cjs, and
// lib/exit-code-registry.cjs (cli-exit.cjs's own dependency). Loading the
// scratch module directly (never via `--check`/`--write`) and calling
// `loadEntries`/`buildDoc` against the REAL committed declaration isolates
// the assertion under test from the doc-page-drift machinery entirely.
function scaffoldScratchDocsModule(tmp, docsSrc) {
fs.mkdirSync(path.join(tmp, 'lib'), { recursive: true });
fs.copyFileSync(path.join(REPO_ROOT, 'scripts', 'lib', 'cli-exit.cjs'), path.join(tmp, 'lib', 'cli-exit.cjs'));
fs.copyFileSync(path.join(REPO_ROOT, 'scripts', 'lib', 'exit-code-registry.cjs'), path.join(tmp, 'lib', 'exit-code-registry.cjs'));
const entryPath = path.join(tmp, 'docsgen-entry.cjs');
fs.writeFileSync(entryPath, docsSrc, 'utf8');
return entryPath;
}
test('T1: a BANDS category with no CATEGORY_ROW_ORDER/CATEGORY_MERGE_INTO entry throws generation, not a silent omission', () => {
const tmp = createTempDir('gsd-exit-code-docs-p9cat-');
try {
const registrySrc = fs.readFileSync(GEN_SCRIPT, 'utf8');
const NEEDLE = " { category: 'shell-signal', allocatable: false, test: (code) => code >= 126 },\n]);";
assert.ok(registrySrc.includes(NEEDLE), 'precondition: BANDS closing entry must still match');
const mutated = registrySrc.replace(
NEEDLE,
" { category: 'shell-signal', allocatable: false, test: (code) => code >= 126 },\n"
+ " { category: 'brand-new-band', allocatable: true, test: (code) => code === 40 },\n]);",
);
assert.notEqual(mutated, registrySrc, 'precondition: the BANDS injection must actually change the source');
fs.writeFileSync(path.join(tmp, 'gen-exit-code-registry.cjs'), mutated, 'utf8');
const entryPath = scaffoldScratchDocsModule(tmp, fs.readFileSync(DOCS_GEN_SCRIPT, 'utf8'));
const scratchDocs = require(entryPath);
const entries = scratchDocs.loadEntries(REAL_DECLARATION_PATH);
// Pre-fix (RED): buildDoc succeeds and silently omits the new band —
// it never appears anywhere in the rendered table. Post-fix (GREEN):
// buildDoc throws naming the unaccounted category.
assert.throws(
() => scratchDocs.buildDoc(entries),
/fail_band_category_unaccounted:.*brand-new-band/,
'an unaccounted BANDS category must fail generation loudly, not render a table that silently omits it',
);
} finally {
cleanup(tmp);
}
});
// T2: a CATEGORY_ROW_ORDER entry with no BAND_PROSE entry must throw rather
// than rendering the literal string "undefined" into the table.
test('T2: a CATEGORY_ROW_ORDER entry with no BAND_PROSE entry throws rather than rendering undefined', () => {
const tmp = createTempDir('gsd-exit-code-docs-p9cat-');
try {
fs.copyFileSync(GEN_SCRIPT, path.join(tmp, 'gen-exit-code-registry.cjs'));
const docsSrc = fs.readFileSync(DOCS_GEN_SCRIPT, 'utf8');
const NEEDLE = " domain: '**Domain band.**";
assert.ok(docsSrc.includes(NEEDLE), 'precondition: BAND_PROSE.domain entry must still match');
// Delete the `domain` prose entry entirely while leaving `domain` in
// CATEGORY_ROW_ORDER — the exact stale-in-one-list-not-the-other shape.
// Line-filtered via splitLines (not a bare-`\n` regex) so this stays
// correct under Windows git-autocrlf CRLF line endings too.
const mutated = splitLines(docsSrc).filter((line) => !line.startsWith(NEEDLE)).join('\n');
assert.notEqual(mutated, docsSrc, 'precondition: the BAND_PROSE deletion must actually change the source');
assert.ok(!mutated.includes("domain: '**Domain band.**"), 'precondition: BAND_PROSE.domain must actually be gone');
const entryPath = scaffoldScratchDocsModule(tmp, mutated);
const scratchDocs = require(entryPath);
const entries = scratchDocs.loadEntries(REAL_DECLARATION_PATH);
assert.throws(
() => scratchDocs.buildDoc(entries),
/fail_band_prose_missing:.*domain/,
'a CATEGORY_ROW_ORDER entry missing from BAND_PROSE must fail generation loudly, not render "undefined"',
);
} finally {
cleanup(tmp);
}
});
// T3: a stale BAND_PROSE category that no BANDS entry (directly, or via
// CATEGORY_MERGE_INTO) produces must throw — dead prose for a band that no
// longer exists is the same drift in the other direction.
test('T3: a stale BAND_PROSE category no BANDS entry produces throws', () => {
const tmp = createTempDir('gsd-exit-code-docs-p9cat-');
try {
fs.copyFileSync(GEN_SCRIPT, path.join(tmp, 'gen-exit-code-registry.cjs'));
const docsSrc = fs.readFileSync(DOCS_GEN_SCRIPT, 'utf8');
const NEEDLE = "const BAND_PROSE = Object.freeze({\n";
assert.ok(docsSrc.includes(NEEDLE), 'precondition: BAND_PROSE opening must still match');
const mutated = docsSrc.replace(
NEEDLE,
`${NEEDLE} 'long-retired-band': 'This band was retired and no BANDS entry produces it any more.',\n`,
);
assert.notEqual(mutated, docsSrc, 'precondition: the stale BAND_PROSE injection must actually change the source');
const entryPath = scaffoldScratchDocsModule(tmp, mutated);
const scratchDocs = require(entryPath);
const entries = scratchDocs.loadEntries(REAL_DECLARATION_PATH);
assert.throws(
() => scratchDocs.buildDoc(entries),
/fail_band_category_stale:.*long-retired-band/,
'a stale BAND_PROSE category with no producing BANDS entry must fail generation loudly',
);
} finally {
cleanup(tmp);
}
});
// T4 (positive control): the REAL, unmodified configuration renders all six
// rows with no literal "undefined" anywhere in the page. Without this, a
// fix that throws unconditionally (rather than only on genuine drift) would
// still pass T1-T3 by accident.
test('T4: the real unmodified configuration renders all six band rows with no "undefined" in the page', () => {
const result = runDocsGen(['--check']);
assert.equal(result.exitCode, 0, result.stderr);
const md = fs.readFileSync(REAL_DOC_PATH, 'utf8');
assert.ok(!md.includes('undefined'), 'the generated page must never contain the literal string "undefined"');
const bandTableSection = md.slice(md.indexOf('| Band | Meaning |'), md.indexOf('## The v1/v2 exit contract'));
// `|---|---|` starts with `|-`, not `| `, so it is already excluded by
// this pattern — only the header line (`| Band | Meaning |`) needs
// subtracting to leave just the data rows.
const rowCount = (bandTableSection.match(/^\| /gm) || []).length - 1;
assert.equal(rowCount, 6, 'the Reserved bands table must render exactly six rows (free, hook-only, node-reserved, outside-every-band, generic, domain)');
});
// Forward guard, not a regression test: buildDoc has no source of
// non-determinism (no Date.now/Math.random/env read), so this cannot
// currently fail — it exists to catch a FUTURE change that introduces one.
test('forward guard: buildDoc stays pure if a future change adds a non-deterministic input', () => {
const once = docsGenerator.buildDoc(registry.EXIT_CODES);
const twice = docsGenerator.buildDoc(registry.EXIT_CODES);
assert.equal(once, twice);
});
});
// ── fast-check properties ─────────────────────────────────────────────────────
describe('exit-code-registry: fast-check properties', () => {
test('nameForExitCode(exitCodeFor(name)) round-trips for every registered name', () => {
fc.assert(
fc.property(fc.constantFrom(...registry.EXIT_CODES.map((e) => e.name)), (name) => {
assert.equal(registry.nameForExitCode(registry.exitCodeFor(name)), name);
}),
{ seed: 2704, numRuns: 200 },
);
});
test('exitCodeFor(nameForExitCode(code)) round-trips for every registered code', () => {
fc.assert(
fc.property(fc.constantFrom(...registry.EXIT_CODES.map((e) => e.code)), (code) => {
assert.equal(registry.exitCodeFor(registry.nameForExitCode(code)), code);
}),
{ seed: 2704, numRuns: 200 },
);
});
test('exitCodeFor never resolves a code for an unregistered string', () => {
fc.assert(
fc.property(fc.string(), (s) => {
fc.pre(!REGISTERED_NAMES.has(s));
assert.throws(() => registry.exitCodeFor(s));
}),
{ seed: 2704, numRuns: 200 },
);
});
// Unlike the two round-trip properties above (which replay only the 5
// shipped constants), this one explores the full integer domain —
// negatives, every band boundary, and values far outside every band —
// rather than a closed set of examples.
test('nameForExitCode(c) either throws or returns a name that round-trips to c, for any integer c', () => {
fc.assert(
fc.property(fc.integer(), (c) => {
let name;
try {
name = registry.nameForExitCode(c);
} catch {
return; // throwing for an unregistered code is a legal outcome
}
assert.notEqual(name, undefined);
assert.equal(registry.exitCodeFor(name), c);
}),
{ seed: 2704, numRuns: 200 },
);
});
});