Repository navigation
test(create-objectstack): boot the scaffold-e2e probe blocks on a HELD port, and show what a block said when it fails - #20526
Merged
objectstack-fleet[bot] merged 2 commits intoSep 28, 2026
Conversation
…D port, and show what a block said when it fails The boot-and-probe suite rewrote each block's port to one an advisory probe had just found free (`pickFreePort(38700)` / `(38900)`), let go of it, and the `os start` stub then waited out its 1.2s boot delay before binding. That port sits inside `ip_local_port_range`, where the kernel also hands out source ports for outbound connections, so under a loaded shard the stub's bind could lose the number and the block exited 1 on its liveness check about two seconds in. The assertion printed nothing the block had said, so its exit path could only be inferred. The port now comes from the kernel (`listen(0)`) in a holder child that keeps it bound with SO_REUSEPORT. The `os start` and container stubs bind the same port with the same flag and end the holder once their own listener is up, so the port is bound at every instant from the kernel's answer to our server's bind: no outbound connect() or other listen(0) can be given it, and any other binder is refused. The holder resets connections, so the block's pre-flight still reads it as nothing serving, and it proves the port is joinable before it announces it, so a flag the platform ignored fails there, by name. Every assertion on a block's exit status or duration now carries the block's own output as its failure message. Pins: the two `os start` blocks run on the handed-over port (the stub names the port it bound), and a deliberate occupier aimed at that port is refused. Claude-Session: https://claude.ai/code/session_local_1d2a197c-c20e-4e90-9be8-413d4d432289 Co-authored-by: Claude <noreply@anthropic.com>
…affold-probe-port
Contributor
📓 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. What this run could not see
Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
Contributor
Author
|
objectstack-fleet
Bot
deleted the
claude/issue-20516-scaffold-probe-port
branch
September 28, 2026 23:56
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #20516
Clause-②: no
What this changes
One file,
packages/create-objectstack/src/scaffold-e2e-boot-probe.test.ts, and it is a tests-only diff (see Publish surface).said(r)as its failure message. That message holds the block's::error::line, the server log it dumps, its exit code and its duration. The merge-queue failure printed only a bareexpected 1 to be +0; that message now names its exit path.pickFreePortis gone. It was an advisory "free right now" probe that let go of the port, which sat insideip_local_port_range.portHolder()spawns a child that asks the kernel for a port (listen(0)) withSO_REUSEPORTand keeps it bound. Theos startstub and the container stub bind the same port with the same flag, so they join the holder's binding instead of racing for the number. Each ends the holder once its own listener is up. The port is bound at every instant between the two. No outboundconnect()and no otherlisten(0)can be given it, and any binder withoutSO_REUSEPORTis refused withEADDRINUSE.curl -fsStherefore still reads "nothing is serving". A holder that accepted without answering would hang that curl, which has no--max-time.reusePortsocket to the port and lets go. A platform that ignored the flag fails there, with a message namingreusePort. Without that check it would fail later in the block asPort N is already in use, which is also the one line a lost port race prints. A new control pins this diagnosis.os startblocks (scaffold-localandregistry-canary) share oneitbody per case. The "accepts the server it booted itself" case now does four things:EADDRINUSE;"port":N).The Docker leg's two callers of the same helper (base port 38900) are converted in the same edit. They have the same defect class, the same file, the same mechanical shape and the same gate family. Removing the advisory helper left them no other port source.
Dispatch hypotheses, measured
H0: no existing helper hands a held port to a child.
packages/cli/test/helpers/serve-process.ts:reservePort()binds port 0 and then closes it (its own docblock calls it "still TOCTOU").holdPort()holds a port but never hands it over.sdui_pick_free_portis a TOCTOU probe — it closes the probe socket before returning, so two concurrent callers scanning from 5180 are both handed 5180 (dequeued PR #10157 from the merge queue) #10167 andsmoke_pick_free_portinscripts/publish-smoke.shis the same TOCTOU probe as #10167 — concurrent callers scanning from 3210 are all handed 3210 #10212 landed a host-shared claim registry for shell scripts, built onflock.portHolder()is new. It reuses this file's ownneighbour()instrument (announce protocol and diagnoses) through two new options instead of adding a second spawner.H1: confirmed. The merge-queue failure's exact shape was reproduced on this host. A listener that serves nothing held the stub's port, and the block exited 1 on its liveness check after 2.0s. With the old assertion form the output was
AssertionError: expected 1 to be +0and nothing else. With the new form, the same kind of run printed:H2: false as literally stated, so the pin is aimed where it can fail. Measured on this host: with an occupier bound on 38700 for the whole run, the OLD advisory source handed out 38701 and the old test passed. A bind probe skips a port that is held the whole time. The old helper could only lose to something that arrived between its probe and the stub's bind. The occupier pin is therefore aimed at the port the source handed over, after the source answers.
this control is vacuous: the occupier was supposed to fail and it came up on port 38702.H3: confirmed. Both
os startblocks share one set ofitbodies (theOS_START_STEPSloop), so both run on the held port.Verification
This dispatch ran on macOS (darwin). The suite is gated on
process.platform === 'linux'(RUNNABLE), and on macOS libuv answersENOTSUPtoreusePort.NOT MEASURED locally:
Reason: this dispatch has no Linux host. CI's Test Core (ubuntu-latest) runs the file on this PR. On this host the file reports 14 skipped cases; the base has 13 by count, and the extra one is the new control.
Measured on this host, from an uncommitted scratch copy with the platform gate lifted. The copy was deleted afterwards and the tree checked clean.
portHolder()refusing loudly wherereusePortis unsupported:the port holder never came up on a kernel-assigned port ...: its listener refused to bind: ENOTSUP.At HEAD
0d76b886f4, after mergingorigin/mainat397572ed5d:pnpm --filter create-objectstack typecheck: exit 0.tsc --listFilesincludes the test file.pnpm --filter create-objectstack exec vitest run --maxWorkers=2: 15 files passed, 1 skipped; 219 tests passed, 14 skipped.pnpm lint: exit 0 in 29s. The changed file is linted, not ignored (eslint JSON: 1 file, 0 errors, 0 warnings).node scripts/check-issue-citations.mjs --base origin/main: exit 0, no issue citations added.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsderived 52 families. Reconciled with--ran, exit codes recorded:pnpm check:dual-build-cjs-loads,pnpm check:lean-entry-closureandpnpm check:type-check-debt. Each exited 3 with PREREQUISITE NOT MET, because each reads every workspace package's builtdist/and this worktree built only this package's closure. None of them can move on a diff to one test file that appears in no build output and in no other package's type program.Declared narrowing — verification ran UNLOCKED.
scripts/pm/os-verify-lock.shcould not take the shared verify lock on this host: no usable
flock. The sharedverify lock is declared Linux-only (
flockis util-linux, and a stock macOS doesnot ship it), so the command below was run directly, without the lock —
a declared narrowing, not a silent one. No serialization guarantee held for this
run, nor for any sibling agent in this container while it ran.
Publish surface
Tests-only, so there is no changeset. I built
create-objectstackand grepped itsfiles(dist,README.md,CHANGELOG.md):PORT_HOLDER_STUB,portHolder,STUB_PORT_HOLDER_PIDandreusePort: 0 hits each.summarizeTree(an export ofsrc/created-summary.ts): found indist/.The
skip-changesetlabel is the seat's to apply.Acceptance notes
packages/cli/test/helpers/serve-process.ts. There the process that binds is the realos serve, which binds withoutSO_REUSEPORT. A holder could only let go before that bind, which is the reserve-then-release race that helper's docblock already declares. Noted, no card (carrier: none).SO_REUSEPORT, any same-user socket that also sets the flag can join the held port. Nothing else in this repository setsreusePort:git grepfinds no hits outside this file. The kernel also never gives anotherlisten(0)a port that has a live listener, so concurrent runs of this file get distinct ports.toContainassertions already print the received output when they fail.Generated by Claude Code