Lane 4 of the coverage epic: add JVM unit tests for the in-scope ViewModels / UI
state holders and the non-UI reporting/push/power/contacts modules. Test-only — no production code was changed. 28 new test files, ~230 new tests
(full suite now 781, all green).
Line coverage of the hand-written logic is ≥95% almost everywhere. Two things
hold the instruction number below 95% and are out of a JVM lane's reach:
Genuinely JVM-untestable Android classes (left to the instrumented suite,
matching the repo's existing convention of extracting pure logic for JVM and
covering the Android seam via E2E):
push/IdleService — a foreground Service that can't be instantiated off-device
(this single class is the entire push package shortfall).
reporting/ReportUploadScheduler — WorkManager.getInstance() is a static on an
abstract class that MockK can't stub (AbstractMethodError); the repo's testable SyncScheduler only works because it injects Provider<WorkManager>.
reporting/ReportUploadWorker HTTP path — unreachable while BuildConfig.DEBUG_REPORT_ENDPOINT is empty (the default); the constant is inlined,
so the post()/onResponse() branches can't be exercised.
CrashReporter.terminate — calls Process.killProcess + exitProcess, which would
tear down the test JVM.
JaCoCo deflates instruction coverage on Kotlin coroutine/Flow suspend state
machines — the generated invokeSuspend label-dispatch and .map{}/.combine{}
operator synthetics count instructions that stay "missed" even when the logic is
fully exercised (hence line coverage stays ~99% while instruction dips). This is why
e.g. ui/mailbox reads 81.7% instruction but 98.9% line, and ui (AppViewModel,
itself 100%) reads 51% — its .map{}.take(1) Flow synthetics.
The JaCoCo verification/ratchet config is owned by capstone lane #251 and is untouched
here.
Lane 4 of the coverage epic: add JVM unit tests for the in-scope ViewModels / UI
state holders and the non-UI `reporting`/`push`/`power`/`contacts` modules.
**Test-only — no production code was changed.** 28 new test files, ~230 new tests
(full suite now 781, all green).
## What's covered
- **New ViewModel tests** (were 0%): `Drafts`, `Outbox`, `Signatures`,
`SignatureEdit`, `AccountSettings`, `AccountSetup`, `ManualSetup`,
`ProblemReports`, `StartupReport`.
- **Gap-filled ViewModels**: `Compose` (contacts autocomplete, inline-image
tracking/pruning, `trySend` guards, send failure, `flushDraft`), `Mailbox`
(pull-to-refresh, search open/close, drawer hooks, action-failure branches,
account-removal fallback), `Reader` (download success/failure, star, delete,
remote-image opt-in), `Settings` (read-through state + every delegator),
`ReportReview` (the `combine` state flow + online-submit path), `AppPassword`.
- **Non-UI modules**: `AppLog`, `AppVersionProvider`, `ReportSubmitter`,
`ReportUploadWorker` (reachable control flow), `CrashReporter.install`,
`LogEntry`, `IntentComposeParser`, `ContactsRepository`,
`ContactsPermissionManager`, `IdlePushManager`, `BatteryOptimizationManager`
(Context-backed methods) and `AndroidBatteryStatusProvider`.
- State-holder value semantics for the data classes / sealed subtypes.
## Measured coverage (lane-4 slice, `jacocoTestReport`, screen composables excluded)
| Package | Instruction | Line |
|---|---|---|
| ui/drafts | 100% | 100% |
| ui/outbox | 100% | 100% |
| ui/onboarding | 98.7% | 98.1% |
| ui/accountsetup | 96.2% | 98.5% |
| ui/lock | 95.7% | 93.0% |
| ui/compose | 94.0% | 99.4% |
| contacts | 93.7% | 100% |
| ui/reporting | 92.6% | 100% |
| power | 90.8% | 96.9% |
| ui/reader | 89.2% | 94.7% |
| ui/settings | 86.9% | 99.4% |
| ui/mailbox | 81.7% | 98.9% |
| reporting | 77.3% | 82.8% |
| push | 23.4% | 19.1% |
| ui (AppViewModel) | 51.1% | 100% |
| **Total in-scope slice** | **85.7%** | **91.1%** |
**Line coverage of the hand-written logic is ≥95% almost everywhere.** Two things
hold the *instruction* number below 95% and are out of a JVM lane's reach:
1. **Genuinely JVM-untestable Android classes** (left to the instrumented suite,
matching the repo's existing convention of extracting pure logic for JVM and
covering the Android seam via E2E):
- `push/IdleService` — a foreground `Service` that can't be instantiated off-device
(this single class is the entire `push` package shortfall).
- `reporting/ReportUploadScheduler` — `WorkManager.getInstance()` is a static on an
abstract class that MockK can't stub (`AbstractMethodError`); the repo's testable
`SyncScheduler` only works because it injects `Provider<WorkManager>`.
- `reporting/ReportUploadWorker` HTTP path — unreachable while
`BuildConfig.DEBUG_REPORT_ENDPOINT` is empty (the default); the constant is inlined,
so the `post()`/`onResponse()` branches can't be exercised.
- `CrashReporter.terminate` — calls `Process.killProcess` + `exitProcess`, which would
tear down the test JVM.
2. **JaCoCo deflates *instruction* coverage on Kotlin coroutine/Flow suspend state
machines** — the generated `invokeSuspend` label-dispatch and `.map{}/.combine{}`
operator synthetics count instructions that stay "missed" even when the logic is
fully exercised (hence line coverage stays ~99% while instruction dips). This is why
e.g. `ui/mailbox` reads 81.7% instruction but 98.9% line, and `ui` (AppViewModel,
itself 100%) reads 51% — its `.map{}.take(1)` Flow synthetics.
The JaCoCo verification/ratchet config is owned by capstone lane #251 and is untouched
here.
## Verification
`assembleDebug` + `testDebugUnitTest` + `compileDebugAndroidTestKotlin` + `lintDebug`
+ `ktlintCheck` + `detekt` + `jacocoTestReport` all green (JDK 21). No androidTest or
emulator E2E added — this lane adds only JVM unit tests.
Closes #249
🤖 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.
Lane 4 of the coverage epic: add JVM unit tests for the in-scope ViewModels / UI
state holders and the non-UI
reporting/push/power/contactsmodules.Test-only — no production code was changed. 28 new test files, ~230 new tests
(full suite now 781, all green).
What's covered
Drafts,Outbox,Signatures,SignatureEdit,AccountSettings,AccountSetup,ManualSetup,ProblemReports,StartupReport.Compose(contacts autocomplete, inline-imagetracking/pruning,
trySendguards, send failure,flushDraft),Mailbox(pull-to-refresh, search open/close, drawer hooks, action-failure branches,
account-removal fallback),
Reader(download success/failure, star, delete,remote-image opt-in),
Settings(read-through state + every delegator),ReportReview(thecombinestate flow + online-submit path),AppPassword.AppLog,AppVersionProvider,ReportSubmitter,ReportUploadWorker(reachable control flow),CrashReporter.install,LogEntry,IntentComposeParser,ContactsRepository,ContactsPermissionManager,IdlePushManager,BatteryOptimizationManager(Context-backed methods) and
AndroidBatteryStatusProvider.Measured coverage (lane-4 slice,
jacocoTestReport, screen composables excluded)Line coverage of the hand-written logic is ≥95% almost everywhere. Two things
hold the instruction number below 95% and are out of a JVM lane's reach:
matching the repo's existing convention of extracting pure logic for JVM and
covering the Android seam via E2E):
push/IdleService— a foregroundServicethat can't be instantiated off-device(this single class is the entire
pushpackage shortfall).reporting/ReportUploadScheduler—WorkManager.getInstance()is a static on anabstract class that MockK can't stub (
AbstractMethodError); the repo's testableSyncScheduleronly works because it injectsProvider<WorkManager>.reporting/ReportUploadWorkerHTTP path — unreachable whileBuildConfig.DEBUG_REPORT_ENDPOINTis empty (the default); the constant is inlined,so the
post()/onResponse()branches can't be exercised.CrashReporter.terminate— callsProcess.killProcess+exitProcess, which wouldtear down the test JVM.
machines — the generated
invokeSuspendlabel-dispatch and.map{}/.combine{}operator synthetics count instructions that stay "missed" even when the logic is
fully exercised (hence line coverage stays ~99% while instruction dips). This is why
e.g.
ui/mailboxreads 81.7% instruction but 98.9% line, andui(AppViewModel,itself 100%) reads 51% — its
.map{}.take(1)Flow synthetics.The JaCoCo verification/ratchet config is owned by capstone lane #251 and is untouched
here.
Verification
assembleDebug+testDebugUnitTest+compileDebugAndroidTestKotlin+lintDebugktlintCheck+detekt+jacocoTestReportall green (JDK 21). No androidTest oremulator E2E added — this lane adds only JVM unit tests.
Closes #249
🤖 Generated with Claude Code