Coverage gate
Skill xiaolai/tdd-guardian-for-claude/skills/tdd-guardian/coverage-gate
Enforce coverage thresholds AND test quality — coverage without behavioral assertions is meaningless.From its SKILL.md
npx -y skills add xiaolai/tdd-guardian-for-claude --skill coverage-gateAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 10 stars10 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
9.6 KB, as published. Nobody here has run it
Coverage Gate
Gate 1: Coverage thresholds
- Run every lane whose
gateOnincludes the active trigger. - Collect the report from each lane with
coverage: "include". - Merge them (see "Multi-lane coverage" below).
- Verify the merge measured something — a report with zero measurable lines scores 100% under the 0/0 convention and must FAIL, not pass. A silent no-op coverage run looks identical to a perfect one.
- Enforce thresholds against the merged totals based on
coverageMode:
Coverage modes
"absolute" (default): Current behavior — all metrics must meet configured thresholds.
"no-decrease": Blocks only if coverage decreased from a recorded baseline.
- On first run (or branch change), records current coverage as the baseline and passes.
- On subsequent runs, compares against baseline:
- Decreased → block (with delta details)
- Equal or improved → pass
- Baseline is per-branch. Switching branches records a new baseline automatically.
| Scenario | absolute | no-decrease |
|---|---|---|
| Coverage 72%, threshold 100% | BLOCK | PASS (if baseline ≤ 72%) |
| Coverage dropped 72% → 70% | BLOCK | BLOCK (decreased) |
| Coverage improved 72% → 75% | BLOCK | PASS (improved) |
Default threshold policy (absolute mode): 100 for all metrics.
Multi-lane coverage
Thresholds apply to the merged total across every contributing lane, never to a single lane. Two merge methods exist and the gate reports which one it used:
| Method | When | Accuracy |
|---|---|---|
single | One contributing lane | Exact |
union | Every report carries per-line detail | Exact — a line hit by any lane counts once |
weighted | At least one report is summary-only | Approximate — a line hit by two lanes is counted twice |
To get an exact union, have every contributing lane emit a per-line format: LCOV, Cobertura, JaCoCo, coverage.py JSON, or coverage-final.json. Summary-only formats (coverage-summary.json) force the weighted fallback. When the gate falls back, it says so in the report — do not quote a weighted number as if it were a union.
Give every contributing lane its own coverageSummaryPath. Two lanes writing the same file means the second overwrites the first and coverage is silently undercounted.
Unmeasured metrics are not zero
A metric the report format does not track is null. With a non-zero threshold that produces a WARNING, never a FAILURE:
- coverage.py and go-cover measure no functions
- go-cover and SimpleCov measure no branches by default
- LCOV measures functions and branches only when the producing tool emits
FNF/FNHandBRF/BRH
Set those thresholds to 0 rather than living with a permanent warning. Treating null as zero would fail every Go and Python project for a dimension their tools cannot report.
If coverage fails:
- List exact metric deltas (against thresholds in absolute mode, against baseline in no-decrease mode), including the covered/total counts, not just percentages.
- Identify uncovered branches/functions by file.
- Name the lane each gap belongs in, per
lane-policy. An uncovered error path in a DB adapter is an integration-lane gap; do not close it with a mock in the unit lane. - Add missing tests, then rerun the full gate.
Gate 2: Test quality (new — enforced alongside coverage)
Coverage alone is insufficient. A test that touches every line but only asserts expect(mock).toHaveBeenCalled() provides zero regression safety.
Follow the assertion hierarchy and mock rules defined in the policy-core skill.
Quality scan procedure
For each test file in the coverage report:
-
Count assertion types per test using the assertion hierarchy from
policy-core:- Level 1-5 (behavior): return value checks, error checks, output checks, state checks, integration checks
- Level 6-7 (wiring): mock call args, mock call counts
-
Flag violations:
- FAIL: Any
it()block where ALL assertions are Level 6-7 - WARN: Any
it()block where Level 6-7 assertions outnumber Level 1-5 assertions by 3:1 or more - PASS: At least one Level 1-5 assertion exists
- FAIL: Any
-
Report format:
Test Quality Summary: ✓ 45/48 tests have behavioral assertions ✗ 3 tests are wiring-only: - docker/container.test.ts: "applies security defaults" (line 52) — mock args only - cli/lifecycle.test.ts: "stops container" (line 48) — mock-was-called only - docker/network.test.ts: "creates network" (line 28) — mock args only -
Gate result: FAIL if any wiring-only tests exist in changed files. WARN (non-blocking) for existing wiring-only tests in unchanged files.
Gate 3: Coverage ignore directive audit
Scan all source files (not test files) for V8 coverage ignore comments. Flag misuse:
- FAIL:
/* v8 ignore next */or/* v8 ignore next N */— silently fails on??, ternaries,catchbodies, and short-circuit operators (&&,||). Replace with/* v8 ignore start *///* v8 ignore stop */. - PASS:
/* v8 ignore start *///* v8 ignore stop */range pairs.
Coverage Ignore Audit:
✗ 2 files use unreliable /* v8 ignore next N */:
- src/config.ts:42 — covers ?? expression, will silently fail
- src/handler.ts:88 — covers ternary, will silently fail
Fix: replace with /* v8 ignore start */ / /* v8 ignore stop */ range comments
Gate 0: Is the merge complete enough to measure against?
Runs before any threshold is compared, because a threshold applied to part of the project is a wrong number stated confidently.
| Situation | Result |
|---|---|
Every lane with coverage: "include" produced a report | Continue |
| A contributing lane ran and produced no report, while coverage is enforced | FAIL, naming the lane. The merge would cover a subset |
That lane is optional: true | Still fails. optional means "do not block on its tests", not "measure the project against whatever is left" |
| That lane is in bootstrap | Skipped — a lane with no tests has nothing to report yet |
| No lane is bound to the trigger at all, while coverage or critical paths are configured | FAIL. Enforcement was configured and is structurally unreachable |
The fix is a config change, not a threshold change: repair the lane, or set coverage: "none" on it if it was never meant to contribute.
Gate 4: Critical-path thresholds
A repo-wide threshold is one number for code with wildly different consequences of failure. criticalPaths adds a second, stricter bar scoped by glob — see the value heuristic in policy-core.
Evaluated from the merged report's per-file entries, so it works across every format the plugin reads.
| Situation | Result |
|---|---|
| Matched files meet the entry's thresholds | PASS |
| Matched files fall below | FAIL, naming the glob, the metric, and the shortfall |
Entry omits thresholds | Inherits coverageThresholds |
| Glob matches zero files | FAIL, naming the glob. Unlike a lane in bootstrap there is no legitimate steady state: either the glob is wrong, or the named code has no measured coverage. Both are what the entry exists to catch |
| Merged report has no per-file data | FAIL — a configured strict rule that cannot be evaluated must not report success |
| A metric the format does not measure | WARNING, never FAIL — null is not zero |
| The merge was weighted rather than a union | Every critical-path percentage is flagged APPROXIMATE — a shared file is counted once per lane |
| A threshold is set on a dimension the merge could not compute exactly | FAIL. Functions and branches have no per-item identity across reports, so a multi-lane merge keeps the strongest lane — which can report 1/1 = 100% for a file with 100 functions. Emit one authoritative report for those files, or set that threshold to 0 on the entry |
Critical paths are absolute bars in both coverage modes. Under no-decrease the repo-wide comparison is against a moving baseline, but a critical path holds its fixed number regardless — and a failing critical path does not bank a new baseline, which would freeze the failure in as the new normal.
Critical Paths:
✗ src/payments/** (7 files) lines: 98.21% < 100.00% (110/112)
✓ src/auth/** (4 files) lines=100.00%, branches=100.00%
✗ src/paymnets/** matched no file — check the glob against your reporter's paths
How to fix wiring-only tests
| Current assertion | Add this | Example |
|---|---|---|
expect(mockCreate).toHaveBeenCalledWith(opts) | Verify return value | expect(result.id).toMatch(/^mx-/) |
expect(mockStop).toHaveBeenCalled() | Verify formatted output | expect(formatter.success).toHaveBeenCalledWith("Mecha stopped.") |
expect(mockCreate).toHaveBeenCalledWith(securityOpts) | Add integration test | const info = await inspect(container); expect(info.HostConfig.ReadonlyRootfs).toBe(true) |
Scope
Covers threshold enforcement: coverage modes, multi-lane merging, null-metric handling, critical-path thresholds, the test-quality scan, and the coverage-ignore directive audit.
Does NOT cover:
| Question | Where |
|---|---|
| How is each report format parsed? | commands/shared/parse-coverage.md |
| Which lanes contribute coverage, and why? | tdd-guardian:lane-policy |
| What coverage tool does language X use? | tdd-guardian:tooling-catalog |
| How is test strength measured beyond coverage? | tdd-guardian:mutation-gate |
| How strong is the claim a test makes? | tdd-guardian:policy-core (S1-S6) |