feat(ci): no-regression JVM coverage gate scoped to the testable surface #323

Merged
JMR-dev merged 1 commits from feat-251-292-coverage-noregression-gate into main 2026-07-04 16:45:48 +00:00
JMR-dev commented 2026-07-04 16:27:53 +00:00 (Migrated from github.com)

Implements a no-regression JVM unit-test coverage gate (the maintainer chose this over an absolute 95% floor), scoped to the testable surface.

What changed

  • Scoped :app:jacocoTestReport — the denominator now excludes code that is structurally unreachable from a JVM unit test: Compose screen/component render code, Android framework entry points (*Activity/*Service/Application/*BackupAgent), Hilt DI (**/di/**), and the src/debug cold-open probe. Kept in scope: ViewModels, repositories, mappers, DAOs, utils, richtext, mail, reporting logic — and the six WorkManager Workers.
  • New :app:jacocoTestCoverageVerification gate — shares the exact same classDirectories/executionData/sourceDirectories as the report, with a BUNDLE / LINE / COVEREDRATIO minimum. Wired into both the check lifecycle task (local parity) and CI's unit-test job, so a coverage regression now fails the CI passed gate.

Baseline & floor

  • Baseline: 80.21% line (4838 / 6032 covered).
  • Floor set to 0.79 — ~1.2% below baseline so ordinary run-to-run noise doesn't red-flag it, but a real drop fails. Verified both ways: the gate passes at 0.79, and fails when temporarily raised to 0.85 (lines covered ratio is 0.80, but expected minimum is 0.85).
  • It's a manual ratchet for now: a comment in build.gradle.kts says to bump the floor up in the same PR whenever coverage rises materially.

Correction to #292 — Workers kept in scope

This supersedes PR #292 (branch build-290-jacoco-scope), which reported 78% but excluded **/*Worker*. All six Workers (SyncWorker, BackfillWorker, PruneWorker, SendWorker, ReportPurgeWorker, ReportUploadWorker) are heavily unit-tested (construct-and-doWork()), so they are kept counted in both numerator and denominator; only their Hilt wiring (WorkManagerModule) is excluded, via **/di/**. Keeping the well-tested Workers in is why the baseline rose from #292's 78% to 80.21%. #292 can be closed as superseded.

Validation (no emulator — build-config change only)

:app:testDebugUnitTest, :app:jacocoTestReport, :app:jacocoTestCoverageVerification, :app:ktlintCheck (incl. ktlintKotlinScriptCheck over build.gradle.kts), :app:detekt — all green. Confirmed in the report XML: 203 ViewModel / 138 Repository / 22 Dao classes and all 6 Workers are present; di/coldopen/Activity/Service/BackupAgent/Compose-screens are absent.

Closes #251
Closes #292

🤖 Generated with Claude Code

Closes #290

Implements a **no-regression** JVM unit-test coverage gate (the maintainer chose this over an absolute 95% floor), scoped to the testable surface. ## What changed - **Scoped `:app:jacocoTestReport`** — the denominator now excludes code that is structurally unreachable from a JVM unit test: Compose screen/component render code, Android framework entry points (`*Activity`/`*Service`/`Application`/`*BackupAgent`), Hilt DI (`**/di/**`), and the `src/debug` cold-open probe. Kept in scope: ViewModels, repositories, mappers, DAOs, utils, richtext, mail, reporting logic — **and the six WorkManager Workers**. - **New `:app:jacocoTestCoverageVerification` gate** — shares the exact same `classDirectories`/`executionData`/`sourceDirectories` as the report, with a `BUNDLE` / `LINE` / `COVEREDRATIO` `minimum`. Wired into both the `check` lifecycle task (local parity) and CI's unit-test job, so a coverage regression now fails the `CI passed` gate. ## Baseline & floor - **Baseline: 80.21% line** (4838 / 6032 covered). - **Floor set to `0.79`** — ~1.2% below baseline so ordinary run-to-run noise doesn't red-flag it, but a real drop fails. Verified both ways: the gate **passes** at 0.79, and **fails** when temporarily raised to 0.85 (`lines covered ratio is 0.80, but expected minimum is 0.85`). - It's a **manual ratchet for now**: a comment in `build.gradle.kts` says to bump the floor up in the same PR whenever coverage rises materially. ## Correction to #292 — Workers kept in scope This **supersedes PR #292** (branch `build-290-jacoco-scope`), which reported 78% but excluded `**/*Worker*`. All six Workers (`SyncWorker`, `BackfillWorker`, `PruneWorker`, `SendWorker`, `ReportPurgeWorker`, `ReportUploadWorker`) are heavily unit-tested (construct-and-`doWork()`), so they are **kept counted** in both numerator and denominator; only their Hilt wiring (`WorkManagerModule`) is excluded, via `**/di/**`. Keeping the well-tested Workers in is why the baseline rose from #292's 78% to 80.21%. **#292 can be closed as superseded.** ## Validation (no emulator — build-config change only) `:app:testDebugUnitTest`, `:app:jacocoTestReport`, `:app:jacocoTestCoverageVerification`, `:app:ktlintCheck` (incl. `ktlintKotlinScriptCheck` over `build.gradle.kts`), `:app:detekt` — all green. Confirmed in the report XML: 203 ViewModel / 138 Repository / 22 Dao classes and all 6 Workers are present; `di`/coldopen/Activity/Service/BackupAgent/Compose-screens are absent. Closes #251 Closes #292 🤖 Generated with [Claude Code](https://claude.com/claude-code) Closes #290
Sign in to join this conversation.