Skip to content

fix(devx): pin the regen self-test fixture to the root's packageManager so Corepack never resolves pnpm latest (#15992) - #16002

Merged
os-project-manager merged 1 commit into
mainfrom
claude/issue-15992-regen-selftest-fixture-pin
Sep 5, 2026
Merged

fix(devx): pin the regen self-test fixture to the root's packageManager so Corepack never resolves pnpm latest (#15992)#16002
os-project-manager merged 1 commit into
mainfrom
claude/issue-15992-regen-selftest-fixture-pin

Conversation

@claude

@claude claude Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Fixes #15992
Fixes #15994
Related: #15990 (first-filed anchor for the same red check, claimed by another session)

p0 fix-forward for a red main; the seat lands it ahead of the queue per the main-red convention.

The CI symptom

main has been red on Lint & Repo Gates since 2026-09-05T17:05Z, and the merge queue has been blocked for every lane. The failing step is Merge-driver wiring gate (pnpm check:merge-driver), inside check-regen-pending.mjs --self-test, section "deferred-merge sequence, replayed on a throwaway repo":

  ✗ regeneration DISCHARGES the deferral and clears the marker
  ✗ a gate that cannot LOAD is PREREQUISITE NOT MET, not `stale`
  ✗ a gate whose RUNNER is not installed refuses the same way
  ✗ a gate that ALREADY refused with an unmet prerequisite is propagated, not relabelled

✗ self-test failed -- 1 failure(s) (cases and floor).

Reference reading: push run 33979882868, job 101343111648, step at 17:26Z. Every lane whose step executed after ~17:22Z fails identically, and the last pass was a merge_group run at ~17:21Z on the same SHA — so the tree was never the cause.

Root cause, in three sentences

The self-test replays the deferred-merge sequence on a throwaway repo and spawns its stub gate with execSync('pnpm -s …'), and the fixture's package.json carried no packageManager field. CI reaches pnpm through Corepack (.github/actions/setup-pnpm = corepack enable + corepack install), and Corepack resolves the version from the project it is invoked in — so an unpinned fixture does not inherit this repository's pin and does not get whatever the machine has either: it re-resolves the npm latest dist-tag on every run. latest moved to pnpm@12.3.4, a major whose CLI rejects -s, so the "clean" stub exited 2 without ever running and every case that reads the stub's own exit code went red on trees nobody had touched.

Production was never affected: the real hook runs pnpm -s in this repository, which is pinned. It was only ever the fixture.

The change

One file, scripts/check-regen-pending.mjs, two hunks:

  1. fixtureSelfTestrunHook now writes the ROOT manifest's packageManager into the fixture package.json, read at self-test time from the repository root rather than written as a literal, so the fixture can never drift from the pnpm this repo pins.
  2. One new case, first in the replayed sequence, pins the regression: it asserts the fixture's manifest carries a real pnpm@VERSION pin and that it equals the root's. Both halves are deliberate — === rootPackageManager alone would compare undefined to undefined if the root manifest ever lost the field, and the fixture would go back to resolving latest in silence.

