Fixes a genuinely-flaky instrumented test root-caused from an API-37 CI failure: SignaturesScreenTest > tappingRadioOnNonDefault_makesItTheDefault intermittently failed with android.database.SQLException: connection is closed (SQLITE_MISUSE 21) due to a Room / viewModelScope teardown race.
Root cause
SignaturesScreenTest builds a real SignaturesViewModel by hand and tore down with a bare @After fun tearDown() = db.close() that never cancelled viewModelScope. The ViewModel's signatures StateFlow is a Room InvalidationTracker Flow kept alive by stateIn(WhileSubscribed(5_000)), so the collector stayed live up to 5s after the UI detached. Sequence: radio-tap write → Room invalidates signatures → waitUntil returns → test ends → db.close() → the still-live collector re-runs observeForAccount().prepare() on the now-closed connection → SQLITE_MISUSE. Timing-dependent, hence intermittent.
Fix
Hold the ViewModel in an androidx.lifecycle.ViewModelStore and, in @After, call store.clear() (→ ViewModel.onCleared() → cancels viewModelScope) beforedb.close(), so the collector is gone before the DB closes. Behaviour and assertions are unchanged; the race is removed by construction.
Siblings audited + fixed (same hazard)
AccountSettingsScreenTest — same live-Room-Flow-vs-close race, previously worked around by never closing the in-memory DB at all. Now clears the ViewModel, then closes the DB.
ComposeScreenTest — ComposeViewModel.init launches a viewModelScope coroutine that reads the real accountSettings/signature Room repos; clears the store before db.close() to avoid the same in-flight-read-vs-close race.
(The remaining *ScreenTest classes that close a Room DB either use fakes with no real viewModelScope, or observe non-Room stores — none hit this hazard.)
Verification
:app:connectedDebugAndroidTestgreen (12/12) for SignaturesScreenTest, AccountSettingsScreenTest, ComposeScreenTest on a cold-booted local emulator.
Note: the flake is intermittent, so one green run cannot prove it is gone — but the fix eliminates the race by construction (the collector is cancelled before the connection closes). CI runs the full multi-API E2E matrix.
## Summary
Fixes a genuinely-flaky instrumented test root-caused from an API-37 CI failure: `SignaturesScreenTest > tappingRadioOnNonDefault_makesItTheDefault` intermittently failed with `android.database.SQLException: connection is closed` (SQLITE_MISUSE 21) due to a Room / `viewModelScope` teardown race.
### Root cause
`SignaturesScreenTest` builds a real `SignaturesViewModel` by hand and tore down with a bare `@After fun tearDown() = db.close()` that **never cancelled `viewModelScope`**. The ViewModel's `signatures` StateFlow is a Room `InvalidationTracker` Flow kept alive by `stateIn(WhileSubscribed(5_000))`, so the collector stayed live up to 5s after the UI detached. Sequence: radio-tap write → Room invalidates `signatures` → `waitUntil` returns → test ends → `db.close()` → the still-live collector re-runs `observeForAccount().prepare()` on the now-closed connection → SQLITE_MISUSE. Timing-dependent, hence intermittent.
### Fix
Hold the ViewModel in an `androidx.lifecycle.ViewModelStore` and, in `@After`, call `store.clear()` (→ `ViewModel.onCleared()` → cancels `viewModelScope`) **before** `db.close()`, so the collector is gone before the DB closes. Behaviour and assertions are unchanged; the race is removed by construction.
### Siblings audited + fixed (same hazard)
- **`AccountSettingsScreenTest`** — same live-Room-Flow-vs-close race, previously worked around by *never closing* the in-memory DB at all. Now clears the ViewModel, then closes the DB.
- **`ComposeScreenTest`** — `ComposeViewModel.init` launches a `viewModelScope` coroutine that reads the real accountSettings/signature Room repos; clears the store before `db.close()` to avoid the same in-flight-read-vs-close race.
(The remaining `*ScreenTest` classes that close a Room DB either use fakes with no real `viewModelScope`, or observe non-Room stores — none hit this hazard.)
### Verification
- `:app:connectedDebugAndroidTest` **green (12/12)** for `SignaturesScreenTest`, `AccountSettingsScreenTest`, `ComposeScreenTest` on a cold-booted local emulator.
- JVM fast gate green: `assembleDebug`, `testDebugUnitTest`, `jacocoTestCoverageVerification`, `compileDebugAndroidTestKotlin`, `lintDebug`, `ktlintCheck`, `detekt`.
Note: the flake is intermittent, so one green run cannot *prove* it is gone — but the fix eliminates the race by construction (the collector is cancelled before the connection closes). CI runs the full multi-API E2E matrix.
🤖 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
Fixes a genuinely-flaky instrumented test root-caused from an API-37 CI failure:
SignaturesScreenTest > tappingRadioOnNonDefault_makesItTheDefaultintermittently failed withandroid.database.SQLException: connection is closed(SQLITE_MISUSE 21) due to a Room /viewModelScopeteardown race.Root cause
SignaturesScreenTestbuilds a realSignaturesViewModelby hand and tore down with a bare@After fun tearDown() = db.close()that never cancelledviewModelScope. The ViewModel'ssignaturesStateFlow is a RoomInvalidationTrackerFlow kept alive bystateIn(WhileSubscribed(5_000)), so the collector stayed live up to 5s after the UI detached. Sequence: radio-tap write → Room invalidatessignatures→waitUntilreturns → test ends →db.close()→ the still-live collector re-runsobserveForAccount().prepare()on the now-closed connection → SQLITE_MISUSE. Timing-dependent, hence intermittent.Fix
Hold the ViewModel in an
androidx.lifecycle.ViewModelStoreand, in@After, callstore.clear()(→ViewModel.onCleared()→ cancelsviewModelScope) beforedb.close(), so the collector is gone before the DB closes. Behaviour and assertions are unchanged; the race is removed by construction.Siblings audited + fixed (same hazard)
AccountSettingsScreenTest— same live-Room-Flow-vs-close race, previously worked around by never closing the in-memory DB at all. Now clears the ViewModel, then closes the DB.ComposeScreenTest—ComposeViewModel.initlaunches aviewModelScopecoroutine that reads the real accountSettings/signature Room repos; clears the store beforedb.close()to avoid the same in-flight-read-vs-close race.(The remaining
*ScreenTestclasses that close a Room DB either use fakes with no realviewModelScope, or observe non-Room stores — none hit this hazard.)Verification
:app:connectedDebugAndroidTestgreen (12/12) forSignaturesScreenTest,AccountSettingsScreenTest,ComposeScreenTeston a cold-booted local emulator.assembleDebug,testDebugUnitTest,jacocoTestCoverageVerification,compileDebugAndroidTestKotlin,lintDebug,ktlintCheck,detekt.Note: the flake is intermittent, so one green run cannot prove it is gone — but the fix eliminates the race by construction (the collector is cancelled before the connection closes). CI runs the full multi-API E2E matrix.
🤖 Generated with Claude Code