Skip to content

fix(database): recover from Room's silent connection-pool wedge (#6608) - #6658

Merged
jamesarich merged 3 commits into
mainfrom
claude/eloquent-babbage-9c585b
Aug 13, 2026
Merged

fix(database): recover from Room's silent connection-pool wedge (#6608)#6658
jamesarich merged 3 commits into
mainfrom
claude/eloquent-babbage-9c585b

Conversation

@jamesarich

@jamesarich jamesarich commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

Why

Field reports in #6608 describe a permanent wedge on 2.8.1-SNAPSHOT: the node list empties and never reloads, updates stop, and only a force-stop recovers. The attached logcats pinned it to a leaked permit in Room 3.0.1's ConnectionPoolImpl — but reading them closely changes the diagnosis in a way that matters:

W/System.err: android.database.SQLException: Error code: 5, message: Timed out attempting to acquire a reader connection.
  Request coroutine: [..., DispatchedCoroutine{Cancelling}@667fccf, Dispatchers.IO.limitedParallelism(1)]
  Writer pool: Pool@b1ce8e7 (capacity=1, permits=0, queue=(size=0)[])
    [1] - BundledSQLiteConnection@3efbc94   Status: Free connection
  at androidx.room3.coroutines.ConnectionPoolImpl.onTimeout(ConnectionPoolImpl.kt:196)
  at androidx.room3.coroutines.Pool.acquireWithTimeout-KLykuaI(ConnectionPoolImpl.kt:242)

That is W/System.err — a printStackTrace() from Room's LOG_TIMEOUT_EXCEPTION mode, not an exception delivered to us. Across the three logs there are 156 dumps in a single second and zero app-side recovery log lines. Room's acquireWithTimeout logs the timeout and loops forever, so a wedged pool produces neither a value nor a failure: every DAO flow simply hangs.

Everything we had built for this failure keyed off a thrown SQLException, so none of it could ever fire — the recovery caps in DatabaseManager were a red herring, not the cause. On top of that, NodeRepositoryImpl.nodeDBbyNum is stateIn(processLifecycle, SharingStarted.Eagerly, …); once its upstream terminates, the sharing coroutine is dead for the process and re-navigation cannot restart it. That is the "node list disappears and never reloads" symptom.

setSingleConnectionPool() and the limitedParallelism(1) query context are unchanged — both remain load-bearing (see the configureCommon KDoc).

