test: keep ViewModel coroutines inside the test that started them - #6669
Conversation
CMP 1.12.0-rc01 moved compose-resources string loading onto a library-owned Dispatchers.Default scope (AsyncCache), so a cold getStringSuspend completes on a real thread. That broke both classes: advanceUntilIdle could no longer drain the load, and a viewModelScope still awaiting one resumed on Dispatchers.Main after resetMain(), failing whichever test started next. #6664 reverted the bump, so both are green again — but the viewModelScope leak was always latent on 1.11.1 and returns the moment 1.12 is re-landed. RadioConfigViewModelTest: track every ViewModel the class builds and cancel its scope in tearDown before resetMain(). Cancelling the awaiting continuation disposes the ResumeAwaitOnCompletion handler, so a deferred completing later no longer touches Main. Also cancel the previously uncancelled NodeRestartTracker scope, and fold the one hand-inlined ViewModel construction into createViewModel so it cannot escape registration. ConnectionsViewModelTest: pre-load the three firmware-notification strings before the test dispatcher is installed, so the awaits are already complete and the notification path stays inside virtual time. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
📝 WalkthroughWalkthroughThe changes improve asynchronous test setup and cleanup. Connections tests preload firmware notification resources. Radio configuration tests track and cancel ViewModel and tracker coroutine scopes during teardown. ChangesTest stability updates
Estimated code review effort: 2 (Simple) | ~10 minutes Mergeability Score: 🔵 Low · up to The change keeps ViewModel-related test behavior inside the originating test, but the new firmware-resource warmup could abort test setup on desktop JVM initialization failures and prevent those tests from running; merge is reasonable with explicit owner awareness or a follow-up to guard the warmup. Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 6✅ Passed checks (6 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/viewmodel/ConnectionsViewModelTest.kt`:
- Around line 296-305: Update warmFirmwareNotificationStrings to wrap all three
getString calls in a single safeCatchingAll block, preserving
CancellationException propagation while preventing ExceptionInInitializerError
during initialization from aborting setup.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 794334be-6469-47ea-8b37-1365eee35944
📒 Files selected for processing (2)
core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/viewmodel/ConnectionsViewModelTest.ktfeature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt
safeCatchingAll exists for the ExceptionInInitializerError compose resources can raise from skiko's lazy static init on the desktop JVM test classpath, and five production call sites already wrap getStringSuspend in it. The warm-up runs on that same classpath, so an unwrapped failure there would fail setUp and take down all ten tests in the class. Warming is an optimisation, not an assertion: if it cannot load, the suite should behave exactly as it did before it existed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reverts d8361cc, restoring #6662's bump now that every regression it caused has a fix on main: - #6666 settles asynchronous resource loads in the 8 tests that failed deterministically (:core:service, :feature:connections) - #6669 keeps ViewModel coroutines inside the test that started them, covering ConnectionsViewModelTest and RadioConfigViewModelTest - #6668 removes the runBlocking getString shim from every Dispatchers.Default-reachable notification path, which is what made the bump a production hazard rather than only a test one Also drops the Renovate rule that blocked 1.12.0-rc01, since its lift condition is now met. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Why
RadioConfigViewModelTestandConnectionsViewModelTestwere red for two days and cost several sessions a lot of misattribution. The root cause was the CMP 1.12.0-rc01 bump (#6662), reverted by #6664 — so both classes are green onmaintoday and this PR does not fix a live failure.It is prevention. CMP 1.12 will be re-landed (#6666 is the groundwork; the Renovate block is marked liftable), and one of these two defects is latent right now and will come straight back with it.
CMP 1.12.0-rc01 changed
compose-resources'AsyncCache:A cold
getStringSuspendnow completes on a real thread, which breaks two test assumptions:advanceUntilIdle()cannot drain it, so anything downstream of a string lookup lands after the assertions.viewModelScopestill awaiting one at test end resumes onDispatchers.MainafterDispatchers.resetMain()→Module with the Main dispatcher is missing, reported asUncaughtExceptionsBeforeTestagainst whichever test starts next.The second one is the important part:
viewModelScopeis not a child ofrunTest, so nothing cancels it when the test body returns. That was true on 1.11.1 too — the bump only made it reachable. Leaving it unfixed means the re-land reintroduces two red classes.🧹 Changes
RadioConfigViewModelTest— track every ViewModel the class builds and cancel its scope intearDownbeforeresetMain(). Cancelling the awaiting continuation disposes theResumeAwaitOnCompletionhandler, so a deferred completing later on a background thread no longer touchesMain. Also cancels the previously uncancelledNodeRestartTrackerscope, and folds the one hand-inlined ViewModel construction intocreateViewModelso it cannot escape registration.ConnectionsViewModelTest— pre-load the three firmware-notification strings before the test dispatcher is installed, so the awaits are already complete and the notification path stays inside virtual time. The warm-up is wrapped insafeCatchingAll: it runs on the desktop JVM test classpath where compose-resources can raiseExceptionInInitializerErrorfrom skiko's lazy static init, and warming is an optimisation rather than an assertion — if it cannot load, the suite must behave exactly as it did before it existed.Test-only; no production code touched.
Testing Performed
Counted
<failure>elements in the JUnit XML rather than trusting the Gradle result — this build retries twice withfailOnPassedAfterRetry=false, so a green build can hide a failed first attempt.RadioConfigViewModelTestUncaughtExceptionsBeforeTest/ missing Main dispatcher)ConnectionsViewModelTestRadioConfigViewModelTestConnectionsViewModelTestConnectionsViewModelTestThe rc01 rows are the load-bearing evidence — on this base these tests pass without the change, so CI green here does not demonstrate the fix.
Full baseline on this base:
spotlessApply spotlessCheck✅ ·detekt✅ ·assembleDebug✅ ·:core:ui+:feature:settingstests ✅.Notes for reviewers
ConnectionsViewModelTestwas not order-dependent, despite being described that way — it failed 100% as a single method in isolation.RadioConfigViewModelTesthas 58@Testmethods but reported 59 and 60<testcase>entries while flaking. Retry attempts appear as extra entries, so a testcase count above the source count is a retry signal needing no failure text.NodeDetailCompassLifecycleTestis unrelated to this PR — CMP-version-independent and still flaky onmain; being handled separately.🤖 Generated with Claude Code