Resumes crash-interrupted work on #192. A prior session's WIP (recovered onto build-192-jacoco) had already added a jacocoTestReport task and a hardcoded JaCoCo toolVersion to app/build.gradle.kts; this PR finishes wiring it up correctly and verifies it end to end.
What was recovered (prior session, unverified):
jacoco plugin applied (Gradle built-in — no org.jetbrains.kotlin.android, no KAPT, per this repo's AGP-9 build constraints).
A jacocoTestReport task scaffold: XML + HTML reports, classDirectories scoped to the debug variant's compiled Kotlin, sourceDirectories scoped to src/main/kotlin, executionData pointed at the unit-test exec file, and a generated-code exclusion list.
toolVersion hardcoded to "0.8.13" directly in app/build.gradle.kts.
What this PR finishes/fixes, verified against the actual compiled output:
Moves the JaCoCo tool version into gradle/libs.versions.toml (jacoco = "0.8.13"), matching how every other plugin version in this repo is sourced, instead of a hardcoded string.
Fixes the generated-code exclusion list. I inspected the real compileDebugKotlin output tree to check what actually needed excluding rather than guessing:
Room's KSP-generated _Impl DAOs/database and the Compose compiler's per-file ComposableSingletons holders are the only generated code that actually lands in classDirectories — Hilt/Dagger's generated Java and AGP's BuildConfig/R/Manifest are compiled by a separate javac task (hiltJavaCompileDebug / compileDebugJavaWithJavac) this report never reads, so those patterns are kept as belt-and-suspenders only.
Dropped the blanket **/*$$* exclude the WIP had. It was silently discarding ~200 real classes' worth of coverage — Kotlin's own $$inlined$ synthetic classes (e.g. the Flow.map { ... } transforms in the repository layer) are hand-written business logic, not generated boilerplate, and excluding them was quietly shrinking the measured surface.
Added Hilt_*/Dagger* prefix patterns so the (currently inert) Hilt exclusions are actually correct if classDirectories' scope ever changes.
DataBinding isn't enabled in this module (no buildFeatures.dataBinding/viewBinding), so there's nothing to exclude for it — documented instead of guessing at a pattern.
Adds a minimal CI step to the existing unit-tests job: runs :app:jacocoTestReport and uploads the XML+HTML report as a build artifact. No coverage-threshold gate yet — a jacocoTestCoverageVerification rule is a natural follow-up once there's a baseline to set a sensible floor against. Left the E2E matrix / managed-device lockstep untouched (out of scope).
Documents the new ./gradlew :app:jacocoTestReport command in CLAUDE.md.
Verification: built and ran on JDK 21 (C:\Program Files\Eclipse Adoptium\jdk-21.0.11.10-hotspot, since JAVA_HOME here defaults to 25). Confirmed both a from-scratch (clean) run and a warm/cached run produce the report cleanly. The generated report shows real signal — 30% instruction / 38% line coverage overall, with spot checks confirming hand-written repository/viewmodel/sync logic shows nonzero coverage and no Hilt/Dagger/BuildConfig/R/Room-_Impl classes leak into the numbers.
Test plan
:app:assembleDebug — pass
:app:testDebugUnitTest — pass
:app:compileDebugAndroidTestKotlin — pass
:app:lintDebug — pass
:app:ktlintCheck — pass (including the Kotlin-script ruleset over app/build.gradle.kts)
:app:detekt — pass
:app:jacocoTestReport — pass, from both a clean and a warm build; XML + HTML reports generated under app/build/reports/jacoco/jacocoTestReport/
CI green on this PR (relies on the new coverage step in .github/workflows/ci.yml's unit-tests job)
## Summary
Resumes crash-interrupted work on #192. A prior session's WIP (recovered onto `build-192-jacoco`) had already added a `jacocoTestReport` task and a hardcoded JaCoCo `toolVersion` to `app/build.gradle.kts`; this PR finishes wiring it up correctly and verifies it end to end.
**What was recovered (prior session, unverified):**
- `jacoco` plugin applied (Gradle built-in — no `org.jetbrains.kotlin.android`, no KAPT, per this repo's AGP-9 build constraints).
- A `jacocoTestReport` task scaffold: XML + HTML reports, `classDirectories` scoped to the debug variant's compiled Kotlin, `sourceDirectories` scoped to `src/main/kotlin`, `executionData` pointed at the unit-test exec file, and a generated-code exclusion list.
- `toolVersion` hardcoded to `"0.8.13"` directly in `app/build.gradle.kts`.
**What this PR finishes/fixes, verified against the actual compiled output:**
- Moves the JaCoCo tool version into `gradle/libs.versions.toml` (`jacoco = "0.8.13"`), matching how every other plugin version in this repo is sourced, instead of a hardcoded string.
- Fixes the generated-code exclusion list. I inspected the real `compileDebugKotlin` output tree to check what actually needed excluding rather than guessing:
- Room's KSP-generated `_Impl` DAOs/database and the Compose compiler's per-file `ComposableSingletons` holders are the only generated code that actually lands in `classDirectories` — Hilt/Dagger's generated Java and AGP's BuildConfig/R/Manifest are compiled by a separate javac task (`hiltJavaCompileDebug` / `compileDebugJavaWithJavac`) this report never reads, so those patterns are kept as belt-and-suspenders only.
- **Dropped the blanket `**/*$$*` exclude** the WIP had. It was silently discarding ~200 real classes' worth of coverage — Kotlin's own `$$inlined$` synthetic classes (e.g. the `Flow.map { ... }` transforms in the repository layer) are hand-written business logic, not generated boilerplate, and excluding them was quietly shrinking the measured surface.
- Added `Hilt_*`/`Dagger*` prefix patterns so the (currently inert) Hilt exclusions are actually correct if `classDirectories`' scope ever changes.
- DataBinding isn't enabled in this module (no `buildFeatures.dataBinding`/`viewBinding`), so there's nothing to exclude for it — documented instead of guessing at a pattern.
- Adds a minimal CI step to the existing `unit-tests` job: runs `:app:jacocoTestReport` and uploads the XML+HTML report as a build artifact. No coverage-threshold gate yet — a `jacocoTestCoverageVerification` rule is a natural follow-up once there's a baseline to set a sensible floor against. Left the E2E matrix / managed-device lockstep untouched (out of scope).
- Documents the new `./gradlew :app:jacocoTestReport` command in `CLAUDE.md`.
**Verification:** built and ran on JDK 21 (`C:\Program Files\Eclipse Adoptium\jdk-21.0.11.10-hotspot`, since `JAVA_HOME` here defaults to 25). Confirmed both a from-scratch (`clean`) run and a warm/cached run produce the report cleanly. The generated report shows real signal — 30% instruction / 38% line coverage overall, with spot checks confirming hand-written repository/viewmodel/sync logic shows nonzero coverage and no Hilt/Dagger/BuildConfig/R/Room-`_Impl` classes leak into the numbers.
## Test plan
- [x] `:app:assembleDebug` — pass
- [x] `:app:testDebugUnitTest` — pass
- [x] `:app:compileDebugAndroidTestKotlin` — pass
- [x] `:app:lintDebug` — pass
- [x] `:app:ktlintCheck` — pass (including the Kotlin-script ruleset over `app/build.gradle.kts`)
- [x] `:app:detekt` — pass
- [x] `:app:jacocoTestReport` — pass, from both a clean and a warm build; XML + HTML reports generated under `app/build/reports/jacoco/jacocoTestReport/`
- [ ] CI green on this PR (relies on the new coverage step in `.github/workflows/ci.yml`'s `unit-tests` job)
Closes #192
🤖 Generated with [Claude Code](https://claude.com/claude-code)
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.
Summary
Resumes crash-interrupted work on #192. A prior session's WIP (recovered onto
build-192-jacoco) had already added ajacocoTestReporttask and a hardcoded JaCoCotoolVersiontoapp/build.gradle.kts; this PR finishes wiring it up correctly and verifies it end to end.What was recovered (prior session, unverified):
jacocoplugin applied (Gradle built-in — noorg.jetbrains.kotlin.android, no KAPT, per this repo's AGP-9 build constraints).jacocoTestReporttask scaffold: XML + HTML reports,classDirectoriesscoped to the debug variant's compiled Kotlin,sourceDirectoriesscoped tosrc/main/kotlin,executionDatapointed at the unit-test exec file, and a generated-code exclusion list.toolVersionhardcoded to"0.8.13"directly inapp/build.gradle.kts.What this PR finishes/fixes, verified against the actual compiled output:
gradle/libs.versions.toml(jacoco = "0.8.13"), matching how every other plugin version in this repo is sourced, instead of a hardcoded string.compileDebugKotlinoutput tree to check what actually needed excluding rather than guessing:_ImplDAOs/database and the Compose compiler's per-fileComposableSingletonsholders are the only generated code that actually lands inclassDirectories— Hilt/Dagger's generated Java and AGP's BuildConfig/R/Manifest are compiled by a separate javac task (hiltJavaCompileDebug/compileDebugJavaWithJavac) this report never reads, so those patterns are kept as belt-and-suspenders only.**/*$$*exclude the WIP had. It was silently discarding ~200 real classes' worth of coverage — Kotlin's own$$inlined$synthetic classes (e.g. theFlow.map { ... }transforms in the repository layer) are hand-written business logic, not generated boilerplate, and excluding them was quietly shrinking the measured surface.Hilt_*/Dagger*prefix patterns so the (currently inert) Hilt exclusions are actually correct ifclassDirectories' scope ever changes.buildFeatures.dataBinding/viewBinding), so there's nothing to exclude for it — documented instead of guessing at a pattern.unit-testsjob: runs:app:jacocoTestReportand uploads the XML+HTML report as a build artifact. No coverage-threshold gate yet — ajacocoTestCoverageVerificationrule is a natural follow-up once there's a baseline to set a sensible floor against. Left the E2E matrix / managed-device lockstep untouched (out of scope)../gradlew :app:jacocoTestReportcommand inCLAUDE.md.Verification: built and ran on JDK 21 (
C:\Program Files\Eclipse Adoptium\jdk-21.0.11.10-hotspot, sinceJAVA_HOMEhere defaults to 25). Confirmed both a from-scratch (clean) run and a warm/cached run produce the report cleanly. The generated report shows real signal — 30% instruction / 38% line coverage overall, with spot checks confirming hand-written repository/viewmodel/sync logic shows nonzero coverage and no Hilt/Dagger/BuildConfig/R/Room-_Implclasses leak into the numbers.Test plan
:app:assembleDebug— pass:app:testDebugUnitTest— pass:app:compileDebugAndroidTestKotlin— pass:app:lintDebug— pass:app:ktlintCheck— pass (including the Kotlin-script ruleset overapp/build.gradle.kts):app:detekt— pass:app:jacocoTestReport— pass, from both a clean and a warm build; XML + HTML reports generated underapp/build/reports/jacoco/jacocoTestReport/.github/workflows/ci.yml'sunit-testsjob)Closes #192
🤖 Generated with Claude Code