Uh oh!
There was an error while loading. Please reload this page.
test(cli): the built-CLI refusal said every boot times out; the child exits 2 at once (#12618) - #12648
Conversation
… exits 2 at once
`RUN_JS_RESOLVES_FROM_DIST` ended "... and every boot below times out." The
first clause is right; the second is not what happens. On a closure-only tree
the child answers ` > Error: Command serve not found.` and exits 2 in ~200 ms,
and all four `bin/run.js` spawners reject from their `child.on('exit')` handler
with `serve exited 2 before <waitFor>` — never from their 150 s timer. A reader
following the old clause looks for a hang there is none of, while the real
reason is already on the child's stderr.
One clause, three sites: the constant, the byte-for-byte `.toBe()` pin that
guards its wording, and the docblock in
`serve-node-env-production-default.e2e.test.ts` that quotes the clause and
vouches for it being true of the siblings. The pin moves in this same commit —
that is the pin doing its job, not an obstacle — and stays `.toBe()`.
The tree already refuted itself in three places, all left intact: the comment
above the constant, this pin file's own header, and the paragraph two below the
vouching docblock, each recording `serve exited 2 before "Server is ready"`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjujZN219uFzBhSYfMykCd📓 Docs Drift Check1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 23 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 9dca3797846fbc0c8d13c03d29eebbd79db32359 && git checkout 9dca3797846fbc0c8d13c03d29eebbd79db32359
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c4f3263d903a446d1c612ff5cfd88988b9812d17 732fe0cbdccf45026343caaf970e66842b14cfc1 && git checkout -B drift-repro c4f3263d903a446d1c612ff5cfd88988b9812d17 && git merge --no-ff 732fe0cbdccf45026343caaf970e66842b14cfc1
node scripts/docs-audit/affected-docs.mjs --json c4f3263d903a446d1c612ff5cfd88988b9812d17 |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#12618
RUN_JS_RESOLVES_FROM_DISTended "… so on an unbuilt tree the child answers"command serve not found"and every boot below times out." The first clause is exactly right. The second is not what happens — and this is the sentence a developer on a fresh worktree reads instead of debugging, so a false explanation here costs the round the refusal exists to save.Measured, on this branch
Reproduced end to end with the card's own recipe, on a closure-only tree (
pnpm --filter '@objectstack/cli^...' build, nopackages/cli/dist):The child exits 2 immediately. All four
bin/run.jsspawners resolve the boot promise from theirchild.on('exit')handler, which rejects withserve exited ${code} before ${waitFor}— never from the timer, which is a different message (serve never printed …/serve never reached …) at 150 s:child.on('exit')serve-mcp-capability-collision.e2e.test.ts:246(rejects:250)150_000:213serve-mcp-stdio-answers.e2e.test.ts:242(rejects:247)150_000:210serve-stdio-stdout-purity.e2e.test.ts:238(rejects:244)150_000:207serve-node-env-production-default.e2e.test.ts:374150_000:365198 ms against a 150 s timer. (The card measured 178 ms; that is my number, not a reconciliation of it.)
The tree already refuted itself — in three places, all left intact
None of these is a copy of the clause; each records the real behaviour and none was edited:
helpers/serve-process.ts:35— 28 lines above the false clause, in the same file: "an unbuilt worktree answered› Error: command serve not found, the harness reportedserve exited 2 before "Server is ready"".serve-built-cli-prerequisite.test.ts:14— this pin file's own header, same sentence.serve-node-env-production-default.e2e.test.ts:206— two paragraphs below the docblock that vouches for "times out": "this file reported3 tests | 1 failed: the unset pin failed withserve exited 2 before "Server is ready"".The census — re-derived, not inherited
Swept for the clause's fingerprints and reverse-checked the instrument both ways, so a zero is a reading rather than a broken grep:
every boot below times out)oclif, a term the population must contain and not a substring of the test term)every boot below hangs forever)grep -rn, filesgit grepcannot see)requireBuiltCli/RUN_JS_RESOLVES_FROM_DIST), fortimes? out / timed out / timeout / hang / stall / never readyExit codes captured before any pipe (
cmd > file; EXIT=$?), after one measured miss whereEXIT=$?behind| catreadcat's status and printed0for a grep that had matched nothing.UNSET_LEG_MEASURES_THE_BUILT_DIST(:208) does not carry the clause, was not flagged by the sweep, and is untouched.The three sites, all repaired in this one commit:
packages/cli/test/helpers/serve-process.ts:63— the constant.packages/cli/test/serve-built-cli-prerequisite.test.ts:103— the byte-for-byte.toBe()pin. Updated in the same commit, and still.toBe()— not relaxed totoContain, not split. The pin doing its job is why this is a card of its own.packages/cli/test/serve-node-env-production-default.e2e.test.ts:189— the docblock that quotes and vouches for the clause. Repairing 1 and 2 alone would leave the tree still saying the false thing, with the authority of a file explaining why it deliberately does something else.A fourth site, beyond the three: a meta-claim about the pinned bytes
serve-built-cli-prerequisite.test.ts:92was titledit('reproduces, byte for byte, what the three private copies threw before the hoist'), and its comment said the literal "is the message measured on09b4f4e4e". Both become false the moment the pin is deliberately reworded — a true pin carrying a false explanation, which is the exact defect class this card is about. Retitled toit('pins the whole refusal, byte for byte'), with the comment now recording that #12539 moved the message without rewording it and that this change reworded one clause. No assertion changed: the.toBe()still compares the whole message. Flagged explicitly because it is beyond the three sites the fingerprint grep can reach.The replacement
One clause. "fails immediately" alone would lose the actionable half, so the clause names the message the reader will actually see. Every word is checkable against the table above, and the true first clause is untouched.
Anti-vacuity — the pin still bites
A green after a prose edit is indistinguishable from a green after the pin stopped comparing anything. So the constant was mutated by one character (
serve exited 2→serve exited 3) with the pin left alone. Confirmed on disk before reading any verdict — anchored injected count 1, anchored removed count 0, blob3147b0cb→c4c6308a,1 insertion 1 deletion— and the pin red, naming the mismatch:Restored under a
trap … EXIT INT TERMwith absolute paths, viagit checkout HEAD -- <path>(namedHEAD, never bare — a bare restore reads the polluted index). Restore proven by blob hash and an empty diff, never by an exit code: worktree blob3147b0cb60d8a129identical to the non-emptyHEADblob,git diff HEAD0 bytes,git status --porcelainempty.The first run of this leg failed loudly and measured nothing — the removal assertion counted a bare
serve exited 2, which also occurs in the file's counter-witness comment at:35. The anchor was widened towith "serve exited 2"and the leg re-run. Recorded because a mis-specified check that had merely been loosened until it passed would have been the same defect one level up.Nothing but prose changed
Both blobs of all three edited files transpiled with
removeComments: trueand the emitted program hashed:helpers/serve-process.ts91b6582b→b889a723serve-built-cli-prerequisite.test.ts2a46d71d→b3aab098serve-node-env-production-default.e2e.test.tsdd50ec0897b3cde7→ identicalSites 1 and 2 are string literals, not comments, so they move the emitted hash legitimately — an inequality here is expected, not a failed proof. The emitted-program diff for those two is exactly the literals and nothing else:
No
requireBuiltCli()logic, no change to which files call it, no spawner timeout, no exit handling.The instrument was reverse-checked in both directions on the comment-only file: mutating a comment token leaves the hash at
dd50ec0897b3cde7(correct — comments are invisible), mutating a code token moves it to1e1eaa96(correct — code is visible). Without both legs the equality would mean nothing.Verification
All of the below on the final commit,
732fe0cbd.serve-built-cli-prerequisite.test.ts(unbuilt tree)Test Files 1 passed (1)·Tests 7 passed (7)serve-built-cli-prerequisite.test.ts(builtdist/, the branch CI takes)Test Files 1 passed (1)·Tests 7 passed (7)pnpm --filter @objectstack/cli buildEXIT=0pnpm lint(repo-wide,eslint . --no-inline-config)VERDICT command-exit 0 · held the lock 93s— full farm, no narrowing claimedpnpm check:cli-test-child-env✓ … 36 spawner source(s) among 100 under packages/cli/test/**…all 4 spawn(s) of the built CLI keep their child out of development/testpnpm check:nul-bytescheck-nul-bytes: OK (scanned 6987 text file(s) … no raw ASCII control bytes)node scripts/check-comment-mask-adoption.mjsOK check:comment-mask-adoption — 20 private comment-stripper(s) … all 20 recordedpnpm check:type-check-coverage✓ … 47 semantic case(s) + 65 observation case(s) … hold.pnpm check:test-source-aliascheck-test-source-alias OK — 72 packages with tests scannedpnpm check:cross-package-test-inputsOK: 20 package(s) read outside themselves, all declaredvitest list— 4 tests enumerated, exit 0Families derived by
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, which reported the answer as derived fromobjectstack-ai/objectstackat732fe0cbd. The 9packages/specfamilies it lists under "THE LAYOUT MOVED" are #12514 and are not refiled here.NOT MEASURED, stated rather than implied
pnpm --filter @objectstack/cli typecheckexits 0, and that green says nothing about these three files.packages/cli/tsconfig.jsondeclaresinclude: ["src"], sopackages/cli/test/**sits outside the tsc program entirely:tsc --noEmit --listFileslists 1279 files, 205 underpackages/cli/src/and 0 underpackages/cli/test/. Reverse-checked — thesrc/count is the positive control proving the grep works. This is a ledgered posture, not a regression (check:type-check-coverageis green and records 18 packages that hide their own tests), and this change does not move it. Reported as unmeasured rather than as a pass.The four
bin/run.jse2e suites were not run — a declared narrowing. The constant they consume is read only byrequireBuiltCli()inbeforeAll, which returns silently on a built tree, so the string is never reached on the path CI takes; the transpile proof above shows no code moved in any of them. Both edited e2e files were confirmed to parse and collect. CI runs the farm regardless.No changeset
packages/clipublishesfiles: ["dist", "README.md", "CHANGELOG.md"]. This diff is confined topackages/cli/test/**, which is outside that set, so the change releases nothing and there is no user-visible behaviour to describe.skip-changesetapplies. Reasoned from the publish surface, not carried over from another card.Scope
Three files, prose only, one commit.
packages/cli/src/commands/serve.ts(PR #12636) and.gitignore(PR #12631) are fenced and untouched — verified against the change set, 0 hits.Generated by Claude Code