Testing
Run the deterministic verification path and targeted database integration tests.
Testing
The repo uses two runners, split by runtime:
| Workspace | Runner | Why |
|---|---|---|
apps/server | bun test | Server code imports Bun built-ins (bun:sql, Bun.file) that Node/vitest cannot load. |
packages/billing, create-app, agent-context, and agent-eval | bun test | Package tests use Bun and Node-compatible APIs. |
apps/docs | bun test | Publication-contract tests run without starting Next.js. |
packages/contracts | vitest via vp test | Pure Zod schemas and helpers. |
apps/web | vitest via vp test | Node-based utilities and static component-state rendering; browser interaction is E2E. |
Why not vitest-on-bun for the server? Verified: vitest always spawns its worker processes with Node even when invoked as
bun x vitest(probe:Bunglobal isundefined, workerprocess.execPathis node), so Bun built-ins don't exist inside vitest tests and there is no supported option to change the worker runtime.bun testhas a jest-compatible API (describe/test/expectfrombun:test), so test files look the same as the vitest suites.
packages/email has no standalone test script:
vp run --filter @app/email dev
vp run --filter @app/email deployThe first command previews sources. The second regenerates and inventory-checks buyer HTML. Run renderer, local-fallback, and provider tests through the server suite.
Commands
pnpm test # root: `vp run -r test` — every workspace suite, each with its own runner
cd apps/server && bun test # server suite standalone
cd packages/contracts && vp test # any vitest workspace standalone (watch mode: `vp test watch`)Verified against the pinned toolchain (vite-plus 0.2.7, vitest ^4.1.10, bun 1.3.x):
vp testin a workspace runs vitest in that workspace, forwarding options (vp test run,vp test watch,--coverage, ...). Workspaces without a vite config use vitest defaults (**/*.test.ts);apps/webhas avitest.config.tsso tests skip the PWA/Sentry build plugins invite.config.ts.- A bare
vp testat the repo root also works: the rootvite.config.tsexcludesapps/server/**so vitest never tries to load Bun built-ins. It runs only the vitest suites — usepnpm testto include the server. bun testdiscovers*.test.tsunderapps/serverand preloadssrc/test-setup.ts(seebunfig.toml), which provides safe env defaults becausesrc/config/env.tsvalidatesprocess.envat import time.
DB-gated integration tests
Unit tests never touch the database. Integration tests run only when RUN_DB_INTEGRATION_TESTS=1
and DATABASE_URL are both set before bun test; they skip visibly otherwise. The explicit flag
prevents an unrelated DATABASE_URL in your shell from changing the default test suite:
docker compose up -d
cd apps/server && dbmate --migrations-dir ./migrations --no-dump-schema up
RUN_DB_INTEGRATION_TESTS=1 DATABASE_URL=postgres://... bun testapps/server/src/domains/notifications/repository.integration.test.ts is the repository-test
template: initDb() in beforeAll, clean up your fixtures and closePool() in afterAll, and
gate with describe.skipIf. The test preload forcibly disables external email, Slack, Google, and
rate limit side effects even if Bun loaded values from a developer .env.
Better Auth has one pg pool for the Bun test process. The global test preload closes it after all
files finish; individual suites must not call closeAuthPool() because later HTTP suites may still
need session storage.
apps/server/src/infra/db/migrate.integration.test.ts is the migration-failure fixture. It applies
one valid temporary migration followed by intentionally invalid transactional DDL, then proves the
valid version remains recorded while the failed file leaves neither its table nor a version row. Use
it with the failed-migration golden path; never point
it at a shared database.
Billing and authentication coverage
Billing remains covered by its DB-gated integration suite. Better Auth's HTTP suite exercises the real Hono handler and Postgres adapter across password signup, verification, authenticated user resolution, logout, password reset and session revocation, magic-link signup, single-use replay rejection, and the optional-Google failure state. The compiled-server smoke test separately proves that the same flow survives the standalone Bun build. Add browser E2E coverage when changing buyer UI flows, providers, cookies, redirects, or auth plugins.
apps/server/src/domains/billing/service.integration.test.tsapps/server/src/lib/auth.integration.test.ts
The upload suite uses a fake provider adapter plus real Postgres to prove owner scoping, expiry, actual metadata validation, failed-object cleanup, replay, and concurrent confirmation without requiring storage credentials:
apps/server/src/domains/upload/service.test.tsapps/server/src/domains/upload/service.integration.test.ts
CI supplies both variables, so these suites run against its Postgres service instead of skipping. CI
also starts the compiled server from the repository root, requires all 9/9 rendered email
templates to preload, and sends a real magic-link request. This guards the external-resource layout
used by the standalone binary rather than proving only source execution.
Browser end-to-end coverage
Playwright runs through pnpm test:e2e from the repository root. It starts the local API and web
application, migrates the exact DATABASE_URL it receives, and exercises browser-visible behavior.
Use a named disposable database; never point the browser suite at a shared, staging, or production
database.
The complete local workflow, CI boundary, failure artifacts, and test-writing rules are in End-to-end testing. Browser coverage complements rather than replaces contract, server, integration, provider, and deployed smoke tests.
Web bundle budget
Every web build runs apps/web/scripts/verify-bundle-budget.ts after Vite emits dist/. It reads
the built HTML rather than guessing from source imports and fails when:
- the module entry plus its module-preload dependencies exceed 512 KiB; or
- any individual JavaScript chunk exceeds 450 KiB.
Route-lazy JavaScript is not charged to initial startup, but it remains subject to the per-chunk limit. The budget does not claim that the complete PWA precache is downloaded on first navigation; measure transfer, parse, and interaction timing in a real browser before making a performance claim. When the budget fails, inspect eager route/layout imports before adding manual vendor chunk rules or raising the limit.