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');