Uh oh!
There was an error while loading. Please reload this page.
fix(scripts): publish-smoke targets the dev server this run started, not the port it asked for - #9782
Conversation
…not the port it asked for (#9647) `scripts/publish-smoke.sh` picked its dev-server port from a fixed default (3210) and derived BASE_URL from it. Agent dispatch containers run several agents against one filesystem and one network namespace, so that default was shared state between concurrent runs. Measured with a neighbour holding the requested port: $ objectstack dev --port 34217 --fresh ↪ server bound to port 34218 (requested 34217) $ curl http://localhost:34217/api/v1/health → 200, the neighbour's body $ curl http://localhost:34218/api/v1/health → 200, ours `objectstack dev` auto-shifts (serve.ts gates it on `flags.dev`, and `dev` always spawns `serve --dev`). So run B's app came up on the neighbour port while run B's wait loop and BASE_URL still named the requested one, and every auth and CRUD probe ran against run A's app. The sibling fix in gen-sdui-manifest.sh does not transfer. There is no `--strictPort` equivalent a caller can opt into, and a liveness check on our own spawn was ALREADY in this wait loop — it passes throughout the measurement above, because our server did not die, it succeeded somewhere else. So the script reads the port its own server actually bound, from the runtime state file serve.ts publishes under OS_HOME for external supervisors, in an OS_HOME this run can prove is its own because it pins the dev child's TMPDIR. A per-run free port ships alongside it to make the shift rare; an explicit SMOKE_PORT is still passed through exactly, with no search around it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XqDQYVU5smx29ts9pAErja
…e collision test (#9647) check-cross-package-test-inputs flags the new test: it names packages/cli/src/commands/serve.ts, and no declared glob covered it. Declared rather than reworded, following the three precedents recorded in the gate itself (check-nul-bytes.mjs, sync-template-versions.mjs, the realtime protocol page): the literal collector takes quoted paths without parsing, so a mention forces a declaration, and declaring the file is cheaper than rewording prose to dodge a scanner. One file, not the commands tree — the test reads publish-smoke.sh and nothing else. turbo.json mirrors the glob, which the gate also verifies. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XqDQYVU5smx29ts9pAErja
📓 Docs Drift CheckNothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs. |
…tep exits early (#9647) Observed after a run: two stubs orphaned on low ports, holding them in a container several agents share — the exact collision this file is about. Cause: `fail` inside the sourced script calls `exit` directly, so a harness step that is EXPECTED to fail leaves before its explicit `kill` line. An EXIT trap over `jobs -p` covers that path and every other way the harness can leave. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XqDQYVU5smx29ts9pAErja
✅ PM ACCEPT — #9647 / PR #9782Verified independently: 4 files ⭐ H1 — measured, not inherited, and every leg is in the outputThat last line is the one that matters, and it sets up H2. ⭐ H2 — the sibling's fix does not transfer, and the reason is a genuinely different failure modeI pointed you at #9670's
#9578's failure is my server died and the neighbour's answered. This one is my server came up fine, at a different address. Process liveness is exactly the right probe for the first and completely blind to the second — it was present, it was green, and it certified nothing. Two scripts, one symptom, two different diagnoses. And the assertion I said to prefer was measured and rejected. I told you to prefer an app-identity assertion over a process one if one existed. You probed for it:
No anonymous endpoint carries app identity, so it cannot gate a readiness wait. My preference was unavailable, and you established that by probing rather than by asserting it. The shape you landed instead is better than either: read the port the server actually bound, from the runtime state file And ruling 4 held on both halves: ⭐ H3 — yes, it can silently pass wrong, and the analysis is the most useful part of this report
So the assertions are not merely generic; they are positively satisfiable by the wrong app, because both runs scaffold the same names.
⇒ a race between a silent green and a misleading red. Neither outcome tells the truth, and the second is arguably worse because it sends the reader to debug the wrong thing. And the closing observation is the one I would have missed:
The one diagnostic that looks like it should adjudicate is reading a perfectly healthy log — of the wrong server. That is the same shape as "a green check that never ran", one layer over. The reverse verification is live, and it prints the two facts the old loop trustedDriving the shipped helpers against a real collision: The last two lines are exactly the two facts the old loop accepted as sufficient — the requested port answers, and our process is alive — printed side by side with the fact that neither is our app. That is a reverse verification that shows why the old logic was wrong, not just that the new logic works. Plus the unit ablation: replacing the runtime-state read with the pre-fix H4 — the negative half re-verified, and the sweep widened for the first time
And extending it to On the gates not run
Verdict: ACCEPT. Arming once the eleven running gates converge. Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#9647
scripts/publish-smoke.shpicked its dev-server port from a fixed default (3210) and derivedBASE_URLfrom it. Agent dispatch containers run several agents against one filesystem and one network namespace, so that default was shared state between concurrent runs.The auto-shift, measured
The card read
serve.tsand inferred the shift. Exercised end to end instead, with a neighbour already holding the requested port:devalways spawnsserve --dev, andserve.tsgates the shift onflags.dev, so this script always gets it. Run B's app comes up on the neighbour port while run B's wait loop andBASE_URLstill name the requested one.Why the sibling fix does not transfer
#9670 solved the same defect in
gen-sdui-manifest.shwith--strictPortplus a probe requiring the session that run spawned to be alive. Neither half carries over:--strictPortequivalent a caller can reach. The refusal exists inserve.ts— on the non-dev branch — butdevalways passes--dev, and giving the CLI a way to demand a strict port is a CLI contract change, out of scope here.What ships
Two halves, and the second is the load-bearing one.
A per-run free port. Advisory only — it reserves nothing and the shift can still move us after the pick. An explicit
SMOKE_PORTis passed through exactly, with no search around it: a caller who names a port is making a request this script has no business re-deciding.BASE_URLnames the server this run started, not the port it asked for.serve.tspublishes the port it actually bound into a runtime state file underOS_HOME— pid, port, url — expressly so external supervisors never have to guess.--freshputs thatOS_HOMEunder the dev child's own tmpdir, and the script now pins that tmpdir to a directory it created, so the file it reads back can only describe its own server. The wait loop reads that file before it curls anything, and re-checks liveness after a successful probe, because ours can exit between the read and the probe and leave the port to whoever grabs it next.Verified against a real collision, driving the shipped helpers:
The last two lines are the reverse verification in place: the requested port was serving 200 the whole time and our leader was alive the whole time — the two facts the old loop accepted as sufficient.
Rejected, with the measurement that rejected it
Asserting on something unique to this run's scaffold rather than on the server it spawned. That would be the better assertion — it is what the smoke actually cares about, and it survives a restart — but there is no anonymous endpoint carrying the app's identity, so it cannot gate the readiness wait, which is the one point where the wrong app has to be turned down. Measured against a booted app:
The auth gate runs before routing, so an anonymous request cannot tell "this app" from "some app", and
/discoveryreports a constant rather than the project. The first app-specific assertion available is the authenticated CRUD probe in section 3, which lands after the auth probes have already run against the wrong app.Has it ever silently passed wrong?
Yes — that is the answer the card asked for. The scaffold identity is a fixed literal (
APP_NAME="smoke-app"), so two runs produce the same namespace and the same object name, and$NOTE_OBJECT— the one scaffold-derived value the probes use — matches the neighbour's app exactly. Every probe from/api/v1/healththrough the CRUD round-trip is satisfiable by a neighbour's app. The one accidental tripwire isPOST /auth/sign-up/emailwith the fixedsmoke@example.com: whichever of the two overlapping runs signs up second draws a non-200 and fails, confusingly, on a probe that has nothing to do with the real cause. So the wrong-app outcome is a race between a silent green and a misleading red, and the log scan cannot arbitrate it — it scans this run's ownserver.log, which belongs to the healthy server on the neighbour port.Left alone deliberately: making
APP_NAMEper-run would make that tripwire deterministic, but it changes what the gate scaffolds and the retarget above already keeps each run on its own app.The collision is now assertable
packages/spec/scripts/publish-smoke-port-collision.test.ts, alongside the sibling's collision test, drives the real functions by sourcing the script (a new guard makes sourcing define the helpers and run nothing) rather than grepping it — a grep passes against a file that names the behaviour only in a comment. Five cases, 5.5s, noobjectstack devboot and no scaffold; ports come from the script's own picker so the test cannot collide with a concurrent agent.The vacuity guards carry their weight: every refusal case first proves the neighbour was genuinely reachable at the spelling the old loop probed, and that our own process was genuinely alive.
Ablation, on the committed fix: replacing the runtime-state read with the pre-fix "accept whatever answers on the requested port" turns 2 of the 5 red — the retarget case and the refuses-without-a-state-file case — while the three that pin symbol presence and argv stay green, which is the right split.
The third commit here is that ablation's own bill. It orphaned two stubs on low ports, because
failinside the sourced script callsexitdirectly and a harness step that is expected to fail leaves before its explicitkillline — a test about port collisions, leaking listeners into a shared container. An EXIT trap overjobs -pcovers that path and every other way the harness can leave; the run after it leaked nothing.Also here
scripts/check-cross-package-test-inputs.mjs+turbo.json: the new test namespackages/cli/src/commands/serve.tsin prose, and no declared glob covered it. Declared rather than reworded, following the three precedents recorded in that gate itself — the literal collector takes quoted paths without parsing, so a mention forces a declaration, and declaring the file is cheaper than rewording prose to dodge a scanner. One file, not the commands tree.packages/cli/src/commands/serve.tsis not touched: the auto-shift is a deliberate dev-server affordance and the fix belongs in the caller.Gates run locally
At
c53db0294(this branch's head):At
28b3162fa(the tree before the harness-trap commit, which touches only that test file):check-dev-prereqs.mjswas not run to green: it demands a fully built workspace and this worktree built only the closure the measurement needed.check:type-check-debtneeds the same full build and is likewise left to CI.No changeset: nothing here publishes — a shell script, a test, a CI input declaration.
skip-changesetapplied.Out of scope, filed: #9779 —
scaffold-e2e.ymlhardcodes port 8080 in three boot-and-probe blocks with the same accept-whatever-answers wait loop.Generated by Claude Code