Testing
Keelmint is tested at three levels, and CI runs all of them on every push. Read this when you add a feature (what to test, where) or when a run fails and you need to know what it needs.
Unit tests
pnpm nx test <project> # one projectpnpm nx run-many -t test # everythingpnpm nx affected -t test # what your change touchesSpec files sit next to the code (*.spec.ts), with Jest; client components use jest-preset-angular. No database or Docker needed: Prisma is stubbed.
API end-to-end tests
docker compose up -d db mailpitpnpm nx e2e keelmint-api-e2eReal HTTP against the built API and a real Postgres, using the separate <product>_test database (TEST_DATABASE_URL), so a run never touches development data. The suite applies migrations, starts the server itself on port 3001 (PORT overrides), truncates tables between tests, reads mail from Mailpit, and runs fake Google and Stripe servers, so it needs no external accounts.
Each product has its own suite: pnpm nx e2e <product>-api-e2e.
Tenant isolation
apps/keelmint/api-e2e/src/api/tenant-isolation.spec.ts creates two organizations with data and holds one table row per organization-scoped route: ids from the other organization must answer 404, and lists must never contain them. A route-coverage guard at the bottom fails when a route the app registers is in neither a row nor an ALLOWED entry with its reason. Add a row for every organization-scoped route you write.
Database integration specs
Specs named *.int.spec.ts (tenant scoping, unit scope, migrations, the seed) run against a real, migrated test database and only when asked:
DB_INTEGRATION=1 pnpm nx run-many -t test -p core-db keelmint-db keelmint-api-todosDB_INTEGRATION is part of the test cache key, so a cached run without it never counts as a run with it.
Load testing
To measure how the API behaves with tens of thousands of members (the figures in Tenant data), seed one large organization into a throwaway database and run the load script against an API pointed at it. Never use keelmint or keelmint_test.
-
Create an empty database whose name has
loadas a token (keelmint_load,load,load_2; notworkload) and migrate it:Terminal window SKIP_PRODUCT_DOTENV=1 DATABASE_URL=postgres://postgres:postgres@localhost:5433/keelmint_load \pnpm exec prisma migrate deploy --config libs/keelmint/db/prisma.config.ts -
Seed it (same two variables):
pnpm nx run keelmint-db:seed-load -- --members=20000 --slug=load. The seed refuses production, non-local hosts and any database whose name lacks theloadtoken, unlessLOAD_ALLOW_DB=<exact database name>is set. Options (each also aLOAD_<NAME>variable):--slug(defaultload, also the email prefix),--members(20000),--share(fraction of members in the big unit subtree, 0.5),--depthand--breadth(4 and 5: 781 units),--todosper member (5),--audit(5000),--tickets(3000).LOAD_PASSWORDsets the accounts’ password (defaultLoadTest!2026). The accounts are<slug>-0@load.local(organization admin),-1(unit admin of the big root unit) and-2(member). It prints the big root unit’s id. A finished slug is left alone; a seed that was interrupted fails on the next run: drop the database and start again. -
Start the API on another port against that database, with
THROTTLE_LIMIThigh (login is rate limited). A unit admin of a large subtree needsNODE_OPTIONS=--max-http-header-size=262144(see Configuration). -
Run
tools/load/run.ts(autocannon) with ts-node:Terminal window ts-node --transpile-only -O '{"module":"commonjs","esModuleInterop":true,"ignoreDeprecations":"6.0"}' \tools/load/run.ts --unit=<big root unit id> [--base=http://localhost:3100] [--slug=load] \[--seconds=10] [--connections=10] [--session-cap=10] [--out=results.json]It logs in as the three accounts and hammers the member list, todos, audit log, chart, helpdesk queue and token refresh, printing requests per second and latencies. It uses one session per connection and the API keeps at most
MAX_ACTIVE_SESSIONS_PER_USER(default 10) per user, so--connectionsmay not exceed--session-cap; raise both together with that variable to test more connections.LOAD_PASSWORDmust match the seed’s.
After any bulk import (this seed, or your own) run ANALYZE on the database: with stale statistics Postgres can pick a bad plan and a query that should take milliseconds stalls.
CI
.github/workflows/ci.yml runs on every push and pull request: a format check, then lint, test, build, typecheck and e2e through Nx, with Postgres and Mailpit as services, and a job that generates a fresh product and runs its e2e suite. It is configured for Nx Cloud task distribution; remove the nx start-ci-run step if you don’t use Nx Cloud. The release workflow (images to GHCR on a version tag) is described in Deploy.