From 0bb48db5f55a4fe1ce3ee09a517cb78101635c70 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 17 Sep 2026 08:56:37 -0300 Subject: [PATCH 1/3] test: use in-memory cache store in quickpay tests Co-Authored-By: Claude Opus 5 (1M context) --- app/src/main/java/to/bitkit/data/CacheStore.kt | 7 ++++--- .../to/bitkit/repositories/QuickPayRepoTest.kt | 14 +++----------- .../java/to/bitkit/test/InMemoryDataStore.kt | 18 ++++++++++++++++++ 3 files changed, 25 insertions(+), 14 deletions(-) create mode 100644 app/src/test/java/to/bitkit/test/InMemoryDataStore.kt diff --git a/app/src/main/java/to/bitkit/data/CacheStore.kt b/app/src/main/java/to/bitkit/data/CacheStore.kt index f45f5e3cdd..32f87e266d 100644 --- a/app/src/main/java/to/bitkit/data/CacheStore.kt +++ b/app/src/main/java/to/bitkit/data/CacheStore.kt @@ -29,10 +29,11 @@ private val Context.appCacheDataStore: DataStore by dataStore( @Suppress("TooManyFunctions") @Singleton -class CacheStore @Inject constructor( - @ApplicationContext private val context: Context, +class CacheStore internal constructor( + private val store: DataStore, ) { - private val store = context.appCacheDataStore + @Inject + constructor(@ApplicationContext context: Context) : this(context.appCacheDataStore) val data: Flow = store.data val backupStatuses: Flow> = data.map { it.backupStatuses } diff --git a/app/src/test/java/to/bitkit/repositories/QuickPayRepoTest.kt b/app/src/test/java/to/bitkit/repositories/QuickPayRepoTest.kt index 8908eced59..b3ca16b3c7 100644 --- a/app/src/test/java/to/bitkit/repositories/QuickPayRepoTest.kt +++ b/app/src/test/java/to/bitkit/repositories/QuickPayRepoTest.kt @@ -1,8 +1,6 @@ package to.bitkit.repositories import android.app.Application -import android.content.Context -import androidx.test.core.app.ApplicationProvider import app.cash.turbine.test import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineStart @@ -12,13 +10,11 @@ import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.withContext import kotlinx.coroutines.yield -import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -43,6 +39,7 @@ import to.bitkit.models.QuickPayLedger import to.bitkit.models.QuickPayRecordPhase import to.bitkit.models.USD import to.bitkit.test.BaseUnitTest +import to.bitkit.test.InMemoryDataStore import to.bitkit.utils.AppError import to.bitkit.utils.LdkError import java.math.BigDecimal @@ -106,8 +103,7 @@ class QuickPayRepoTest : BaseUnitTest() { } """.trimIndent() } - private val context = ApplicationProvider.getApplicationContext() - private val cacheStore = CacheStore(context) + private val cacheStore = CacheStore(InMemoryDataStore(AppCacheData())) private val settingsStore: SettingsStore = mock() private val currencyRepo: CurrencyRepo = mock() private val lightningRepo: LightningRepo = mock() @@ -122,8 +118,7 @@ class QuickPayRepoTest : BaseUnitTest() { private lateinit var sut: QuickPayRepo @Before - fun setUp() = runBlocking { - cacheStore.reset() + fun setUp() { paymentRows = null whenever(settingsStore.data).thenReturn(settingsData) whenever(lightningRepo.lightningState).thenReturn(lightningState) @@ -145,9 +140,6 @@ class QuickPayRepoTest : BaseUnitTest() { sut = repo() } - @After - fun tearDown() = runBlocking { cacheStore.reset() } - @Test fun `reserveBound on clock rollback keeps existing spend`() = test { assertNotNull(sut.reserveBound("a", 500u).getOrThrow()) diff --git a/app/src/test/java/to/bitkit/test/InMemoryDataStore.kt b/app/src/test/java/to/bitkit/test/InMemoryDataStore.kt new file mode 100644 index 0000000000..67033955b5 --- /dev/null +++ b/app/src/test/java/to/bitkit/test/InMemoryDataStore.kt @@ -0,0 +1,18 @@ +package to.bitkit.test + +import androidx.datastore.core.DataStore +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock + +class InMemoryDataStore(initial: T) : DataStore { + private val state = MutableStateFlow(initial) + private val mutex = Mutex() + + override val data: Flow = state + + override suspend fun updateData(transform: suspend (t: T) -> T): T = mutex.withLock { + transform(state.value).also { state.value = it } + } +} From 30be0e17a41c2d9bda0aae5651f8021cd96d18f1 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 17 Sep 2026 09:01:47 -0300 Subject: [PATCH 2/3] test: run lightning service ldk queue on test dispatcher Co-Authored-By: Claude Opus 5 (1M context) --- .../main/java/to/bitkit/async/ServiceQueue.kt | 2 + .../to/bitkit/services/LightningService.kt | 105 +++++++++++------- .../bitkit/services/LightningServiceTest.kt | 2 +- 3 files changed, 67 insertions(+), 42 deletions(-) diff --git a/app/src/main/java/to/bitkit/async/ServiceQueue.kt b/app/src/main/java/to/bitkit/async/ServiceQueue.kt index ea3b4ad0a8..d81323687b 100644 --- a/app/src/main/java/to/bitkit/async/ServiceQueue.kt +++ b/app/src/main/java/to/bitkit/async/ServiceQueue.kt @@ -17,6 +17,8 @@ enum class ServiceQueue { private val scope by lazy { CoroutineScope(newSingleThreadDispatcher(name) + SupervisorJob()) } + val queueContext: CoroutineContext get() = scope.coroutineContext + fun blocking( coroutineContext: CoroutineContext = scope.coroutineContext, block: suspend CoroutineScope.() -> T, diff --git a/app/src/main/java/to/bitkit/services/LightningService.kt b/app/src/main/java/to/bitkit/services/LightningService.kt index bc887e9cd1..71b75c30ef 100644 --- a/app/src/main/java/to/bitkit/services/LightningService.kt +++ b/app/src/main/java/to/bitkit/services/LightningService.kt @@ -124,15 +124,16 @@ data class AddressDerivationInfo( @Suppress("LargeClass", "LongParameterList", "TooManyFunctions") @Singleton -class LightningService @Inject constructor( - @BgDispatcher private val bgDispatcher: CoroutineDispatcher, - @IoDispatcher private val ioDispatcher: CoroutineDispatcher, +class LightningService internal constructor( + private val bgDispatcher: CoroutineDispatcher, + private val ioDispatcher: CoroutineDispatcher, private val keychain: Keychain, private val vssStoreIdProvider: VssStoreIdProvider, private val settingsStore: SettingsStore, private val watchOnlyAccountStore: WatchOnlyAccountStore, private val loggerLdk: LoggerLdk, private val watchOnlyAccountLifecycleCoordinator: WatchOnlyAccountLifecycleCoordinator, + private val ldkQueue: CoroutineContext, ) : BaseCoroutineScope(bgDispatcher, TAG) { companion object { @@ -168,6 +169,28 @@ class LightningService @Inject constructor( ) } + @Inject + constructor( + @BgDispatcher bgDispatcher: CoroutineDispatcher, + @IoDispatcher ioDispatcher: CoroutineDispatcher, + keychain: Keychain, + vssStoreIdProvider: VssStoreIdProvider, + settingsStore: SettingsStore, + watchOnlyAccountStore: WatchOnlyAccountStore, + loggerLdk: LoggerLdk, + watchOnlyAccountLifecycleCoordinator: WatchOnlyAccountLifecycleCoordinator, + ) : this( + bgDispatcher = bgDispatcher, + ioDispatcher = ioDispatcher, + keychain = keychain, + vssStoreIdProvider = vssStoreIdProvider, + settingsStore = settingsStore, + watchOnlyAccountStore = watchOnlyAccountStore, + loggerLdk = loggerLdk, + watchOnlyAccountLifecycleCoordinator = watchOnlyAccountLifecycleCoordinator, + ldkQueue = ServiceQueue.LDK.queueContext, + ) + @Volatile var node: Node? = null @@ -244,7 +267,7 @@ class LightningService @Inject constructor( customRgsServerUrl: String?, config: Config, channelMigration: ChannelDataMigration? = null, - ): Node = ServiceQueue.LDK.background { + ): Node = ServiceQueue.LDK.background(ldkQueue) { val storedSettings = settingsStore.data.first() val settings = storedSettings.withRequiredNativeSegwitMonitoring() if (settings != storedSettings) { @@ -343,7 +366,7 @@ class LightningService @Inject constructor( Logger.debug("Starting node…", context = TAG) - ServiceQueue.LDK.background { + ServiceQueue.LDK.background(ldkQueue) { try { node.start() } catch (e: NodeException) { @@ -393,7 +416,7 @@ class LightningService @Inject constructor( } val desiredConfigs = enabledOnchainWalletAccountConfigs(walletRecords, currentWalletIndex) - ServiceQueue.LDK.background { + ServiceQueue.LDK.background(ldkQueue) { val trackedAccounts = node.listOnchainWalletAccounts() val managedKeys = (walletRecords + accountsPendingRemoval).mapNotNull { record -> when (record.addressType) { @@ -453,7 +476,7 @@ class LightningService @Inject constructor( } Logger.debug("Stopping node…", context = TAG) - ServiceQueue.LDK.background { + ServiceQueue.LDK.background(ldkQueue) { runSuspendCatching { node.stop() } .onFailure { if (it !is NodeException.NotRunning) Logger.warn("Node stop error", it, context = TAG) @@ -598,7 +621,7 @@ class LightningService @Inject constructor( reconcileWatchOnlyAccounts(syncAfterReconcile = false) Logger.verbose("Syncing LDK…", context = TAG) - ServiceQueue.LDK.background { + ServiceQueue.LDK.background(ldkQueue) { node.syncWallets() } @@ -611,7 +634,7 @@ class LightningService @Inject constructor( val node = this.node ?: throw ServiceError.NodeNotSetup() val msg = runCatching { message.uByteList }.getOrNull() ?: throw ServiceError.InvalidNodeSigningMessage() - return ServiceQueue.LDK.background { + return ServiceQueue.LDK.background(ldkQueue) { node.signMessage(msg) } } @@ -619,7 +642,7 @@ class LightningService @Inject constructor( suspend fun newAddress(): String { val node = this.node ?: throw ServiceError.NodeNotSetup() - return ServiceQueue.LDK.background { + return ServiceQueue.LDK.background(ldkQueue) { node.onchainPayment().newAddress() } } @@ -632,7 +655,7 @@ class LightningService @Inject constructor( suspend fun newAddressInfoForType(addressType: AddressType): AddressDerivationInfo { val node = this.node ?: throw ServiceError.NodeNotSetup() - return ServiceQueue.LDK.background { + return ServiceQueue.LDK.background(ldkQueue) { val addressInfo = node.onchainPayment().newAddressInfoForType(addressType.toLdkAddressType()) AddressDerivationInfo(address = addressInfo.address, index = addressInfo.index.toInt()) } @@ -641,7 +664,7 @@ class LightningService @Inject constructor( suspend fun addressInfoForType(addressType: AddressType, receiveIndex: Int): AddressDerivationInfo { val node = this.node ?: throw ServiceError.NodeNotSetup() - return ServiceQueue.LDK.background { + return ServiceQueue.LDK.background(ldkQueue) { val addressInfo = node.onchainPayment().addressInfoForTypeAtIndex( addressType.toLdkAddressType(), KeychainKind.EXTERNAL, @@ -660,7 +683,7 @@ class LightningService @Inject constructor( val node = this.node ?: throw ServiceError.NodeNotSetup() val keychain = if (isChange) KeychainKind.INTERNAL else KeychainKind.EXTERNAL - return ServiceQueue.LDK.background { + return ServiceQueue.LDK.background(ldkQueue) { node.onchainPayment() .addressInfosForType( addressType.toLdkAddressType(), @@ -675,7 +698,7 @@ class LightningService @Inject constructor( suspend fun revealReceiveAddresses(toReceiveIndex: Int, forType: AddressType) { val node = this.node ?: throw ServiceError.NodeNotSetup() - ServiceQueue.LDK.background { + ServiceQueue.LDK.background(ldkQueue) { node.onchainPayment().revealReceiveAddressesTo(forType.toLdkAddressType(), toReceiveIndex.toUInt()) } } @@ -684,7 +707,7 @@ class LightningService @Inject constructor( suspend fun connectToTrustedPeers() { val node = this.node ?: throw ServiceError.NodeNotSetup() - ServiceQueue.LDK.background { + ServiceQueue.LDK.background(ldkQueue) { for (peer in trustedPeers) { try { node.connect(peer.nodeId, peer.address, persist = true) @@ -721,7 +744,7 @@ class LightningService @Inject constructor( val node = this.node ?: throw ServiceError.NodeNotSetup() val uri = peer.uri - return ServiceQueue.LDK.background { + return ServiceQueue.LDK.background(ldkQueue) { try { Logger.debug("Connecting peer: $uri", context = TAG) node.connect(peer.nodeId, peer.address, persist = true) @@ -739,7 +762,7 @@ class LightningService @Inject constructor( val node = this.node ?: throw ServiceError.NodeNotSetup() val uri = peer.uri - return ServiceQueue.LDK.background { + return ServiceQueue.LDK.background(ldkQueue) { try { Logger.debug("Disconnecting peer: $uri", context = TAG) node.disconnect(peer.nodeId) @@ -774,7 +797,7 @@ class LightningService @Inject constructor( ): Result { val node = this.node ?: throw ServiceError.NodeNotSetup() - return ServiceQueue.LDK.background { + return ServiceQueue.LDK.background(ldkQueue) { try { val pushToCounterpartyMsat = pushToCounterpartySats?.let { it * 1000u } Logger.debug("Initiating channel open (sats: '$channelAmountSats') with: '${peer.uri}'", context = TAG) @@ -823,7 +846,7 @@ class LightningService @Inject constructor( } try { - ServiceQueue.LDK.background { + ServiceQueue.LDK.background(ldkQueue) { Logger.debug("Initiating channel close (force=$force): '$channelId'", context = TAG) if (force) { node.forceCloseChannel(userChannelId, counterpartyNodeId, forceCloseReason.orEmpty()) @@ -891,7 +914,7 @@ class LightningService @Inject constructor( val message = description - return ServiceQueue.LDK.background { + return ServiceQueue.LDK.background(ldkQueue) { val bolt11Invoice: Bolt11Invoice = if (amountMsat != null) { node.bolt11Payment() .receive( @@ -925,7 +948,7 @@ class LightningService @Inject constructor( context = TAG, ) - return ServiceQueue.LDK.background { + return ServiceQueue.LDK.background(ldkQueue) { if (isMaxAmount) { node.onchainPayment().sendAllToAddress( address = address, @@ -951,7 +974,7 @@ class LightningService @Inject constructor( val bolt11Invoice = runCatching { Bolt11Invoice.fromStr(bolt11) } .getOrElse { throw LdkError(it as NodeException) } - return ServiceQueue.LDK.background { + return ServiceQueue.LDK.background(ldkQueue) { runCatching { when (sats != null) { true -> node.bolt11Payment().sendUsingAmount(bolt11Invoice, sats * 1000u, null) @@ -966,7 +989,7 @@ class LightningService @Inject constructor( suspend fun estimateRoutingFees(bolt11: String): Result { val node = this.node ?: throw ServiceError.NodeNotSetup() - return ServiceQueue.LDK.background { + return ServiceQueue.LDK.background(ldkQueue) { return@background runCatching { val invoice = Bolt11Invoice.fromStr(bolt11) val feesMsat = node.bolt11Payment().estimateRoutingFees(invoice) @@ -981,7 +1004,7 @@ class LightningService @Inject constructor( suspend fun estimateRoutingFeesForAmount(bolt11: String, amountSats: ULong): Result { val node = this.node ?: throw ServiceError.NodeNotSetup() - return ServiceQueue.LDK.background { + return ServiceQueue.LDK.background(ldkQueue) { return@background runCatching { val invoice = Bolt11Invoice.fromStr(bolt11) val amountMsat = amountSats * 1000u @@ -1008,7 +1031,7 @@ class LightningService @Inject constructor( context = TAG ) - return ServiceQueue.LDK.background { + return ServiceQueue.LDK.background(ldkQueue) { runCatching { val handles = node.bolt11Payment().sendProbes(bolt11Invoice, null) Result.success(handles.map { it.paymentId }.toSet()) @@ -1032,7 +1055,7 @@ class LightningService @Inject constructor( context = TAG ) - return ServiceQueue.LDK.background { + return ServiceQueue.LDK.background(ldkQueue) { runCatching { val handles = node.bolt11Payment().sendProbesUsingAmount(bolt11Invoice, amountMsat, null) Result.success(handles.map { it.paymentId }.toSet()) @@ -1051,7 +1074,7 @@ class LightningService @Inject constructor( context = TAG, ) - return ServiceQueue.LDK.background { + return ServiceQueue.LDK.background(ldkQueue) { runCatching { val handles = node.spontaneousPayment().sendProbes(amountMsat, nodeId) Result.success(handles.map { it.paymentId }.toSet()) @@ -1075,7 +1098,7 @@ class LightningService @Inject constructor( suspend fun listSpendableOutputs(): Result> { val node = this.node ?: throw ServiceError.NodeNotSetup() - return ServiceQueue.LDK.background { + return ServiceQueue.LDK.background(ldkQueue) { return@background runCatching { val result = node.onchainPayment().listSpendableOutputs() Result.success(result) @@ -1093,7 +1116,7 @@ class LightningService @Inject constructor( ): Result> { val node = this.node ?: throw ServiceError.NodeNotSetup() - return ServiceQueue.LDK.background { + return ServiceQueue.LDK.background(ldkQueue) { runCatching { val result = node.onchainPayment().selectUtxosWithAlgorithm( targetAmountSats = targetAmountSats, @@ -1115,7 +1138,7 @@ class LightningService @Inject constructor( Logger.info("RBF for txid='$txid' using satsPerVByte='$satsPerVByte'", context = TAG) - return ServiceQueue.LDK.background { + return ServiceQueue.LDK.background(ldkQueue) { return@background try { node.onchainPayment().bumpFeeByRbf( txid = txid, @@ -1136,7 +1159,7 @@ class LightningService @Inject constructor( Logger.info("CPFP for txid='$txid' using satsPerVByte='$satsPerVByte', to address='$toAddress'", context = TAG) - return ServiceQueue.LDK.background { + return ServiceQueue.LDK.background(ldkQueue) { return@background try { node.onchainPayment().accelerateByCpfp( txid = txid, @@ -1156,7 +1179,7 @@ class LightningService @Inject constructor( Logger.debug("Calculating CPFP fee for parentTxid $parentTxid", context = TAG) - return ServiceQueue.LDK.background { + return ServiceQueue.LDK.background(ldkQueue) { return@background try { node.onchainPayment().calculateCpfpFeeRate( parentTxid = parentTxid, @@ -1176,7 +1199,7 @@ class LightningService @Inject constructor( ): ULong { val node = this.node ?: throw ServiceError.NodeNotSetup() - return ServiceQueue.LDK.background { + return ServiceQueue.LDK.background(ldkQueue) { return@background runCatching { node.onchainPayment().calculateTotalFee( address = address, @@ -1209,7 +1232,7 @@ class LightningService @Inject constructor( ): ULong { val node = this.node ?: throw ServiceError.NodeNotSetup() - return ServiceQueue.LDK.background { + return ServiceQueue.LDK.background(ldkQueue) { node.onchainPayment().calculateSendAllFee( address = address, retainReserves = true, @@ -1268,7 +1291,7 @@ class LightningService @Inject constructor( suspend fun getAddressBalance(address: String): ULong { val node = this.node ?: throw ServiceError.NodeNotSetup() - return ServiceQueue.LDK.background { + return ServiceQueue.LDK.background(ldkQueue) { runCatching { node.getAddressBalance(addressStr = address) }.onFailure { @@ -1278,31 +1301,31 @@ class LightningService @Inject constructor( } suspend fun getBalanceForAddressType(addressType: AddressType): AddressTypeBalance = - ServiceQueue.LDK.background { + ServiceQueue.LDK.background(ldkQueue) { val n = node ?: throw ServiceError.NodeNotSetup() n.getBalanceForAddressType(addressType.toLdkAddressType()) } - suspend fun setPrimaryAddressType(addressType: AddressType) = ServiceQueue.LDK.background { + suspend fun setPrimaryAddressType(addressType: AddressType) = ServiceQueue.LDK.background(ldkQueue) { val n = node ?: throw ServiceError.NodeNotSetup() val mnemonic = keychain.loadString(Keychain.Key.BIP39_MNEMONIC.name) ?: throw ServiceError.MnemonicNotFound() val passphrase = keychain.loadString(Keychain.Key.BIP39_PASSPHRASE.name) n.setPrimaryAddressTypeWithMnemonic(addressType.toLdkAddressType(), mnemonic, passphrase) } - suspend fun addAddressTypeToMonitor(addressType: AddressType) = ServiceQueue.LDK.background { + suspend fun addAddressTypeToMonitor(addressType: AddressType) = ServiceQueue.LDK.background(ldkQueue) { val n = node ?: throw ServiceError.NodeNotSetup() val mnemonic = keychain.loadString(Keychain.Key.BIP39_MNEMONIC.name) ?: throw ServiceError.MnemonicNotFound() val passphrase = keychain.loadString(Keychain.Key.BIP39_PASSPHRASE.name) n.addAddressTypeToMonitorWithMnemonic(addressType.toLdkAddressType(), mnemonic, passphrase) } - suspend fun removeAddressTypeFromMonitor(addressType: AddressType) = ServiceQueue.LDK.background { + suspend fun removeAddressTypeFromMonitor(addressType: AddressType) = ServiceQueue.LDK.background(ldkQueue) { val n = node ?: throw ServiceError.NodeNotSetup() n.removeAddressTypeFromMonitor(addressType.toLdkAddressType()) } - suspend fun listMonitoredAddressTypes(): List = ServiceQueue.LDK.background { + suspend fun listMonitoredAddressTypes(): List = ServiceQueue.LDK.background(ldkQueue) { val n = node ?: throw ServiceError.NodeNotSetup() n.listMonitoredAddressTypes().map { it.toBitkitAddressType() } } @@ -1332,7 +1355,7 @@ class LightningService @Inject constructor( suspend fun listPayments(): List? { val node = this.node ?: return null - return ServiceQueue.LDK.background { + return ServiceQueue.LDK.background(ldkQueue) { node.listPayments() } } diff --git a/app/src/test/java/to/bitkit/services/LightningServiceTest.kt b/app/src/test/java/to/bitkit/services/LightningServiceTest.kt index d4285c2efd..13064c142a 100644 --- a/app/src/test/java/to/bitkit/services/LightningServiceTest.kt +++ b/app/src/test/java/to/bitkit/services/LightningServiceTest.kt @@ -82,6 +82,7 @@ class LightningServiceTest : BaseUnitTest() { watchOnlyAccountStore = watchOnlyAccountStore, loggerLdk = loggerLdk, watchOnlyAccountLifecycleCoordinator = watchOnlyAccountLifecycleCoordinator, + ldkQueue = testDispatcher, ) sut.node = node } @@ -220,7 +221,6 @@ class LightningServiceTest : BaseUnitTest() { releaseEvent.complete(event) testScheduler.advanceUntilIdle() - // timeout() polls in real time: teardown finishes on the LDK/IO threads, not virtual time. // Without the self-join guard, stop() would deadlock and node.stop() would never run. verify(node, timeout(VERIFY_TIMEOUT_MS)).stop() verify(node, timeout(VERIFY_TIMEOUT_MS)).destroy() From c5cb0b6d411efb4a655185423f27e5c8aaf91b73 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 18 Sep 2026 12:09:27 -0300 Subject: [PATCH 3/3] test: explain why teardown verifies keep a timeout Co-Authored-By: Claude Opus 5 (1M context) --- app/src/test/java/to/bitkit/services/LightningServiceTest.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/test/java/to/bitkit/services/LightningServiceTest.kt b/app/src/test/java/to/bitkit/services/LightningServiceTest.kt index 13064c142a..14ecdf1b26 100644 --- a/app/src/test/java/to/bitkit/services/LightningServiceTest.kt +++ b/app/src/test/java/to/bitkit/services/LightningServiceTest.kt @@ -221,6 +221,8 @@ class LightningServiceTest : BaseUnitTest() { releaseEvent.complete(event) testScheduler.advanceUntilIdle() + // timeout() is kept as a guard: the LDK queue is injected here, so teardown completes on + // virtual time, but the arguments cost nothing if a future path completes off it. // Without the self-join guard, stop() would deadlock and node.stop() would never run. verify(node, timeout(VERIFY_TIMEOUT_MS)).stop() verify(node, timeout(VERIFY_TIMEOUT_MS)).destroy()