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
This commit is contained in:
@@ -29,6 +29,7 @@ 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');
|
||||
@@ -205,6 +206,7 @@ describe('gen-exit-code-registry: REASON', () => {
|
||||
'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', () => {
|
||||
@@ -302,6 +304,49 @@ describe('gen-exit-code-registry: required string fields', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// #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 = [
|
||||
@@ -639,6 +684,10 @@ describe('gen-exit-code-registry: CLI positive controls for the ten guard rows',
|
||||
['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) {
|
||||
@@ -668,6 +717,291 @@ describe('gen-exit-code-registry: CLI positive controls for the ten guard rows',
|
||||
});
|
||||
});
|
||||
|
||||
// ── 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', () => {
|
||||
|
||||
Reference in New Issue
Block a user