[#240] Handle broker/platform/room bind failures as controlled command errors#245
Conversation
…d errors From the #238 audit: broker serve, platform serve, and room serve awaited `server.listen(port, host, resolve)` with no 'error' listener, so an occupied or invalid bind either hung the listen promise or crashed the process with an uncaught Server error. - Extract the smallest shared listen helper (`src/cli/commands/listen.ts`): `listenOrError` binds while guarding the one-shot 'error' event — resolving a failure outcome instead of hanging/throwing, and removing the guard on success so post-bind behavior is unchanged. It only observes its own listen attempt (never inspects/kills/mutates a foreign listener). `listenErrorMessage` renders a controlled, token-free message from the bind coordinates + OS error code only. - broker/platform/room serve now use it and, on a bind failure, write the token-free message to stderr and return non-zero (no hang, no uncaught error). room serve returns before mutating current-room state or warning. - launch (#232) reuses the same helper: its `listen()` delegates to `listenOrError(..., "127.0.0.1")` mapped to the prior boolean — mechanical reuse, behavior unchanged. Loopback-default/`--allow-remote` policy, SIGINT/SIGTERM lifecycle, ports, and remote-exposure policy are unchanged; no new dependencies; #241-#243 untouched. Tests (`test/cli-listen-errors.test.ts`): per surface (broker, platform serve, room serve) an occupied-port case (real EADDRINUSE) and a deterministic injected invalid-bind case (no OS hostname resolution) each assert a controlled non-zero, token-free error and that the foreign listener is left untouched; plus `listenOrError`/`listenErrorMessage` unit coverage. Existing #232 launcher and serve tests stay green. Full suite 439/439. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
project7-interns
left a comment
There was a problem hiding this comment.
Verdict: BLOCK
Epic Alignment: PASS
The diff stays within #240: controlled bind failures for broker/platform/room plus mechanical #232 helper reuse; #241-#243 are untouched.
Checked (evidence)
- Structural gate: live REST PR body contains filled EPIC Alignment, Self-Verification, and Deviations sections.
- Bind handling: src/cli/commands/listen.ts:13-27 resolves own-listen errors; broker/index.ts:34-40, platform/index.ts:47-53, and room/index.ts:932-938 return controlled non-zero errors.
- Launcher: launch/index.ts:184-187 maps the shared outcome to the existing boolean behavior.
- Tests: test/cli-listen-errors.test.ts covers occupied-port and injected invalid-bind paths for all three surfaces, token-free stderr, and untouched foreign listeners.
- Riskiest part: helper extraction; it removes its one-shot error listener on success and preserves launcher failure-as-false semantics.
- Kill-list: scanned all items — clean.
- CI: gh pr checks 245 → Release gates pending (live; checked twice).
Findings
- [blocking] Required CI has not completed.
- File: N/A
- Why it fails: mandatory live checks evidence is unavailable while Release gates is pending.
- Do instead: wait for the required check to complete successfully, then request re-review; no code change is identified.
Decision
The reviewed diff aligns with #240, but approval is blocked until live CI completes successfully.
|
RE2 — APPROVE (code/tests) — authoritative verdict in project chat. One CI caveat below. The #240 contract + Batch-38 amendments are fully and correctly met:
CI caveat (merge gate, not the code): |
project7-interns
left a comment
There was a problem hiding this comment.
Verdict: APPROVE
Epic Alignment: PASS
The diff stays within #240’s controlled bind-error scope; loopback/allow-remote policy, signal lifecycle, and #232 behavior are preserved.
Checked (evidence)
- Re-review range: no code delta; head remains 3c2c213 from the initial review.
- Bind handling: src/cli/commands/listen.ts:13-43 converts own-listen errors into bounded outcomes; broker/index.ts:34-40, platform/index.ts:47-53, and room/index.ts:932-938 emit controlled non-zero errors.
- Launcher: launch/index.ts:184-187 maps the helper outcome to the prior boolean semantics.
- Tests: test/cli-listen-errors.test.ts covers real occupied ports and deterministic injected invalid-bind errors for broker/platform/room, token-free stderr, and untouched foreign listeners.
- Riskiest part: shared helper extraction; its one-shot listener is removed on both paths and preserves launcher failure-as-false behavior.
- Kill-list: scanned all items — clean.
- CI: gh pr checks 245 → Release gates pass (live, run 29386362460/job 87261224684).
Findings
- None.
Decision
APPROVE. The #240 contract is implemented with deterministic per-surface coverage and verified green live CI.
Fixes #240
Implements the reviewed #240 CLI bind-failure contract (Batch 40, code batch). Baseline fresh
origin/main@1a98db6.EPIC Alignment
src/cli/commands/*+ one new helper + one test).platform serve/room serve; loopback-default + explicit--allow-remoteand SIGINT/SIGTERM lifecycle unchanged; error output token-free; never inspects/kills/mutates a foreign listener. Amendments honored: the contract covers both occupied-port and invalid-bind; the shared helper extracted from the launcher preserves Launcher UX: make bare agentgather open the local dashboard #232 behavior exactly (mechanical reuse).What changed
src/cli/commands/listen.ts(new, smallest shared helper):listenOrError(server, port, host)— binds while guarding the one-shot'error'event: an occupied/invalid bind resolves a failure outcome instead of hanging the listen promise or throwing an uncaughtServererror. The guard is removed on success, so post-bind behavior is identical to a bareserver.listen. It only observes its own listen attempt — never inspects/kills/mutates any other listener.listenErrorMessage(host, port, error)— a controlled, token-free message built from the bind coordinates + OS error code only (EADDRINUSE / EACCES / EADDRNOTAVAIL / fallback).broker serve,platform serve,room serve— replace the unguardedawait new Promise(res => server.listen(...))withlistenOrError; on failure writelistenErrorMessageto stderr and return non-zero (no hang, no uncaught error).room servereturns before mutating current-room state or emitting the--allow-remotewarning. Each command takes an optional injectablelistenhook (defaulting tolistenOrError) so the invalid-bind path is deterministically testable.launch(Launcher UX: make bare agentgather open the local dashboard #232) — itslisten()now delegates tolistenOrError(server, port, "127.0.0.1").then(o => o.ok), preserving the exact prior boolean behavior (localhost-only; bind failure → refuse with the existing token-free message, never a kill). Mechanical reuse, no functional change.Self-Verification
test/cli-listen-errors.test.tsoccupies a real localhost port and runs each serve on it → real EADDRINUSE → returns1(awaited, so no uncaught error / no hang).listenhook returning{ ok:false, error:{ code:"EADDRNOTAVAIL" } }→ returns1with a controlled token-free message; no hostname lookup involved.broker serve(SIGINT),platform serve(waitForShutdown), androom serve(SIGTERM) tests stay green.tgl_|token=|Bearer|Authorizationand that the occupying server is still listening after each failure.listenOrError/listenErrorMessageunit tests; the existingcli-launch(launch refuses an occupied/incompatible port…Launcher UX: make bare agentgather open the local dashboard #232) andbrowser-launchsuites pass unchanged.node:httptypes + internal module only); no port or remote-exposure policy change; loopback-default/--allow-remoteand signal lifecycle untouched; nolaunch/#232functional change; no#241–#243files touched.pnpm typecheck✓ ·pnpm lint✓ ·pnpm kit-guard✓ (4 panes clean) ·pnpm no-stub✓ ·pnpm build✓ ·node --test dist/test/**/*.js→ 439 pass / 0 fail.Deviations
listen.ts) rather than inlining per command — the ticket explicitly allows "reuse or extract the smallest shared listen helper," and this is the smallest surface that letslaunch/#232delegate with zero behavior change while the three serve commands share one proven pattern.listenhook added to each serve command (defaulting to the real helper) solely so the invalid-bind path is exercised deterministically without OS hostname resolution — matching the amendment's requirement. broker/room gain ahooksparam (default{}); the CLI dispatcher passes none, so runtime behavior is unchanged.