Nothing else moves. execSync('pnpm -s …'), the workflows, the composite actions and every governed surface are untouched; the .git/info/exclude invariant that keeps the stub out of the index (#9258) is unchanged, since adding a key to that JSON does not make it tracked.

The self-test's battery floor is unaffected: SELF_TEST_BATTERIES floors each callee at 1 registration, not at an assertion count, so a new check() inside fixtureSelfTest neither raises nor lowers it. SELF_TEST_BATTERY_FLOOR stays 3.

Reproduction — the way CI sees it

The local machine's pnpm is a plain 10.31.0 global install, not Corepack, which is why the gate passed locally on the broken tree. A PATH shim is what makes the reproduction real:

mkdir -p "$SHIM" "$CPHOME"
printf '#!/bin/sh\nexec corepack pnpm "$@"\n' > "$SHIM/pnpm" && chmod +x "$SHIM/pnpm"
COREPACK_HOME="$CPHOME" corepack install          # materialises the root pin, 10.31.0
PATH="$SHIM:$PATH" COREPACK_HOME="$CPHOME" node scripts/check-regen-pending.mjs --self-test

Unfixed tree — red, exactly the CI shape:

✗ regeneration DISCHARGES the deferral and clears the marker
… ✗ self-test failed -- 1 failure(s) (cases and floor).       exit 1

Fixed tree — green, against the same Corepack store, which by then had already written lastKnownGood = 12.3.4:

✓ the fixture pins the ROOT's packageManager, so Corepack cannot resolve `latest`
… ✓ check-regen-pending self-test passed.                      exit 0

That last detail is the load-bearing one: a store that has already seen 12.x is not repaired by COREPACK_DEFAULT_TO_LATEST=0, but it is repaired by the pin.

Isolated, the mechanism is one command. Unpinned fixture directory:

$ pnpm -s check:spec-changes
error: unexpected argument '-s' found          exit 2

The same directory with the root pin added: exit 0, no output.

Verification

  • pnpm check:merge-driverexit 0, both halves (git-merge-regen --self-test, then ✓ check-regen-pending self-test passed.). git-merge-regen.mjs writes no throwaway fixture at all — it only reads the real root manifest — so it was never implicated and stays green.
  • Ablation of the new case, from the committed tree: deleting packageManager: rootPackageManager (marker count 1 → 0, blob hash moved off the HEAD blob) turns the new line into ✗ the fixture pins the ROOT's packageManager …. Restored via git checkout HEAD -- PATH, proven by an empty git diff HEAD and a file hash byte-identical to the HEAD blob.
  • node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands derived 33 families; all 33 ran green at b101c2e05. Two (@objectstack/spec check:docs and check:generated) first refused for a missing build artifact in a fresh worktree — json-schema/ absent and api-surface read off an unbuilt dist — which is NOT MEASURED, not a red; after pnpm --filter @objectstack/spec build both are exit 0 and the working tree stays clean. --ran reconciliation: 33 derived, 33 run, 0 NOT-MEASURED, 0 UNRUN.
  • The two test files that read this script (packages/spec/scripts/dist-freshness.test.ts, schema-tree-freshness.test.ts): 2 files, 21 tests, all passing.
  • turbo ls --affected against the merge base reports no packages — the changed path is a root-level script in no workspace, so no package test/typecheck is owed.
  • Lint, narrowed and declared: eslint --no-inline-config --format json over the changed file reports 1 file linted, 0 errors, 0 warnings. The narrowing excludes nothing, because eslint.config.mjs states of itself that this repo "runs one eslint.config.mjs, which never enables type-aware linting (no parserOptions.project, no typed @typescript-eslint rules) for ANY file" — with no cross-file type program, this diff cannot move the verdict on any file it does not touch. The repo-wide pnpm lint is CI's run.

Changeset

None, and skip-changeset is applied. The diff is scripts/check-regen-pending.mjs — a root-level repo tool in no workspace package, publishing nothing, so there is no user-visible release note it could feed. turbo ls --affected returning no packages is the mechanical reading of the same fact.

🤖 Generated with Claude Code

https://claude.ai/code/session_01M59rPZZFzqhfMUPFqqZTkf


Generated by Claude Code


Generated by Claude Code

`check-regen-pending.mjs --self-test` replays the deferred-merge sequence on a
throwaway repo and spawns its stub gate with `pnpm -s`. The fixture manifest
carried no `packageManager`, and Corepack resolves the version from the project
it is invoked in: for an unpinned project that is the registry's `latest`
dist-tag, re-resolved every run. When `latest` moved to a pnpm major whose CLI
rejects `-s`, the "clean" stub exited 2 without running and the gate went red on
every job, on trees nobody had touched.

The fixture now copies the ROOT manifest's pin, read at self-test time so it can
never drift from the pnpm this repo pins, and one case asserts both halves — the
value is a real `pnpm@<version>` pin and it equals the root's — so a root
manifest that lost the field cannot make the comparison hold vacuously.

Production is untouched: the real hook runs `pnpm -s` in this repository, which
is pinned.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M59rPZZFzqhfMUPFqqZTkf
@claude claude Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 5, 2026
@github-actions github-actions Bot added the size/s label Sep 5, 2026
@os-project-manager
os-project-manager marked this pull request as ready for review September 5, 2026 18:18
@os-project-manager
os-project-manager added this pull request to the merge queue Sep 5, 2026
Merged via the queue into main with commit f50c394 Sep 5, 2026
38 of 39 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-15992-regen-selftest-fixture-pin branch September 5, 2026 19:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/s skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants