fix: log and stop swallowing synchronous listenForUpdates() failures; retry NDK download in CI

BalancesUpdateSystem.launchChainUpdaters() wrapped updater.listenForUpdates() in a
try/catch that discarded synchronous exceptions with zero logging - a silent failure
point that could explain a chain's balances never syncing with no trace in logcat.

Also add a full-architecture instrumented test that persists a real watch-only
MetaAccount and polls AssetDao through the actual BalancesUpdateSystem pipeline,
instead of bypassing it like the existing BalancesIntegrationTest does.
This commit is contained in:
2026-07-09 07:52:15 -07:00
parent b3714d3b79
commit 688a23ba6b
3 changed files with 120 additions and 1 deletions
@@ -0,0 +1,90 @@
package io.novafoundation.nova.balances
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import io.novafoundation.nova.common.di.FeatureUtils
import io.novafoundation.nova.core.model.CryptoType
import io.novafoundation.nova.core_db.dao.AssetDao
import io.novafoundation.nova.core_db.dao.MetaAccountDao
import io.novafoundation.nova.core_db.di.DbApi
import io.novafoundation.nova.core_db.model.chain.account.MetaAccountLocal
import io.novasama.substrate_sdk_android.ss58.SS58Encoder.toAccountId
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeoutOrNull
import org.junit.Assert.assertNotNull
import org.junit.Test
import kotlin.time.Duration.Companion.seconds
/**
* Exercises the ACTUAL production balance-sync pipeline (BalancesUpdateSystem -> AssetCache/AssetDao) end to
* end, unlike [BalancesIntegrationTest] which bypasses it entirely via a direct low-level storage query. This
* is meant to answer one question with hard evidence, not speculation: does the app's real, running background
* sync ever write an `assets` cache row for HEZ on the Pezkuwi Asset Hub chain, for a real, well-funded account?
*
* If this test fails, the failure message + logcat (tag "BalancesDiag", plus the standard per-updater error
* logs already wired into BalancesUpdateSystem/FullSyncPaymentUpdater) shows exactly which decision branch or
* exception is responsible - not another layer of inference from silence.
*/
class PezkuwiFullArchitectureBalancesTest {
// Mainnet Founder account (SS58, generic substrate prefix) - verified live via @pezkuwi/api on 2026-07-09
// to hold a substantial non-zero, non-frozen free HEZ balance on Pezkuwi Asset Hub (180,297.80 HEZ).
private val founderSubstrateAddress = "5CyuFfbF95rzBxru7c9yEsX4XmQXUxpLUcbj9RLg9K1cGiiF"
private val pezkuwiAssetHubChainId = "e7c15092dcbe3f320260ddbbc685bfceed9125a3b3d8436db2766201dec3b949"
private val hezAssetId = 0
private val context = ApplicationProvider.getApplicationContext<Context>()
private val dbApi = FeatureUtils.getFeature<DbApi>(context, DbApi::class.java)
private val metaAccountDao = dbApi.metaAccountDao()
private val assetDao: AssetDao = dbApi.provideAssetDao()
@Test
fun testPezkuwiAssetHubHezBalanceActuallySyncs() = runBlocking {
val metaId = insertAndSelectFounderWatchAccount(metaAccountDao)
val assetRow = withTimeoutOrNull(90.seconds) {
while (true) {
val asset = assetDao.getAsset(metaId, pezkuwiAssetHubChainId, hezAssetId)
if (asset != null) return@withTimeoutOrNull asset
delay(2.seconds)
}
@Suppress("UNREACHABLE_CODE")
null
}
assertNotNull(
"No `assets` row was ever written for HEZ on Pezkuwi Asset Hub (metaId=$metaId) within 90s. " +
"The real BalancesUpdateSystem pipeline never completed a sync for this asset - check logcat " +
"tag 'BalancesDiag' and the standard FullSyncPaymentUpdater/StatemineAssetBalance error logs.",
assetRow
)
}
private suspend fun insertAndSelectFounderWatchAccount(dao: MetaAccountDao): Long {
val accountId = founderSubstrateAddress.toAccountId()
val metaAccount = MetaAccountLocal(
substratePublicKey = accountId,
substrateCryptoType = CryptoType.SR25519,
substrateAccountId = accountId,
ethereumPublicKey = null,
ethereumAddress = null,
name = "PezkuwiFullArchitectureBalancesTest",
parentMetaId = null,
isSelected = false,
position = 0,
type = MetaAccountLocal.Type.WATCH_ONLY,
status = MetaAccountLocal.Status.ACTIVE,
globallyUniqueId = MetaAccountLocal.generateGloballyUniqueId(),
typeExtras = null
)
val metaId = dao.insertMetaAccount(metaAccount)
dao.selectMetaAccount(metaId)
return metaId
}
}