LearnNewsExamplesServices
Frontmatter
id15221
titleUnit-test chroma port is machine-global — concurrent agent runs deadlock on 18180
stateClosed
labels
bugaitestingregression
assigneesneo-opus-vega
createdAt10:08 AM
updatedAt12:35 PM
githubUrlhttps://github.com/neomjs/neo/issues/15221
authorneo-opus-grace
commentsCount4
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAt12:35 PM

Unit-test chroma port is machine-global — concurrent agent runs deadlock on 18180

Closed Backlog/active-chunk-6 bugaitestingregression
neo-opus-grace
neo-opus-grace commented on 10:08 AM

Context

Live incident, 2026-07-16 ~07:55–08:05Z on the shared agent machine: four agents' npm run test-unit invocations wedged simultaneously at 0% CPU (checkouts: claude, opus-vega, fable, plus a worktree agent). Also retro-explains an earlier same-day exit-143 stall a reviewer hit and documented as a TOOLING_GAP during a PR review. Incident broadcast with the workaround went out at 08:06Z.

The Problem

test/playwright/playwright.config.unit.mjs isolates the test chroma's data dir per process (neo-chroma-unit-test-${process.pid}) but binds its port to a machine-global fixed default (NEO_CHROMA_PORT_TEST || 18180) with reuseExistingServer: false. The isolation is half-done:

  • Two agents running unit suites concurrently contend for one port — the loser's webServer boot wedges.
  • Worse, the failure is sticky: a runner that dies without its graceful shutdown orphans its chroma (observed: PID 23930, PPID 1, 29+ minutes old, dead runner's tmpdir, LISTENING on 18180). Because reuseExistingServer: false forbids adoption, every subsequent unit run machine-wide wedges against the corpse — three runners sat ESTABLISHED against it, polling, at 0% CPU.
  • Cross-checkout process cleanup is (correctly) permission-gated for agents, so an agent cannot self-heal the machine — only the operator can reap the orphan.

Verified workaround (unblocked a run immediately): NEO_CHROMA_PORT_TEST=18190 npm run test-unit -- <spec> --workers=1.

The Architectural Reality

  • The config already models per-process isolation for the data dir — the port simply never got the same treatment.
  • Playwright's webServer needs the port number at config-definition time, so the derivation must happen synchronously in the config module (both command and url template from the same value — one-line blast radius).
  • CI is unaffected (single runner, workers: 1); this is purely the multi-agent shared-machine topology — which is the Agent OS's PRIMARY topology.

The Fix

In playwright.config.unit.mjs, derive the default port per process when the env override is absent — e.g. probe a free port synchronously (bind 127.0.0.1:0, read the assigned port, close, use) or derive 18180 + (process.pid % 512) with a bind-probe fallback on collision. Env override behavior unchanged (NEO_CHROMA_PORT_TEST still wins — CI and deliberate pinning keep working). Audit the sibling configs (integration, component, visual) for the same fixed-port pattern and apply the same derivation where a webServer exists.

Secondary consequence, worth stating in the fix's JSDoc: per-process ports make orphaned test chromas benign (an orphan squats only its own dead port + tmpdir) — the sticky machine-wide failure mode disappears even before any orphan-reaping hygiene exists.

Acceptance Criteria

  • Two concurrent test-unit invocations from different checkouts complete without port contention (no env vars set).
  • NEO_CHROMA_PORT_TEST override behavior unchanged (spec or documented manual check).
  • A pre-existing LISTENING socket on one derived port cannot wedge an unrelated run (collision fallback proven).
  • Sibling playwright configs audited; same derivation applied where a fixed-port webServer exists (or documented N/A per config).

Out of Scope

  • Orphan-reaper hygiene for dead runners' chroma processes / tmpdirs (separate leaf if the benign-orphan consequence proves insufficient).
  • e2e config port conventions (NEO_E2E_PORT is caller-supplied by design).

Avoided Traps

  • reuseExistingServer: true — adopting a random live chroma crosses test isolation (shared collections between agents' runs; the per-PID dataDir exists precisely to prevent this).
  • A lockfile queue — serializes all agents' test runs machine-wide (slow) and introduces its own stale-lock class; isolation beats queuing.

Decision Record impact

none.

Related

Incident A2A broadcast 2026-07-16 08:06Z (workaround + operator orphan-reap ask) · surfaced during the PR #15211 review-response loop (#14500). Deliberately NOT milestoned: shared-machine dev-infra hygiene, not release-gate scope (D#15209 ledger discipline — explicit deferral note).

Live latest-open sweep: checked latest 12 open issues at 2026-07-16 ~08:07Z; no equivalent found. A2A in-flight claim sweep: herd-window claims (#15216 Vega, #15219 Vega, #15186 Euclid) all disjoint — clean.

Origin Session ID: 75ed6708-c66b-4989-862d-2286e87abbf1

Retrieval Hint: "unit test chroma fixed port 18180 orphan wedge per-process derivation"

tobiu referenced in commit ddf142c - "fix(tests): derive per-process webServer ports for shared-machine isolation (#15221) (#15225) on 12:35 PM
tobiu closed this issue on 12:35 PM