fix: eliminate Circle DeFi isAccountOpen race that flaked unit tests#4177
Conversation
…itTest loadCirclePositions hopped to a hard-coded Dispatchers.IO and, on the "no cached and no on-chain account" branch, unconditionally wrote isAccountOpen = false. A concurrent onCreateAccount — also on real IO — could win the race on fast environments and then have its true stomped back to false when loadCirclePositions' stale finding finally landed. On slower CI this order flipped deterministically enough to fail CircleDeFiPositionsViewModelTest via a 5s real-time timeout. Inject an IO CoroutineDispatcher so tests run every coroutine under one TestScheduler, and preserve isAccountOpen from current state on the no-account branch so a racing create is never overwritten. Co-Authored-By: aminsato <Amin.saradar@yahoo.com>
📝 WalkthroughWalkthroughThis pull request refactors the Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
The duplicate CoroutineDispatchersModule in app/di redeclared a qualifier and provider that MainDataModule already supplies for the whole SingletonComponent. Delete it and import com.vultisig.wallet.data.IoDispatcher instead. Co-Authored-By: aminsato <Amin.saradar@yahoo.com>
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
app/src/main/java/com/vultisig/wallet/ui/screens/v2/defi/circle/CircleDeFiPositionsViewModel.kt (1)
214-230:⚠️ Potential issue | 🟠 MajorUse
safeLaunchforonCreateAccount()and rethrowCancellationExceptioninfetchAssociatedMscaAccount().
runCatchinginonCreateAccount()andcatch (t: Throwable)infetchAssociatedMscaAccount()both swallowCancellationException. If the ViewModel is cleared mid-request, the exception is treated as a failure, allowing the error snackbar to show andisAccountOpento be flipped after the scope is cancelled.Replace the bare
viewModelScope.launchinonCreateAccount()withviewModelScope.safeLaunch(per coding guidelines for network calls). ForfetchAssociatedMscaAccount(), add an explicitcatch (t: CancellationException)block before thecatch (t: Throwable)block to rethrow cancellation:Possible fix for fetchAssociatedMscaAccount()
} catch (t: Throwable) { + } catch (t: CancellationException) { + throw t + } catch (t: Throwable) { Timber.e(t) null }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/com/vultisig/wallet/ui/screens/v2/defi/circle/CircleDeFiPositionsViewModel.kt` around lines 214 - 230, The current onCreateAccount() uses viewModelScope.launch with runCatching which swallows CancellationException and fetchAssociatedMscaAccount() catches Throwable and likewise masks cancellations; change onCreateAccount() to use viewModelScope.safeLaunch (per project network-call pattern) so cancellations are handled by the safe launcher, and inside fetchAssociatedMscaAccount() add an explicit catch (t: CancellationException) { throw t } before the general catch (t: Throwable) to rethrow cancellation; ensure any runCatching blocks that handle network calls do not absorb CancellationException (filter or rethrow) and reference the functions onCreateAccount(), fetchAssociatedMscaAccount(), and any runCatching usages to locate the changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Outside diff comments:
In
`@app/src/main/java/com/vultisig/wallet/ui/screens/v2/defi/circle/CircleDeFiPositionsViewModel.kt`:
- Around line 214-230: The current onCreateAccount() uses viewModelScope.launch
with runCatching which swallows CancellationException and
fetchAssociatedMscaAccount() catches Throwable and likewise masks cancellations;
change onCreateAccount() to use viewModelScope.safeLaunch (per project
network-call pattern) so cancellations are handled by the safe launcher, and
inside fetchAssociatedMscaAccount() add an explicit catch (t:
CancellationException) { throw t } before the general catch (t: Throwable) to
rethrow cancellation; ensure any runCatching blocks that handle network calls do
not absorb CancellationException (filter or rethrow) and reference the functions
onCreateAccount(), fetchAssociatedMscaAccount(), and any runCatching usages to
locate the changes.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 3c1942d8-756a-4808-9f6a-9aa6fb5b5c50
📒 Files selected for processing (1)
app/src/main/java/com/vultisig/wallet/ui/screens/v2/defi/circle/CircleDeFiPositionsViewModel.kt
Summary
CircleDeFiPositionsViewModelTest > onCreateAccount shows a Success snackbar and saves the new address on successstarted failing on CI with aTimeoutCancellationExceptionfrom the test's 5s real-time wait onisAccountOpen.CircleDeFiPositionsViewModel, not a test-only flake. BothloadCirclePositions()(triggered bysetData) andonCreateAccount()hop to a hard-codedDispatchers.IO; the test scheduler cannot order them. On the "no cached + no on-chain account" branch,loadCirclePositionsunconditionally writesisAccountOpen = false. When that write lands after a racingonCreateAccountsuccess, it stompsisAccountOpenback tofalse— both in the test and in production.IoDispatcher-qualifiedCoroutineDispatcher(defaults toDispatchers.IOvia a new Hilt module); replace everyDispatchers.IOusage in the ViewModel with it. Stop stompingisAccountOpenon the no-account branch — preserve current state so a concurrent create wins cleanly. Update the test to passtestDispatcheras the IO dispatcher, and drop theDispatchers.Default.limitedParallelism(1)+ real-time timeout hacks (everything now completes synchronously under one scheduler).Note on the "Could not connect to maven central to look up the latest available version for io.ktor:ktor-client-mock:3.0.3" lines in the failing CI log: those are Lint's version-lookup warnings, emitted for ~10 unrelated libraries in the same run (rive, hilt, okhttp, timber, commons-compress, kotlinx-datetime, …). They are not the failure cause; the CI line just interleaved with the test failure log.
Test plan
./gradlew :app:testDebugUnitTest --tests CircleDeFiPositionsViewModelTest --rerun-tasksrun 10× → 10/10 pass./gradlew :app:testDebugUnitTest(full suite) → pass./gradlew :app:testReleaseUnitTest(the CI task that was red) → pass./gradlew :app:assembleDebug→ builds (Hilt wiring compiles)./gradlew ktfmtFormat→ cleanCo-Authored-By: aminsato Amin.saradar@yahoo.com
Summary by CodeRabbit
Bug Fixes
Refactor
Tests