-
Notifications
You must be signed in to change notification settings - Fork 54
test(swift-sdk): first swift sdk integration tests with local network #3712
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: v3.1-dev
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,84 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import XCTest | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import SwiftData | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @testable import SwiftDashSDK | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| final class CoreSendIntegrationTests: IntegrationTestCase { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private let fundingDash: Double = 0.5 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private var fundingDuffs: UInt64 { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| UInt64(fundingDash * 1e8) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func testWalletToWalletViaSpv() async throws { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try await env.ensureSPVStarted() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let alice = try await env.makeTestWallet(name: "core-send-alice") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let bob = try await env.makeTestWallet(name: "core-send-bob") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let aliceAddress = try alice.getCoreWallet().nextReceiveAddress() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _ = try await env.fund(address: aliceAddress, dash: fundingDash) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let bobAddress = try bob.getCoreWallet().nextReceiveAddress() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _ = try await env.fund(address: bobAddress, dash: fundingDash) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try await alice.waitForSpendable(exactly: fundingDuffs, timeout: 90) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try await bob.waitForSpendable(exactly: fundingDuffs, timeout: 90) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let iterations = 5 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let amount: UInt64 = 100_000 // 0.001 DASH per hop | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for i in 0 ..< iterations { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let aliceSends = (i % 2 == 0) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let sender = aliceSends ? alice: bob | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let receiver = aliceSends ? bob: alice | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let receiverBalanceBefore = try receiver.getPlatformWallet().balance().spendable | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let recipientAddress = try receiver.getCoreWallet().nextReceiveAddress() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let beforeTxids = try await readTxids() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _ = try sender.getCoreWallet().sendToAddresses( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| recipients: [(address: recipientAddress, amountDuffs: amount)] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| guard let sendTxid = try await waitForNewTxid(notIn: beforeTxids) else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| XCTFail("send PersistentTransaction row never appeared on iteration \(i)") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _ = try await env.mine(1, including: sendTxid) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try await Wait.until( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "receiver +\(amount) after iteration \(i)", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| timeout: 60, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pollInterval: 0.01 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try receiver.getPlatformWallet().balance().spendable | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| == receiverBalanceBefore + amount | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let aliceFinal = try alice.getPlatformWallet().balance().spendable | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let bobFinal = try bob.getPlatformWallet().balance().spendable | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| XCTAssertLessThanOrEqual(aliceFinal + bobFinal, 2 * fundingDuffs) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Validate via the SwiftData | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let expectedTotalTxs = 2 + iterations | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let aliceWalletId = alice.getPlatformWallet().walletId | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let bobWalletId = bob.getPlatformWallet().walletId | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let container = env.modelContainer | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try await MainActor.run { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let context = ModelContext(container) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let allTxCount = try context.fetchCount(FetchDescriptor<PersistentTransaction>()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| XCTAssertEqual(allTxCount, expectedTotalTxs) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+64
to
+67
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Suggestion: Carried-forward prior finding: global PersistentTransaction count is coupled to the process-shared SwiftData store
source: ['claude', 'codex'] |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let aliceTxoCount = try context.fetchCount(FetchDescriptor<PersistentTxo>( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| predicate: #Predicate<PersistentTxo>{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $0.walletId == aliceWalletId | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let bobTxoCount = try context.fetchCount(FetchDescriptor<PersistentTxo>( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| predicate: #Predicate<PersistentTxo>{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $0.walletId == bobWalletId | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| XCTAssertEqual(aliceTxoCount, 1 + iterations) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| XCTAssertEqual(bobTxoCount, 1 + iterations) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+64
to
+82
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Suggestion: Global PersistentTransaction count is coupled to a process-shared SwiftData store
source: ['claude', 'codex']
Comment on lines
+64
to
+82
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Suggestion: Carried-forward prior finding: global PersistentTransaction count is coupled to the process-shared SwiftData store
Suggested change
source: ['claude', 'codex'] |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,189 @@ | ||
| import XCTest | ||
| import SwiftData | ||
| @testable import SwiftDashSDK | ||
|
|
||
| /// Regression guard for the "self-send flips to phantom-incoming after | ||
| /// a mid-flight restart" bug. The mempool sighting taints | ||
| /// `PersistentTxo.isSpent` for the input; the persister's load | ||
| /// callback then filters that row out of the restored UTXO set, so | ||
| /// the catch-up classifier on the next launch sees a missing input, | ||
| /// emits `direction=Incoming`, and rewrites `netAmount` to the sum of | ||
| /// the wallet's own outputs in the tx. | ||
| final class PersisterRestartClassificationIntegrationTests: IntegrationTestCase { | ||
| private let fundingDash: Double = 0.5 | ||
| private var fundingDuffs: UInt64 { UInt64(fundingDash * 1e8) } | ||
| private let sendAmount: UInt64 = 10_000 | ||
|
|
||
| func testSelfSendClassificationSurvivesMidFlightRestart() async throws { | ||
| try await env.ensureSPVStarted() | ||
| let alice = try await env.makeTestWallet(name: "restart-class-alice") | ||
|
|
||
| let aliceFundingAddr = try alice.getCoreWallet().nextReceiveAddress() | ||
| _ = try await env.fund(address: aliceFundingAddr, dash: fundingDash) | ||
| try await alice.waitForSpendable(exactly: fundingDuffs, timeout: 90) | ||
|
|
||
| let beforeTxids = try await readTxids() | ||
|
|
||
| let aliceSecondAddr = try alice.getCoreWallet().nextReceiveAddress() | ||
| _ = try alice.getCoreWallet().sendToAddresses( | ||
| recipients: [(address: aliceSecondAddr, amountDuffs: sendAmount)] | ||
| ) | ||
|
|
||
| guard let sendTxid = try await waitForNewTxid(notIn: beforeTxids) else { | ||
| XCTFail("self-send PersistentTransaction row never appeared within 60s") | ||
| return | ||
| } | ||
|
|
||
| // Control: mempool sighting must already be Internal/-fee | ||
| // (Rust still holds the input in memory at this point). | ||
| try await assertSelfSendRow( | ||
| txid: sendTxid, | ||
| phase: "mempool sighting" | ||
| ) | ||
|
|
||
| try await env.restartWalletManager() | ||
| _ = try await env.mine(1, including: sendTxid) | ||
| try await env.ensureSPVStarted() | ||
| try await env.waitForSPVUpToDate() | ||
|
|
||
| try await assertTxIsMined( | ||
| txid: sendTxid, | ||
| phase: "post-restart catch-up" | ||
| ) | ||
|
|
||
| // Regression: classification must survive the restart. Pre-fix | ||
| // the row flips to Incoming / +sum_of_outputs. | ||
| try await assertSelfSendRow( | ||
| txid: sendTxid, | ||
| phase: "post-restart catch-up" | ||
| ) | ||
| } | ||
|
|
||
| /// Same regression as the test above, but the self-send tx never | ||
| /// gets a confirming block — it stays in the mempool across the | ||
| /// restart. Exercises the catch-up classifier on the mempool-only | ||
| /// path: after the SPV reconnects, the masternodes replay the | ||
| /// wallet's own tx via INV/mempool and the classifier reprocesses | ||
| /// it without any new block to anchor it. | ||
| func testSelfSendClassificationSurvivesMempoolOnlyRestart() async throws { | ||
| try await env.ensureSPVStarted() | ||
| let alice = try await env.makeTestWallet(name: "restart-class-alice-no-mine") | ||
|
|
||
| let aliceFundingAddr = try alice.getCoreWallet().nextReceiveAddress() | ||
| _ = try await env.fund(address: aliceFundingAddr, dash: fundingDash) | ||
| try await alice.waitForSpendable(exactly: fundingDuffs, timeout: 90) | ||
|
|
||
| let beforeTxids = try await readTxids() | ||
|
|
||
| let aliceSecondAddr = try alice.getCoreWallet().nextReceiveAddress() | ||
| _ = try alice.getCoreWallet().sendToAddresses( | ||
| recipients: [(address: aliceSecondAddr, amountDuffs: sendAmount)] | ||
| ) | ||
|
|
||
| guard let sendTxid = try await waitForNewTxid(notIn: beforeTxids) else { | ||
| XCTFail("self-send PersistentTransaction row never appeared within 60s") | ||
| return | ||
| } | ||
|
|
||
| try await assertSelfSendRow( | ||
| txid: sendTxid, | ||
| phase: "mempool sighting" | ||
| ) | ||
|
|
||
| try await env.restartWalletManager() | ||
| try await env.ensureSPVStarted() | ||
|
|
||
| try await assertTxIsInMempool( | ||
| txid: sendTxid, | ||
| phase: "post-restart mempool-only catch-up" | ||
| ) | ||
|
|
||
| try await assertSelfSendRow( | ||
| txid: sendTxid, | ||
| phase: "post-restart mempool-only catch-up" | ||
| ) | ||
| } | ||
|
|
||
| // MARK: - Helpers | ||
|
|
||
| /// Sendable snapshot of the columns the test cares about. | ||
| /// `PersistentTransaction` itself can't cross the `MainActor.run` | ||
| /// boundary because `@Model` types are not Sendable. | ||
| private struct TxSnapshot: Sendable { | ||
| let direction: UInt32 | ||
| let netAmount: Int64 | ||
| let context: UInt32 | ||
| } | ||
|
|
||
| private func fetchTransaction(_ txid: Data) async throws -> TxSnapshot? { | ||
| let container = env.modelContainer | ||
| return try await MainActor.run { | ||
| let ctx = ModelContext(container) | ||
| guard let row = try ctx.fetch(FetchDescriptor<PersistentTransaction>( | ||
| predicate: #Predicate { $0.txid == txid } | ||
| )).first else { return nil } | ||
| return TxSnapshot( | ||
| direction: row.direction, | ||
| netAmount: row.netAmount, | ||
| context: row.context | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| /// Asserts the row is in a mined state: inBlock (2) | ||
| private func assertTxIsMined(txid: Data, phase: String) async throws { | ||
| guard let row = try await fetchTransaction(txid) else { | ||
| XCTFail("\(phase): tx row missing for \(txid.toHexString()) (expected mined)") | ||
| return | ||
| } | ||
|
|
||
| let inBlock = TransactionContextType.inBlock.rawValue | ||
| XCTAssertTrue( | ||
| row.context == inBlock, | ||
| "\(phase): context=\(row.context) — expected inBlock(\(inBlock))" | ||
| ) | ||
| } | ||
|
|
||
| /// Asserts the row is in a mempool-equivalent state: mempool (0) | ||
| /// or instantSend (1) — i.e., observed but not yet in a block. | ||
| /// Used after the variant that restarts without mining. | ||
| private func assertTxIsInMempool(txid: Data, phase: String) async throws { | ||
| guard let row = try await fetchTransaction(txid) else { | ||
| XCTFail("\(phase): tx row missing for \(txid.toHexString()) (expected mempool)") | ||
| return | ||
| } | ||
|
|
||
| let mempool = TransactionContextType.mempool.rawValue | ||
| let instantSend = TransactionContextType.instantSend.rawValue | ||
|
|
||
| XCTAssertTrue( | ||
| row.context == mempool || row.context == instantSend, | ||
| "\(phase): context=\(row.context) — expected mempool(\(mempool)) or instantSend(\(instantSend))" | ||
| ) | ||
| } | ||
|
|
||
| private func assertSelfSendRow(txid: Data, phase: String) async throws { | ||
| guard let row = try await fetchTransaction(txid) else { | ||
| XCTFail("\(phase): row missing for txid \(txid.toHexString())") | ||
| return | ||
| } | ||
|
|
||
| XCTAssertEqual( | ||
| row.direction, 2, | ||
| "\(phase): direction=\(row.direction) (expected 2=Internal). " + | ||
| "context=\(row.context), netAmount=\(row.netAmount). " + | ||
| "Positive netAmount with direction=0 is the phantom-incoming signature." | ||
| ) | ||
|
|
||
| XCTAssertLessThan( | ||
| row.netAmount, 0, | ||
| "\(phase): netAmount=\(row.netAmount) (expected <0; self-send only leaves the fee)." | ||
| ) | ||
|
|
||
| XCTAssertLessThan( | ||
| abs(row.netAmount), 100_000, | ||
| "\(phase): fee too large: |netAmount|=\(abs(row.netAmount))" | ||
| ) | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 Suggestion: Carried-forward prior finding: global PersistentTransaction count is coupled to the process-shared SwiftData store
IntegrationTestCase.sharedEnv()memoises oneIntegrationTestEnvper process, andIntegrationTestEnv.bootstrap()creates a single in-memoryModelContainerthat every test reuses.tearDown()only purges Keychain mnemonics — SwiftData rows accumulate across tests. The assertionXCTAssertEqual(allTxCount, expectedTotalTxs)counts everyPersistentTransactionin the shared store, so as soon as any second test persists transactions (or this test is rerun in the same process), it flakes. The per-wallet TXO counts below (lines 63–75) already filter bywalletIdand are the correct pattern — scope the total similarly (e.g.walletId == aliceWalletId || walletId == bobWalletId) or otherwise restrict to wallets created by this test.source: ['claude', 'codex']