SsuperslateDocs

Testing

Run the deterministic verification path and targeted database integration tests.

Testing

The repo uses two runners, split by runtime:

WorkspaceRunnerWhy
apps/serverbun testServer code imports Bun built-ins (bun:sql, Bun.file) that Node/vitest cannot load.
packages/billing, create-app, agent-context, and agent-evalbun testPackage tests use Bun and Node-compatible APIs.
apps/docsbun testPublication-contract tests run without starting Next.js.
packages/contractsvitest via vp testPure Zod schemas and helpers.
apps/webvitest via vp testNode-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: Bun global is undefined, worker process.execPath is node), so Bun built-ins don't exist inside vitest tests and there is no supported option to change the worker runtime. bun test has a jest-compatible API (describe/test/expect from bun: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 deploy

The 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 test in 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/web has a vitest.config.ts so tests skip the PWA/Sentry build plugins in vite.config.ts.
  • A bare vp test at the repo root also works: the root vite.config.ts excludes apps/server/** so vitest never tries to load Bun built-ins. It runs only the vitest suites — use pnpm test to include the server.
  • bun test discovers *.test.ts under apps/server and preloads src/test-setup.ts (see bunfig.toml), which provides safe env defaults because src/config/env.ts validates process.env at 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 test

apps/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.ts
  • apps/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.ts
  • apps/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.

On this page