Skip to content

test(compose): prepare tests for re-landing CMP 1.12 (fixes 8 of 10 regressions) - #6666

Merged
jamesarich merged 2 commits into
mainfrom
claude/loving-lalande-bf538c
Aug 13, 2026
Merged

test(compose): prepare tests for re-landing CMP 1.12 (fixes 8 of 10 regressions)#6666
jamesarich merged 2 commits into
mainfrom
claude/loving-lalande-bf538c

Conversation

@jamesarich

@jamesarich jamesarich commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

Why

#6664 reverted CMP 1.12.0-rc01 because it broke 10 tests. This PR fixes 8 of them, so that work does not have to be redone when the bump is re-landed.

This has no effect on current main, which is on CMP 1.11.1. The changes are test-only and correct under both 1.11.1 and 1.12.x. Their value is as re-land groundwork.

The cause is a behaviour change in compose-resources, not a bug in our code and not a wrong resource lookup. Between 1.11.1 and 1.12.0-rc01, AsyncCache.getOrLoad moved the load out of the caller's scope:

// 1.11.1 — ran in the CALLER's scope, i.e. inline on the test dispatcher
suspend fun getOrLoad(key: K, load: suspend () -> V): V = coroutineScope {
    val deferred = mutex.withLock { ... async(start = CoroutineStart.LAZY) { load() } ... }
    deferred.await()
}

// 1.12.0-rc01 — private scope with NO dispatcher, so Dispatchers.Default
private val cacheScope = CoroutineScope(SupervisorJob())
cached = SharedRequest(cacheScope.async { load() })

Every string-resource read becomes genuinely asynchronous. Lookup is still correct — values come back null, never wrong. Tests running on UnconfinedTestDispatcher that read .value on the next line, and tests that pump only advanceUntilIdle(), observe the state before it lands.

This also explains why only some tests in each class failed: the cache is process-wide, so whichever test touches a given string first pays the async cost and the rest hit it warm.

🛠️ Changes

Tests only. No production change.

  • core/testing gains TestScope.runUntilSettled { } — pumps runCurrent() and waits in real time between passes. Used by the notification and bonding tests. On 1.11.1 the predicate is already satisfied on the first pass, so it returns immediately.
  • ScannerViewModelTest instead pre-loads its five scan-failure strings. It asserts on exact virtual-time BLE retry cooldowns and therefore cannot wait in real time: any real-time suspension inside runTest causes the virtual clock to advance to the next scheduled task, firing the 30s cooldown early. That constraint is documented in the helper's KDoc so the next person doesn't rediscover it.
  • The pre-load is best-effort via safeCatchingAll (the same wrapper ScannerViewModel uses around these lookups), which absorbs both the unmocked-Resources exception and the Error skiko's initializer can raise on the JVM test classpath, while still re-throwing CancellationException. ScannerViewModelTest is in commonTest, so it also runs under testAndroidHostTest, where the stubs leave Resources.getSystem() unmocked; resources never resolve there and the ViewModel's untranslated fallback — byte-identical text, produced synchronously — is what the assertions match. That is why CI only ever showed [jvm] failures for this class.

Testing Performed

Read the CMP version column. Now that main is on 1.11.1, all 8 of these tests pass without this PR. So this PR's own CI going green proves only that nothing is broken on 1.11.1 — it is not evidence the fixes work. The evidence for that comes from the runs below marked 1.12.0-rc01, executed while the branch was based on rc01.

Check CMP Result
3× repeat, :feature:connections:testAndroidHostTest (50 tests), --rerun-tasks 1.12.0-rc01 3/3 pass
3× repeat, :core:service:testAndroidHostTest (11 tests), --rerun-tasks 1.12.0-rc01 3/3 pass
2× consecutive :feature:connections:jvmTest --tests "*ScannerViewModelTest*" 1.12.0-rc01 2/2 pass
9 per-method cold-JVM isolation runs (one test per JVM) 1.12.0-rc01 9/9 pass
Full spotlessApply spotlessCheck detekt assembleDebug test allTests 1.12.0-rc01 green except known pre-existing failures
CI run 31657204256, reports-shard-feature artifact 1.12.0-rc01 all 8 pass, first attempt
Post-rebase re-verification (44 + 50 + 11 tests, --rerun-tasks) 1.11.1 pass, entries == distinct, no hangs

