You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit c27e160
Browse filesBrowse the repository at this point in the historyBrowse files
fix(create-objectstack): the boot-probe neighbour reports WHY it did not come up (#19429)
Fixes#19424
Clause-②: no
## The reading this starts from
The boot-probe harness spawned its neighbour with `stdio: 'ignore'` and
waited on 80 blind `curl` probes 0.25s apart — exactly 20s — through
`execFileSync('bash', …, { stdio: 'ignore' })`, then threw naming only
the port. Both ends discarded the evidence by construction, so the
card's two same-run readings (`20999ms` red and `299ms` green, same
titled assertion, same runner, seconds apart) were indistinguishable to
a reader. A budget chosen against an unreadable failure is a guess, so
this PR chooses none.
⛔ **What this PR does not claim.** Not that the CI child died, not that
the runner was loaded, not that 20s is too short. No frequency was
measured and none is inherited; the job behind the card could not be
re-run.
## What WAS measured (this repo, this container, `576d5df660`)
1. **The pre-fix helper answers three different causes with one
sentence.** Driven against a healthy child, a child that exits at once
with code 3, and a child that stays alive and never listens: **20761ms /
20751ms / 20778ms**, every one of them `the neighbour never came up on
port N` and nothing else. Second run, same three: 20930ms / 20771ms /
20976ms.
2. **The healthy child failed too, and the cause was invisible to every
check the harness could run.** `node` refused the bind — `listen
EADDRINUSE: address already in use 0.0.0.0:39510` — on a port
`pickFreePort` had just approved and on which `ss -ltn` showed **no
listener at all**. `lsof` found it: `TCP 192.0.2.2:39510 ->
160.79.104.10:443 (ESTABLISHED)`. The port was an unrelated process's
local **ephemeral source port**. `pickFreePort` proves a port is
bindable at the instant it asks; its own docblock already says "advisory
only".
3. **`process.kill(pid, 0)` is NOT a liveness check in this harness**,
so the obvious repair is a trap. With the worker's event loop blocked
inside `execFileSync` the exited child is an unreaped **zombie**, and
`kill(pid, 0)` on a zombie succeeds: it answered "alive" about a child
that had died 20 seconds earlier.
⚠️ Reading 2 is a mechanism reproduced **here**, ⛔ not a diagnosis of CI
run 35529522740. What it establishes is narrower and enough: a port can
be refused *after* passing `pickFreePort`, and the old instrument
reports that case in exactly the words it uses for a slow runner.
## What changed
`packages/create-objectstack/src/scaffold-e2e-boot-probe.test.ts` only.
⛔ Nothing the suite pins (#9779) is weakened, skipped or quarantined —
the eight existing assertions are untouched and still green.
- **The child announces its own listener.** `NEIGHBOUR-LISTENING port`
from the `listen` callback, or `NEIGHBOUR-LISTEN-ERROR errno` from an
`error` handler — written with `process.stdout.write(…, cb)` and exiting
from the callback, because stdout is a pipe here and `process.exit()` on
the next line truncates an async pipe write.
- **The parent awaits that line**, with the `exit` event, the `spawn`
error and the child's captured stdout/stderr wired to the same promise.
Five outcomes now report themselves the moment they happen and name
themselves: never spawned · exited before announcing (code, signal, and
the child's own last words) · refused the bind (with the errno) · alive
and never announced · announced and then went mute.
- **The child asks the KERNEL for the port** (`listen(0)`) and reports
back what it got, so the window `pickFreePort` leaves open is closed
rather than merely reported on: the neighbour holds the binding
continuously from before its caller learns the number.
- ⛔ **No budget was raised.** The remaining ceiling is a backstop for
the one outcome none of the others covers — announced, alive, still not
answering — never the thing that decides the other four. That is what
makes this a different instrument rather than a bigger number.
## The control that can FAIL
⛔ A green suite is no evidence that a diagnostic works. Five new cases
break the neighbour four different ways and pin that the harness names
THAT way, beside the healthy path:
| deliberately broken child | verdict pinned | observed |
|---|---|---|
| a second child aimed at a port the first one holds | `its listener
refused to bind: EADDRINUSE` | 108ms |
| a child that exits at once | `it exited before announcing a listener
(code 3` plus its own stderr | 40ms |
| a child that stays alive and never listens | `it stayed alive and
never announced a listener`, and ⛔ NOT the exit wording | 2006ms (its
own 2s backstop) |
| an executable that does not exist | `it never spawned` plus `ENOENT` |
3ms |
| **the healthy path** | reports a port that answers at the exact
spelling the block probes | 69ms |
The same three broken children against the pre-fix helper: 20761ms /
20751ms / 20778ms, one undifferentiated sentence.
**Ablation — one-time, restored, not left in the tree.** Collapsing the
headline out of the diagnosis (anchor `: ${headline}\n`, replaced by an
inert marker) turned **exactly** the four broken-child controls red and
left the healthy control and all eight pre-existing assertions green:
`Tests 4 failed | 9 passed (13)`. Driven through
`scripts/ablation-replace.mjs`, which proved the mutation reached disk
(`anchor 1 -> 0`, blob `9129d5f471c2 -> e0cd4e4d17af`) and proved the
restore (`blob after restore 9129d5f` = `blob at HEAD`, `git diff
HEAD` empty). The merge commit that follows does not touch the ablated
file.
## Readings, all on `0dd94da744` (the final commit)
- `pnpm --filter create-objectstack test` — **16 files, 208 tests, all
pass** (13 in this file).
- `pnpm --filter create-objectstack typecheck` — **exit 0**, and the
test file is genuinely in that program: `tsc --noEmit --listFiles` names
it (1 hit), so the green is not an `exclude` speaking.
- `pnpm lint` (`eslint . --no-inline-config`, the whole repo — a full
run, ⛔ not a narrowing) — **exit 0**.
- Derived gate families (`node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack`) — **51 derived · 48 exit 0 · 3 exit
3**. The three are `check:dual-build-cjs-loads`,
`check:lean-entry-closure` and `check:type-check-debt`, each printing
`PREREQUISITE NOT MET` because it reads a full closure build this
worktree does not carry. ⛔ Those three are **NOT MEASURED** here —
neither pass nor finding; CI builds the closure and owns them. `--ran`
reconciliation, exit codes recorded: `51 derived, 48 run, 3
NOT-MEASURED, 0 UNRUN`.
- `pnpm check:cross-package-test-inputs` — **exit 0**. The diff adds no
new escaping read, so `scripts/cross-package-test-inputs.mjs` and
`turbo.json` were left alone: the gate was asked, ⛔ not pre-empted.
## Changeset
`skip-changeset`, measured rather than assumed. After `pnpm --filter
create-objectstack build`, the identifiers this PR adds
(`scaffold-e2e-boot-probe`, `NEIGHBOUR-LISTENING`, `NeighbourOptions`)
occur **0** times across every artefact `files[]` ships —
`dist/index.js`, `dist/created-summary.js`, `dist/chunk-ZIUW7UEA.js`,
`dist/created-summary.d.ts` — while the positive control
`summarizeTree`/`formatBytes` occurs **14** times across those same
four. ⇒ nothing published moves.
⚠️ The label itself was **not written**: the dispatch order forbids
label writes it does not name, and it names none. It still needs
applying by the owning seat.
## Acceptance notes
- `pickFreePort` is still used by the other four cases in this file and
the same staleness applies to them in principle. **Noted, not filed:**
there the port is bound by the `os start` stub *inside* the block, which
already prints `Port N is already in use.` and exits 1, so that path
fails legibly today. Carrier: this PR's reviewer; no other PR is queued
on this file.
- The card's retracted note about a fixed port 38700 bind conflict
**stays retracted** — `:147 pickFreePort(base)` scans 400 candidates and
38700/38900 are bases. The EADDRINUSE above is a different mechanism (a
port that passed the scan and was refused afterwards), ⛔ not that claim
revived.
- The neighbour wait was the only clock touched. The workflow scripts
under test, their `curl` spellings and their own timeouts are unchanged.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01QCdUBjM47SxioST9z5Zwdf)_
Co-authored-by: Claude <noreply@anthropic.com>
0 commit comments