🐛 Fixes

  • Detect the silent stall. observeCurrentDb now bounds the first emission of each re-latched DAO flow (45s, comfortably above any legitimate cold-open/migration/backfill-contended query, and above Room's own 30s acquire timeout). A missing first emission raises DbFlowStalledException, which routes into the existing pool-reopen recovery. Only the first emission per latch is bounded — later emissions are event-driven and legitimately sparse.
  • Keep recovery available for the life of the process. MAX_FLOW_POOL_RECOVERIES_PER_MANAGER_LIFETIME became MAX_FLOW_POOL_RECOVERIES_PER_WINDOW over a sliding 10-minute window. Reporters see wedges "multiple times within minutes", which exhausted the old lifetime budget and made every later wedge permanent by construction.
  • Restart eagerly-shared repository flows. retryOnDbPoolFailure restarts a database-backed flow after a recoverable pool failure with capped exponential backoff (reset on a successful emission; non-database failures still propagate). Applied to every observeCurrentDb call site, so a terminal failure can no longer kill a process-lifetime SharingStarted.Eagerly StateFlow.

🧹 Cleanups

  • RadioConfigRepositoryImplTest builds SwitchingChannelSetDataSource with the real FakeDatabaseProvider instead of a mokkery autofill mock, which returns null for the non-null DAO flow.

Known remaining gap (not fixed here)

The one-shot write path has the same exposure and is not addressed: withDb runs its callback under NonCancellable on dispatchers.io.limitedParallelism(1), so a callback wedged inside Room can neither be cancelled nor timed out, and every later withDb queues behind it forever. Reopening the pool does not help because the lane never drains. That is the likely cause of the "disconnect impossible" half of #6608 and needs its own design — a bounded-wait scheme or a per-pool lane — rather than a timeout that cannot interrupt a NonCancellable block.

The TAK-server/session-lease backpressure wedge that drives the cancellation churn is being handled on a separate track.

Testing Performed

Baseline gate green: spotlessApply spotlessCheck detekt assembleDebug test allTests kmpSmokeCompile.

New tests:

  • DatabaseManagerShutdownTest.silentlyStalledFlowIsRecoveredWithoutAnyThrownFailure — a DAO flow that never emits and never fails (exactly what a leaked permit looks like) is recovered.
  • …quietFlowAfterAFirstEmissionIsNotTreatedAsStalled — a flow that emits once then stays quiet for 3× the deadline does not trigger recovery.
  • …flowPoolRecoveryResumesAfterTheRateWindowClears — the [Bug]: Stale node connection still occurring after fix for #6491 #6608 regression: after the budget is spent, a later wedge still recovers once the window clears.
  • …recurringFlowPoolRecoveryNeverClosesDetachedPoolsBeforeShutdown — recovery retains every replaced pool and only close() reclaims them.
  • DbFlowRecoveryTest — restart on acquire-timeout and closed-pool failures, propagation of unrelated failures, backoff reset after a successful emission.
  • PoisonedPoolNodeFlowRecoveryTest — poisons the pool for far more failures than any recovery budget allows and proves nodeDBbyNumFlow, myNodeInfoFlow, and an eagerly-shared StateFlow over them still reach a value.

Rebased onto #6661 — and this changes one policy it just shipped

#6661 landed first and rewrote withDb, so this is rebased onto it. Its hasReachedRecoveryLimit(origin) / recordRecovery(origin) seam is kept and my sliding window moved inside it, which is cleaner than either branch alone.

The policy change, stated plainly: #6661 bounded wedge recovery with MAX_WEDGE_POOL_RECOVERIES_PER_MANAGER_LIFETIME = 3 and quarantined the pool once spent. That is the same lifetime-budget shape this PR removes for the Flow path, and it has the same consequence. The quarantine clears in only three places — closeInactiveDatabase, the reopen-replacement path, and close(). The quarantined pool is _currentDb, so it is never an inactive eviction candidate, and the reopen path is exactly what a spent budget refuses. So after three wedges in one process, writes fail fast until a device switch or a force-stop. Given #6608 reporters see wedges recurring within minutes, three arrives quickly.

Both budgets are now one sliding window (POOL_RECOVERY_WINDOW_MS), with per-origin deques and per-origin limits (6 Flow, 3 wedge). Per-origin so a Flow-recovery storm cannot starve the write path's budget; limits kept distinct so #6661's chosen sensitivity is preserved rather than silently re-tuned.

Quarantine expiry is the load-bearing half. Windowing the budget alone would change nothing observable: recovery is refused until a write is admitted, and writes are refused until recovery happens. The quarantine therefore expires with the same window. It is evaluated in the quarantine map rather than against the budget deques because the gate holds writerTrackerMutex while the deques are guarded by mutex, and the established order is mutexwriterTrackerMutex; reading the budget there would invert it.

Expiry re-admits against the still-wedged pool, since nothing replaced it — so the first write back pays one WITH_DB_TIMEOUT_MS deadline and its abandonment publishes the replacement. That sacrificial write is the guaranteed floor; when DAO Flows are collecting, a stall recovery usually replaces the pool sooner. writesRecoverAfterTheWedgeWindowClears pins the whole cycle rather than stopping at re-admission, which would pass even if recovery never completed.

History: the CMP 1.12.0-rc01 detour

Earlier revisions of this description reported main as red and attributed CI failures across three theories. Final resolution: the failures were regressions from the CMP 1.12.0-rc01 bump (#6662), which merged with its test shards cancelled and was masked afterwards by build-cache replay. It was reverted in #6664, and this branch is rebased onto that revert (d8361ccd1). The full attribution trail — including two incorrect attributions by this PR's author, and their corrections — is preserved in the comments.

The one known residual flake repo-wide is NodeDetailCompassLifecycleTest (~66% per run, CMP-independent, waitUntil timeout); if this PR merges only after a retry that flipped it, the merge report will say so.

Review follow-up

CodeRabbit caught a real defect in the first push: I had added trimming of detached (replaced) pools to bound memory, gated on the reader/writer access counters. Those counters only cover withReadDb/withDb. DAO Flow collectors are untracked, and Paging factories latch a raw currentDb.valuePacketRepositoryImpl.kt:69 builds its paging source that way — so trimming could close a pool a live PagingSource still held and surface Connection pool is closed. That would have traded a recoverable wedge for a new crash.

Trimming is removed; the documented invariant (only close() reclaims detached pools) is restored, and the recovery rate window is what bounds churn. The trade-off is explicit: under a repeated wedge, replaced pools accumulate until shutdown, capped in rate rather than in total. Correctness over memory — an idle detached pool costs one SQLite connection and a statement cache, while closing one out from under a Paging source breaks the message list.

One pre-existing test (flowPoolRecoveryStopsAfterIntermittentFailuresFillTheRateWindow) caught a real defect in the first draft of this change: the recovery counter reset sat downstream of the new channel hop, where an upstream failure can overtake the value emitted a moment earlier. The reset now runs in the producing coroutine.

Not validated on a device — the wedge is a timing-dependent field failure we have not reproduced locally.


Upstream draft — Google Issue Tracker filing for the Room bug (for James to file; not submitted)

Google Issue Tracker draft — DO NOT FILE AUTOMATICALLY

Component: Android Public Tracker > Android Public Tracker > Jetpack (androidx) > Room
Type: Bug · Version: androidx.room3 3.0.1 (also present in 3.0.0)
Title: ConnectionPoolImpl leaks a connection permit when an acquire times out after acquire() already
succeeded, permanently wedging the pool (permits=0 with all connections Free)


Summary

Pool.acquireWithTimeout can drop a successfully acquired ConnectionWrapper without recycling it, losing the
semaphore permit that the acquisition consumed. With setSingleConnectionPool() (capacity = 1) the pool becomes
permanently unusable: every subsequent useConnection waits the full 30s timeout, and because the pool's default
onTimeout mode is LOG_TIMEOUT_EXCEPTION, acquireWithTimeout's while (true) loop retries forever. The
application sees neither a value nor an exception — every database read hangs for the life of the process, while
logcat fills with the acquire-timeout dump.

We are hitting this in production in Meshtastic-Android (KMP, BundledSQLiteDriver, Android + JVM desktop). Field
reports describe a permanent app wedge that only a force-stop clears.

Analysis

ConnectionPoolImpl.kt (3.0.1), Pool.acquireWithTimeout:

suspend fun acquireWithTimeout(timeout: Duration, onTimeout: () -> Unit): ConnectionWrapper {
    while (true) {
        var connection: ConnectionWrapper? = null
        var exceptionThrown: Throwable? = null
        try {
            withTimeout(timeout) { connection = acquire() }   // (1) permit consumed inside acquire()
        } catch (ex: Throwable) {
            exceptionThrown = ex
        }
        try {
            if (exceptionThrown is TimeoutCancellationException) {
                onTimeout.invoke()                            // (2) only LOGS in the default mode
            } else if (exceptionThrown != null) {
                throw exceptionThrown
            } else if (connection != null) {
                return connection
            }
        } catch (ex: Throwable) {
            connection?.let { recycle(it) }                   // (3) the ONLY recycle-on-failure path
            throw ex
        }
    }
}

The defect is the interaction of (1), (2) and (3):

  • acquire() consumes a permit from connectionPermits and returns a wrapper. The assignment to the outer
    connection var therefore happens before withTimeout returns.
  • withTimeout still throws TimeoutCancellationException if its deadline elapses as the block completes. The
    result is exceptionThrown is TimeoutCancellationException and connection != null.
  • In that state, branch (2) runs. onTimeout is ConnectionPoolImpl.onTimeout, whose behavior depends on the
    internal var onTimeout mode. The default is LOG_TIMEOUT_EXCEPTION, which calls
    ex.printStackTrace() and returns normally.
  • Because nothing throws, the catch at (3) — the only place a stranded connection is recycled — never runs.
    The loop iterates, connection is re-initialized to null, and the acquired wrapper is unreachable. Its permit
    is never released.

Only THROW_TIMEOUT_EXCEPTION mode is safe here, and that mode is not publicly configurable
(see the TODO(b/404380974) above internal var timeout).

Two consequences:

  1. Permit leak. capacity = 1 (single-connection pool) means one leak wedges the pool forever. A multi-reader
    pool degrades one permit at a time toward the same end state.
  2. The wedge is invisible to application code. In LOG_TIMEOUT_EXCEPTION mode acquireWithTimeout never
    returns and never throws, so useConnection — and every DAO query/Flow above it — hangs indefinitely. An app
    cannot detect or recover from this through normal error handling; it can only infer it from a missing result.

Cancellation churn makes the race reachable in practice: our DB flows are re-latched via flatMapLatest on device
switches, so acquisitions are routinely cancelled mid-flight (the dumps below show the requesting coroutine in
Cancelling state).

Evidence (production logcat, Meshtastic-Android 2.8.1)

W/System.err: android.database.SQLException: Error code: 5, message: Timed out attempting to acquire a reader connection.

Request coroutine: [..., DispatchedCoroutine{Cancelling}@667fccf, Dispatchers.IO.limitedParallelism(1)]

Writer pool:
	androidx.room3.coroutines.Pool@b1ce8e7 (capacity=1, permits=0, queue=(size=0)[])
		[1] - androidx.sqlite.driver.bundled.BundledSQLiteConnection@3efbc94
		Status: Free connection
		Prepared Statement Cache Size: 25
Reader pool:
	androidx.room3.coroutines.Pool@b1ce8e7 (capacity=1, permits=0, queue=(size=0)[])
		[1] - androidx.sqlite.driver.bundled.BundledSQLiteConnection@3efbc94
		Status: Free connection
		Prepared Statement Cache Size: 25
	at androidx.sqlite.SQLite__SQLiteKt.throwSQLiteException(SQLite.kt:64)
	at androidx.room3.coroutines.ConnectionPoolImpl.onTimeout(ConnectionPoolImpl.kt:196)
	at androidx.room3.coroutines.ConnectionPoolImpl.useConnection$lambda$0(ConnectionPoolImpl.kt:156)
	at androidx.room3.coroutines.Pool.acquireWithTimeout-KLykuaI(ConnectionPoolImpl.kt:242)

The dump is self-consistent with the analysis and, as far as we can tell, with nothing else:

  • permits=0 — the single permit is gone.
  • queue=(size=0)[] — the connection is not in availableConnections, so recycle() was never called for it.
  • Status: Free connectionConnectionWrapper.dump prints this only when acquireCoroutineContext == null.
    markAcquired(...) is applied in useConnection after acquireWithTimeout returns, so a wrapper that was
    acquired inside acquire() but dropped by the timeout branch is exactly one that shows Free while its permit is
    held. A connection genuinely in use would print Coroutine: [...] instead.
  • W/System.err with the frame ConnectionPoolImpl.onTimeout reached from Pool.acquireWithTimeout confirms
    LOG_TIMEOUT_EXCEPTION (printStackTrace), i.e. the exception is never delivered to the caller.
  • 156 such dumps within the same second, and zero across three field logs are followed by any recovery: every
    waiter in the app is parked on the wedged pool.

Once wedged, the state never clears — reported repeatedly, and only force-stopping the app restores function.

Suggested fix

In Pool.acquireWithTimeout, recycle an acquired connection whenever the iteration is not going to return it,
independently of whether onTimeout throws:

if (exceptionThrown is TimeoutCancellationException) {
    val stranded = connection
    connection = null
    stranded?.let { recycle(it) }   // release the permit before retrying/logging
    onTimeout.invoke()
}

Equivalently: move the recycle into a finally that runs unless the wrapper is being returned.

Secondary requests:

  1. Make onTimeout / timeout publicly configurable (b/404380974). LOG_TIMEOUT_EXCEPTION turns a pool-level
    fault into an unbounded silent hang, which application code cannot recover from. At minimum, consider making
    THROW_TIMEOUT_EXCEPTION the default, or bounding the while (true) retry loop.
  2. Consider asserting the invariant permits == capacity when every connection is Free, so a leaked permit is
    surfaced as a diagnosable error rather than an infinite wait.

Environment

  • androidx.room3 3.0.1, androidx.sqlite BundledSQLiteDriver
  • Kotlin Multiplatform: Android (minSdk 26) and JVM desktop; both platforms reproduce the wedge in the field
  • Pool configured with setSingleConnectionPool() and
    setQueryCoroutineContext(ioDispatcher.limitedParallelism(1))
  • Long-lived DAO Flows re-latched with flatMapLatest on database switches (the cancellation churn)

Notes for James before filing

  • Attach the three field logcats from [Bug]: Stale node connection still occurring after fix for #6491 #6608 (user-attachments 30935731 / 30935732 /
    30935733). 30935732 downloaded as a 9-byte file for me — re-fetch it from the issue before attaching.
  • We have not reproduced this in an isolated harness; the analysis is from the 3.0.1 sources plus the field dumps.
    If the Room team asks for a repro, the shape to try is: capacity = 1 pool, a coroutine that cancels acquisitions
    in a tight loop, and timeout reduced so the withTimeout-completes-as-deadline-elapses window is hit often.

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The change adds database-flow retry handling, stalled-flow detection, sliding-window recovery limits, detached-pool cleanup, and regression tests. Data-source flows now use the retry operator for recoverable Room pool failures.

Changes

Database flow recovery

Layer / File(s) Summary
Flow retry operator
core/database/src/commonMain/kotlin/org/meshtastic/core/database/DbFlowRecovery.kt, core/database/src/commonTest/kotlin/org/meshtastic/core/database/DbFlowRecoveryTest.kt
Adds retryOnDbPoolFailure with recoverable-error filtering, capped backoff, retry logging, and reset-after-emission behavior. Tests cover timeout, closed-pool, unrelated failures, and backoff reset.
Database manager recovery
core/database/src/commonMain/kotlin/org/meshtastic/core/database/DatabaseManager.kt, core/database/src/commonTest/kotlin/org/meshtastic/core/database/DatabaseManagerShutdownTest.kt, core/database/src/commonTest/kotlin/org/meshtastic/core/database/DatabaseManagerTestFixture.kt
DatabaseManager detects stalled initial emissions, applies sliding-window recovery limits, retains and trims detached pools, and exposes a test clock. Tests cover stalled flows, recovery-window expiry, and pool retention.
Data-source integration
core/data/src/commonMain/kotlin/org/meshtastic/core/data/datasource/*DataSource.kt, core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/NodeRepositoryImpl.kt, core/data/src/commonTest/kotlin/org/meshtastic/core/data/datasource/PoisonedPoolNodeFlowRecoveryTest.kt, core/data/src/commonTest/kotlin/org/meshtastic/core/data/repository/RadioConfigRepositoryImplTest.kt
Data-source flows retry recoverable database-pool failures. Node-flow regression tests cover repeated acquisition failures, and radio configuration tests use FakeDatabaseProvider cleanup.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant DataSource
  participant DatabaseManager
  participant DatabasePool
  DataSource->>DatabaseManager: observe current database flow
  DatabaseManager->>DatabasePool: acquire pool and collect query
  DatabasePool-->>DatabaseManager: emit data or pool failure
  DatabaseManager->>DatabasePool: relatch replacement pool after recovery
  DatabaseManager-->>DataSource: publish recovered flow value
Loading

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 database-flow recovery only; it adds no nullable or presence semantics for telemetry fields and no physical-metric default of 0. NodeItem and NodeItemCompact are unchanged.
Tests Prove The Path, Not The End State ✅ Passed Added recovery tests inject failures and assert retries, attempts, pool replacement, retention, and exact emissions; no new fake-store seed check or Unconfined order assertion. Size-only Radio asse...
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: recovering from Room connection-pool wedges that silently stall database flows.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch

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.

@github-actions github-actions Bot added the bugfix PR tag label Aug 12, 2026
@jamesarich
jamesarich marked this pull request as ready for review August 12, 2026 19:09
@github-actions

This comment has been minimized.

@codecov

codecov Bot commented Aug 12, 2026

Copy link
Copy Markdown

❌ 11 Tests Failed:

Tests completed Failed Passed Skipped
6039 11 6028 0
View the top 2 failed test(s) by shortest run time
org.meshtastic.feature.settings.radio.RadioConfigViewModelTest::updateChannels serializes overlapping channel saves()[jvm]
Stack Traces | 0.028s run time
kotlinx.coroutines.CompletionHandlerException: Exception in completion handler ResumeAwaitOnCompletion@25304ab7[job@6df5db6d] for "coroutine#1433":DeferredCoroutine{Completed}@6df5db6d
	at kotlinx.coroutines.JobSupport.completeStateFinalization(JobSupport.kt:313)
	at kotlinx.coroutines.JobSupport.tryFinalizeSimpleState(JobSupport.kt:288)
	at kotlinx.coroutines.JobSupport.tryMakeCompleting(JobSupport.kt:887)
	at kotlinx.coroutines.JobSupport.makeCompletingOnce$kotlinx_coroutines_core(JobSupport.kt:859)
	at kotlinx.coroutines.AbstractCoroutine.resumeWith(AbstractCoroutine.kt:99)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:47)
	at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:100)
	at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:586)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:807)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:717)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:704)
Caused by: kotlinx.coroutines.DispatchException: Coroutine dispatcher Dispatchers.Main threw an exception, context = [CoroutineId(1431), "coroutine#1431":StandaloneCoroutine{Active}@4f2569af, Dispatchers.Main]
	at kotlinx.coroutines.internal.DispatchedContinuationKt.safeIsDispatchNeeded(DispatchedContinuation.kt:264)
	at kotlinx.coroutines.DispatchedTaskKt.dispatch(DispatchedTask.kt:144)
	at kotlinx.coroutines.CancellableContinuationImpl.dispatchResume(CancellableContinuationImpl.kt:470)
	at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl$kotlinx_coroutines_core(CancellableContinuationImpl.kt:504)
	at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl$kotlinx_coroutines_core$default(CancellableContinuationImpl.kt:493)
	at kotlinx.coroutines.CancellableContinuationImpl.resumeWith(CancellableContinuationImpl.kt:359)
	at kotlinx.coroutines.ResumeAwaitOnCompletion.invoke(JobSupport.kt:1557)
	at kotlinx.coroutines.JobSupport.completeStateFinalization(JobSupport.kt:311)
	... 10 more
Caused by: java.lang.IllegalStateException: Dispatchers.Main was accessed when the platform dispatcher was absent and the test dispatcher was unset. Please make sure that Dispatchers.setMain() is called before accessing Dispatchers.Main and that Dispatchers.Main is not accessed after Dispatchers.resetMain().
	at kotlinx.coroutines.test.internal.TestMainDispatcherJvmKt.reportMissingMainCoroutineDispatcher(TestMainDispatcherJvm.kt:45)
	at kotlinx.coroutines.test.internal.TestMainDispatcherJvmKt.access$reportMissingMainCoroutineDispatcher(TestMainDispatcherJvm.kt:1)
	at kotlinx.coroutines.test.internal.TestMainDispatcherFactory.createDispatcher$lambda$2(TestMainDispatcherJvm.kt:20)
	at kotlin.SynchronizedLazyImpl.getValue(LazyJVM.kt:86)
	at kotlinx.coroutines.test.internal.TestMainDispatcher.getMainDispatcher(TestMainDispatcher.kt:18)
	at kotlinx.coroutines.test.internal.TestMainDispatcher.getDispatcher(TestMainDispatcher.kt:22)
	at kotlinx.coroutines.test.internal.TestMainDispatcher.isDispatchNeeded(TestMainDispatcher.kt:32)
	at kotlinx.coroutines.internal.DispatchedContinuationKt.safeIsDispatchNeeded(DispatchedContinuation.kt:262)
	... 17 more
Caused by: java.lang.IllegalStateException: Module with the Main dispatcher is missing. Add dependency providing the Main dispatcher, e.g. 'kotlinx-coroutines-android' and ensure it has the same version as 'kotlinx-coroutines-core'
	at kotlinx.coroutines.internal.MainDispatchersKt.throwMissingMainDispatcherException(MainDispatchers.kt:77)
	at kotlinx.coroutines.internal.MissingMainCoroutineDispatcher.missing(MainDispatchers.kt:108)
	at kotlinx.coroutines.internal.MissingMainCoroutineDispatcher.dispatch(MainDispatchers.kt:101)
	at kotlinx.coroutines.internal.MissingMainCoroutineDispatcher.dispatch(MainDispatchers.kt:84)
	at kotlinx.coroutines.test.internal.TestMainDispatcherFactory.createDispatcher$lambda$2(TestMainDispatcherJvm.kt:22)
	... 22 more
org.meshtastic.core.ui.viewmodel.ConnectionsViewModelTest::connected older known node exposes Android firmware update notice()[jvm]
Stack Traces | 0.105s run time
org.opentest4j.AssertionFailedError: expected: <1> but was: <0>
	at org.junit.jupiter.api.Assertions.assertEquals(Assertions.java:1210)
	at kotlin.test.junit5.JUnit5Asserter.assertEquals(JUnitSupport.kt:32)
	at kotlin.test.AssertionsKt__AssertionsKt.assertEquals(Assertions.kt:63)
	at kotlin.test.AssertionsKt.assertEquals(Unknown Source)
	at kotlin.test.AssertionsKt__AssertionsKt.assertEquals$default(Assertions.kt:62)
	at kotlin.test.AssertionsKt.assertEquals$default(Unknown Source)
	at org.meshtastic.core.ui.viewmodel.ConnectionsViewModelTest$connected older known node exposes Android firmware update notice$1.invokeSuspend(ConnectionsViewModelTest.kt:201)
	at org.meshtastic.core.ui.viewmodel.ConnectionsViewModelTest$connected older known node exposes Android firmware update notice$1.invoke(ConnectionsViewModelTest.kt)
	at org.meshtastic.core.ui.viewmodel.ConnectionsViewModelTest$connected older known node exposes Android firmware update notice$1.invoke(ConnectionsViewModelTest.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 kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt.runTest-8Mi8wO0$default(TestBuilders.kt:159)
	at kotlinx.coroutines.test.TestBuildersKt.runTest-8Mi8wO0$default(TestBuilders.kt:1)
	at org.meshtastic.core.ui.viewmodel.ConnectionsViewModelTest.connected older known node exposes Android firmware update notice(ConnectionsViewModelTest.kt:179)
View the full list of 9 ❄️ flaky test(s)
org.meshtastic.core.service.MeshNotificationManagerImplConversationTest::notification ids are namespaced per type so a node num cannot clobber the service notification

Flake rate in main: 100.00% (Passed 0 times, Failed 6 times)

Stack Traces | 0.473s run time
java.lang.AssertionError: service notification should survive expected:<1> but was:<0>
	at org.junit.Assert.fail(Assert.java:89)
	at org.junit.Assert.failNotEquals(Assert.java:835)
	at org.junit.Assert.assertEquals(Assert.java:120)
	at kotlin.test.junit.JUnitAsserter.assertEquals(JUnitSupport.kt:32)
	at kotlin.test.AssertionsKt__AssertionsKt.assertEquals(Assertions.kt:63)
	at kotlin.test.AssertionsKt.assertEquals(Unknown Source)
	at org.meshtastic.core.service.MeshNotificationManagerImplConversationTest$notification ids are namespaced per type so a node num cannot clobber the service notification$1.invokeSuspend(MeshNotificationManagerImplConversationTest.kt:224)
	at org.meshtastic.core.service.MeshNotificationManagerImplConversationTest$notification ids are namespaced per type so a node num cannot clobber the service notification$1.invoke(MeshNotificationManagerImplConversationTest.kt)
	at org.meshtastic.core.service.MeshNotificationManagerImplConversationTest$notification ids are namespaced per type so a node num cannot clobber the service notification$1.invoke(MeshNotificationManagerImplConversationTest.kt)
	at org.meshtastic.core.testing.TestScopesKt$runWithRenderScope$1.invokeSuspend(TestScopes.kt:30)
	at org.meshtastic.core.testing.TestScopesKt$runWithRenderScope$1.invoke(TestScopes.kt)
	at org.meshtastic.core.testing.TestScopesKt$runWithRenderScope$1.invoke(TestScopes.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 kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt.runTest-8Mi8wO0$default(TestBuilders.kt:159)
	at kotlinx.coroutines.test.TestBuildersKt.runTest-8Mi8wO0$default(TestBuilders.kt:1)
	at org.meshtastic.core.testing.TestScopesKt.runWithRenderScope(TestScopes.kt:27)
	at org.meshtastic.core.service.MeshNotificationManagerImplConversationTest.notification ids are namespaced per type so a node num cannot clobber the service notification(MeshNotificationManagerImplConversationTest.kt:215)
	at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
	at java.base/java.lang.reflect.Method.invoke(Method.java:565)
	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
	at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
	at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
	at org.robolectric.RobolectricTestRunner$HelperTestRunner$1.evaluate(RobolectricTestRunner.java:524)
	at org.robolectric.internal.SandboxTestRunner.executeInSandbox(SandboxTestRunner.java:494)
	at org.robolectric.internal.SandboxTestRunner.access$900(SandboxTestRunner.java:67)
	at org.robolectric.internal.SandboxTestRunner$7.evaluate(SandboxTestRunner.java:442)
	at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
	at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
	at org.robolectric.internal.SandboxTestRunner.access$600(SandboxTestRunner.java:67)
	at org.robolectric.internal.SandboxTestRunner$6.evaluate(SandboxTestRunner.java:333)
	at org.robolectric.internal.SandboxTestRunner$3.evaluate(SandboxTestRunner.java:233)
	at org.robolectric.internal.SandboxTestRunner$5.lambda$evaluate$0(SandboxTestRunner.java:317)
	at org.robolectric.internal.bytecode.Sandbox.lambda$runOnMainThread$0(Sandbox.java:101)
	at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:328)
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1090)
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:614)
	at java.base/java.lang.Thread.run(Thread.java:1474)
org.meshtastic.core.service.MeshNotificationManagerImplTest::service state rendering is deferred from the caller

Flake rate in main: 100.00% (Passed 0 times, Failed 6 times)

Stack Traces | 0.971s run time
java.lang.AssertionError: actual value is null
	at org.junit.Assert.fail(Assert.java:89)
	at org.junit.Assert.assertTrue(Assert.java:42)
	at org.junit.Assert.assertNotNull(Assert.java:713)
	at kotlin.test.junit.JUnitAsserter.assertNotNull(JUnitSupport.kt:48)
	at kotlin.test.AssertionsKt__AssertionsKt.assertNotNull(Assertions.kt:147)
	at kotlin.test.AssertionsKt.assertNotNull(Unknown Source)
	at kotlin.test.AssertionsKt__AssertionsKt.assertNotNull$default(Assertions.kt:145)
	at kotlin.test.AssertionsKt.assertNotNull$default(Unknown Source)
	at org.meshtastic.core.service.MeshNotificationManagerImplTest$service state rendering is deferred from the caller$1.invokeSuspend(MeshNotificationManagerImplTest.kt:125)
	at org.meshtastic.core.service.MeshNotificationManagerImplTest$service state rendering is deferred from the caller$1.invoke(MeshNotificationManagerImplTest.kt)
	at org.meshtastic.core.service.MeshNotificationManagerImplTest$service state rendering is deferred from the caller$1.invoke(MeshNotificationManagerImplTest.kt)
	at org.meshtastic.core.testing.TestScopesKt$runWithRenderScope$1.invokeSuspend(TestScopes.kt:30)
	at org.meshtastic.core.testing.TestScopesKt$runWithRenderScope$1.invoke(TestScopes.kt)
	at org.meshtastic.core.testing.TestScopesKt$runWithRenderScope$1.invoke(TestScopes.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 kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt.runTest-8Mi8wO0$default(TestBuilders.kt:159)
	at kotlinx.coroutines.test.TestBuildersKt.runTest-8Mi8wO0$default(TestBuilders.kt:1)
	at org.meshtastic.core.testing.TestScopesKt.runWithRenderScope(TestScopes.kt:27)
	at org.meshtastic.core.service.MeshNotificationManagerImplTest.service state rendering is deferred from the caller(MeshNotificationManagerImplTest.kt:117)
	at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
	at java.base/java.lang.reflect.Method.invoke(Method.java:565)
	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
	at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
	at org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java:27)
	at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
	at org.robolectric.RobolectricTestRunner$HelperTestRunner$1.evaluate(RobolectricTestRunner.java:524)
	at org.robolectric.internal.SandboxTestRunner.executeInSandbox(SandboxTestRunner.java:494)
	at org.robolectric.internal.SandboxTestRunner.access$900(SandboxTestRunner.java:67)
	at org.robolectric.internal.SandboxTestRunner$7.evaluate(SandboxTestRunner.java:442)
	at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
	at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
	at org.robolectric.internal.SandboxTestRunner.access$600(SandboxTestRunner.java:67)
	at org.robolectric.internal.SandboxTestRunner$6.evaluate(SandboxTestRunner.java:333)
	at org.robolectric.internal.SandboxTestRunner$3.evaluate(SandboxTestRunner.java:233)
	at org.robolectric.internal.SandboxTestRunner$5.lambda$evaluate$0(SandboxTestRunner.java:317)
	at org.robolectric.internal.bytecode.Sandbox.lambda$runOnMainThread$0(Sandbox.java:101)
	at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:328)
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1090)
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:614)
	at java.base/java.lang.Thread.run(Thread.java:1474)
org.meshtastic.core.service.MeshNotificationManagerImplTest::service state seeds local stats before the local node row is available

Flake rate in main: 100.00% (Passed 0 times, Failed 6 times)

Stack Traces | 0.119s run time
java.lang.AssertionError: actual value is null
	at org.junit.Assert.fail(Assert.java:89)
	at org.junit.Assert.assertTrue(Assert.java:42)
	at org.junit.Assert.assertNotNull(Assert.java:713)
	at kotlin.test.junit.JUnitAsserter.assertNotNull(JUnitSupport.kt:48)
	at kotlin.test.AssertionsKt__AssertionsKt.assertNotNull(Assertions.kt:147)
	at kotlin.test.AssertionsKt.assertNotNull(Unknown Source)
	at kotlin.test.AssertionsKt__AssertionsKt.assertNotNull$default(Assertions.kt:145)
	at kotlin.test.AssertionsKt.assertNotNull$default(Unknown Source)
	at org.meshtastic.core.service.MeshNotificationManagerImplTest$service state seeds local stats before the local node row is available$1.invokeSuspend(MeshNotificationManagerImplTest.kt:173)
	at org.meshtastic.core.service.MeshNotificationManagerImplTest$service state seeds local stats before the local node row is available$1.invoke(MeshNotificationManagerImplTest.kt)
	at org.meshtastic.core.service.MeshNotificationManagerImplTest$service state seeds local stats before the local node row is available$1.invoke(MeshNotificationManagerImplTest.kt)
	at org.meshtastic.core.testing.TestScopesKt$runWithRenderScope$1.invokeSuspend(TestScopes.kt:30)
	at org.meshtastic.core.testing.TestScopesKt$runWithRenderScope$1.invoke(TestScopes.kt)
	at org.meshtastic.core.testing.TestScopesKt$runWithRenderScope$1.invoke(TestScopes.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 kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt.runTest-8Mi8wO0$default(TestBuilders.kt:159)
	at kotlinx.coroutines.test.TestBuildersKt.runTest-8Mi8wO0$default(TestBuilders.kt:1)
	at org.meshtastic.core.testing.TestScopesKt.runWithRenderScope(TestScopes.kt:27)
	at org.meshtastic.core.service.MeshNotificationManagerImplTest.service state seeds local stats before the local node row is available(MeshNotificationManagerImplTest.kt:163)
	at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
	at java.base/java.lang.reflect.Method.invoke(Method.java:565)
	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
	at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
	at org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java:27)
	at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
	at org.robolectric.RobolectricTestRunner$HelperTestRunner$1.evaluate(RobolectricTestRunner.java:524)
	at org.robolectric.internal.SandboxTestRunner.executeInSandbox(SandboxTestRunner.java:494)
	at org.robolectric.internal.SandboxTestRunner.access$900(SandboxTestRunner.java:67)
	at org.robolectric.internal.SandboxTestRunner$7.evaluate(SandboxTestRunner.java:442)
	at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
	at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
	at org.robolectric.internal.SandboxTestRunner.access$600(SandboxTestRunner.java:67)
	at org.robolectric.internal.SandboxTestRunner$6.evaluate(SandboxTestRunner.java:333)
	at org.robolectric.internal.SandboxTestRunner$3.evaluate(SandboxTestRunner.java:233)
	at org.robolectric.internal.SandboxTestRunner$5.lambda$evaluate$0(SandboxTestRunner.java:317)
	at org.robolectric.internal.bytecode.Sandbox.lambda$runOnMainThread$0(Sandbox.java:101)
	at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:328)
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1090)
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:614)
	at java.base/java.lang.Thread.run(Thread.java:1474)
org.meshtastic.feature.connections.AndroidScannerViewModelBondingTest::security exception does not arm the transport and surfaces an error

Flake rate in main: 100.00% (Passed 0 times, Failed 6 times)

Stack Traces | 0.331s run time
java.lang.AssertionError: actual value is null
	at org.junit.Assert.fail(Assert.java:89)
	at org.junit.Assert.assertTrue(Assert.java:42)
	at org.junit.Assert.assertNotNull(Assert.java:713)
	at kotlin.test.junit.JUnitAsserter.assertNotNull(JUnitSupport.kt:48)
	at kotlin.test.AssertionsKt__AssertionsKt.assertNotNull(Assertions.kt:147)
	at kotlin.test.AssertionsKt.assertNotNull(Unknown Source)
	at kotlin.test.AssertionsKt__AssertionsKt.assertNotNull$default(Assertions.kt:145)
	at kotlin.test.AssertionsKt.assertNotNull$default(Unknown Source)
	at org.meshtastic.feature.connections.AndroidScannerViewModelBondingTest$security exception does not arm the transport and surfaces an error$1.invokeSuspend(AndroidScannerViewModelBondingTest.kt:163)
	at org.meshtastic.feature.connections.AndroidScannerViewModelBondingTest$security exception does not arm the transport and surfaces an error$1.invoke(AndroidScannerViewModelBondingTest.kt)
	at org.meshtastic.feature.connections.AndroidScannerViewModelBondingTest$security exception does not arm the transport and surfaces an error$1.invoke(AndroidScannerViewModelBondingTest.kt)
	at kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTest$2$1$1.invokeSuspend(TestBuilders.kt:317)
	at kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTest$2$1$1.invoke(TestBuilders.kt)
	at kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTest$2$1$1.invoke(TestBuilders.kt)
	at kotlinx.coroutines.intrinsics.UndispatchedKt.startCoroutineUndispatched(Undispatched.kt:20)
	at kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt:360)
	at kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt:134)
	at kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTest$2$1.invokeSuspend(TestBuilders.kt:312)
	at kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTest$2$1.invoke(TestBuilders.kt)
	at kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTest$2$1.invoke(TestBuilders.kt)
	at kotlinx.coroutines.test.TestBuildersJvmKt$createTestResult$1.invokeSuspend(TestBuildersJvm.kt:11)
	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 kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt.runTest-8Mi8wO0$default(TestBuilders.kt:159)
	at kotlinx.coroutines.test.TestBuildersKt.runTest-8Mi8wO0$default(TestBuilders.kt:1)
	at org.meshtastic.feature.connections.AndroidScannerViewModelBondingTest.security exception does not arm the transport and surfaces an error(AndroidScannerViewModelBondingTest.kt:154)
	at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
	at java.base/java.lang.reflect.Method.invoke(Method.java:565)
	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
	at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
	at org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java:27)
	at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
	at org.robolectric.RobolectricTestRunner$HelperTestRunner$1.evaluate(RobolectricTestRunner.java:524)
	at org.robolectric.internal.SandboxTestRunner.executeInSandbox(SandboxTestRunner.java:494)
	at org.robolectric.internal.SandboxTestRunner.access$900(SandboxTestRunner.java:67)
	at org.robolectric.internal.SandboxTestRunner$7.evaluate(SandboxTestRunner.java:442)
	at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
	at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
	at org.robolectric.internal.SandboxTestRunner.access$600(SandboxTestRunner.java:67)
	at org.robolectric.internal.SandboxTestRunner$6.evaluate(SandboxTestRunner.java:333)
	at org.robolectric.internal.SandboxTestRunner$3.evaluate(SandboxTestRunner.java:233)
	at org.robolectric.internal.SandboxTestRunner$5.lambda$evaluate$0(SandboxTestRunner.java:317)
	at org.robolectric.internal.bytecode.Sandbox.lambda$runOnMainThread$0(Sandbox.java:101)
	at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:328)
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1090)
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:614)
	at java.base/java.lang.Thread.run(Thread.java:1474)
org.meshtastic.feature.connections.ScannerViewModelTest::bluetooth-disabled failure allows an immediate retry once the user re-enables it()[jvm]

Flake rate in main: 100.00% (Passed 0 times, Failed 6 times)

Stack Traces | 0.081s run time
org.opentest4j.AssertionFailedError: expected: <Bluetooth is off. Turn it on to scan for nearby devices.> but was: <null>
	at org.junit.jupiter.api.Assertions.assertEquals(Assertions.java:1210)
	at kotlin.test.junit5.JUnit5Asserter.assertEquals(JUnitSupport.kt:32)
	at kotlin.test.AssertionsKt__AssertionsKt.assertEquals(Assertions.kt:63)
	at kotlin.test.AssertionsKt.assertEquals(Unknown Source)
	at kotlin.test.AssertionsKt__AssertionsKt.assertEquals$default(Assertions.kt:62)
	at kotlin.test.AssertionsKt.assertEquals$default(Unknown Source)
	at org.meshtastic.feature.connections.ScannerViewModelTest$bluetooth-disabled failure allows an immediate retry once the user re-enables it$1.invokeSuspend(ScannerViewModelTest.kt:172)
	at org.meshtastic.feature.connections.ScannerViewModelTest$bluetooth-disabled failure allows an immediate retry once the user re-enables it$1.invoke(ScannerViewModelTest.kt)
	at org.meshtastic.feature.connections.ScannerViewModelTest$bluetooth-disabled failure allows an immediate retry once the user re-enables it$1.invoke(ScannerViewModelTest.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 kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt.runTest-8Mi8wO0$default(TestBuilders.kt:159)
	at kotlinx.coroutines.test.TestBuildersKt.runTest-8Mi8wO0$default(TestBuilders.kt:1)
	at org.meshtastic.feature.connections.ScannerViewModelTest.bluetooth-disabled failure allows an immediate retry once the user re-enables it(ScannerViewModelTest.kt:156)
org.meshtastic.feature.connections.ScannerViewModelTest::location-services-disabled failure allows an immediate retry()[jvm]

Flake rate in main: 100.00% (Passed 0 times, Failed 6 times)

Stack Traces | 0.093s run time
org.opentest4j.AssertionFailedError: expected: <Location services are off. Turn them on to scan for nearby devices.> but was: <null>
	at org.junit.jupiter.api.Assertions.assertEquals(Assertions.java:1210)
	at kotlin.test.junit5.JUnit5Asserter.assertEquals(JUnitSupport.kt:32)
	at kotlin.test.AssertionsKt__AssertionsKt.assertEquals(Assertions.kt:63)
	at kotlin.test.AssertionsKt.assertEquals(Unknown Source)
	at kotlin.test.AssertionsKt__AssertionsKt.assertEquals$default(Assertions.kt:62)
	at kotlin.test.AssertionsKt.assertEquals$default(Unknown Source)
	at org.meshtastic.feature.connections.ScannerViewModelTest$location-services-disabled failure allows an immediate retry$1.invokeSuspend(ScannerViewModelTest.kt:193)
	at org.meshtastic.feature.connections.ScannerViewModelTest$location-services-disabled failure allows an immediate retry$1.invoke(ScannerViewModelTest.kt)
	at org.meshtastic.feature.connections.ScannerViewModelTest$location-services-disabled failure allows an immediate retry$1.invoke(ScannerViewModelTest.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 kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt.runTest-8Mi8wO0$default(TestBuilders.kt:159)
	at kotlinx.coroutines.test.TestBuildersKt.runTest-8Mi8wO0$default(TestBuilders.kt:1)
	at org.meshtastic.feature.connections.ScannerViewModelTest.location-services-disabled failure allows an immediate retry(ScannerViewModelTest.kt:180)
org.meshtastic.feature.connections.ScannerViewModelTest::scan quota failure honors retry-after cooldown()[jvm]

Flake rate in main: 100.00% (Passed 0 times, Failed 6 times)

Stack Traces | 0.196s run time
org.opentest4j.AssertionFailedError: expected: <Bluetooth scan limit reached. Try again in 31 seconds.> but was: <null>
	at org.junit.jupiter.api.Assertions.assertEquals(Assertions.java:1210)
	at kotlin.test.junit5.JUnit5Asserter.assertEquals(JUnitSupport.kt:32)
	at kotlin.test.AssertionsKt__AssertionsKt.assertEquals(Assertions.kt:63)
	at kotlin.test.AssertionsKt.assertEquals(Unknown Source)
	at kotlin.test.AssertionsKt__AssertionsKt.assertEquals$default(Assertions.kt:62)
	at kotlin.test.AssertionsKt.assertEquals$default(Unknown Source)
	at org.meshtastic.feature.connections.ScannerViewModelTest$scan quota failure honors retry-after cooldown$1.invokeSuspend(ScannerViewModelTest.kt:217)
	at org.meshtastic.feature.connections.ScannerViewModelTest$scan quota failure honors retry-after cooldown$1.invoke(ScannerViewModelTest.kt)
	at org.meshtastic.feature.connections.ScannerViewModelTest$scan quota failure honors retry-after cooldown$1.invoke(ScannerViewModelTest.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 kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt.runTest-8Mi8wO0$default(TestBuilders.kt:159)
	at kotlinx.coroutines.test.TestBuildersKt.runTest-8Mi8wO0$default(TestBuilders.kt:1)
	at org.meshtastic.feature.connections.ScannerViewModelTest.scan quota failure honors retry-after cooldown(ScannerViewModelTest.kt:203)
org.meshtastic.feature.connections.ScannerViewModelTest::scan startup failure clears scanning state disables auto-scan and surfaces error()[jvm]

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

Stack Traces | 1.38s run time
org.opentest4j.AssertionFailedError: expected: <Bluetooth scan couldn't start. Try again, or toggle Bluetooth if the problem continues.> but was: <null>
	at org.junit.jupiter.api.Assertions.assertEquals(Assertions.java:1210)
	at kotlin.test.junit5.JUnit5Asserter.assertEquals(JUnitSupport.kt:32)
	at kotlin.test.AssertionsKt__AssertionsKt.assertEquals(Assertions.kt:63)
	at kotlin.test.AssertionsKt.assertEquals(Unknown Source)
	at kotlin.test.AssertionsKt__AssertionsKt.assertEquals$default(Assertions.kt:62)
	at kotlin.test.AssertionsKt.assertEquals$default(Unknown Source)
	at org.meshtastic.feature.connections.ScannerViewModelTest$scan startup failure clears scanning state disables auto-scan and surfaces error$1.invokeSuspend(ScannerViewModelTest.kt:124)
	at org.meshtastic.feature.connections.ScannerViewModelTest$scan startup failure clears scanning state disables auto-scan and surfaces error$1.invoke(ScannerViewModelTest.kt)
	at org.meshtastic.feature.connections.ScannerViewModelTest$scan startup failure clears scanning state disables auto-scan and surfaces error$1.invoke(ScannerViewModelTest.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 kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt.runTest-8Mi8wO0$default(TestBuilders.kt:159)
	at kotlinx.coroutines.test.TestBuildersKt.runTest-8Mi8wO0$default(TestBuilders.kt:1)
	at org.meshtastic.feature.connections.ScannerViewModelTest.scan startup failure clears scanning state disables auto-scan and surfaces error(ScannerViewModelTest.kt:116)
org.meshtastic.feature.node.detail.NodeDetailCompassLifecycleTest::compassSelectionFollowsScreenLifecycleAndDismissal()[jvm]

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

Stack Traces | 22.6s 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

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor
⚠️ Action not completed

Review rate limited.


Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 55 seconds.

@jamesarich

Copy link
Copy Markdown
Collaborator Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

@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

🧹 Nitpick comments (1)
core/database/src/commonTest/kotlin/org/meshtastic/core/database/DbFlowRecoveryTest.kt (1)

38-104: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use Turbine for the changed Flow tests.

  • core/database/src/commonTest/kotlin/org/meshtastic/core/database/DbFlowRecoveryTest.kt#L38-L104: replace toList()-based Flow assertions with Turbine assertions.
  • core/database/src/commonTest/kotlin/org/meshtastic/core/database/DatabaseManagerShutdownTest.kt#L372-L505: use Turbine to collect and assert Flow events.
  • core/data/src/commonTest/kotlin/org/meshtastic/core/data/datasource/PoisonedPoolNodeFlowRecoveryTest.kt#L54-L84: use Turbine for recovery and eager-sharing Flow assertions.

As per coding guidelines, “Use Turbine for Flow testing.”

🤖 Prompt for 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.

In
`@core/database/src/commonTest/kotlin/org/meshtastic/core/database/DbFlowRecoveryTest.kt`
around lines 38 - 104, Replace toList()-based Flow assertions with Turbine event
assertions in DbFlowRecoveryTest.kt lines 38-104, DatabaseManagerShutdownTest.kt
lines 372-505, and PoisonedPoolNodeFlowRecoveryTest.kt lines 54-84. Use Turbine
to await expected emissions, completion, and failures while preserving each
test’s existing recovery, shutdown, and eager-sharing expectations.

Source: Coding guidelines

🤖 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/database/src/commonMain/kotlin/org/meshtastic/core/database/DatabaseManager.kt`:
- Around line 1035-1051: Update trimDetachedDatabasesLocked and its
detached-pool lifecycle tracking so pools are not closed while DAO Flow
collectors or holders of earlier currentDb values may still be active. Track
each detached Flow generation through cancellation completion, and only make a
pool trimmable after all associated Flow users finish; otherwise retain detached
pools until close(). Do not use replacement count or
hasActiveDatabaseAccessLocked alone as proof of safety.

---

Nitpick comments:
In
`@core/database/src/commonTest/kotlin/org/meshtastic/core/database/DbFlowRecoveryTest.kt`:
- Around line 38-104: Replace toList()-based Flow assertions with Turbine event
assertions in DbFlowRecoveryTest.kt lines 38-104, DatabaseManagerShutdownTest.kt
lines 372-505, and PoisonedPoolNodeFlowRecoveryTest.kt lines 54-84. Use Turbine
to await expected emissions, completion, and failures while preserving each
test’s existing recovery, shutdown, and eager-sharing expectations.
🪄 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: ac983c02-b08d-4a50-a8e9-b9275d90bb32

📥 Commits

Reviewing files that changed from the base of the PR and between 94ceaa6 and 1860cd9.

📒 Files selected for processing (12)
  • core/data/src/commonMain/kotlin/org/meshtastic/core/data/datasource/DeviceLinkLocalDataSource.kt
  • core/data/src/commonMain/kotlin/org/meshtastic/core/data/datasource/EventFirmwareEditionLocalDataSource.kt
  • core/data/src/commonMain/kotlin/org/meshtastic/core/data/datasource/SwitchingChannelSetDataSource.kt
  • core/data/src/commonMain/kotlin/org/meshtastic/core/data/datasource/SwitchingNodeInfoReadDataSource.kt
  • core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/NodeRepositoryImpl.kt
  • core/data/src/commonTest/kotlin/org/meshtastic/core/data/datasource/PoisonedPoolNodeFlowRecoveryTest.kt
  • core/data/src/commonTest/kotlin/org/meshtastic/core/data/repository/RadioConfigRepositoryImplTest.kt
  • core/database/src/commonMain/kotlin/org/meshtastic/core/database/DatabaseManager.kt
  • core/database/src/commonMain/kotlin/org/meshtastic/core/database/DbFlowRecovery.kt
  • core/database/src/commonTest/kotlin/org/meshtastic/core/database/DatabaseManagerShutdownTest.kt
  • core/database/src/commonTest/kotlin/org/meshtastic/core/database/DatabaseManagerTestFixture.kt
  • core/database/src/commonTest/kotlin/org/meshtastic/core/database/DbFlowRecoveryTest.kt

@jamesarich

Copy link
Copy Markdown
Collaborator Author

Addressed the Turbine nitpick in addc45d.

DbFlowRecoveryTest and PoisonedPoolNodeFlowRecoveryTest now use Turbine instead of take/toList/firstawaitError() states "this failure is terminal" more directly than wrapping the collection in assertFailsWith.

One wrinkle worth recording for anyone writing Flow tests against retry/backoff code here: Turbine's await deadline runs on the test scheduler's virtual clock, which the retry backoff also advances, so the default 1s deadline trips as soon as a retry sleeps. Both suites pass an explicit timeout sized to the accumulated capped backoff rather than to any wall-clock expectation.

Left DatabaseManagerShutdownTest on its existing idiom: those tests assert on pool rebuild counts and closedDatabases rather than on flow event sequences, and its surrounding cases all use terminal first()/assertFailsWith — converting only the new ones would split the file's style for no gain.

🤖 Addressed by Claude Code

@jamesarich
jamesarich enabled auto-merge August 12, 2026 20:40
@jamesarich
jamesarich added this pull request to the merge queue Aug 12, 2026
@jamesarich
jamesarich removed this pull request from the merge queue due to a manual request Aug 12, 2026
@jamesarich
jamesarich force-pushed the claude/eloquent-babbage-9c585b branch from addc45d to f7fa095 Compare August 12, 2026 21:52
@github-actions

This comment has been minimized.

@jamesarich

Copy link
Copy Markdown
Collaborator Author

shard-core failure is pre-existing on main, not from this PR

The only failing task in that run is :core:ui:allTests, with one test: ConnectionsViewModelTest > connected older known node exposes Android firmware update notice. This PR touches neither :core:ui nor anything it depends on.

The modules this PR does change ran for real in that same job and passed — :core:data:jvmTest and :core:data:testAndroidHostTest both executed with no FROM-CACHE/UP-TO-DATE marker.

Bisected on a clean checkout of origin/main with none of these changes applied:

commit :core:ui:jvmTest + :feature:connections:jvmTest
28dd35ef9 (before the bump) BUILD SUCCESSFUL
bc4e9da3a (#6662, CMP 1.12.0-rc01) fails
d5848ad5e (current main) fails

Two things explain why this went unnoticed: #6662 merged with all three test shards cancelled, so these tests never gated it; and the shard that ran on #6661 afterwards replayed :core:service, :feature:connections, and :feature:node test tasks FROM-CACHE, so it never executed them either. Current main's green tick is therefore not evidence that main is healthy.

Because the cache has been masking this, the true blast radius is unknown — any task not re-executed since bc4e9da3a could also be affected. Establishing that needs a --no-build-cache --rerun-tasks run at d5848ad5e across all shards.

Not enqueueing this PR until main is green, since the queue would eject it for a failure it didn't cause. No fix pushed here on purpose: the regression belongs to #6662 and should be reverted or fixed forward on its own.

🤖 Addressed by Claude Code

@github-actions

This comment has been minimized.

@jamesarich

Copy link
Copy Markdown
Collaborator Author

Correction: I was wrong above — at least part of this is mine

My previous comment attributed the shard-core failure to #6662. That is not supported, and I'm retracting it.

I checked CI runs on main itself rather than relying on my local bisect:

  • d5848ad5e (current main, run 31642782057): :core:ui:jvmTest genuinely executed, ConnectionsViewModelTest 0 failures.
  • 1b89b6392 (later main): same, 0 failures.
  • This PR (f7fa09553, run 31644503439): ConnectionsViewModelTest fails 3×.

So that test passes on main and fails on this branch. My earlier evidence came from a local run whose environment disagrees with CI on main too, so I'm discarding it as a basis for attribution.

shard-feature on this PR also fails :feature:settings:allTests (RadioConfigViewModelTest, via UncaughtExceptionsBeforeTest), which is outside the set attributable to #6662.

Working hypothesis, not yet confirmed: retryOnDbPoolFailure converts a terminal DB-classified failure into an unbounded retry loop, so a fake or test double that fails with a pool-shaped message now retries forever instead of terminating the flow — which under runTest's virtual clock never settles and surfaces as an uncaught exception blamed on a later test. The UncaughtExceptionsBeforeTest wrapper misattributes by design, so the named test is not necessarily the affected one.

Investigating now. This PR should not be merged in its current state.

🤖 Addressed by Claude Code

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@jamesarich

Copy link
Copy Markdown
Collaborator Author

Final attribution, with the experiments that settle it

My retraction above said "at least part of this is mine." Completing the isolation shows that was also wrong — but for a subtler reason than my first comment. Both of my earlier attributions failed the same way: they trusted whole-module runs of order-dependent tests.

Run filtered (single class, --rerun-tasks, clean worktree at a normal path):

class clean main d5848ad5e this branch 57943edc3
RadioConfigViewModelTest (:feature:settings) 6/6 fail fails
ConnectionsViewModelTest (:core:ui) 3/3 fail 3/3 fail

Both classes fail deterministically on clean main when they run without the rest of their module in front of them. In whole-module runs the outcome flips with test ordering — which is why CI showed them green at d5848ad5e, red on this PR, and why my two differentials pointed in opposite directions. The failure signature is a coroutine reaching Dispatchers.Main after Dispatchers.resetMain() ("Module with the Main dispatcher is missing"), i.e. a leak from an earlier test in the class, and the UncaughtExceptionsBeforeTest wrapper then blames whichever test runs next.

Nothing in :feature:settings or :core:ui references DatabaseProvider, observeCurrentDb, or retryOnDbPoolFailure — this PR's code cannot execute in those test paths.

So the overall state of main has two independent problems, neither from this PR:

  1. chore(deps): bump the Compose stack to CMP 1.12.0-rc01 in lockstep #6662 (CMP 1.12.0-rc01) introduced real failures in :core:service / :feature:connections / :feature:node — established CI-side by real-execution runs at 28dd35ef9 (green) vs bc4e9da3a (24 failures), with the shards on chore(deps): bump the Compose stack to CMP 1.12.0-rc01 in lockstep #6662 itself cancelled and everything since cache-replayed.
  2. An order-dependent dispatcher-leak family (RadioConfigViewModelTest, ConnectionsViewModelTest, likely NodeDetailCompassLifecycleTest) that fails on main whenever ordering exposes it, possibly also surfaced by the CMP bump.

57943ed (narrowing the retry predicate to the actual wedge signature) stays: it's correct regardless — a closed pool is handled by re-latching, and retrying it kept flows alive past their scope. The modules this PR touches remain green: 738 jvm tests, 0 failures, forced.

Holding enqueue until main is actually green under real execution.

🤖 Addressed by Claude Code

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@jamesarich

Copy link
Copy Markdown
Collaborator Author

main is unblocked — a rebase should clear the shard failures here

#6664 merged as d8361ccd1, reverting CMP back to 1.11.1. This PR was being held out of the merge queue until main was green, so that hold is lifted.

Worth knowing before re-running CI: this branch is based on d5848ad5e, which predates the revert and carries CMP 1.12.0-rc01. The current red on shard-core and shard-feature (run 31647360065) is therefore very likely the CMP regression rather than anything in this PR — that bump broke 10 tests across :core:service, :feature:connections, :core:ui and :feature:settings. Rebasing onto d8361ccd1 should clear them without any change to this branch's own code.

One caveat when you do re-run: NodeDetailCompassLifecycleTest::compassSelectionFollowsScreenLifecycleAndDismissal()[jvm] is an unrelated pre-existing flake (Codecov: 66.67% failure rate on main, reproduces on 1.11.1) and may still fail. It is owned separately and is not a signal about this PR.

🤖 Addressed by Claude Code

jamesarich and others added 2 commits August 12, 2026 21:06
Room 3.0.1 logs its pool acquire-timeout and retries forever instead of
throwing, so a leaked permit makes DAO flows hang with no exception and no
value. Every catch-based recovery path was therefore unreachable, and the
node list stayed empty for the process lifetime.

Detect the wedge by absence: bound the first emission of each re-latched
DAO flow and route a stall into the existing pool-reopen path. Restart
eagerly-shared repository flows after a recoverable pool failure so a
terminal upstream can no longer kill a process-lifetime StateFlow.

Replace both manager-lifetime recovery caps with one sliding rate window,
per origin, inside the seam #6661 introduced. A lifetime budget is spent
early when a wedge recurs within minutes, after which every later wedge is
permanent -- the failure #6608 reports. The wedged-pool quarantine expires
with the same window, without which the budget and the admission gate
block each other and the pool stays write-dead for the process.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Retrying a closed pool kept a flow alive that used to terminate, leaving a
backoff loop inside a scope that had already gone away — it surfaced as a
coroutine resuming on a reset Main dispatcher in unrelated ViewModel tests.
observeCurrentDb already re-latches onto the replacement pool, so retrying
a closed pool adds nothing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jamesarich
jamesarich force-pushed the claude/eloquent-babbage-9c585b branch from 2dac3fa to b029602 Compare August 13, 2026 02:11
@jamesarich
jamesarich added this pull request to the merge queue Aug 13, 2026
Merged via the queue into main with commit b3e3804 Aug 13, 2026
15 checks passed
@jamesarich
jamesarich deleted the claude/eloquent-babbage-9c585b branch August 13, 2026 03:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bugfix PR tag

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant