T-2MBO-e2e-servers-get-per-worktree-ports
State: open/ready · Impact: medium · Complexity: small
AUTO-DEFINED: this spec was best-effort machine-authored by /sdlc:task-auto-define on 2026-09-27. Review the Goal, Approach, Today, Areas, and Acceptance-criteria carefully before trusting it.
Playwright e2e fixture servers bind a hardcoded, shared address instead of a
per-worktree one. When two worktrees run the e2e suite at once, the second
run’s Playwright attaches to the first run’s server and tests the wrong build.
Each run should get its own server. Found during
T-98C8-polish-lint-config-patch-save.
solutions/determined/playwright.config.ts’s webServer array starts every
fixture server on a hardcoded 127.0.0.1:<port> address with
reuseExistingServer: true: smoke on 15090, notes on 15091, vaults on
15092, fitness on 15104, and the polish-lint server on 15105. fitness
already has a per-checkout override — FITNESS_E2E_PORT is read by both the
config and solutions/determined/tests/ui/serve-fitness.ts to move it off
15104 when another checkout’s server holds that port — but the other four, including
polish-lint, do not. When two worktrees run the e2e suite at the same time,
the second run’s reuseExistingServer: true finds the first run’s server
already listening on the shared port and attaches to it instead of starting
its own, so the second worktree’s suite runs against the first worktree’s
build.
Proposed
Section titled “Proposed”Every fixture server in the webServer array gets its port from
@sksizer/dev-ports (packages/ts/dev-ports), the repo’s lease-based port
resolver. dev-ports.yaml already declares all five servers by name:
e2e-smoke (15090), e2e-notes (15091), e2e-vaults (15092),
e2e-fitness (15104) and e2e-polish-lint (15105). The resolver’s
fall-forward exists for this case: the same project running twice, such as
two worktrees. It takes an O_EXCL lease per candidate port, checks that
nothing listens there, and searches upward inside the determined band. So
two worktrees running the suite at once each get their own server, and
neither reuses a server it did not start.
Do not derive a port from the worktree path or branch. @sksizer/dev-ports
replaced exactly that scheme (a dev-port.sh that hashed $(pwd)), which
claimed twelve ports twice.
Approach
Section titled “Approach”- In
playwright.config.ts, resolve the five names withresolveDevPortsfrom@sksizer/dev-ports, and build eachwebServerurland each matchingprojects[].use.baseURLfrom the result. The resolver is async and Playwright reads the config once at load time.solutions/determinedis"type": "module", so top-levelawaitin the config is the first thing to try. - Pass each resolved port to its
tests/ui/serve-*.tslauncher through thewebServer[].env, asFITNESS_E2E_PORTdoes forserve-fitness.tstoday. ReplaceFITNESS_E2E_PORTwith the same mechanism, so every launcher reads its port the same way. - Release the leases when the run ends: call each
Resolution.release()in aglobalTeardownor a processexithandler. A lease left behind only costs a later run one fall-forward step, so a crashed run is safe. - Decide
reuseExistingServer. With leases, a port this run resolved is one no other lease holder owns, so reuse can only attach to a server this worktree started earlier. Keeptrueif that holds; otherwise set it tofalsefor the leased servers. - Verify by running the
polishproject from two worktrees at once and confirming each starts its own polish-lint server on a distinct port.
| Area | Note |
|---|---|
solutions/determined/playwright.config.ts | webServer array hardcodes every fixture server’s port; resolve them through @sksizer/dev-ports |
packages/ts/dev-ports/resolve.ts | resolveDevPort/resolveDevPorts and Resolution.release(); consumed here, not changed |
dev-ports.yaml | Already declares the five e2e-* names; update the e2e-fitness note once FITNESS_E2E_PORT is gone |
solutions/determined/tests/ui/serve-fitness.ts | Reads FITNESS_E2E_PORT today; switch to the shared port variable |
solutions/determined/tests/ui/serve-polish-lint.ts, serve-smoke.ts, serve-notes.ts, serve-vaults.ts | Fixed ports today; read the resolved port from their env |
Acceptance criteria
Section titled “Acceptance criteria”- AC-1: Running the e2e suite (or the
polishproject alone) from two separate worktrees at the same time starts two independent fixture servers on two different ports, not one worktree’s suite silently attaching to the other’s already-listening server. - AC-2: Every fixture server in
solutions/determined/playwright.config.ts’swebServerarray gets its port fromresolveDevPort/resolveDevPortsunder itsdev-ports.yamlname, and itsprojects[].use.baseURLuses the same resolved port. No port is derived from the worktree path or branch. - AC-3:
reuseExistingServer: truestill lets a single worktree re-run its own suite against its own already-running server, but no longer causes a second worktree’s Playwright run to attach to a first worktree’s server.
Out of scope
Section titled “Out of scope”- Rewriting the sharing suite’s servers in
playwright.sharing.config.ts— a separate config with its own two servers (:15093 / :15094), not touched here. - Changing how
smoke’sPLATFORM_WEB_URLormvux’sMVUX_WEB_URLexternal-server overrides work — those point the suite at an already-running dev server, a different use case from worktree isolation.
Dependencies
Section titled “Dependencies”- none
Discovery context
Section titled “Discovery context”Spawned by /sdlc:spawn-task-pr on 2026-09-26 UTC from
T-98C8-polish-lint-config-patch-save in
https://github.com/sksizer/dev.
Dedup search (spawn-from-post-mortem)
Section titled “Dedup search (spawn-from-post-mortem)”Bullet: The polish e2e port 15105 was held by another checkout’s Storybook; the suite ran only after a temporary, uncommitted port edit — the polish harness should read its port from an env var, as the fitness one does. Keywords searched: uncommitted, storybook, temporary, checkout, another, harness, fitness, polish Excluded: T-50QX-polish-rule-options-with-no-default Top candidates (score / status / headline):
- 33 / open/ready / T-OLTA-task-work-step7-worktree-baseline-dir — task-work Step 7 resolves the quality baseline dir from a worktree
- 29 / closed/obsoleted / T-521G-pilot-consumer-polish — Pilot the installed artifact on the polish repo end to end
- 27 / closed/done / T-2CXH-scope-release-roster-off-moon-app-vitest — Release roster’s whole-repo bun test collects moon-managed apps’ vitest suites
- 27 / planning/proposed / T-I71M-stories-agent-manager-workbench — Storybook mocks of the Agent Manager workbench
- 26 / closed/done / T-ZEPK-harness-install-dev-link-mode — sdlc harness install claude:
auto-detect dev mode + —link mode
Decision: LINKED-EXISTING T-2MBO-e2e-servers-get-per-worktree-ports (override)
Rationale: T-2MBO never surfaced in the automated top-5 — its body describes the general
mechanism (another checkout’s already-listening server) rather than this bullet’s specific
manifestation (Storybook holding port 15105), so keyword overlap was too thin to rank. Reading
T-2MBO directly shows an exact match: the same hardcoded polish-lint port 15105 in
solutions/determined/playwright.config.ts, spawned the same day from sibling task T-98C8’s post-mortem, proposing the same fix this bullet asks for (an env-var override following the FITNESS_E2E_PORT precedent). Still open (open/ready), unimplemented. Linked here instead of spawning a duplicate.