The default suite
go test ./... runs everywhere with no Docker daemon and no network. It covers the domain packages, view rendering, provider webhook parsing, backup encryption, and log redaction. Keep it that way: a suite that needs infrastructure to run is a suite that stops being run.
Alongside it CI runs TypeScript checking, frontend builds, Biome, Atlas validation, Mintlify validation, vulnerability scanning, and container builds.
The integration suite
Tests underinternal/integrationtest carry the integration build tag and start PostgreSQL through Testcontainers:
- migrations applying from a bare database, and the v0.4 → v0.5 adoption of existing customers onto subscriptions;
- top-up crediting the wallet exactly once across a replayed webhook, a second event, and a reconciliation;
- overpayment and underpayment resolving in the ledger while the order still settles;
- a saved cart staying saved while short and being charged once when funded;
- concurrency limits holding under simultaneous purchases;
- the outbox and the operator notice being written once per settlement;
- maintenance blocking new orders while a paid order still settles;
- the durable fulfillment job committing in the settlement transaction;
- retention removing disposable rows and never an audit event;
- operator bootstrap creating exactly one owner, and the setup token being single-use;
- sign-in answering identically for an unknown address and a wrong password, lockout after repeated failures, and the session, TOTP, and recovery-code lifecycles;
- the last active owner surviving both role revocation and suspension, and only an owner granting the owner role;
- suspension revoking live sessions, a password change sparing only the session that made it, and a reset link working once;
- audit classification, both sort directions, and keyset pages that never repeat or skip a row.
load_test.go is part of the same suite. It is not a throughput benchmark: it drives the same settlement, cart purchase, and subscription creation from many goroutines at once, because a duplicated charge only appears when the same work arrives from several directions simultaneously.
Running the suite is the cheapest way to catch a class of defect that compiles
and unit-tests cleanly: a Go enum widened without its database CHECK, an expiry
computed from the application clock while its row’s created_at comes from the
database’s, or a constraint whose derived name was guessed wrong. Each of those
only fails against a real PostgreSQL.
Migration upgrade gate
Themigrations CI job runs once per supported baseline — the empty database plus each prefix of the migration history — applies the rest, then applies head a second time to prove it is a no-op. A migration that cannot run on top of a released version fails there.
What a financial change owes
New payment, wallet, authentication, RBAC, import, and Remnawave fulfillment behaviour must include focused failure-path and idempotency tests from its first implementation. In practice that means proving, for the new path:- The same input applied twice produces one effect.
- A partial failure leaves no half-applied state.
- Every rejection carries a stable machine reason, not a formatted message.
Provider fixtures
internal/payments/testdata holds recorded webhook shapes with every real identifier replaced. Replaying them keeps parsing and signature verification honest without a sandbox account. Never commit a real payment payload, token, or signature — synthesise one instead.
Browser coverage
apps/web/e2e holds the Playwright suites, run with bun run e2e from apps/web. They run against a production build rather than the dev server: Next.js behaves differently in development, and a gate that passes against a build nobody ships is not a gate.
The suites cover what a component test cannot see, because it is a property of a rendered page rather than of a component — the accessibility, layout, and localisation gates — plus the access boundary of both panels: every operator and customer route refuses an anonymous visitor, and nothing behind a session gate paints before the session is known.
The operator journey behind the session gate is covered. CI applies the
migrations, starts a real API, redeems the one-time setup token to seed an
owner, and operator-journey.spec.ts signs in for real, proves the session
survives a navigation, and sweeps every panel page. Locally, set PANEL_EMAIL
and PANEL_PASSWORD against a running stack; without them that spec skips and
says so rather than pretending to pass.
A missing translation renders as a marker rather than as its own key — see
apps/web/i18n/missing-message.ts — and the sweep looks for exactly that. The
previous check searched for a dotted key, which cannot tell a missing message
from data: the audit log stores action names in the same shape, and the check
failed on a fully translated page the first time it ran against an API with rows
in it.
The customer journey is covered too, and needs no seeding at all: the panel
provisions an account on first sign-in, so customer-journey.spec.ts signs a
Telegram login-widget payload with the same bot token the API is configured with
and posts it through the web server’s /v1 proxy. Set CUSTOMER_BOT_TOKEN to
that token to run it locally. internal/customerauth carries an interop test
asserting the TypeScript signing and the Go verification agree — without it, a
mismatch would fail every run and get “fixed” by weakening the signature check
the suite exists to exercise.
Nothing behind a Remnawave entitlement is covered. The customer the suite
creates has signed in and bought nothing, so the screens that need a
subscription render their empty state rather than their populated one. That is
tracked in the roadmap’s verification debt for v0.10.
Performance budgets
bun --filter @omniflow/web perf:budget runs against a finished next build
and fails when a route’s first-load JavaScript, the stylesheet payload, or the
self-hosted fonts exceed their ceilings. CI runs it in the web job, straight
after the build it measures.
Sizes are uncompressed, which is what Next’s own route-bundle-stats.json
reports and the more honest figure to gate on: compression hides a regression
that still costs a phone its parse and execute time.
Images, server response time, and field core web vitals are deliberately not
measured. They need a running installation with real content and real clients,
and a number invented from a local build would read as a gate that had passed.