Seed & test
Seeding the dev database and the test patterns that actually catch regressions.
Seed
cd apps/server && bun run seedThe seed lives in apps/server/scripts/seed.ts. It creates:
- A super-admin user (
[email protected]/admin1234). - One organization with the admin as owner.
- The default role rows (Owner / Admin / Member) with a permission grant per role drawn from the
@app/sharedcatalog.
Re-running the seed is idempotent: existing rows are upserted, not duplicated.
For local feature development, a seed grants you a logged-in admin in seconds — no need to walk through the register flow each time.
Test layers
The boilerplate runs three test layers, each exercising a different surface:
Domain & application — Vitest, in-memory
Pure unit tests for aggregates and use cases. No Prisma, no Express. Inject in-memory fakes for repositories and the event bus.
test('User.create rejects empty email', () => {
const result = User.create({ email: '', /* ... */ });
expect(result.isErr()).toBe(true);
expect(result.error.code).toBe('USER_EMAIL_INVALID');
});Run: bun run test --filter=@app/server (Vitest). These should be the largest tier.
Infrastructure — Vitest with a real DB
Repository tests run against a real Postgres (or sqlite for speed if your queries are portable). They're worth the extra friction for the mapping layer — the row-to-aggregate translation is where row-shape regressions hide.
The events module also runs with the real InMemoryEventBus rather than a stub so subscribe/publish ordering is exercised.
HTTP — supertest
Mount the actual router with a test container. Assert status codes, response shapes, and behavioral pairs:
- "user without grant gets 403, user with grant gets 200"
- "missing field returns 422 with
issues[].pathpointing at the missing key" - "second POST with the same idempotency key returns the original response, not a duplicate"
These tests are valuable when each test couldn't pass if the production code stopped doing the right thing.
End-to-end — Playwright
tests/ at the repo root. Boots the full app and clicks through real flows: register, log in, create an organization, invite a member. Slow; reserve them for journeys you must not break.
What "challenging" means
The CLAUDE.md rule: a test that wouldn't fail when the logic regresses isn't worth writing.
Coverage theater looks like:
test('createUser returns user', async () => {
const result = await createUser({ email: '[email protected]', name: 'A', password: 'p' });
expect(result.isOk()).toBe(true);
});If the use case starts returning the wrong user, this test still passes. Replace it with one that asserts the user was actually persisted (via the fake repo), or one that asserts the user.created event fires with the right payload.
BullMQ end-to-end
Skip ioredis-mock for BullMQ — it doesn't run BullMQ's Lua. Either:
- Bring up a real Redis (Docker or Railway-staging) and run the suite there, or
- Unit-test the producer (asserts the wrapper called
queue.addwith the right payload) and the processor (asserts it does the right thing for a given job) separately.
Most of the time option 2 is enough.
Locale and i18n
When testing a controller that returns translated copy, set Accept-Language on the test request. The server-side i18n runtime in apps/server/src/i18n/ resolves the dictionary from headers; tests should not hit the default locale by accident.