| 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.
Analyzing existing coverage data is free; producing it is not. A coverage run is a full suite run
plus instrumentation — minutes, and normally CI's job. Prefer whatever lcov.info is already on
disk, and when you do need fresh numbers, scope the run to the package or path in question rather
than running pnpm coverage. Generating repo-wide coverage is an explicit-request activity.
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.