test: raise coverage 49.67% → 98.84%, fix the config that hid the gap - #28
Merged
Merged
Conversation
The reported coverage figure was wrong. `vitest.config.ts` set no
`coverage.include`, so the v8 provider only instrumented files that some
test had already imported — 131 of 176 source files were absent from the
report entirely rather than counted as 0%. That reported 83.14% while
true coverage was 48.41%, and it is what CI uploads to Codecov.
It is why `src/services/familyService.ts` (756 lines) and
`src/app/(web)/tools/layout.tsx` (the /tools authorization gate) could sit
at zero tests without anyone seeing it.
Coverage over authored code, measured on the same denominator throughout:
Statements 49.67% -> 98.84% (3,610/3,652)
Lines 50.20% -> 99.70% (3,363/3,373)
Branches 37.51% -> 92.21%
Functions 38.35% -> 98.81%
Test files 50 -> 116 Test cases 812 -> 1,535
Scope excludes vendored shadcn/ui primitives, generated MP models,
build-time scripts, loading.tsx skeletons, barrels and type-only modules.
Config
- `coverage.include: ['src/**/*.{ts,tsx}']` — the actual fix. Note
`coverage.all` was REMOVED in Vitest 5: setting it is a type error and
does nothing, so `include` is the only lever.
- Directory exclusions now end in `**`. A bare `'src/components/ui/'`
matches nothing, which had quietly kept those files in the denominator.
- `thresholds: { statements: 97, lines: 98 }`, enforced by the existing
test:coverage CI job. Branches and functions are left unenforced: a
single defensive guard can trip them, and a threshold people learn to
override is worse than none. Verified the gate fires by setting it to
an impossible value and confirming exit 1.
- Renamed `vitest.config.ts` -> `.mts`. As `.ts` it was loaded as
CommonJS and Vite warned it breaks when `configLoader: 'native'`
becomes the default. The rename exposed a second instance of the same
problem: `__dirname` only worked because of the CJS load, and as real
ESM it would have thrown at config load and stopped every test from
starting. Now uses `import.meta.dirname`.
Build was already broken on dev
`tsconfig.json` includes `**/*.ts`, `next.config.ts` sets no
`ignoreBuildErrors`, and CI never runs `next build` — so two committed
test files failed type checking while CI stayed green. Their fixtures
built `CommunicationInfo`/`MessageInfo`/`FileUploadParams` values out of
raw MP column names (`Author_User_ID`, `From_Contact`) instead of the DTO
fields the helper requires. The tests passed because the mocks never
inspected the missing fields. Fixtures now carry explicit type
annotations so the compiler enforces the shape; `npm run build` succeeds.
Runner output: 1,507 lines -> 17, zero warnings
- Barcode tests mocked react-pdf's `View` as the string 'View', which
React read as a capitalised unknown tag: two warnings per element, 797
lines for one 65-bar barcode. Mocked as `rpdf-view` (the dash matters —
any undashed unknown tag still draws "unrecognized in this browser").
- Added console spies to the 15 test files that deliberately drive error
paths. The MP logger is unconditional by design, so these were expected
logs plus stack traces. The spies use `mockImplementation`, so they
still record and assertions on what was logged keep working.
Source fix (14 lines)
`Select` fields in the group wizard and Add/Edit Family passed
`value={... : undefined}`, leaving them uncontrolled until first
selection and logging a React controlled/uncontrolled warning. Silencing
that in tests would have hidden a real bug: while a field is
uncontrolled, React state is not the source of truth for it, so
`form.reset()` between wizard runs is not guaranteed to clear the
trigger. Changed to `""`, Radix's documented "no selection" value.
Also adds @testing-library/user-event (the standard RTL companion, not
previously installed).
TODOs: 8 opened, 4 resolved. 9 open — 3 high (filter injection in
FamilyService, field loss in removeGroup, address PII into logs from
mergeTemplate), 4 medium, 2 low. Docs updated: CLAUDE.md, DECISIONS.md,
testing references, and a new facts snapshot.
Verified: test:run exit 0 (1,535 passing) · test:coverage exit 0 ·
tsc --noEmit 0 errors · eslint 0 problems · next build exit 0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
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.
The reported coverage number was wrong
vitest.config.tsset nocoverage.include, so the v8 provider only instrumented files that some test had already imported. 131 of 176 source files were absent from the report entirely rather than counted as 0%. It read 83.14% while true coverage was 48.41% — and this is the report CI uploads to Codecov.That's how
src/services/familyService.ts(756 lines) andsrc/app/(web)/tools/layout.tsx(the/toolsauthorization gate) sat at zero tests without anyone seeing it.Results
Measured on the same denominator before and after:
Scope excludes vendored shadcn/ui primitives, generated MP models, build-time scripts,
loading.tsxskeletons, barrels, and type-only modules.Config changes
coverage.include: ['src/**/*.{ts,tsx}']— the actual fix. Worth knowing:coverage.allwas removed in Vitest 5, so setting it is a type error that does nothing.includeis the only lever.**. A bare'src/components/ui/'matches nothing, which had quietly kept those files in the denominator.thresholds: { statements: 97, lines: 98 }, enforced by the existingtest:coverageCI job. Branches and functions are deliberately unenforced — a single defensive guard can trip them, and a threshold people learn to override is worse than none. I verified the gate actually fires by setting it to an impossible value and confirming exit 1.vitest.config.ts→.mts. As.tsit loaded as CommonJS and Vite warned it breaks whenconfigLoader: 'native'becomes the default. The rename exposed a second instance of the same problem:__dirnameonly worked because of the CJS load. As real ESM it would have thrown at config load and stopped every test from starting. Nowimport.meta.dirname.npm run buildwas already broken ondevtsconfig.jsonincludes**/*.ts,next.config.tssets noignoreBuildErrors, and CI never runsnext build— so two committed test files failed type checking while CI stayed green.Their fixtures built
CommunicationInfo/MessageInfo/FileUploadParamsvalues from raw MP column names (Author_User_ID,From_Contact) instead of the DTO fields the helper requires. The tests passed because the mocks never inspected the missing fields. Fixtures now carry explicit type annotations so the compiler enforces the shape. Build succeeds.Runner output: 1,507 lines → 17, zero warnings
Viewas the string'View', which React read as a capitalised unknown tag — two warnings per element, 797 lines for one 65-bar barcode. Nowrpdf-view; the dash matters, since any undashed unknown tag still draws "unrecognized in this browser".mockImplementation, so they still record and assertions on logged content keep working.One source fix (14 lines)
Selectfields in the group wizard and Add/Edit Family passedvalue={... : undefined}, leaving them uncontrolled until first selection and logging a React controlled/uncontrolled warning.Silencing that in tests would have hidden a real bug: while a field is uncontrolled, React state is not the source of truth for it, so
form.reset()between wizard runs isn't guaranteed to clear the trigger. Changed to"", Radix's documented "no selection" value. Diff is exactly 14 lines, nothing else.Review pointers
113 files, but only a handful need real attention:
vitest.config.mts— the config contract and thresholds.tsxsource files — 14 identical one-line changeshelper.test.ts/provider.test.ts— the corrected fixtures*.test.*files and docsTODOs: 8 opened, 4 resolved
9 open — 3 high, all found by reading code while writing tests for it:
FamilyServiceinterpolates client-suppliedenvelopeNo/donorIdinto MP$filterstrings with no validation; the only filters in that file lacking avalidatePositiveIntguard.removeGroupguardsgroupedFieldsbut notgroupOrder, so a non-empty group can be stripped from the order while its fields remain.buildSavePayloadthen drops them. Real MP page-config data loss, no error surfaced.mergeTemplatelogs the raw docxtemplater error, which attaches the live merge scope (names, addresses).Plus 4 medium (incl. no type-check gate in CI) and 2 low. Docs updated:
CLAUDE.md,DECISIONS.md, testing references, new facts snapshot.Verification
npm run test:runnpm run test:coveragenpx tsc --noEmitnpx eslint .npm run build🤖 Generated with Claude Code