Uh oh!
There was an error while loading. Please reload this page.
test(cli): read back the port the e2e serve child actually bound, and fail loudly on a silent drift - #12546
Conversation
📓 Docs Drift Check10 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 2548dd583c5c0620bfc4bcd34c88779204f44a0c && git checkout 2548dd583c5c0620bfc4bcd34c88779204f44a0c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c0f5e8f21539f5299e3c5dec0e4039ed0387989c c4474afa315dc2026836c861978e9cf2fe28827a && git checkout -B drift-repro c0f5e8f21539f5299e3c5dec0e4039ed0387989c && git merge --no-ff c4474afa315dc2026836c861978e9cf2fe28827a
node scripts/docs-audit/affected-docs.mjs --json c0f5e8f21539f5299e3c5dec0e4039ed0387989c |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#12525
runServe()now reads the child's real bound port back out of its ready banner and rejects when it is not the port the harness asked for.The false green, produced on demand
bin/run-dev.js:28pinsNODE_ENV=developmentbefore argv is parsed, soserve.ts:1318'sportAutoShiftAllowedis true for everyrunServe()child: a taken port is not an error,getAvailablePort()walks to the next free one, and the boot succeeds. The harness never learned the child's real port — it was an opaque element of the argv it built — so a test that afterwards talks to the port it asked for reaches whatever else is holding it.Measured on this branch, twice, with the requested port really held:
runServe()✓ Server is ready,➜ API: http://localhost:43539/PORT DRIFT …The ablated row is the defect, reproduced: a healthy banner, every assertion on it green, for a server nobody in the test knew the address of.
What the rejection says
⛔
os serve's auto-shift rule is unchanged. Auto-shifting in development is correct and deliberate; the defect was that the harness could not tell the difference. No file underpackages/cli/srcis touched.#12525's body — and the first draft of this helper's docblock — put "
bin/run.jswithNODE_ENVunset" in the loud column. That is measurably wrong, and the corrected table now lives inboundPortFromBanner's docblock:serve.ts:1318isflags.dev || process.env.NODE_ENV === 'development'— either half alone opens the auto-shift branch. A spawner is loud only when both are false.--dev,NODE_ENVnotdevelopmentexit 1— loud,portContentionError()names it--dev, whateverNODE_ENVbin/run-dev.js(pinsNODE_ENVat:28)Re-derived locally:
serve-mcp-stdio-answers,serve-mcp-capability-collisionandserve-stdio-stdout-purityall spawnserve -p PORT **--dev**(:167,:172,:159), so thebin/run.jsentry buys them nothing — they are in the silent column by the--devroute, and are handled by #12526 (⛔ untouched here).serve-node-env-production-default.e2e.test.tsis the only file in this directory that is actually loud.packages/cli/test/serve-process-child-env.e2e.test.tsis not arunServe()caller, so this change does not reach it — but it is the same defect with live consequences::160spawn(TSX, [CLI, 'serve', 'objectstack.config.ts', '--port', port]), whereCLIis the helper'sbin/run-dev.js⇒ auto-shift branch open.:189fetcheshttp://localhost:${port}/api/v1/auth/sign-in/email⇒ it talks to the port it asked for, never the one the child bound.INVALID_ORIGINvs 401), i.e. exactly the "assertion that can never go red for the reason it exists" class thatchildEnv()'s own docblock warns about. On a lost race the answer can come from a neighbouring agent's server.packages/cli/test/serve-app-anchored-optional-import.e2e.test.ts:176has the same blind spawn but only reads the child's output, so its exposure is lower. Both are reported for triage rather than folded in.Surface
packages/cli/test/helpers/serve-process.ts(the read-back), plus one declared amendment:hold()is hoisted out ofserve-port-bind-probe.test.tsinto the helper asholdPort(), because a second file needs it and this directory has already paid once for duplicating a port instrument (the three blind drawsreservePort()replaced). No behaviour change there — the same 9 tests pass.Verification — all on
c4474afa3, closure builtvitest runthe new file +serve-port-bind-probevitest runall fiverunServe()callerspnpm lint(repo-wide,eslint . --no-inline-config)check:nul-bytes·cli-test-child-env·engine-double-contract·where-matcher·query-options-erasure·type-check-coverage·cross-package-test-inputs·test-source-aliasc4474afa3check:type-check-debtratchets)@objectstack/cli, same code composition (TS7006×60, TS2835×57, TS2339×24, TS2307×3, TS18046×2), 0 of them naming any file in this diffThe last row is a declared narrowing:
--re-measurere-runs all 34 ledger entries and needs the whole workspace built, so the one entry this diff can move was reconstructed instead — and reproducing the recorded number exactly is what makes it a measurement rather than a guess. CI runs the full farm.The ablation restored under a
trapand proved it: on-disk blob back to936ea7c5…= theHEADblob, injected marker count 0,git diff HEADempty.Test-only (
packages/cli/package.jsonpublishesdist,README.md,CHANGELOG.md— no test file ships), soskip-changeset.Generated by Claude Code