| name | analyzing-coverage |
| description | Use when measuring or chasing Vitest/v8 code coverage in the Trilium monorepo — "what's below 100%?", "which files need tests?", "what lines of X are uncovered?", "take <area> to 100%", or feeding coverage gaps to test-writing agents. Provides one reusable analyzer (coverage.mjs) for lcov.info / coverage-summary.json / coverage-final.json, the correct commands to produce that data on Windows, and the known footguns. Pairs with writing-unit-tests. |
Analyzing coverage in Trilium
There is one coverage analyzer — coverage.mjs in this skill folder. Don't write a new throwaway parser; every past session that did (cov-analyze.mjs, cov-parse.mjs, cov-lines.mjs, cov-gaps.cjs) reinvented the same two operations. Use this instead.
node .claude/skills/analyzing-coverage/coverage.mjs <coverage-file> [summary|gaps] [options]
It auto-detects the format, so point it at whatever Vitest produced:
lcov.info — the default lcov reporter, so it's almost always already on disk. Supports both modes.
coverage-summary.json — from --coverage.reporter=json-summary. summary only (no per-line detail).
coverage-final.json — from --coverage.reporter=json. Supports both modes.
Two modes
summary (default) — list files below a threshold, worst-first, plus aggregate totals over the matched set. This is the "where's the coverage debt?" view.
node .claude/skills/analyzing-coverage/coverage.mjs \
apps/server/test-output/vitest/coverage/lcov.info \
--filter packages/trilium-core/src/services
Options: --threshold N (default 100), --metric lines|branches|functions|any (default any — flag a file if any metric is below), --top N, --json.
gaps — for the file(s) matched by --filter, print the exact uncovered line numbers (statements + functions) and uncovered branch lines, compressed to ranges. This is the "what does my new test have to exercise?" view — feed it straight into a test-writing agent's prompt.
node .claude/skills/analyzing-coverage/coverage.mjs \
apps/server/test-output/vitest/coverage/lcov.info gaps \
--filter becca/entities/bnote.ts
--filter takes a path substring; repeat it or comma-separate to match any (--filter src/services,src/entities). --json on either mode emits machine-readable output for workflows.
Producing the coverage data
trilium-core has no runner of its own — its coverage is measured through the apps/server and apps/standalone suites (both set allowExternal: true + a core include glob; see writing-unit-tests). Pick the suite that exercises your file:
| Target area | Suite to run | lcov lands at |
|---|
apps/client/src/** | client | apps/client/test-output/vitest/coverage/lcov.info |
apps/server/src/**, packages/trilium-core/src/** | server | apps/server/test-output/vitest/coverage/lcov.info |
Run the suite (or a scoped subset) with coverage, then analyze the lcov:
pnpm --filter @triliumnext/client test --coverage
pnpm --filter server test --coverage
cd apps/server
CI=true node ../../node_modules/vitest/vitest.mjs run \
../../packages/trilium-core/src/becca/entities/bnote.spec.ts \
--reporter=dot --coverage --coverage.reporter=lcov \
--coverage.reportsDirectory=./test-output/cov-bnote
node ../../.claude/skills/analyzing-coverage/coverage.mjs \
./test-output/cov-bnote/lcov.info gaps --filter bnote.ts
Footguns (learned the hard way)
- Always pass
--reporter=dot for scoped runs — the project's configured html/@vitest/ui reporter can crash at end-of-run and abort coverage.
- The v8
text reporter crashes (PARSE_ERROR remapping unrelated core files) on single-spec --coverage runs. Use lcov/json/json-summary and analyze with this script — never rely on the terminal table for a scoped run.
- The per-file table row renders blank when a single included file is exactly 100% (cosmetic v8 quirk). This script reads the raw data, so it shows the real number.
- Multi-file client runs must
cd apps/client first. Running multiple specs from the repo root with --root apps/client triggers a vitest "failed to find the runner" crash; a single spec with --root is fine.
- Isolated vs full-suite gaps: when you run only your spec, lines covered by other specs in the full suite show as uncovered. That's expected — only your assigned lines need to disappear. Don't chase the rest.
--coverage.reportsDirectory is relative to --root, so it can double a path prefix — pass a simple relative dir like ./test-output/cov-<slug>.
- For provably-dead defensive branches, mark with
/* v8 ignore next N -- reason */ rather than writing a fake test (sanctioned by writing-unit-tests).
Writing the tests
This skill only measures. To actually raise coverage — fixtures, the real-DB vs mocked-becca decision, import paths, component rendering — use the writing-unit-tests skill. The typical loop: run suite with --coverage → coverage.mjs … summary to pick the worst file → coverage.mjs … gaps --filter <file> to get the line list → write tests → re-run scoped with --coverage → gaps again until your lines are gone.
For a large fan-out (e.g. "take all of becca to 100%"), a Workflow that gives one agent per file its gaps line-list as the assignment works well — see the pattern this skill's analyzer was extracted from.