From 1e81cac5049d3601015e694a6e009e1c3501dcfc Mon Sep 17 00:00:00 2001 From: openhands Date: Mon, 19 Oct 2026 20:16:28 +0200 Subject: [PATCH] test(ui): isolate the fixture port so parallel runs stop colliding The self-hosted CI runner packs several jobs onto the shared host network (capacity 4). The UI suite pinned the fixture server to 127.0.0.1:3107 and set reuseExistingServer: true, so overlapping runs fought over the bind: one job won it, and every browser in the other got "connection refused" on all four projects. Pass one random high port per run through the environment (shared by the main process and its worker) and only reuse an existing server outside CI. Verified with the full ui suite: 150 passed, 2 skipped. --- playwright.ui.config.ts | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/playwright.ui.config.ts b/playwright.ui.config.ts index d2ab7c67..dfab4155 100644 --- a/playwright.ui.config.ts +++ b/playwright.ui.config.ts @@ -1,8 +1,24 @@ +import { randomInt } from "node:crypto"; import { defineConfig } from "@playwright/test"; -const port = Number(process.env.UI_TEST_PORT || 3107); -if (!Number.isInteger(port) || port < 1024 || port > 65535) - throw Error("Invalid UI_TEST_PORT"); +// The fixture server binds a loopback port. The CI runner packs several jobs +// onto the shared host network (capacity > 1), so a fixed port makes concurrent +// runs fight over the bind: one wins, the other's browser then gets +// "connection refused". Pick one random high port for the whole run and pass it +// to every worker through the environment, unless a port is pinned explicitly +// (e.g. a developer reusing a manually started fixture server). +function fixturePort() { + const requested = Number(process.env.UI_TEST_PORT); + if (Number.isInteger(requested) && requested >= 1024 && requested <= 65535) + return requested; + const shared = Number(process.env.PW_UI_TEST_PORT); + if (Number.isInteger(shared) && shared >= 1024 && shared <= 65535) + return shared; + const port = randomInt(30000, 50000); + process.env.PW_UI_TEST_PORT = String(port); + return port; +} +const port = fixturePort(); const baseURL = `http://127.0.0.1:${port}`; export default defineConfig({ testDir: "./e2e/ui", @@ -65,7 +81,7 @@ export default defineConfig({ command: "node e2e/ui/fixture-server.mjs", url: `${baseURL}/health`, timeout: 120000, - reuseExistingServer: true, + reuseExistingServer: !process.env.CI, env: { UI_TEST_PORT: String(port) }, }, });