Commit 0368a33
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
0 commit comments