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.
This commit is contained in:
Satoshi Qazi Muhammed committed 2026-07-09 12:08:27 -07:00
1 parent c5174d0ccf
commit 72a881c059
2 files changed
+38 -28

No files matched your search

@@ -2,27 +2,40 @@ package io.novafoundation.nova.balances
import android.content.Context import android.content.Context
import androidx.test.core.app.ApplicationProvider import androidx.test.core.app.ApplicationProvider
import com.google.gson.Gson
import io.novafoundation.nova.common.di.FeatureUtils 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.model.CryptoType
import io.novafoundation.nova.core_db.dao.AssetDao import io.novafoundation.nova.core_db.dao.AssetDao
import io.novafoundation.nova.core_db.dao.MetaAccountDao import io.novafoundation.nova.core_db.dao.MetaAccountDao
import io.novafoundation.nova.core_db.di.DbApi import io.novafoundation.nova.core_db.di.DbApi
import io.novafoundation.nova.core_db.model.chain.account.MetaAccountLocal import io.novafoundation.nova.core_db.model.chain.account.MetaAccountLocal
import io.novafoundation.nova.feature_assets.di.AssetsFeatureApi 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 io.novasama.substrate_sdk_android.ss58.SS58Encoder.toAccountId
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeoutOrNull import kotlinx.coroutines.withTimeoutOrNull
import org.junit.Assert.assertNotNull import org.junit.Assert.assertTrue
import org.junit.Test import org.junit.Test
import java.net.URL
import kotlin.time.Duration.Companion.seconds 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<AssetFixture>)
/** /**
* Exercises the ACTUAL production balance-sync pipeline (BalancesUpdateSystem -> AssetCache/AssetDao) end to * 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 * 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 * is meant to answer one question with hard evidence, not speculation: for a real, well-funded mainnet Founder
* sync ever write an `assets` cache row for HEZ on the Pezkuwi Asset Hub chain, for a real, well-funded account? * 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, * 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 * 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. * 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/ * 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 - * FullSyncPaymentUpdater/NativeAssetBalance/StatemineAssetBalance show exactly which decision branch or
* not another layer of inference from silence. * exception is responsible - not another layer of inference from silence.
*/ */
class PezkuwiFullArchitectureBalancesTest { 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 context = ApplicationProvider.getApplicationContext<Context>()
private val dbApi = FeatureUtils.getFeature<DbApi>(context, DbApi::class.java) private val dbApi = FeatureUtils.getFeature<DbApi>(context, DbApi::class.java)
@@ -50,36 +57,38 @@ class PezkuwiFullArchitectureBalancesTest {
private val assetsFeatureApi = FeatureUtils.getFeature<AssetsFeatureApi>(context, AssetsFeatureApi::class.java) private val assetsFeatureApi = FeatureUtils.getFeature<AssetsFeatureApi>(context, AssetsFeatureApi::class.java)
@Test @Test
fun testPezkuwiAssetHubHezBalanceActuallySyncs() = runBlocking { fun testPezkuwiEcosystemAssetsActuallySync() = runBlocking {
val fixture: AssetsFixtureFile = Gson().fromJson(URL(TEST_ASSETS_URL).readText())
val updateSystemJob = launch { assetsFeatureApi.updateSystem.start().collect {} } val updateSystemJob = launch { assetsFeatureApi.updateSystem.start().collect {} }
try { try {
val metaId = insertAndSelectFounderWatchAccount(metaAccountDao) val metaId = insertAndSelectWatchAccount(metaAccountDao, fixture.account)
val assetRow = withTimeoutOrNull(90.seconds) { val stillMissing = fixture.assets.toMutableList()
while (true) { withTimeoutOrNull(120.seconds) {
val asset = assetDao.getAsset(metaId, pezkuwiAssetHubChainId, hezAssetId) while (stillMissing.isNotEmpty()) {
if (asset != null) return@withTimeoutOrNull asset stillMissing.removeAll { asset ->
assetDao.getAsset(metaId, asset.chainId, asset.assetId) != null
delay(2.seconds) }
if (stillMissing.isNotEmpty()) delay(2.seconds)
} }
@Suppress("UNREACHABLE_CODE")
null
} }
assertNotNull( assertTrue(
"No `assets` row was ever written for HEZ on Pezkuwi Asset Hub (metaId=$metaId) within 90s. " + "No `assets` row was ever written for: ${stillMissing.joinToString { "${it.symbol} on ${it.chainName}" }} " +
"The real BalancesUpdateSystem pipeline never completed a sync for this asset - check the " + "(metaId=$metaId) within 120s, out of ${fixture.assets.size} total. The real BalancesUpdateSystem " +
"standard FullSyncPaymentUpdater/NativeAssetBalance error logs in logcat.", "pipeline never completed a sync for these - check the standard FullSyncPaymentUpdater/" +
assetRow "NativeAssetBalance/StatemineAssetBalance error logs in logcat.",
stillMissing.isEmpty()
) )
} finally { } finally {
updateSystemJob.cancel() updateSystemJob.cancel()
} }
} }
private suspend fun insertAndSelectFounderWatchAccount(dao: MetaAccountDao): Long { private suspend fun insertAndSelectWatchAccount(dao: MetaAccountDao, substrateAddress: String): Long {
val accountId = founderSubstrateAddress.toAccountId() val accountId = substrateAddress.toAccountId()
val metaAccount = MetaAccountLocal( val metaAccount = MetaAccountLocal(
substratePublicKey = accountId, substratePublicKey = accountId,
+1
View File
@@ -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", "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_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", "INFURA_API_KEY", readStringSecret("INFURA_API_KEY")
buildConfigField "String", "DWELLIR_API_KEY", readStringSecret("DWELLIR_API_KEY") buildConfigField "String", "DWELLIR_API_KEY", readStringSecret("DWELLIR_API_KEY")