Files
pentagi/frontend/docs/e2e.md
T
Sergey KozyrenkoandClaude Opus 4.8 c479aec390 test(e2e): make the hover contrast assertion actually measure hover
The probes carry `transition-colors`, so reading the computed background right
after hover() sampled a colour still interpolating away from the rest state —
the half of the gate that exists to catch hover regressions could not catch one.
Disabling the transition on the probes fixes it: verified by giving one variant a
deliberately unreadable hover fill, which the assertion now reports as "badge
blue on hover" and previously let through.

Key the probe list off the variant union so a new badge variant fails to compile
until it is probed, rather than silently going unmeasured.

Also correct two route-manifest ownerships that misdirect diff-scoping: the flows
provider is mounted by the detail route as well as the list, and the provider
detail page was attributed to the list route that never renders it.

Doc fixes: the route sweep is tagged @cross, not @coverage; the licenses link in
CONTRIBUTING resolved outside the repository; and the frontend prettierignore
duplicated entries prettier already reads from gitignore.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 22:18:37 +07:00

9.2 KiB

Frontend E2E tests (Playwright)

End-to-end tests for the PentAGI UI. The default tier runs fully offline against network mocks — no backend, no secrets, no VPN, no LLM keys — so anyone (including fork-PR authors) can run it, and CI runs it on every pull request.

One-time bootstrap

cd frontend
corepack enable          # once per machine, activates the pinned pnpm
pnpm install
pnpm e2e:setup           # downloads the Chromium build (~300MB)

On Linux, browser system dependencies may be needed once: pnpm exec playwright install --with-deps chromium (requires sudo). On Windows, use WSL — native Windows is untested.

Running

pnpm e2e                 # mock tier (default): builds the production bundle,
                         # serves it via `vite preview`, mocks the whole API
pnpm e2e:ui              # same, in Playwright UI mode (watch/debug)
CI=1 pnpm e2e            # byte-identical reproduction of a CI run
                         # (same retries and trace policy; CI-style reporters)

./e2e/tools/run-local-tier.sh          # Tier 2: branch image + isolated docker
                                       # stack + mock LLM; runs specs/real/**
E2E_TIER=stand E2E_BASE_URL=https://… pnpm e2e   # live stand: @stand smoke only
                                       # (flow-run drives a real paid agent run
                                       # and is scoped to the local tier)

pnpm e2e:visual                        # visual snapshots (pinned container)
pnpm e2e:visual:update                 # regenerate baselines after a UI change

Every mock-tier run rebuilds the bundle and starts its own vite preview — deliberately: reusing a server already listening on the port would silently test a previous commit's dist. A stray listener on 8100 fails the run loudly; kill it and rerun.

Tiers

