mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-10-26 14:30:13 +00:00
416aea2d14
* fix(collab): reconnect catch-up only when this tab's frames can be missing (BUG-3523)
The provider sent the whole document as one update on every reconnect,
appending a full copy to the op-log on each laptop wake or network blip.
It now sends it only when an edit was made with no socket open, or a
local update went out on a socket that then closed and no barrier ack
cleared it.
The barrier: once local sends go quiet (2s), the client sends one
`barrier {n}`; the server answers `barrier_ack {n, ok}` in readLoop
order, after every earlier frame was persisted, with ok=false when an
op-log append on that conn failed since the last barrier. An ok ack
covering every local send clears the debt; ok=false keeps it and logs
the item; a server that never answers leaves the tab owed.
Also counts materializer give-ups (pad_materialize_giveup_total{kind}).
Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
* fix(collab): a SyncStep2 answer is stored but not counted as content (BUG-3523)
A step2 is y-protocols' answer to a peer's step1 and repeats content other
rows carry: the answering tab's own edits went out as updates (and are
repaired with a full catch-up if they may be missing), and everything
else it holds came through the relay. Counting it kept items pending and
made the materializer replay a full copy of the document per joining tab.
Measured first: recorded multi-tab op-logs, across a dormancy prune,
materialize to identical text with and without their step2 rows.
Applied at append only. The startup backfill keeps the older rule for
legacy rows, written by clients with no failed-append repair.
Also: a barrier_ack with ok=false now resends the document on the live
socket at once (lead ruling), instead of only owing the next connection.
Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
* docs(collab): say what the barrier ack's write deadline bounds (BUG-3523)
Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
* test(e2e): take the server's data dir from one module (BUG-3523)
The step2 spec resolved .pad-e2e one level too shallow: it passed where
PAD_E2E_DATA_DIR was set and could not open the database in CI.
playwright.config.ts and the spec now read e2e/lib/data-dir.ts.
Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
119 lines
4.4 KiB
TypeScript
119 lines
4.4 KiB
TypeScript
import { defineConfig, devices } from '@playwright/test';
|
|
import { dirname, resolve } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { E2E_DATA_DIR } from './e2e/lib/data-dir';
|
|
|
|
// 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 = E2E_DATA_DIR;
|
|
|
|
// 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,
|
|
// The public origin the e2e server serves (PLAN-2310 DR-3). MCP
|
|
// addressing comes from configuration, never the request's Host,
|
|
// and off cloud /mcp, /oauth/* and /.well-known/oauth-* refuse a
|
|
// Host that is not configured (DR-6, 421). A spec that exercises
|
|
// MCP therefore needs this to match the origin the browser and
|
|
// the test client use.
|
|
PAD_URL: BASE_URL,
|
|
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'
|
|
}
|
|
}
|
|
});
|