31 KiB
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 -- <reason> */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.tsbefore the libsql file is opened (Windows EBUSY constraint) — do not move migration intobeforeAll/beforeEach. Per-test isolation is done by deleting rows, never by re-migrating or unlinking the file. - Test style (API):
createApp()per test; auth viaauthToken(app, email, role)→bearer(token); activities viaseedActivity(name, insoleTypes?); deterministic timing by back-dating rows withdb.update(workSessions).set({ startTime: new Date(Date.now() - N) }). Mirrorapps/api/test/sessions.test.ts. - Test style (clients): Testing Library + jsdom; mirror the existing
*.test.tsxin 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 oxlintclean. - Tracking: SoleLog in Plane (project id
1956d3d3-15ed-4850-93d8-96d5fb17f213), not Jira. Docs live indocs/. 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:coveragescript in each workspace that emits a text + json-summary report. Phase B tasks consumeyarn workspace <name> test:coverageto find uncovered lines. -
Step 1: Install the matching coverage provider (all three workspaces)
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:
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):
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:coveragescript to all threepackage.json
In each of apps/api, apps/worker, apps/admin package.json scripts, add:
"test:coverage": "vitest run --coverage",
- Step 5: Run coverage in each workspace and confirm it produces a report
Run:
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
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(addresetDb) - Modify:
apps/api/test/sessions.test.ts(adopt it as proof — removes reliance on accumulated state)
Interfaces:
-
Produces:
export async function resetDb(): Promise<void>— deletes all rows fromworkSessions,activities, and the better-auth tables (session,account,user,verification), leaving the migrated schema intact. Phase B API domain tasks call it inbeforeEach. -
Step 1: Write a failing test for
resetDb
Create apps/api/test/reset-db.test.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
resetDbinhelpers.ts
Add these imports and function to apps/api/test/helpers.ts (extend the existing schema import):
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<void> {
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
resetDbinsessions.test.tsas 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
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 isEPIC. -
For each domain in Phase B (18 total),
create_work_item(name: "Tests — <domain>", parent: EPIC). -
Step 2: Create the checklist doc skeleton
Create docs/reference/domain-test-checklist.md:
# 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 |
<!-- Phase B appends one "## <domain>" section per domain below. -->
Fill the baseline table now from the Task 1 Step 5 coverage-summary.json totals.
- Step 3: Commit
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):
- Measure.
yarn workspace <ws> test:coverageand 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. - 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 (
.onlywhile iterating). - Add the domain's one lifecycle test (specified per task).
- 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.
- Verify near-100%. Re-run coverage for this domain's files. Remaining uncovered lines must be either tested or
/* c8 ignore next -- <reason> */. Runnpx oxfmton changed files;npx oxlintclean. - Record + commit. Append a
## <domain>section todocs/reference/domain-test-checklist.mdwith 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:
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:
/healthreturns{ status: 'ok' }(exists).env.tsapplies 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.tsbranch coverage (each default), add an idempotent-migration test. Acceptance: near-100% onenv.ts,health.ts;db/client.ts/migrate.tscovered orc8 ignoreon 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/me401 without token; returns{ user: { id, email, name, role } }with token.getSessionUsermaps role (default'worker') andbanned.adminGuard: 401 no session, 403 banned or non-admin, passes for active admin.isAdmintrue only for roleadmin.- 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% onrequire-user.ts,me.ts;auth.tsconfig 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_typesto all three. - reorder endpoint sets
sort_orderfrom theidsarray; rejects empty array (400). insole_typesround-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% onactivities.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:trueprepends a Worker column.- quoting escapes commas/quotes/newlines in fields.
formatDurationrenders H:MM:SS (or the current format) incl. zero and paused columns./api/export401 without token; onlycompletedrows; 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% oncsv.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/reportand/api/admin/exportwith correct totals. (Match request/response shapes toadmin.test.tsandreport.test.ts.) Acceptance: near-100% on the session parts ofadmin.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_<from>_<to>.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 ofadmin.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
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
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
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.