Files
pad/web/playwright.config.ts
xarmian c8492db29f fix(e2e): disable rate limiting on the E2E server to stop 429 flakes (BUG-2089) (#922)
The E2E harness runs the real pad binary with the real rate limiter, and
every Playwright test shares one loopback IP (127.0.0.1). The auth limiter
(5 logins/min/IP, burst 5) trips as soon as a spec logs in a couple of
browser clients — collab-persistence.spec.ts logs in two per test — so
browserLogin fails with "in-page login failed with status 429". This was
deterministic, not flaky: it failed on TASK-2058's own PR and its push to
main, and on every downstream PR since.

Add a test-only env knob PAD_DISABLE_RATE_LIMITS: when truthy, New() leaves
Server.rateLimiters nil, which RateLimit() already treats as a pass-through
(Stop() and the MCP path are already nil-safe). Wire it into the Playwright
webServer.env; run-pad.mjs spawns the binary with inherited env so it
reaches the pad process. Limiters stay fully active in prod/self-host — the
knob is an explicit opt-in only the E2E server sets.

Verified: collab-persistence.spec.ts passes locally with the fix; the
existing limiter tests still pass (limiters on when the env is unset); new
TestRateLimit_DisabledByEnv pins the bypass.

Claude-Session: https://claude.ai/code/session_015yuBJQYfDj95cgX3DaD8SF
2026-07-11 14:30:32 -04:00

111 lines
4.0 KiB
TypeScript

import { defineConfig, devices } from '@playwright/test';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
// Anchor all test paths to the config file's directory so runs are
// invariant under the caller's cwd (Playwright's default resolves
// relative paths against cwd, which differs between `npx playwright
// test` and CI invocations).
const HERE = dirname(fileURLToPath(import.meta.url));
/**
* Playwright config for Pad end-to-end smoke tests.
*
* The suite exercises the real Pad binary (built from this repo) serving
* its embedded web UI. That's the same shape a user gets from
* `pad server start`, so regressions that only show up once the web
* assets are embedded are caught here rather than in svelte-check.
*
* See e2e/global-setup.ts for how admin bootstrap + workspace seeding
* happen before any test runs.
*/
const E2E_PORT = Number(process.env.PAD_E2E_PORT ?? 17800);
const E2E_HOST = process.env.PAD_E2E_HOST ?? '127.0.0.1';
const BASE_URL = `http://${E2E_HOST}:${E2E_PORT}`;
// Build a private data dir for this run so the test instance never
// touches the developer's real ~/.pad or another CI run's artifacts.
const DATA_DIR = process.env.PAD_E2E_DATA_DIR ?? resolve(HERE, '..', '.pad-e2e');
// Pad binary relative to the repo root. `make build` / `make build-go`
// writes ./pad in the repo root; CI builds it explicitly before the
// e2e job runs.
const PAD_BINARY = process.env.PAD_BINARY ?? resolve(HERE, '..', 'pad');
export default defineConfig({
testDir: './e2e',
timeout: 30_000,
expect: { timeout: 5_000 },
// Playwright's reporter list kept minimal; CI uses the list reporter
// and uploads the HTML report as an artifact on failure.
reporter: process.env.CI ? [['list'], ['html', { open: 'never' }]] : 'list',
// Run tests in parallel but cap workers on CI to keep the suite under
// the 2-minute target quoted in TASK-689.
fullyParallel: true,
workers: process.env.CI ? 2 : undefined,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 1 : 0,
use: {
baseURL: BASE_URL,
trace: 'retain-on-failure',
video: 'retain-on-failure',
screenshot: 'only-on-failure'
// NOTE: auth is applied per-test via the fixture in e2e/fixtures.ts
// rather than config.use.storageState. The config approach proved
// flaky: Playwright resolves `storageState` against disk at
// project-setup time (before globalSetup runs), so the file written
// by globalSetup wasn't picked up. Passing cookies through an
// explicit `test.extend` fixture sidesteps that ordering entirely.
},
globalSetup: './e2e/global-setup.ts',
projects: [
{
name: 'desktop-chromium',
use: { ...devices['Desktop Chrome'] }
},
{
// Mobile viewport — triggers the mobile BottomSheet branch
// (CONVE-639: isMobile ≤ 639.98px on WorkspaceSwitcher).
// Pixel 7 ships with a Chromium defaultBrowserType, so we keep
// one installed browser for both projects and avoid downloading
// WebKit in CI.
name: 'mobile-chromium',
use: { ...devices['Pixel 7'] }
}
],
webServer: {
// Wipe the data dir BEFORE the server starts, so migrations run
// against an empty SQLite file every run. Doing this in globalSetup
// would race with the already-running server — it has the DB file
// open by then. The wrapper script is Node-based so it works on
// Windows (cmd/PowerShell), not just POSIX shells.
command: `node ${resolve(HERE, 'e2e', 'run-pad.mjs')}`,
url: `${BASE_URL}/api/v1/health`,
timeout: 30_000,
reuseExistingServer: !process.env.CI,
stdout: 'pipe',
stderr: 'pipe',
env: {
PAD_BINARY,
PAD_HOST: E2E_HOST,
PAD_PORT: String(E2E_PORT),
PAD_DATA_DIR: DATA_DIR,
PAD_LOG_LEVEL: 'warn',
// Every E2E test shares one loopback IP, so the auth limiter
// (5 logins/min/IP) trips as soon as a spec logs in a couple of
// browser clients (BUG-2089). Disable rate limiting for the E2E
// server only — never in prod/self-host. run-pad.mjs spawns the
// binary with inherited env, so this reaches the pad process.
PAD_DISABLE_RATE_LIMITS: '1'
}
}
});