@@ -928,10 +928,40 @@ function fixtureSelfTest() {
928928 'node -e "console.error(\'stub-gate: PREREQUISITE NOT MET — the dependency yaml is not installed\'); process.exit(3)"' ,
929929 } ;
930930
931+ /**
932+ * The pnpm the fixture's stub gate runs under, pinned to the repository's own.
933+ *
934+ * `runCheck` spawns every gate with `pnpm -s`, and CI reaches pnpm through
935+ * Corepack (`.github/actions/setup-pnpm`). Corepack resolves the version from
936+ * the project it is invoked IN, so a fixture carrying no `packageManager` field
937+ * does not inherit this repo's pin and does not get "whatever the machine has"
938+ * either — it gets the registry's `latest` dist-tag, re-resolved on every run.
939+ * That made this throwaway fixture a canary for whatever pnpm major npm had
940+ * published that morning: the day `latest` moved to a major whose CLI rejects
941+ * `-s`, the "clean" stub exited 2 without ever running, and every case that
942+ * reads the stub's own exit code went red on a tree nobody had touched. It is
943+ * the FIXTURE that was unpinned, never production — the real hook runs `pnpm -s`
944+ * in this repository, which is pinned.
945+ *
946+ * Read off the ROOT manifest rather than written as a literal, so the fixture
947+ * cannot drift from the pnpm this repo pins. The case below asserts both halves,
948+ * because a missing field on either side would compare equal and silently
949+ * restore the bug.
950+ */
951+ const rootPackageManager = JSON . parse ( readFileSync ( join ( REPO_ROOT , 'package.json' ) , 'utf8' ) ) . packageManager ;
952+
931953 const runHook = ( gate , args = [ ] ) => {
932954 writeFileSync (
933955 join ( dir , 'package.json' ) ,
934- `${ JSON . stringify ( { name : 'os-regen-fixture' , scripts : { 'check:spec-changes' : GATE_STUBS [ gate ] } } , null , 2 ) } \n` ,
956+ `${ JSON . stringify (
957+ {
958+ name : 'os-regen-fixture' ,
959+ packageManager : rootPackageManager ,
960+ scripts : { 'check:spec-changes' : GATE_STUBS [ gate ] } ,
961+ } ,
962+ null ,
963+ 2 ,
964+ ) } \n`,
935965 ) ;
936966 // `spawnSync`, not `execFileSync`: every message this script prints goes to
937967 // STDERR, which execFileSync returns only on the failure path — capturing the
@@ -1000,6 +1030,20 @@ function fixtureSelfTest() {
10001030 writeFileSync ( marker , `${ pendingPath } \n` ) ;
10011031
10021032 const merge = runHook ( 'stale' ) ;
1033+
1034+ // The fixture's OWN prerequisite, asserted before any verdict is read out of a
1035+ // gate's exit code: every case below spawns `pnpm -s` inside this directory, so
1036+ // an unpinned fixture grades the registry's `latest` pnpm instead of the one
1037+ // this repository pins, and the failure lands on cases that have nothing to do
1038+ // with pnpm. ⛔ Not `=== rootPackageManager` on its own: were the root manifest
1039+ // to lose the field, both sides would read `undefined`, this case would pass,
1040+ // and the fixture would be back to resolving `latest` in silence.
1041+ const fixtureManifest = JSON . parse ( readFileSync ( join ( dir , 'package.json' ) , 'utf8' ) ) ;
1042+ check ( "the fixture pins the ROOT's packageManager, so Corepack cannot resolve `latest`" ,
1043+ typeof rootPackageManager === 'string'
1044+ && / ^ p n p m @ \d / . test ( rootPackageManager )
1045+ && fixtureManifest . packageManager === rootPackageManager ) ;
1046+
10031047 check ( 'a MERGE commit with stale artifacts is ACCEPTED as a deferral' , merge . code === 0 ) ;
10041048 check ( ' …and says so — "DEFERRED", not a silent pass' , / D E F E R R E D t o t h e n e x t c o m m i t / . test ( merge . out ) ) ;
10051049 check ( ' …and points at the sanctioned procedure, never at skipping the hook' ,
0 commit comments