From 7684ac54d413f9a2c80b0e871bac877718273050 Mon Sep 17 00:00:00 2001 From: joshunrau Date: Mon, 9 Nov 2026 16:36:55 -0500 Subject: [PATCH] test(testing): run the e2e suite on two workers by default Outside CI the suite ran on Playwright's default of half the cores. At five workers it logs in faster than the API's login throttle allows (50 a minute): from mail.spec.ts onwards tests fail with "Login failed (429)", and each failure restarts a worker that logs in again. That many workers also leave the machine little for anything else running. The default is now two workers, and still one on CI. E2E_WORKERS in .env overrides both, and anything but a positive integer fails while the config loads. Co-Authored-By: Claude Fable 5.2 --- .agents/docs/architecture/testing-strategy.md | 3 ++- .env.template | 2 ++ testing/AGENTS.md | 8 ++++++++ testing/playwright.config.ts | 6 ++++-- testing/src/support/env.ts | 15 +++++++++++++++ 5 files changed, 31 insertions(+), 3 deletions(-) diff --git a/.agents/docs/architecture/testing-strategy.md b/.agents/docs/architecture/testing-strategy.md index 338fbe34c..9bd600778 100644 --- a/.agents/docs/architecture/testing-strategy.md +++ b/.agents/docs/architecture/testing-strategy.md @@ -180,7 +180,8 @@ coverage is therefore a smoke subset, not the full suite. `webServer` boots `apps/api`, `apps/gateway` and `apps/web` by running each app's own `pnpm dev:test` with `NODE_ENV=test`, which is what gives the API an in-memory Mongo replica set instead of an -external database. `workers: 1` on CI; `fullyParallel` otherwise. The default assertion timeout is +external database. `workers: 1` on CI and 2 elsewhere, with `fullyParallel` on (`testing/AGENTS.md` +§Running has why, and the override). The default assertion timeout is raised to 15s because a 2-core runner doing real DB work misses the 5s default. ## Known warts diff --git a/.env.template b/.env.template index 1b8924cca..4d9984798 100644 --- a/.env.template +++ b/.env.template @@ -122,6 +122,8 @@ PLAYGROUND_DEV_SERVER_PORT=3750 GATEWAY_DEV_SERVER_PORT=3500 # The port to use for the Vite (full web app) development server WEB_DEV_SERVER_PORT=3000 +# The number of end-to-end tests to run in parallel (default: 2, or 1 when CI is set) +E2E_WORKERS= # Set an arbitrary delay (in milliseconds) for all responses (useful for testing suspense) API_RESPONSE_DELAY= # If set to 'true' and NODE_ENV === 'development', then login is automated diff --git a/testing/AGENTS.md b/testing/AGENTS.md index acbbf9387..2cea8293e 100644 --- a/testing/AGENTS.md +++ b/testing/AGENTS.md @@ -112,6 +112,14 @@ preview is an iframe on the other loopback name, so the spec drives it through a is served from the root of the API, and its Redoc page requests `/spec.json`, which the web origin's `/api` proxy does not route. +**The suite runs on two workers, and one on CI.** Playwright's own default of half the cores logs in +faster than the API's login throttle allows (50 a minute), so tests partway through the suite fail +with `Login failed … (429)`, and each failure restarts a worker that logs in again. Set `E2E_WORKERS` +in the root `.env` to change the count; `src/support/env.ts` reads it and throws on anything but a +positive integer. A shell export loses to `.env` wherever `.env` carries the key, blank included: the +`test:*` scripts load it through `env-cmd`, whose values win, and `.env.template` carries the key +blank. For one run, pass `--workers=` after the script instead. + Projects: `setup` (with `teardown` attached), `chromium` (every spec), and `firefox`, which greps `/@smoke/`. **Cross-browser coverage exists only for tests whose title contains `@smoke`** — add the tag to a critical flow, leave it off everything else. diff --git a/testing/playwright.config.ts b/testing/playwright.config.ts index cf144e0aa..c1fb7a024 100644 --- a/testing/playwright.config.ts +++ b/testing/playwright.config.ts @@ -2,7 +2,7 @@ import * as path from 'node:path'; import { defineConfig, devices } from '@playwright/test'; -import { apiPort, baseURL, gatewayPort, playgroundPort, webPort } from './src/support/env'; +import { apiPort, baseURL, gatewayPort, playgroundPort, webPort, workerCount } from './src/support/env'; const webServerDefaults = { gracefulShutdown: { signal: 'SIGINT', timeout: 1000 }, @@ -65,5 +65,7 @@ export default defineConfig({ url: `http://localhost:${playgroundPort}` } ], - workers: process.env.CI ? 1 : undefined + // Playwright's own default is half the cores. That many workers log in faster than the API's login + // throttle allows partway through the suite, and leave a developer's machine little for anything else. + workers: workerCount ?? (process.env.CI ? 1 : 2) }); diff --git a/testing/src/support/env.ts b/testing/src/support/env.ts index 53308cc7e..5ef3dfb54 100644 --- a/testing/src/support/env.ts +++ b/testing/src/support/env.ts @@ -16,6 +16,18 @@ function requiredNumber(name: string): number { return value; } +function optionalPositiveInteger(name: string): number | undefined { + const raw = process.env[name]; + if (!raw) { + return undefined; + } + const value = parseNumber(raw); + if (!Number.isInteger(value) || value < 1) { + throw new Error(`Expected ${name} to be a positive integer, got ${raw}`); + } + return value; +} + // Read straight from the same `.env` apps/web builds `config.meta.contactEmail` from: a developer's // value differs from the `.env.template` one CI generates, so a literal here only passes locally. export const contactEmail = required('CONTACT_EMAIL'); @@ -25,6 +37,9 @@ export const gatewayPort = requiredNumber('GATEWAY_DEV_SERVER_PORT'); export const playgroundPort = requiredNumber('PLAYGROUND_DEV_SERVER_PORT'); export const webPort = requiredNumber('WEB_DEV_SERVER_PORT'); +/** How many tests run at once, where `.env` says; `playwright.config.ts` holds the default. */ +export const workerCount = optionalPositiveInteger('E2E_WORKERS'); + /** How often, in milliseconds, the API pulls completed assignments from the gateway. */ export const gatewayRefreshInterval = requiredNumber('GATEWAY_REFRESH_INTERVAL');