Testing and delivery
EverDuet treats tests as the shipping contract, but “run everything constantly” is not the strategy. The goal is fast local confidence and a complete release gate with bounded memory.
The feedback pyramid
while editing bun run test:changed
focused related tests + owning typechecks
before PR bun run test:ci
lint + prod/test types + coverage units + builds
on Coolify PR isolated ephemeral PostgreSQL + Redis
same verification + migrations + BullMQ canary
after merge production health + affected behaviorEach layer answers a different question. A unit test cannot prove a migration applies to PostgreSQL; a production smoke test should not be the first place a pure domain bug is discovered.
Unit tests stay deterministic
Web and admin Jest suites run at most two workers. Coverage is serial because multiple instrumented workers can create large memory peaks on a small builder. Admin tests default to Node and opt into jsdom only for components.
/** @jest-environment node */
test('claims a pending invitation once', async () => {
// exercise domain behavior without mounting a browser
})Separating behavior tests from TypeScript validation keeps the ordinary unit loop quick while the release gate still requires both.
Typecheck production and tests separately
The repository has two explicit contracts:
- production source compiles under strict TypeScript;
- Jest helpers, mocks, Playwright fixtures, and E2E configuration compile too.
bun run typecheck
bun run typecheck:testThis catches a common blind spot: tests can pass at runtime while their mocks have drifted from production types, especially when transforms skip full type analysis.
Changed-file selection is code
scripts/test-changed-plan.mjs maps file boundaries to validation steps and has its own unit tests.
Examples:
| Change | Selected work |
|---|---|
| Web component | related Jest tests + web production/test types |
packages/core | package units + core typecheck + dependent app types |
| Prisma schema | client generation + DB integration prerequisites |
| Playwright config | E2E typecheck + credential-boundary notice |
| Root quality config | complete unit and typecheck gates |
The selection logic is deterministic and reviewable, not a collection of shell guesses.
Contract tests protect the public boundary
The API client is generated from OpenAPI. Tests verify that:
- operation IDs match;
- every schema reference resolves;
- request values validate before transport;
- success and error responses validate at runtime;
- generated source is current.
This makes the public API an architectural exercise with an enforceable contract rather than a second undocumented route style.
Database integration tests use real PostgreSQL
Prisma mocks are useful for domain branching, but cannot prove triggers, partial uniqueness, transaction locking, or SQL backfills. The integration tier runs against disposable PostgreSQL and is selected for schema and relationship-integrity changes.
The Coolify PR gate
Every trusted same-repository PR receives an isolated Compose stack:
check container
├── lint
├── production + test typechecks
├── unit/package/script tests
├── web + admin + worker + docs builds
├── Prisma migrations on ephemeral PostgreSQL
└── BullMQ canary on ephemeral RedisPostgreSQL and Redis use temporary storage. No production secrets or domains enter the preview stack.
Why GitHub Actions are manual fallbacks
The project is hosted on its own Coolify machine. Running the exact container release gate there avoids paid runner usage and reduces “CI passed in a different environment” drift. Focused GitHub Actions remain available for manually dispatched production smoke or database operations.
A useful test asks about risk
Before adding a test, name the failure it prevents:
- pure rule regression: unit test;
- component state/copy regression: component test;
- route contract regression: route test;
- database concurrency/invariant: PostgreSQL integration test;
- auth and browser integration: focused E2E;
- container/release ordering: Compose health gate.
This keeps the suite fast and reliable instead of duplicating every assertion at every layer.
Next: operations on Coolify.