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.ts only 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 (withVirtualBackoff from 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.