diff --git a/scripts/ablation-dist-preflight.mjs b/scripts/ablation-dist-preflight.mjs index 08abad688f..c9ad125b20 100644 --- a/scripts/ablation-dist-preflight.mjs +++ b/scripts/ablation-dist-preflight.mjs @@ -551,6 +551,12 @@ function run(argv) { if (!v.ok || !tv.ok) process.exit(1); } +// Returned by `selfTest()` only after its verdict is printed. The dispatch +// refuses anything else: a `return` that leaves the function above that line +// prints nothing and still exits 0 — a self-test that never finished, reported +// as one that passed (#13798). +const SELF_TEST_VERDICT = 'ablation-dist-preflight self-test reached its verdict'; + function selfTest() { const cases = [ ['missing dist is red', { mode: 'present', distExists: false, scanned: 0, codeHits: 0, mapHits: 0 }, false], @@ -742,6 +748,8 @@ function selfTest() { process.exit(1); } console.log('✓ ablation-dist-preflight self-test: all cases pass.'); + + return SELF_TEST_VERDICT; } const argv = process.argv.slice(2); @@ -750,5 +758,14 @@ const invokedDirectly = isEntrypoint(import.meta.url); if (!invokedDirectly) { // imported as a module — expose the exports and do nothing else -} else if (argv.includes('--self-test')) selfTest(); +} else if (argv.includes('--self-test')) { + if (selfTest() !== SELF_TEST_VERDICT) { + console.error( + '\n✗ ablation-dist-preflight self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } +} else run(argv); diff --git a/scripts/check-adr-0087-registration.mjs b/scripts/check-adr-0087-registration.mjs index 669b693772..bd098e5cd3 100644 --- a/scripts/check-adr-0087-registration.mjs +++ b/scripts/check-adr-0087-registration.mjs @@ -3224,6 +3224,12 @@ function auditStock(cwd, head) { // real commits would be testing an imitation of the code path that ships. // --------------------------------------------------------------------------- +// Returned by `selfTest()` only after its verdict is printed. The dispatch +// refuses anything else: a `return` that leaves the function above that line +// prints nothing and still exits 0 — a self-test that never finished, reported +// as one that passed (#13798). +const SELF_TEST_VERDICT = 'check-adr-0087-registration self-test reached its verdict'; + function selfTest() { const failures = []; let checked = 0; @@ -4898,6 +4904,8 @@ function selfTest() { process.exit(1); } console.log(`✓ check-adr-0087-registration --self-test: ${checked} assertions over real temp git repos (real scan()/assertInputs() path)`); + + return SELF_TEST_VERDICT; } // --------------------------------------------------------------------------- @@ -4922,7 +4930,14 @@ if (isEntrypoint(import.meta.url)) { }; if (argv.includes('--self-test')) { - selfTest(); + if (selfTest() !== SELF_TEST_VERDICT) { + console.error( + '\n✗ check-adr-0087-registration self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } } else if (argv.includes('--list')) { list(REPO_ROOT, 'HEAD'); } else if (argv.includes('--audit-stock')) { diff --git a/scripts/check-adr-anchors.mjs b/scripts/check-adr-anchors.mjs index 183105ad21..43d0d06ceb 100644 --- a/scripts/check-adr-anchors.mjs +++ b/scripts/check-adr-anchors.mjs @@ -953,7 +953,24 @@ function ambiguousAnchorRefs(anchorList, allowlist) { return hits; } -if (process.argv.includes('--self-test')) selfTest(); +// Set by `selfTest()` only after its verdict is printed, and read at the +// dispatch: a `return` that leaves the function above that line prints nothing +// and still exits 0 — a self-test that never finished, reported as one that +// passed (#13798). The self-test's own exit code stays load-bearing, so the +// handshake is a flag rather than a returned sentinel. +let selfTestReachedVerdict = false; + +if (process.argv.includes('--self-test')) { + selfTest(); + if (!selfTestReachedVerdict) { + console.error( + '\n✗ check-adr-anchors self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } +} let adrFiles; try { @@ -1909,5 +1926,6 @@ function selfTest() { `✓ check-adr-anchors --self-test: ${checked} assertions over the real auditAdrDirectory() / ` + 'auditCitedNumbers() / assembleAnchors() paths.', ); + selfTestReachedVerdict = true; process.exit(0); } diff --git a/scripts/check-adr-links.mjs b/scripts/check-adr-links.mjs index cc88723d47..88c0d140e0 100644 --- a/scripts/check-adr-links.mjs +++ b/scripts/check-adr-links.mjs @@ -365,6 +365,12 @@ function assert(cond, message) { } } +// Returned by `selfTest()` only after its verdict is printed. The dispatch +// refuses anything else: a `return` that leaves the function above that line +// prints nothing and still exits 0 — a self-test that never finished, reported +// as one that passed (#13798). +const SELF_TEST_VERDICT = 'check-adr-links self-test reached its verdict'; + function selfTest() { // 1. Discrimination: verbatim link shapes are invisible; prose ones are not. const doc = [ @@ -452,12 +458,23 @@ function selfTest() { ); console.log('✅ check-adr-links --self-test: discrimination, census, ADR-0046 pin and baseline staleness all verified'); + + return SELF_TEST_VERDICT; } /* Run only when invoked as a program. The extractor is exported so a future * caller (or a REPL session chasing a false positive) can import it without the * import itself sweeping the repo. */ if (isEntrypoint(import.meta.url)) { - if (process.argv.includes('--self-test')) selfTest(); + if (process.argv.includes('--self-test')) { + if (selfTest() !== SELF_TEST_VERDICT) { + console.error( + '\n✗ check-adr-links self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + } else runCheck(); } diff --git a/scripts/check-agent-model-declared.mjs b/scripts/check-agent-model-declared.mjs index 6f2ebea950..3084de0df3 100644 --- a/scripts/check-agent-model-declared.mjs +++ b/scripts/check-agent-model-declared.mjs @@ -325,6 +325,13 @@ function report(problems) { // Self-test — pins the RED paths so the gate cannot rot into a no-op. // --------------------------------------------------------------------------- +// Set by `selfTest()` only after its verdict is printed, and read at the +// dispatch: a `return` that leaves the function above that line prints nothing +// and still exits 0 — a self-test that never finished, reported as one that +// passed (#13798). The self-test's own exit code stays load-bearing, so the +// handshake is a flag rather than a returned sentinel. +let selfTestReachedVerdict = false; + function selfTest() { const fm = (modelLine) => `---\nname: demo\ndescription: >\n A demo agent.\n${modelLine ? `${modelLine}\n` : ''}---\n\nBody.\n`; @@ -538,12 +545,24 @@ function selfTest() { process.exit(1); } console.log(`\n✓ check-agent-model-declared self-test: ${cases.length} cases pass.`); + selfTestReachedVerdict = true; } // --------------------------------------------------------------------------- function main() { - if (process.argv.includes('--self-test')) return selfTest(); + if (process.argv.includes('--self-test')) { + const selfTestCode = selfTest(); + if (!selfTestReachedVerdict) { + console.error( + '\n✗ check-agent-model-declared self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + return selfTestCode; + } const { files, problems: layout } = readAgentFiles(); const { problems, results } = runAllChecks(files, INHERIT_JUSTIFIED); diff --git a/scripts/check-agent-test-spelling.mjs b/scripts/check-agent-test-spelling.mjs index 6daee8ae8e..b4993d562e 100644 --- a/scripts/check-agent-test-spelling.mjs +++ b/scripts/check-agent-test-spelling.mjs @@ -811,6 +811,13 @@ function baseFixtureFiles(extra = {}) { }; } +// Set by `selfTest()` only after its verdict is printed, and read at the +// dispatch: a `return` that leaves the function above that line prints nothing +// and still exits 0 — a self-test that never finished, reported as one that +// passed (#13798). The self-test's own exit code stays load-bearing, so the +// handshake is a flag rather than a returned sentinel. +let selfTestReachedVerdict = false; + function selfTest() { const failures = []; const t = (name, actual, expected) => { @@ -1014,12 +1021,24 @@ function selfTest() { return 1; } console.log('\n✓ check-agent-test-spelling --self-test: all cases pass'); + selfTestReachedVerdict = true; return 0; } if (isEntrypoint(import.meta.url)) { const argv = process.argv.slice(2); - if (argv.includes('--self-test')) process.exit(selfTest()); + if (argv.includes('--self-test')) { + const selfTestCode = selfTest(); + if (!selfTestReachedVerdict) { + console.error( + '\n✗ check-agent-test-spelling self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + process.exit(selfTestCode); + } else if (argv.includes('--list')) list(); else process.exit(run()); } diff --git a/scripts/check-aggregator-roster.mjs b/scripts/check-aggregator-roster.mjs index 1bd3a16dea..40c8b7cfc0 100644 --- a/scripts/check-aggregator-roster.mjs +++ b/scripts/check-aggregator-roster.mjs @@ -373,6 +373,12 @@ async function main() { // ── Self-test ─────────────────────────────────────────────────────────────── +// Returned by `selfTest()` only after its verdict is printed. The dispatch +// refuses anything else: a `return` that leaves the function above that line +// prints nothing and still exits 0 — a self-test that never finished, reported +// as one that passed (#13798). +const SELF_TEST_VERDICT = 'check-aggregator-roster self-test reached its verdict'; + async function selfTest() { const failures = []; let checked = 0; @@ -594,6 +600,8 @@ async function selfTest() { `(baseline + a dropped member and a phantom needs: entry for each of the ${REQUIRED_AGGREGATORS.length} required aggregators + ` + `six refusals + the --leg cross-check + the malformed-roster and laundered-non-member pins + the CI wiring).`, ); + + return SELF_TEST_VERDICT; } // The CLI dispatch is guarded so that IMPORTING this module is inert: `judge` @@ -601,6 +609,15 @@ async function selfTest() { // that ran its gate on import would silently judge THIS repo instead and print // a verdict about the wrong subject (`check:entry-guard`). if (isEntrypoint(import.meta.url)) { - if (process.argv.includes('--self-test')) await selfTest(); + if (process.argv.includes('--self-test')) { + if ((await selfTest()) !== SELF_TEST_VERDICT) { + console.error( + '\n✗ check-aggregator-roster self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + } else await main(); } diff --git a/scripts/check-auth-mount-ledger.mjs b/scripts/check-auth-mount-ledger.mjs index fa0d1fbd9b..c14f7cd83e 100644 --- a/scripts/check-auth-mount-ledger.mjs +++ b/scripts/check-auth-mount-ledger.mjs @@ -550,6 +550,13 @@ const REAL_NOTE = 'no SDK method builds this URL -- the sys_user unlock_user action posts it directly; ' + 'platform-admin gated (ADR-0068)'; +// Set by `selfTest()` only after its verdict is printed, and read at the +// dispatch: a `return` that leaves the function above that line prints nothing +// and still exits 0 — a self-test that never finished, reported as one that +// passed (#13798). The self-test's own exit code stays load-bearing, so the +// handshake is a flag rather than a returned sentinel. +let selfTestReachedVerdict = false; + function selfTest() { const fail = []; let cases = 0; @@ -820,6 +827,7 @@ function selfTest() { process.exit(1); } console.log(`check-auth-mount-ledger --self-test: ${cases} assertions OK (right boundary, lane exclusion, rationale, pending ratchet).`); + selfTestReachedVerdict = true; } // --------------------------------------------------------------------------- @@ -834,7 +842,18 @@ function refuse(why) { function main() { const argv = process.argv.slice(2); - if (argv.includes('--self-test')) return selfTest(); + if (argv.includes('--self-test')) { + const selfTestCode = selfTest(); + if (!selfTestReachedVerdict) { + console.error( + '\n✗ check-auth-mount-ledger self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + return selfTestCode; + } const mountAbs = join(ROOT, MOUNT_SOURCE); const ledgerAbs = join(ROOT, LEDGER_SOURCE); diff --git a/scripts/check-bash32-floor.mjs b/scripts/check-bash32-floor.mjs index a43639760b..29fc9cfccc 100644 --- a/scripts/check-bash32-floor.mjs +++ b/scripts/check-bash32-floor.mjs @@ -697,6 +697,13 @@ function fixtureRepo(files) { return dir; } +// Set by `selfTest()` only after its verdict is printed, and read at the +// dispatch: a `return` that leaves the function above that line prints nothing +// and still exits 0 — a self-test that never finished, reported as one that +// passed (#13798). The self-test's own exit code stays load-bearing, so the +// handshake is a flag rather than a returned sentinel. +let selfTestReachedVerdict = false; + function selfTest() { const SELF = fileURLToPath(import.meta.url); let failed = 0; @@ -1097,12 +1104,24 @@ function selfTest() { process.exit(1); } console.log(`\n✓ check-bash32-floor self-test: ${cases} cases pass.`); + selfTestReachedVerdict = true; } // --------------------------------------------------------------------------- function main() { - if (process.argv.includes('--self-test')) return selfTest(); + if (process.argv.includes('--self-test')) { + const selfTestCode = selfTest(); + if (!selfTestReachedVerdict) { + console.error( + '\n✗ check-bash32-floor self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + return selfTestCode; + } const rootFlag = process.argv.indexOf('--root'); const root = rootFlag === -1 ? REPO_ROOT : process.argv[rootFlag + 1]; diff --git a/scripts/check-changeset-no-major.mjs b/scripts/check-changeset-no-major.mjs index abc66b65a6..cd623e2fa7 100644 --- a/scripts/check-changeset-no-major.mjs +++ b/scripts/check-changeset-no-major.mjs @@ -791,6 +791,12 @@ function main(argv) { // ── Self-test ──────────────────────────────────────────────────────────────── +// Returned by `selfTest()` only after its verdict is printed. The dispatch +// refuses anything else: a `return` that leaves the function above that line +// prints nothing and still exits 0 — a self-test that never finished, reported +// as one that passed (#13798). +const SELF_TEST_VERDICT = 'check-changeset-no-major self-test reached its verdict'; + function selfTest() { const failures = []; let checked = 0; @@ -1508,6 +1514,8 @@ function selfTest() { `✓ check-changeset-no-major --self-test: ${checked} assertions ` + '(frontmatter dialects measured against @changesets/parse + the pre/exit exemption switch in both directions + the #7005 diff scoping over real temp git repos + the #4690 pins + the wiring).', ); + + return SELF_TEST_VERDICT; } // ── main ───────────────────────────────────────────────────────────────────── @@ -1515,7 +1523,14 @@ function selfTest() { const argv = process.argv.slice(2); if (argv.includes('--self-test')) { - selfTest(); + if (selfTest() !== SELF_TEST_VERDICT) { + console.error( + '\n✗ check-changeset-no-major self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } } else if (argv.includes('--list')) { list(); } else { diff --git a/scripts/check-ci-filter-parity.mjs b/scripts/check-ci-filter-parity.mjs index b73662d58c..4b43cad016 100644 --- a/scripts/check-ci-filter-parity.mjs +++ b/scripts/check-ci-filter-parity.mjs @@ -413,6 +413,13 @@ function list(root = REPO_ROOT, table = CROSS_PACKAGE_TEST_INPUTS) { // dispatch-gates.mjs, so only the joined value is pathy and it exists at runtime // alone. +// Set by `selfTest()` only after its verdict is printed, and read at the +// dispatch: a `return` that leaves the function above that line prints nothing +// and still exits 0 — a self-test that never finished, reported as one that +// passed (#13798). The self-test's own exit code stays load-bearing, so the +// handshake is a flag rather than a returned sentinel. +let selfTestReachedVerdict = false; + export async function selfTest() { /** A ci.yml source carrying the two scheduling lists, in the real shape. */ const REAL_TEST_IF = @@ -663,6 +670,7 @@ export async function selfTest() { `pre-#10015 rollback uncovering the ten it fixed plus #10848's one plus #10178's two plus #12201's one plus #12924's one, ` + `and the CI wiring read out of lint.yml.`, ); + selfTestReachedVerdict = true; return 0; } @@ -672,7 +680,18 @@ export async function selfTest() { // into an importer's stdout and hand it this gate's exit status // (`check:entry-guard`; the sibling gate's header records what that cost). if (isEntrypoint(import.meta.url)) { - if (process.argv.includes('--self-test')) process.exit(await selfTest()); + if (process.argv.includes('--self-test')) { + const selfTestCode = await selfTest(); + if (!selfTestReachedVerdict) { + console.error( + '\n✗ check-ci-filter-parity self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + process.exit(selfTestCode); + } if (process.argv.includes('--list')) process.exit(list()); process.exit(main()); } diff --git a/scripts/check-cli-command-ids.mjs b/scripts/check-cli-command-ids.mjs index 4d13f01994..4fa1892dad 100644 --- a/scripts/check-cli-command-ids.mjs +++ b/scripts/check-cli-command-ids.mjs @@ -444,6 +444,13 @@ function list() { return 0; } +// Set by `selfTest()` only after its verdict is printed, and read at the +// dispatch: a `return` that leaves the function above that line prints nothing +// and still exits 0 — a self-test that never finished, reported as one that +// passed (#13798). The self-test's own exit code stays load-bearing, so the +// handshake is a flag rather than a returned sentinel. +let selfTestReachedVerdict = false; + function selfTest() { const cases = []; const t = (name, ok, detail = '') => cases.push({ name, ok, detail }); @@ -609,12 +616,24 @@ function selfTest() { + 'all six measured noise shapes stay out on the delimiter rule alone; ' + 'the fixture ledger is scoped to file AND text; and the live repo returns a green verdict).', ); + selfTestReachedVerdict = true; return 0; } if (isEntrypoint(import.meta.url)) { const argv = process.argv.slice(2); - if (argv.includes('--self-test')) process.exit(selfTest()); + if (argv.includes('--self-test')) { + const selfTestCode = selfTest(); + if (!selfTestReachedVerdict) { + console.error( + '\n✗ check-cli-command-ids self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + process.exit(selfTestCode); + } else if (argv.includes('--list')) process.exit(list()); else process.exit(main()); } diff --git a/scripts/check-cli-test-child-env.mjs b/scripts/check-cli-test-child-env.mjs index 2210188b92..0f37839136 100644 --- a/scripts/check-cli-test-child-env.mjs +++ b/scripts/check-cli-test-child-env.mjs @@ -1640,6 +1640,14 @@ function auditRoot(root) { * because "exits non-zero" is the property being claimed and it is not * observable from inside the process making the claim. */ + +// Set by `selfTest()` only after its verdict is printed, and read at the +// dispatch: a `return` that leaves the function above that line prints nothing +// and still exits 0 — a self-test that never finished, reported as one that +// passed (#13798). The self-test's own exit code stays load-bearing, so the +// handshake is a flag rather than a returned sentinel. +let selfTestReachedVerdict = false; + export function selfTest() { const cases = []; const t = (name, ok, detail) => cases.push({ name, ok: Boolean(ok), detail }); @@ -2505,12 +2513,24 @@ export function selfTest() { + 'while the tsx shim and a file that only NAMES bin/run.js stay out of that population; ' + 'the ratchet fails in both directions; and all four refusals are paired with a tree that still returns a verdict).', ); + selfTestReachedVerdict = true; return 0; } if (isEntrypoint(import.meta.url)) { const argv = process.argv.slice(2); - if (argv.includes('--self-test')) process.exit(selfTest()); + if (argv.includes('--self-test')) { + const selfTestCode = selfTest(); + if (!selfTestReachedVerdict) { + console.error( + '\n✗ check-cli-test-child-env self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + process.exit(selfTestCode); + } else if (argv.includes('--list')) process.exit(list()); else if (argv.includes('--audit-root')) process.exit(auditRoot(argv[argv.indexOf('--audit-root') + 1])); else process.exit(main()); diff --git a/scripts/check-comment-mask-adoption.mjs b/scripts/check-comment-mask-adoption.mjs index 52d1b3abf4..57915ce9db 100644 --- a/scripts/check-comment-mask-adoption.mjs +++ b/scripts/check-comment-mask-adoption.mjs @@ -427,6 +427,13 @@ function list() { return 0; } +// Set by `selfTest()` only after its verdict is printed, and read at the +// dispatch: a `return` that leaves the function above that line prints nothing +// and still exits 0 — a self-test that never finished, reported as one that +// passed (#13798). The self-test's own exit code stays load-bearing, so the +// handshake is a flag rather than a returned sentinel. +let selfTestReachedVerdict = false; + export function selfTest() { let failures = 0; const t = (name, ok) => { @@ -503,10 +510,23 @@ const m = maskComments(src);`) === ''); shapesIn(readFileSync(join(REPO_ROOT, CANONICAL), 'utf8')).includes('scanner-decl')); console.log(`\n${failures === 0 ? 'PASS' : 'FAIL'} check-comment-mask-adoption --self-test (${failures} failure(s))`); + selfTestReachedVerdict = true; return failures === 0 ? 0 : 1; } if (isEntrypoint(import.meta.url)) { const argv = process.argv.slice(2); - process.exit(argv.includes('--self-test') ? selfTest() : argv.includes('--list') ? list() : main()); + if (argv.includes('--self-test')) { + const selfTestCode = selfTest(); + if (!selfTestReachedVerdict) { + console.error( + '\n✗ check-comment-mask-adoption self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + process.exit(selfTestCode); + } + process.exit(argv.includes('--list') ? list() : main()); } diff --git a/scripts/check-comment-mask-corpus.mjs b/scripts/check-comment-mask-corpus.mjs index a29ef21ced..e00d487875 100644 --- a/scripts/check-comment-mask-corpus.mjs +++ b/scripts/check-comment-mask-corpus.mjs @@ -476,6 +476,12 @@ async function runSelfTestCases(parse) { return { failures, cases }; } +// Returned by `selfTest()` only after its verdict is printed. The dispatch +// refuses anything else: a `return` that leaves the function above that line +// prints nothing and still exits 0 — a self-test that never finished, reported +// as one that passed (#13798). +const SELF_TEST_VERDICT = 'check-comment-mask-corpus self-test reached its verdict'; + export async function selfTest() { const parse = await loadParser(); const { failures, cases } = await runSelfTestCases(parse); @@ -485,10 +491,21 @@ export async function selfTest() { process.exit(EXIT_DISAGREEMENT); } console.log(`\nAll ${cases.length} self-test cases passed.`); + + return SELF_TEST_VERDICT; } if (isEntrypoint(import.meta.url)) { const argv = process.argv.slice(2); - if (argv.includes('--self-test')) await selfTest(); + if (argv.includes('--self-test')) { + if ((await selfTest()) !== SELF_TEST_VERDICT) { + console.error( + '\n✗ check-comment-mask-corpus self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + } else await main(argv); } diff --git a/scripts/check-console-injection.mjs b/scripts/check-console-injection.mjs index 9ba8aadcf7..76fa9489d7 100644 --- a/scripts/check-console-injection.mjs +++ b/scripts/check-console-injection.mjs @@ -508,6 +508,12 @@ function stampFor({ skew = true, freshWitness = FRESH, staleDetector = STALE } = }; } +// Returned by `selfTest()` only after its verdict is printed. The dispatch +// refuses anything else: a `return` that leaves the function above that line +// prints nothing and still exits 0 — a self-test that never finished, reported +// as one that passed (#13798). +const SELF_TEST_VERDICT = 'check-console-injection self-test reached its verdict'; + function selfTest() { const failures = []; let checked = 0; @@ -788,6 +794,8 @@ function selfTest() { process.exit(1); } console.log(`✓ check-console-injection --self-test: ${checked} assertions over real fixture trees (real evaluate() path)`); + + return SELF_TEST_VERDICT; } // ── entry point ────────────────────────────────────────────────────────────── @@ -805,7 +813,14 @@ const invokedDirectly = if (!invokedDirectly) { // imported as a module — expose evaluate() and do nothing else } else if (process.argv.includes('--self-test')) { - selfTest(); + if (selfTest() !== SELF_TEST_VERDICT) { + console.error( + '\n✗ check-console-injection self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } } else { const argOf = (flag, fallback) => { const i = process.argv.indexOf(flag); diff --git a/scripts/check-console-intercept-disarm.mjs b/scripts/check-console-intercept-disarm.mjs index ca57638fb8..631b0f8a60 100644 --- a/scripts/check-console-intercept-disarm.mjs +++ b/scripts/check-console-intercept-disarm.mjs @@ -361,6 +361,12 @@ const DISARMED = `import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { disableConsoleIntercept: true } }); `; +// Returned by `selfTest()` only after its verdict is printed. The dispatch +// refuses anything else: a `return` that leaves the function above that line +// prints nothing and still exits 0 — a self-test that never finished, reported +// as one that passed (#13798). +const SELF_TEST_VERDICT = 'check-console-intercept-disarm self-test reached its verdict'; + function selfTest() { const cases = [ { @@ -516,9 +522,20 @@ function selfTest() { if (failures > 0) process.exit(1); console.log(`self-test OK: ${cases.length} cases + real-tree population floor.`); + + return SELF_TEST_VERDICT; } if (isEntrypoint(import.meta.url)) { - if (process.argv.includes('--self-test')) selfTest(); + if (process.argv.includes('--self-test')) { + if (selfTest() !== SELF_TEST_VERDICT) { + console.error( + '\n✗ check-console-intercept-disarm self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + } else main(); } diff --git a/scripts/check-corpus-claim-drift.mjs b/scripts/check-corpus-claim-drift.mjs index 50d42816e4..5459313267 100644 --- a/scripts/check-corpus-claim-drift.mjs +++ b/scripts/check-corpus-claim-drift.mjs @@ -448,6 +448,13 @@ function ratchetRemedyCarriesAuthority(message) { return message.includes(RATCHET_AUTHORITY_MARKER); } +// Set by `selfTest()` only after its verdict is printed, and read at the +// dispatch: a `return` that leaves the function above that line prints nothing +// and still exits 0 — a self-test that never finished, reported as one that +// passed (#13798). The self-test's own exit code stays load-bearing, so the +// handshake is a flag rather than a returned sentinel. +let selfTestReachedVerdict = false; + function selfTest() { const failures = []; const expect = (label, cond) => { @@ -838,10 +845,21 @@ function selfTest() { + 'the ratchet-DOWN one is not, and the success body reports what was READ per root and what ' + 'each ROW reached — all of it also driven through a real child process.', ); + selfTestReachedVerdict = true; process.exit(0); } -if (process.argv.includes('--self-test')) selfTest(); +if (process.argv.includes('--self-test')) { + selfTest(); + if (!selfTestReachedVerdict) { + console.error( + '\n✗ check-corpus-claim-drift self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } +} /* Table hygiene precedes everything: a row that matches the empty string would * report the whole corpus, and over `--update` would write that into a ledger diff --git a/scripts/check-cross-package-test-inputs.mjs b/scripts/check-cross-package-test-inputs.mjs index 31c5c5c797..d63fa85f5d 100644 --- a/scripts/check-cross-package-test-inputs.mjs +++ b/scripts/check-cross-package-test-inputs.mjs @@ -1480,6 +1480,12 @@ function unionInto(listPath, changedPath) { } } +// Returned by `selfTest()` only after its verdict is printed. The dispatch +// refuses anything else: a `return` that leaves the function above that line +// prints nothing and still exits 0 — a self-test that never finished, reported +// as one that passed (#13798). +const SELF_TEST_VERDICT = 'check-cross-package-test-inputs self-test reached its verdict'; + function selfTest() { const cases = []; const ok = (label, cond) => cases.push({ label, cond }); @@ -2319,6 +2325,8 @@ function selfTest() { process.exit(1); } console.log(`\nAll ${cases.length} self-test cases passed.`); + + return SELF_TEST_VERDICT; } // --------------------------------------------------------------------------- @@ -2346,7 +2354,16 @@ function selfTest() { // `check:entry-guard`, which fails any other spelling in `scripts/**`. if (isEntrypoint(import.meta.url)) { const argv = process.argv.slice(2); - if (argv.includes('--self-test')) selfTest(); + if (argv.includes('--self-test')) { + if (selfTest() !== SELF_TEST_VERDICT) { + console.error( + '\n✗ check-cross-package-test-inputs self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + } else if (argv.includes('--list-escapes')) { for (const [name, info] of [...findEscapingPackages()].sort()) { console.log(`${name} (${info.dir})`); diff --git a/scripts/check-cross-repo-closer-outcome.mjs b/scripts/check-cross-repo-closer-outcome.mjs index d2c1416921..9765330775 100644 --- a/scripts/check-cross-repo-closer-outcome.mjs +++ b/scripts/check-cross-repo-closer-outcome.mjs @@ -1145,6 +1145,12 @@ const MUTATIONS = [ }, ]; +// Returned by `selfTest()` only after its verdict is printed. The dispatch +// refuses anything else: a `return` that leaves the function above that line +// prints nothing and still exits 0 — a self-test that never finished, reported +// as one that passed (#13798). +const SELF_TEST_VERDICT = 'check-cross-repo-closer-outcome self-test reached its verdict'; + async function selfTest() { const root = repoRoot(); const { source, problems } = extractScript(root); @@ -1213,6 +1219,8 @@ async function selfTest() { `✓ check-cross-repo-closer-outcome --self-test: ${checked} assertions, ` + `${MUTATIONS.length} mutations of the shipped script each driven to red.`, ); + + return SELF_TEST_VERDICT; } // The CLI dispatch is guarded so that IMPORTING this module is inert. The @@ -1220,7 +1228,16 @@ async function selfTest() { // tree (a pre-fix checkout), and a module that runs its gate on import would // silently judge THIS repo instead and print a pass about the wrong subject. if (isEntrypoint(import.meta.url)) { - if (process.argv.includes('--self-test')) await selfTest(); + if (process.argv.includes('--self-test')) { + if ((await selfTest()) !== SELF_TEST_VERDICT) { + console.error( + '\n✗ check-cross-repo-closer-outcome self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + } else if (process.argv.includes('--list')) list(); else await main(); } diff --git a/scripts/check-declaration-mirrors.mjs b/scripts/check-declaration-mirrors.mjs index 80f10697b0..226142a865 100644 --- a/scripts/check-declaration-mirrors.mjs +++ b/scripts/check-declaration-mirrors.mjs @@ -400,6 +400,12 @@ async function main() { // ── self-test ─────────────────────────────────────────────────────────────── +// Returned by `selfTest()` only after its verdict is printed. The dispatch +// refuses anything else: a `return` that leaves the function above that line +// prints nothing and still exits 0 — a self-test that never finished, reported +// as one that passed (#13798). +const SELF_TEST_VERDICT = 'check-declaration-mirrors self-test reached its verdict'; + async function selfTest() { const cases = []; const ok = (label, cond) => cases.push({ label, cond }); @@ -550,9 +556,20 @@ async function selfTest() { process.exit(1); } console.log(`\nAll ${cases.length} self-test cases passed.`); + + return SELF_TEST_VERDICT; } if (isEntrypoint(import.meta.url)) { - if (process.argv.includes('--self-test')) await selfTest(); + if (process.argv.includes('--self-test')) { + if ((await selfTest()) !== SELF_TEST_VERDICT) { + console.error( + '\n✗ check-declaration-mirrors self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + } else await main(); } diff --git a/scripts/check-declared-population-live.mjs b/scripts/check-declared-population-live.mjs index 610591f681..9b9f8c57cd 100644 --- a/scripts/check-declared-population-live.mjs +++ b/scripts/check-declared-population-live.mjs @@ -194,6 +194,13 @@ function main(argv) { return 0; } +// Set by `selfTest()` only after its verdict is printed, and read at the +// dispatch: a `return` that leaves the function above that line prints nothing +// and still exits 0 — a self-test that never finished, reported as one that +// passed (#13798). The self-test's own exit code stays load-bearing, so the +// handshake is a flag rather than a returned sentinel. +let selfTestReachedVerdict = false; + function selfTest() { const failures = []; let checked = 0; @@ -270,10 +277,23 @@ function selfTest() { return 1; } console.log(`check-declared-population-live --self-test: ${checked} assertion(s) passed.`); + selfTestReachedVerdict = true; return 0; } if (isEntrypoint(import.meta.url)) { const argv = process.argv.slice(2); - process.exit(argv.includes('--self-test') ? selfTest() : main(argv)); + if (argv.includes('--self-test')) { + const selfTestCode = selfTest(); + if (!selfTestReachedVerdict) { + console.error( + '\n✗ check-declared-population-live self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + process.exit(selfTestCode); + } + process.exit(main(argv)); } diff --git a/scripts/check-dev-prereqs.mjs b/scripts/check-dev-prereqs.mjs index e267e1390f..12e99ce2d1 100644 --- a/scripts/check-dev-prereqs.mjs +++ b/scripts/check-dev-prereqs.mjs @@ -581,6 +581,14 @@ function stamp(root, cwd, amplifiers = AMPLIFIERS) { * that matches nothing (#4690). These fixtures drive `inspect` to every verdict * and pin the exclusions that keep it from printing a red whose fix is wrong. */ + +// Set by `selfTest()` only after its verdict is printed, and read at the +// dispatch: a `return` that leaves the function above that line prints nothing +// and still exits 0 — a self-test that never finished, reported as one that +// passed (#13798). The self-test's own exit code stays load-bearing, so the +// handshake is a flag rather than a returned sentinel. +let selfTestReachedVerdict = false; + function selfTest() { const failures = []; const expect = (label, actual, wanted) => { @@ -823,11 +831,21 @@ function selfTest() { return 1; } console.log('✓ check:dev-prereqs --self-test — every verdict reachable, exclusions and freshness coverage pinned (16 cases), plus the shared workspace enumerator.'); + selfTestReachedVerdict = true; return 0; } if (process.argv.includes('--self-test')) { - process.exit(selfTest()); + const selfTestCode = selfTest(); + if (!selfTestReachedVerdict) { + console.error( + '\n✗ check-dev-prereqs self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + process.exit(selfTestCode); } try { diff --git a/scripts/check-dispatcher-error-vocabulary.mjs b/scripts/check-dispatcher-error-vocabulary.mjs index 26d2c4ab58..b48735c672 100644 --- a/scripts/check-dispatcher-error-vocabulary.mjs +++ b/scripts/check-dispatcher-error-vocabulary.mjs @@ -2130,6 +2130,13 @@ export function checkDoorTyping({ doorSource, files }) { // Self-test // --------------------------------------------------------------------------- +// Set by `selfTest()` only after its verdict is printed, and read at the +// dispatch: a `return` that leaves the function above that line prints nothing +// and still exits 0 — a self-test that never finished, reported as one that +// passed (#13798). The self-test's own exit code stays load-bearing, so the +// handshake is a flag rather than a returned sentinel. +let selfTestReachedVerdict = false; + function selfTest() { const fail = []; let cases = 0; @@ -3514,6 +3521,7 @@ function selfTest() { `check-dispatcher-error-vocabulary --self-test: ${Object.keys(samples).length} shapes ` + `+ ${cases} assertions OK (vocabulary + #9098 door typing)`, ); + selfTestReachedVerdict = true; } // --------------------------------------------------------------------------- @@ -3522,7 +3530,18 @@ function selfTest() { function main() { const argv = process.argv.slice(2); - if (argv.includes('--self-test')) return selfTest(); + if (argv.includes('--self-test')) { + const selfTestCode = selfTest(); + if (!selfTestReachedVerdict) { + console.error( + '\n✗ check-dispatcher-error-vocabulary self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + return selfTestCode; + } const readFile = (abs) => readFileSync(abs, 'utf8'); const ledger = parseLedgerCodes(readFileSync(join(ROOT, LEDGER_ZOD), 'utf8')); diff --git a/scripts/check-doc-anchors.mjs b/scripts/check-doc-anchors.mjs index cb30a9f975..9f9eb73c21 100644 --- a/scripts/check-doc-anchors.mjs +++ b/scripts/check-doc-anchors.mjs @@ -524,6 +524,12 @@ function idOf(headingLine) { return headingIds(headingLine)[0]; } +// Returned by `selfTest()` only after its verdict is printed. The dispatch +// refuses anything else: a `return` that leaves the function above that line +// prints nothing and still exits 0 — a self-test that never finished, reported +// as one that passed (#13798). +const SELF_TEST_VERDICT = 'check-doc-anchors self-test reached its verdict'; + function selfTest() { // 1. Slug parity with the renderer, on the two shapes that actually broke. for (const [heading, expected] of PARITY_PINS) { @@ -623,12 +629,23 @@ function selfTest() { console.log( `✅ check-doc-anchors --self-test: slug parity, custom ids, duplicate counters, extraction discrimination and both finding classes verified (${live.checked} live fragment links)`, ); + + return SELF_TEST_VERDICT; } /* Run only when invoked as a program — the extractor and the slug helpers are * exported so a caller chasing a false positive can import them without the * import itself sweeping the repo. */ if (isEntrypoint(import.meta.url)) { - if (process.argv.includes('--self-test')) selfTest(); + if (process.argv.includes('--self-test')) { + if (selfTest() !== SELF_TEST_VERDICT) { + console.error( + '\n✗ check-doc-anchors self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + } else runCheck(); } diff --git a/scripts/check-doc-frontmatter.mjs b/scripts/check-doc-frontmatter.mjs index e55f709b17..771b8a4a2e 100644 --- a/scripts/check-doc-frontmatter.mjs +++ b/scripts/check-doc-frontmatter.mjs @@ -855,6 +855,14 @@ export function main(roots = ROOTS) { * adding a `defineDocs` call, so a third one -- or a fourth `blogSchema` key -- * must fail this battery rather than arrive unowned. */ + +// Set by `selfTest()` only after its verdict is printed, and read at the +// dispatch: a `return` that leaves the function above that line prints nothing +// and still exits 0 — a self-test that never finished, reported as one that +// passed (#13798). The self-test's own exit code stays load-bearing, so the +// handshake is a flag rather than a returned sentinel. +let selfTestReachedVerdict = false; + export async function selfTest() { const failures = []; let checked = 0; @@ -1315,6 +1323,7 @@ export async function selfTest() { `extractor, ROOTS pinned against every defineDocs call in source.config.ts, and the CI wiring read out of ` + `lint.yml.`, ); + selfTestReachedVerdict = true; return 0; } @@ -1323,6 +1332,17 @@ export async function selfTest() { // that ran its gate on import would silently judge THIS repo instead and print // a verdict about the wrong subject (`check:entry-guard`). if (isEntrypoint(import.meta.url)) { - if (process.argv.includes('--self-test')) process.exit(await selfTest()); + if (process.argv.includes('--self-test')) { + const selfTestCode = await selfTest(); + if (!selfTestReachedVerdict) { + console.error( + '\n✗ check-doc-frontmatter self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + process.exit(selfTestCode); + } process.exit(main()); } diff --git a/scripts/check-doc-route-spelling.mjs b/scripts/check-doc-route-spelling.mjs index 174ee2646c..f8db478137 100644 --- a/scripts/check-doc-route-spelling.mjs +++ b/scripts/check-doc-route-spelling.mjs @@ -617,6 +617,14 @@ function exitCodeFor(flagCount, advisory) { // walker, judged against fixture LEDGER FILES parsed by the real parser — // never against the regexes alone (#4913: a gate can run, stay green, and be // structurally unable to reach the thing it claims to check). + +// Set by `selfTest()` only after its verdict is printed, and read at the +// dispatch: a `return` that leaves the function above that line prints nothing +// and still exits 0 — a self-test that never finished, reported as one that +// passed (#13798). The self-test's own exit code stays load-bearing, so the +// handshake is a flag rather than a returned sentinel. +let selfTestReachedVerdict = false; + function selfTest() { const failures = []; const expect = (label, got, want) => { @@ -855,12 +863,24 @@ function selfTest() { + 'surface mentions, the allow marker, optional and empty segments), and every hard-error direction ' + '(dead root, empty root, evaporated extraction, missing and unreadable ledger — each red naming its ' + 'subject, green again on restore) all hold.'); + selfTestReachedVerdict = true; } // ── Main ──────────────────────────────────────────────────────────────────── function main() { - if (process.argv.includes('--self-test')) return selfTest(); + if (process.argv.includes('--self-test')) { + const selfTestCode = selfTest(); + if (!selfTestReachedVerdict) { + console.error( + '\n✗ check-doc-route-spelling self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + return selfTestCode; + } const advisory = process.argv.includes('--advisory'); let result; diff --git a/scripts/check-docs-image-tag.mjs b/scripts/check-docs-image-tag.mjs index 8f4e60bde6..a6a912988d 100644 --- a/scripts/check-docs-image-tag.mjs +++ b/scripts/check-docs-image-tag.mjs @@ -673,6 +673,12 @@ function report(findings, stats, expected, proseStats) { // Self-test -- every limb has a positive control, each paired with its green. // --------------------------------------------------------------------------- +// Returned by `selfTest()` only after its verdict is printed. The dispatch +// refuses anything else: a `return` that leaves the function above that line +// prints nothing and still exits 0 — a self-test that never finished, reported +// as one that passed (#13798). +const SELF_TEST_VERDICT = 'check-docs-image-tag self-test reached its verdict'; + async function selfTest() { const failures = []; let checked = 0; @@ -1183,6 +1189,8 @@ async function selfTest() { + 'anchor -- observed FAILING, and the X.Y.Z metavariable, the historical "removed in 17.0.0" sentences, ' + "the upgrade-checklist rows and the reader's own app version observed EXCLUDED.", ); + + return SELF_TEST_VERDICT; } // --------------------------------------------------------------------------- @@ -1208,7 +1216,14 @@ function main() { // file and the branch is taken exactly as before. if (isEntrypoint(import.meta.url)) { if (process.argv.includes('--self-test')) { - await selfTest(); + if ((await selfTest()) !== SELF_TEST_VERDICT) { + console.error( + '\n✗ check-docs-image-tag self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } } else { main(); } diff --git a/scripts/check-docs-locale-catch-all.mjs b/scripts/check-docs-locale-catch-all.mjs index b7ca7031bf..5d41e272e2 100644 --- a/scripts/check-docs-locale-catch-all.mjs +++ b/scripts/check-docs-locale-catch-all.mjs @@ -539,6 +539,12 @@ function writeFixture( }; } +// Returned by `selfTest()` only after its verdict is printed. The dispatch +// refuses anything else: a `return` that leaves the function above that line +// prints nothing and still exits 0 — a self-test that never finished, reported +// as one that passed (#13798). +const SELF_TEST_VERDICT = 'check-docs-locale-catch-all self-test reached its verdict'; + function selfTest() { const failures = []; let checked = 0; @@ -710,6 +716,8 @@ function selfTest() { + "catch-all requirement off WITHOUT taking the run green, the OG marker's NAME observed free while " + 'its dot is not, and a widening that still excludes /og/ observed staying green.', ); + + return SELF_TEST_VERDICT; } function main() { @@ -724,6 +732,15 @@ function main() { } if (isEntrypoint(import.meta.url)) { - if (process.argv.includes('--self-test')) selfTest(); + if (process.argv.includes('--self-test')) { + if (selfTest() !== SELF_TEST_VERDICT) { + console.error( + '\n✗ check-docs-locale-catch-all self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + } else main(); } diff --git a/scripts/check-docs-nav-label.mjs b/scripts/check-docs-nav-label.mjs index 133338135e..228d86a6a7 100644 --- a/scripts/check-docs-nav-label.mjs +++ b/scripts/check-docs-nav-label.mjs @@ -509,6 +509,14 @@ export async function main(argv = []) { * pages their sidebar label, and asserting it against a hand-written stub would * be asserting against this file's own idea of the module. */ + +// Set by `selfTest()` only after its verdict is printed, and read at the +// dispatch: a `return` that leaves the function above that line prints nothing +// and still exits 0 — a self-test that never finished, reported as one that +// passed (#13798). The self-test's own exit code stays load-bearing, so the +// handshake is a flag rather than a returned sentinel. +let selfTestReachedVerdict = false; + export async function selfTest() { const failures = []; let checked = 0; @@ -687,10 +695,22 @@ export async function selfTest() { 'the real apps/docs population asserted non-empty and node_modules-free, and the CI wiring read out of ' + 'lint.yml.', ); + selfTestReachedVerdict = true; return 0; } if (isEntrypoint(import.meta.url)) { - if (process.argv.includes('--self-test')) process.exit(await selfTest()); + if (process.argv.includes('--self-test')) { + const selfTestCode = await selfTest(); + if (!selfTestReachedVerdict) { + console.error( + '\n✗ check-docs-nav-label self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + process.exit(selfTestCode); + } process.exit(await main(process.argv.slice(2))); } diff --git a/scripts/check-docs-redirects.mjs b/scripts/check-docs-redirects.mjs index c076f51937..3ebf586d64 100644 --- a/scripts/check-docs-redirects.mjs +++ b/scripts/check-docs-redirects.mjs @@ -448,6 +448,13 @@ function report(findings, stats, label) { * every red fixture below. The dirty one is asserted by its EXACT finding set, * so an over-eager checker fails just as loudly as a blind one. */ + +// Returned by `selfTest()` only after its verdict is printed. The dispatch +// refuses anything else: a `return` that leaves the function above that line +// prints nothing and still exits 0 — a self-test that never finished, reported +// as one that passed (#13798). +const SELF_TEST_VERDICT = 'check-docs-redirects self-test reached its verdict'; + async function selfTest() { const failures = []; let checked = 0; @@ -675,6 +682,8 @@ async function selfTest() { `✓ check-docs-redirects --self-test: ${checked} assertions over a temp fixture (real loadTable + checkTable path); ` + 'every limb -- dead page, wildcard directory, chain -- observed FAILING and observed silent.', ); + + return SELF_TEST_VERDICT; } // --------------------------------------------------------------------------- @@ -692,7 +701,14 @@ async function main() { * table (and calling `process.exit` out from under its caller). */ if (isEntrypoint(import.meta.url)) { if (process.argv.includes('--self-test')) { - await selfTest(); + if ((await selfTest()) !== SELF_TEST_VERDICT) { + console.error( + '\n✗ check-docs-redirects self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } } else { await main(); } diff --git a/scripts/check-docs-section-name.mjs b/scripts/check-docs-section-name.mjs index 278b8a7906..d9342ab773 100644 --- a/scripts/check-docs-section-name.mjs +++ b/scripts/check-docs-section-name.mjs @@ -1043,6 +1043,13 @@ function baseFixtureFiles(extra = {}) { }; } +// Set by `selfTest()` only after its verdict is printed, and read at the +// dispatch: a `return` that leaves the function above that line prints nothing +// and still exits 0 — a self-test that never finished, reported as one that +// passed (#13798). The self-test's own exit code stays load-bearing, so the +// handshake is a flag rather than a returned sentinel. +let selfTestReachedVerdict = false; + export function selfTest() { const cases = []; const trees = []; @@ -1297,11 +1304,23 @@ export function selfTest() { + '(real temp trees on disk; both historical misses reproduced as RED, both arms driven RED, ' + 'the duplicate-key and syntax-error boundaries pinned, every refusal exercised).', ); + selfTestReachedVerdict = true; return 0; } if (isEntrypoint(import.meta.url)) { - if (process.argv.includes('--self-test')) process.exit(selfTest()); + if (process.argv.includes('--self-test')) { + const selfTestCode = selfTest(); + if (!selfTestReachedVerdict) { + console.error( + '\n✗ check-docs-section-name self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + process.exit(selfTestCode); + } else if (process.argv.includes('--list')) process.exit(list()); else process.exit(run()); } diff --git a/scripts/check-docs-single-h1.mjs b/scripts/check-docs-single-h1.mjs index 5c2f151006..776e08985e 100644 --- a/scripts/check-docs-single-h1.mjs +++ b/scripts/check-docs-single-h1.mjs @@ -370,6 +370,14 @@ function main(argv) { * `scanTree`. A live corpus that is green cannot tell a working gate from a * blind one, and this gate lands with its corpus green by construction. */ + +// Set by `selfTest()` only after its verdict is printed, and read at the +// dispatch: a `return` that leaves the function above that line prints nothing +// and still exits 0 — a self-test that never finished, reported as one that +// passed (#13798). The self-test's own exit code stays load-bearing, so the +// handshake is a flag rather than a returned sentinel. +let selfTestReachedVerdict = false; + export function selfTest() { const cases = []; const t = (name, ok, detail) => cases.push({ name, ok: Boolean(ok), detail }); @@ -495,9 +503,22 @@ export function selfTest() { `✓ check-docs-single-h1 self-test: ${cases.length} cases pass (both fence spellings, inline-code equality, ` + `indentation, a synthetic carve-out, and both anti-vacuity limbs).`, ); + selfTestReachedVerdict = true; return 0; } if (isEntrypoint(import.meta.url)) { - process.exit(process.argv.includes('--self-test') ? selfTest() : main(process.argv.slice(2))); + if (process.argv.includes('--self-test')) { + const selfTestCode = selfTest(); + if (!selfTestReachedVerdict) { + console.error( + '\n✗ check-docs-single-h1 self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + process.exit(selfTestCode); + } + process.exit(main(process.argv.slice(2))); } diff --git a/scripts/check-driver-conformance.mjs b/scripts/check-driver-conformance.mjs index 63647e8d12..ad73041e47 100644 --- a/scripts/check-driver-conformance.mjs +++ b/scripts/check-driver-conformance.mjs @@ -1575,6 +1575,12 @@ function report() { // against synthetic inputs so a refactor that neuters the detection fails here // rather than silently passing every future PR. +// Returned by `selfTest()` only after its verdict is printed. The dispatch +// refuses anything else: a `return` that leaves the function above that line +// prints nothing and still exits 0 — a self-test that never finished, reported +// as one that passed (#13798). +const SELF_TEST_VERDICT = 'check-driver-conformance self-test reached its verdict'; + function selfTest() { const failures = []; const expect = (label, cond) => { @@ -2304,7 +2310,18 @@ function selfTest() { + 'the invariant is asserted against the real tree over a non-empty population, so it cannot ' + 'pass vacuously.', ); + + return SELF_TEST_VERDICT; } -if (process.argv.includes('--self-test')) selfTest(); +if (process.argv.includes('--self-test')) { + if (selfTest() !== SELF_TEST_VERDICT) { + console.error( + '\n✗ check-driver-conformance self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } +} else report(); diff --git a/scripts/check-driver-memory-census.mjs b/scripts/check-driver-memory-census.mjs index 8872cf1f49..f1981309d2 100644 --- a/scripts/check-driver-memory-census.mjs +++ b/scripts/check-driver-memory-census.mjs @@ -521,6 +521,12 @@ function report({ list = false } = {}) { // the reconciler at both sides of every decision they make, so a refactor that // neuters either fails HERE rather than turning every future PR green. +// Returned by `selfTest()` only after its verdict is printed. The dispatch +// refuses anything else: a `return` that leaves the function above that line +// prints nothing and still exits 0 — a self-test that never finished, reported +// as one that passed (#13798). +const SELF_TEST_VERDICT = 'check-driver-memory-census self-test reached its verdict'; + function selfTest() { const failures = []; const expect = (label, cond) => { if (!cond) failures.push(label); }; @@ -734,6 +740,8 @@ function selfTest() { + 'proves discovery reaches every ruled consumer in the real tree, and holds the ruled set to the ' + 'rulings that admitted it — a claim that survives the set shrinking to empty.', ); + + return SELF_TEST_VERDICT; } const argv = process.argv.slice(2); @@ -742,5 +750,14 @@ const invokedDirectly = isEntrypoint(import.meta.url); if (!invokedDirectly) { // imported as a module — expose the exports and do nothing else -} else if (argv.includes('--self-test')) selfTest(); +} else if (argv.includes('--self-test')) { + if (selfTest() !== SELF_TEST_VERDICT) { + console.error( + '\n✗ check-driver-memory-census self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } +} else report({ list: argv.includes('--list') }); diff --git a/scripts/check-dts-emitted.mjs b/scripts/check-dts-emitted.mjs index d4a18319eb..86247ad9f7 100644 --- a/scripts/check-dts-emitted.mjs +++ b/scripts/check-dts-emitted.mjs @@ -225,6 +225,14 @@ function run(dir) { // The two directions this guard can be wrong in are both silent: over-matching // makes every build fail, under-matching waves the DTS-less dist through -- the // exact artifact #11907 recorded. So both get asserted rather than assumed. + +// Set by `selfTest()` only after its verdict is printed, and read at the +// dispatch: a `return` that leaves the function above that line prints nothing +// and still exits 0 — a self-test that never finished, reported as one that +// passed (#13798). The self-test's own exit code stays load-bearing, so the +// handshake is a flag rather than a returned sentinel. +let selfTestReachedVerdict = false; + function selfTest() { const failures = []; const eq = (label, actual, expected) => { @@ -293,6 +301,7 @@ function selfTest() { return 1; } console.log('check-dts-emitted self-test: all assertions passed.'); + selfTestReachedVerdict = true; return 0; } @@ -303,5 +312,17 @@ function selfTest() { // failure this guard exists to catch, one level up. if (isEntrypoint(import.meta.url)) { const isSelfTest = process.argv.includes('--self-test'); - process.exit(isSelfTest ? selfTest() : run(process.cwd())); + if (isSelfTest) { + const selfTestCode = selfTest(); + if (!selfTestReachedVerdict) { + console.error( + '\n✗ check-dts-emitted self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + process.exit(selfTestCode); + } + process.exit(run(process.cwd())); } diff --git a/scripts/check-dual-build-cjs-loads.mjs b/scripts/check-dual-build-cjs-loads.mjs index 51b1e3891c..c33244f1f5 100644 --- a/scripts/check-dual-build-cjs-loads.mjs +++ b/scripts/check-dual-build-cjs-loads.mjs @@ -1074,6 +1074,13 @@ function writePkg(root, name, manifest, files) { return dir; } +// Set by `selfTest()` only after its verdict is printed, and read at the +// dispatch: a `return` that leaves the function above that line prints nothing +// and still exits 0 — a self-test that never finished, reported as one that +// passed (#13798). The self-test's own exit code stays load-bearing, so the +// handshake is a flag rather than a returned sentinel. +let selfTestReachedVerdict = false; + export async function selfTest() { const cases = []; const t = (name, ok, detail) => cases.push({ name, ok: Boolean(ok), detail }); @@ -1434,11 +1441,25 @@ export async function selfTest() { } console.log(`✓ check-dual-build-cjs-loads self-test: ${cases.length} cases pass (real emitted bytes, real spawns; both stale-ledger directions including the orphan one, every vacuity floor driven to zero with its green control, the parse failure the ledger may never silence, the record's ref reproducible, quoted from the record and printed on the pass path, and TYPED in both directions — the #13112 shape red, the identically-spelled CJS-first package green).`); + selfTestReachedVerdict = true; return EXIT_OK; } if (isEntrypoint(import.meta.url)) { const argv = process.argv.slice(2); - const code = argv.includes('--self-test') ? await selfTest() : await main(argv); + let code; + if (argv.includes('--self-test')) { + code = await selfTest(); + if (!selfTestReachedVerdict) { + console.error( + '\n✗ check-dual-build-cjs-loads self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + } else { + code = await main(argv); + } process.exit(code); } diff --git a/scripts/check-durability-degradation-log-level.mjs b/scripts/check-durability-degradation-log-level.mjs index 44729ad889..329db22c26 100644 --- a/scripts/check-durability-degradation-log-level.mjs +++ b/scripts/check-durability-degradation-log-level.mjs @@ -4164,6 +4164,14 @@ function reportDepthCost() { // A checker nobody checks is the shape this gate exists to prevent. These // fixtures pin both directions: it must FLAG the #4420 shape and must NOT flag // the shapes that are legitimately `warn`. + +// Set by `selfTest()` only after its verdict is printed, and read at the +// dispatch: a `return` that leaves the function above that line prints nothing +// and still exits 0 — a self-test that never finished, reported as one that +// passed (#13798). The self-test's own exit code stays load-bearing, so the +// handshake is a flag rather than a returned sentinel. +let selfTestReachedVerdict = false; + function selfTest() { const cases = [ { @@ -5153,6 +5161,7 @@ function selfTest() { return 1; } console.log(`\n✓ self-test (log-level rule): ${cases.length} case(s) passed\n`); + selfTestReachedVerdict = true; return 0; } @@ -5164,6 +5173,11 @@ function selfTest() { // the FLAGGING ones are those same seams as they read BEFORE those fixes. A // gate for a family that has recurred three times must be pinned against the // three instances, in both directions, or the fourth recurrence lands green. +// The dispatch calls TWO self-test entries and combines their statuses, so each +// one needs its own handshake: a `return` above either verdict prints nothing and +// leaves `undefined`, which the `||` below reads as a pass (#13798). +let readSeamsReachedVerdict = false; + function selfTestReadSeams() { // The guard `DatabaseLoader` and `MetadataProtocol` both wrote after the // fixes, reproduced verbatim so the fixtures exercise the real shape. @@ -6378,6 +6392,7 @@ function selfTestReadSeams() { `\n✓ self-test (read-seam invention rule): ${cases.length} case(s) passed, and the baseline ` + 'offer stays marked maintainer-only (#8435)\n', ); + readSeamsReachedVerdict = true; return 0; } @@ -6385,7 +6400,23 @@ const args = process.argv.slice(2); if (args.includes('--self-test')) { // Both rules' fixtures always run — a red one must not hide the other. const logLevelStatus = selfTest(); + if (!selfTestReachedVerdict) { + console.error( + '\n✗ check-durability-degradation-log-level self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } const readSeamStatus = selfTestReadSeams(); + if (!readSeamsReachedVerdict) { + console.error( + '\n✗ check-durability-degradation-log-level self-test: selfTestReadSeams() returned without\n' + + 'reaching its verdict, so no success line was printed. Exiting 0 here would report a\n' + + 'self-test that never finished as a self-test that passed.\n', + ); + process.exit(1); + } process.exit(logLevelStatus || readSeamStatus ? 1 : 0); } else if (args.includes('--depth-cost')) { // A diagnostic, deliberately not part of any verdict — see `reportDepthCost`. diff --git a/scripts/check-empty-changeset.mjs b/scripts/check-empty-changeset.mjs index f925affcfd..18143fa8b8 100644 --- a/scripts/check-empty-changeset.mjs +++ b/scripts/check-empty-changeset.mjs @@ -404,6 +404,12 @@ function list() { // commits, so a fixture that is not two real commits would be testing an // imitation of the code path that ships. +// Returned by `selfTest()` only after its verdict is printed. The dispatch +// refuses anything else: a `return` that leaves the function above that line +// prints nothing and still exits 0 — a self-test that never finished, reported +// as one that passed (#13798). +const SELF_TEST_VERDICT = 'check-empty-changeset self-test reached its verdict'; + function selfTest() { const failures = []; let checked = 0; @@ -1431,6 +1437,8 @@ function selfTest() { process.exit(1); } console.log(`✓ check-empty-changeset --self-test: ${checked} assertions over real temp git repos (real scan() path)`); + + return SELF_TEST_VERDICT; } // ── main ───────────────────────────────────────────────────────────────────── @@ -1438,7 +1446,14 @@ function selfTest() { const argv = process.argv.slice(2); if (argv.includes('--self-test')) { - selfTest(); + if (selfTest() !== SELF_TEST_VERDICT) { + console.error( + '\n✗ check-empty-changeset self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } } else if (argv.includes('--list')) { list(); } else { diff --git a/scripts/check-engine-double-contract.mjs b/scripts/check-engine-double-contract.mjs index e5dbf928a9..8e2198e515 100644 --- a/scripts/check-engine-double-contract.mjs +++ b/scripts/check-engine-double-contract.mjs @@ -3337,6 +3337,12 @@ function report() { // against synthetic sources on BOTH sides of every decision it makes, so a // refactor that neuters it fails here instead of turning every future PR green. +// Returned by `selfTest()` only after its verdict is printed. The dispatch +// refuses anything else: a `return` that leaves the function above that line +// prints nothing and still exits 0 — a self-test that never finished, reported +// as one that passed (#13798). +const SELF_TEST_VERDICT = 'check-engine-double-contract self-test reached its verdict'; + function selfTest() { const failures = []; const expect = (label, cond) => { if (!cond) failures.push(label); }; @@ -4840,9 +4846,20 @@ const driver: any = { create: async (o: string, d: any) => d, find: async (o: st + 'while refusing a router, a file-local look-alike, a wrong-module import, a type ' + 'argument and a driver-shaped verb, and moving discovery and the census together.', ); + + return SELF_TEST_VERDICT; } -if (process.argv.includes('--self-test')) selfTest(); +if (process.argv.includes('--self-test')) { + if (selfTest() !== SELF_TEST_VERDICT) { + console.error( + '\n✗ check-engine-double-contract self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } +} else if (process.argv.includes('--write')) writeLedger(); else if (process.argv.includes('--census')) censusReport(); else report(); diff --git a/scripts/check-engine-split-ratio.mjs b/scripts/check-engine-split-ratio.mjs index c41da1f50c..85e77ea559 100644 --- a/scripts/check-engine-split-ratio.mjs +++ b/scripts/check-engine-split-ratio.mjs @@ -213,6 +213,13 @@ function main(argv) { // ── self-test ──────────────────────────────────────────────────────────────── +// Set by `selfTest()` only after its verdict is printed, and read at the +// dispatch: a `return` that leaves the function above that line prints nothing +// and still exits 0 — a self-test that never finished, reported as one that +// passed (#13798). The self-test's own exit code stays load-bearing, so the +// handshake is a flag rather than a returned sentinel. +let selfTestReachedVerdict = false; + function selfTest() { let failures = 0; const t = (name, ok, detail = '') => { @@ -332,10 +339,23 @@ function selfTest() { console.log(failures === 0 ? `\ncheck-engine-split-ratio --self-test: all cases passed.` : `\ncheck-engine-split-ratio --self-test: ${failures} FAILED.`); + selfTestReachedVerdict = true; return failures === 0 ? 0 : 1; } // Exports bindings, so an import for those exports alone must run nothing (#10667). if (isEntrypoint(import.meta.url)) { - process.exit(process.argv.includes('--self-test') ? selfTest() : main(process.argv.slice(2))); + if (process.argv.includes('--self-test')) { + const selfTestCode = selfTest(); + if (!selfTestReachedVerdict) { + console.error( + '\n✗ check-engine-split-ratio self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + process.exit(selfTestCode); + } + process.exit(main(process.argv.slice(2))); } diff --git a/scripts/check-entry-guard.mjs b/scripts/check-entry-guard.mjs index 72b03556e6..b74a30ab08 100644 --- a/scripts/check-entry-guard.mjs +++ b/scripts/check-entry-guard.mjs @@ -625,6 +625,13 @@ function list() { // Self-test -- fixture sources, not this tree // --------------------------------------------------------------------------- +// Set by `selfTest()` only after its verdict is printed, and read at the +// dispatch: a `return` that leaves the function above that line prints nothing +// and still exits 0 — a self-test that never finished, reported as one that +// passed (#13798). The self-test's own exit code stays load-bearing, so the +// handshake is a flag rather than a returned sentinel. +let selfTestReachedVerdict = false; + export function selfTest() { const cases = []; const t = (name, ok, detail) => cases.push({ name, ok: Boolean(ok), detail }); @@ -807,10 +814,23 @@ export function selfTest() { `and the import-safety rule recognised on both sides (dispatch/exit/argv-branch/try rejected; declarations, non-exporters and all three guard spellings accepted) — ` + `plus the dispatch-gates scan surface, derived from the walked root and held apart from the baseline roster.`, ); + selfTestReachedVerdict = true; return 0; } if (isEntrypoint(import.meta.url)) { const argv = process.argv; - process.exit(argv.includes('--self-test') ? selfTest() : argv.includes('--list') ? list() : main()); + if (argv.includes('--self-test')) { + const selfTestCode = selfTest(); + if (!selfTestReachedVerdict) { + console.error( + '\n✗ check-entry-guard self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + process.exit(selfTestCode); + } + process.exit(argv.includes('--list') ? list() : main()); } diff --git a/scripts/check-error-code-casing.mjs b/scripts/check-error-code-casing.mjs index de36342cb9..b903b61f60 100644 --- a/scripts/check-error-code-casing.mjs +++ b/scripts/check-error-code-casing.mjs @@ -299,6 +299,13 @@ export function findViolations(src, file, stats = null) { return hits; } +// Set by `selfTest()` only after its verdict is printed, and read at the +// dispatch: a `return` that leaves the function above that line prints nothing +// and still exits 0 — a self-test that never finished, reported as one that +// passed (#13798). The self-test's own exit code stays load-bearing, so the +// handshake is a flag rather than a returned sentinel. +let selfTestReachedVerdict = false; + function selfTest() { const cases = [ // [source, expectedHitCount, label] @@ -437,10 +444,22 @@ function selfTest() { console.log( `✓ check-error-code-casing self-test: ${cases.length} recognizer case(s) + ${partitionCases.length + 1} registry case(s) pass.`, ); + selfTestReachedVerdict = true; } function main() { - if (process.argv.includes('--self-test')) return selfTest(); + if (process.argv.includes('--self-test')) { + const selfTestCode = selfTest(); + if (!selfTestReachedVerdict) { + console.error( + '\n✗ check-error-code-casing self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + return selfTestCode; + } const files = SCAN_ROOTS.flatMap((r) => walk(join(ROOT, r))); const stats = { optOuts: 0, exempt: 0 }; diff --git a/scripts/check-error-status-conformance.mjs b/scripts/check-error-status-conformance.mjs index dd3aa03d63..b2b9625ec9 100644 --- a/scripts/check-error-status-conformance.mjs +++ b/scripts/check-error-status-conformance.mjs @@ -765,6 +765,13 @@ function runFixture({ files, handling, catalog, members }) { }; } +// Set by `selfTest()` only after its verdict is printed, and read at the +// dispatch: a `return` that leaves the function above that line prints nothing +// and still exits 0 — a self-test that never finished, reported as one that +// passed (#13798). The self-test's own exit code stays load-bearing, so the +// handshake is a flag rather than a returned sentinel. +let selfTestReachedVerdict = false; + function selfTest() { const failures = []; const check = (name, ok, detail) => { if (!ok) failures.push(`${name}${detail ? ` — ${detail}` : ''}`); }; @@ -1081,6 +1088,7 @@ function selfTest() { + 'baseline-expanding remedy stays maintainer-only, and a baselined code leaving the unpinned census is ' + 'named a producer or a removed doc entry — never the wrong one of the two.', ); + selfTestReachedVerdict = true; process.exit(0); } @@ -1238,6 +1246,16 @@ function main() { // import of any of them walked the scan root, read every source file and printed // this gate's full report into the importer's stdout before returning a binding. if (isEntrypoint(import.meta.url)) { - if (process.argv.includes('--self-test')) selfTest(); + if (process.argv.includes('--self-test')) { + selfTest(); + if (!selfTestReachedVerdict) { + console.error( + '\n✗ check-error-status-conformance self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + } main(); } diff --git a/scripts/check-examples-live-imports.mjs b/scripts/check-examples-live-imports.mjs index a1546fa4b2..95ae0f25ee 100644 --- a/scripts/check-examples-live-imports.mjs +++ b/scripts/check-examples-live-imports.mjs @@ -751,6 +751,12 @@ function verify() { // class (a comment, a string literal that is not a specifier, a relative path // that stays inside the package) gets a negative one. +// Returned by `selfTest()` only after its verdict is printed. The dispatch +// refuses anything else: a `return` that leaves the function above that line +// prints nothing and still exits 0 — a self-test that never finished, reported +// as one that passed (#13798). +const SELF_TEST_VERDICT = 'check-examples-live-imports self-test reached its verdict'; + function selfTest() { const apps = [ { dir: 'examples/app-showcase', name: '@objectstack/example-showcase' }, @@ -908,6 +914,8 @@ function selfTest() { process.exit(1); } console.log(`\nAll ${cases.length} self-test cases passed.`); + + return SELF_TEST_VERDICT; } const argv = process.argv.slice(2); @@ -916,7 +924,16 @@ const invokedDirectly = isEntrypoint(import.meta.url); if (!invokedDirectly) { // imported as a module — expose the exports and do nothing else -} else if (argv.includes('--self-test')) selfTest(); +} else if (argv.includes('--self-test')) { + if (selfTest() !== SELF_TEST_VERDICT) { + console.error( + '\n✗ check-examples-live-imports self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } +} else if (argv.includes('--list')) list(); else if (argv.includes('--json')) { const { rows, unresolved } = collect(); diff --git a/scripts/check-filter-alias-parity.mjs b/scripts/check-filter-alias-parity.mjs index 8aec055229..ec42e2b21f 100644 --- a/scripts/check-filter-alias-parity.mjs +++ b/scripts/check-filter-alias-parity.mjs @@ -484,6 +484,12 @@ export const FILTER_SLOT_QUERY_PARAMS: readonly string[] = (() => { `; } +// Returned by `selfTest()` only after its verdict is printed. The dispatch +// refuses anything else: a `return` that leaves the function above that line +// prints nothing and still exits 0 — a self-test that never finished, reported +// as one that passed (#13798). +const SELF_TEST_VERDICT = 'check-filter-alias-parity self-test reached its verdict'; + function selfTest() { const failures = []; const check = (name, condition, detail) => { @@ -612,13 +618,22 @@ function selfTest() { 'check:filter-alias-parity --self-test passed (parity at four and at five spellings, both drift ' + 'directions, the two-hop `$` alias, four rot shapes, and the CI wiring)', ); + + return SELF_TEST_VERDICT; } // ───────────────────────────────────────────────────────────────────────────── function main() { if (process.argv.includes('--self-test')) { - selfTest(); + if (selfTest() !== SELF_TEST_VERDICT) { + console.error( + '\n✗ check-filter-alias-parity self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } return; } selfTest(); diff --git a/scripts/check-i18n-bundles.mjs b/scripts/check-i18n-bundles.mjs index b185716a0f..c7ee5eb6cb 100644 --- a/scripts/check-i18n-bundles.mjs +++ b/scripts/check-i18n-bundles.mjs @@ -428,6 +428,12 @@ function populationVerdict(population, activeFilter) { // verdicts do not contaminate each other. // --------------------------------------------------------------------------- +// Returned by `selfTest()` only after its verdict is printed. The dispatch +// refuses anything else: a `return` that leaves the function above that line +// prints nothing and still exits 0 — a self-test that never finished, reported +// as one that passed (#13798). +const SELF_TEST_VERDICT = 'check-i18n-bundles self-test reached its verdict'; + function selfTest() { const failures = []; const expect = (name, cond, detail) => { @@ -940,10 +946,19 @@ function selfTest() { 'the population walk is CWD-independent; and the build-prerequisite closure names every ' + 'package this gate extracts plus the CLI, refusing whole rather than naming some.', ); + + return SELF_TEST_VERDICT; } if (process.argv.includes('--self-test')) { - selfTest(); + if (selfTest() !== SELF_TEST_VERDICT) { + console.error( + '\n✗ check-i18n-bundles self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } process.exit(0); } diff --git a/scripts/check-i18n-coverage.mjs b/scripts/check-i18n-coverage.mjs index 5edb0e2596..696bcf65df 100644 --- a/scripts/check-i18n-coverage.mjs +++ b/scripts/check-i18n-coverage.mjs @@ -629,6 +629,12 @@ function measureAllConfigs(configPaths, measure) { // always builds first, so nothing else would ever exercise it. // --------------------------------------------------------------------------- +// Returned by `selfTest()` only after its verdict is printed. The dispatch +// refuses anything else: a `return` that leaves the function above that line +// prints nothing and still exits 0 — a self-test that never finished, reported +// as one that passed (#13798). +const SELF_TEST_VERDICT = 'check-i18n-coverage self-test reached its verdict'; + function selfTest() { const failures = []; const expect = (name, cond, detail) => { @@ -1087,10 +1093,19 @@ function selfTest() { `the build-prerequisite closure names all ${onRoot.length} of them plus the CLI, and refuses whole rather ` + `than naming some.`, ); + + return SELF_TEST_VERDICT; } if (process.argv.includes('--self-test')) { - selfTest(); + if (selfTest() !== SELF_TEST_VERDICT) { + console.error( + '\n✗ check-i18n-coverage self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } process.exit(0); } diff --git a/scripts/check-i18n-stale-fill.mjs b/scripts/check-i18n-stale-fill.mjs index 0946efd969..e5e0d8e5e6 100644 --- a/scripts/check-i18n-stale-fill.mjs +++ b/scripts/check-i18n-stale-fill.mjs @@ -496,6 +496,13 @@ function discoverProvenanceServing() { return rows; } +// Set by `selfTest()` only after its verdict is printed, and read at the +// dispatch: a `return` that leaves the function above that line prints nothing +// and still exits 0 — a self-test that never finished, reported as one that +// passed (#13798). The self-test's own exit code stays load-bearing, so the +// handshake is a flag rather than a returned sentinel. +let selfTestReachedVerdict = false; + function selfTest() { let failures = 0; const expect = (what, ok) => { @@ -720,6 +727,7 @@ function selfTest() { ); console.log(failures === 0 ? '\ncheck-i18n-stale-fill: self-test OK\n' : `\ncheck-i18n-stale-fill: self-test FAILED (${failures})\n`); + selfTestReachedVerdict = true; process.exit(failures === 0 ? 0 : 1); } @@ -913,6 +921,16 @@ console.log(`\ncheck-i18n-stale-fill: OK (${sets.length} bundle set(s) — no ne } if (isEntrypoint(import.meta.url)) { - if (process.argv.includes('--self-test')) selfTest(); + if (process.argv.includes('--self-test')) { + selfTest(); + if (!selfTestReachedVerdict) { + console.error( + '\n✗ check-i18n-stale-fill self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + } main(); } diff --git a/scripts/check-init-service-contract.mjs b/scripts/check-init-service-contract.mjs index 46b93863e8..22a11b2c17 100644 --- a/scripts/check-init-service-contract.mjs +++ b/scripts/check-init-service-contract.mjs @@ -499,6 +499,12 @@ function list() { // ── Self-test ──────────────────────────────────────────────────────────────── +// Returned by `selfTest()` only after its verdict is printed. The dispatch +// refuses anything else: a `return` that leaves the function above that line +// prints nothing and still exits 0 — a self-test that never finished, reported +// as one that passed (#13798). +const SELF_TEST_VERDICT = 'check-init-service-contract self-test reached its verdict'; + function selfTest() { const assert = (cond, msg) => { if (!cond) { console.error('✗ self-test: ' + msg); process.exit(1); } }; @@ -839,8 +845,19 @@ function selfTest() { } console.log('✓ self-test: 19 cases'); + + return SELF_TEST_VERDICT; } -if (process.argv.includes('--self-test')) selfTest(); +if (process.argv.includes('--self-test')) { + if (selfTest() !== SELF_TEST_VERDICT) { + console.error( + '\n✗ check-init-service-contract self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } +} else if (process.argv.includes('--list')) list(); else audit(); diff --git a/scripts/check-kernel-hook-pairs.mjs b/scripts/check-kernel-hook-pairs.mjs index c5f5cac8bc..32d0da19e7 100644 --- a/scripts/check-kernel-hook-pairs.mjs +++ b/scripts/check-kernel-hook-pairs.mjs @@ -305,6 +305,12 @@ function assert(condition, message) { } } +// Returned by `selfTest()` only after its verdict is printed. The dispatch +// refuses anything else: a `return` that leaves the function above that line +// prints nothing and still exits 0 — a self-test that never finished, reported +// as one that passed (#13798). +const SELF_TEST_VERDICT = 'check-kernel-hook-pairs self-test reached its verdict'; + function selfTest() { const PINNED = (hook) => ` describe('Kernel', () => { @@ -441,6 +447,8 @@ function selfTest() { } console.log('✓ self-test: 10 cases'); + + return SELF_TEST_VERDICT; } // Only act when invoked as the entry point. The audit helpers above are @@ -449,7 +457,16 @@ function selfTest() { // before it is pinned in CI) — an import that audited, printed and possibly // called process.exit(1) as a side effect would make that impossible. if (isEntrypoint(import.meta.url)) { - if (process.argv.includes('--self-test')) selfTest(); + if (process.argv.includes('--self-test')) { + if (selfTest() !== SELF_TEST_VERDICT) { + console.error( + '\n✗ check-kernel-hook-pairs self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + } else if (process.argv.includes('--list')) list(); else run(); } diff --git a/scripts/check-keyed-text-bounds.mjs b/scripts/check-keyed-text-bounds.mjs index 15197c885c..f046ddc268 100644 --- a/scripts/check-keyed-text-bounds.mjs +++ b/scripts/check-keyed-text-bounds.mjs @@ -1145,6 +1145,13 @@ function objectFile(body) { return `import { ObjectSchema, Field } from '@objectstack/spec/data';\n\nexport const X = ObjectSchema.create(${body});\n`; } +// Set by `selfTest()` only after its verdict is printed, and read at the +// dispatch: a `return` that leaves the function above that line prints nothing +// and still exits 0 — a self-test that never finished, reported as one that +// passed (#13798). The self-test's own exit code stays load-bearing, so the +// handshake is a flag rather than a returned sentinel. +let selfTestReachedVerdict = false; + export function selfTest() { let failures = 0; const t = (name, ok, detail) => { @@ -1422,10 +1429,23 @@ export function selfTest() { } console.log(`\n${failures === 0 ? 'PASS' : 'FAIL'} check-keyed-text-bounds --self-test (${failures} failure(s))`); + selfTestReachedVerdict = true; return failures === 0 ? 0 : 1; } if (isEntrypoint(import.meta.url)) { const argv = process.argv.slice(2); - process.exit(argv.includes('--self-test') ? selfTest() : argv.includes('--list') ? list() : main()); + if (argv.includes('--self-test')) { + const selfTestCode = selfTest(); + if (!selfTestReachedVerdict) { + console.error( + '\n✗ check-keyed-text-bounds self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + process.exit(selfTestCode); + } + process.exit(argv.includes('--list') ? list() : main()); } diff --git a/scripts/check-live-db-isolation.mjs b/scripts/check-live-db-isolation.mjs index 5bf267d7a1..18c6cef115 100644 --- a/scripts/check-live-db-isolation.mjs +++ b/scripts/check-live-db-isolation.mjs @@ -241,6 +241,12 @@ function main() { console.log('check-live-db-isolation: PASS -- every live suite derives its database'); } +// Returned by `selfTest()` only after its verdict is printed. The dispatch +// refuses anything else: a `return` that leaves the function above that line +// prints nothing and still exits 0 — a self-test that never finished, reported +// as one that passed (#13798). +const SELF_TEST_VERDICT = 'check-live-db-isolation self-test reached its verdict'; + function selfTest() { const cases = []; const bt = String.fromCharCode(96); @@ -313,6 +319,8 @@ function selfTest() { process.exit(1); } console.log(`\ncheck-live-db-isolation --self-test: PASS (${cases.length} cases)`); + + return SELF_TEST_VERDICT; } // This file exports its detector so `--self-test` drives the real functions @@ -321,6 +329,15 @@ function selfTest() { // and its `process.exit`, inside the importer. `check:entry-guard` enforces this // (and caught exactly that here on the first run). if (isEntrypoint(import.meta.url)) { - if (process.argv.includes('--self-test')) selfTest(); + if (process.argv.includes('--self-test')) { + if (selfTest() !== SELF_TEST_VERDICT) { + console.error( + '\n✗ check-live-db-isolation self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + } else main(); } diff --git a/scripts/check-logger-receiver-detach.mjs b/scripts/check-logger-receiver-detach.mjs index 411eb58d89..a0414cd025 100644 --- a/scripts/check-logger-receiver-detach.mjs +++ b/scripts/check-logger-receiver-detach.mjs @@ -577,6 +577,12 @@ function measure() { // Self-test // --------------------------------------------------------------------------- +// Returned by `selfTest()` only after its verdict is printed. The dispatch +// refuses anything else: a `return` that leaves the function above that line +// prints nothing and still exits 0 — a self-test that never finished, reported +// as one that passed (#13798). +const SELF_TEST_VERDICT = 'check-logger-receiver-detach self-test reached its verdict'; + function selfTest() { const failures = []; const expect = (what, ok) => { if (!ok) failures.push(what); }; @@ -699,6 +705,8 @@ function selfTest() { + ' entry is decorative and no declined one fires; the walk is a narrowing and its\n' + ' roots match the declared watch hints.', ); + + return SELF_TEST_VERDICT; } // --------------------------------------------------------------------------- @@ -714,7 +722,14 @@ if (!isEntrypoint(import.meta.url)) { // Imported (another gate's self-test, or a measurement helper). Walking the // tree as an import side effect would make this file impossible to reuse. } else if (process.argv.includes('--self-test')) { - selfTest(); + if (selfTest() !== SELF_TEST_VERDICT) { + console.error( + '\n✗ check-logger-receiver-detach self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } } else if (process.argv.includes('--census')) { const { files, findings } = measure(); console.log(`census: ${files.length} non-test TS file(s) under ${SCAN_ROOTS.join(', ')}`); diff --git a/scripts/check-merge-queue-triage-outcome.mjs b/scripts/check-merge-queue-triage-outcome.mjs index 357a4125fb..e258d3ef8b 100644 --- a/scripts/check-merge-queue-triage-outcome.mjs +++ b/scripts/check-merge-queue-triage-outcome.mjs @@ -1215,6 +1215,12 @@ const MUTATIONS = [ }, ]; +// Returned by `selfTest()` only after its verdict is printed. The dispatch +// refuses anything else: a `return` that leaves the function above that line +// prints nothing and still exits 0 — a self-test that never finished, reported +// as one that passed (#13798). +const SELF_TEST_VERDICT = 'check-merge-queue-triage-outcome self-test reached its verdict'; + async function selfTest() { const root = repoRoot(); const { source, problems } = extractScript(root); @@ -1297,6 +1303,8 @@ async function selfTest() { `✓ check-merge-queue-triage-outcome --self-test: ${checked} assertions, ` + `${MUTATIONS.length} mutations of the shipped script each driven to red.`, ); + + return SELF_TEST_VERDICT; } // The CLI dispatch is guarded so that IMPORTING this module is inert: the @@ -1304,7 +1312,16 @@ async function selfTest() { // tree, and a module that ran its gate on import would silently judge THIS repo // instead and print a pass about the wrong subject. if (isEntrypoint(import.meta.url)) { - if (process.argv.includes('--self-test')) await selfTest(); + if (process.argv.includes('--self-test')) { + if ((await selfTest()) !== SELF_TEST_VERDICT) { + console.error( + '\n✗ check-merge-queue-triage-outcome self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + } else if (process.argv.includes('--list')) list(); else await main(); } diff --git a/scripts/check-meta-type-normalized.mjs b/scripts/check-meta-type-normalized.mjs index 848e71b0a2..7aab9ed79d 100644 --- a/scripts/check-meta-type-normalized.mjs +++ b/scripts/check-meta-type-normalized.mjs @@ -187,6 +187,13 @@ function findViolations(file) { * the pattern quoted inside a comment. A guard nobody tests is a guard that * silently stops matching. */ + +// Returned by `selfTest()` only after its verdict is printed. The dispatch +// refuses anything else: a `return` that leaves the function above that line +// prints nothing and still exits 0 — a self-test that never finished, reported +// as one that passed (#13798). +const SELF_TEST_VERDICT = 'check-meta-type-normalized self-test reached its verdict'; + function selfTest() { const fixture = ` // if (req.params.type === 'doc') {} -- quoted in a line comment @@ -228,11 +235,20 @@ function selfTest() { process.exit(1); } console.log('check:meta-type-normalized --self-test passed (5 shapes caught, pass-through and comments untouched)'); + + return SELF_TEST_VERDICT; } function main() { if (process.argv.includes('--self-test')) { - selfTest(); + if (selfTest() !== SELF_TEST_VERDICT) { + console.error( + '\n✗ check-meta-type-normalized self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } return; } selfTest(); diff --git a/scripts/check-nul-bytes.mjs b/scripts/check-nul-bytes.mjs index 944267042b..e8b13d9569 100644 --- a/scripts/check-nul-bytes.mjs +++ b/scripts/check-nul-bytes.mjs @@ -704,6 +704,12 @@ function checkCharClassReferences(root, assert) { } } +// Returned by `selfTest()` only after its verdict is printed. The dispatch +// refuses anything else: a `return` that leaves the function above that line +// prints nothing and still exits 0 — a self-test that never finished, reported +// as one that passed (#13798). +const SELF_TEST_VERDICT = 'check-nul-bytes self-test reached its verdict'; + function selfTest() { const failures = []; // Counted rather than written down: some assertions run inside a loop, and a @@ -1154,6 +1160,8 @@ function selfTest() { process.exit(1); } console.log(`✓ check-nul-bytes --self-test: ${checked} assertions over a temp git repo (real scan() path)`); + + return SELF_TEST_VERDICT; } // Exports bindings, so an import for those exports alone must run nothing (#10667). @@ -1162,7 +1170,14 @@ const invokedDirectly = isEntrypoint(import.meta.url); if (!invokedDirectly) { // imported as a module — expose the exports and do nothing else } else if (process.argv.includes('--self-test')) { - selfTest(); + if (selfTest() !== SELF_TEST_VERDICT) { + console.error( + '\n✗ check-nul-bytes self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } } else if (process.argv.includes('--list')) { const result = scan(repoRoot()); for (const f of result.skipped.binary) console.log(`binary ${f}`); diff --git a/scripts/check-objectql-double-limit.mjs b/scripts/check-objectql-double-limit.mjs index a46bd2ffed..5437e14f46 100644 --- a/scripts/check-objectql-double-limit.mjs +++ b/scripts/check-objectql-double-limit.mjs @@ -1089,6 +1089,12 @@ async function judgeFixture(src) { return { found, results }; } +// Returned by `selfTest()` only after its verdict is printed. The dispatch +// refuses anything else: a `return` that leaves the function above that line +// prints nothing and still exits 0 — a self-test that never finished, reported +// as one that passed (#13798). +const SELF_TEST_VERDICT = 'check-objectql-double-limit self-test reached its verdict'; + async function selfTest() { const failures = []; const expect = (label, cond) => { if (!cond) failures.push(label); }; @@ -1252,6 +1258,8 @@ async function selfTest() { ' table map and through a module-scope fixture are both driven; the ledger\n' + ' reconciles in both directions.', ); + + return SELF_TEST_VERDICT; } // --------------------------------------------------------------------------- @@ -1281,7 +1289,14 @@ if (!isEntrypoint(import.meta.url)) { // corpus scan as an import side effect would make this file impossible to // reuse without also failing someone else's process. } else if (process.argv.includes('--self-test')) { - await selfTest(); + if ((await selfTest()) !== SELF_TEST_VERDICT) { + console.error( + '\n✗ check-objectql-double-limit self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } } else if (process.argv.includes('--census')) { const { measured, census } = await measure(); reportCensus(census, ''); diff --git a/scripts/check-optional-error-sink-contract.mjs b/scripts/check-optional-error-sink-contract.mjs index 66624fba96..315d9e5a8b 100644 --- a/scripts/check-optional-error-sink-contract.mjs +++ b/scripts/check-optional-error-sink-contract.mjs @@ -794,6 +794,14 @@ function run({ list = false } = {}) { * `expectSinks` / `expectCast` / `expectImpure` / `expectNoError` can tell the * two apart, so each case states all of them. */ + +// Set by `selfTest()` only after its verdict is printed, and read at the +// dispatch: a `return` that leaves the function above that line prints nothing +// and still exits 0 — a self-test that never finished, reported as one that +// passed (#13798). The self-test's own exit code stays load-bearing, so the +// handshake is a flag rather than a returned sentinel. +let selfTestReachedVerdict = false; + function selfTest() { const cases = [ { @@ -1073,11 +1081,21 @@ function selfTest() { `all three narrowings pinned as counts, prefilter pinned over ${LOG_CHANNELS.size} channel(s) ` + '× 4 spellings plus its reject side.', ); + selfTestReachedVerdict = true; return 0; } const argv = process.argv.slice(2); if (argv.includes('--self-test')) { - process.exit(selfTest()); + const selfTestCode = selfTest(); + if (!selfTestReachedVerdict) { + console.error( + '\n✗ check-optional-error-sink-contract self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + process.exit(selfTestCode); } process.exit(run({ list: argv.includes('--list') })); diff --git a/scripts/check-org-identifier.mjs b/scripts/check-org-identifier.mjs index 153a0ae42f..50237d50af 100644 --- a/scripts/check-org-identifier.mjs +++ b/scripts/check-org-identifier.mjs @@ -583,6 +583,14 @@ export function countSessionBindings(text, file) { * that a widened alias list could satisfy would be that harness. These can be * satisfied only by resolving where the value came from. */ + +// Set by `selfTest()` only after its verdict is printed, and read at the +// dispatch: a `return` that leaves the function above that line prints nothing +// and still exits 0 — a self-test that never finished, reported as one that +// passed (#13798). The self-test's own exit code stays load-bearing, so the +// handshake is a flag rather than a returned sentinel. +let selfTestReachedVerdict = false; + function selfTest() { const BT = String.fromCharCode(96); // backtick, kept out of the literals below const cases = [ @@ -686,6 +694,7 @@ function selfTest() { process.exit(1); } console.log(`✓ check-org-identifier self-test: ${cases.length + 1} cases pass.`); + selfTestReachedVerdict = true; } function sourceFiles(root) { @@ -710,7 +719,18 @@ function repoRoot() { } function main() { - if (process.argv.includes('--self-test')) return selfTest(); + if (process.argv.includes('--self-test')) { + const selfTestCode = selfTest(); + if (!selfTestReachedVerdict) { + console.error( + '\n✗ check-org-identifier self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + return selfTestCode; + } const root = repoRoot(); const files = sourceFiles(root); diff --git a/scripts/check-overlay-whitelist-table.mjs b/scripts/check-overlay-whitelist-table.mjs index e51fff82ed..0daaee2832 100644 --- a/scripts/check-overlay-whitelist-table.mjs +++ b/scripts/check-overlay-whitelist-table.mjs @@ -839,6 +839,13 @@ function run(registryText, tableText) { return { structure: [], leg1, leg2, entries: reg.entries, rows: tab.rows }; } +// Set by `selfTest()` only after its verdict is printed, and read at the +// dispatch: a `return` that leaves the function above that line prints nothing +// and still exits 0 — a self-test that never finished, reported as one that +// passed (#13798). The self-test's own exit code stays load-bearing, so the +// handshake is a flag rather than a returned sentinel. +let selfTestReachedVerdict = false; + export function selfTest() { const failures = []; const check = (name, ok, detail = '') => { @@ -1125,6 +1132,7 @@ export function selfTest() { 'table is green on all three legs, leg 3 goes red on a stale total and on a stale ' + `true-set independently, and ${refusals} structural/parser cases are refused.`, ); + selfTestReachedVerdict = true; } // --------------------------------------------------------------------------- @@ -1132,7 +1140,18 @@ export function selfTest() { // --------------------------------------------------------------------------- function main() { - if (process.argv.includes('--self-test')) return selfTest(); + if (process.argv.includes('--self-test')) { + const selfTestCode = selfTest(); + if (!selfTestReachedVerdict) { + console.error( + '\n✗ check-overlay-whitelist-table self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + return selfTestCode; + } const registryText = readFileSync(join(ROOT, REGISTRY_FILE), 'utf8'); const docText = readFileSync(join(ROOT, DOC_FILE), 'utf8'); diff --git a/scripts/check-override-consistency.mjs b/scripts/check-override-consistency.mjs index 655ff7689c..142dc2a6aa 100644 --- a/scripts/check-override-consistency.mjs +++ b/scripts/check-override-consistency.mjs @@ -429,6 +429,12 @@ const FIXTURE_LOCKFILE = [ '', ].join('\n'); +// Returned by `selfTest()` only after its verdict is printed. The dispatch +// refuses anything else: a `return` that leaves the function above that line +// prints nothing and still exits 0 — a self-test that never finished, reported +// as one that passed (#13798). +const SELF_TEST_VERDICT = 'check-override-consistency self-test reached its verdict'; + function selfTest() { const index = buildConsumerIndex(FIXTURE_LOCKFILE); const idleKeys = (keys) => @@ -562,11 +568,20 @@ function selfTest() { '\n zero-consumer and self-expiring overrides are, and the manifest rule still' + '\n separates reachable declarations from unreachable ones.', ); + + return SELF_TEST_VERDICT; } function main() { if (process.argv.includes('--self-test')) { - selfTest(); + if (selfTest() !== SELF_TEST_VERDICT) { + console.error( + '\n✗ check-override-consistency self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } return; } const overrides = readOverrides(); diff --git a/scripts/check-parse-guard.mjs b/scripts/check-parse-guard.mjs index 1dee44e21a..e198c5684f 100644 --- a/scripts/check-parse-guard.mjs +++ b/scripts/check-parse-guard.mjs @@ -529,6 +529,13 @@ export function reportOutside(rows) { // Self-test -- fixture sources, not this tree // --------------------------------------------------------------------------- +// Set by `selfTest()` only after its verdict is printed, and read at the +// dispatch: a `return` that leaves the function above that line prints nothing +// and still exits 0 — a self-test that never finished, reported as one that +// passed (#13798). The self-test's own exit code stays load-bearing, so the +// handshake is a flag rather than a returned sentinel. +let selfTestReachedVerdict = false; + export function selfTest() { const cases = []; const t = (name, ok, detail) => cases.push({ name, ok: Boolean(ok), detail }); @@ -745,10 +752,22 @@ export function selfTest() { + `the owning package.json, so no row is printed under a reason that is false of it) -- plus the ` + `dispatch-gates scan surface, derived from the walked root, with the census side held out of it.`, ); + selfTestReachedVerdict = true; return 0; } if (isEntrypoint(import.meta.url)) { - if (process.argv.includes('--self-test')) process.exit(selfTest()); + if (process.argv.includes('--self-test')) { + const selfTestCode = selfTest(); + if (!selfTestReachedVerdict) { + console.error( + '\n✗ check-parse-guard self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + process.exit(selfTestCode); + } process.exit(main()); } diff --git a/scripts/check-partof-closing-keyword.mjs b/scripts/check-partof-closing-keyword.mjs index 460021ceb8..03afadf7ba 100644 --- a/scripts/check-partof-closing-keyword.mjs +++ b/scripts/check-partof-closing-keyword.mjs @@ -212,6 +212,12 @@ export function judge(ctx) { // activity type without which a reworded body can never go green. // --------------------------------------------------------------------------- +// Returned by `selfTest()` only after its verdict is printed. The dispatch +// refuses anything else: a `return` that leaves the function above that line +// prints nothing and still exits 0 — a self-test that never finished, reported +// as one that passed (#13798). +const SELF_TEST_VERDICT = 'check-partof-closing-keyword self-test reached its verdict'; + function selfTest() { const cases = []; const t = (name, actual, expected) => cases.push([name, actual, expected]); @@ -351,6 +357,8 @@ function selfTest() { process.exit(1); } console.log(`✓ check-partof-closing-keyword self-test: ${cases.length} cases pass.`); + + return SELF_TEST_VERDICT; } // The basename comparison, as in the sweep: this file is imported by nothing @@ -358,7 +366,14 @@ function selfTest() { const isMain = isEntrypoint(import.meta.url); if (isMain) { if (process.argv.includes('--self-test')) { - selfTest(); + if (selfTest() !== SELF_TEST_VERDICT) { + console.error( + '\n✗ check-partof-closing-keyword self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } } else { const result = judge(readPrContext(process.env)); const emit = result.exit === EXIT_CLEAN ? console.log : console.error; diff --git a/scripts/check-plugin-teardown-shape.mjs b/scripts/check-plugin-teardown-shape.mjs index 81deb0d35f..b63cd9fe79 100644 --- a/scripts/check-plugin-teardown-shape.mjs +++ b/scripts/check-plugin-teardown-shape.mjs @@ -532,6 +532,13 @@ function auditRoot(root) { // Self-test -- fixture trees, and one REAL revision // --------------------------------------------------------------------------- +// Set by `selfTest()` only after its verdict is printed, and read at the +// dispatch: a `return` that leaves the function above that line prints nothing +// and still exits 0 — a self-test that never finished, reported as one that +// passed (#13798). The self-test's own exit code stays load-bearing, so the +// handshake is a flag rather than a returned sentinel. +let selfTestReachedVerdict = false; + export function selfTest() { const cases = []; const t = (name, ok, detail) => cases.push({ name, ok: Boolean(ok), detail }); @@ -757,12 +764,24 @@ export function selfTest() { + `(real pre-#10375 fixture reds, the repaired file and both delegating-alias directions stay green, ` + `every roster name reds, every excluded name stays green, and all five refusals are paired with a tree that still returns a verdict).`, ); + selfTestReachedVerdict = true; return 0; } if (isEntrypoint(import.meta.url)) { const argv = process.argv.slice(2); - if (argv.includes('--self-test')) process.exit(selfTest()); + if (argv.includes('--self-test')) { + const selfTestCode = selfTest(); + if (!selfTestReachedVerdict) { + console.error( + '\n✗ check-plugin-teardown-shape self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + process.exit(selfTestCode); + } else if (argv.includes('--list')) process.exit(list()); else if (argv.includes('--audit-root')) process.exit(auditRoot(argv[argv.indexOf('--audit-root') + 1])); else process.exit(main()); diff --git a/scripts/check-pnpm-acquisition.mjs b/scripts/check-pnpm-acquisition.mjs index 1141dbe7fb..41bbb791c2 100644 --- a/scripts/check-pnpm-acquisition.mjs +++ b/scripts/check-pnpm-acquisition.mjs @@ -361,6 +361,13 @@ function withFixture(files, fn) { } } +// Set by `selfTest()` only after its verdict is printed, and read at the +// dispatch: a `return` that leaves the function above that line prints nothing +// and still exits 0 — a self-test that never finished, reported as one that +// passed (#13798). The self-test's own exit code stays load-bearing, so the +// handshake is a flag rather than a returned sentinel. +let selfTestReachedVerdict = false; + export function selfTest() { const cases = []; const t = (name, ok, detail) => cases.push({ name, ok: Boolean(ok), detail }); @@ -548,6 +555,7 @@ export function selfTest() { return 1; } console.log(`check-pnpm-acquisition --self-test: ${cases.length} cases pass (real fixture roots through the real scan()).`); + selfTestReachedVerdict = true; return 0; } @@ -560,7 +568,18 @@ function list() { // Exports bindings, so an import for those exports alone must run nothing. if (isEntrypoint(import.meta.url)) { - if (process.argv.includes('--self-test')) process.exit(selfTest()); + if (process.argv.includes('--self-test')) { + const selfTestCode = selfTest(); + if (!selfTestReachedVerdict) { + console.error( + '\n✗ check-pnpm-acquisition self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + process.exit(selfTestCode); + } else if (process.argv.includes('--list')) process.exit(list()); else process.exit(main()); } diff --git a/scripts/check-position-name-fold-loaders.mjs b/scripts/check-position-name-fold-loaders.mjs index 472d0fb1b3..e220dd283b 100644 --- a/scripts/check-position-name-fold-loaders.mjs +++ b/scripts/check-position-name-fold-loaders.mjs @@ -290,6 +290,13 @@ function report(loaders) { ); } +// Set by `selfTest()` only after its verdict is printed, and read at the +// dispatch: a `return` that leaves the function above that line prints nothing +// and still exits 0 — a self-test that never finished, reported as one that +// passed (#13798). The self-test's own exit code stays load-bearing, so the +// handshake is a flag rather than a returned sentinel. +let selfTestReachedVerdict = false; + function selfTest() { let failed = 0; let cases = 0; @@ -361,10 +368,22 @@ function selfTest() { process.exit(1); } console.log(`\n✓ check-position-name-fold-loaders self-test: ${cases} cases pass.`); + selfTestReachedVerdict = true; } function main() { - if (process.argv.includes('--self-test')) return selfTest(); + if (process.argv.includes('--self-test')) { + const selfTestCode = selfTest(); + if (!selfTestReachedVerdict) { + console.error( + '\n✗ check-position-name-fold-loaders self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + return selfTestCode; + } const { scanned, references } = scanTree(); diff --git a/scripts/check-prerelease-pin-watch.mjs b/scripts/check-prerelease-pin-watch.mjs index 23428da531..a360307dd8 100644 --- a/scripts/check-prerelease-pin-watch.mjs +++ b/scripts/check-prerelease-pin-watch.mjs @@ -547,6 +547,13 @@ export function render(result, { verbose = false } = {}) { // CLI // --------------------------------------------------------------------------- +// Set by `selfTest()` only after its verdict is printed, and read at the +// dispatch: a `return` that leaves the function above that line prints nothing +// and still exits 0 — a self-test that never finished, reported as one that +// passed (#13798). The self-test's own exit code stays load-bearing, so the +// handshake is a flag rather than a returned sentinel. +let selfTestReachedVerdict = false; + async function main(argv) { const has = (f) => argv.includes(f); const val = (f, d) => { @@ -564,7 +571,18 @@ async function main(argv) { ); return 0; } - if (has('--self-test')) return selfTest(); + if (has('--self-test')) { + const selfTestCode = selfTest(); + if (!selfTestReachedVerdict) { + console.error( + '\n✗ check-prerelease-pin-watch self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + return selfTestCode; + } const strict = has('--strict'); const asJson = has('--json'); @@ -991,6 +1009,7 @@ function selfTest() { return 1; } console.log('✓ check-prerelease-pin-watch --self-test: all checks passed'); + selfTestReachedVerdict = true; return 0; } diff --git a/scripts/check-published-files.mjs b/scripts/check-published-files.mjs index b1e7814206..6a392a8546 100644 --- a/scripts/check-published-files.mjs +++ b/scripts/check-published-files.mjs @@ -423,6 +423,13 @@ function exportsVerdict(manifest) { * into noise, and one that under-matches makes SUFFICIENT wave a broken package * through. Both failures are silent, so they get asserted rather than assumed. */ + +// Returned by `selfTest()` only after its verdict is printed. The dispatch +// refuses anything else: a `return` that leaves the function above that line +// prints nothing and still exits 0 — a self-test that never finished, reported +// as one that passed (#13798). +const SELF_TEST_VERDICT = 'check-published-files self-test reached its verdict'; + function selfTest() { const cases = [ ['dist', 'dist/index.js', true], @@ -609,10 +616,19 @@ function selfTest() { `${liveDeclaring} of ${livePublishable} live) and the shared workspace enumerator's own ` + `assertions, over ${liveGlobs.length} live workspace glob(s).`, ); + + return SELF_TEST_VERDICT; } if (process.argv.includes('--self-test')) { - selfTest(); + if (selfTest() !== SELF_TEST_VERDICT) { + console.error( + '\n✗ check-published-files self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } process.exit(0); } diff --git a/scripts/check-published-list-mirrors.mjs b/scripts/check-published-list-mirrors.mjs index 30fb41bf0c..434e1b10d3 100644 --- a/scripts/check-published-list-mirrors.mjs +++ b/scripts/check-published-list-mirrors.mjs @@ -354,6 +354,12 @@ async function main() { ); } +// Returned by `selfTest()` only after its verdict is printed. The dispatch +// refuses anything else: a `return` that leaves the function above that line +// prints nothing and still exits 0 — a self-test that never finished, reported +// as one that passed (#13798). +const SELF_TEST_VERDICT = 'check-published-list-mirrors self-test reached its verdict'; + async function selfTest() { const cases = []; const ok = (label, cond) => cases.push({ label, cond }); @@ -481,9 +487,20 @@ async function selfTest() { process.exit(1); } console.log(`\nAll ${cases.length} self-test cases passed.`); + + return SELF_TEST_VERDICT; } if (isEntrypoint(import.meta.url)) { - if (process.argv.includes('--self-test')) await selfTest(); + if (process.argv.includes('--self-test')) { + if ((await selfTest()) !== SELF_TEST_VERDICT) { + console.error( + '\n✗ check-published-list-mirrors self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + } else await main(); } diff --git a/scripts/check-published-readme-exports.mjs b/scripts/check-published-readme-exports.mjs index def94f7603..4fb77339c7 100644 --- a/scripts/check-published-readme-exports.mjs +++ b/scripts/check-published-readme-exports.mjs @@ -2067,6 +2067,12 @@ function run({ unreadReport: wantsUnreadReport = false } = {}) { // to exclude must produce none. // --------------------------------------------------------------------------- +// Returned by `selfTest()` only after its verdict is printed. The dispatch +// refuses anything else: a `return` that leaves the function above that line +// prints nothing and still exits 0 — a self-test that never finished, reported +// as one that passed (#13798). +const SELF_TEST_VERDICT = 'check-published-readme-exports self-test reached its verdict'; + function selfTest() { const failures = []; const eq = (label, actual, expected) => { @@ -3493,6 +3499,8 @@ function selfTest() { ' population as resolved/total, so a recogniser that stops matching shows up as a\n' + ' denominator that fell rather than as a defect count that never moved.', ); + + return SELF_TEST_VERDICT; } /* Run only when invoked as a program — `publishedDocs` and the extractors are @@ -3500,7 +3508,14 @@ function selfTest() { * import itself building a TypeScript program and sweeping the workspace. */ if (isEntrypoint(import.meta.url)) { if (process.argv.includes('--self-test')) { - selfTest(); + if (selfTest() !== SELF_TEST_VERDICT) { + console.error( + '\n✗ check-published-readme-exports self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } process.exit(0); } try { diff --git a/scripts/check-published-readme-links.mjs b/scripts/check-published-readme-links.mjs index 85b96db20f..b121dd57ec 100644 --- a/scripts/check-published-readme-links.mjs +++ b/scripts/check-published-readme-links.mjs @@ -725,6 +725,12 @@ async function run() { // Self-test: every limb observed FAILING and observed SILENT. // --------------------------------------------------------------------------- +// Returned by `selfTest()` only after its verdict is printed. The dispatch +// refuses anything else: a `return` that leaves the function above that line +// prints nothing and still exits 0 — a self-test that never finished, reported +// as one that passed (#13798). +const SELF_TEST_VERDICT = 'check-published-readme-links self-test reached its verdict'; + function selfTest() { const failures = []; const ok = (label, cond) => { @@ -1206,6 +1212,8 @@ function selfTest() { + ' pointy-bracket destination counted exactly ONCE; and angle-bracketed non-links —\n' + ' tags, prose, bare addresses — claimed by nothing).', ); + + return SELF_TEST_VERDICT; } /* Run only when invoked as a program — the extractor, the classifier and the @@ -1213,7 +1221,14 @@ function selfTest() { * without the import itself sweeping the workspace. */ if (isEntrypoint(import.meta.url)) { if (process.argv.includes('--self-test')) { - selfTest(); + if (selfTest() !== SELF_TEST_VERDICT) { + console.error( + '\n✗ check-published-readme-links self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } process.exit(0); } try { diff --git a/scripts/check-query-options-erasure-ratchet.mjs b/scripts/check-query-options-erasure-ratchet.mjs index ea17669445..64ad4f2e54 100644 --- a/scripts/check-query-options-erasure-ratchet.mjs +++ b/scripts/check-query-options-erasure-ratchet.mjs @@ -494,6 +494,12 @@ const GUARD_CLOSURE_CASES = [ [[FAKE_HELPER, RAW_CALL]]], ]; +// Returned by `selfTest()` only after its verdict is printed. The dispatch +// refuses anything else: a `return` that leaves the function above that line +// prints nothing and still exits 0 — a self-test that never finished, reported +// as one that passed (#13798). +const SELF_TEST_VERDICT = 'check-query-options-erasure-ratchet self-test reached its verdict'; + async function selfTest() { const failures = []; const assert = (cond, msg) => { if (!cond) failures.push(msg); }; @@ -904,6 +910,8 @@ async function selfTest() { `over ${GUARD_CLOSURE_CASES.length} synthetic tree(s)), ` + `and ${HEADROOM_CANARY_FILE} parses at --stack-size=${PARSER_STACK_SIZE_KB} through this gate's own channel.`, ); + + return SELF_TEST_VERDICT; } // --------------------------------------------------------------------------- @@ -1001,7 +1009,14 @@ async function main() { // full ESLint passes over packages/** inside the importer. if (isEntrypoint(import.meta.url)) { if (process.argv.includes('--self-test')) { - await selfTest(); + if ((await selfTest()) !== SELF_TEST_VERDICT) { + console.error( + '\n✗ check-query-options-erasure-ratchet self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } process.exit(0); } await main(); diff --git a/scripts/check-quick-reference-counts.mjs b/scripts/check-quick-reference-counts.mjs index e3ffc98a38..8e219152c2 100644 --- a/scripts/check-quick-reference-counts.mjs +++ b/scripts/check-quick-reference-counts.mjs @@ -530,6 +530,13 @@ const GOOD_PAGE = [ '', ].join('\n'); +// Set by `selfTest()` only after its verdict is printed, and read at the +// dispatch: a `return` that leaves the function above that line prints nothing +// and still exits 0 — a self-test that never finished, reported as one that +// passed (#13798). The self-test's own exit code stays load-bearing, so the +// handshake is a flag rather than a returned sentinel. +let selfTestReachedVerdict = false; + function selfTest() { const failures = []; const expect = (label, got, want) => { @@ -786,10 +793,22 @@ function selfTest() { process.exit(1); } console.log('✓ check-quick-reference-counts self-test: 22 cases pass.'); + selfTestReachedVerdict = true; } function main() { - if (process.argv.includes('--self-test')) return selfTest(); + if (process.argv.includes('--self-test')) { + const selfTestCode = selfTest(); + if (!selfTestReachedVerdict) { + console.error( + '\n✗ check-quick-reference-counts self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + return selfTestCode; + } const full = join(ROOT, TARGET); if (!existsSync(full)) { diff --git a/scripts/check-ratchet-remedy-authority.mjs b/scripts/check-ratchet-remedy-authority.mjs index 54b9bca37b..54f604a19a 100644 --- a/scripts/check-ratchet-remedy-authority.mjs +++ b/scripts/check-ratchet-remedy-authority.mjs @@ -1011,6 +1011,13 @@ function main() { // approved everything, or a sweep that read nothing, would keep the corpus run // green with the convention entirely gone. +// Set by `selfTest()` only after its verdict is printed, and read at the +// dispatch: a `return` that leaves the function above that line prints nothing +// and still exits 0 — a self-test that never finished, reported as one that +// passed (#13798). The self-test's own exit code stays load-bearing, so the +// handshake is a flag rather than a returned sentinel. +let selfTestReachedVerdict = false; + function selfTest() { const failures = []; const expect = (label, cond) => { if (!cond) failures.push(label); }; @@ -1269,6 +1276,7 @@ function selfTest() { + 'itself, refusal is told apart from discouragement, and the sweep still reaches every known ' + 'instance.', ); + selfTestReachedVerdict = true; process.exit(0); } @@ -1277,6 +1285,16 @@ const invokedDirectly = isEntrypoint(import.meta.url); if (!invokedDirectly) { // imported as a module — expose the exports and do nothing else -} else if (process.argv.includes('--self-test')) selfTest(); +} else if (process.argv.includes('--self-test')) { + selfTest(); + if (!selfTestReachedVerdict) { + console.error( + '\n✗ check-ratchet-remedy-authority self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } +} else if (process.argv.includes('--list')) list(); else main(); diff --git a/scripts/check-react-page-adapter-contract.mjs b/scripts/check-react-page-adapter-contract.mjs index 0a93d818f9..1021defd70 100644 --- a/scripts/check-react-page-adapter-contract.mjs +++ b/scripts/check-react-page-adapter-contract.mjs @@ -708,6 +708,13 @@ function report({ population, findings, censusProblems }) { // Self-test // --------------------------------------------------------------------------- +// Set by `selfTest()` only after its verdict is printed, and read at the +// dispatch: a `return` that leaves the function above that line prints nothing +// and still exits 0 — a self-test that never finished, reported as one that +// passed (#13798). The self-test's own exit code stays load-bearing, so the +// handshake is a flag rather than a returned sentinel. +let selfTestReachedVerdict = false; + export function selfTest() { const failures = []; let checked = 0; @@ -976,6 +983,7 @@ export function selfTest() { + `the selector observed refusing the engine.find / useQuery / webhook shapes it must not fabricate on, ` + `and an empty sweep of EITHER half observed failing the census.`, ); + selfTestReachedVerdict = true; return 0; } @@ -990,5 +998,17 @@ function main() { } if (isEntrypoint(import.meta.url)) { - process.exit(process.argv.includes('--self-test') ? selfTest() : main()); + if (process.argv.includes('--self-test')) { + const selfTestCode = selfTest(); + if (!selfTestReachedVerdict) { + console.error( + '\n✗ check-react-page-adapter-contract self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + process.exit(selfTestCode); + } + process.exit(main()); } diff --git a/scripts/check-refd-timer-probe.mjs b/scripts/check-refd-timer-probe.mjs index 63e9dd93f8..a46f505e6a 100644 --- a/scripts/check-refd-timer-probe.mjs +++ b/scripts/check-refd-timer-probe.mjs @@ -262,6 +262,13 @@ export const refdTimeouts = () => process.${PROBE}().filter((r) => r === 'Timeout').length; `; +// Set by `selfTest()` only after its verdict is printed, and read at the +// dispatch: a `return` that leaves the function above that line prints nothing +// and still exits 0 — a self-test that never finished, reported as one that +// passed (#13798). The self-test's own exit code stays load-bearing, so the +// handshake is a flag rather than a returned sentinel. +let selfTestReachedVerdict = false; + function selfTest() { const cases = [ { @@ -419,12 +426,24 @@ function selfTest() { process.exit(1); } console.log(`\n✓ check-refd-timer-probe self-test: ${cases.length} cases pass, negative controls included.`); + selfTestReachedVerdict = true; } // --------------------------------------------------------------------------- function main() { - if (process.argv.includes('--self-test')) return selfTest(); + if (process.argv.includes('--self-test')) { + const selfTestCode = selfTest(); + if (!selfTestReachedVerdict) { + console.error( + '\n✗ check-refd-timer-probe self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + return selfTestCode; + } const tree = readTree(); const { problems, sites } = judge(tree); diff --git a/scripts/check-release-page-status.mjs b/scripts/check-release-page-status.mjs index b42d052ded..d33f860199 100644 --- a/scripts/check-release-page-status.mjs +++ b/scripts/check-release-page-status.mjs @@ -536,6 +536,13 @@ const CURRENT_INDEX_V17 = + 'against the routes the server actually mounts (21 dead methods out, 40+ real ones in) ' + '(current series: 17.0.0, released 2026-08-14).'; +// Set by `selfTest()` only after its verdict is printed, and read at the +// dispatch: a `return` that leaves the function above that line prints nothing +// and still exits 0 — a self-test that never finished, reported as one that +// passed (#13798). The self-test's own exit code stays load-bearing, so the +// handshake is a flag rather than a returned sentinel. +let selfTestReachedVerdict = false; + function selfTest() { const failures = []; const expect = (label, cond) => { @@ -798,6 +805,7 @@ function selfTest() { + '"nothing shipped", the two real stale statuses and both stale index entries go RED, the two ' + 'corrected ones stay GREEN with their RC trains named, and the cutoff is printed on both paths.', ); + selfTestReachedVerdict = true; process.exit(0); } @@ -894,6 +902,16 @@ function main() { // scope floor from it), and unguarded the whole gate ran inside any importer, // printing its verdict over theirs. if (isEntrypoint(import.meta.url)) { - if (process.argv.includes('--self-test')) selfTest(); + if (process.argv.includes('--self-test')) { + selfTest(); + if (!selfTestReachedVerdict) { + console.error( + '\n✗ check-release-page-status self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + } main(); } diff --git a/scripts/check-release-section-coverage.mjs b/scripts/check-release-section-coverage.mjs index b25cc84ab5..30d13ebec7 100644 --- a/scripts/check-release-section-coverage.mjs +++ b/scripts/check-release-section-coverage.mjs @@ -608,6 +608,13 @@ const CURRENT_INDEX_V16 = const NO_PARENTHETICAL_INDEX_V13 = '- [v13.0.0](/docs/releases/v13) — Permission Model v2 (ADR-0090): Roles and Profiles converge.'; +// Set by `selfTest()` only after its verdict is printed, and read at the +// dispatch: a `return` that leaves the function above that line prints nothing +// and still exits 0 — a self-test that never finished, reported as one that +// passed (#13798). The self-test's own exit code stays load-bearing, so the +// handshake is a flag rather than a returned sentinel. +let selfTestReachedVerdict = false; + export function selfTest() { // ⛔ FIRST, and it RETURNS rather than recording a case. The floor lives in // another file; a read that did not happen measured nothing — least of all @@ -969,6 +976,7 @@ export function selfTest() { + 'with a read that could not happen refusing as PREREQUISITE NOT MET instead of rendering as a ' + 'verdict about that gate.', ); + selfTestReachedVerdict = true; return EXIT_OK; } @@ -1069,7 +1077,18 @@ function annotate(findings) { // ── Run ────────────────────────────────────────────────────────────────────── function main(argv) { - if (argv.includes('--self-test')) return selfTest(); + if (argv.includes('--self-test')) { + const selfTestCode = selfTest(); + if (!selfTestReachedVerdict) { + console.error( + '\n✗ check-release-section-coverage self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + return selfTestCode; + } const strict = argv.includes('--strict'); const versions = gaVersions(readFileSync(SPEC_CHANGELOG, 'utf8')); diff --git a/scripts/check-required-contexts.mjs b/scripts/check-required-contexts.mjs index 4212fdd018..cfaae92c85 100644 --- a/scripts/check-required-contexts.mjs +++ b/scripts/check-required-contexts.mjs @@ -1576,6 +1576,12 @@ async function main() { // ── Self-test ─────────────────────────────────────────────────────────────── +// Returned by `selfTest()` only after its verdict is printed. The dispatch +// refuses anything else: a `return` that leaves the function above that line +// prints nothing and still exits 0 — a self-test that never finished, reported +// as one that passed (#13798). +const SELF_TEST_VERDICT = 'check-required-contexts self-test reached its verdict'; + async function selfTest() { const failures = []; let checked = 0; @@ -2872,6 +2878,8 @@ async function selfTest() { `the live required-set diff and its off-the-required-path wiring (#9642), with the comment-vs-code recognizer ` + `pinned in both directions + the #4690 pins).`, ); + + return SELF_TEST_VERDICT; } // Exports bindings, so an import for those exports alone must run nothing (#10667). @@ -2880,7 +2888,14 @@ const invokedDirectly = isEntrypoint(import.meta.url); if (!invokedDirectly) { // imported as a module — expose the exports and do nothing else } else if (process.argv.includes('--self-test')) { - await selfTest(); + if ((await selfTest()) !== SELF_TEST_VERDICT) { + console.error( + '\n✗ check-required-contexts self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } } else if (process.argv.includes('--verify-required-set')) { // Report-only, off the required path (#9642). Exit 0 = swept (0 or N // disagreements); exit 2 = the live set could not be read (ENVIRONMENT). diff --git a/scripts/check-resume-authority-declared.mjs b/scripts/check-resume-authority-declared.mjs index 0e4c22dcc7..aca54b54c4 100644 --- a/scripts/check-resume-authority-declared.mjs +++ b/scripts/check-resume-authority-declared.mjs @@ -334,6 +334,12 @@ function report({ list = false, scanRoots = DEFAULT_SCAN_ROOTS } = {}) { // both sides of every decision it makes, so a refactor that neuters it fails // here rather than turning every future PR green. +// Returned by `selfTest()` only after its verdict is printed. The dispatch +// refuses anything else: a `return` that leaves the function above that line +// prints nothing and still exits 0 — a self-test that never finished, reported +// as one that passed (#13798). +const SELF_TEST_VERDICT = 'check-resume-authority-declared self-test reached its verdict'; + function selfTest() { const failures = []; const expect = (label, cond) => { if (!cond) failures.push(label); }; @@ -448,11 +454,22 @@ const b = defineActionDescriptor({ type: 'b', version: '1.0.0', name: 'B', suppo + 'independently, treats a non-literal argument as opaque rather than as a violation, and proves ' + 'discovery reaches the four pausing built-ins.', ); + + return SELF_TEST_VERDICT; } const argv = process.argv.slice(2); const dirFlag = argv.indexOf('--packages-dir'); const scanRoots = dirFlag === -1 ? DEFAULT_SCAN_ROOTS : [argv[dirFlag + 1]]; -if (argv.includes('--self-test')) selfTest(); +if (argv.includes('--self-test')) { + if (selfTest() !== SELF_TEST_VERDICT) { + console.error( + '\n✗ check-resume-authority-declared self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } +} else report({ list: argv.includes('--list'), scanRoots }); diff --git a/scripts/check-role-word.mjs b/scripts/check-role-word.mjs index 3293631d34..3f281ea70a 100644 --- a/scripts/check-role-word.mjs +++ b/scripts/check-role-word.mjs @@ -1026,6 +1026,13 @@ function missingRootsMessage(missing) { ); } +// Set by `selfTest()` only after its verdict is printed, and read at the +// dispatch: a `return` that leaves the function above that line prints nothing +// and still exits 0 — a self-test that never finished, reported as one that +// passed (#13798). The self-test's own exit code stays load-bearing, so the +// handshake is a flag rather than a returned sentinel. +let selfTestReachedVerdict = false; + function selfTest() { const failures = []; const expect = (label, cond) => { @@ -1850,10 +1857,21 @@ function selfTest() { + 'exemptions are disjoint, proven against a fixture the vendor-wire scan DOES claim once ' + 'the region mask is withheld, and both volumes are published separately on every run.', ); + selfTestReachedVerdict = true; process.exit(0); } -if (process.argv.includes('--self-test')) selfTest(); +if (process.argv.includes('--self-test')) { + selfTest(); + if (!selfTestReachedVerdict) { + console.error( + '\n✗ check-role-word self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } +} /* Probed for ALL roots first, so one message names every missing one rather * than the run dying at whichever comes first — and probed here, ahead of both diff --git a/scripts/check-route-envelope.mjs b/scripts/check-route-envelope.mjs index baa394a30b..8fa687373b 100644 --- a/scripts/check-route-envelope.mjs +++ b/scripts/check-route-envelope.mjs @@ -2093,6 +2093,12 @@ function audit() { // ── Self-test ──────────────────────────────────────────────────────────────── // Both cases below are regressions the regex predecessor actually had. +// Returned by `selfTest()` only after its verdict is printed. The dispatch +// refuses anything else: a `return` that leaves the function above that line +// prints nothing and still exits 0 — a self-test that never finished, reported +// as one that passed (#13798). +const SELF_TEST_VERDICT = 'check-route-envelope self-test reached its verdict'; + function selfTest() { const assert = (cond, msg) => { if (!cond) { console.error('✗ self-test: ' + msg); process.exit(1); } }; @@ -2767,6 +2773,8 @@ function selfTest() { ); console.log('✓ check-route-envelope self-test passed'); + + return SELF_TEST_VERDICT; } // Exports bindings, so an import for those exports alone must run nothing (#10667). @@ -2774,5 +2782,14 @@ const invokedDirectly = isEntrypoint(import.meta.url); if (!invokedDirectly) { // imported as a module — expose the exports and do nothing else -} else if (process.argv.includes('--self-test')) selfTest(); +} else if (process.argv.includes('--self-test')) { + if (selfTest() !== SELF_TEST_VERDICT) { + console.error( + '\n✗ check-route-envelope self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } +} else audit(); diff --git a/scripts/check-runner-env-posture.mjs b/scripts/check-runner-env-posture.mjs index 7deeb18aa0..f880a2f737 100644 --- a/scripts/check-runner-env-posture.mjs +++ b/scripts/check-runner-env-posture.mjs @@ -234,6 +234,13 @@ function report(findings, fileCount) { // Self-test — the shapes, not today's corpus // --------------------------------------------------------------------------- +// Set by `selfTest()` only after its verdict is printed, and read at the +// dispatch: a `return` that leaves the function above that line prints nothing +// and still exits 0 — a self-test that never finished, reported as one that +// passed (#13798). The self-test's own exit code stays load-bearing, so the +// handshake is a flag rather than a returned sentinel. +let selfTestReachedVerdict = false; + export function selfTest() { const cases = []; const t = (name, actual, expected) => cases.push([name, actual, expected]); @@ -303,12 +310,22 @@ export function selfTest() { return 1; } console.log(`✓ check-runner-env-posture self-test: ${cases.length} cases pass.`); + selfTestReachedVerdict = true; return 0; } if (isEntrypoint(import.meta.url)) { if (process.argv.includes('--self-test')) { - process.exit(selfTest()); + const selfTestCode = selfTest(); + if (!selfTestReachedVerdict) { + console.error( + '\n✗ check-runner-env-posture self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + process.exit(selfTestCode); } else { const files = collectFiles(); process.exit(report(scanTree(), files.length)); diff --git a/scripts/check-runtime-services-index.mjs b/scripts/check-runtime-services-index.mjs index 3c0118964b..3744ed3d35 100644 --- a/scripts/check-runtime-services-index.mjs +++ b/scripts/check-runtime-services-index.mjs @@ -697,6 +697,12 @@ function main() { // --------------------------------------------------------------------------- // Self-test: every limb observed FAILING on a synthetic tree, and observed silent. +// Returned by `selfTest()` only after its verdict is printed. The dispatch +// refuses anything else: a `return` that leaves the function above that line +// prints nothing and still exits 0 — a self-test that never finished, reported +// as one that passed (#13798). +const SELF_TEST_VERDICT = 'check-runtime-services-index self-test reached its verdict'; + function selfTest() { const failures = []; let checked = 0; @@ -1063,6 +1069,8 @@ function selfTest() { process.exit(1); } console.log(`✓ check-runtime-services-index --self-test: ${checked} assertions over a temp fixture (real run() path); every limb -- chapter list, kernel table, meta.json, order, href, title premise, registry slot (incl. the split-line registration), stability matrix (missing row, stale row, order) and stability LABEL on both tables, canonical-source rows (page-less row, page with no row, prose label, duplicate, missing path, and never read as a stability claim), label VOCABULARY (undefined label named with its allowed set and reported only against the page, every defined label accepted, the two legends drifting apart from EACH OTHER while every label they name is still in the enum, a legend widening it alone, legend order, section scoping past a decoy in each file, and a missing legend refused), empty tree, missing versioning.mdx, empty Source-of-Truth list -- observed FAILING and observed silent.`); + + return SELF_TEST_VERDICT; } // Exports bindings, so an import for those exports alone must run nothing (#10667). @@ -1070,5 +1078,14 @@ const invokedDirectly = isEntrypoint(import.meta.url); if (!invokedDirectly) { // imported as a module — expose the exports and do nothing else -} else if (process.argv.includes('--self-test')) selfTest(); +} else if (process.argv.includes('--self-test')) { + if (selfTest() !== SELF_TEST_VERDICT) { + console.error( + '\n✗ check-runtime-services-index self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } +} else main(); diff --git a/scripts/check-sdui-lockstep.mjs b/scripts/check-sdui-lockstep.mjs index 6cf1e29629..7b5565aebe 100644 --- a/scripts/check-sdui-lockstep.mjs +++ b/scripts/check-sdui-lockstep.mjs @@ -507,6 +507,12 @@ import { SOMEWHERE_ELSE } from './elsewhere.js'; export const stamp = { code: SOMEWHERE_ELSE, message: 'x' }; `; +// Returned by `selfTest()` only after its verdict is printed. The dispatch +// refuses anything else: a `return` that leaves the function above that line +// prints nothing and still exits 0 — a self-test that never finished, reported +// as one that passed (#13798). +const SELF_TEST_VERDICT = 'check-sdui-lockstep self-test reached its verdict'; + export function selfTest() { const failures = []; const check = (label, ok, detail) => { @@ -708,11 +714,20 @@ export function selfTest() { 'check:sdui-lockstep --self-test passed (the constant-vs-literal decomposition and its unresolvable ' + 'direction, the region reader in both drift directions, all four refusal classes, and the CI wiring)', ); + + return SELF_TEST_VERDICT; } function main() { if (process.argv.includes('--self-test')) { - selfTest(); + if (selfTest() !== SELF_TEST_VERDICT) { + console.error( + '\n✗ check-sdui-lockstep self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } return; } if (process.argv.includes('--update')) { diff --git a/scripts/check-sdui-manifest.mjs b/scripts/check-sdui-manifest.mjs index 915b0acc00..870703d578 100644 --- a/scripts/check-sdui-manifest.mjs +++ b/scripts/check-sdui-manifest.mjs @@ -138,6 +138,12 @@ export function checkTree(root) { return problems; } +// Returned by `selfTest()` only after its verdict is printed. The dispatch +// refuses anything else: a `return` that leaves the function above that line +// prints nothing and still exits 0 — a self-test that never finished, reported +// as one that passed (#13798). +const SELF_TEST_VERDICT = 'check-sdui-manifest self-test reached its verdict'; + function selfTest() { const mk = (mutate) => { const root = mkdtempSync(join(tmpdir(), 'sdui-manifest-check-')); @@ -196,11 +202,20 @@ function selfTest() { process.exit(1); } console.log(`✓ check-sdui-manifest self-test: ${cases.length} cases behave (green passes; absence, tamper, moved pin, emptiness are RED).`); + + return SELF_TEST_VERDICT; } if (isEntrypoint(import.meta.url)) { if (process.argv.includes('--self-test')) { - selfTest(); + if (selfTest() !== SELF_TEST_VERDICT) { + console.error( + '\n✗ check-sdui-manifest self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } } else { const problems = checkTree(DEFAULT_ROOT); if (problems.length) { diff --git a/scripts/check-section-landing-index.mjs b/scripts/check-section-landing-index.mjs index 2d630cf010..1bc0f5e5f1 100644 --- a/scripts/check-section-landing-index.mjs +++ b/scripts/check-section-landing-index.mjs @@ -431,6 +431,12 @@ function main() { // --------------------------------------------------------------------------- // Self-test +// Returned by `selfTest()` only after its verdict is printed. The dispatch +// refuses anything else: a `return` that leaves the function above that line +// prints nothing and still exits 0 — a self-test that never finished, reported +// as one that passed (#13798). +const SELF_TEST_VERDICT = 'check-section-landing-index self-test reached its verdict'; + function selfTest() { const failures = []; let checked = 0; @@ -597,6 +603,8 @@ function selfTest() { `\`index\`/\`---Group---\` filtering, opt-in skipping, and all seven refusals (empty block, page with no file, ` + `unparseable meta.json, no \`pages\` array, unreadable section, empty census, short census) -- observed FAILING and observed silent.` ); + + return SELF_TEST_VERDICT; } // Exports bindings, so an import for those exports alone must run nothing (#10667). @@ -604,5 +612,14 @@ const invokedDirectly = isEntrypoint(import.meta.url); if (!invokedDirectly) { // imported as a module — expose the exports and do nothing else -} else if (process.argv.includes('--self-test')) selfTest(); +} else if (process.argv.includes('--self-test')) { + if (selfTest() !== SELF_TEST_VERDICT) { + console.error( + '\n✗ check-section-landing-index self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } +} else main(); diff --git a/scripts/check-shard-attestation.mjs b/scripts/check-shard-attestation.mjs index 6982d6ca28..92f8a8313d 100644 --- a/scripts/check-shard-attestation.mjs +++ b/scripts/check-shard-attestation.mjs @@ -904,6 +904,13 @@ async function main() { * The dominance experiment, in the shape #3668 set for `cancelled` — run as * fixtures because a dev cannot fabricate a real runner-starved CI run. */ + +// Returned by `selfTest()` only after its verdict is printed. The dispatch +// refuses anything else: a `return` that leaves the function above that line +// prints nothing and still exits 0 — a self-test that never finished, reported +// as one that passed (#13798). +const SELF_TEST_VERDICT = 'check-shard-attestation self-test reached its verdict'; + async function selfTest() { const failures = []; let checked = 0; @@ -1375,6 +1382,8 @@ async function selfTest() { console.log( `✓ check-shard-attestation --self-test: ${checked} assertions (dominance experiment + both #6082 counter-examples + the #4928 guard + the #6589 classifier pins + the #10889 quoting pins + the #11998 attempt-scoping sequence).`, ); + + return SELF_TEST_VERDICT; } // Exports bindings, so an import for those exports alone must run nothing (#10667). @@ -1383,7 +1392,14 @@ const invokedDirectly = isEntrypoint(import.meta.url); if (!invokedDirectly) { // imported as a module — expose the exports and do nothing else } else if (process.argv.includes('--self-test')) { - await selfTest(); + if ((await selfTest()) !== SELF_TEST_VERDICT) { + console.error( + '\n✗ check-shard-attestation self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } } else if (process.argv.includes('--emit')) { emit(); } else if (process.argv.includes('--verify')) { diff --git a/scripts/check-single-authz-resolver.mjs b/scripts/check-single-authz-resolver.mjs index d113ee0136..77b55ee5c4 100644 --- a/scripts/check-single-authz-resolver.mjs +++ b/scripts/check-single-authz-resolver.mjs @@ -538,6 +538,13 @@ function mentionOnlyFixtureBody(tables = GRANT_TABLES) { `\n};\nexport const NAMES = [${tables.map((t) => `'${t}'`).join(', ')}];\n`; } +// Set by `selfTest()` only after its verdict is printed, and read at the +// dispatch: a `return` that leaves the function above that line prints nothing +// and still exits 0 — a self-test that never finished, reported as one that +// passed (#13798). The self-test's own exit code stays load-bearing, so the +// handshake is a flag rather than a returned sentinel. +let selfTestReachedVerdict = false; + function selfTest() { const failures = []; const expect = (label, got, want) => { @@ -854,10 +861,22 @@ function selfTest() { 'renamed, green when restored) and the empty-scan hard error (red when one declared root ' + 'yields nothing and when the whole scan does, green when restored) all hold.', ); + selfTestReachedVerdict = true; } function main() { - if (process.argv.includes('--self-test')) return selfTest(); + if (process.argv.includes('--self-test')) { + const selfTestCode = selfTest(); + if (!selfTestReachedVerdict) { + console.error( + '\n✗ check-single-authz-resolver self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + return selfTestCode; + } let errors; try { diff --git a/scripts/check-single-claim-paths.mjs b/scripts/check-single-claim-paths.mjs index a1c6fa4ebb..c2d2730436 100644 --- a/scripts/check-single-claim-paths.mjs +++ b/scripts/check-single-claim-paths.mjs @@ -377,6 +377,13 @@ const githubApi = (token) => async (path) => { // own invariants, the short-circuit that makes this affordable, and the wiring. // --------------------------------------------------------------------------- +// Set by `selfTest()` only after its verdict is printed, and read at the +// dispatch: a `return` that leaves the function above that line prints nothing +// and still exits 0 — a self-test that never finished, reported as one that +// passed (#13798). The self-test's own exit code stays load-bearing, so the +// handshake is a flag rather than a returned sentinel. +let selfTestReachedVerdict = false; + function selfTest() { const cases = []; const t = (name, actual, expected) => cases.push([name, actual, expected]); @@ -479,6 +486,7 @@ function selfTest() { }; const ctxOf = (number) => ({ number: String(number), repo: 'o/r', token: 't' }); + selfTestReachedVerdict = true; return (async () => { calls.length = 0; const quiet = await collect(ctxOf(200), fakeApi({ 200: ['fixture/tree/alpha.ts', 'fixture/tree/beta.ts'] })); @@ -551,6 +559,14 @@ const isMain = isEntrypoint(import.meta.url); if (isMain) { if (process.argv.includes('--self-test')) { await selfTest(); + if (!selfTestReachedVerdict) { + console.error( + '\n✗ check-single-claim-paths self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } } else { const ctx = readPrContext(process.env); const resolved = ctx === null ? null : await collect(ctx, githubApi(ctx.token)); diff --git a/scripts/check-skill-compatibility-version.mjs b/scripts/check-skill-compatibility-version.mjs index 4672bc5a08..9a97cf7970 100644 --- a/scripts/check-skill-compatibility-version.mjs +++ b/scripts/check-skill-compatibility-version.mjs @@ -471,6 +471,13 @@ function report(problems) { // Self-test — pins the RED paths so the gate cannot rot into a no-op. // --------------------------------------------------------------------------- +// Set by `selfTest()` only after its verdict is printed, and read at the +// dispatch: a `return` that leaves the function above that line prints nothing +// and still exits 0 — a self-test that never finished, reported as one that +// passed (#13798). The self-test's own exit code stays load-bearing, so the +// handshake is a flag rather than a returned sentinel. +let selfTestReachedVerdict = false; + function selfTest() { console.log('check-skill-compatibility-version self-test\n'); @@ -753,12 +760,24 @@ function selfTest() { process.exit(1); } console.log(`\n✓ check-skill-compatibility-version self-test: ${cases.length} cases pass, plus 7 dispatch-gates declaration cases.`); + selfTestReachedVerdict = true; } // --------------------------------------------------------------------------- function main() { - if (process.argv.includes('--self-test')) return selfTest(); + if (process.argv.includes('--self-test')) { + const selfTestCode = selfTest(); + if (!selfTestReachedVerdict) { + console.error( + '\n✗ check-skill-compatibility-version self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + return selfTestCode; + } const { files, problems: layout } = readSkillFiles(); const pkgs = readWorkspacePackages(); diff --git a/scripts/check-skill-frame-freshness.mjs b/scripts/check-skill-frame-freshness.mjs index a90b3c65dc..1203988631 100644 --- a/scripts/check-skill-frame-freshness.mjs +++ b/scripts/check-skill-frame-freshness.mjs @@ -791,6 +791,13 @@ function setOriginMain(dir, sha) { git(['update-ref', 'refs/remotes/origin/main', sha], { cwd: dir }); } +// Set by `selfTest()` only after its verdict is printed, and read at the +// dispatch: a `return` that leaves the function above that line prints nothing +// and still exits 0 — a self-test that never finished, reported as one that +// passed (#13798). The self-test's own exit code stays load-bearing, so the +// handshake is a flag rather than a returned sentinel. +let selfTestReachedVerdict = false; + function selfTest() { const real = realFrameFiles(); const twoAxis = twoAxisFrameFiles(); @@ -1019,13 +1026,25 @@ function selfTest() { process.exit(1); } console.log(`✓ check-skill-frame-freshness self-test: ${cases.length} cases pass.`); + selfTestReachedVerdict = true; } // --------------------------------------------------------------------------- function main() { const argv = process.argv.slice(2); - if (argv.includes('--self-test')) return selfTest(); + if (argv.includes('--self-test')) { + const selfTestCode = selfTest(); + if (!selfTestReachedVerdict) { + console.error( + '\n✗ check-skill-frame-freshness self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + return selfTestCode; + } const refAt = argv.indexOf('--ref'); const verdict = evaluate({ diff --git a/scripts/check-skill-frame-sync.mjs b/scripts/check-skill-frame-sync.mjs index 18f4db8b3e..788daab0e8 100644 --- a/scripts/check-skill-frame-sync.mjs +++ b/scripts/check-skill-frame-sync.mjs @@ -657,6 +657,13 @@ function mutate(copies, id, from, to) { }); } +// Set by `selfTest()` only after its verdict is printed, and read at the +// dispatch: a `return` that leaves the function above that line prints nothing +// and still exits 0 — a self-test that never finished, reported as one that +// passed (#13798). The self-test's own exit code stays load-bearing, so the +// handshake is a flag rather than a returned sentinel. +let selfTestReachedVerdict = false; + function selfTest() { const base = readCopies(); const scanOf = (copies) => { @@ -903,12 +910,24 @@ function selfTest() { process.exit(1); } console.log(`✓ check-skill-frame-sync self-test: ${cases.length} cases pass, plus 5 dispatch-gates declaration cases.`); + selfTestReachedVerdict = true; } // --------------------------------------------------------------------------- function main() { - if (process.argv.includes('--self-test')) return selfTest(); + if (process.argv.includes('--self-test')) { + const selfTestCode = selfTest(); + if (!selfTestReachedVerdict) { + console.error( + '\n✗ check-skill-frame-sync self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + return selfTestCode; + } const copies = readCopies(); const scanFiles = SCAN_ROOTS.flatMap((root) => walkMarkdown(root, [])); diff --git a/scripts/check-slot-lookup-ratchet.mjs b/scripts/check-slot-lookup-ratchet.mjs index a3d7557ee9..338bdabe26 100644 --- a/scripts/check-slot-lookup-ratchet.mjs +++ b/scripts/check-slot-lookup-ratchet.mjs @@ -416,6 +416,12 @@ const DIFF_CASES = (() => { ]; })(); +// Returned by `selfTest()` only after its verdict is printed. The dispatch +// refuses anything else: a `return` that leaves the function above that line +// prints nothing and still exits 0 — a self-test that never finished, reported +// as one that passed (#13798). +const SELF_TEST_VERDICT = 'check-slot-lookup-ratchet self-test reached its verdict'; + async function selfTest() { const failures = []; const assert = (cond, msg) => { if (!cond) failures.push(msg); }; @@ -573,6 +579,8 @@ async function selfTest() { `synthetic witness pair that survives baseline zero; ${DIFF_CASES.length} ratchet ` + `comparison case(s); and both refusals proved in both directions.`, ); + + return SELF_TEST_VERDICT; } // --------------------------------------------------------------------------- @@ -632,7 +640,14 @@ async function main() { } if (process.argv.includes('--self-test')) { - await selfTest(); + if ((await selfTest()) !== SELF_TEST_VERDICT) { + console.error( + '\n✗ check-slot-lookup-ratchet self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } process.exit(0); } await main(); diff --git a/scripts/check-spec-parsed-alias.mjs b/scripts/check-spec-parsed-alias.mjs index aa9895fa3a..a0722447f7 100644 --- a/scripts/check-spec-parsed-alias.mjs +++ b/scripts/check-spec-parsed-alias.mjs @@ -266,6 +266,12 @@ function loadCorpus() { return files; } +// Returned by `selfTest()` only after its verdict is printed. The dispatch +// refuses anything else: a `return` that leaves the function above that line +// prints nothing and still exits 0 — a self-test that never finished, reported +// as one that passed (#13798). +const SELF_TEST_VERDICT = 'check-spec-parsed-alias self-test reached its verdict'; + function selfTest() { const failures = []; const check = (label, actual, expected) => { @@ -460,6 +466,8 @@ export type Iso0 = Assert, z.infer< typeof process.exit(1); } console.log('check-spec-parsed-alias --self-test: 18 assertions passed'); + + return SELF_TEST_VERDICT; } // Exports bindings, so an import for those exports alone must run nothing (#10667). @@ -468,7 +476,14 @@ const invokedDirectly = isEntrypoint(import.meta.url); if (!invokedDirectly) { // imported as a module — expose the exports and do nothing else } else if (process.argv.includes('--self-test')) { - selfTest(); + if (selfTest() !== SELF_TEST_VERDICT) { + console.error( + '\n✗ check-spec-parsed-alias self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } } else { const pins = readIsomorphicPins(readFileSync(PIN_FILE, 'utf8')); const files = loadCorpus(); diff --git a/scripts/check-stack-collection-maps.mjs b/scripts/check-stack-collection-maps.mjs index bf4e1f5cf6..591ed0c8c9 100644 --- a/scripts/check-stack-collection-maps.mjs +++ b/scripts/check-stack-collection-maps.mjs @@ -871,6 +871,13 @@ function run({ list = false } = {}) { // its own output), so a green run proves nothing about whether it CAN fire. The // assertions below drive both failure directions on synthetic input. +// Set by `selfTest()` only after its verdict is printed, and read at the +// dispatch: a `return` that leaves the function above that line prints nothing +// and still exits 0 — a self-test that never finished, reported as one that +// passed (#13798). The self-test's own exit code stays load-bearing, so the +// handshake is a flag rather than a returned sentinel. +let selfTestReachedVerdict = false; + function selfTest() { const failures = []; const eq = (label, actual, expected) => { @@ -991,6 +998,7 @@ export const ObjectStackDefinitionSchema = lazySchema(() => strictObject({ return 1; } console.log('✓ check-stack-collection-maps --self-test: 16 assertions over synthetic sources'); + selfTestReachedVerdict = true; return 0; } @@ -998,6 +1006,17 @@ export const ObjectStackDefinitionSchema = lazySchema(() => strictObject({ // self-test and by anything else that wants to ask what a site enumerates. if (isEntrypoint(import.meta.url)) { const argv = process.argv.slice(2); - if (argv.includes('--self-test')) process.exit(selfTest()); + if (argv.includes('--self-test')) { + const selfTestCode = selfTest(); + if (!selfTestReachedVerdict) { + console.error( + '\n✗ check-stack-collection-maps self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + process.exit(selfTestCode); + } process.exit(run({ list: argv.includes('--list') })); } diff --git a/scripts/check-stall-guard-budget.mjs b/scripts/check-stall-guard-budget.mjs index e9392a6add..802b17d0bc 100644 --- a/scripts/check-stall-guard-budget.mjs +++ b/scripts/check-stall-guard-budget.mjs @@ -647,6 +647,14 @@ export function run(root, parseYaml, io = {}) { * the selector can only shrink the finding set, and the empty set is the fixed * point of shrinking. So these fixtures are the only instrument watching it. */ + +// Set by `selfTest()` only after its verdict is printed, and read at the +// dispatch: a `return` that leaves the function above that line prints nothing +// and still exits 0 — a self-test that never finished, reported as one that +// passed (#13798). The self-test's own exit code stays load-bearing, so the +// handshake is a flag rather than a returned sentinel. +let selfTestReachedVerdict = false; + export async function selfTest() { const { parse } = await requireDependency('yaml', () => import('yaml'), import.meta.url); const failures = []; @@ -1001,13 +1009,23 @@ export async function selfTest() { 'REFUSE, and the sibling note proven to follow JOB membership -- absent for a lone guarded step, and absent for two ' + 'jobs sharing only a --stall-minutes value or only a job id.', ); + selfTestReachedVerdict = true; return 0; } // Exports bindings, so an import for those exports alone must run nothing (#10667). if (isEntrypoint(import.meta.url)) { if (process.argv.includes('--self-test')) { - process.exit(await selfTest()); + const selfTestCode = await selfTest(); + if (!selfTestReachedVerdict) { + console.error( + '\n✗ check-stall-guard-budget self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + process.exit(selfTestCode); } else { const { parse } = await requireDependency('yaml', () => import('yaml'), import.meta.url); if (process.argv.includes('--list')) { diff --git a/scripts/check-startup-registry-verdict.mjs b/scripts/check-startup-registry-verdict.mjs index 28a87bba93..90e7b6143e 100644 --- a/scripts/check-startup-registry-verdict.mjs +++ b/scripts/check-startup-registry-verdict.mjs @@ -1051,6 +1051,14 @@ function run({ list = false, packagesDir } = {}) { // gate must flag the three recording shapes and must NOT flag the read-only // probe, the deferred probe, or the sealed verdict — which are precisely what // #4771 and #4772 were fixed INTO. + +// Set by `selfTest()` only after its verdict is printed, and read at the +// dispatch: a `return` that leaves the function above that line prints nothing +// and still exits 0 — a self-test that never finished, reported as one that +// passed (#13798). The self-test's own exit code stays load-bearing, so the +// handshake is a flag rather than a returned sentinel. +let selfTestReachedVerdict = false; + function selfTest() { // Every fixture is analysed on its own, so the provider index is built from // the fixture itself — which means each one must declare the provider it is @@ -1434,6 +1442,7 @@ function selfTest() { return 1; } console.log(`\n✓ self-test: ${cases.length} analysis case(s) + the dead-root hard error (red when the scan root is renamed, green when restored) all passed\n`); + selfTestReachedVerdict = true; return 0; } @@ -1444,7 +1453,16 @@ const invokedDirectly = isEntrypoint(import.meta.url); if (!invokedDirectly) { // imported as a module — expose the exports and do nothing else } else if (args.includes('--self-test')) { - process.exit(selfTest()); + const selfTestCode = selfTest(); + if (!selfTestReachedVerdict) { + console.error( + '\n✗ check-startup-registry-verdict self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + process.exit(selfTestCode); } else { const dirFlag = args.indexOf('--packages-dir'); process.exit( diff --git a/scripts/check-system-context-census.mjs b/scripts/check-system-context-census.mjs index 804912155a..8c2c4e5c16 100644 --- a/scripts/check-system-context-census.mjs +++ b/scripts/check-system-context-census.mjs @@ -1208,6 +1208,13 @@ function fixtureUnenforcedTable({ linesTotal = 6, dropTestsRow = false, dated = ].join('\n'); } +// Set by `selfTest()` only after its verdict is printed, and read at the +// dispatch: a `return` that leaves the function above that line prints nothing +// and still exits 0 — a self-test that never finished, reported as one that +// passed (#13798). The self-test's own exit code stays load-bearing, so the +// handshake is a flag rather than a returned sentinel. +let selfTestReachedVerdict = false; + function selfTest() { let failures = 0; const t = (name, ok, detail = '') => { @@ -1689,10 +1696,23 @@ function selfTest() { ? '\ncheck-system-context-census --self-test: all cases passed\n' : `\ncheck-system-context-census --self-test: ${failures} case(s) FAILED\n` ); + selfTestReachedVerdict = true; return failures === 0 ? 0 : 1; } if (isEntrypoint(import.meta.url)) { const argv = process.argv.slice(2); - process.exit(argv.includes('--self-test') ? selfTest() : run({ fix: argv.includes('--fix') })); + if (argv.includes('--self-test')) { + const selfTestCode = selfTest(); + if (!selfTestReachedVerdict) { + console.error( + '\n✗ check-system-context-census self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + process.exit(selfTestCode); + } + process.exit(run({ fix: argv.includes('--fix') })); } diff --git a/scripts/check-tenant-audit-census.mjs b/scripts/check-tenant-audit-census.mjs index ef7c1fa047..e7152c84c2 100644 --- a/scripts/check-tenant-audit-census.mjs +++ b/scripts/check-tenant-audit-census.mjs @@ -597,6 +597,14 @@ export function checkPage(census, pageText, countsText) { * rules are driven here against pages a clean tree does not contain -- and the * REAL census, so a rule that stops reading the tree fails here too. */ + +// Set by `selfTest()` only after its verdict is printed, and read at the +// dispatch: a `return` that leaves the function above that line prints nothing +// and still exits 0 — a self-test that never finished, reported as one that +// passed (#13798). The self-test's own exit code stays load-bearing, so the +// handshake is a flag rather than a returned sentinel. +let selfTestReachedVerdict = false; + export function selfTest() { const cases = []; const t = (name, ok, detail) => cases.push({ name, ok: Boolean(ok), detail }); @@ -753,11 +761,23 @@ export function selfTest() { + 'stale measurement date pass, while the population figure beside them, a deleted ' + 'unenforced row, an undated block and a reworded unenforced claim all fail).', ); + selfTestReachedVerdict = true; return 0; } function main(argv) { - if (argv.includes('--self-test')) return selfTest(); + if (argv.includes('--self-test')) { + const selfTestCode = selfTest(); + if (!selfTestReachedVerdict) { + console.error( + '\n✗ check-tenant-audit-census self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + return selfTestCode; + } let page; let counts; diff --git a/scripts/check-tenant-chokepoint.mjs b/scripts/check-tenant-chokepoint.mjs index 9342d45fe6..91f00151d7 100644 --- a/scripts/check-tenant-chokepoint.mjs +++ b/scripts/check-tenant-chokepoint.mjs @@ -318,6 +318,12 @@ export function violationsOf(file, { builders, unclassifiable }, exempt = EXEMPT const wrap = (body) => `class SqlDriver {\n${body}\n}`; +// Returned by `selfTest()` only after its verdict is printed. The dispatch +// refuses anything else: a `return` that leaves the function above that line +// prints nothing and still exits 0 — a self-test that never finished, reported +// as one that passed (#13798). +const SELF_TEST_VERDICT = 'check-tenant-chokepoint self-test reached its verdict'; + function selfTest() { const failures = []; const assert = (cond, msg) => { if (!cond) failures.push(msg); }; @@ -477,6 +483,8 @@ function selfTest() { `✓ check:tenant-chokepoint self-test: ${reports.length} reporting shape(s), ` + `${silent.length} silent counterpart(s), fatal/exemption/floor channels proved in both directions.`, ); + + return SELF_TEST_VERDICT; } // --------------------------------------------------------------------------- @@ -562,7 +570,14 @@ const invokedDirectly = isEntrypoint(import.meta.url); if (!invokedDirectly) { // imported as a module — expose the exports and do nothing else } else if (process.argv.includes('--self-test')) { - selfTest(); + if (selfTest() !== SELF_TEST_VERDICT) { + console.error( + '\n✗ check-tenant-chokepoint self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } process.exit(0); } diff --git a/scripts/check-test-completeness.mjs b/scripts/check-test-completeness.mjs index fd164cedf7..e111fce1e7 100644 --- a/scripts/check-test-completeness.mjs +++ b/scripts/check-test-completeness.mjs @@ -573,6 +573,12 @@ function reportVerdict(verdict) { process.exit(verdict.exit); } +// Returned by `selfTest()` only after its verdict is printed. The dispatch +// refuses anything else: a `return` that leaves the function above that line +// prints nothing and still exits 0 — a self-test that never finished, reported +// as one that passed (#13798). +const SELF_TEST_VERDICT = 'check-test-completeness self-test reached its verdict'; + function selfTest({ quiet = false } = {}) { const eq = (actual, expected, what) => { const a = JSON.stringify(actual); @@ -901,12 +907,21 @@ function selfTest({ quiet = false } = {}) { ); if (!quiet) console.log('check-test-completeness: self-test OK'); + + return SELF_TEST_VERDICT; } function main() { const argv = process.argv.slice(2); if (argv.includes('--self-test')) { - selfTest(); + if (selfTest() !== SELF_TEST_VERDICT) { + console.error( + '\n✗ check-test-completeness self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } return; } // Every invocation, not a lint step -- see the header note on why. diff --git a/scripts/check-test-source-alias.mjs b/scripts/check-test-source-alias.mjs index e23749014d..6d7e480931 100644 --- a/scripts/check-test-source-alias.mjs +++ b/scripts/check-test-source-alias.mjs @@ -2413,6 +2413,12 @@ function buildFixtureTree() { return root; } +// Returned by `selfTest()` only after its verdict is printed. The dispatch +// refuses anything else: a `return` that leaves the function above that line +// prints nothing and still exits 0 — a self-test that never finished, reported +// as one that passed (#13798). +const SELF_TEST_VERDICT = 'check-test-source-alias self-test reached its verdict'; + function selfTest() { const root = buildFixtureTree(); const problems = []; @@ -2903,13 +2909,22 @@ function selfTest() { process.exit(1); } console.log('check-test-source-alias --self-test OK'); + + return SELF_TEST_VERDICT; } // ── entry point ───────────────────────────────────────────────────────────── const argv = process.argv.slice(2); if (argv.includes('--self-test')) { - selfTest(); + if (selfTest() !== SELF_TEST_VERDICT) { + console.error( + '\n✗ check-test-source-alias self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } } else if (argv.includes('--list')) { printList(REPO_ROOT); } else { diff --git a/scripts/check-test-typecheck.mts b/scripts/check-test-typecheck.mts index 444184125e..6f380753a5 100644 --- a/scripts/check-test-typecheck.mts +++ b/scripts/check-test-typecheck.mts @@ -510,7 +510,13 @@ function runTsc(): string { return output; } -function selfTest(): void { +// Returned by `selfTest()` only after its verdict is printed. The dispatch +// refuses anything else: a `return` that leaves the function above that line +// prints nothing and still exits 0 — a self-test that never finished, reported +// as one that passed (#13798). +const SELF_TEST_VERDICT = 'check-test-typecheck self-test reached its verdict'; + +function selfTest(): string { // The two REAL signatures from the ablation that produced #13470: `packages/ // rest`'s two call sites passed a bad request AND a bad response, tsc showed // only the request error, and PR #13466's repair uncovered the response one. @@ -970,10 +976,19 @@ function selfTest(): void { + 'measured counts through unchanged, and preserves an authored `_note` verbatim in its own key) ' + 'all hold.', ); + + return SELF_TEST_VERDICT; } if (process.argv.includes('--self-test')) { - selfTest(); + if (selfTest() !== SELF_TEST_VERDICT) { + console.error( + '\n✗ check-test-typecheck self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } process.exit(0); } diff --git a/scripts/check-turbo-task-graph.mjs b/scripts/check-turbo-task-graph.mjs index 496dab4db3..9893d75d61 100644 --- a/scripts/check-turbo-task-graph.mjs +++ b/scripts/check-turbo-task-graph.mjs @@ -883,6 +883,12 @@ export function selfTest() { return failures; } +// Returned by `runSelfTest()` only after its verdict is printed. The dispatch +// refuses anything else: a `return` that leaves the function above that line +// prints nothing and still exits 0 — a self-test that never finished, reported +// as one that passed (#13798). +const SELF_TEST_VERDICT = 'check-turbo-task-graph self-test reached its verdict'; + function runSelfTest() { const failures = selfTest(); if (failures.length) { @@ -891,10 +897,21 @@ function runSelfTest() { process.exit(1); } console.log('OK: check-turbo-task-graph --self-test — all cases passed.'); + + return SELF_TEST_VERDICT; } // Exports bindings, so an import for those exports alone must run nothing (#10667). if (isEntrypoint(import.meta.url)) { - if (process.argv.includes('--self-test')) runSelfTest(); + if (process.argv.includes('--self-test')) { + if (runSelfTest() !== SELF_TEST_VERDICT) { + console.error( + '\n✗ check-turbo-task-graph self-test: runSelfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + } else main(); } diff --git a/scripts/check-undeclared-dep-imports.mjs b/scripts/check-undeclared-dep-imports.mjs index 2b8a637036..cee9f445b8 100644 --- a/scripts/check-undeclared-dep-imports.mjs +++ b/scripts/check-undeclared-dep-imports.mjs @@ -709,6 +709,13 @@ function makeTree(root, { manifest, files, workspace }) { for (const [rel, text] of Object.entries(files)) fixture(root, `packages/subject/${rel}`, text); } +// Set by `selfTest()` only after its verdict is printed, and read at the +// dispatch: a `return` that leaves the function above that line prints nothing +// and still exits 0 — a self-test that never finished, reported as one that +// passed (#13798). The self-test's own exit code stays load-bearing, so the +// handshake is a flag rather than a returned sentinel. +let selfTestReachedVerdict = false; + function selfTest() { let failures = 0; const t = (name, ok) => { if (!ok) { failures += 1; console.error(` FAIL ${name}`); } else console.log(` ok ${name}`); }; @@ -988,10 +995,23 @@ function selfTest() { } console.log(`\n${failures === 0 ? 'PASS' : 'FAIL'} check-undeclared-dep-imports --self-test (${failures} failure(s))`); + selfTestReachedVerdict = true; return failures === 0 ? 0 : 1; } if (isEntrypoint(import.meta.url)) { const argv = process.argv.slice(2); - process.exit(argv.includes('--self-test') ? selfTest() : argv.includes('--list') ? list() : main()); + if (argv.includes('--self-test')) { + const selfTestCode = selfTest(); + if (!selfTestReachedVerdict) { + console.error( + '\n✗ check-undeclared-dep-imports self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + process.exit(selfTestCode); + } + process.exit(argv.includes('--list') ? list() : main()); } diff --git a/scripts/check-vendor-version-stamps.mjs b/scripts/check-vendor-version-stamps.mjs index 3bf0641ef1..241aa513a4 100644 --- a/scripts/check-vendor-version-stamps.mjs +++ b/scripts/check-vendor-version-stamps.mjs @@ -900,6 +900,13 @@ function collectFiles() { return files; } +// Set by `selfTest()` only after its verdict is printed, and read at the +// dispatch: a `return` that leaves the function above that line prints nothing +// and still exits 0 — a self-test that never finished, reported as one that +// passed (#13798). The self-test's own exit code stays load-bearing, so the +// handshake is a flag rather than a returned sentinel. +let selfTestReachedVerdict = false; + function selfTest() { const failures = []; let ran = 0; @@ -1219,6 +1226,7 @@ function selfTest() { // value is the exact defect this gate exists to stop, and a self-test summary // that carries one would be the gate committing it in its own voice. console.log(`check-vendor-version-stamps --self-test: ${ran} checks pass.`); + selfTestReachedVerdict = true; process.exit(0); } @@ -1232,7 +1240,17 @@ function selfTest() { // "was I run, or imported?"; a hand-typed argv comparison gets it wrong through // a symlink, silently. if (isEntrypoint(import.meta.url)) { - if (process.argv.includes('--self-test')) selfTest(); + if (process.argv.includes('--self-test')) { + selfTest(); + if (!selfTestReachedVerdict) { + console.error( + '\n✗ check-vendor-version-stamps self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + } // ── Main ──────────────────────────────────────────────────────────────────── diff --git a/scripts/check-verify-stand-in-erasure.mjs b/scripts/check-verify-stand-in-erasure.mjs index 4e010b6eca..96ca534792 100644 --- a/scripts/check-verify-stand-in-erasure.mjs +++ b/scripts/check-verify-stand-in-erasure.mjs @@ -388,6 +388,12 @@ function report() { // --------------------------------------------------------------------------- +// Returned by `selfTest()` only after its verdict is printed. The dispatch +// refuses anything else: a `return` that leaves the function above that line +// prints nothing and still exits 0 — a self-test that never finished, reported +// as one that passed (#13798). +const SELF_TEST_VERDICT = 'check-verify-stand-in-erasure self-test reached its verdict'; + function selfTest() { const failures = []; const expect = (label, ok) => { @@ -554,7 +560,18 @@ function selfTest() { 'declared in packages/verify and published as a type from its index; and proves discovery, both ' + 'ledgers and the census reach the real tree — all ten known call sites of it.', ); + + return SELF_TEST_VERDICT; } -if (process.argv.includes('--self-test')) selfTest(); +if (process.argv.includes('--self-test')) { + if (selfTest() !== SELF_TEST_VERDICT) { + console.error( + '\n✗ check-verify-stand-in-erasure self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } +} else report(); diff --git a/scripts/check-watch-hint-literal.mjs b/scripts/check-watch-hint-literal.mjs index e586146866..3528a18f03 100644 --- a/scripts/check-watch-hint-literal.mjs +++ b/scripts/check-watch-hint-literal.mjs @@ -436,6 +436,13 @@ function main() { // Self-test -- fixture sources, plus the live tree // --------------------------------------------------------------------------- +// Set by `selfTest()` only after its verdict is printed, and read at the +// dispatch: a `return` that leaves the function above that line prints nothing +// and still exits 0 — a self-test that never finished, reported as one that +// passed (#13798). The self-test's own exit code stays load-bearing, so the +// handshake is a flag rather than a returned sentinel. +let selfTestReachedVerdict = false; + export function selfTest() { const cases = []; const t = (name, ok, detail) => cases.push({ name, ok: Boolean(ok), detail }); @@ -602,10 +609,23 @@ export function selfTest() { + 'per-name floor proved against a population that is healthy on every name but one, unrostered ' + 'spellings of the idiom discovered, and the live repo-wide population judged.', ); + selfTestReachedVerdict = true; return 0; } if (isEntrypoint(import.meta.url)) { const argv = process.argv; - process.exit(argv.includes('--self-test') ? selfTest() : argv.includes('--list') ? list() : main()); + if (argv.includes('--self-test')) { + const selfTestCode = selfTest(); + if (!selfTestReachedVerdict) { + console.error( + '\n✗ check-watch-hint-literal self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + process.exit(selfTestCode); + } + process.exit(argv.includes('--list') ? list() : main()); } diff --git a/scripts/check-where-matcher-conformance.mjs b/scripts/check-where-matcher-conformance.mjs index 23550c7374..cb87e3a5fb 100644 --- a/scripts/check-where-matcher-conformance.mjs +++ b/scripts/check-where-matcher-conformance.mjs @@ -832,6 +832,12 @@ function judgeFixture(src) { return { found, results: found.map(judge) }; } +// Returned by `selfTest()` only after its verdict is printed. The dispatch +// refuses anything else: a `return` that leaves the function above that line +// prints nothing and still exits 0 — a self-test that never finished, reported +// as one that passed (#13798). +const SELF_TEST_VERDICT = 'check-where-matcher-conformance self-test reached its verdict'; + function selfTest() { const failures = []; const expect = (label, cond) => { if (!cond) failures.push(label); }; @@ -987,6 +993,8 @@ function selfTest() { ' inverted survivor filter are each declined for their own recorded reason; the\n' + ' ledger reconciles in both directions.', ); + + return SELF_TEST_VERDICT; } // --------------------------------------------------------------------------- @@ -1000,7 +1008,14 @@ if (!invokedDirectly) { // the corpus scan as an import side effect would make this file impossible to // reuse without also failing someone else's process. } else if (process.argv.includes('--self-test')) { - selfTest(); + if (selfTest() !== SELF_TEST_VERDICT) { + console.error( + '\n✗ check-where-matcher-conformance self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } } else { if (!existsSync(resolve(repoRoot, BASELINE_PATH))) { console.error(`check-where-matcher-conformance: missing ${BASELINE_PATH}`); diff --git a/scripts/check-whole-set-label-write.mjs b/scripts/check-whole-set-label-write.mjs index 1b31959b46..45313aba23 100644 --- a/scripts/check-whole-set-label-write.mjs +++ b/scripts/check-whole-set-label-write.mjs @@ -818,6 +818,13 @@ function withTree(files, fn) { } } +// Set by `selfTest()` only after its verdict is printed, and read at the +// dispatch: a `return` that leaves the function above that line prints nothing +// and still exits 0 — a self-test that never finished, reported as one that +// passed (#13798). The self-test's own exit code stays load-bearing, so the +// handshake is a flag rather than a returned sentinel. +let selfTestReachedVerdict = false; + export function selfTest() { const failures = []; const silent = () => {}; @@ -905,12 +912,24 @@ export function selfTest() { } const cases = Object.keys(RED_CASES).length + Object.keys(GREEN_CASES).length; console.log(`✓ check-whole-set-label-write --self-test: all cases pass (${cases} fixture trees + 5 refusals + 1 allowlist hatch)`); + selfTestReachedVerdict = true; return 0; } if (isEntrypoint(import.meta.url)) { const argv = process.argv.slice(2); - if (argv.includes('--self-test')) process.exit(selfTest()); + if (argv.includes('--self-test')) { + const selfTestCode = selfTest(); + if (!selfTestReachedVerdict) { + console.error( + '\n✗ check-whole-set-label-write self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + process.exit(selfTestCode); + } else if (argv.includes('--list')) list(); else process.exit(run()); } diff --git a/scripts/check-widget-option-census.mjs b/scripts/check-widget-option-census.mjs index c449c483ce..9cb6645176 100644 --- a/scripts/check-widget-option-census.mjs +++ b/scripts/check-widget-option-census.mjs @@ -476,8 +476,26 @@ function readTree(root = REPO_ROOT) { return { spec: read(SPEC_FILE) ?? '', parser: read(PARSER_FILE) ?? '', evidence }; } +// Set by `selfTest()` only after its verdict is printed, and read at the +// dispatch: a `return` that leaves the function above that line prints nothing +// and still exits 0 — a self-test that never finished, reported as one that +// passed (#13798). The self-test's own exit code stays load-bearing, so the +// handshake is a flag rather than a returned sentinel. +let selfTestReachedVerdict = false; + function main() { - if (process.argv.includes('--self-test')) return selfTest(); + if (process.argv.includes('--self-test')) { + const selfTestCode = selfTest(); + if (!selfTestReachedVerdict) { + console.error( + '\n✗ check-widget-option-census self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + return selfTestCode; + } const tree = readTree(); const { problems, declared, census } = judge(tree); @@ -753,6 +771,7 @@ function selfTest() { `\n✓ check-widget-option-census self-test: ${cases.length} cases pass, ` + 'negative controls (comment, commented-out property, string) and refusals included.', ); + selfTestReachedVerdict = true; } if (isEntrypoint(import.meta.url)) { diff --git a/scripts/check-wildcard-fallthrough.mjs b/scripts/check-wildcard-fallthrough.mjs index 0c29fbab6c..3757b905cd 100644 --- a/scripts/check-wildcard-fallthrough.mjs +++ b/scripts/check-wildcard-fallthrough.mjs @@ -448,6 +448,12 @@ function audit() { // ── Self-test ──────────────────────────────────────────────────────────────── +// Returned by `selfTest()` only after its verdict is printed. The dispatch +// refuses anything else: a `return` that leaves the function above that line +// prints nothing and still exits 0 — a self-test that never finished, reported +// as one that passed (#13798). +const SELF_TEST_VERDICT = 'check-wildcard-fallthrough self-test reached its verdict'; + function selfTest() { const assert = (cond, msg) => { if (!cond) { console.error('✗ self-test: ' + msg); process.exit(1); } }; const parse = (code) => parseSourceFile('t.ts', code); @@ -518,9 +524,20 @@ function selfTest() { ); console.log('✓ self-test: 17 cases'); + + return SELF_TEST_VERDICT; } -if (process.argv.includes('--self-test')) selfTest(); +if (process.argv.includes('--self-test')) { + if (selfTest() !== SELF_TEST_VERDICT) { + console.error( + '\n✗ check-wildcard-fallthrough self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } +} else if (process.argv.includes('--list')) { for (const s of scan()) { console.log(`${s.yields ? 'yields ' : 'TERMINAL'} ${s.file}:${s.line} ${s.method}('${s.pattern}')${s.resolvedHandler ? '' : ' [handler unresolved]'}`); diff --git a/scripts/check-workflow-status-functions.mjs b/scripts/check-workflow-status-functions.mjs index 5c035c76f7..e589fa8a2b 100644 --- a/scripts/check-workflow-status-functions.mjs +++ b/scripts/check-workflow-status-functions.mjs @@ -294,6 +294,12 @@ function list() { // git index, so a temp dir (no `git init`) is the faithful analogue of that // script's temp repo: it exercises the real discovery path, not an imitation. +// Returned by `selfTest()` only after its verdict is printed. The dispatch +// refuses anything else: a `return` that leaves the function above that line +// prints nothing and still exits 0 — a self-test that never finished, reported +// as one that passed (#13798). +const SELF_TEST_VERDICT = 'check-workflow-status-functions self-test reached its verdict'; + function selfTest() { const failures = []; let checked = 0; @@ -600,6 +606,8 @@ jobs: process.exit(1); } console.log(`✓ check-workflow-status-functions --self-test: ${checked} assertions over temp fixture roots (real scan() path)`); + + return SELF_TEST_VERDICT; } // Exports bindings, so an import for those exports alone must run nothing (#10667). @@ -608,7 +616,14 @@ const invokedDirectly = isEntrypoint(import.meta.url); if (!invokedDirectly) { // imported as a module — expose the exports and do nothing else } else if (process.argv.includes('--self-test')) { - selfTest(); + if (selfTest() !== SELF_TEST_VERDICT) { + console.error( + '\n✗ check-workflow-status-functions self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } } else if (process.argv.includes('--list')) { list(); } else { diff --git a/scripts/docs-audit/affected-docs.mjs b/scripts/docs-audit/affected-docs.mjs index e7dd5f517b..8f843b23ad 100644 --- a/scripts/docs-audit/affected-docs.mjs +++ b/scripts/docs-audit/affected-docs.mjs @@ -611,8 +611,22 @@ function rulePatternFor(span) { } // Short-circuit before any git or filesystem work — the self-test needs no repo state. + +// Returned by `selfTest()` only after its verdict is printed. The dispatch +// refuses anything else: a `return` that leaves the function above that line +// prints nothing and still exits 0 — a self-test that never finished, reported +// as one that passed (#13798). +const SELF_TEST_VERDICT = 'affected-docs self-test reached its verdict'; + if (args.includes('--self-test')) { - selfTest(); + if (selfTest() !== SELF_TEST_VERDICT) { + console.error( + '\n✗ affected-docs self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } process.exit(0); } @@ -4806,6 +4820,8 @@ function selfTest() { process.exit(1); } console.log(`✓ affected-docs self-test: ${total} cases pass.`); + + return SELF_TEST_VERDICT; } diff --git a/scripts/docs-audit/check-audit-scope.mjs b/scripts/docs-audit/check-audit-scope.mjs index 47721ef476..53dd724ef4 100644 --- a/scripts/docs-audit/check-audit-scope.mjs +++ b/scripts/docs-audit/check-audit-scope.mjs @@ -601,10 +601,24 @@ export async function checkScopeInjection(source) { // --- main -------------------------------------------------------------------- // Exports bindings, so an import for those exports alone must run nothing (#10667). + +// Returned by `selfTest()` only after its verdict is printed. The dispatch +// refuses anything else: a `return` that leaves the function above that line +// prints nothing and still exits 0 — a self-test that never finished, reported +// as one that passed (#13798). +const SELF_TEST_VERDICT = 'check-audit-scope self-test reached its verdict'; + if (isEntrypoint(import.meta.url)) { try { if (args.includes('--self-test')) { - await selfTest(); + if ((await selfTest()) !== SELF_TEST_VERDICT) { + console.error( + '\n✗ check-audit-scope self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } process.exit(0); } await main(); @@ -1015,4 +1029,6 @@ async function selfTest() { process.exit(1); } console.log(`✓ check-audit-scope self-test: ${total} cases pass.`); + + return SELF_TEST_VERDICT; } diff --git a/scripts/import-prerequisite.mjs b/scripts/import-prerequisite.mjs index a7ccff1f24..41bb31647a 100644 --- a/scripts/import-prerequisite.mjs +++ b/scripts/import-prerequisite.mjs @@ -594,6 +594,14 @@ function prerequisiteNotMetText(importerUrl, verdict, measures) { * built when the real problem is something else" is the failure this gate's * diagnosis is supposed to end, not reproduce one level down. */ + +// Set by `selfTest()` only after its verdict is printed, and read at the +// dispatch: a `return` that leaves the function above that line prints nothing +// and still exits 0 — a self-test that never finished, reported as one that +// passed (#13798). The self-test's own exit code stays load-bearing, so the +// handshake is a flag rather than a returned sentinel. +let selfTestReachedVerdict = false; + export function selfTest() { const cases = []; const t = (name, ok, detail) => cases.push({ name, ok: Boolean(ok), detail }); @@ -924,10 +932,22 @@ export function selfTest() { `✓ import-prerequisite self-test: ${cases.length} cases pass — not-installed, workspace-unbuilt, ` + `broken-install and dependency-missing stay distinct, and a resolved-then-threw package is rethrown.`, ); + selfTestReachedVerdict = true; return 0; } if (isEntrypoint(import.meta.url)) { - if (process.argv.includes('--self-test')) process.exit(selfTest()); + if (process.argv.includes('--self-test')) { + const selfTestCode = selfTest(); + if (!selfTestReachedVerdict) { + console.error( + '\n✗ import-prerequisite self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + process.exit(selfTestCode); + } console.log('usage: node scripts/import-prerequisite.mjs --self-test'); } diff --git a/scripts/invoked-as.mjs b/scripts/invoked-as.mjs index d545f8d2e3..913329c0fd 100644 --- a/scripts/invoked-as.mjs +++ b/scripts/invoked-as.mjs @@ -159,6 +159,14 @@ export function isEntrypoint(importMetaUrl) { * A model of a symlink would have passed against every one of the eleven * broken spellings this module replaces. */ + +// Set by `selfTest()` only after its verdict is printed, and read at the +// dispatch: a `return` that leaves the function above that line prints nothing +// and still exits 0 — a self-test that never finished, reported as one that +// passed (#13798). The self-test's own exit code stays load-bearing, so the +// handshake is a flag rather than a returned sentinel. +let selfTestReachedVerdict = false; + export function selfTest() { const cases = []; const t = (name, ok, detail) => cases.push({ name, ok: Boolean(ok), detail }); @@ -251,10 +259,22 @@ export function selfTest() { return 1; } console.log(`✓ invoked-as self-test: ${cases.length} cases pass (real symlink, different-name symlink, percent-encoding path, and both import directions).`); + selfTestReachedVerdict = true; return 0; } if (isEntrypoint(import.meta.url)) { - if (process.argv.includes('--self-test')) process.exit(selfTest()); + if (process.argv.includes('--self-test')) { + const selfTestCode = selfTest(); + if (!selfTestReachedVerdict) { + console.error( + '\n✗ invoked-as self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + process.exit(selfTestCode); + } console.log('usage: node scripts/invoked-as.mjs --self-test'); } diff --git a/scripts/js-comment-mask.mjs b/scripts/js-comment-mask.mjs index 5291aa36e1..f12dd5bdcf 100644 --- a/scripts/js-comment-mask.mjs +++ b/scripts/js-comment-mask.mjs @@ -411,6 +411,13 @@ export function maskComments(source) { * `GHOST` marks genuinely commented-out text that must NOT survive (keeping it * makes the gate FABRICATE a finding out of prose). */ + +// Returned by `selfTest()` only after its verdict is printed. The dispatch +// refuses anything else: a `return` that leaves the function above that line +// prints nothing and still exits 0 — a self-test that never finished, reported +// as one that passed (#13798). +const SELF_TEST_VERDICT = 'js-comment-mask self-test reached its verdict'; + export function selfTest() { const BT = String.fromCharCode(96); // backtick, kept out of the literal below const cases = [ @@ -569,13 +576,24 @@ export function selfTest() { process.exit(1); } console.log(`\u2713 js-comment-mask self-test: ${total} cases pass (${cases.length} mask/strip corpus, ${extra.length} interpolation view).`); + + return SELF_TEST_VERDICT; } // Executed only as a CLI. Importing this module must have NO side effect: the // gates below it are the callers, and a shared module that exits on import is // a shared module nobody can share. if (isEntrypoint(import.meta.url)) { - if (process.argv.includes('--self-test')) selfTest(); + if (process.argv.includes('--self-test')) { + if (selfTest() !== SELF_TEST_VERDICT) { + console.error( + '\n✗ js-comment-mask self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + } else { console.error('usage: node scripts/js-comment-mask.mjs --self-test'); process.exit(2); diff --git a/scripts/measure-position-name-fold-census.mjs b/scripts/measure-position-name-fold-census.mjs index f16f99c304..da07fd2d85 100644 --- a/scripts/measure-position-name-fold-census.mjs +++ b/scripts/measure-position-name-fold-census.mjs @@ -686,6 +686,13 @@ const AUDIT_CONTROLS = [ }, ]; +// Set by `selfTest()` only after its verdict is printed, and read at the +// dispatch: a `return` that leaves the function above that line prints nothing +// and still exits 0 — a self-test that never finished, reported as one that +// passed (#13798). The self-test's own exit code stays load-bearing, so the +// handshake is a flag rather than a returned sentinel. +let selfTestReachedVerdict = false; + function selfTest({ quiet = false } = {}) { const problems = []; const c = census(); @@ -785,6 +792,7 @@ function selfTest({ quiet = false } = {}) { + ` ${AUDIT_CONTROLS.length} audit-generator controls pass\n`, ); } + selfTestReachedVerdict = true; return 0; } @@ -905,7 +913,18 @@ function main(argv) { process.stdout.write(`${AUDIT_SCHEMA}\n`); return 0; } - if (argv.includes('--self-test')) return selfTest(); + if (argv.includes('--self-test')) { + const selfTestCode = selfTest(); + if (!selfTestReachedVerdict) { + console.error( + '\n✗ measure-position-name-fold-census self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + return selfTestCode; + } // ⚠️ Every reading is gated on the controls. The ruling asked for a census // WITH positive controls, and a number printed by an instrument that has not diff --git a/scripts/measure-stall-guard-headroom.mjs b/scripts/measure-stall-guard-headroom.mjs index 18e5cd8fea..8980c0a7b4 100644 --- a/scripts/measure-stall-guard-headroom.mjs +++ b/scripts/measure-stall-guard-headroom.mjs @@ -710,6 +710,14 @@ export async function main(argv, io = {}) { * this repo -- all seven guarded steps carry distinct names, which case 8 asserts * rather than assumes -- and a refusal branch nobody can trigger is decoration. */ + +// Set by `selfTest()` only after its verdict is printed, and read at the +// dispatch: a `return` that leaves the function above that line prints nothing +// and still exits 0 — a self-test that never finished, reported as one that +// passed (#13798). The self-test's own exit code stays load-bearing, so the +// handshake is a flag rather than a returned sentinel. +let selfTestReachedVerdict = false; + export async function selfTest() { const failures = []; let checked = 0; @@ -1026,11 +1034,25 @@ export async function selfTest() { return 1; } console.log(`measure-stall-guard-headroom --self-test: ${checked} assertion(s) passed.`); + selfTestReachedVerdict = true; return 0; } if (isEntrypoint(import.meta.url)) { const argv = process.argv.slice(2); - const code = argv.includes('--self-test') ? await selfTest() : await main(argv); + let code; + if (argv.includes('--self-test')) { + code = await selfTest(); + if (!selfTestReachedVerdict) { + console.error( + '\n✗ measure-stall-guard-headroom self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + } else { + code = await main(argv); + } process.exit(code); } diff --git a/scripts/measure-test-shard-timings.mjs b/scripts/measure-test-shard-timings.mjs index 924c32ed46..d271b125ce 100644 --- a/scripts/measure-test-shard-timings.mjs +++ b/scripts/measure-test-shard-timings.mjs @@ -174,6 +174,12 @@ export function buildDataset({ perSummary, fileCounts, provenance }) { }; } +// Returned by `selfTest()` only after its verdict is printed. The dispatch +// refuses anything else: a `return` that leaves the function above that line +// prints nothing and still exits 0 — a self-test that never finished, reported +// as one that passed (#13798). +const SELF_TEST_VERDICT = 'measure-test-shard-timings self-test reached its verdict'; + function selfTest() { const summary = (tasks) => ({ tasks }); const testTask = (pkg, start, end, status = 'MISS', exitCode = 0) => ({ @@ -279,6 +285,8 @@ function selfTest() { if (flat === null || path.basename(flat) !== 'spec') throw new Error('workspace: a depth-1 package stopped resolving'); console.log('measure-test-shard-timings: self-test OK'); + + return SELF_TEST_VERDICT; } // Resolve a package name to its directory, so the fallback rate can be derived @@ -331,7 +339,14 @@ function packageDirForName(name) { function main() { const argv = process.argv.slice(2); if (argv.includes('--self-test')) { - selfTest(); + if (selfTest() !== SELF_TEST_VERDICT) { + console.error( + '\n✗ measure-test-shard-timings self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } return; } let out = DEFAULT_OUT; diff --git a/scripts/objectui-changeset-digest.mjs b/scripts/objectui-changeset-digest.mjs index 01483f9d1c..36b62d7e12 100644 --- a/scripts/objectui-changeset-digest.mjs +++ b/scripts/objectui-changeset-digest.mjs @@ -1113,6 +1113,13 @@ export function buildDigest({ // CLI // --------------------------------------------------------------------------- +// Set by `selfTest()` only after its verdict is printed, and read at the +// dispatch: a `return` that leaves the function above that line prints nothing +// and still exits 0 — a self-test that never finished, reported as one that +// passed (#13798). The self-test's own exit code stays load-bearing, so the +// handshake is a flag rather than a returned sentinel. +let selfTestReachedVerdict = false; + function main(argv) { const has = (f) => argv.includes(f); const val = (f, d) => { @@ -1131,7 +1138,18 @@ function main(argv) { return 0; } - if (has('--self-test')) return selfTest(); + if (has('--self-test')) { + const selfTestCode = selfTest(); + if (!selfTestReachedVerdict) { + console.error( + '\n✗ objectui-changeset-digest self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + return selfTestCode; + } const objectuiRoot = val('--objectui-root', join(REPO_ROOT, '..', 'objectui')); const frameworkRoot = val('--framework-root', REPO_ROOT); @@ -3281,6 +3299,7 @@ function selfTest() { return 1; } console.log('✓ objectui-changeset-digest --self-test: all checks passed'); + selfTestReachedVerdict = true; return 0; } diff --git a/scripts/objectui-range.mjs b/scripts/objectui-range.mjs index 231bdd0902..d017e4f52d 100644 --- a/scripts/objectui-range.mjs +++ b/scripts/objectui-range.mjs @@ -348,6 +348,13 @@ function main() { // code over it, assert the ARTIFACT (the markdown a maintainer pastes). // --------------------------------------------------------------------------- +// Set by `selfTest()` only after its verdict is printed, and read at the +// dispatch: a `return` that leaves the function above that line prints nothing +// and still exits 0 — a self-test that never finished, reported as one that +// passed (#13798). The self-test's own exit code stays load-bearing, so the +// handshake is a flag rather than a returned sentinel. +let selfTestReachedVerdict = false; + function selfTest() { const failures = []; const check = (name, cond, detail = '') => { @@ -661,10 +668,23 @@ function selfTest() { return 1; } console.log('✓ objectui-range --self-test: all checks passed'); + selfTestReachedVerdict = true; return 0; } if (isEntrypoint(import.meta.url)) { if (has('-h') || has('--help')) process.exit(printHelp()); - process.exit(has('--self-test') ? selfTest() : main()); + if (has('--self-test')) { + const selfTestCode = selfTest(); + if (!selfTestReachedVerdict) { + console.error( + '\n✗ objectui-range self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + process.exit(selfTestCode); + } + process.exit(main()); } diff --git a/scripts/partition-test-shards.mjs b/scripts/partition-test-shards.mjs index b51038edc1..e3f1c00586 100644 --- a/scripts/partition-test-shards.mjs +++ b/scripts/partition-test-shards.mjs @@ -312,6 +312,12 @@ export function readPackageItems(parsed, listPath) { return items; } +// Returned by `selfTest()` only after its verdict is printed. The dispatch +// refuses anything else: a `return` that leaves the function above that line +// prints nothing and still exits 0 — a self-test that never finished, reported +// as one that passed (#13798). +const SELF_TEST_VERDICT = 'partition-test-shards self-test reached its verdict'; + function selfTest() { const mk = (name, weight) => ({ name, weight }); // Coverage + determinism: every package lands in exactly one bin, and two @@ -543,12 +549,21 @@ function selfTest() { `max/mean ${balance.ratio.toFixed(2)}x <= ${MAX_SHARD_OVER_MEAN}x, floor ${balance.floor.toFixed(0)}s, ` + `bins ${balance.totals.map((t) => t.toFixed(0)).join('/')}s)` ); + + return SELF_TEST_VERDICT; } function main() { const argv = process.argv.slice(2); if (argv.includes('--self-test')) { - selfTest(); + if (selfTest() !== SELF_TEST_VERDICT) { + console.error( + '\n✗ partition-test-shards self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } return; } diff --git a/scripts/pr-labels.mjs b/scripts/pr-labels.mjs index c71988a992..151c9124b8 100644 --- a/scripts/pr-labels.mjs +++ b/scripts/pr-labels.mjs @@ -517,6 +517,12 @@ async function runPaths(dryRun) { // Self-test. // --------------------------------------------------------------------------- +// Returned by `selfTest()` only after its verdict is printed. The dispatch +// refuses anything else: a `return` that leaves the function above that line +// prints nothing and still exits 0 — a self-test that never finished, reported +// as one that passed (#13798). +const SELF_TEST_VERDICT = 'pr-labels self-test reached its verdict'; + function selfTest() { const failures = []; const check = (name, actual, expected) => { @@ -741,6 +747,8 @@ function selfTest() { process.exit(1); } console.log('VERDICT: pr-labels self-test PASSED'); + + return SELF_TEST_VERDICT; } // --------------------------------------------------------------------------- @@ -750,7 +758,14 @@ async function main() { const dryRun = args.has('--dry-run'); if (args.has('--self-test')) { - selfTest(); + if (selfTest() !== SELF_TEST_VERDICT) { + console.error( + '\n✗ pr-labels self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } return; } if (args.has('--size')) { diff --git a/scripts/publish-smoke-pack.mjs b/scripts/publish-smoke-pack.mjs index bd6fe231ea..5545a0ce6e 100644 --- a/scripts/publish-smoke-pack.mjs +++ b/scripts/publish-smoke-pack.mjs @@ -179,6 +179,13 @@ async function main() { * (a `@objectstack/*` scope glob in the pinning prose, a by-name exclusion in * the derivation) is invisible to any fixture whose names all start with `@`. */ + +// Returned by `selfTest()` only after its verdict is printed. The dispatch +// refuses anything else: a `return` that leaves the function above that line +// prints nothing and still exits 0 — a self-test that never finished, reported +// as one that passed (#13798). +const SELF_TEST_VERDICT = 'publish-smoke-pack self-test reached its verdict'; + function selfTest() { const cases = []; const check = (name, fn) => { @@ -244,11 +251,20 @@ function selfTest() { console.log('publish-smoke-pack self-test'); for (const line of cases) console.log(line); console.log(process.exitCode === 1 ? 'SELF-TEST FAILED' : `SELF-TEST PASSED (${cases.length} cases)`); + + return SELF_TEST_VERDICT; } if (isEntrypoint(import.meta.url)) { if (process.argv.includes('--self-test')) { - selfTest(); + if (selfTest() !== SELF_TEST_VERDICT) { + console.error( + '\n✗ publish-smoke-pack self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } } else { main().catch((err) => { console.error(err.stack ?? String(err)); diff --git a/scripts/qa/qa-rollup.mjs b/scripts/qa/qa-rollup.mjs index d800c9bc09..bbea74521d 100755 --- a/scripts/qa/qa-rollup.mjs +++ b/scripts/qa/qa-rollup.mjs @@ -875,6 +875,12 @@ function assert(cond, msg, failures) { return cond ? 1 : 0; } +// Returned by `selfTest()` only after its verdict is printed. The dispatch +// refuses anything else: a `return` that leaves the function above that line +// prints nothing and still exits 0 — a self-test that never finished, reported +// as one that passed (#13798). +const SELF_TEST_VERDICT = 'qa-rollup self-test reached its verdict'; + async function selfTest() { const failures = []; let checked = 0; @@ -1123,6 +1129,8 @@ async function selfTest() { console.log( `✓ qa-rollup --self-test: ${checked} assertions over ${FIXTURE_TITLES.length} canonical shapes and ${RETIRED_TITLES.length} retired ones`, ); + + return SELF_TEST_VERDICT; } // The guard comes FIRST, then the mode. The other order — `--self-test` @@ -1132,6 +1140,15 @@ async function selfTest() { // (this branch does not exit on success) and shows only as foreign output on // the importer's stdout. if (isEntrypoint(import.meta.url)) { - if (process.argv.includes('--self-test')) await selfTest(); + if (process.argv.includes('--self-test')) { + if ((await selfTest()) !== SELF_TEST_VERDICT) { + console.error( + '\n✗ qa-rollup self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + } else await main(process.argv.slice(2)); } diff --git a/scripts/release-github-releases.mjs b/scripts/release-github-releases.mjs index c44b340298..7b97ecdb02 100644 --- a/scripts/release-github-releases.mjs +++ b/scripts/release-github-releases.mjs @@ -675,6 +675,12 @@ function stubFetch({ existing = {}, failCreateFor = new Set() } = {}) { return { impl, calls }; } +// Returned by `selfTest()` only after its verdict is printed. The dispatch +// refuses anything else: a `return` that leaves the function above that line +// prints nothing and still exits 0 — a self-test that never finished, reported +// as one that passed (#13798). +const SELF_TEST_VERDICT = 'release-github-releases self-test reached its verdict'; + async function selfTest() { /** @type {string[]} */ const failures = []; @@ -957,13 +963,22 @@ async function selfTest() { `✓ release-github-releases --self-test: ${assertions} assertions ` + `(real packages/spec/CHANGELOG.md 17.0.0-rc.2 section = ${measure(rc2 ?? '')} chars -> ${measure(big.body)}, limit ${BODY_LIMIT})`, ); + + return SELF_TEST_VERDICT; } const invokedDirectly = isEntrypoint(import.meta.url); if (invokedDirectly) { try { if (process.argv.includes('--self-test')) { - await selfTest(); + if ((await selfTest()) !== SELF_TEST_VERDICT) { + console.error( + '\n✗ release-github-releases self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } } else { await main({ dryRun: process.argv.includes('--dry-run') }); } diff --git a/scripts/render-release-coverage-anchor.mjs b/scripts/render-release-coverage-anchor.mjs index f715cbcccd..baf675af33 100644 --- a/scripts/render-release-coverage-anchor.mjs +++ b/scripts/render-release-coverage-anchor.mjs @@ -165,6 +165,13 @@ function expect(what, ok) { console.error(` FAIL ${what}`); } +// Set by `selfTest()` only after its verdict is printed, and read at the +// dispatch: a `return` that leaves the function above that line prints nothing +// and still exits 0 — a self-test that never finished, reported as one that +// passed (#13798). The self-test's own exit code stays load-bearing, so the +// handshake is a flag rather than a returned sentinel. +let selfTestReachedVerdict = false; + function selfTest() { const base = { report: 'check-release-section-coverage: 2 finding(s)', @@ -227,11 +234,23 @@ function selfTest() { console.log(failures === 0 ? `\nOK render-release-coverage-anchor --self-test: ${assertions} assertions pass` : `\nFAILED ${failures} of ${assertions} assertion(s)`); + selfTestReachedVerdict = true; return failures === 0 ? 0 : 1; } function main(argv) { - if (argv.includes('--self-test')) return selfTest(); + if (argv.includes('--self-test')) { + const selfTestCode = selfTest(); + if (!selfTestReachedVerdict) { + console.error( + '\n✗ render-release-coverage-anchor self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + return selfTestCode; + } const tmp = process.env.RUNNER_TEMP || '.'; const read = (name) => { diff --git a/scripts/sync-docs-image-tags.mjs b/scripts/sync-docs-image-tags.mjs index d2282e10d5..2b2e490499 100644 --- a/scripts/sync-docs-image-tags.mjs +++ b/scripts/sync-docs-image-tags.mjs @@ -286,6 +286,12 @@ function main() { // positive control on a temp fixture, paired with a byte-identity control on a clean one. // --------------------------------------------------------------------------- +// Returned by `selfTest()` only after its verdict is printed. The dispatch +// refuses anything else: a `return` that leaves the function above that line +// prints nothing and still exits 0 — a self-test that never finished, reported +// as one that passed (#13798). +const SELF_TEST_VERDICT = 'sync-docs-image-tags self-test reached its verdict'; + function selfTest() { const failures = []; let checked = 0; @@ -578,6 +584,8 @@ function selfTest() { + 'BYTE-IDENTICAL and unwritten; rolling tags, the X.Y.Z metavariable, placeholders, interpolations and ' + 'version-shaped prose are observed UNMOVED.', ); + + return SELF_TEST_VERDICT; } // --------------------------------------------------------------------------- @@ -587,7 +595,14 @@ function selfTest() { // the gate's version of the same bug. if (isEntrypoint(import.meta.url)) { if (process.argv.includes('--self-test')) { - selfTest(); + if (selfTest() !== SELF_TEST_VERDICT) { + console.error( + '\n✗ sync-docs-image-tags self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } } else { main(); } diff --git a/scripts/sync-template-versions.mjs b/scripts/sync-template-versions.mjs index fa725f01ec..f00d969628 100644 --- a/scripts/sync-template-versions.mjs +++ b/scripts/sync-template-versions.mjs @@ -534,6 +534,12 @@ function runFixture(script) { return { status: result.status, output: `${result.stdout ?? ''}${result.stderr ?? ''}` }; } +// Returned by `selfTest()` only after its verdict is printed. The dispatch +// refuses anything else: a `return` that leaves the function above that line +// prints nothing and still exits 0 — a self-test that never finished, reported +// as one that passed (#13798). +const SELF_TEST_VERDICT = 'sync-template-versions self-test reached its verdict'; + function selfTest() { const failures = []; let checked = 0; @@ -768,6 +774,8 @@ function selfTest() { 'The STALE -> rewritten direction and the discovery walk belong to ' + 'packages/create-objectstack/src/template-version-stamps.test.ts and are deliberately not restated here.', ); + + return SELF_TEST_VERDICT; } // Entry-point guard (#9554), the same one #9064 added to check-docs-image-tag.mjs @@ -776,7 +784,14 @@ function selfTest() { // export it was working around. if (isEntrypoint(import.meta.url)) { if (process.argv.includes('--self-test')) { - selfTest(); + if (selfTest() !== SELF_TEST_VERDICT) { + console.error( + '\n✗ sync-template-versions self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } } else { main(); } diff --git a/scripts/ts-parse.mjs b/scripts/ts-parse.mjs index a49e92a954..5da949a769 100644 --- a/scripts/ts-parse.mjs +++ b/scripts/ts-parse.mjs @@ -513,6 +513,14 @@ export function transpileChecked(fileName, text, transpileOptions = {}) { * spawn a real child and read what it printed and what status it left, exactly * as `invoked-as.mjs` drives a real symlink rather than a model of one. */ + +// Set by `selfTest()` only after its verdict is printed, and read at the +// dispatch: a `return` that leaves the function above that line prints nothing +// and still exits 0 — a self-test that never finished, reported as one that +// passed (#13798). The self-test's own exit code stays load-bearing, so the +// handshake is a flag rather than a returned sentinel. +let selfTestReachedVerdict = false; + export function selfTest() { const cases = []; const t = (name, ok, detail) => cases.push({ name, ok: Boolean(ok), detail }); @@ -766,10 +774,22 @@ export function selfTest() { + `across all three parser entry points, both ScriptKind directions, a Program's transitive import ` + `included, and a caller’s try/catch cannot swallow any of it).`, ); + selfTestReachedVerdict = true; return 0; } if (isEntrypoint(import.meta.url)) { - if (process.argv.includes('--self-test')) process.exit(selfTest()); + if (process.argv.includes('--self-test')) { + const selfTestCode = selfTest(); + if (!selfTestReachedVerdict) { + console.error( + '\n✗ ts-parse self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } + process.exit(selfTestCode); + } console.log('usage: node scripts/ts-parse.mjs --self-test'); }