Tier Backend Needs Used for
mock (default) Playwright route/WS mocks replaying cassettes against the production bundle nothing the PR gate; every cassette spec
local branch-built image in an isolated compose stack (pentagi-e2e, ports 8444/5433 — coexists with a dev stack) + an OpenAI-compatible mock LLM driving the real agent loop docker specs/real/**: the fidelity check — real GraphQL/WS/pub-sub end to end
stand a live stand E2E_BASE_URL + creds deploy smoke, version-skew checks

Tier-2 notes: the runner isolates the stack from your .env (--env-file /dev/null), seeds flow ids from 90001 so sandbox containers (pentagi-terminal-<id>) never collide with a developer stack on the same docker daemon, and removes those sandboxes on exit. The deterministic agent transcript lives in e2e/mock-llm/scenario.mjs. Iterating: E2E_SKIP_BUILD=1 reuses the image, E2E_KEEP_STACK=1 leaves the stack up.

Visual snapshots

Baselines live in the repo (e2e/specs/visual/*-snapshots/, linux-suffixed) and are generated ONLY inside the pinned mcr.microsoft.com/playwright container — never run the visual project on the host: macOS pixels produce parallel baselines that will never match CI. pnpm e2e:visual derives the image tag from the installed @playwright/test version, builds dist on the host, and compares inside the container; pnpm e2e:visual:update regenerates baselines (commit them with the UI change that caused the diff). The xterm canvas is masked — WebGL rendering is driver-dependent. The CI e2e-visual job is advisory and never a required check.

Debugging a red CI run

  1. Open the failed run (link in the PR comment) and download the e2e-report artifact.
  2. pnpm exec playwright show-trace <path-to>/trace.zip — full timeline, network, console, and DOM snapshots for each failed test.
  3. Reproduce locally with CI=1 pnpm e2e.

A test failing with every API call the app made must have a cassette entry means the app issued a request the cassette does not cover — the failure lists the exact method/operation. Add the missing entry to the spec's cassette.

Layout

e2e/
  playwright.config.ts   # tiers via E2E_TIER; mock tier builds+serves the prod bundle
  fixtures/              # merged `test`: backend/cassette options, auth seeding, error log
  mocks/                 # cassette-driven GraphQL + REST + graphql-ws mock engine
    cassettes/           # typed cassettes (checked by `pnpm typescript`)
  helpers/               # shared asserts (page errors, …)
  specs/                 # *.spec.ts, tagged (@smoke, …)

Key conventions:

  • Selectors: getByRole/getByLabel first; data-testid only where no accessible name exists (add it to the component in the same PR).
  • Cassettes are TypeScript modules typed against the generated GraphQL types — schema or operation drift fails pnpm typescript, not the runtime.
  • The clock is pinned (UTC, fixed epoch) on the mock tier: keep cassette timestamps on the CASSETTE_EPOCH day or date renders change under you.
  • Unmatched calls fail the test — the mock tier is hermetic by design; nothing ever leaks to a real backend.
  • Assert errors via pageerror/unhandledrejection (expectCleanPage): the production bundle strips app console output, so console-based asserts are meaningless on the mock tier.

Tags

Specs are tagged (test.describe(..., { tag: '@x' })) so runs can be filtered with --grep / --grep-invert (e.g. pnpm e2e --grep @smoke):

Tag Meaning
@smoke Sanity subset — auth, nav, the load-bearing happy paths
@flows Flow list / detail / subscription / terminal specs
@crud Create-read-update-delete journeys (knowledge, api tokens, templates)
@coverage Surface coverage (dashboard, settings, resources)
@cross Cross-cutting: themes, responsive, a11y, contrast, route sweep
@visual Screenshot baselines — runs only in the visual project
@real Tier 2 — real backend + mock LLM (specs/real/**)
@stand Tier 3 — LLM-independent smoke against a live stand

Two conventions the gate reserves:

  • @quarantine — tag a newly-flaky spec to isolate it and drop it from the gate with pnpm e2e --grep-invert @quarantine (leave a tracking note); fix and untag rather than let it rot. Nothing is quarantined today.
  • @generated — a spec whose cassette was recorder-derived and passed a semantic-assertion review; see the LLM recipe below.

Stand tier (Tier 3)

Runs the LLM-independent @stand smoke against a real deployment. Label a PR e2e:stand (or dispatch the workflow with tier: stand); the job runs in a protected e2e-stand Environment whose required reviewers approve before any secret is exposed, so even a mislabeled run blocks on a human. Fork PRs never reach it.

Before the browser specs, a schema-compat pre-flight (e2e/tools/schema-compat.mjs) introspects the stand's live GraphQL schema and validates every frontend operation against it — a renamed or missing field fails once, readably, instead of as dozens of red specs (the deploy-skew class we hit manually). Run it anywhere: E2E_BASE_URL=https://… node e2e/tools/schema-compat.mjs (against the local self-signed Tier-2 stack, prefix NODE_TLS_REJECT_UNAUTHORIZED=0; a real stand has a valid cert).

  • Trend: node e2e/tools/trend.mjs turns a run's results.json (written under CI=1) into one JSONL record (p50/p95 spec duration, slowest three, pass/flaky/fail counts). Each CI run uploads its own e2e-trend artifact; aggregate across runs offline (gh run download) to see duration creep and flake, not just green/red.
  • Affected routes: pnpm exec tsx e2e/tools/affected.ts <base> prints the manifest routes a diff touches (backed by each route's owning sources in e2e/routes.ts). Empty output = no frontend route changed. This is the substrate for scoping runs and the exploratory agent to the changed surface; the mapping logic is unit-tested (e2e/affected-routes.unit.test.ts).

LLM advisory layer (Phase 3, opt-in)

Deterministic specs are the gate; an LLM is only ever an advisory second reviewer, never a merge gate. The intended stack is first-party, not a bespoke bot:

  1. npx playwright init-agents --loop=claude generates the planner / generator / healer agent definitions; the connected playwright-mcp drives the browser in accessibility-snapshot mode (a smaller prompt-injection surface than raw DOM or vision).
  2. Scope the agent to the changed surface with affected.ts (the routes) — do not hand it the whole app.
  3. Guardrails are mandatory because PentAGI renders adversarial content by design (tool output, target responses): treat all page text as untrusted data, never instructions; an action allowlist (no settings/token mutations, no off-origin navigation) on real stands; throwaway scoped credentials; the PR report renders page-derived strings as escaped, length-capped quotes; screenshots posted publicly come only from mock/scripted tiers (a real stand's api-tokens dialog shows a live secret); a hard per-run token cap.
  4. A generated spec merges only with its cassette (so it runs on the Tier-1 gate), a semantic-assertion review, and an @generated tag.

This section is the recipe, not yet wired — the deterministic tiers above are the foundation it plugs into.