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.
| Workspace | Command | Runtime |
|---|---|---|
apps/web | vitest run | Node |
packages/contracts | vitest run | Node |
packages/billing, create-app, agent-context, and agent-eval | vitest run | Node |
apps/docs | vitest run | Node |
apps/server | bun --bun vitest run | Bun |
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=forksgives worker processes withprocess.versions.bunset, theBunglobal present, andimport('bun').SQLresolving. Thethreadspool does not work under Bun, soapps/server/vitest.config.tspinspool: '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 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
pnpm --filter ./apps/server test # server suite standalone
cd packages/contracts && vitest # any workspace standalone; bare `vitest` is watch modeVerified against the pinned toolchain (vite-plus 0.2.9, vitest ^4.1.10, bun 1.3.14):
- Each workspace declares
vitestas 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, andapps/docshave avitest.config.ts—apps/web's skips the PWA/Sentry build plugins invite.config.ts, andapps/docs's scopes discovery toscripts/. apps/server/vitest.config.tsloadssrc/test-setup.tsviasetupFiles, which provides safe env defaults becausesrc/config/env.tsvalidatesprocess.envat import time. It also setsfileParallelism: falseso the DB-gated suites do not contend over one Postgres database.- A bare
vp testat the repo root still excludesapps/server/**, which needs the Bun runtime; usepnpm testto 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 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. 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.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.
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.