Retry verification, from the JUnit XML rather than from log markers — this repo retries tests and caches a task that passes after a retry, so a test that flipped would otherwise be invisible. From CI run 31657204256 (rc01):

total entries: 2241   distinct: 2240
repeated (retried): NodeDetailCompassLifecycleTest  (the known flake — retried, failed both times)
Class CI entries source @Test
ScannerViewModelTest 88 44 × 2 targets
AndroidScannerViewModelBondingTest 6 6
MeshNotificationManagerImplTest 6 6
MeshNotificationManagerImplConversationTest 5 5

Counts match exactly with no repeats, so all 8 previously-failing tests passed on first attempt — none skipped, none passing only after a retry.

On 1.11.1 the per-test times are 0.002–0.134s, confirming runUntilSettled returns on its first pass rather than polling to its 10s timeout when the resource has already resolved inline. Passing alone would not have distinguished those two cases.

The per-method isolation runs were done specifically to rule out tests passing only because an earlier test in the same JVM warmed the process-wide cache — the order-dependence this root cause predicts. All nine pass alone on a cold cache.

Re-land prerequisites — this PR is not sufficient on its own

Re-landing CMP 1.12 needs all of the following. This PR is only the first:

  1. These test fixes (this PR) — covers 8 of the 10 failures.

  2. core/ui ConnectionsViewModelTest and feature/settings RadioConfigViewModelTest — the other 2. Same AsyncCache cause: ConnectionsViewModel dispatches its firmware notification via getStringSuspend, including title = getStringSuspend(Res.string.firmware_update_available), and the test asserts the dispatch happened after advanceUntilIdle(). They pass on 1.11.1 today only because the revert stepped off rc01; they will fail again the moment the bump returns.

  3. The blocking runBlocking resource lookup at core/resources/.../GetString.kt:2629 call sites across 6 files. This is the production-side face of the same finding, and on 1.12 it is a permanent, unrecoverable hang, not a slow call:

    ServiceScope is CoroutineScope(dispatchers.default + SupervisorJob()) (core/service/.../di/CoreServiceModule.kt:33), so the packet/notification pipeline runs on Dispatchers.Default. On 1.12 getString() blocks its calling thread while the load it awaits is dispatched to Dispatchers.Default — so a blocking caller on a Default worker parks that worker awaiting work that needs a Default worker. Once enough callers do this concurrently, the pool is starved: every subsequent getString in the process hangs forever, main thread included, with no recovery short of a restart. jstack shows Default workers parked on BlockingCoroutine. A message burst is the trigger.

    The measured threshold was 16 concurrent blocking callers, but that number is the Dispatchers.Default parallelism of the machine it was measured on (= CPU count). On an 8-core phone it wedges at 8, so real devices are easier to wedge than the dev machine, not harder.

    Separately, the first cold getString costs ~197.7ms on the main thread, paid by initChannels() during MeshService.onCreate.

    Benign on 1.11.1 only because resolution was inline. A mitigation migrating Default-reachable sites to getStringSuspend is staged on claude/sad-gagarin-2f1ff1. CMP 1.12 must not be re-landed before this is resolved.

  4. Removing the Renovate rule revert(deps): back out CMP 1.12.0-rc01 until its test regressions are fixed #6664 added to .github/renovate.json blocking 1.12.0-rc01, which is now on main.

Out of scope

  • NodeDetailCompassLifecycleTest — genuinely not a CMP regression. Codecov reports a 66.67% failure rate on main and it reproduces on 1.11.1. An earlier revision of this branch raised its waitUntil timeout; that was reverted once the flake data showed the timeout was treating a pre-existing flake as CMP fallout. Owned separately.

@coderabbitai

coderabbitai Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds runUntilSettled for asynchronous test state and updates notification, scanner, and bonding tests to wait for required state or preload resources before assertions.

Changes

Asynchronous test settling

