100% test coverage
Every line and every branch, deterministic, with no new exclusions
Why 100% and not 90%
At 90%, the question for every uncovered line is "is this one of the 10% we accept?", and an agent will always answer yes. At 100%, there is no question. Every uncovered line is a finding, and the only answers are a test or a deletion.
Full coverage is a floor, not a goal. It does not prove the tests check anything; that is what mutation testing and the assertion gate are for. What it does prove is that no line of code ships without having run at least once under test, and it has a useful side effect: code that is hard to cover is usually code that should not exist.
The rules
From AGENTS.md:
100% test coverage: Coverage must be complete and deterministic. A branch that only a spawned subprocess reaches gets a direct in-process unit test.
Never add a coverage exclusion: The list in
scripts/check-coverage-exclusions/exclusions.tsonly ever shrinks, and the ratchet fails on an entry a branch adds. Answer a coverage gap by restructuring the code.
The gate checks lines and branches, per file, from the lcov report
(scripts/coverage-check.ts).
The chobble-template
does the same through Bun, with
coverageThreshold = { lines = 1.0, functions = 1.0 } in
bunfig.toml.
Deterministic means no lucky runs
A line that is covered on some runs and not others is a flaky gate. Two common causes, and the rules that remove them:
- Subprocess coverage. Code reached only by a spawned child process may or may not be counted, depending on the tooling. So the rule is that such a branch also gets a direct, in-process unit test.
- Timing. A retry path that only runs when something is slow is
covered by luck. Retry paths run under a virtual clock instead
(
withVirtualBackofffrom the test utilities), so the "every retry failed" branch runs on every run, instantly.
Code shaped for the coverage merge
The test runner runs files in parallel isolates and merges their
coverage. Three times, that merge reported a function's lines as
unhit while the tests clearly ran them. One case had a known shape:
the logic sat in the inner arrow of a curried ternary helper.
Rewritten as a plain two-argument function with an if/return body,
it passed with no exclusion
(#2504).
So the design guide asks for a "house shape": logic in a flat, named, module-level function, and currying from a shared combinator rather than hand-written closures.
// src/shared/types.ts: the logic is a plain two-argument function.
const hasRole = (levels: readonly AdminLevel[], level: AdminLevel): boolean =>
levels.includes(level);
// Currying comes from the shared combinator in #shared/fp-bind.ts.
const roleIn = bindFirst(hasRole);
export const isStaffRole = roleIn(STAFF_ADMIN_LEVELS);
export const isOwnerRole = roleIn(OWNER_ADMIN_LEVELS);
The document is careful to say this is a working rule from one case, not a proven cause. That honesty matters: the next agent to hit the problem knows what is known and what is guessed.
Coverage while you work
Running the full suite with coverage on a shared machine is slow and
unkind to the other agents. deno task cov:files runs only the given
test files with the full harness, then prints the lines and branches
that run left uncovered, optionally limited to one source file with
--only. An agent can close a gap in a minute and leave the full
gate to CI.