Skip to content

Commit 0368a33

Browse files
objectstack-fleet[bot]hotlongclaude
authored
test(create-objectstack): boot the scaffold-e2e probe blocks on a HELD port, and show what a block said when it fails (#20526)
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 - **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. - #10167 and #10212 landed a host-shared claim registry for shell scripts, built on `flock`. - #15273 landed a change to the asserted quantity. - So `portHolder()` is new. It reuses this file's own `neighbour()` 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 +0` and nothing else. With the new form, the same kind of run printed: ```text AssertionError: the block exited 1 after 2.025s — its own output: --- block output --- ::error::the server this step started exited before becoming healthy Port 52750 is already in use. ObjectStack does not auto-select a different port in production mode: expected 1 to be +0 ``` - **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. - Ablation against the old advisory source, measured on this host: red, with `this control is vacuous: the occupier was supposed to fail and it came up on port 38702`. - The same pin against the holder only runs on Linux (see *Verification*). - **H3: confirmed.** Both `os start` blocks share one set of `it` bodies (the `OS_START_STEPS` loop), 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 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](https://claude.ai/code/session_local_1d2a197c-c20e-4e90-9be8-413d4d432289)_ Co-authored-by: Jack Zhuang <50353452+hotlong@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
1 parent b43a814 commit 0368a33

1 file changed

Lines changed: 220 additions & 69 deletions

File tree

0 commit comments

Comments
 (0)