Layer / File(s) Summary
Coroutine settling utility
core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/TestScopes.kt
Adds TestScope.runUntilSettled with scheduler draining, predicate checks, real-time timeout handling, and dispatcher delays.
Notification state waits
core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/MeshNotificationManagerImpl*.kt
Replaces unconditional scheduler idling with waits for active service and low-battery notifications.
Scanner failure setup and waits
feature/connections/src/*Test/kotlin/org/meshtastic/feature/connections/*Test.kt
Waits for asynchronous bonding errors and preloads scan-failure resources before assertions.

Estimated code review effort: 2 (Simple) | ~10 minutes

Mergeability Score: 🔵 Low · up to ee7e8

This PR changes test synchronization for asynchronous resource loading and does not modify production code. A bounded concern remains because the new suspend test helper may swallow cancellation, so the change is mergeable with explicit owner follow-up to preserve cancellation semantics.

Possibly related PRs

Suggested reviewers: jeremiah-k

🚥 Pre-merge checks | ✅ 6
✅ Passed checks (6 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Sibling Call Sites And Presence Semantics ✅ Passed The diff changes only test code and test support. It adds async settling/resource warming and introduces no nullable field, zero guard, presence check, or zero-default physical field.
Tests Prove The Path, Not The End State ✅ Passed Changed tests wait for real side effects and verify IDs/tags, bond call counts, scan attempts, and error messages; resource warming is not fake-state seeding, and no Unconfined emission order is ad...
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the test updates for Compose Multiplatform 1.12 and the regression fixes described in the pull request.

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
`@feature/connections/src/commonTest/kotlin/org/meshtastic/feature/connections/ScannerViewModelTest.kt`:
- Around line 754-761: Update warmScanFailureStrings() to use
org.meshtastic.core.common.util.safeCatching instead of runCatching, preserving
the existing resource-warming operations while allowing CancellationException to
propagate.

Apply the same fix in
`@feature/connections/src/commonTest/kotlin/org/meshtastic/feature/connections/ScannerViewModelTest.kt`
around lines 743 - 749.
🪄 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: d570b1bb-c457-46d9-bb5c-bd3d93985e5d

📥 Commits

Reviewing files that changed from the base of the PR and between 1b89b63 and ee7e8a6.

📒 Files selected for processing (5)
  • core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/MeshNotificationManagerImplConversationTest.kt
  • core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/MeshNotificationManagerImplTest.kt
  • core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/TestScopes.kt
  • feature/connections/src/androidHostTest/kotlin/org/meshtastic/feature/connections/AndroidScannerViewModelBondingTest.kt
  • feature/connections/src/commonTest/kotlin/org/meshtastic/feature/connections/ScannerViewModelTest.kt

@github-actions

This comment has been minimized.

@codecov

codecov Bot commented Aug 13, 2026

Copy link
Copy Markdown

❌ 1 Tests Failed:

Tests completed Failed Passed Skipped
6013 1 6012 0
View the full list of 1 ❄️ flaky test(s)
org.meshtastic.feature.node.detail.NodeDetailCompassLifecycleTest::compassSelectionFollowsScreenLifecycleAndDismissal()[jvm]

Flake rate in main: 66.67% (Passed 2 times, Failed 4 times)

Stack Traces | 17.1s run time
androidx.compose.ui.test.ComposeTimeoutException: Condition still not satisfied after 1000 ms
	at androidx.compose.ui.test.SkikoComposeUiTest.waitUntil(ComposeUiTest.skiko.kt:468)
	at androidx.compose.ui.test.ComposeUiTest.waitUntil$default(ComposeUiTest.kt:228)
	at org.meshtastic.feature.node.detail.NodeDetailCompassLifecycleTest$compassSelectionFollowsScreenLifecycleAndDismissal$1.invokeSuspend(NodeDetailCompassLifecycleTest.kt:140)
	at org.meshtastic.feature.node.detail.NodeDetailCompassLifecycleTest$compassSelectionFollowsScreenLifecycleAndDismissal$1.invoke(NodeDetailCompassLifecycleTest.kt)
	at org.meshtastic.feature.node.detail.NodeDetailCompassLifecycleTest$compassSelectionFollowsScreenLifecycleAndDismissal$1.invoke(NodeDetailCompassLifecycleTest.kt)
	at androidx.compose.ui.test.v2.ComposeUiTest_skikoKt$runComposeUiTest$1.invokeSuspend(ComposeUiTest.skiko.kt:89)
	at androidx.compose.ui.test.v2.ComposeUiTest_skikoKt$runComposeUiTest$1.invoke(ComposeUiTest.skiko.kt)
	at androidx.compose.ui.test.v2.ComposeUiTest_skikoKt$runComposeUiTest$1.invoke(ComposeUiTest.skiko.kt)
	at androidx.compose.ui.test.SkikoComposeUiTest$runTest$1$1$1$1$1.invokeSuspend(ComposeUiTest.skiko.kt:276)
	at androidx.compose.ui.test.SkikoComposeUiTest$runTest$1$1$1$1$1.invoke(ComposeUiTest.skiko.kt)
	at androidx.compose.ui.test.SkikoComposeUiTest$runTest$1$1$1$1$1.invoke(ComposeUiTest.skiko.kt)
	at androidx.compose.ui.platform.FrameRecomposer$withMonotonicFrameClock$2.invokeSuspend(FrameRecomposer.skiko.kt:182)
	at androidx.compose.ui.platform.FrameRecomposer$withMonotonicFrameClock$2.invoke(FrameRecomposer.skiko.kt)
	at androidx.compose.ui.platform.FrameRecomposer$withMonotonicFrameClock$2.invoke(FrameRecomposer.skiko.kt)
	at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatched(Undispatched.kt:66)
	at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:43)
	at kotlinx.coroutines.BuildersKt__Builders_commonKt.withContext(Builders.common.kt:497)
	at kotlinx.coroutines.BuildersKt.withContext(Unknown Source)
	at androidx.compose.ui.platform.FrameRecomposer.withMonotonicFrameClock(FrameRecomposer.skiko.kt:181)
	at androidx.compose.ui.test.SkikoComposeUiTest$runTest$1.invokeSuspend(ComposeUiTest.skiko.kt:275)
	at androidx.compose.ui.test.SkikoComposeUiTest$runTest$1.invoke(ComposeUiTest.skiko.kt)
	at androidx.compose.ui.test.SkikoComposeUiTest$runTest$1.invoke(ComposeUiTest.skiko.kt)
	at kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTest$2$1$1.invokeSuspend(TestBuilders.kt:317)
	at _COROUTINE._BOUNDARY._(CoroutineDebugging.kt:42)
	at androidx.compose.ui.test.SkikoComposeUiTest$runTest$1.invokeSuspend(ComposeUiTest.skiko.kt:275)
	at kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTest$2$1$1.invokeSuspend(TestBuilders.kt:317)
Caused by: androidx.compose.ui.test.ComposeTimeoutException: Condition still not satisfied after 1000 ms
	at androidx.compose.ui.test.SkikoComposeUiTest.waitUntil(ComposeUiTest.skiko.kt:468)
	at androidx.compose.ui.test.ComposeUiTest.waitUntil$default(ComposeUiTest.kt:228)
	at org.meshtastic.feature.node.detail.NodeDetailCompassLifecycleTest$compassSelectionFollowsScreenLifecycleAndDismissal$1.invokeSuspend(NodeDetailCompassLifecycleTest.kt:140)
	at org.meshtastic.feature.node.detail.NodeDetailCompassLifecycleTest$compassSelectionFollowsScreenLifecycleAndDismissal$1.invoke(NodeDetailCompassLifecycleTest.kt)
	at org.meshtastic.feature.node.detail.NodeDetailCompassLifecycleTest$compassSelectionFollowsScreenLifecycleAndDismissal$1.invoke(NodeDetailCompassLifecycleTest.kt)
	at androidx.compose.ui.test.v2.ComposeUiTest_skikoKt$runComposeUiTest$1.invokeSuspend(ComposeUiTest.skiko.kt:89)
	at androidx.compose.ui.test.v2.ComposeUiTest_skikoKt$runComposeUiTest$1.invoke(ComposeUiTest.skiko.kt)
	at androidx.compose.ui.test.v2.ComposeUiTest_skikoKt$runComposeUiTest$1.invoke(ComposeUiTest.skiko.kt)
	at androidx.compose.ui.test.SkikoComposeUiTest$runTest$1$1$1$1$1.invokeSuspend(ComposeUiTest.skiko.kt:276)
	at androidx.compose.ui.test.SkikoComposeUiTest$runTest$1$1$1$1$1.invoke(ComposeUiTest.skiko.kt)
	at androidx.compose.ui.test.SkikoComposeUiTest$runTest$1$1$1$1$1.invoke(ComposeUiTest.skiko.kt)
	at androidx.compose.ui.platform.FrameRecomposer$withMonotonicFrameClock$2.invokeSuspend(FrameRecomposer.skiko.kt:182)
	at androidx.compose.ui.platform.FrameRecomposer$withMonotonicFrameClock$2.invoke(FrameRecomposer.skiko.kt)
	at androidx.compose.ui.platform.FrameRecomposer$withMonotonicFrameClock$2.invoke(FrameRecomposer.skiko.kt)
	at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatched(Undispatched.kt:66)
	at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:43)
	at kotlinx.coroutines.BuildersKt__Builders_commonKt.withContext(Builders.common.kt:497)
	at kotlinx.coroutines.BuildersKt.withContext(Unknown Source)
	at androidx.compose.ui.platform.FrameRecomposer.withMonotonicFrameClock(FrameRecomposer.skiko.kt:181)
	at androidx.compose.ui.test.SkikoComposeUiTest$runTest$1.invokeSuspend(ComposeUiTest.skiko.kt:275)
	at androidx.compose.ui.test.SkikoComposeUiTest$runTest$1.invoke(ComposeUiTest.skiko.kt)
	at androidx.compose.ui.test.SkikoComposeUiTest$runTest$1.invoke(ComposeUiTest.skiko.kt)
	at kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTest$2$1$1.invokeSuspend(TestBuilders.kt:317)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:34)
	at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:100)
	at kotlinx.coroutines.test.TestDispatcher.processEvent$kotlinx_coroutines_test(TestDispatcher.kt:24)
	at kotlinx.coroutines.test.TestCoroutineScheduler.tryRunNextTaskUnless$kotlinx_coroutines_test(TestCoroutineScheduler.kt:98)
	at kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTest$2$1$workRunner$1.invokeSuspend(TestBuilders.kt:326)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:34)
	at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:100)
	at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:256)
	at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:54)
	at kotlinx.coroutines.BuildersKt__BuildersKt.runBlockingImpl(Builders.kt:30)
	at kotlinx.coroutines.BuildersKt.runBlockingImpl(Unknown Source)
	at kotlinx.coroutines.BuildersKt__Builders_concurrentKt.runBlockingK(Builders.concurrent.kt:172)
	at kotlinx.coroutines.BuildersKt.runBlockingK(Unknown Source)
	at kotlinx.coroutines.BuildersKt__Builders_concurrentKt.runBlockingK$default(Builders.concurrent.kt:157)
	at kotlinx.coroutines.BuildersKt.runBlockingK$default(Unknown Source)
	at kotlinx.coroutines.test.TestBuildersJvmKt.createTestResult(TestBuildersJvm.kt:10)
	at kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt.runTest-8Mi8wO0(TestBuilders.kt:309)
	at kotlinx.coroutines.test.TestBuildersKt.runTest-8Mi8wO0(TestBuilders.kt:1)
	at kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt.runTest-8Mi8wO0(TestBuilders.kt:167)
	at kotlinx.coroutines.test.TestBuildersKt.runTest-8Mi8wO0(TestBuilders.kt:1)
	at androidx.compose.ui.test.SkikoComposeUiTest.runTest(ComposeUiTest.skiko.kt:263)
	at androidx.compose.ui.test.v2.ComposeUiTest_skikoKt.runSkikoComposeUiTest-uV_hrag(ComposeUiTest.skiko.kt:137)
	at androidx.compose.ui.test.v2.ComposeUiTest_skikoKt.runSkikoComposeUiTest-uV_hrag$default(ComposeUiTest.skiko.kt:119)
	at androidx.compose.ui.test.v2.ComposeUiTest_skikoKt.runComposeUiTest-exY8QGI(ComposeUiTest.skiko.kt:84)
	at androidx.compose.ui.test.v2.ComposeUiTest_skikoKt.runComposeUiTest-exY8QGI$default(ComposeUiTest.skiko.kt:78)
	at org.meshtastic.feature.node.detail.NodeDetailCompassLifecycleTest.compassSelectionFollowsScreenLifecycleAndDismissal(NodeDetailCompassLifecycleTest.kt:92)

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

@jamesarich

Copy link
Copy Markdown
Collaborator Author

CI red is the known compass flake, not this PR

shard-feature failed with exactly one test, and it is the one this PR explicitly does not touch:

NodeDetailCompassLifecycleTest::compassSelectionFollowsScreenLifecycleAndDismissal()[jvm] — Codecov reports a 66.67% flake rate on main (passed 2, failed 4), and it reproduces on CMP 1.11.1 as well as 1.12.0-rc01, so it is not a CMP regression. It is owned by a separate task. An earlier revision of this branch raised its waitUntil timeout; that change was reverted once the main-branch flake data showed the timeout was treating a pre-existing flake as CMP fallout.

Parsing the reports-shard-feature artifact from run 31657204256:

total entries: 2241   distinct: 2240
repeated (retried): NodeDetailCompassLifecycleTest::compassSelectionFollowsScreenLifecycleAndDismissal
failures:           NodeDetailCompassLifecycleTest::compassSelectionFollowsScreenLifecycleAndDismissal

The four classes this PR fixes, as executed in that same CI run, against their source @Test counts:

Class CI entries source @Test
ScannerViewModelTest 88 44 × 2 targets (jvmTest + testAndroidHostTest)
AndroidScannerViewModelBondingTest 6 6
MeshNotificationManagerImplTest 6 6
MeshNotificationManagerImplConversationTest 5 5

Counts match exactly with no repeats, so all 8 previously-failing tests passed on first attempt — none was skipped, and none passed only after a retry. That matters because this repo retries tests and caches a task that passes after a retry, which can hide a flip; entries == distinct rules that out positively rather than by absence of log markers. Note the instrument working as intended: the only entry with entries > distinct is the compass test, retried and failed both times.

jamesarich and others added 2 commits August 12, 2026 21:03
…bump

CMP 1.12.0-rc01 changed `AsyncCache.getOrLoad` in compose-resources to run
each load on a private `Dispatchers.Default` scope instead of the caller's,
so every string-resource read is now genuinely asynchronous. Tests that read
state on the next line, or that pump only `advanceUntilIdle()`, observed it
before it landed.

Adds `TestScope.runUntilSettled` for tests that just need the value to
arrive, and pre-loads the scan-failure strings in ScannerViewModelTest,
which asserts on exact virtual-time BLE cooldowns and so cannot wait in
real time.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
runCatching swallows CancellationException. Use safeCatchingAll, which the
ViewModel already wraps these same lookups in: it re-throws
CancellationException while still absorbing both the unmocked-Resources
exception under androidHostTest and the Error skiko's initializer can raise
on the JVM test classpath.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@jamesarich jamesarich changed the title test(compose): settle asynchronous resource loads after the CMP 1.12 bump test(compose): prepare tests for re-landing CMP 1.12 (fixes 8 of 10 regressions) Aug 13, 2026
@jamesarich
jamesarich force-pushed the claude/loving-lalande-bf538c branch from 5bb4b83 to 305a04a Compare August 13, 2026 02:06
@jamesarich
jamesarich added this pull request to the merge queue Aug 13, 2026
Merged via the queue into main with commit 83851c0 Aug 13, 2026
15 checks passed
@jamesarich
jamesarich deleted the claude/loving-lalande-bf538c branch August 13, 2026 11:47
jamesarich added a commit that referenced this pull request Aug 13, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

testing Test additions or modifications

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant