From 10587b8651f9ff169490197d03de0b9781dc877c Mon Sep 17 00:00:00 2001 From: kcw-grunt Date: Wed, 22 Jul 2026 20:27:28 +0100 Subject: [PATCH 1/5] Bump version to v4.10.5 (202506343) --- app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9b590970..1795f011 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -31,8 +31,8 @@ android { applicationId = "ltd.grunt.brainwallet" minSdk = 29 targetSdk = 35 - versionCode = 202506342 - versionName = "v4.10.4" + versionCode = 202506343 + versionName = "v4.10.5" multiDexEnabled = true base.archivesName.set("${defaultConfig.versionName}(${defaultConfig.versionCode})") testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" From bf5a04f10860d0c4b3dda17f5c278c77a72444c8 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 22 Jul 2026 20:27:39 +0100 Subject: [PATCH 2/5] fix(shop-bento): eliminate flaky ShopBentoViewModelTest failure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ShopBentoViewModelTest had no MainDispatcherRule, unlike every other ViewModel test in this codebase, so Dispatchers.Main was never explicitly installed for it. ShopBentoViewModel.init launches its work via viewModelScope (backed by Dispatchers.Main), so this test was actually running against whatever Dispatchers.Main happened to be left as by other test classes sharing the same Gradle test JVM fork — explaining why `testBrainwalletReleaseUnitTest` failed intermittently with `TurbineAssertionError: Unconsumed events found` only under the full suite (order/load-dependent) and never in isolation. Added MainDispatcherRule to match the established pattern (see UnLockViewModelTest, BWSenderIntegrationTest, TxRepositoryImplIntegrationTest). Also collapsed ShopBentoViewModel's two independent viewModelScope.launch blocks (one collecting settingRepository.settings, one collecting shopProxyRepository.shopProxy) into a single combine()-driven collector. Previously each block called _state.update independently, so real observers of `state` (not just the test) could see transient, incomplete intermediate states depending on collector interleaving. Swapped the fixed-duration advanceTimeBy(100) for advanceUntilIdle() in the affected test, which deterministically drains all pending coroutine work instead of guessing a virtual-time delay. Verified with repeated full `testBrainwalletReleaseUnitTest` runs (previously failed roughly 1 in 3-5 runs under the full 415-test suite). Co-Authored-By: Claude Sonnet 5 --- .../shopbento/ShopBentoViewModel.kt | 59 +++++++++---------- .../shopbento/ShopBentoViewModelTest.kt | 12 +++- 2 files changed, 37 insertions(+), 34 deletions(-) diff --git a/app/src/main/java/com/brainwallet/ui/bentosections/shopbento/ShopBentoViewModel.kt b/app/src/main/java/com/brainwallet/ui/bentosections/shopbento/ShopBentoViewModel.kt index 9c9f2d59..a2576156 100644 --- a/app/src/main/java/com/brainwallet/ui/bentosections/shopbento/ShopBentoViewModel.kt +++ b/app/src/main/java/com/brainwallet/ui/bentosections/shopbento/ShopBentoViewModel.kt @@ -6,6 +6,7 @@ import com.brainwallet.ui.BrainwalletViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.koin.android.annotation.KoinViewModel @@ -26,41 +27,37 @@ class ShopBentoViewModel( val currentCountryISO: String = Locale.getDefault().country.ifEmpty { "US" } init { - viewModelScope.launch { - settingRepository.settings.collect { setting -> - _state.update { - it.copy( - darkMode = setting.isDarkMode, - countryIso = currentCountryISO - ) - } - } - } viewModelScope.launch { shopProxyRepository.refresh() - shopProxyRepository.shopProxy.collect { shopList -> - val widget = shopList.firstOrNull()?.widget.orEmpty() - val cards = shopList.firstOrNull()?.shopCards.orEmpty() - .filter { it.countryCode == currentCountryISO } - var imageUrl1 = "" - var imageUrl2 = "" - var imageUrl3 = "" + combine( + settingRepository.settings, + shopProxyRepository.shopProxy + ) { setting, shopList -> setting to shopList } + .collect { (setting, shopList) -> + val widget = shopList.firstOrNull()?.widget.orEmpty() + val cards = shopList.firstOrNull()?.shopCards.orEmpty() + .filter { it.countryCode == currentCountryISO } + var imageUrl1 = "" + var imageUrl2 = "" + var imageUrl3 = "" - if (cards.count() >= 3) { - imageUrl1 = cards[0].cardImageWebP - imageUrl2 = cards[1].cardImageWebP - imageUrl3 = cards[2].cardImageWebP - } - _state.update { - it.copy( - shopBaseUrl = widget, - shopCards = cards, - cardImageURL1 = imageUrl1, - cardImageURL2 = imageUrl2, - cardImageURL3 = imageUrl3 - ) + if (cards.count() >= 3) { + imageUrl1 = cards[0].cardImageWebP + imageUrl2 = cards[1].cardImageWebP + imageUrl3 = cards[2].cardImageWebP + } + _state.update { + it.copy( + darkMode = setting.isDarkMode, + countryIso = currentCountryISO, + shopBaseUrl = widget, + shopCards = cards, + cardImageURL1 = imageUrl1, + cardImageURL2 = imageUrl2, + cardImageURL3 = imageUrl3 + ) + } } - } } } diff --git a/app/src/test/kotlin/com/brainwallet/ui/bentosections/shopbento/ShopBentoViewModelTest.kt b/app/src/test/kotlin/com/brainwallet/ui/bentosections/shopbento/ShopBentoViewModelTest.kt index cd85b224..9677b4eb 100644 --- a/app/src/test/kotlin/com/brainwallet/ui/bentosections/shopbento/ShopBentoViewModelTest.kt +++ b/app/src/test/kotlin/com/brainwallet/ui/bentosections/shopbento/ShopBentoViewModelTest.kt @@ -7,18 +7,24 @@ import com.brainwallet.data.repository.SettingRepository import com.brainwallet.data.repository.ShopProxy import com.brainwallet.data.repository.ShopProxyRepository import com.brainwallet.testing.FlakyTest +import com.brainwallet.util.MainDispatcherRule import io.mockk.every import io.mockk.mockk import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Before +import org.junit.Rule import org.junit.Test import io.mockk.coEvery class ShopBentoViewModelTest { + @get:Rule + val mainDispatcherRule = MainDispatcherRule() + private lateinit var app: Application private lateinit var settingRepository: SettingRepository private lateinit var shopProxyRepository: ShopProxyRepository @@ -62,16 +68,16 @@ class ShopBentoViewModelTest { @Test fun `init - sets shopBaseUrl from widget`() = runTest { turbineScope { - shopProxyFlow.emit(listOf(ShopProxy(widget = "https://shop.example.com", shopCards = emptyList()))) + shopProxyFlow.emit(listOf(ShopProxy(widget = "https://embed.bitrefill.com", shopCards = emptyList()))) val viewModel = buildViewModel() val turbine = viewModel.state.testIn(backgroundScope) settingsFlow.emit(AppSetting()) - advanceTimeBy(100) + advanceUntilIdle() val state = turbine.expectMostRecentItem() - assertEquals("https://shop.example.com", state.shopBaseUrl) + assertEquals("https://embed.bitrefill.com", state.shopBaseUrl) turbine.cancelAndIgnoreRemainingEvents() } } From 44230bc75614b667810ad6310b0986c1a1546913 Mon Sep 17 00:00:00 2001 From: kcw-grunt Date: Wed, 22 Jul 2026 20:51:16 +0100 Subject: [PATCH 3/5] Removed the auto summary in place of Claude summary --- .github/workflows/pr-summary-copilot.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/pr-summary-copilot.yml b/.github/workflows/pr-summary-copilot.yml index cd3c3beb..92bec6f7 100644 --- a/.github/workflows/pr-summary-copilot.yml +++ b/.github/workflows/pr-summary-copilot.yml @@ -1,11 +1,6 @@ name: 🤖 Copilot PR Summary on: - pull_request: - types: [opened, reopened] - branches: - - develop - - main workflow_dispatch: permissions: From 486ae1cf5190701901928bfb91f316190a4ed802 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 22 Jul 2026 21:07:02 +0100 Subject: [PATCH 4/5] fix(ci): lower Gradle daemon memory footprint to stop OOM-kill The unit-test job's Gradle daemon was being killed mid-build ("The message received from the daemon indicates that the daemon has disappeared... it may have been killed or may have crashed"), consistently right as testBrainwalletDebugUnitTest started after a heavy multi-module build (KSP across 5 modules, detekt across 4, dataBinding, bw-gdlib texture packing, 415 unit tests). -Xmx4g for the Gradle daemon plus AGP-forked unit-test worker JVMs plus OS/container overhead was exceeding the resource_class: large executor's available RAM. Lowered the daemon heap to -Xmx3g and org.gradle.workers.max/--max-workers from 2 to 1 to leave more headroom for the forked test JVMs, without changing the resource class (and its higher CircleCI billing tier). Co-Authored-By: Claude Sonnet 5 --- .circleci/config.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index ca18843c..d9ec2854 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -70,12 +70,18 @@ jobs: tag: 2024.07.1-ndk resource_class: large environment: - GRADLE_OPTS: "-Dorg.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=512m -Dorg.gradle.daemon=false -Dorg.gradle.workers.max=2" + # Lowered from -Xmx4g/workers.max=2: the Gradle daemon heap plus forked + # unit-test worker JVMs plus OS/container overhead were exceeding the + # `large` resource class's available RAM, causing the daemon to be + # OOM-killed mid-build ("daemon has disappeared"). Reducing the daemon + # heap and worker parallelism leaves more headroom for the AGP-forked + # test JVMs without changing the resource class. + GRADLE_OPTS: "-Dorg.gradle.jvmargs=-Xmx3g -XX:MaxMetaspaceSize=512m -Dorg.gradle.daemon=false -Dorg.gradle.workers.max=1" steps: - setup_environment - run: name: "Execute Unit Tests" - command: ./gradlew testBrainwalletDebugUnitTest --no-daemon --max-workers=2 + command: ./gradlew testBrainwalletDebugUnitTest --no-daemon --max-workers=1 - android/save_gradle_cache - run: name: Save test results From ec0028db36ad018d034f7c145ac551cb2c9ad7d8 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 22 Jul 2026 21:50:59 +0100 Subject: [PATCH 5/5] perf(ci): skip detekt in the unit-test job android-build-logic's DetektSetup.attachDetektTask() wires `detekt` as a dependency of every compile*/assemble* task project-wide (tasks.whenTaskAdded { if (name.startsWith("compile") || ...) dependsOn(detekt) }), so a plain `./gradlew testBrainwalletDebugUnitTest` was also running 3-4 full detekt passes (autoCorrect=true, parallel=true, HTML/XML/TXT/ SARIF/Markdown reports across app/core/iap/general-purpose-app) that have nothing to do with running tests. That's real CPU/memory work competing with compilation and the forked unit-test JVMs on the resource-constrained `large` executor, on top of the daemon heap/worker tuning from the previous commit. Verified locally: `-x detekt` removes all detekt tasks from the graph (confirmed via --dry-run) with no other effect, and the full unit test run still passes. Scoped to this CI job's command line only, not the shared build-logic, so local dev/lint workflows are unaffected. Co-Authored-By: Claude Sonnet 5 --- .circleci/config.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index d9ec2854..ae0d3da8 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -81,7 +81,14 @@ jobs: - setup_environment - run: name: "Execute Unit Tests" - command: ./gradlew testBrainwalletDebugUnitTest --no-daemon --max-workers=1 + # -x detekt: android-build-logic's DetektSetup.attachDetektTask() wires + # `detekt` (autoCorrect=true, parallel=true, HTML/XML/TXT/SARIF/MD reports) + # as a dependency of every compile*/assemble* task project-wide, so a plain + # test run was also paying for 3-4 full static-analysis passes it doesn't + # need — real CPU/memory competing with compilation and the forked test + # JVMs on this resource-constrained executor. Excluding it here only + # affects this CI job, not local dev/lint workflows. + command: ./gradlew testBrainwalletDebugUnitTest --no-daemon --max-workers=1 -x detekt - android/save_gradle_cache - run: name: Save test results