Gantry has two kinds of tests with different jobs. Protected frontend tests run against a deterministic fixture build and guard the accepted product identity. Real-platform tests run against the local control plane, PostgreSQL, K3s, Argo CD and telemetry backends and establish integration behavior. Neither is a substitute for the other: a green fixture suite proves nothing about Kubernetes, and a real-platform run is not a stable visual baseline.
Accepted results are summarized in the README verification guide and detailed in the architecture. Real-mode screenshot differences are classified in visual-regression.md.
| Layer | Tooling | Location | Command | What it establishes |
|---|---|---|---|---|
| Lint / types / build | ESLint, tsc, next build |
repository root | npm run lint, npm run typecheck, npm run build |
Static correctness and a production artifact. npm run check runs lint, typecheck, unit tests and build in sequence. |
| Frontend unit | Vitest, Testing Library, jsdom | tests/unit |
npm test |
Catalog filtering and sorting, name/identity/secret-reference validation, wizard defaults and blocking rules, Scorecard evaluation, store simulations, chart sampling. 28 tests in six files. |
| Protected frontend E2E | Playwright | tests/e2e |
npm run e2e (e2e:desktop, e2e:mobile) |
36 tests against the production build of the fixture artifact: flows, catalog filters, production safety, resources, drift, Scorecard, live state, reduced motion, axe, visual baselines, mobile. |
| Dev/Turbopack regression | Playwright | tests/e2e/service-tabs.spec.ts |
npm run e2e:dev |
3 tests against next dev; guards the Service Detail tab regression described below. |
| Control-plane integration | go test with real PostgreSQL |
control-plane/internal/** |
scripts/platform/test-control-plane.sh (inside WSL) |
Migrations, authentication, API success/error contracts, authorization, idempotency and concurrency against a real database. |
| Real-platform browser | Playwright | tests/platform/platform.spec.ts |
npx playwright test --config playwright.platform.config.ts |
Five scenarios against the running platform: full creation/resource/deploy/telemetry, policy rejection and remediation, role differences, drift, accessibility. |
| Platform verification | Python and shell scripts | scripts/platform |
See operations | Admission probes, failure matrix, extended verification, state and telemetry audits, resource measurement. |
NEXT_PUBLIC_GANTRY_MODE=demo is the only way to enable the fixture adapter. src/data/index.ts selects between the HTTP/SSE adapter and src/data/mock; nothing else in the tree chooses a data source. With the variable set, next.config.ts writes build output to .next-demo so the fixture artifact never overwrites a real-mode build.
$env:NEXT_PUBLIC_GANTRY_MODE='demo'
npm run build
npm run e2e
npm run e2e:dev
Remove-Item Env:NEXT_PUBLIC_GANTRY_MODEThe protected suite starts npm run start on port 3100 (playwright.config.ts). The dev configuration starts npm run dev on port 3200 and uses a localhost origin because Next.js blocks cross-origin requests to development assets. Do not run e2e:dev while another next dev is using .next/dev.
Fixture data describes a demo organisation, Tidewater, with seeded teams and services. Everything is generated relative to the current time; in-browser mutations persist in localStorage under gantry.demo.v1 and can be reset from Settings. Permission checks are simulated from the demo user's team, telemetry is a deterministic series, and repository links are placeholders. Real mode uses none of this, and the real-platform suite never uses the fixture adapter.
The accepted frontend defines the product. Backend work integrates behind it; a change to the surfaces below is a deliberate decision that must be documented here and reflected in a regenerated baseline, not a side effect.
- Brand and tokens. The Gantry name and mark, warm-neutral surfaces, a single accent, semantic status tones, Instrument Sans and JetBrains Mono. Sources:
src/app/globals.css,src/components/shell/brand.tsx, design-system.md. - Information architecture. The route map in wireframe/README.md and the tab structure of service pages. Backend identifiers may change; paths and their meaning must not.
- Shell. Text-first sidebar, top bar with live indicator and theme toggle, mobile sheet navigation, command palette on Ctrl/⌘ K and
Gchords. - Flagship interactions. The nine-step Create Service wizard with inline validation and plan preview; the service identity header and overview; deployment detail ordered as stage strip, failure block (what / where / why / what next), stage details and raw output behind technical detail; policy failures with a human explanation, affected resource, remediation and documentation link; observability as SLO rows, four focused charts with deployment markers, logs with trace links and a trace waterfall; the focus-centred dependency graph with a list equivalent; environment comparison and the GitOps sync panel; the resource request lifecycle with secret references only; Scorecards that show evidence and remediation and never a percentage; production action safety with reason and permission-denied states; live, updating, stale, reconnecting and lost connection states; empty, loading, error and permission-denied states.
- Responsive behaviour. Dense tables at 768 px and above, recomposed card lists below; no horizontal page overflow at 390 px; the create flow collapses its stepper into a progress bar.
- Accessibility. Landmarks, skip link, keyboard paths, visible focus rings, labelled controls, text equivalents for charts, reduced-motion support and underlined inline links. The axe suite must stay free of serious and critical violations.
Interaction rules the tests assume:
- A failed operation never changes the running version. Production shows what it actually runs.
- Retrying without a change reproduces the same policy result.
- Every visible action either works or states that it is unavailable. There are no dead controls.
- No credential values appear anywhere; secret references only.
- Status is never colour-only.
- Filters and search live in the URL and survive navigation.
| Spec | Tests | Coverage |
|---|---|---|
tests/e2e/flows.spec.ts |
14 | Flows A–E (home to deployment, create, failed deployment to runbook, degraded service to trace, palette), catalog filters, production safety, resource lifecycle, drift, Scorecard, live state, incident chain, reduced motion |
tests/e2e/a11y.spec.ts |
9 | axe (WCAG 2.1 A/AA) on eight routes and the open command palette; zero serious or critical violations |
tests/e2e/service-tabs.spec.ts |
3 | Every Service Detail tab in one browser session, linear and non-linear order, deep links; no error overlay, page error or console error |
tests/e2e/visual.spec.ts |
6 | Desktop screenshots: home, catalog, create, service detail, failed deployment, observability |
tests/e2e/mobile.mobile.spec.ts |
4 | 390×844: overflow check, critical mobile task, search, two screenshots |
Baselines live in tests/e2e/__screenshots__/ (six desktop, two mobile). They are deterministic because the suite freezes the clock at 2026-09-07T19:30:00Z, disables animations, forces the dark colour scheme, UTC and en-US, and masks the live-updated timestamp. The comparison allows a 2% pixel difference.
Regenerate with npm run e2e:update-snapshots only for an intentional change, and record the reason in this file or in visual-regression.md. Real-mode screenshots use different services, workflow identifiers, timestamps and measured telemetry, so they are never interchangeable with the fixture baselines; the visual-regression notes classify each real surface as expected data difference, intentional integration change or fixed regression.
src/data/api.tsis thePlatformApicontract. Components talk to the platform only through it; see backend-integration.md for the operation-by-operation behavior.src/data/hooks.tsowns query keys. Keep them stable: SSE invalidations use them to refresh views.src/domain/**holds the shared types and labels.src/data/mock/**is replaceable fixture code.filterServices,buildGraphandevaluateScorecardthere are pure and unit-tested.
next.config.ts sets experimental.workerThreads: true with cpus: 2. Static-params evaluation in development and static generation in builds run in worker threads instead of forked child processes. Keep the flag unless the control plane requires otherwise, and rerun npm run e2e:dev after touching it. The reason is the regression below.
Recorded 2026-09-07 during frontend acceptance; the fix and its regression test remain in place.
Symptom. In next dev (Next.js 16.3.4, Turbopack) on /services/checkout-api, Overview and Environments rendered, then further tab navigation failed with the runtime error Jest worker encountered 2 child process exceptions, exceeding retry limit. Once it appeared, every /services/[slug]/* request returned HTTP 500 while static routes kept working. The development log showed Failed to generate static paths for /services/[slug] first, then the worker error on every request, MaxListenersExceededWarning and write EPIPE.
Root cause. Three things combined; none of them is in the React code.
- Every request to a dynamic app route in development makes Next.js fork a
static-paths-workerchild process (jest-worker, one retry) to evaluate static params, even when no route definesgenerateStaticParams. Every Service Detail tab is such a route. On a cached route the fork runs in the background; on an uncached route the render waits for it. When the fork fails, the cache entry is evicted, so the next request throws the error into the render. - The failing dev server had outlived the console that started it. From then on Windows refused to initialise any console-attached child process it spawned: exit code 3221225794 (
0xC0000142,STATUS_DLL_INIT_FAILED). Attaching the inspector to the live server confirmed it: a plainspawn, a jest-workerforkand a stdio-ignored spawn all died with that code; children created with a hidden or detached console succeeded; the same worker call from a fresh process succeeded in about 200 ms. - jest-worker re-forks a child that dies at startup indefinitely and
end()cannot stop it, so each failed request left a respawn loop behind. The affected server was running 69 such loops, which explains the listener warnings, the EPIPE and why the failure never recovered.
The chain was reproduced deliberately: a fresh dev server in its own console served every tab; killing only its console host and requesting a not-yet-compiled tab returned HTTP 500 with the exact error, while cached tabs still served 200.
Fix. experimental.workerThreads: true moves static-params evaluation and static generation into worker threads. Threads need no process creation, so a dev server that has lost its console keeps serving dynamic routes, and there is no respawn loop. Under the same trigger, all nine never-compiled tabs returned HTTP 200 and the log stayed clean. Rendering does not change, so the visual baseline was not regenerated. Residual limitation: Turbopack's PostCSS pool is still a child process, so stop a dev server with the terminal that started it rather than leaving it running.
Regression test. tests/e2e/service-tabs.spec.ts runs three tests in one browser session on checkout-api: the linear walk across all nine tabs and back; a non-linear order plus history back/forward; and direct deep links to every tab, environments/production, dependencies?select= and resources?request=1. Each step asserts exactly one aria-current="page" tab with the right label, tab-specific content, no Next.js error overlay, no app error boundary, no pageerror, no console error and a per-navigation time budget. It runs against the production build in npm run e2e and against the Turbopack dev server in npm run e2e:dev, which widens the budget for on-demand compilation.
The real-platform browser suite signs in with locally derived credentials, creates a real Go service with a Redis claim, deploys an unsafe production change and repairs it, compares roles, provokes and reconciles drift, and scans real routes with axe. It writes results under .gantry/evidence, which is ignored by Git. Trace, video and HAR capture are disabled so credentials never land in test artifacts. Every full run consumes bounded local capacity (six services, eight claims); inspect existing evidence before rerunning it.
scripts/platform/test-control-plane.sh formats the Go sources and runs go test ./... with GANTRY_TEST_DATABASE_URL pointed at the local PostgreSQL, so integration tests exercise real migrations, transactions and constraints. The remaining verification scripts, their ordering constraints and the rule to run failure scenarios serially are in operations.md.
Real-mode accessibility adds keyboard access to horizontally scrolling code and pipeline regions, command-palette focus return, and mobile layouts for long Kubernetes paths and real deployment headers. These were accessibility and integration fixes, not baseline regeneration; the accepted screenshot files were not changed.