test: raise non-UI unit coverage from 71.9% to 99.5% statements - #71
Merged
Conversation
Push unit test coverage for non-UI functional code past the 90% target, fix the measurement that was hiding the gap, and document (without fixing) the defects the old coverage was pointing away from. Coverage — non-UI functional code (all src/**/*.ts plus src/contexts/*.tsx, excluding generated models, codegen scripts, and tests; 760 statements): Statements 71.93% -> 99.47% (756/760) Branches 70.73% -> 95.49% (297/311) Functions 72.28% -> 98.20% (164/167) Lines 72.93% -> 99.72% (738/740) 279 tests / 21 files -> 419 tests / 30 files, still ~3s. tsc --noEmit and eslint . clean. New test files (9) file.service.test.ts 35 tests (+88 stmts) procedure.service.test.ts 16 tests (+27) communication.service.test.ts 13 tests (+25) metadata.service.test.ts 8 tests (+12) domain.service.test.ts 8 tests (+11) client-credentials.test.ts 5 tests (+7) lib/utils.test.ts 7 tests lib/auth-client.test.ts 4 tests shared-actions/domain.test.ts 3 tests The five untested MP sub-services were 163 of the 213 missing statements. All five share the ensureValidToken -> getHttpClient -> error-wrap shape that table.service.ts already had covered, so the harness was copy-adaptable. Extended (6): provider.test.ts 9 -> 24 (the CommunicationService and FileService pass-throughs were entirely untested; provider.ts 60% -> 100%), auth.test.ts 12 -> 25, contact-logs/actions.test.ts 19 -> 24, domainTimezoneService.test.ts 16 -> 18, http-client.test.ts 26 -> 28, plus branch fills in contact-lookup-details/actions, user-menu/actions, and user-context. Source change (one) src/lib/auth.ts: extract the customSession callback body to an exported enrichSessionUser(user, session). Behavior identical. The better-auth plugin closes over its callback and never exposes it, so this was the only way to unit test the logic short of driving a full getSession() through the whole auth stack. auth.ts 44% -> 96%. Removed 7 tautological tests The old auth.test.ts "Name Splitting" and "Session Structure" blocks re-implemented the transformation inside the test body and asserted against their own copy — they would have passed with the customSession callback deleted outright, which is why auth.ts reported 18.5% while the file held 12 tests. Rewritten to call the real export. Verified by mutation: replacing firstName with a constant fails 6 tests; the old versions failed none. Measurement vitest.config.ts gains an explicit coverage.include — without it, v8 reports only on files some test imported, so untested files silently leave the denominator (the repo read 71.6% while true statement coverage was 32.7%). Adds per-glob coverage.thresholds; a breach fails the run with exit 1, which was verified by deliberately breaching one rather than only confirming a pass. Excludes components/ui/ (thin Radix wrappers) and the codegen scripts (dev-only tooling). Feature components stay visible in the report but ungated. Two Vitest 4 notes for whoever edits this next: coverage.all no longer exists and setting it is a tsc error (include replaces it), and --reporter=basic was removed. Deferred to .claude/TODO — no behavior changes in this commit Eight files, one per issue. Six were already known and are still present: numeric IDs interpolated into MP filters unsanitized (confirmed exploitable, in a file at 100%/100% coverage), searchContacts and getCurrentUserProfile as 'use server' actions with no session check, contact-log actions that authenticate but never authorize, an N+1 lookup fetch, and client.ts discarding expires_in. Two found while writing these tests: - contact-logs/actions.ts re-implements the dp_Users User_ID lookup that SessionContextService exists to serve, and throws when it cannot resolve — contradicting that service's log-and-proceed policy for unattributed writes. - contact-logs.tsx: 602 lines at 0%, driving every MP write a user can reach. Out of scope for a non-UI target, but the highest-value gap left. Tests that pin behavior a TODO proposes changing carry a comment naming the TODO file, so the assertion reads as a snapshot rather than a specification. Docs .claude/references/testing.md: the 95.39% / 228 tests / 19 files claims were wrong on all three counts and not reproducible under any configuration. Rewritten against measured numbers, with the new mock patterns (MP sub-service harness, fetch stubbing, FormData assertions) and an explicit rule against asserting on a re-implementation of the subject. .claude/docs/TestCoverage.md: rewritten as current state, with each §5 finding linked to its TODO file. No Ministry Platform data was read or written. Every test mocks at a boundary above the network — which matters most for communication.service (sends real email/SMS in production), procedure.service (procs can mutate), and the file and table write paths. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CI has been red on main since 64f18f0 ("Package Update Cleanup"): `npm ci` fails in ~8s on an ajv lockfile mismatch, before any test runs. Found while verifying this branch — the failure is byte-identical on main, and this branch touches neither package.json nor package-lock.json. Cause: eslint wants ajv@^6, @hookform/resolvers wants ajv@^8, and the lockfile carries only one ajv entry (6.15.0). The nested ajv@8.x subtree, plus fast-uri and json-schema-traverse@1.0.0, is missing. Same mechanism as investigate-emnapi-lockfile-drift.md — a Windows install pruning nested entries that npm ci on Linux then requires. The TODO notes that the regeneration must happen on Linux (or with --os=linux --cpu=x64), since fixing ajv on Windows risks reintroducing the emnapi drift in the same commit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Pushes unit test coverage for non-UI functional code past the 90% target, fixes the measurement that was hiding the gap, and documents — without fixing — the defects the old coverage was pointing away from.
Non-UI functional code = every
src/**/*.tsplussrc/contexts/*.tsx, excluding generated MP models, the two codegen scripts, and test files. 760 statements.279 tests / 21 files → 419 tests / 30 files, still ~3s.
Tests added
Nine new files, six extended. The five untested MP sub-services were 163 of the 213 missing statements:
file.service.test.tsprocedure.service.test.tscommunication.service.test.tsmetadata.service.test.tsdomain.service.test.tsclient-credentials.test.tslib/utils.test.ts,lib/auth-client.test.ts,shared-actions/domain.test.tsAll five sub-services share the
ensureValidToken→getHttpClient→ error-wrap shape thattable.service.tsalready had covered, so the harness was copy-adaptable.Extended:
provider.test.ts9 → 24 (theCommunicationService/FileServicepass-throughs were entirely untested;provider.ts60% → 100%),auth.test.ts12 → 25,contact-logs/actions.test.ts19 → 24,domainTimezoneService.test.ts16 → 18,http-client.test.ts26 → 28, plus branch fills incontact-lookup-details/actions,user-menu/actions, anduser-context.One source change
src/lib/auth.ts— thecustomSessioncallback body is extracted to an exportedenrichSessionUser(user, session). Behavior identical. The better-auth plugin closes over its callback and never exposes it, so this was the only way to unit test the logic short of driving a fullgetSession()through the whole auth stack.auth.ts44% → 96%.7 tautological tests removed
The old
auth.test.ts"Name Splitting" and "Session Structure" blocks re-implemented the transformation inside the test body and asserted against their own copy:They would have passed with the
customSessioncallback deleted outright — which is whyauth.tsreported 18.5% while the file held 12 tests. Now rewritten to call the real export.Verified by mutation: replacing
firstNamewith a constant fails 6 tests. The old versions failed none.Measurement is now honest and ratcheted
vitest.config.tsgains an explicitcoverage.include. Without it, v8 reports only on files some test imported, so untested files silently leave the denominator — the repo read 71.6% while true statement coverage was 32.7%.Adds per-glob
coverage.thresholds. A breach fails the run with exit 1, verified by deliberately breaching one rather than only confirming a pass:Excludes
components/ui/(thin Radix wrappers) and the codegen scripts (dev-only, run manually). Feature components stay visible in the report but ungated.Two Vitest 4 notes for whoever edits this next:
coverage.allno longer exists and setting it is atscerror (includereplaces it), and--reporter=basicwas removed.Deferred to
.claude/TODO— no behavior changes hereEight files, one per issue. Six were already known and are all still present in today's tree, each verified rather than assumed:
mp-filter-injection-numeric-ids.md🔴 — numeric IDs interpolated into MP filters unsanitized. Confirmed exploitable;getContactLogById("1 OR 1=1")producesfilter: "Contact_Log_ID = 1 OR 1=1". The file is at 100% statements and 100% branches.server-action-search-contacts-unauthenticated.md🔴 —'use server', zerogetSessioncalls, returns 20 contacts with email and mobile phone.server-action-user-profile-unauthenticated.md🔴 — no auth and no ownership check; returns any user's profile plus roles and groups.contact-log-actions-authenticate-but-not-authorize.md🟠 — any authenticated session can delete any contact log by ID. Needs a policy decision, not just code.n-plus-1-contact-log-types-lookup.md🟡 —getContactLogTypes()called insidelogs.map().mp-client-token-lifetime-ignores-expires-in.md🟡 — every token capped at 5 minutes; the comment says the opposite of what the code does.Two found while writing these tests:
contact-log-actions-bypass-session-context-service.md🟠 —contact-logs/actions.tsre-implements thedp_UsersUser_ID lookup twice, workresolveMpUserIdalready caches andSessionContextService.getActingUserIdForWrite()exists to serve. It also throws when User_ID won't resolve, contradicting that service's documented log-and-proceed policy for unattributed writes.contact-logs-component-untested.md🔴 —contact-logs.tsx, 602 lines at 0%, driving every MP write a user can reach. Out of scope for a non-UI target, but the highest-value gap left in the repo.Tests pinning behavior a TODO proposes changing carry a comment naming the TODO file, so the assertion reads as a snapshot rather than a specification.
Docs
.claude/references/testing.md— the 95.39% / 228 tests / 19 files claims were wrong on all three counts and not reproducible under any configuration. Rewritten against measured numbers, with the new mock patterns (MP sub-service harness,fetchstubbing,FormDataassertions) and an explicit rule against asserting on a re-implementation of the subject..claude/docs/TestCoverage.md— rewritten as current state, each §5 finding linked to its TODO file.Note on the two numbers
npm run test:coverageprints 71.45% — the whole-app figure, with untested feature components and app pages in the denominator but ungated. The 99.47% above is the non-UI subset. Both are honest; they differ only in denominator, and the reproduction command for each is inTestCoverage.md§3. Quoting either without its scope is how the old 95.39% claim happened.Test plan
npm run test:run— 419 passed (30 files)npm run test:coverage— passes, threshold gate satisfiednpx tsc --noEmit— cleannpx eslint .— clean🤖 Generated with Claude Code