Strangler capstone (lane 6 of 6) — the ratchet that makes the whole push safe. Depends on the JaCoCo report (#192 / PR #241); its final threshold lands as lanes 1–5 complete.
Scope: build + CI. Add jacocoTestCoverageVerification with a minimum-coverage rule, wired into the CI coverage job so a PR fails when it drops below the floor.
Strangler mechanism: start the floor at the current baseline (~38% line per PR #241) and ratchet it upward as each lane lands — prefer per-package minimums so covered packages can't regress while others catch up — until the overall floor is ≥95%. Document the ratchet in CLAUDE.md.
Target: enforced ≥95% overall in CI, with no regression permitted below each package's floor.
**Strangler capstone (lane 6 of 6)** — the ratchet that makes the whole push safe. Depends on the JaCoCo report (#192 / PR #241); its final threshold lands as lanes 1–5 complete.
**Scope:** build + CI. Add `jacocoTestCoverageVerification` with a minimum-coverage rule, wired into the CI coverage job so a PR **fails** when it drops below the floor.
**Strangler mechanism:** start the floor at the current baseline (~38% line per PR #241) and **ratchet it upward** as each lane lands — prefer per-package minimums so covered packages can't regress while others catch up — until the overall floor is **≥95%**. Document the ratchet in CLAUDE.md.
**Target:** enforced ≥95% overall in CI, with no regression permitted below each package's floor.
Heads-up from lane 1 (#246 / PR #254) for the ratchet design:
Instruction coverage of data/repository can't reach 95% by tests alone — ~82.8% of that package's missed instructions are Kotlin-coroutine synthetics: Flow.map collector continuations (…$$inlined$map$1$1) and suspend-lambda state machines (…$openMessage$2), whose suspend/resume dispatch branches don't execute under synchronous test mocks (known JaCoCo × coroutines limitation). Only ~4 lines in MailRepositoryImpl are genuinely unhit; line coverage is already ≥98%.
For the capstone, the ratchet should either (a) exclude these synthetic classes from JaCoCo counting, or (b) set per-package instruction floors to the achievable level while holding line coverage ≥95%. Expect the same pattern in the other coroutine-heavy packages (sync, viewmodels).
Heads-up from **lane 1 (#246 / PR #254)** for the ratchet design:
Instruction coverage of `data/repository` can't reach 95% by tests alone — ~82.8% of that package's *missed instructions* are Kotlin-coroutine synthetics: `Flow.map` collector continuations (`…$$inlined$map$1$1`) and suspend-lambda state machines (`…$openMessage$2`), whose suspend/resume dispatch branches don't execute under synchronous test mocks (known JaCoCo × coroutines limitation). Only ~4 lines in `MailRepositoryImpl` are genuinely unhit; **line** coverage is already ≥98%.
For the capstone, the ratchet should either (a) **exclude these synthetic classes** from JaCoCo counting, or (b) set per-package **instruction** floors to the achievable level while holding **line** coverage ≥95%. Expect the same pattern in the other coroutine-heavy packages (sync, viewmodels).
Update from lane 4 (#249 / PR #256) — reinforces that a strict per-package 95% instruction gate is not achievable; recommend gating on line ≥95% (and/or instruction with explicit exclusions). Two structural reasons:
JVM-untestable Android classes (repo convention: JVM-test the extracted logic, cover the Android seam via E2E):
push/IdleService — a foreground Service, can't be instantiated off-device; it is the entirepush shortfall (23% instr / 19% line).
reporting/ReportUploadScheduler — WorkManager.getInstance() is a static on an abstract class MockK can't stub (AbstractMethodError). SyncScheduler is testable only because it injects Provider<WorkManager>.
reporting/ReportUploadWorker HTTP path — unreachable while BuildConfig.DEBUG_REPORT_ENDPOINT is empty (default, inlined constant).
CrashReporter.terminate — Process.killProcess/exitProcess would kill the test JVM.
JaCoCo coroutine/Flow synthetic deflation (same as lanes 1–2): invokeSuspend label-dispatch + .map{}/.combine{} operator synthetics count as "missed" even when fully exercised. E.g. ui/mailbox = 81.7% instruction but 98.9% line; ui (AppViewModel) = 51% instruction but 100% line (its .map{}.take(1) synthetics).
Lane-4 line coverage is ≥95% for most in-scope packages (drafts/outbox/contacts/ui-reporting 100% line; compose 99.4; settings 99.4; mailbox 98.9). Recommended ratchet: per-package LINE floors ≥95%, plus either exclude the untestable classes above or accept a lower instruction floor for push/reporting. The testability seams for those classes are captured in a separate follow-up (see below). Lane 3 (#248, instrumented) may also surface that instrumented coverage isn't wired into the JVM report — fold that into the ratchet too.
Update from **lane 4 (#249 / PR #256)** — reinforces that a strict per-package **95% instruction** gate is not achievable; recommend gating on **line ≥95%** (and/or instruction with explicit exclusions). Two structural reasons:
1. **JVM-untestable Android classes** (repo convention: JVM-test the extracted logic, cover the Android seam via E2E):
- `push/IdleService` — a foreground `Service`, can't be instantiated off-device; it is the *entire* `push` shortfall (23% instr / 19% line).
- `reporting/ReportUploadScheduler` — `WorkManager.getInstance()` is a static on an abstract class MockK can't stub (`AbstractMethodError`). `SyncScheduler` is testable only because it injects `Provider<WorkManager>`.
- `reporting/ReportUploadWorker` HTTP path — unreachable while `BuildConfig.DEBUG_REPORT_ENDPOINT` is empty (default, inlined constant).
- `CrashReporter.terminate` — `Process.killProcess`/`exitProcess` would kill the test JVM.
2. **JaCoCo coroutine/Flow synthetic deflation** (same as lanes 1–2): `invokeSuspend` label-dispatch + `.map{}/.combine{}` operator synthetics count as "missed" even when fully exercised. E.g. `ui/mailbox` = **81.7% instruction but 98.9% line**; `ui` (AppViewModel) = **51% instruction but 100% line** (its `.map{}.take(1)` synthetics).
Lane-4 line coverage is ≥95% for most in-scope packages (drafts/outbox/contacts/ui-reporting 100% line; compose 99.4; settings 99.4; mailbox 98.9). **Recommended ratchet: per-package LINE floors ≥95%**, plus either exclude the untestable classes above or accept a lower instruction floor for `push`/`reporting`. The testability seams for those classes are captured in a separate follow-up (see below). Lane 3 (#248, instrumented) may also surface that instrumented coverage isn't wired into the JVM report — fold that into the ratchet too.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Strangler capstone (lane 6 of 6) — the ratchet that makes the whole push safe. Depends on the JaCoCo report (#192 / PR #241); its final threshold lands as lanes 1–5 complete.
Scope: build + CI. Add
jacocoTestCoverageVerificationwith a minimum-coverage rule, wired into the CI coverage job so a PR fails when it drops below the floor.Strangler mechanism: start the floor at the current baseline (~38% line per PR #241) and ratchet it upward as each lane lands — prefer per-package minimums so covered packages can't regress while others catch up — until the overall floor is ≥95%. Document the ratchet in CLAUDE.md.
Target: enforced ≥95% overall in CI, with no regression permitted below each package's floor.
Heads-up from lane 1 (#246 / PR #254) for the ratchet design:
Instruction coverage of
data/repositorycan't reach 95% by tests alone — ~82.8% of that package's missed instructions are Kotlin-coroutine synthetics:Flow.mapcollector continuations (…$$inlined$map$1$1) and suspend-lambda state machines (…$openMessage$2), whose suspend/resume dispatch branches don't execute under synchronous test mocks (known JaCoCo × coroutines limitation). Only ~4 lines inMailRepositoryImplare genuinely unhit; line coverage is already ≥98%.For the capstone, the ratchet should either (a) exclude these synthetic classes from JaCoCo counting, or (b) set per-package instruction floors to the achievable level while holding line coverage ≥95%. Expect the same pattern in the other coroutine-heavy packages (sync, viewmodels).
Update from lane 4 (#249 / PR #256) — reinforces that a strict per-package 95% instruction gate is not achievable; recommend gating on line ≥95% (and/or instruction with explicit exclusions). Two structural reasons:
push/IdleService— a foregroundService, can't be instantiated off-device; it is the entirepushshortfall (23% instr / 19% line).reporting/ReportUploadScheduler—WorkManager.getInstance()is a static on an abstract class MockK can't stub (AbstractMethodError).SyncScheduleris testable only because it injectsProvider<WorkManager>.reporting/ReportUploadWorkerHTTP path — unreachable whileBuildConfig.DEBUG_REPORT_ENDPOINTis empty (default, inlined constant).CrashReporter.terminate—Process.killProcess/exitProcesswould kill the test JVM.invokeSuspendlabel-dispatch +.map{}/.combine{}operator synthetics count as "missed" even when fully exercised. E.g.ui/mailbox= 81.7% instruction but 98.9% line;ui(AppViewModel) = 51% instruction but 100% line (its.map{}.take(1)synthetics).Lane-4 line coverage is ≥95% for most in-scope packages (drafts/outbox/contacts/ui-reporting 100% line; compose 99.4; settings 99.4; mailbox 98.9). Recommended ratchet: per-package LINE floors ≥95%, plus either exclude the untestable classes above or accept a lower instruction floor for
push/reporting. The testability seams for those classes are captured in a separate follow-up (see below). Lane 3 (#248, instrumented) may also surface that instrumented coverage isn't wired into the JVM report — fold that into the ratchet too.