SsuperslateDocs

Testing

Run the deterministic verification path and targeted database integration tests.

Testing

Vitest is the only test runner. Every workspace imports describe/test/expect from vitest and runs vitest run. No test file uses a Bun API.

WorkspaceCommandRuntime
apps/webvitest runNode
packages/contractsvitest runNode
packages/billing, create-app, agent-context, and agent-evalvitest runNode
apps/docsvitest runNode
apps/serverbun --bun vitest runBun

apps/server runs the same vitest on the Bun runtime for one reason: its data layer still imports SQL from 'bun'. That is the only remaining Bun dependency in server source, and it is tracked in TEC-262. When that lands, the script becomes vitest run and no test file changes — the runner and its API are already vitest.

Verified on bun 1.3.14 + vitest 4.1.10: bun --bun vitest --pool=forks gives worker processes with process.versions.bun set, the Bun global present, and import('bun').SQL resolving. The threads pool does not work under Bun, so apps/server/vitest.config.ts pins pool: 'forks'. This supersedes an earlier finding that vitest can only ever spawn Node workers.

One Bun-specific quirk needs a config line: Bun's CJS/ESM interop drops zod's z named export when vitest externalizes it, so the Bun-runtime workspaces set test.server.deps.inline: ['zod']. Node-runtime workspaces need nothing.

The split is enforced by types, not convention. Only apps/server, apps/web, and internal/operations/commercial declare @types/bun; every other workspace declares @types/node with "types": ["node"], so Bun is not an ambient global there and a reintroduced Bun.file() fails pnpm typecheck with TS2868 instead of failing at runtime.

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
pnpm --filter ./apps/server test       # server suite standalone
cd packages/contracts && vitest        # any workspace standalone; bare `vitest` is watch mode

Verified against the pinned toolchain (vite-plus 0.2.9, vitest ^4.1.10, bun 1.3.14):

  • Each workspace declares vitest as a devDependency and calls it directly, so the runner version is explicit per workspace rather than inherited from the Vite+ bundle.
  • Workspaces without a vitest config use vitest defaults (**/*.test.ts). apps/web, apps/server, and apps/docs have a vitest.config.tsapps/web's skips the PWA/Sentry build plugins in vite.config.ts, and apps/docs's scopes discovery to scripts/.
  • apps/server/vitest.config.ts loads src/test-setup.ts via setupFiles, which provides safe env defaults because src/config/env.ts validates process.env at import time. It also sets fileParallelism: false so the DB-gated suites do not contend over one Postgres database.
  • A bare vp test at the repo root still excludes apps/server/**, which needs the Bun runtime; use pnpm test to include it.

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 the run; 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://... pnpm 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. src/test-setup.ts forcibly disables external email, Slack, Google, and rate limit side effects even if a developer .env was loaded.

Better Auth opens one pg pool per test process. Vitest's forks pool isolates each test file, so each file gets its own pool and src/test-setup.ts closes it in afterAll. Individual suites must still not call closeAuthPool() themselves: it latches closed, and later HTTP cases in the same file would lose 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. When you change buyer UI flows, providers, cookies, redirects, or auth plugins, walk the affected flow in a real browser yourself — no automated suite covers it.

  • 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.

What the suite does not cover

There is no browser automation in this repository. Focus order, pointer interaction, portals, element measurement, animation, service-worker updates, and real navigation are unverified by pnpm test, and so are the redirect and cookie behaviors that only appear in a browser.

Treat that as a deliberate boundary rather than a gap to work around. Verify those flows by hand in pnpm dev before release, and keep the checks that do run — contracts, server units, real-Postgres integration, the compiled-binary smoke, and the production build — as the automated gate. If your product later earns browser automation, add it as its own workspace with its own runner rather than widening a unit-test suite to drive a browser.

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