From 72a881c0591e74cc001057eb58d598911d41e749 Mon Sep 17 00:00:00 2001 From: Satoshi Qazi Muhammed Date: Thu, 9 Jul 2026 12:08:27 -0700 Subject: [PATCH] test: expand full-architecture test to cover the whole Pezkuwi ecosystem Was HEZ-on-Asset-Hub-only. Now data-driven from wallet-utils' new pezkuwi_assets_for_testBalance.json (TEST_ASSETS_URL), asserting every asset - HEZ/PEZ/USDT/DOT/ETH/BTC across Pezkuwi's 3 chains - gets an assets DB row written via a single shared BalancesUpdateSystem run, not just one asset on one chain. Failure message lists exactly which assets never synced, by name and chain. --- .../PezkuwiFullArchitectureBalancesTest.kt | 65 +++++++++++-------- runtime/build.gradle | 1 + 2 files changed, 38 insertions(+), 28 deletions(-) diff --git a/app/src/androidTest/java/io/novafoundation/nova/balances/PezkuwiFullArchitectureBalancesTest.kt b/app/src/androidTest/java/io/novafoundation/nova/balances/PezkuwiFullArchitectureBalancesTest.kt index 08377910..7ecf6dd6 100644 --- a/app/src/androidTest/java/io/novafoundation/nova/balances/PezkuwiFullArchitectureBalancesTest.kt +++ b/app/src/androidTest/java/io/novafoundation/nova/balances/PezkuwiFullArchitectureBalancesTest.kt @@ -2,27 +2,40 @@ package io.novafoundation.nova.balances import android.content.Context import androidx.test.core.app.ApplicationProvider +import com.google.gson.Gson import io.novafoundation.nova.common.di.FeatureUtils +import io.novafoundation.nova.common.utils.fromJson 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.novafoundation.nova.feature_assets.di.AssetsFeatureApi +import io.novafoundation.nova.runtime.BuildConfig.TEST_ASSETS_URL import io.novasama.substrate_sdk_android.ss58.SS58Encoder.toAccountId import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withTimeoutOrNull -import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue import org.junit.Test +import java.net.URL import kotlin.time.Duration.Companion.seconds +private data class AssetFixture(val chainId: String, val chainName: String, val assetId: Int, val symbol: String) +private data class AssetsFixtureFile(val account: String, val assets: List) + /** * 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? + * is meant to answer one question with hard evidence, not speculation: for a real, well-funded mainnet Founder + * account, does the app's real, running background sync ever write an `assets` cache row for every asset in + * wallet-utils' pezkuwi_assets_for_testBalance.json (HEZ/PEZ/USDT/DOT/ETH/BTC across Pezkuwi's chains) - not + * just the native balance on one chain, which is all the older [BalancesIntegrationTest] fixture covers. + * + * A single watch-only account is created and selected once, so a single BalancesUpdateSystem run has to + * successfully sync every asset in the fixture - this is what actually caught the 2026-07-09 HEZ-on-Asset-Hub + * silent sync failure (5 of 6 assets on that chain synced fine; only HEZ silently never did). * * BalancesUpdateSystem.start() is a cold flow - in production it's only ever collected by RootInteractor, * which is wired to the root Activity/ViewModel lifecycle. A bare instrumented test never launches that @@ -30,17 +43,11 @@ import kotlin.time.Duration.Companion.seconds * uses, instead of relying on app UI lifecycle to start it. * * If this test fails, the standard per-updater error logs already wired into BalancesUpdateSystem/ - * FullSyncPaymentUpdater/NativeAssetBalance show exactly which decision branch or exception is responsible - - * not another layer of inference from silence. + * FullSyncPaymentUpdater/NativeAssetBalance/StatemineAssetBalance show 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() private val dbApi = FeatureUtils.getFeature(context, DbApi::class.java) @@ -50,36 +57,38 @@ class PezkuwiFullArchitectureBalancesTest { private val assetsFeatureApi = FeatureUtils.getFeature(context, AssetsFeatureApi::class.java) @Test - fun testPezkuwiAssetHubHezBalanceActuallySyncs() = runBlocking { + fun testPezkuwiEcosystemAssetsActuallySync() = runBlocking { + val fixture: AssetsFixtureFile = Gson().fromJson(URL(TEST_ASSETS_URL).readText()) + val updateSystemJob = launch { assetsFeatureApi.updateSystem.start().collect {} } try { - val metaId = insertAndSelectFounderWatchAccount(metaAccountDao) + val metaId = insertAndSelectWatchAccount(metaAccountDao, fixture.account) - val assetRow = withTimeoutOrNull(90.seconds) { - while (true) { - val asset = assetDao.getAsset(metaId, pezkuwiAssetHubChainId, hezAssetId) - if (asset != null) return@withTimeoutOrNull asset - - delay(2.seconds) + val stillMissing = fixture.assets.toMutableList() + withTimeoutOrNull(120.seconds) { + while (stillMissing.isNotEmpty()) { + stillMissing.removeAll { asset -> + assetDao.getAsset(metaId, asset.chainId, asset.assetId) != null + } + if (stillMissing.isNotEmpty()) 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 the " + - "standard FullSyncPaymentUpdater/NativeAssetBalance error logs in logcat.", - assetRow + assertTrue( + "No `assets` row was ever written for: ${stillMissing.joinToString { "${it.symbol} on ${it.chainName}" }} " + + "(metaId=$metaId) within 120s, out of ${fixture.assets.size} total. The real BalancesUpdateSystem " + + "pipeline never completed a sync for these - check the standard FullSyncPaymentUpdater/" + + "NativeAssetBalance/StatemineAssetBalance error logs in logcat.", + stillMissing.isEmpty() ) } finally { updateSystemJob.cancel() } } - private suspend fun insertAndSelectFounderWatchAccount(dao: MetaAccountDao): Long { - val accountId = founderSubstrateAddress.toAccountId() + private suspend fun insertAndSelectWatchAccount(dao: MetaAccountDao, substrateAddress: String): Long { + val accountId = substrateAddress.toAccountId() val metaAccount = MetaAccountLocal( substratePublicKey = accountId, diff --git a/runtime/build.gradle b/runtime/build.gradle index 207a8520..7d2d4e0e 100644 --- a/runtime/build.gradle +++ b/runtime/build.gradle @@ -13,6 +13,7 @@ android { buildConfigField "String", "PRE_CONFIGURED_CHAIN_DETAILS_URL", "\"https://raw.githubusercontent.com/pezkuwichain/pezkuwi-wallet-utils/master/chains/v22/preConfigured/details\"" buildConfigField "String", "TEST_CHAINS_URL", "\"https://raw.githubusercontent.com/pezkuwichain/pezkuwi-wallet-utils/master/tests/chains_for_testBalance.json\"" + buildConfigField "String", "TEST_ASSETS_URL", "\"https://raw.githubusercontent.com/pezkuwichain/pezkuwi-wallet-utils/master/tests/pezkuwi_assets_for_testBalance.json\"" buildConfigField "String", "INFURA_API_KEY", readStringSecret("INFURA_API_KEY") buildConfigField "String", "DWELLIR_API_KEY", readStringSecret("DWELLIR_API_KEY")