Tests run on Vitest. Every app and package has a test script and pnpm test at the repo root runs them all through Turbo, after building dependencies (dependsOn: ["^build"]).

Where tests live

The older notes describe tests/unit/ and tests/integration/ subfolders. Those folders do not exist. Tests are named after the feature they cover, for example autofit-import-worker.test.ts, app-trust-proxy.test.ts, assistant.test.ts.
apps/core-api/package.json has a test:integration script that points at vitest.integration.config.mts. That file is not in the repo, and there is no Vitest config file anywhere in the workspace. pnpm test:integration cannot run as it stands. Everything runs under the default vitest run.

Commands

The core-api test script is vitest run --passWithNoTests.

Unit testing a service

Services are factories that take their dependencies as arguments, so a test passes plain objects.
Relative imports in tests end in .js, the same as in source. What to cover for a service:
  • The happy path.
  • Every branch that throws an AppError, asserting the code.
  • Tenancy: the same id under a different studioId is not found.
  • Role or permission dependent behaviour.

Test seams built into the code

The code is written to be tested without infrastructure. Use these before reaching for a mock library. When you write new code, keep this property. Take the clock, the network and the model as arguments.

Testing the Express shell

For behaviour that lives in app.ts or a middleware, build the app with createApp and drive it with supertest. The modules behind /v1 pull in everything, so tests replace them with stubs using vi.mock before importing the app:
This is the pattern in tests/app-trust-proxy.test.ts, which checks that the rate limiter keys on the forwarded client address. createApp takes a context object, so a test passes only the env fields the shell reads. To test a single router, mount it on a bare Express app with the error middleware, set req.auth in a small middleware in front of it, and give the router a context whose prisma is a fake.

What must be tested

This list combines backend/docs/TESTING.md with what the existing test files cover:
  • Auth guards. Missing token, bad token, wrong role, each lane.
  • Tenancy. A principal of studio A cannot read or change studio B.
  • Money and limits. Plan limits, seat counts, subscription state changes, anything that calls the payment provider.
  • Message sending. Anything that pushes, sends WhatsApp or email. Assert that silent paths stay silent.
  • Webhooks. A bad token or signature is rejected, and a valid delivery repeated twice is applied once.
  • Idempotent jobs. Running a scheduler function twice with the same now changes nothing the second time.
  • A regression test for a fixed bug. Several files in the folder exist for one specific failure, for example app-trust-proxy.test.ts and archived-studio-quiet.test.ts.
There is no enforced coverage percentage.

Database migrations

Migrations are hand written SQL and are applied with prisma migrate deploy. Before applying one to a shared environment, replay the whole migration folder against a throwaway local Postgres to prove it applies cleanly from scratch. pnpm db:migrate:status shows what a database has applied.

Smoke scripts

A few scripts exercise a running API or a real provider. They are not part of pnpm test.

Gates

ESLint enforces a few project rules through the local perform-internal plugin: no-direct-process-env, no-em-dash, and no-loose-version-pins, which flags caret and tilde ranges in the package.json files it is configured for. Some dependencies in apps/core-api/package.json still use a caret range.
Typecheck and tests depend on the built dist of workspace packages. After pulling changes to @repo/database, @perform/queue or @perform/smartsend, errors such as a missing Prisma model or queue name usually mean a stale build. Rebuild the package and try again.