# Per-domain Test Safety Net — Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. This plan is also structured to run as a **Workflow** (one agent per domain in Phase B) — see "Execution vehicle" at the end. **Goal:** Give every domain across `apps/api`, `apps/worker`, and `apps/admin` a measured, near-100% test safety net (gap-fill + one lifecycle test per domain), and log/triage any bugs found. **Architecture:** Three-phase. **Phase A** lays the foundation you can't measure without: coverage tooling in all three workspaces and a per-test DB reset that isolates domain suites. **Phase B** sweeps each domain with the same repeatable procedure (measure → fill to near-100% → add one lifecycle test → record checklist → log bugs). **Phase C** synthesizes: aggregate coverage, finalize the living checklist doc, update tracking. **Tech Stack:** Vitest 3 + `@vitest/coverage-v8` (v8 provider). API: Hono + better-auth + Drizzle/libsql, node env, real file DB, serial. Clients: Vite + React + Testing Library, jsdom env. Yarn 4 workspaces. oxlint + oxfmt. ## Global Constraints - **Coverage target:** near-100% per domain. Uncovered lines are either tested or annotated `/* c8 ignore next -- */` with a real reason. **No silent gaps.** No CI threshold *gate* (enforced by review for now). - **Vitest version:** all three workspaces are on `vitest@^3.0.0` → coverage package must be `@vitest/coverage-v8@^3`. - **API harness invariant:** migration runs once at top level in `apps/api/test/setup.ts` *before* the libsql file is opened (Windows EBUSY constraint) — do not move migration into `beforeAll`/`beforeEach`. Per-test isolation is done by **deleting rows**, never by re-migrating or unlinking the file. - **Test style (API):** `createApp()` per test; auth via `authToken(app, email, role)` → `bearer(token)`; activities via `seedActivity(name, insoleTypes?)`; deterministic timing by back-dating rows with `db.update(workSessions).set({ startTime: new Date(Date.now() - N) })`. Mirror `apps/api/test/sessions.test.ts`. - **Test style (clients):** Testing Library + jsdom; mirror the existing `*.test.tsx` in the same workspace for how query hooks/fetch are mocked. Do not invent a new mocking approach. - **Bug handling:** every bug found → a Plane work item (child of the epic from Task 3). Trivial/safe fix → apply inline in the same commit as the test that exposes it, and note it in the commit body. Risky/behavior-changing → leave a **failing or `.skip`-with-reason test**, file the Plane ticket, do **not** change behavior. Flag which in the domain's report. - **Commits:** one commit per domain task (Phase B) and per foundation task (Phase A). oxfmt changed files before committing; keep `npx oxlint` clean. - **Tracking:** SoleLog in **Plane** (project id `1956d3d3-15ed-4850-93d8-96d5fb17f213`), not Jira. Docs live in `docs/`. Never touch `.env*`, `docker-compose*`, `.mcp.json`, `CLAUDE.local.md`. --- ## Phase A — Foundation ### Task 1: Coverage tooling in all three workspaces **Files:** - Modify: `apps/api/package.json`, `apps/api/vitest.config.ts` - Modify: `apps/worker/package.json`, `apps/worker/vitest.config.ts` - Modify: `apps/admin/package.json`, `apps/admin/vitest.config.ts` **Interfaces:** - Produces: a `test:coverage` script in each workspace that emits a text + json-summary report. Phase B tasks consume `yarn workspace test:coverage` to find uncovered lines. - [ ] **Step 1: Install the matching coverage provider (all three workspaces)** ```bash yarn workspace @solelog/api add -D @vitest/coverage-v8@^3 yarn workspace @solelog/worker add -D @vitest/coverage-v8@^3 yarn workspace @solelog/admin add -D @vitest/coverage-v8@^3 ``` - [ ] **Step 2: Add coverage config to the API vitest config** Replace `apps/api/vitest.config.ts` with: ```ts import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { environment: 'node', setupFiles: ['./test/setup.ts'], fileParallelism: false, coverage: { provider: 'v8', all: true, reporter: ['text', 'json-summary'], include: ['src/**'], // Generated / non-logic files: excluded with intent (not silent gaps). exclude: ['src/db/schema.ts', 'src/index.ts', '**/*.d.ts'], }, }, }); ``` - [ ] **Step 3: Add coverage config to the worker AND admin vitest configs** In BOTH `apps/worker/vitest.config.ts` and `apps/admin/vitest.config.ts`, add a `coverage` block inside `test` (keep the existing `environment`/`globals`/`setupFiles`/`plugins`): ```ts coverage: { provider: 'v8', all: true, reporter: ['text', 'json-summary'], include: ['src/**'], exclude: ['src/main.tsx', 'src/vite-env.d.ts', 'src/test/**', '**/*.d.ts'], }, ``` - [ ] **Step 4: Add the `test:coverage` script to all three `package.json`** In each of `apps/api`, `apps/worker`, `apps/admin` package.json `scripts`, add: ```json "test:coverage": "vitest run --coverage", ``` - [ ] **Step 5: Run coverage in each workspace and confirm it produces a report** Run: ```bash yarn workspace @solelog/api test:coverage yarn workspace @solelog/worker test:coverage yarn workspace @solelog/admin test:coverage ``` Expected: all suites still green; a coverage table prints; `coverage/coverage-summary.json` is written in each workspace. Note the per-file % — this is the Phase B work-list. - [ ] **Step 6: Ignore the coverage output dir** Confirm `coverage/` is git-ignored (add `coverage/` to root `.gitignore` if not already). Never commit the report. - [ ] **Step 7: Commit** ```bash git add apps/*/package.json apps/*/vitest.config.ts .gitignore yarn.lock git commit -m "test: add @vitest/coverage-v8 + test:coverage to all workspaces" ``` --- ### Task 2: Per-test DB isolation for the API suite **Files:** - Modify: `apps/api/test/helpers.ts` (add `resetDb`) - Modify: `apps/api/test/sessions.test.ts` (adopt it as proof — removes reliance on accumulated state) **Interfaces:** - Produces: `export async function resetDb(): Promise` — deletes all rows from `workSessions`, `activities`, and the better-auth tables (`session`, `account`, `user`, `verification`), leaving the migrated schema intact. Phase B API domain tasks call it in `beforeEach`. - [ ] **Step 1: Write a failing test for `resetDb`** Create `apps/api/test/reset-db.test.ts`: ```ts import { describe, it, expect, beforeEach } from 'vitest'; import { db } from '../src/db/client'; import { workSessions, user } from '../src/db/schema'; import { resetDb, seedActivity, authToken } from './helpers'; import { createApp } from '../src/app'; describe('resetDb', () => { beforeEach(resetDb); it('clears domain and auth rows so counts start from zero', async () => { const app = createApp(); await authToken(app, 'reset-a@example.com'); await seedActivity('Zagen'); expect((await db.select().from(user)).length).toBeGreaterThan(0); await resetDb(); expect(await db.select().from(user)).toHaveLength(0); expect(await db.select().from(workSessions)).toHaveLength(0); }); }); ``` - [ ] **Step 2: Run it to verify it fails** Run: `yarn workspace @solelog/api test reset-db` Expected: FAIL — `resetDb` is not exported from `./helpers`. - [ ] **Step 3: Implement `resetDb` in `helpers.ts`** Add these imports and function to `apps/api/test/helpers.ts` (extend the existing schema import): ```ts import { activities, session, account, user, verification, workSessions } from '../src/db/schema'; // Wipe all data rows (schema stays; migration already ran once in setup.ts). // Order respects FKs: work sessions → sessions/accounts → users; activities last. export async function resetDb(): Promise { await db.delete(workSessions); await db.delete(session); await db.delete(account); await db.delete(verification); await db.delete(user); await db.delete(activities); } ``` - [ ] **Step 4: Run it to verify it passes** Run: `yarn workspace @solelog/api test reset-db` Expected: PASS. - [ ] **Step 5: Adopt `resetDb` in `sessions.test.ts` as proof of isolation** At the top of `apps/api/test/sessions.test.ts`, add `beforeEach` to the imports and add `beforeEach(resetDb)` inside each `describe` block (`'session lifecycle'` and `'session reads'`). Import `resetDb` from `./helpers`. - [ ] **Step 6: Run the full API suite** Run: `yarn workspace @solelog/api test` Expected: all green (103+ tests). If any test now fails because it depended on another test's rows, that IS the fragility we're removing — fix that test to seed its own data. - [ ] **Step 7: Commit** ```bash git add apps/api/test/helpers.ts apps/api/test/reset-db.test.ts apps/api/test/sessions.test.ts git commit -m "test(api): add resetDb helper + per-test isolation (proof: sessions suite)" ``` --- ### Task 3: Tracking scaffold + living checklist doc **Files:** - Create: `docs/reference/domain-test-checklist.md` (the living "guaranteed behaviors" doc) - Plane: create the epic + one child per domain **Interfaces:** - Produces: Plane epic id (call it `EPIC`) and per-domain child ids; the checklist doc that Phase B appends to. - [ ] **Step 1: Create the Plane epic and per-domain child work items** Using the `plane` MCP tools (project id `1956d3d3-15ed-4850-93d8-96d5fb17f213`): - `create_work_item(name: "Testing & hardening — per-domain safety net")` → this is `EPIC`. - For each domain in Phase B (18 total), `create_work_item(name: "Tests — ", parent: EPIC)`. - [ ] **Step 2: Create the checklist doc skeleton** Create `docs/reference/domain-test-checklist.md`: ```markdown # Domain test checklist — guaranteed behaviors Living definition-of-done. Each domain lists the behaviors its tests guarantee. When you change a domain, its box must still hold (or you update this list on purpose). Baseline established 2026-07-22 (see plan 2026-07-22-per-domain-test-safety-net). ## Coverage baseline (Task 1) | Workspace | Statements | Branches | Functions | Lines | | --------- | ---------- | -------- | --------- | ----- | | api | TBD | TBD | TBD | TBD | | worker | TBD | TBD | TBD | TBD | | admin | TBD | TBD | TBD | TBD | ``` Fill the baseline table now from the Task 1 Step 5 `coverage-summary.json` totals. - [ ] **Step 3: Commit** ```bash git add docs/reference/domain-test-checklist.md git commit -m "docs: domain-test-checklist skeleton + coverage baseline" ``` --- ## Phase B — Per-domain sweep **Domain Procedure (every Phase B task follows these six steps — the mechanics are identical, only the inputs in each task differ):** 1. **Measure.** `yarn workspace test:coverage` and read the uncovered lines for this domain's src files (listed per task). That uncovered list + the task's behavior checklist = the exact work-list. 2. **Write one failing test per gap/behavior.** For each unchecked behavior and each uncovered branch: add a test (unit or route-level) mirroring the workspace's existing style. Run it, confirm it fails for the right reason (`.only` while iterating). 3. **Add the domain's one lifecycle test** (specified per task). 4. **Make green.** If a test fails because the code is *wrong* (not because the test is): it's a **bug** → Global Constraints "Bug handling". Otherwise the code is already correct and the test just locks it in. 5. **Verify near-100%.** Re-run coverage for this domain's files. Remaining uncovered lines must be either tested or `/* c8 ignore next -- */`. Run `npx oxfmt` on changed files; `npx oxlint` clean. 6. **Record + commit.** Append a `## ` section to `docs/reference/domain-test-checklist.md` with the guaranteed-behavior bullets. Commit test+doc (+ inline trivial fix) as one commit. Update the domain's Plane child to Done (note bugs found). **Anchor lifecycle test (fully-runnable reference — sets the quality bar for all lifecycle tests):** Create `apps/api/test/integration/worker-session-lifecycle.test.ts`: ```ts import { describe, it, expect, beforeEach } from 'vitest'; import { eq } from 'drizzle-orm'; import { createApp } from '../../src/app'; import { db } from '../../src/db/client'; import { workSessions } from '../../src/db/schema'; import { authToken, bearer, seedActivity, resetDb } from '../helpers'; describe('worker session lifecycle (start→pause→resume→stop→history→export)', () => { beforeEach(resetDb); it('excludes paused time, shows in history, and exports a CSV row', async () => { const app = createApp(); const token = await authToken(app, 'lifecycle@example.com'); const activityId = await seedActivity('Frezen', ['Kurk']); // start const startRes = await app.request('/api/sessions/start', { method: 'POST', headers: bearer(token), body: JSON.stringify({ activity_id: activityId, insole_type: 'Kurk', pair_count: 3 }), }); expect(startRes.status).toBe(200); const started = await startRes.json(); // pause, then backdate the pause start by 4s so resume accumulates a real span await app.request(`/api/sessions/${started.id}/pause`, { method: 'POST', headers: bearer(token) }); await db .update(workSessions) .set({ pausedAt: new Date(Date.now() - 4000) }) .where(eq(workSessions.id, started.id)); const resumeRes = await app.request(`/api/sessions/${started.id}/resume`, { method: 'POST', headers: bearer(token), }); expect((await resumeRes.json()).paused_seconds).toBeGreaterThan(0); // backdate the start by 10s so worked-time is deterministic-ish, then stop await db .update(workSessions) .set({ startTime: new Date(Date.now() - 10000) }) .where(eq(workSessions.id, started.id)); const stopRes = await app.request(`/api/sessions/${started.id}/stop`, { method: 'POST', headers: bearer(token), }); const stopped = await stopRes.json(); expect(stopped.status).toBe('completed'); expect(stopped.paused_seconds).toBeGreaterThan(0); expect(stopped.duration_seconds).toBeLessThan(10); // paused time excluded const wall = Math.round((Date.parse(stopped.end_time) - Date.parse(stopped.start_time)) / 1000); expect(stopped.duration_seconds + stopped.paused_seconds).toBe(wall); // history shows it const hist = await (await app.request('/api/sessions', { headers: bearer(token) })).json(); expect(hist).toHaveLength(1); expect(hist[0].id).toBe(started.id); expect(hist[0].activity_name).toBe('Frezen'); // export contains the completed row const csvRes = await app.request('/api/export', { headers: bearer(token) }); expect(csvRes.status).toBe(200); const csv = await csvRes.text(); expect(csv).toContain('Frezen'); expect(csv.trim().split('\n').length).toBe(2); // header + 1 row }); }); ``` > For every OTHER lifecycle test below, the task gives the exact endpoint chain and assertions; write it in this same shape, mirroring the referenced existing test file for request/mock mechanics. Put API integration tests in `apps/api/test/integration/`, client ones next to the screen they exercise. --- ### API domains Coverage command for all API tasks: `yarn workspace @solelog/api test:coverage`. ### Task B-A0: Infra (health, db, schema, seed, env, migrate) **Files:** src `src/routes/health.ts`, `src/db/client.ts`, `src/db/migrate.ts`, `src/env.ts`; tests `apps/api/test/{health,db,schema,seed}.test.ts`. **Behavior checklist:** - `/health` returns `{ status: 'ok' }` (exists). - `env.ts` applies each dev default when the var is unset, and reads the override when set. - `runMigrations()` is idempotent (running twice does not throw). - seed creates the dev admin + worker with the expected roles (exists). **Lifecycle test:** none (infra). Focus: fill `env.ts` branch coverage (each default), add an idempotent-migration test. **Acceptance:** near-100% on `env.ts`, `health.ts`; `db/client.ts`/`migrate.ts` covered or `c8 ignore` on unreachable error branches. ### Task B-A1: Auth & `/me` **Files:** src `src/auth.ts`, `src/routes/me.ts`, `src/lib/require-user.ts`, CORS in `src/app.ts`; tests `apps/api/test/{auth,me,cors}.test.ts`. **Behavior checklist:** - sign-in returns a bearer token; sign-in with wrong password fails. - `/api/me` 401 without token; returns `{ user: { id, email, name, role } }` with token. - `getSessionUser` maps role (default `'worker'`) and `banned`. - `adminGuard`: 401 no session, 403 banned or non-admin, passes for active admin. - `isAdmin` true only for role `admin`. - CORS: allowed origin echoed; preflight OPTIONS handled. **Lifecycle test:** `apps/api/test/integration/auth-lifecycle.test.ts` — create worker → sign in → call `/api/me` (200, role worker) → call an admin route (403) → assert bearer works across requests. **Acceptance:** near-100% on `require-user.ts`, `me.ts`; `auth.ts` config covered by the round-trips (ignore untestable option branches with a reason). ### Task B-A2: Activities **Files:** src `src/routes/activities.ts`; test `apps/api/test/activities.test.ts`. **Behavior checklist:** - GET list (public/authed per current behavior) ordered by `sort_order`. - POST/PUT/DELETE are admin-only (worker → 403; no token → 401). - create validates name non-empty (400) and defaults `insole_types` to all three. - reorder endpoint sets `sort_order` from the `ids` array; rejects empty array (400). - `insole_types` round-trips as a subset. **Lifecycle test:** `activities-lifecycle` — admin creates 3 activities → reorders them → worker GET reflects new order → admin deletes one → gone from list. **Acceptance:** near-100% on `activities.ts`. ### Task B-A3: Work-sessions **Files:** src `src/routes/sessions.ts`, `src/lib/work-session.ts`; test `apps/api/test/{sessions,work-session}.test.ts` (already deep — expect mostly gap-fill + the anchor lifecycle test). **Behavior checklist:** start (200/400/404/401), pause (200/404/409 double), resume (200/409 running/409 not-paused), stop (duration excludes paused; 409 double), discard (200; duration null), ownership scoping (B can't touch A → 404), reads (history newest-first + activity join; active-only; per-user scoping), `toWorkSession` mapper shapes every field incl. `paused_at`. **Lifecycle test:** the anchor `worker-session-lifecycle.test.ts` above (create it here). **Acceptance:** near-100% on `sessions.ts`, `work-session.ts`. ### Task B-A4: Worker export / CSV **Files:** src `src/lib/csv.ts` (+ `/api/export` in `sessions.ts`); tests `apps/api/test/{csv,export}.test.ts`. **Behavior checklist:** - `buildSessionsCsv(rows, { includeWorker:false })` byte-shape unchanged (worker export). - `includeWorker:true` prepends a Worker column. - quoting escapes commas/quotes/newlines in fields. - `formatDuration` renders H:MM:SS (or the current format) incl. zero and paused columns. - `/api/export` 401 without token; only `completed` rows; ordered by start time; filename header. **Lifecycle test:** covered by the worker-session anchor (export step). Add a CSV-escaping edge unit test (activity name with a comma + quote). **Acceptance:** near-100% on `csv.ts`. ### Task B-A5: Admin sessions **Files:** src `src/routes/admin.ts` (session parts: cross-user list, active, manual create/edit, stop, discard, users roster); test `apps/api/test/admin.test.ts`. **Behavior checklist:** - every `/api/admin/*` route: 401 no token, 403 worker. - GET cross-user sessions includes `user_name`/`user_email`; active-only variant. - manual create (`CreateManualSessionInput`) computes duration from start/end; validation 400. - edit (`AdminUpdateSessionInput`) updates fields incl. status; 404 missing. - admin stop/discard of a running session; 409 when already closed. - users roster returns id/name (used by pickers). **Lifecycle test:** `admin-worker-session.test.ts` — worker starts a session → admin stops it via the admin endpoint → session appears in `/api/admin/report` and `/api/admin/export` with correct totals. (Match request/response shapes to `admin.test.ts` and `report.test.ts`.) **Acceptance:** near-100% on the session parts of `admin.ts`. ### Task B-A6: Reports + admin export **Files:** src `src/routes/admin.ts` (report/export parts: `parseReportQuery`, `buildSessionFilters`, `/api/admin/report`, `/api/admin/export`); tests `apps/api/test/{report,export}.test.ts`. **Behavior checklist:** - report: completed-only; date-range + user/type/activity filters; totals (worked/paused/pairs/sessions); by_worker/by_activity/by_type sorted worked desc; empty range → zeros; null insole_type bucketed `"Onbekend"`. - admin export: all-users CSV with Worker column; filename `solelog-report__.csv`; honors the same filters. - 401/403 gating. **Lifecycle test:** `report-aggregation.test.ts` — seed known completed sessions for 2 workers × 2 activities → assert every total and each breakdown row exactly → apply a worker filter → assert it narrows. **Acceptance:** near-100% on the report/export parts of `admin.ts`. ### Task B-A7: Admin users **Files:** src `src/routes/admin-users.ts`; test `apps/api/test/admin-users.test.ts` (already strong — gap-fill + lifecycle). **Behavior checklist:** list (role+status, ordered by name); create (409 dup; mixed-case email re-select; 400 invalid; password floor 8); set role (self-demote guard, last-admin guard, 404); deactivate (bans + deletes sessions; self + last-admin guards; 404); reactivate (clears ban); reset password (400 invalid; new works / old fails); `activeAdminsExcluding` unit; all routes gated (401/403 incl. banned admin). **Lifecycle test:** `user-lifecycle.test.ts` — admin creates worker → worker signs in (token works) → admin deactivates → worker sign-in blocked **and** the pre-issued token no longer authorizes `/api/me` (session rows deleted) → admin reactivates → worker signs in again. **Acceptance:** near-100% on `admin-users.ts`. --- ### Worker client domains Coverage command: `yarn workspace @solelog/worker test:coverage`. Mirror existing `*.test.tsx` for mocking. ### Task B-W1: Auth (context/storage/login) **Files:** src `src/auth/AuthContext.tsx`, `src/lib/auth-storage.ts`, `src/screens/Login.tsx`; tests alongside. **Behavior checklist:** storage set/get/clear token; AuthContext exposes user + login/logout, restores token on mount, clears on 401; Login submits credentials, shows error on failure, disables submit while pending. **Lifecycle test:** `Login` → successful submit stores token and flips context to authenticated (app renders the tabbed shell). **Acceptance:** near-100% on `auth-storage.ts`, `AuthContext.tsx`, `Login.tsx`. ### Task B-W2: Stopwatch + reconcile **Files:** src `src/screens/Stopwatch.tsx`, `src/lib/stopwatch.ts`; tests alongside (`Stopwatch.test.tsx` exists — mirror its hook mocking). **Behavior checklist:** zool select filters handelingen and clears chosen activity; pair-count stepper floor 1 + text entry; start disabled until an activity is chosen; tap display starts/pauses/resumes; discard double-tap arm/confirm window; adopts a server-active session when idle; 409 on stop/discard resets the timer; `elapsedSeconds`/`formatTime` math incl. paused accumulator. **Lifecycle test (reconcile):** running locally, the active-sessions poll returns a list **without** the local session → the timer resets and the "door de beheerder gestopt" notice shows. (Drive by mocking `useActiveSessions` return across renders, as `Stopwatch.test.tsx` already does.) **Acceptance:** near-100% on `stopwatch.ts`, `Stopwatch.tsx`. ### Task B-W3: History **Files:** src `src/screens/History.tsx`, `src/lib/export.ts`; tests alongside. **Behavior checklist:** renders sessions newest-first with activity name + duration; shows the paused line when `paused_seconds>0`; empty state; export triggers a download (blob/object-URL path in `export.ts`); loading/error states. **Lifecycle test:** list renders a completed session with a paused line, then the export action calls the download path. **Acceptance:** near-100% on `History.tsx`, `export.ts`. ### Task B-W4: Account **Files:** src `src/screens/Account.tsx`; test alongside. **Behavior checklist:** shows the signed-in user; logout clears auth and returns to Login. **Lifecycle test:** click logout → context becomes unauthenticated → Login shows. **Acceptance:** near-100% on `Account.tsx`. ### Task B-W5: lib/api + app/nav **Files:** src `src/lib/api.ts`, `src/App.tsx`, `src/components/TabBar.tsx`, `src/api/*.ts`; tests alongside. **Behavior checklist:** `apiFetch` attaches bearer, parses JSON, throws `ApiError` with status on non-2xx, handles empty body; `App` routes between tabs by auth state; TabBar switches active screen; each `api/*` hook hits the right path/method. **Lifecycle test:** unauthenticated `App` shows Login; authenticated shows Stopwatch with the tab bar. **Acceptance:** near-100% on `api.ts`, `App.tsx`, `TabBar.tsx`, `api/*.ts`. --- ### Admin client domains Coverage command: `yarn workspace @solelog/admin test:coverage`. Mirror existing `*.test.tsx`. ### Task B-D1: Auth (admin-only gate) **Files:** src `src/auth/AuthContext.tsx`, Login; tests alongside. **Behavior checklist:** login as admin succeeds; login as **worker is rejected** (admin-only gate via `role` on `/api/me`); token persisted/restored; logout clears. **Lifecycle test:** worker credentials → rejected with the not-authorized message; admin credentials → shell renders. **Acceptance:** near-100% on admin `AuthContext.tsx`, `Login`. ### Task B-D2: Live view **Files:** src `src/screens/Live.tsx`, `src/lib/elapsed.ts`; tests alongside. **Behavior checklist:** renders active sessions with worker + activity + running elapsed; auto-refresh interval wired; paused card state; empty state; `elapsed` math. **Lifecycle test:** active-sessions data renders cards; a paused session shows the paused treatment. **Acceptance:** near-100% on `Live.tsx`, `elapsed.ts`. ### Task B-D3: Sessions + SessionForm **Files:** src `src/screens/Sessions.tsx`, `src/components/SessionForm.tsx`; tests alongside. **Behavior checklist:** list + status filter; create/edit form validation (times, pair count, activity); row actions stop/discard; disabled/pending states; error surfacing. **Lifecycle test:** open create form → fill valid manual session → submit → appears in the list; edit it → change persists. **Acceptance:** near-100% on `Sessions.tsx`, `SessionForm.tsx`. ### Task B-D4: Reports + date-range **Files:** src `src/screens/Reports.tsx`, `src/api/reports.ts`, `src/lib/date-range.ts`; tests alongside. **Behavior checklist:** period presets (week/month/all) map to correct ISO ranges; filters change the query; totals + three breakdown tables render; export button triggers download; `filtersToQuery`/date helpers unit-covered; empty state. **Lifecycle test:** pick "Deze maand" + a worker filter → the report query reflects both and tables render the returned rows. **Acceptance:** near-100% on `Reports.tsx`, `reports.ts`, `date-range.ts`. ### Task B-D5: Users **Files:** src `src/screens/Users.tsx`, `src/components/UserForm.tsx`, `src/api/users.ts`; tests alongside. **Behavior checklist:** list with role/status pills; own-row "jij" badge hides role/deactivate; create form validation (email, password min 8); role toggle; inline password reset; deactivate/reactivate; error surfacing (e.g. last-admin 400). **Lifecycle test:** create a worker via the form → appears in the list → toggle to admin → reflected; reset password inline succeeds. **Acceptance:** near-100% on `Users.tsx`, `UserForm.tsx`, `users.ts`. ### Task B-D6: Activities + app/nav **Files:** src `src/screens/Activities.tsx`, `src/api/activities.ts`, `src/App.tsx`, `src/components/Sidebar.tsx`; tests alongside. **Behavior checklist:** handelingen CRUD (create/edit/delete) with insole-type selection; up/down reorder; Sidebar nav highlights active route + routes render; empty/error states. **Lifecycle test:** create an activity → edit its insole types → reorder → delete; Sidebar navigation switches screens. **Acceptance:** near-100% on `Activities.tsx`, admin `api/activities.ts`, `App.tsx`, `Sidebar.tsx`. --- ## Phase C — Synthesis ### Task C1: Aggregate, finalize, and record **Files:** `docs/reference/domain-test-checklist.md`, `docs/roadmap.md`, `docs/sessions/2026-07-22-per-domain-test-safety-net.md`. - [ ] **Step 1: Re-run coverage in all three workspaces and fill the final table** ```bash yarn workspace @solelog/api test:coverage yarn workspace @solelog/worker test:coverage yarn workspace @solelog/admin test:coverage ``` Update the coverage table in `domain-test-checklist.md` with final numbers. Every domain section present. Any file below near-100% must name why (with its `c8 ignore` reasons). - [ ] **Step 2: Full green + lint sweep** ```bash yarn workspace @solelog/api test && yarn workspace @solelog/worker test && yarn workspace @solelog/admin test yarn workspace @solelog/api typecheck && yarn workspace @solelog/worker typecheck && yarn workspace @solelog/admin typecheck npx oxlint ``` Expected: all green, typechecks clean, oxlint exit 0. - [ ] **Step 3: Write the session log** Create `docs/sessions/2026-07-22-per-domain-test-safety-net.md`: goal, per-domain outcomes, coverage before/after, and the **bug list** (each with its Plane id + inline-fixed vs deferred). - [ ] **Step 4: Roadmap note** Add a short "Testing & hardening" note to `docs/roadmap.md` (near Phase 5) recording the per-domain safety net + coverage baseline. - [ ] **Step 5: Close tracking** Set the Plane epic (Task 3 `EPIC`) to Done once all domain children are Done. Leave deferred-bug tickets open. - [ ] **Step 6: Commit** ```bash git add docs/ git commit -m "docs: finalize domain-test-checklist + session log + roadmap (test safety net)" ``` --- ## Execution vehicle Phase A is sequential (tooling → harness → tracking) — run inline or as three serial workflow steps. **Phase B is the fan-out:** each domain task is independent (touches its own test files), so run them as a Workflow — one agent per domain following the Domain Procedure, each returning `{ domain, coverageBefore, coverageAfter, bugs: [{summary, severity, fixed|deferred, planId}], newTests }`. API tasks that add integration files under `apps/api/test/integration/` share no source with client tasks, so all 18 can pipeline; the only shared file is `docs/reference/domain-test-checklist.md`, so either (a) have each agent return its checklist section as data and append them in Phase C, or (b) run with worktree isolation. Phase C synthesizes: aggregate coverage, write the doc/session log, flip Plane. Then independently verify (git log + full suites + coverage + typechecks + oxlint) before reporting. ## Self-review notes - Spec §1 coverage tooling → Task 1. §2 harness → Task 2. §3 domain map (7 API + 5 worker + 6 admin = 18) → Tasks B-A0..A7 (8, incl. infra), B-W1..W5 (5), B-D1..D6 (6). §4 lifecycle tests → one per domain (anchor fully coded; others specified). §5 Plane → Task 3 + per-task close. §6 deliverables → Phase C. §7 workflow → Execution vehicle. - Near-100% target enforced per-task (Acceptance) with `c8 ignore`-with-reason as the only escape. - Note: API infra is split out as B-A0 so the "7 API domains" in the spec map to the seven feature routers; B-A0 covers non-router infra that would otherwise be an unmeasured gap.