Testing

One Vitest preset shared by every package and app.

@kreogen/testing exports a Vitest configuration preset. A package that wants tests writes three lines:

packages/auth/vitest.config.ts
import { definePreset } from '@kreogen/testing';

export default definePreset({ root: import.meta.dirname });
bun run test          # everything, through turbo
bun test --filter auth

Options

OptionDefaultFor
rootrequiredimport.meta.dirname of the package
environment"node""jsdom" for anything rendering React
reactimplied by jsdomAdds the React plugin
setupFiles[]Resolved relative to root
coverageThresholdnoneMinimum line and function coverage

The threshold applies only when coverage is collected, so a plain vitest run stays fast.

What the preset settles

The @kreogen/* alias. Packages resolve to source, and that alias has to exist wherever code is resolved. It used to be written by hand in every vitest config as well as in the tsconfig — four copies of one path, with a note in CLAUDE.md asking people to remember all of them.

Two remain: packages/typescript-config/nextjs.json, which the editor and tsc read, and this package, which every test run reads. A test in packages/testing/__tests__ fails when they disagree — nothing else would notice, and the symptom is a suite resolving a stale path and passing against code nobody is shipping.

A server-only stub. That module throws outside a React Server Component graph, and a unit test is not one. Stubbing it is what makes the server modules — the ones that most need testing — importable at all.

Placeholder environment variables. Every package validates its environment at import through keys(), which is right for a running app and wrong for a test asserting on a pure function. DATABASE_URL, BETTER_AUTH_SECRET and the public URLs get parseable defaults, applied only when genuinely unset — so a suite running against real infrastructure still gets the real values.

DATABASE_URL's placeholder is a valid connection string rather than an obviously fake one, because it is handed to a pg pool whose constructor parses it eagerly. Nothing connects unless a test issues a query.

Machine-readable output in CI. JUnit reporter and coverage in text-summary, lcov and cobertura, written under the package directory. That placement matters: turbo runs tests per package, and the pipeline's earlier globs pointed at repository-root paths nothing wrote — so every run published an empty report and no coverage at all, while going green.

Why it calls defineConfig itself

definePreset returns a finished config rather than an object for the caller to wrap. Wrapping was the obvious API and it does not typecheck: bun resolves more than one copy of vite, and a config produced against one copy's types is not assignable to another's defineConfig, even at identical versions. Doing it here means only this package's copy is ever involved.

What is excluded from coverage

__tests__, generated, *.config.* and keys.ts. Generated Prisma output and env schemas are not code anyone writes tests for, and leaving them in makes the number meaningless.

The one suite that boots a server

apps/web/__tests__/routes.test.ts is the exception to everything above. It starts a real Next server with every declared variable emptied and requests every route, asserting that none answers 5xx, that an unknown post is a 404 and that /sitemap.xml matches the pages on disk.

It exists because the property it checks cannot be checked structurally. packages/cms degraded correctly in its query functions and threw from the Feed component beside them, which put /legal/terms and /legal/privacy — both linked from the sign-up form — on a 500 in any project generated with cms and no token. A structural check was tried against both bugs in that route and caught neither.

It costs roughly ninety seconds, almost all of it Turbopack compiling routes on first request. If that becomes too slow, shorten the route table rather than replacing the approach.

In CI

The test job runs against a real Postgres service. Most suites use stand-ins; that database exists so tenancy and auth are checked against the thing that actually enforces the constraints.