Skip to content

Commit 62d830e

Browse files
fix(scripts): teardown-shape self-test refuses an unreachable pinned control as PREREQUISITE NOT MET (exit 3) (#18644)
Fixes #18217 Clause-②: no — a gate's exit register is not a published contract face, and nothing under any package's `files[]` moves. Re-judged from the actual diff, measured below. ## What moved, and what deliberately did not `node scripts/check-plugin-teardown-shape.mjs --self-test` exited **1** — the code reserved for a tree that is genuinely wrong — when a shallow clone could not reach the commit its positive control is pinned to. The true condition is PREREQUISITE NOT MET, which this tree already answers with exit **3**. One file, three lines of behaviour: 1. The unreachable-control refusal returns `EXIT_PREREQUISITE_NOT_MET` instead of `1`. 2. The entry point's verdict handshake flattened **every** early return to 1, so a deliberately chosen code could not have survived it. It now preserves the prerequisite code alone; every other early return — including a bare `return`, which arrives as `undefined` and whose `process.exit(undefined)` is the exit-0 hole that handshake exists to close — is still 1. 3. A one-line note beside the pin says why this code differs from the failure code. ⛔ The refusal itself is unchanged: it fires where it fired, and its text — which already names `git fetch --unshallow origin main` — is byte-identical. Only the code moved. A self-test that RAN and genuinely failed still exits 1. ### Import or respell: measured on this tree, then followed Both spellings of the constant exist under `scripts/`. **Imported** by check-dts-closure, check-i18n-bundles, check-i18n-coverage, check-i18n-walk-parity, check-sourcemap-no-sources-content, check-vendor-export-contract, ci/select-shard-timings-run, pm/check-dispatch-gates, pm/check-governed-merges and pm/dispatch-gates — the gate the card names as owning this code. **Re-declared locally** by check-test-completeness, check-release-section-coverage, pm/changeset-deadline-census, pm/check-half-states, pm/check-prior-rulings and pm/check-settings-deny-roster — each one a script that does not otherwise depend on that frame. This gate already imports `scripts/import-prerequisite.mjs` (`requireDefaultExport`), so importing the constant from it adds no dependency edge and leaves the number declared once, in the module that argues it. Taken: **import**. ## Acceptance, both directions, on a REAL shallow clone Not faked by editing the pin to a nonexistent sha. A throwaway `git clone --depth 1 file://…` of this branch's base: `git rev-parse --is-shallow-repository` reads `true`, `git cat-file -e 621a487` exits 1 (the object is genuinely absent), and `node_modules` is symlinked in so the import-prerequisite guard is not the thing being measured. | leg (same clone, same rev) | script under test | exit | |---|---|---| | unreachable pinned control | `origin/main` @ 25c9a83 | **1** | | unreachable pinned control | this branch | **3** | `diff` over both runs' stdout **and** stderr is empty: the refusal text, remedy line included, is byte-identical. | leg | reading | |---|---| | reachable control, this branch | exit 0, 48 cases pass | | reachable control, `origin/main` @ 25c9a83 | exit 0, 47 cases pass | | injected genuine failure (`preFixResult.findings.length === 1` flipped to `=== 2`) | exit **1**, `1 of 48 case(s) failed` — not 3 | | production leg (no `--self-test`), both trees | exit 0, verdict line byte-identical | The pass line is byte-identical except its case COUNT, which went **up**, 47 to 48: the added case is this behaviour's pin. That battery's floor moves 1 to 2 with it, so deleting the case reds instead of silently shrinking the battery. Nothing goes down. ### The added case The self-test spawns itself under a `git` that resolves nothing (a stub first on `PATH`, exiting 128). The branch taken is the same `show.status !== 0` the real shallow clone takes; what is pinned is the exit code together with the remedy text. Out of process deliberately — the code is only observable at the dispatch, and an in-process call would read a number the entry point could still flatten. ## Ablation ledger Every leg through `scripts/ablation-replace.mjs` (anchor must hit, before/after counts, blob hashes, proven restore, EXIT/INT/TERM trap). Each restore printed `blob == HEAD (e4d9e35)` and an empty `git diff HEAD`. | mutation | prediction | observed | |---|---|---| | `return EXIT_PREREQUISITE_NOT_MET;` to `return 1;` | the new pin reds | `1 of 48 case(s) failed`, exit 1 — the case reports the child status as 1 | | dispatch `selfTestCode === EXIT_PREREQUISITE_NOT_MET ? … : 1` to `process.exit(1);` | the new pin reds | `1 of 48 case(s) failed`, exit 1 | | `preFixResult.findings.length === 1` to `=== 2` (a self-test that RAN and failed) | exit 1, never 3 | exit **1**, `1 of 48 case(s) failed` | | `const SELF = fileURLToPath(import.meta.url);` to `return;` (the hole-2 handshake probe) | handshake holds: paragraph printed, exit 1, never 0 | exit **1**, paragraph printed | ## Gates Derived at 19cae6e with `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`: **30 commands**, every one run, each exit code recorded, then reconciled: ``` ✓ dispatch-gates --ran: 30 derived famil(ies) accounted for — 30 run, 0 NOT-MEASURED (a DERIVED zero — all 30 recorded an exit code and none of them is 3). ``` All 30 recorded `exit 0`. `check:pm-dispatch-gates` was detached and waited on by recorded PID with `tail --pid`, 818.5s on this box; its `exit 0` is read from two facts rather than claimed — the `&&` chain advanced past the first leg (so that leg exited 0), and the second leg was then re-run standalone with its status captured before any pipe (`exit=0`, 812.2s, 1746 cases pass). ## Changeset `skip-changeset`, measured rather than assumed: the diff's only path is judged against every non-private manifest's `files[]` (70 manifests; the repo root is `private`). `scripts/check-plugin-teardown-shape.mjs` lies under no package directory, so nothing published moves. Firing control on the same probe: `packages/lint/CHANGELOG.md` reads PUBLISHED via `@objectstack/lint` `files[] "CHANGELOG.md"`. ## Acceptance notes - `node scripts/measure-self-test-floor.mjs --probe --only scripts/check-plugin-teardown-shape.mjs` refuses on this tree (its handshake recogniser and the landed spellings disagree about three unrelated files) — so the handshake column is NOT MEASURED by that instrument here. Control: the refusal is byte-identical on `origin/main` @ 25c9a83, so it pre-exists this change. The handshake is instead proven directly by the fourth ablation leg above. Reported to the dispatching seat as a candidate card; not filed from here. - `lint.yml`'s step comment for this gate says "one `git show` and two spawns"; the added case makes it three. Not edited: this card's declared file surface is `scripts/`. Noted, not filed. --- _Generated by [Claude Code](https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent d6137fd commit 62d830e

1 file changed

Lines changed: 36 additions & 5 deletions

File tree

‎scripts/check-plugin-teardown-shape.mjs‎

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -141,10 +141,10 @@
141141
import { spawnSync } from 'node:child_process';
142142
import { mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, symlinkSync, writeFileSync } from 'node:fs';
143143
import { tmpdir } from 'node:os';
144-
import { dirname, join, relative, resolve, sep } from 'node:path';
144+
import { delimiter, dirname, join, relative, resolve, sep } from 'node:path';
145145
import { fileURLToPath } from 'node:url';
146146

147-
import { requireDefaultExport } from './import-prerequisite.mjs';
147+
import { EXIT_PREREQUISITE_NOT_MET, requireDefaultExport } from './import-prerequisite.mjs';
148148
const ts = await requireDefaultExport('typescript', () => import('typescript'), import.meta.url);
149149

150150
import { isEntrypoint } from './invoked-as.mjs';
@@ -175,7 +175,7 @@ import { parseSourceFile } from './ts-parse.mjs';
175175
// self-test runs once per process, and it is what lets the existing sink route
176176
// through `registerCase()` with no case rewritten and no assertion changed.
177177
const SELF_TEST_BATTERIES = Object.freeze({
178-
'the positive control: the REAL pre-#10375 revision': 1,
178+
'the positive control: the REAL pre-#10375 revision': 2,
179179
'the negative control from the same file, as REPAIRED': 1,
180180
'the delegating alias, both directions': 3,
181181
'every roster name reds': 4,
@@ -334,6 +334,10 @@ export const DELIBERATELY_EXCLUDED = {
334334
* landed the #9371 fix -- `async stop()`, no `destroy()`. The revision is the
335335
* squash merge's first parent, so the fixture is pinned to a commit rather than
336336
* to a copy of a file that could be edited into passing.
337+
*
338+
* A checkout that cannot reach this rev refuses with `EXIT_PREREQUISITE_NOT_MET`,
339+
* not the finding code 1: could-not-run is a different fact from ran-and-was-wrong
340+
* (#18217).
337341
*/
338342
const POSITIVE_CONTROL = {
339343
rev: '621a487607881c66b2899b7e3477115229a156b4',
@@ -702,7 +706,7 @@ export function selfTest() {
702706
+ '\n git fetch --unshallow origin main',
703707
);
704708
rmSync(dir, { recursive: true, force: true });
705-
return 1;
709+
return EXIT_PREREQUISITE_NOT_MET;
706710
}
707711
const preFix = tree('positive', { [POSITIVE_CONTROL.path.slice(POPULATION_ROOT.length + 1)]: show.stdout });
708712
const preFixResult = audit(preFix);
@@ -715,6 +719,28 @@ export function selfTest() {
715719
JSON.stringify(preFixResult.findings),
716720
);
717721

722+
// ...and the other side of the same branch, OUT OF PROCESS: a checkout that
723+
// cannot resolve the pinned rev. The shallow clone that produced #18217 is
724+
// reproduced here by shadowing `git` with one that resolves nothing -- the
725+
// branch taken is the one above, `show.status !== 0`, and what is pinned is
726+
// that it refuses with the PREREQUISITE code while still naming the remedy.
727+
// Pinned out of process because the code is only observable at the dispatch:
728+
// an in-process call would read a number the entry point could still flatten.
729+
const noRevBin = join(dir, 'no-such-rev-bin');
730+
mkdirSync(noRevBin, { recursive: true });
731+
writeFileSync(join(noRevBin, 'git'), '#!/bin/sh\nexit 128\n', { mode: 0o755 });
732+
const unreachable = spawnSync(process.execPath, [SELF, '--self-test'], {
733+
encoding: 'utf8',
734+
env: { ...process.env, PATH: `${noRevBin}${delimiter}${process.env.PATH ?? ''}` },
735+
});
736+
t(
737+
'an UNREACHABLE positive control refuses as PREREQUISITE NOT MET, naming the remedy -- not as a finding',
738+
unreachable.status === EXIT_PREREQUISITE_NOT_MET
739+
&& unreachable.status !== 1
740+
&& /git fetch --unshallow origin main/.test(`${unreachable.stderr ?? ''}`),
741+
JSON.stringify({ status: unreachable.status, err: (unreachable.stderr || '').slice(0, 240) }),
742+
);
743+
718744
// -- the negative control from the same file, as REPAIRED ----------------
719745
battery('the negative control from the same file, as REPAIRED');
720746
const repaired = readFileSync(join(REPO_ROOT, POSITIVE_CONTROL.path), 'utf8');
@@ -920,7 +946,12 @@ if (isEntrypoint(import.meta.url)) {
920946
+ 'so no success line was printed. Exiting 0 here would report a self-test\n'
921947
+ 'that never finished as a self-test that passed.\n',
922948
);
923-
process.exit(1);
949+
// 1 is still the default for every early return, INCLUDING one that returns
950+
// nothing at all (an injected bare `return` arrives here as `undefined`, and
951+
// `process.exit(undefined)` is exit 0 -- the hole this handshake exists to
952+
// close). The single exception is the code the self-test chose deliberately:
953+
// a refusal because a prerequisite was not met is NOT MEASURED, not a finding.
954+
process.exit(selfTestCode === EXIT_PREREQUISITE_NOT_MET ? EXIT_PREREQUISITE_NOT_MET : 1);
924955
}
925956
process.exit(selfTestCode);
926957
}

0 commit comments

Comments
 (0)