housekeeping-service — TESTING_STRATEGY
Layered tests: domain → application → contract → integration → end-to-end. Coverage gate ≥ 85% lines / ≥ 80% branches for
src/domain/**andsrc/application/**. Mutation testing on the domain quarterly. CI fails on any drop.
1. Test pyramid
┌──────────────────────┐
│ e2e (saga) │ ~10 scenarios
├──────────────────────┤
│ integration (db, │ ~80 scenarios
│ pubsub, sync, ai) │
├──────────────────────┤
│ contract / api │ 100% endpoints
├──────────────────────┤
│ application unit │ hundreds
├──────────────────────┤
│ domain unit │ thousands (fast)
└──────────────────────┘
Tooling: Jest + ts-jest, supertest for HTTP, testcontainers for Postgres + Pub/Sub emulator, node:test for the renderer-side sync simulator.
2. Domain unit tests (tests/unit/domain/**)
Pure, no I/O. Fast (< 10 ms each).
- One spec per aggregate file, mirroring the file structure.
- One spec per state-machine transition (allowed and denied).
- Property-based tests for
TimeWindow,LinenCount, ID factories usingfast-check. - Invariant suites:
housekeeping-task.invariants.spec.tsroom-status.transitions.spec.tscleaning-checklist.frozen.spec.tsinspection.completeness.spec.tslinen-inventory.nonnegative.spec.ts
Coverage target: ≥ 95% lines for src/domain/**.
3. Application unit tests (tests/unit/application/**)
Use cases with in-memory ports (fakes, not mocks). Each use case has:
- A happy-path spec.
- A spec per domain error mapped.
- A concurrency-conflict spec where applicable.
- An idempotency spec (replay returns same outcome).
- An outbox-append spec (asserts subject + payload shape).
Example matrix:
| Use case | Specs |
|---|---|
CreateTaskUseCase | happy, room blocked, room not found, duplicate for room (open task), idempotent replay, outbox append shape |
AssignTaskUseCase | happy, reassign, staff off-duty, transition denied, idempotent, outbox |
CompleteTaskUseCase | happy, mandatory item missing, linen overdraw, room state flip emitted, with-inspection-required path |
RequireMaintenanceUseCase | pause path, terminal path, both emit room.maintenance_required.v1 |
Coverage target: ≥ 85% lines for src/application/**.
4. Contract tests (tests/integration/api-contract.spec.ts)
- Loads
contracts/openapi.yamland asserts every operation responds with the declared shape (usingopenapi-response-validator). - Asserts every error response uses the RFC 7807 envelope and a valid
MELMASTOON.HOUSEKEEPING.*code. - Asserts
Idempotency-Keyenforcement on mutating endpoints. - Asserts ETag emitted on aggregate reads and honoured on
If-Match.
5. Integration tests (tests/integration/**)
Spin up Postgres + Pub/Sub emulator + a fake iam-service JWT issuer with testcontainers. Each suite is fully isolated (own database). Targets:
| Suite | Purpose |
|---|---|
tenant-isolation.spec.ts | Asserts RLS blocks cross-tenant reads/writes/updates on every table. MANDATORY. |
db-schema.spec.ts | Asserts live schema matches migrations/*.sql; rejects drift. |
outbox-relay.spec.ts | Asserts events are published once, in id order, with retry on transient publisher failures. |
inbox-idempotency.spec.ts | Replays the same (topic, message_id) 5× and asserts a single side-effect. |
turnover-saga.spec.ts | End-to-end: publish reservation.checked_out.v1 → assert task created, room flipped, outbox event emitted within 2 s p95. |
early-checkout.spec.ts | Priority bump path. |
mid-stay-modification.spec.ts | reservation.modification.requested.v1 with mid_stay_clean → task created. |
maintenance-roundtrip.spec.ts | requireMaintenance → emits → consume maintenance.work_order.completed.v1 → new post_maintenance task. |
room-status-state-machine.spec.ts | Property-based: random walks over allowed transitions; denied transitions return ROOM_STATE_CONFLICT. |
inspection-flow.spec.ts | Pass + fail paths (re-clean creation). |
linen-low-stock.spec.ts | Crossing low-watermark fires alert exactly once per debounce window. |
staffing-gap.spec.ts | Active shifts vs queued task minutes triggers gap detection with debounce. |
lost-and-found.spec.ts | Recorded → matched → returned; recorded → disposed; retention auto-dispose path. |
concurrency.spec.ts | Two clients reassign same task simultaneously → one wins, one gets 409. |
partition-pruning.spec.ts | Asserts hot reads use partition-pruned plans (EXPLAIN check). |
oidc-pubsub-push.spec.ts | Rejects unsigned/expired/wrong-audience push tokens. |
6. Sync tests (tests/integration/sync/**)
pull-cursor.spec.ts— first pull returns full set; subsequent pulls return only deltas.push-conflict-policies.spec.ts— covers each policy (server_authoritative, lww+diff, append_only, max-of, client_wins_if_newer) with adversarial inputs; assertsaudit_eventsrow written on each conflict.cursor-expiration.spec.ts— old cursor returns410 SYNC_CURSOR_EXPIRED.linen-derivation.spec.ts— pushedlinen_movementsrecomputeon_handcorrectly.op-id-replay.spec.ts— duplicateop_idreturns the original outcome.
7. AI port tests
routing.adapter.spec.ts— request shape, timeout, fallback to empty suggestion.routing-handler.spec.ts— applies suggestion only when no manual edits sincegeneratedAt; otherwise surfaces banner only.- Circuit-breaker spec — opens after 5 consecutive failures, half-opens after 30 s.
8. Performance tests (perf/**, run weekly in staging)
k6 scripts:
board-read.k6.js— 200 active rooms, 50 RPS sustained, asserts p99 < 250 ms.task-mutation.k6.js— 30 RPS mixed assign/start/complete, asserts p99 < 400 ms.turnover-saga.k6.js— 10 events/s, asserts p95 ≤ 2 s.sync-push.k6.js— bursts of 100 ops, asserts p99 < 1 s.
Failures cut a Jira perf-regression ticket and block the next prod release until acknowledged.
9. Resilience tests
- Toxiproxy in front of Postgres / Pub/Sub: latency injection, packet loss, full disconnect — assert proper 503 / retry behaviour and no data loss.
- Outbox relay killed mid-batch — restart processes resume from highest
published_at. - Cloud Run instance kill mid-handler — duplicate inbox row prevents double effect.
10. Mutation testing
Stryker on src/domain/** quarterly. Threshold: ≥ 75% mutation score. Tracking ticket cut for any survived mutants.
11. Static analysis
- TypeScript strict mode,
noUncheckedIndexedAccess. - ESLint with
@typescript-eslint,eslint-plugin-import,eslint-plugin-security. tsc --noEmitin CI.- Prettier for formatting.
madge --circularto forbid circular module deps.npm audit --production+ Renovate for dep upgrades.gitleaksin pre-commit.
12. CI gates (block merge)
| Gate | Threshold |
|---|---|
| Lint | 0 errors |
| Type check | 0 errors |
| Domain unit tests | 100% pass, ≥ 95% lines, ≥ 90% branches |
| Application unit tests | 100% pass, ≥ 85% lines, ≥ 80% branches |
| Contract tests | 100% pass |
Integration core (tenant-isolation, outbox-relay, inbox-idempotency, turnover-saga, room-status-state-machine) | 100% pass |
| OpenAPI ↔ controllers diff | empty |
| Migration up + down on a fresh DB | success |
gitleaks | 0 leaks |
Nightly extended job runs the rest of the integration suite + sync + perf smoke. Weekly run executes full perf + Stryker.
13. Local testing
npm test runs unit + contract. npm run test:int runs integration with testcontainers (requires Docker). npm run test:perf:smoke runs a 60-second k6 smoke against localhost.
14. Cross-link
- Domain types under test:
DOMAIN_MODEL.md. - Use cases under test:
APPLICATION_LOGIC.md. - Contract source:
API_CONTRACTS.md. - Sync surface:
SYNC_CONTRACT.md. - Failure scenarios fed back into tests:
FAILURE_MODES.md.