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 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 defaultlcovreporter, 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.
# trilium-core services below 100%, worst first (measured through the server suite):
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
# ### packages/trilium-core/src/becca/entities/bnote.ts — lines 78% (...)
# uncovered lines: 163, 240-257, 266-267, ...
# uncovered branch lines: 245, 249, ...
--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:
# Whole package (slow but complete):
pnpm --filter @triliumnext/client test --coverage
pnpm --filter server test --coverage
# Scoped to specific specs (fast iteration). On Windows/sandbox, pnpm exec can
# EPERM — call the hoisted binary in the REPO-ROOT node_modules directly:
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=dotfor scoped runs — the project's configuredhtml/@vitest/uireporter can crash at end-of-run and abort coverage. - The v8
textreporter crashes (PARSE_ERRORremapping unrelated core files) on single-spec--coverageruns. Uselcov/json/json-summaryand 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/clientfirst. Running multiple specs from the repo root with--root apps/clienttriggers a vitest "failed to find the runner" crash; a single spec with--rootis 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.reportsDirectoryis 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.