Skip to content

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 into
mainfrom
claude/issue-20516-scaffold-probe-port
Sep 28, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-20516-scaffold-probe-port

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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).

  1. Every verdict on a block's run now carries the block's own output. Each assertion on a block's exit status or duration passes 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 bare expected 1 to be +0; that message now names its exit path.
  2. The port is HELD from the kernel's answer until our server binds it. pickFreePort is gone. It was an advisory "free right now" probe that let go of the port, which sat inside ip_local_port_range. portHolder() spawns a child that asks the kernel for a port (listen(0)) with SO_REUSEPORT and keeps it bound. The os start stub 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 outbound connect() and no other listen(0) can be given it, and any binder without SO_REUSEPORT is refused with EADDRINUSE.
    • The holder accepts and resets every connection. The block's pre-flight curl -fsS therefore still reads "nothing is serving". A holder that accepted without answering would hang that curl, which has no --max-time.
    • Before it announces its port, the holder binds a second reusePort socket to the port and lets go. A platform that ignored the flag fails there, with a message naming reusePort. Without that check it would fail later in the block as Port N is already in use, which is also the one line a lost port race prints. A new control pins this diagnosis.
  3. Pins (triage grade 5879541220). Both os start blocks (scaffold-local and registry-canary) share one it body per case. The "accepts the server it booted itself" case now does four things:
    • asserts the holder serves nothing;
    • aims a deliberate occupier at the handed-over port and asserts it is refused with EADDRINUSE;
    • runs the block and asserts it passed;
    • asserts the block ran on that port: the stub's answer names the port it bound ("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

Verification

This dispatch ran on macOS (darwin). The suite is gated on process.platform === 'linux' (RUNNABLE), and on macOS libuv answers ENOTSUP to reusePort.

  • NOT MEASURED locally:

    • the suite's green path on the new port source;
    • the holder-side half of the occupier ablation.

    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.

    • H1, in both assertion forms.
    • H2 as literally stated.
    • The occupier ablation against the old advisory source.
    • portHolder() refusing loudly where reusePort is unsupported: the port holder never came up on a kernel-assigned port ...: its listener refused to bind: ENOTSUP.
    • Every child program text was also syntax-checked the way the child runs it.

At HEAD 0d76b886f4, after merging origin/main at 397572ed5d:

  • pnpm --filter create-objectstack typecheck: exit 0. tsc --listFiles includes 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 --commands derived 52 families. Reconciled with --ran, exit codes recorded:
    • 49 ran, all exit 0.
    • 3 are NOT MEASURED: pnpm check:dual-build-cjs-loads, pnpm check:lean-entry-closure and pnpm check:type-check-debt. Each exited 3 with PREREQUISITE NOT MET, because each reads every workspace package's built dist/ 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.sh
could not take the shared verify lock on this host: no usable flock. The shared
verify lock is declared Linux-only (flock is util-linux, and a stock macOS does
not 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.

pnpm --workspace-concurrency=2 --filter 'create-objectstack^...' build
pnpm --filter create-objectstack typecheck
pnpm --filter create-objectstack exec vitest run --maxWorkers=2
pnpm --filter create-objectstack exec vitest run --maxWorkers=2 -t SCRATCH src/zz-scratch-issue-20516.test.ts
pnpm --filter create-objectstack build
pnpm --filter create-objectstack typecheck && pnpm --filter create-objectstack exec vitest run --maxWorkers=2

Publish surface

Tests-only, so there is no changeset. I built create-objectstack and grepped its files (dist, README.md, CHANGELOG.md):

  • PORT_HOLDER_STUB, portHolder, STUB_PORT_HOLDER_PID and reusePort: 0 hits each.
  • Positive control, summarizeTree (an export of src/created-summary.ts): found in dist/.

The skip-changeset label is the seat's to apply.

Acceptance notes

  • The hand-over technique does not carry over to packages/cli/test/helpers/serve-process.ts. There the process that binds is the real os serve, which binds without SO_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).
  • With SO_REUSEPORT, any same-user socket that also sets the flag can join the held port. Nothing else in this repository sets reusePort: git grep finds no hits outside this file. The kernel also never gives another listen(0) a port that has a live listener, so concurrent runs of this file get distinct ports.
  • Only the status and duration assertions carry the block-output message. The toContain assertions already print the received output when they fail.

Generated by Claude Code

hotlong and others added 2 commits September 29, 2026 07:05
…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>
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing 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): node scripts/docs-audit/affected-docs.mjs --json 397572ed5da04c14eed7b4bf934a4c353822df6c → packageMentionDocs.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

skip-changeset: applied by the domain:cli seat on its own reading (tests only)

domain:cli execution PM seat #6024 · session local_1d2a197c-c20e-4e90-9be8-413d4d432289 · written 2026-09-28T23:23Z

  • The diff: one file, packages/create-objectstack/src/scaffold-e2e-boot-probe.test.ts (+220 −69), at head 0d76b886. No .changeset/*.md is added or edited.
  • What ships: create-objectstack's package.json files is dist, README.md and CHANGELOG.md. A src/*.test.ts file is not in the tarball, and the dev measured the new helpers (portHolder, PORT_HOLDER_STUB) with 0 hits in the shipped files, using a positive control that is found in dist/.
  • So: tests only, with no consumer-visible change. The repo's tests-only route is this label; ⛔ not an empty changeset. Check Changeset re-runs on the label.

@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 28, 2026
@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 28, 2026 23:39
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 28, 2026
Merged via the queue into main with commit 0368a33 Sep 28, 2026
41 of 42 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20516-scaffold-probe-port branch September 28, 2026 23:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

1 participant