Skip to content

test(swift-sdk): add testnet identity-discovery UI test#3560

Open
llbartekll wants to merge 9 commits intov3.1-devfrom
claude/swift-xcuitest-wallet-persistence
Open

test(swift-sdk): add testnet identity-discovery UI test#3560
llbartekll wants to merge 9 commits intov3.1-devfrom
claude/swift-xcuitest-wallet-persistence

Conversation

@llbartekll
Copy link
Copy Markdown
Contributor

@llbartekll llbartekll commented Apr 28, 2026

Issue being fixed or feature implemented

Extends the SwiftExampleApp UI test suite with an end-to-end test that exercises the SDK paths the existing local-only wallet tests don't touch: importing a wallet from a known mnemonic, DIP-9 identity discovery, and reading the discovered identity's balance from Platform.

This is one regression check across roughly half the SDK — PlatformWallet.fromMnemonic, platform_wallet_discover_identities, DIP-9 TRANSFER key derivation, DAPI lookup, balance refresh.

What was done?

  • New CreditTransferTest.testImportWalletAndDiscoverIdentity — imports a testnet wallet from UI_TEST_TESTNET_MNEMONIC, runs identity discovery, asserts the expected identity ID surfaces with a non-zero balance. Skipped when the env var is unset, so the rest of the suite continues to run locally without test-network credentials.
  • Accessibility identifiers added across the views the test traverses (wallet create/import, identities tab, search-wallets sheet, identity detail, options/network picker, transitions form). accessibilityValue on the balance label so the raw credit count is parseable from XCUITest (the displayed %.8f DASH rounding hides amounts below 1000 credits).
  • Helpers extended in WalletFlow.swift for testnet flows: import-with-mnemonic, network switch, identity discovery, balance read, plus credit-transfer form navigation/execution scaffolding kept in place for the follow-up that re-adds the transfer assertion.

The class and file are named CreditTransferTest so the deferred transfer assertion can be re-added as a sibling method — helpers are already wired up.

How Has This Been Tested?

  • Manual run from Xcode against testnet, with UI_TEST_TESTNET_MNEMONIC exported through Xcode's scheme env vars (note: xcodebuild test ENV=... does not propagate env vars to the XCUITest runner — use the TEST_RUNNER_<NAME> prefix from the command line, see the file's leading comment).
  • Test passes against a wallet whose mnemonic discovers identity 3ou98WEERy6ExmmHWYWsFtyhgW8rmr1giceZYTFqdAAA with a non-zero balance.
  • Verified the existing 3 wallet UI tests (testCreateGeneratedWalletFlow, testWalletPersistsAcrossRelaunch, testWalletDeletionCleanupSurvivesRelaunch) still pass.
  • Verified the test skips cleanly when the env var is unset.

Breaking Changes

None — accessibility identifiers and accessibilityValue are additive; no app-side logic changed.

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added or updated relevant unit/integration/functional/e2e tests
  • I have added "!" to the title and described breaking changes in the corresponding section if my code contains any
  • I have made corresponding changes to the documentation if needed

For repository code-owners and collaborators only

  • I have assigned this pull request to a milestone

Summary by CodeRabbit

  • Tests

    • Added UI tests for wallet persistence, wallet-deletion cleanup, and credit-transfer flows; introduced shared test helpers and deterministic waits; CI smoke runs now include these scenarios and run on a daily schedule with concurrency cancellation.
  • Accessibility

    • Added stable accessibility identifiers across the app for improved assistive tech support and reliable UI testing.
  • UI

    • Network status shows a consistent Switching/Connected/Disconnected presentation with stable identifiers; balance display exposes stable metadata for testing without changing its visible format.
  • Documentation

    • Added README describing the UI test suite and local/CI run instructions.

llbartekll and others added 2 commits April 28, 2026 09:00
Extract Identifier enum, XCUIElement helpers, and the create/delete
wallet flow into Support/ for cross-class reuse. Rewrite the existing
testCreateGeneratedWalletFlow on top of the extracted helpers; behavior
is unchanged.

Add WalletPersistenceTests with two SDK-backed integration tests:
- testWalletPersistsAcrossRelaunch validates loadFromPersistor() after
  a cold restart (SwiftData rehydration + Keychain read).
- testWalletDeletionCleanupSurvivesRelaunch guards atomic SwiftData +
  Keychain mnemonic cleanup; the orphan-mnemonic recovery prompt fires
  on relaunch if either side leaks.

Both new tests use addTeardownBlock + MainActor.assumeIsolated for
best-effort wallet cleanup if assertions halt mid-flow.

Extend swift-example-app-ui-smoke.yml -only-testing list to include
the two new tests; -parallel-testing-enabled NO and
-maximum-concurrent-test-simulator-destinations 1 retained.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Imports a wallet from `UI_TEST_TESTNET_MNEMONIC`, runs DIP-9 identity
discovery, and asserts the registered identity surfaces with a non-zero
balance. Skipped when the env var is unset. Test is scaffolded under
CreditTransferTest so the credit-transfer assertion can be re-added as
a sibling method (helpers for that flow are already in WalletFlow).

Adds accessibility identifiers across the views the test traverses
(wallet creation, identities tab, search-wallets sheet, identity detail,
options/network picker, transition form) and one accessibilityValue on
the balance label so the raw credit count is parseable from XCUITest.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added this to the v3.1.0 milestone Apr 28, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 28, 2026

Warning

Rate limit exceeded

@QuantumExplorer has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 3 minutes and 5 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 53fce408-5207-48b3-8d89-843040ee82b1

📥 Commits

Reviewing files that changed from the base of the PR and between 4e4cf1a and 3823ee4.

📒 Files selected for processing (7)
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/AppState.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/ContentView.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CreateWalletView.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/IdentitiesContentView.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/IdentityDetailView.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/OptionsView.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/SearchWalletsForIdentitiesView.swift
📝 Walkthrough

Walkthrough

Adds deterministic accessibility identifiers across the SwiftExampleApp UI, centralizes test identifier constants and XCUI helper utilities, implements extensive UI-test flow helpers and new UI test suites (wallet persistence and credit transfer), updates network-switch state handling in AppState, and extends the CI workflow to run the UI smoke tests.

Changes

Cohort / File(s) Summary
App Views — Accessibility & Network UI
packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/ContentView.swift, .../Core/Views/CreateWalletView.swift, .../Core/Views/IdentitiesContentView.swift, .../Views/IdentitiesView.swift, .../Views/IdentityDetailView.swift, .../Views/OptionsView.swift, .../Views/SearchWalletsForIdentitiesView.swift, .../Views/TransitionDetailView.swift, .../Views/TransitionInputView.swift
Added stable accessibilityIdentifiers to tabs, controls, inputs, menu items, rows, and balance metadata; refactored OptionsView to surface network-switch readiness via AppState.isSwitchingNetwork and expose identifiers for picker/status.
App State
packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/AppState.swift
Introduced @Published var isSwitchingNetwork and a monotonic request ID to track async network switches and prevent premature readiness resets.
UI Test Identifiers
packages/swift-sdk/SwiftExampleApp/SwiftExampleAppUITests/Support/Identifiers.swift
New centralized Identifier enum with constants and helpers (tabs, wallet controls, options, identities, search, identity detail) for deterministic test lookups.
XCUI Helper Utilities
packages/swift-sdk/SwiftExampleApp/SwiftExampleAppUITests/Support/XCUIElement+Helpers.swift
Added element lookup helpers (returning firstMatch), waiter wrappers (enabled, non-existence, switch-on), scroll-to-hittable, isSwitchOn convenience, and a recovery-prompt guard.
Shared Wallet Flow Helpers
packages/swift-sdk/SwiftExampleApp/SwiftExampleAppUITests/Support/WalletFlow.swift
Large collection of @MainActor helper routines for tab navigation (with fallbacks), create/import/delete wallet flows (including robust toggle handling and backup flow), network switching to Testnet (with status wait), identity discovery flow, balance parsing via accessibilityValue, and cleanup utilities.
New UI Test Suites
packages/swift-sdk/SwiftExampleApp/SwiftExampleAppUITests/CreditTransferTest.swift, .../WalletPersistenceTests.swift
Added CreditTransferTest (mnemonic-gated import, identity discovery, read balance) and WalletPersistenceTests (persistency and deletion cleanup across relaunch) with setup/teardown and recovery-prompt checks.
UI Tests Consolidation
packages/swift-sdk/SwiftExampleApp/SwiftExampleAppUITests/SwiftExampleAppUITests.swift
Removed in-file identifier constants and ad-hoc helpers in favor of centralized helpers and common utilities; replaced direct waits with shared assertion helpers.
CI Workflow
.github/workflows/swift-example-app-ui-smoke.yml
Workflow now runs daily and on dispatch, adds concurrency cancellation, forwards a secret-backed TEST_RUNNER_UI_TEST_MNEMONIC env var into the XCUITest process, and expands -only-testing to include wallet persistence and credit-transfer UI tests.
Docs
packages/swift-sdk/SwiftExampleApp/SwiftExampleAppUITests/README.md
Added README documenting the UI test suite, local/CI run instructions, simulator hygiene, and mnemonic/secret guidance for CI forks.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 I hopped through code with nimble paws,

Labels placed without a pause,
Wallets, tests, and network flows align,
Identifiers steady — outputs fine,
A carrot-cheered commit, neat and bright! 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'test(swift-sdk): add testnet identity-discovery UI test' accurately describes the primary change: adding a new UI test for testnet identity discovery workflows.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/swift-xcuitest-wallet-persistence

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 3 minutes and 5 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

@thepastaclaw
Copy link
Copy Markdown
Collaborator

thepastaclaw commented Apr 28, 2026

✅ Review complete (commit 3823ee4)

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/OptionsView.swift`:
- Around line 88-110: The status label incorrectly shows "Connected" whenever
appState.sdk != nil even while a network switch is in progress; change the logic
so isSwitchingNetwork remains true until the async switch completes (or derive
connected state from a network-specific readiness signal on the SDK) so that the
UI only shows "Connected" when the new network is actually ready; update the
state transitions in switchAppNetworkToTestnet (and any reset helpers) to
set/clear isSwitchingNetwork around the full async completion and use that
combined with appState.sdk (or a new appState.sdk.isReady flag) to drive the
Label bound to accessibilityIdentifier "options.networkStatusLabel".

In
`@packages/swift-sdk/SwiftExampleApp/SwiftExampleAppUITests/CreditTransferTest.swift`:
- Around line 96-101: The test is asserting a live testnet funding floor
(XCTAssertGreaterThan(balance, 0)); instead assert only that a readable balance
was returned: call readIdentityBalanceCredits(in:) and replace the
XCTAssertGreaterThan check with an assertion that the read succeeded (if the
function returns an Optional use XCTAssertNotNil(balance, "..."), or if it
returns an Int use XCTAssertGreaterThanOrEqual(balance, 0, "...") to allow
zero). Update the failure message to say the balance should be readable rather
than non-zero and keep the test focused on discovery/readability rather than
funding.

In
`@packages/swift-sdk/SwiftExampleApp/SwiftExampleAppUITests/Support/WalletFlow.swift`:
- Around line 933-969: The negative branch in assertWalletRowVisible(true/false)
can false-pass because it never scrolls and queries app-wide button/text rows
that may be off-screen; change the exists == false path to reuse
scrollToWalletRow(named:in:) to bring the specific wallet row into the viewport
(or obtain the per-wallet XCUIElement) and then assert non-existence against
that element (e.g. wait for row.exists == false or use an
XCTNSPredicateExpectation on the returned row) instead of the current app-level
buttonRow/textRow checks so off-screen elements can't make the assertion pass
incorrectly.
- Around line 492-518: The test currently assumes the picker auto-selects the
imported wallet (walletPicker / Identifier.SearchWallets.walletPicker) which is
flaky; instead explicitly drive selection to walletName: after
walletPicker.waitForExistence, open the picker UI and locate the row/button/cell
whose label equals walletName (or the table cell in
SearchWalletsForIdentitiesView) and tap it to select that wallet, then assert
the selection; alternatively, if simpler, add a deterministic cleanup step
(delete competing wallets) before importing so only walletName remains—reference
walletPicker, walletName, and SearchWalletsForIdentitiesView when updating the
helper.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 655844a1-5da6-46a1-acaf-193e6c0801cd

📥 Commits

Reviewing files that changed from the base of the PR and between 9bd37f2 and b3c85a2.

📒 Files selected for processing (16)
  • .github/workflows/swift-example-app-ui-smoke.yml
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/ContentView.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/CreateWalletView.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/IdentitiesContentView.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/IdentitiesView.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/IdentityDetailView.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/OptionsView.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/SearchWalletsForIdentitiesView.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/TransitionDetailView.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/TransitionInputView.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleAppUITests/CreditTransferTest.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleAppUITests/Support/Identifiers.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleAppUITests/Support/WalletFlow.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleAppUITests/Support/XCUIElement+Helpers.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleAppUITests/SwiftExampleAppUITests.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleAppUITests/WalletPersistenceTests.swift

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 28, 2026

✅ DashSDKFFI.xcframework built for this PR.

SwiftPM (host the zip at a stable URL, then use):

.binaryTarget(
  name: "DashSDKFFI",
  url: "https://your.cdn.example/DashSDKFFI.xcframework.zip",
  checksum: "ff27351f1cc521742a7be124e9ebc864b4e8d297884c979eeef0a9680d7d69dc"
)

Xcode manual integration:

  • Download 'DashSDKFFI.xcframework' artifact from the run link above.
  • Drag it into your app target (Frameworks, Libraries & Embedded Content) and set Embed & Sign.
  • If using the Swift wrapper package, point its binaryTarget to the xcframework location or add the package and place the xcframework at the expected path.

Copy link
Copy Markdown
Collaborator

@thepastaclaw thepastaclaw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Test-only PR that adds accessibility identifiers and a new XCUITest (CreditTransferTest.testImportWalletAndDiscoverIdentity) plus shared helpers extracted from the existing test class. No app-side logic or consensus-critical code is touched. The main concerns are about test reliability: a verifiable race in switchAppNetworkToTestnet (the Connected label can be true via the stale SDK throughout the switch), brittle assumptions about picker default state, and ~200 lines of unused credit-transfer scaffolding likely to rot before the follow-up lands.

Reviewed commit: b3c85a2

🔴 1 blocking | 🟡 4 suggestion(s) | 💬 2 nitpick(s)

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/swift-sdk/SwiftExampleApp/SwiftExampleAppUITests/Support/WalletFlow.swift`:
- [BLOCKING] lines 375-439: switchAppNetworkToTestnet can return while the app is still on the previous network
  `switchAppNetworkToTestnet` waits for `options.networkStatusLabel` to contain `Connected` as proof the switch finished, but the app provides no such guarantee. In `OptionsView` (lines 27-50), tapping the segmented control sets `appState.currentNetwork` and clears `isSwitchingNetwork` once the local resets finish; the actual SDK rebuild runs separately on the `currentNetwork.didSet` task in `AppState` (lines 12-17, 105-126). The status label renders `Connected` whenever `appState.sdk != nil`, and `switchNetwork` never nils the old SDK before constructing the new one (`sdk = newSDK` only after `try SDK(network:)` succeeds). On a simulator left on a non-testnet network from a previous run, the helper can therefore observe `Connected` against the stale SDK and return before testnet DAPI is bound — the discovery step then runs against the wrong backend and the test result is nondeterministic. Gate on a stronger signal: e.g. an SDK-level state transition published from `AppState` or a test-only flag flipped after `switchNetwork` completes, rather than the always-green status label.
- [SUGGESTION] lines 492-519: runIdentityDiscovery relies on "only one wallet exists" without enforcing it
  `runIdentityDiscovery` trusts the picker's default-first auto-selection and only sanity-checks `walletPicker.label.contains(walletName)`. The invariant is never enforced: `SearchWalletsForIdentitiesView` queries `@Query(sort: \PersistentWallet.createdAt) hdWallets` unfiltered, and `CreditTransferTest` only deletes the row matching `expectedSenderWalletIdHex` (lines 69-76 of `CreditTransferTest.swift`). Any other persisted wallet from prior local runs still appears in the picker and, being older, becomes the default — the picker-label assertion will then fail loudly even though import and discovery are themselves working. The comment in the helper acknowledges this assumption but the test does not enforce it. Either reset `PersistentWallet` rows for unrelated wallets before discovery (e.g. a `cleanupWalletsByPrefix`-style sweep on entry, or a full SwiftData reset), or drive the picker to explicitly select `walletName` instead of trusting the default.
- [SUGGESTION] lines 624-833: Credit-transfer helpers (~200 lines) are added but unused
  `navigateToIdentityCreditTransferForm`, `executeCreditTransfer`, `waitForCreditTransferSuccess`, and `cleanupWalletsByPrefix` are defined but have no callers anywhere in the test suite — grep confirms only the definitions exist. Carrying ~200 lines of unexecuted UI test helpers is risky: the most fragile string matching in the file lives here (`label CONTAINS[c] "manage identities"`, the `"💳 Manually Enter Recipient"` literal, the `transition.input.toIdentityId` identifier, the `"State Transitions"` cell label), and none of it is exercised, so it will silently rot against UI changes until the follow-up tries to use it. Note also that `executeCreditTransfer` calls `app.swipeDown()` after typing the amount — `importWallet` explicitly warns that `swipeDown` can dismiss sheets; the Transfer Credits screen is push-navigated rather than presented so it may not bite, but the inconsistency is worth verifying before the helper is wired in. Either land these helpers together with the test that uses them, or trim to what the discovery test actually needs and re-add the rest in the follow-up PR.
- [SUGGESTION] lines 466-490: Identity-discovery retry loop can close an already-open menu
  The retry loop binds `for attempt in 1...3 where !sheetOpened` purely as a counter and then has to do `_ = attempt` to silence the unused-variable warning — that's a tell that the index shouldn't be bound at all (`for _ in 1...3 where !sheetOpened` is clearer). More substantively, every iteration unconditionally calls `addMenu.tap()` again. If the previous attempt actually opened the SwiftUI Menu but the search-item lookup transiently failed, the next `addMenu.tap()` will *close* the menu, putting the retry into a worse state than the first attempt. Probe the search item or the menu's selected state before re-tapping, e.g. only call `addMenu.tap()` when neither `searchMenuItem.exists` nor `addMenu.isSelected`.

In `packages/swift-sdk/SwiftExampleApp/SwiftExampleAppUITests/CreditTransferTest.swift`:
- [SUGGESTION] lines 53-59: Recovery-prompt guard runs before the network switch
  `failIfRecoveryPromptVisible(in: app, timeout: 2)` runs immediately after `app.launch()`, then `switchAppNetworkToTestnet` runs. A wallet left over from a previous testnet run, when the simulator boots on a different default network (regtest, etc.), only triggers the orphan-mnemonic recovery prompt *after* the network switch rebinds the wallet-scoped services. In that scenario the guard runs against a state where the prompt has not yet appeared and misses it. Re-run the guard after `switchAppNetworkToTestnet`, or move the existing call to right after the switch so the prompt is detected on whichever network the test actually executes against.

Comment thread .github/workflows/swift-example-app-ui-smoke.yml
@llbartekll llbartekll marked this pull request as draft April 29, 2026 06:05
Network-switch race (BLOCKING from both reviewers): lift
`isSwitchingNetwork` from OptionsView's local @State to AppState. The
flag now spans the full async cycle (currentNetwork.didSet → Task →
switchNetwork → sdk = newSDK), so the network status label and
test-side `Connected` predicate stop reading the previous network's
SDK as a successful switch.

Test reliability:
- runIdentityDiscovery drives the wallet picker explicitly instead of
  trusting default-first auto-selection, so unrelated wallets on the
  simulator no longer hijack discovery.
- assertWalletRowVisible(exists: false) scrolls to the top and sweeps
  down before declaring absent, closing a false-pass on long lists
  where SwiftUI's lazy List could leave a still-persisted row outside
  the accessibility tree.
- CreditTransferTest drops the `balance > 0` floor (couples to live
  testnet funding state); readIdentityBalanceCredits already XCFails
  on parse error, so reaching the next line is the readability signal.
- Re-run failIfRecoveryPromptVisible after switchAppNetworkToTestnet —
  a leftover testnet wallet on a simulator booted to a different
  default network only triggers the orphan-mnemonic prompt after the
  network switch rebinds wallet-scoped services.
- cleanupWalletsByPrefix("ImportTransfer-") on entry sweeps random-
  suffixed wallets from prior failed runs (the deterministic
  walletId-based check missed them).
- Discovery retry loop only re-taps the Add menu when its menu item
  isn't still visible from a prior attempt — re-tapping while the menu
  is open closes it.

Trim:
- Delete unused credit-transfer helpers (navigateToIdentityCreditTransferForm,
  executeCreditTransfer, waitForCreditTransferSuccess) and
  Identifier.Transition. They will return alongside the credit-transfer
  test in a follow-up; the corresponding app-side .accessibilityIdentifier
  calls on TransitionDetailView/TransitionInputView are kept (harmless).

CI:
- Wire CreditTransferTest into the smoke workflow with -only-testing.
  Self-skips when UI_TEST_TESTNET_MNEMONIC is unset, so the new line is
  a no-op for forks/PRs without the secret while still exercising the
  build path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@llbartekll llbartekll marked this pull request as ready for review April 29, 2026 07:10
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
packages/swift-sdk/SwiftExampleApp/SwiftExampleAppUITests/Support/WalletFlow.swift (1)

528-532: Wait for picker selection to propagate before asserting label text.

The assertion on Line 528 can race right after walletOption.tap(). A short predicate wait on walletPicker.label containing walletName will make this less flaky on slower simulators.

Proposed stabilization
-    XCTAssertTrue(
-        walletPicker.label.contains(walletName),
-        "Picker shows \"\(walletPicker.label)\" but the test imported \(walletName).",
-        file: file, line: line
-    )
+    let selectedWalletPredicate = NSPredicate { object, _ in
+        guard let element = object as? XCUIElement, element.exists else { return false }
+        return element.label.contains(walletName)
+    }
+    let selectedWalletResult = XCTWaiter.wait(
+        for: [XCTNSPredicateExpectation(predicate: selectedWalletPredicate, object: walletPicker)],
+        timeout: 5
+    )
+    XCTAssertEqual(
+        selectedWalletResult,
+        .completed,
+        "Picker shows \"\(walletPicker.label)\" but the test imported \(walletName).",
+        file: file, line: line
+    )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/swift-sdk/SwiftExampleApp/SwiftExampleAppUITests/Support/WalletFlow.swift`
around lines 528 - 532, The assertion on walletPicker.label can race immediately
after walletOption.tap(); add an explicit predicate wait that walletPicker.label
contains walletName (e.g., create an NSPredicate "label CONTAINS %@", use
expectation(for:evaluatedWith:handler:) or XCTNSPredicateExpectation, then
wait(for:timeout:)) before calling XCTAssertTrue so the picker selection has
time to propagate; update the flow around walletOption.tap(), walletPicker and
the XCTAssertTrue to perform the predicate wait first and then assert.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/AppState.swift`:
- Around line 24-28: The isSwitchingNetwork flag is being cleared
unconditionally by each spawned Task after awaiting switchNetwork(to:), which
allows an earlier task to flip it false while a later switch is still running;
fix by introducing a request token/version (e.g., an Int or UUID property like
networkSwitchRequestID) that you increment/assign before creating the Task,
capture that token inside the Task, and only set isSwitchingNetwork = false if
the captured token matches the current networkSwitchRequestID after await
switchNetwork(to:); apply this change to both places that spawn the Task so
completions are serialized/guarded and stale tasks cannot clear readiness.

In
`@packages/swift-sdk/SwiftExampleApp/SwiftExampleAppUITests/CreditTransferTest.swift`:
- Around line 5-8: Update the stale header comment at the top of
CreditTransferTest.swift (the file-level comment in the CreditTransferTest test)
to reflect the current assertions: change the phrase that claims the test
asserts a "non-zero balance" to state it only verifies that the identity's
balance is readable/accessible (and keep or clarify that the actual
credit-transfer assertion is deferred). Edit the header comment block above the
test implementation to match the test's scope (balance readability) and remove
or adjust any misleading wording about asserting a non-zero balance.

---

Nitpick comments:
In
`@packages/swift-sdk/SwiftExampleApp/SwiftExampleAppUITests/Support/WalletFlow.swift`:
- Around line 528-532: The assertion on walletPicker.label can race immediately
after walletOption.tap(); add an explicit predicate wait that walletPicker.label
contains walletName (e.g., create an NSPredicate "label CONTAINS %@", use
expectation(for:evaluatedWith:handler:) or XCTNSPredicateExpectation, then
wait(for:timeout:)) before calling XCTAssertTrue so the picker selection has
time to propagate; update the flow around walletOption.tap(), walletPicker and
the XCTAssertTrue to perform the predicate wait first and then assert.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fed7c149-4bb2-4a06-b438-e518503b44eb

📥 Commits

Reviewing files that changed from the base of the PR and between b3c85a2 and 7c0ae94.

📒 Files selected for processing (6)
  • .github/workflows/swift-example-app-ui-smoke.yml
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/AppState.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/OptionsView.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleAppUITests/CreditTransferTest.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleAppUITests/Support/Identifiers.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleAppUITests/Support/WalletFlow.swift
🚧 Files skipped from review as they are similar to previous changes (2)
  • .github/workflows/swift-example-app-ui-smoke.yml
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/OptionsView.swift

Comment thread packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/AppState.swift Outdated
@llbartekll llbartekll marked this pull request as draft April 29, 2026 07:17
llbartekll and others added 4 commits April 29, 2026 09:51
Three findings from coderabbit's re-review of 7c0ae94:

- AppState: overlapping network switches could cause an earlier task
  to clear `isSwitchingNetwork` while a later switch was still running.
  Add a monotonic `networkSwitchRequestID`; each spawned task captures
  its id at start and only clears the flag when its id still matches.
- CreditTransferTest header: aligned the file-level comment with the
  current scope (balance readability, not non-zero balance).
- WalletFlow `runIdentityDiscovery`: replaced the bare label-contains
  assertion after `walletOption.tap()` with an `XCTNSPredicateExpectation`
  wait so the picker has time to propagate the selection on slower
  simulators.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Renames the testnet mnemonic env var the XCUITest reads to
`UI_TEST_MNEMONIC` and forwards it from a GitHub secret of the same
name into the xcodebuild step as `TEST_RUNNER_UI_TEST_MNEMONIC` (the
prefix is required — xcodebuild strips it before handing the env to
the XCUITest runner process).

Empty on fork PRs (GitHub withholds secrets there), at which point the
test self-skips. Once a repository secret named `UI_TEST_MNEMONIC` is
set, `testImportWalletAndDiscoverIdentity` runs end-to-end against
testnet on every push to a non-fork branch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a 23:00 UTC daily cron to the smoke workflow, alongside the
existing manual `workflow_dispatch` trigger. Mirrors the repo's
nightly pattern (separate workflow with its own cron, like
`tests-rs-nightly-long-running.yml`).

Also adds a concurrency block so a manual dispatch and a cron run
don't fight over the single self-hosted macOS ARM64 runner — the
older run is cancelled in favor of the newer one.

The cron only takes effect once this lands on the default branch
(`v3.1-dev`); GitHub Actions schedules run against the default
branch's workflow file.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Captures the operational gotchas that took multiple sessions to
re-discover:

- `TEST_RUNNER_` prefix gate when passing `UI_TEST_MNEMONIC` via
  `xcodebuild test ENV=`.
- `simctl erase` is the only way to clear stale Keychain entries
  between runs (uninstall alone leaves orphan mnemonics that trip
  the recovery prompt).
- SwiftData migration failures from leftover stores manifest as a
  "Expected root tab bar" timeout, not an obvious crash.
- The CI nightly cron's reliance on the self-hosted Mac being online.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@llbartekll llbartekll marked this pull request as ready for review April 29, 2026 08:48
@llbartekll llbartekll requested a review from thepastaclaw April 29, 2026 08:58
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
packages/swift-sdk/SwiftExampleApp/SwiftExampleAppUITests/Support/WalletFlow.swift (1)

407-434: ⚠️ Potential issue | 🟠 Major

Wait for Testnet selection to latch before accepting “Connected”.

Right now the helper can still pass if the app remains connected on the previous network and the segmented-control tap doesn’t take effect. Add an explicit wait that testnetButton.isSelected == true before asserting connection status.

Suggested patch
@@
-    if testnetButton.isSelected {
-        // Already on Testnet; status should already be Connected.
-    } else {
+    if !testnetButton.isSelected {
         testnetButton.tap()
     }
+
+    let selectedResult = XCTWaiter.wait(
+        for: [
+            XCTNSPredicateExpectation(
+                predicate: NSPredicate(format: "isSelected == true"),
+                object: testnetButton
+            ),
+        ],
+        timeout: 10
+    )
+    XCTAssertEqual(
+        selectedResult,
+        .completed,
+        "Expected Testnet segment to be selected before waiting for connected status.",
+        file: file, line: line
+    )
@@
     let result = XCTWaiter.wait(
         for: [XCTNSPredicateExpectation(predicate: connectedPredicate, object: statusLabel)],
         timeout: timeout
     )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/swift-sdk/SwiftExampleApp/SwiftExampleAppUITests/Support/WalletFlow.swift`
around lines 407 - 434, The helper can report "Connected" for the previous
network if the segmented control tap hasn't latched; fix by waiting for the
Testnet segment selection to take effect before checking network status: after
tapping testnetButton (or the existing isSelected check) add an explicit wait
using an NSPredicate/XCTNSPredicateExpectation that verifies
testnetButton.isSelected == true (with a reasonable timeout) and assert the wait
completes successfully, then proceed to build statusLabel and wait for the
"Connected" predicate (retain connectedPredicate, statusLabel, and the existing
XCTWaiter usage).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@packages/swift-sdk/SwiftExampleApp/SwiftExampleAppUITests/Support/WalletFlow.swift`:
- Around line 518-520: The NSPredicate used to find walletOption is too
permissive and can match prefixes; update the predicate in the walletOption
construction (the app.buttons.matching(...) call) to require a trailing space
after the walletName token (e.g., use "label BEGINSWITH %@ " with walletName as
the argument) so it only matches the exact name token rather than any prefix;
keep the rest of the selector (firstMatch) unchanged.
- Around line 653-667: cleanupWalletsByPrefix is only checking visible buttons
and can miss off-screen wallets; change it to iterate over the full query (use
app.buttons.matching(predicate).allElementsBoundByIndex) or repeatedly scroll
the wallets list (element(Identifier.walletsScreen)) while locating elements
matching the predicate so every persisted wallet is discovered, and call
bestEffortDeleteWallet(named:in:) for each found name until none remain; ensure
the loop continues until the full query is empty rather than stopping when no
visible match is found.

---

Duplicate comments:
In
`@packages/swift-sdk/SwiftExampleApp/SwiftExampleAppUITests/Support/WalletFlow.swift`:
- Around line 407-434: The helper can report "Connected" for the previous
network if the segmented control tap hasn't latched; fix by waiting for the
Testnet segment selection to take effect before checking network status: after
tapping testnetButton (or the existing isSelected check) add an explicit wait
using an NSPredicate/XCTNSPredicateExpectation that verifies
testnetButton.isSelected == true (with a reasonable timeout) and assert the wait
completes successfully, then proceed to build statusLabel and wait for the
"Connected" predicate (retain connectedPredicate, statusLabel, and the existing
XCTWaiter usage).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 032ec7d6-0160-4c45-b81b-78633330445e

📥 Commits

Reviewing files that changed from the base of the PR and between 7c0ae94 and 4e4cf1a.

📒 Files selected for processing (5)
  • .github/workflows/swift-example-app-ui-smoke.yml
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/AppState.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleAppUITests/CreditTransferTest.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleAppUITests/README.md
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleAppUITests/Support/WalletFlow.swift
✅ Files skipped from review due to trivial changes (1)
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleAppUITests/README.md
🚧 Files skipped from review as they are similar to previous changes (3)
  • .github/workflows/swift-example-app-ui-smoke.yml
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleAppUITests/CreditTransferTest.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/AppState.swift

Three findings from coderabbit's re-review:

- `switchAppNetworkToTestnet`: wait for the testnet segment's
  `isSelected == true` before trusting the "Connected" predicate.
  The AppState fix already covers the rebind-in-progress race; this
  closes the missed-tap window where the picker setter never fires
  and the previous network's "Connected" still satisfies the wait.
- `runIdentityDiscovery`: anchor the wallet-option predicate with a
  trailing space (`BEGINSWITH "<walletName> "`) so a longer wallet
  name sharing a prefix can't win firstMatch.
- `cleanupWalletsByPrefix`: scroll-and-sweep the full wallets list
  instead of bailing when no matching row is visible in the current
  viewport. Resets to top, then up to 10 delete+reset cycles + 10
  scroll passes per call.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Collaborator

@thepastaclaw thepastaclaw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed 296d9661.

The follow-up fixes address the reliability issues from my earlier pass: the network-status signal now stays honest across the full SDK rebind, overlapping switches no longer clear readiness early, the discovery flow explicitly selects the imported wallet, the recovery-prompt guard runs after the testnet hop, and the unused transfer scaffolding is trimmed back out. The workflow/docs wiring for UI_TEST_MNEMONIC also makes the new smoke path much easier to understand and run.

Looks good to me.

Copy link
Copy Markdown
Collaborator

@thepastaclaw thepastaclaw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Test-only PR adding a testnet UI test, accessibility identifiers, and a nightly CI smoke workflow. No consensus-critical or blocking issues found — Codex's blocking claim about overlapping network switches is a false positive (the synchronous setter runs to completion before either spawned Task executes, so both tasks read the post-update currentNetwork and build SDKs for the same target). Remaining findings are around CI artifact hygiene, fixture/secret coupling, and a real-but-narrow flakiness window in switchAppNetworkToTestnet.

Reviewed commit: 4e4cf1a

🟡 3 suggestion(s) | 💬 1 nitpick(s)

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/swift-sdk/SwiftExampleApp/SwiftExampleAppUITests/Support/WalletFlow.swift`:
- [SUGGESTION] lines 413-427: switchAppNetworkToTestnet can return before the switch actually starts
  The helper waits only for `options.networkStatusLabel` to contain `"Connected"`. That label is not network-aware: when `CreditTransferTest` runs on a simulator that was already left on mainnet/regtest from a prior run, the label reads `"Connected"` *before* the tap. `XCTNSPredicateExpectation` polls (default ~1s cadence) and the predicate can evaluate against the stale pre-tap snapshot before the `currentNetwork` `didSet` → `beginNetworkSwitch` → SwiftUI rerender chain has flipped the label to `"Switching..."`. The helper then returns and the test proceeds with wallet import + identity discovery while the SDK is still bound to the previous network. Make the readiness signal encode the post-tap transition: either expose the active network in the label (e.g. `"Connected · Testnet"`) and match on that, or wait for `appState.isSwitchingNetwork == true` first (a `"Switching..."` substring match) and only then wait for `"Connected"`.
- [SUGGESTION] lines 285-303: Mnemonic typed into plain TextField may surface in uploaded xcresult artifact
  `importWallet()` types the testnet mnemonic into `mnemonicField` (`app.textFields`, not `app.secureTextFields` — confirming the field is a plain SwiftUI `TextField`). The CI workflow at `.github/workflows/swift-example-app-ui-smoke.yml` then uploads `SwiftExampleAppUITests.xcresult` as an artifact with 14-day retention readable by anyone with repo read access. xcresult bundles include the XCUITest activity log (which records `typeText` arguments) and any failure screenshots — a flake while the Create Wallet sheet is visible (e.g. the import-button-enable wait at 297-303) would capture the mnemonic. Although this is a testnet mnemonic, it controls the pre-funded identity referenced by the hardcoded fixtures in `CreditTransferTest.swift`; leaking it lets anyone with repo read access drain testnet credits, hijack the identity, or interfere with future CI runs. Either redact (paste via a pre-cleared `UIPasteboard` hop, or wire a `setLaunchArgument` path that bypasses the visible field for tests) or scope artifact retention more tightly and rotate on observed leak.

In `packages/swift-sdk/SwiftExampleApp/SwiftExampleAppUITests/CreditTransferTest.swift`:
- [SUGGESTION] lines 28-36: Hardcoded sender identityId/walletId silently couple to the rotating CI secret
  `expectedSenderIdentityIdBase58` and `expectedSenderWalletIdHex` are deterministic functions of the mnemonic stored in `secrets.UI_TEST_MNEMONIC`. Nothing in the repo links them: a future maintainer rotating the secret (e.g. after the funded identity is exhausted, or in response to the artifact-leak surface flagged below) will see this test fail with an opaque `"identity row not found within 60s"` timeout from `waitForIdentityRow`, with no breadcrumb that the constants here also need to be regenerated. Either drop the walletId/identityId expectations and rely on the looser "`runIdentityDiscovery` saw +N (N>0)" + "`readIdentityBalanceCredits` parses" properties already covered, or add a comment block here naming the regeneration steps (import the new mnemonic into a clean simulator, observe the resulting `wallets.walletRow.<hex>` identifier and the discovered identity's base58 ID, paste both here).

Comment on lines +413 to +427
let statusLabel = app.descendants(matching: .any)
.matching(identifier: Identifier.Options.networkStatusLabel)
.firstMatch
XCTAssertTrue(
statusLabel.waitForExistence(timeout: 10),
"Expected network status label.",
file: file, line: line
)
let connectedPredicate = NSPredicate { object, _ in
guard let element = object as? XCUIElement, element.exists else { return false }
return element.label.contains("Connected")
}
let result = XCTWaiter.wait(
for: [XCTNSPredicateExpectation(predicate: connectedPredicate, object: statusLabel)],
timeout: timeout
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: switchAppNetworkToTestnet can return before the switch actually starts

The helper waits only for options.networkStatusLabel to contain "Connected". That label is not network-aware: when CreditTransferTest runs on a simulator that was already left on mainnet/regtest from a prior run, the label reads "Connected" before the tap. XCTNSPredicateExpectation polls (default ~1s cadence) and the predicate can evaluate against the stale pre-tap snapshot before the currentNetwork didSetbeginNetworkSwitch → SwiftUI rerender chain has flipped the label to "Switching...". The helper then returns and the test proceeds with wallet import + identity discovery while the SDK is still bound to the previous network. Make the readiness signal encode the post-tap transition: either expose the active network in the label (e.g. "Connected · Testnet") and match on that, or wait for appState.isSwitchingNetwork == true first (a "Switching..." substring match) and only then wait for "Connected".

source: ['codex']

🤖 Fix this with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/swift-sdk/SwiftExampleApp/SwiftExampleAppUITests/Support/WalletFlow.swift`:
- [SUGGESTION] lines 413-427: switchAppNetworkToTestnet can return before the switch actually starts
  The helper waits only for `options.networkStatusLabel` to contain `"Connected"`. That label is not network-aware: when `CreditTransferTest` runs on a simulator that was already left on mainnet/regtest from a prior run, the label reads `"Connected"` *before* the tap. `XCTNSPredicateExpectation` polls (default ~1s cadence) and the predicate can evaluate against the stale pre-tap snapshot before the `currentNetwork` `didSet` → `beginNetworkSwitch` → SwiftUI rerender chain has flipped the label to `"Switching..."`. The helper then returns and the test proceeds with wallet import + identity discovery while the SDK is still bound to the previous network. Make the readiness signal encode the post-tap transition: either expose the active network in the label (e.g. `"Connected · Testnet"`) and match on that, or wait for `appState.isSwitchingNetwork == true` first (a `"Switching..."` substring match) and only then wait for `"Connected"`.

Comment on lines +28 to +36
/// The pre-registered identity behind the sender mnemonic. Discovery
/// must surface this exact ID — that's the regression check on the
/// discovery path.
private let expectedSenderIdentityIdBase58 = "3ou98WEERy6ExmmHWYWsFtyhgW8rmr1giceZYTFqdAAA"

/// walletId derived from the test mnemonic (deterministic). Lets us
/// detect an already-imported wallet from a prior run and reuse it
/// instead of failing on `Wallet operation: Wallet already exists`.
private let expectedSenderWalletIdHex = "2450ec6b6dc2b1b0476875305a6870dee743d47474e3838642b655b68a600793"
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: Hardcoded sender identityId/walletId silently couple to the rotating CI secret

expectedSenderIdentityIdBase58 and expectedSenderWalletIdHex are deterministic functions of the mnemonic stored in secrets.UI_TEST_MNEMONIC. Nothing in the repo links them: a future maintainer rotating the secret (e.g. after the funded identity is exhausted, or in response to the artifact-leak surface flagged below) will see this test fail with an opaque "identity row not found within 60s" timeout from waitForIdentityRow, with no breadcrumb that the constants here also need to be regenerated. Either drop the walletId/identityId expectations and rely on the looser "runIdentityDiscovery saw +N (N>0)" + "readIdentityBalanceCredits parses" properties already covered, or add a comment block here naming the regeneration steps (import the new mnemonic into a clean simulator, observe the resulting wallets.walletRow.<hex> identifier and the discovered identity's base58 ID, paste both here).

source: ['claude']

🤖 Fix this with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/swift-sdk/SwiftExampleApp/SwiftExampleAppUITests/CreditTransferTest.swift`:
- [SUGGESTION] lines 28-36: Hardcoded sender identityId/walletId silently couple to the rotating CI secret
  `expectedSenderIdentityIdBase58` and `expectedSenderWalletIdHex` are deterministic functions of the mnemonic stored in `secrets.UI_TEST_MNEMONIC`. Nothing in the repo links them: a future maintainer rotating the secret (e.g. after the funded identity is exhausted, or in response to the artifact-leak surface flagged below) will see this test fail with an opaque `"identity row not found within 60s"` timeout from `waitForIdentityRow`, with no breadcrumb that the constants here also need to be regenerated. Either drop the walletId/identityId expectations and rely on the looser "`runIdentityDiscovery` saw +N (N>0)" + "`readIdentityBalanceCredits` parses" properties already covered, or add a comment block here naming the regeneration steps (import the new mnemonic into a clean simulator, observe the resulting `wallets.walletRow.<hex>` identifier and the discovered identity's base58 ID, paste both here).

Comment on lines +285 to +303
let mnemonicField = textField(Identifier.mnemonicField, in: app)
XCTAssertTrue(
mnemonicField.waitForExistence(timeout: 5),
"Expected mnemonic field after toggling Import.",
file: file, line: line
)
mnemonicField.tap()
mnemonicField.typeText(mnemonic)
// Don't swipe down to dismiss the keyboard — the Create button lives
// in the navigation bar, not under the keyboard, so the swipe is
// unnecessary AND a sheet-on-app swipeDown will dismiss the sheet.

let createButton = button(Identifier.createWalletButton, in: app)
XCTAssertTrue(
waitForElementToBeEnabled(createButton, timeout: 5),
"Expected Create button to enable after filling import form.",
file: file, line: line
)
createButton.tap()
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: Mnemonic typed into plain TextField may surface in uploaded xcresult artifact

importWallet() types the testnet mnemonic into mnemonicField (app.textFields, not app.secureTextFields — confirming the field is a plain SwiftUI TextField). The CI workflow at .github/workflows/swift-example-app-ui-smoke.yml then uploads SwiftExampleAppUITests.xcresult as an artifact with 14-day retention readable by anyone with repo read access. xcresult bundles include the XCUITest activity log (which records typeText arguments) and any failure screenshots — a flake while the Create Wallet sheet is visible (e.g. the import-button-enable wait at 297-303) would capture the mnemonic. Although this is a testnet mnemonic, it controls the pre-funded identity referenced by the hardcoded fixtures in CreditTransferTest.swift; leaking it lets anyone with repo read access drain testnet credits, hijack the identity, or interfere with future CI runs. Either redact (paste via a pre-cleared UIPasteboard hop, or wire a setLaunchArgument path that bypasses the visible field for tests) or scope artifact retention more tightly and rotate on observed leak.

source: ['claude']

🤖 Fix this with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/swift-sdk/SwiftExampleApp/SwiftExampleAppUITests/Support/WalletFlow.swift`:
- [SUGGESTION] lines 285-303: Mnemonic typed into plain TextField may surface in uploaded xcresult artifact
  `importWallet()` types the testnet mnemonic into `mnemonicField` (`app.textFields`, not `app.secureTextFields` — confirming the field is a plain SwiftUI `TextField`). The CI workflow at `.github/workflows/swift-example-app-ui-smoke.yml` then uploads `SwiftExampleAppUITests.xcresult` as an artifact with 14-day retention readable by anyone with repo read access. xcresult bundles include the XCUITest activity log (which records `typeText` arguments) and any failure screenshots — a flake while the Create Wallet sheet is visible (e.g. the import-button-enable wait at 297-303) would capture the mnemonic. Although this is a testnet mnemonic, it controls the pre-funded identity referenced by the hardcoded fixtures in `CreditTransferTest.swift`; leaking it lets anyone with repo read access drain testnet credits, hijack the identity, or interfere with future CI runs. Either redact (paste via a pre-cleared `UIPasteboard` hop, or wire a `setLaunchArgument` path that bypasses the visible field for tests) or scope artifact retention more tightly and rotate on observed leak.

Comment on lines +559 to +563
let foundPredicate = NSPredicate { object, _ in
guard let element = object as? XCUIElement, element.exists else { return false }
let label = element.label
return label.hasPrefix("+") && label != "+0"
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💬 Nitpick: foundCount predicate accepts any non-+0 string starting with +

label.hasPrefix("+") && label != "+0" would match malformed labels like "+", "+x", or "+ ". SearchWalletsForIdentitiesView only renders "+\(finding.foundCount)" so this won't false-positive today, but a stricter predicate (label.hasPrefix("+") && label.dropFirst().allSatisfy(\.isNumber) && label != "+0") protects against future label format drift without coupling the test to an integer value.

source: ['claude']

Copy link
Copy Markdown
Collaborator

@thepastaclaw thepastaclaw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Test-only PR adding XCUITest coverage for testnet wallet import and DIP-9 identity discovery, plus shared helpers and accessibility identifiers. No production code or consensus paths are touched. I found one substantive coverage mismatch around the claimed non-zero balance assertion, plus two smaller cleanup/docs issues that could make the new UI tests harder to run or diagnose.

Reviewed commit: 3823ee4

🟡 2 suggestion(s) | 💬 1 nitpick(s)

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/swift-sdk/SwiftExampleApp/SwiftExampleAppUITests/CreditTransferTest.swift`:
- [SUGGESTION] lines 110-115: Discovery test never asserts a non-zero balance was actually loaded
  `_ = readIdentityBalanceCredits(in: app)` only verifies that `identityDetail.balanceLabel` exposes a parseable number, which conflicts with the PR description's stated goal of asserting that the discovered identity surfaces with a non-zero balance. Two reinforcing reasons this matters: (1) the in-file comment intentionally disclaims the floor ("deliberately don't assert a non-zero floor"), so the test's coverage and the PR description are inconsistent — anyone copying the description's claim will assume balance regression coverage that doesn't exist; (2) `IdentityDetailView.onAppear` (Views/IdentityDetailView.swift:431-459) only refreshes DPNS/DashPay/tokens — it does not refresh balance, so a regression where discovery still finds the identity but stops persisting/surfacing its balance would render `0` and silently pass this test. Either tighten the PR description and the in-file comment to match what is actually asserted (parseability only), or add an explicit `XCTAssertGreaterThan(credits, 0, ...)` so the discovery+balance path is genuinely guarded.

In `packages/swift-sdk/SwiftExampleApp/SwiftExampleAppUITests/Support/WalletFlow.swift`:
- [SUGGESTION] lines 705-752: `bestEffortDeleteWallet` teardown does not dismiss the orphan-mnemonic recovery alert
  All three teardown blocks (CreditTransferTest.swift:93-100, WalletPersistenceTests.swift:39-48 and 82-91) launch a fresh app and call `bestEffortDeleteWallet`, but the helper never checks for or dismisses a `"Recover Wallet?"` alert. If a prior assertion fails after a partial wallet write that leaks Keychain or SwiftData state — exactly the scenario `testWalletDeletionCleanupSurvivesRelaunch` is designed to catch — the recovery prompt blocks the UI on the cleanup launch, the wallet row never appears, the `guard row.waitForExistence` exits silently, and the residue persists into the next run. The next test then trips `failIfRecoveryPromptVisible` with a confusing pre-existing residue rather than a clean failure attributable to its own setup. Add a best-effort dismiss of the recovery alert at the top of `bestEffortDeleteWallet`, or document this as a known gap.

Comment on lines +110 to +115
// Helper XCTFails if the balance can't be parsed; reaching this
// line confirms identityDetail.balanceLabel exposed a readable
// credit count. We deliberately don't assert a non-zero floor —
// the test's scope is discovery + balance readability, not the
// external testnet-funding state of the fixture identity.
_ = readIdentityBalanceCredits(in: app)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: Discovery test never asserts a non-zero balance was actually loaded

_ = readIdentityBalanceCredits(in: app) only verifies that identityDetail.balanceLabel exposes a parseable number, which conflicts with the PR description's stated goal of asserting that the discovered identity surfaces with a non-zero balance. Two reinforcing reasons this matters: (1) the in-file comment intentionally disclaims the floor ("deliberately don't assert a non-zero floor"), so the test's coverage and the PR description are inconsistent — anyone copying the description's claim will assume balance regression coverage that doesn't exist; (2) IdentityDetailView.onAppear (Views/IdentityDetailView.swift:431-459) only refreshes DPNS/DashPay/tokens — it does not refresh balance, so a regression where discovery still finds the identity but stops persisting/surfacing its balance would render 0 and silently pass this test. Either tighten the PR description and the in-file comment to match what is actually asserted (parseability only), or add an explicit XCTAssertGreaterThan(credits, 0, ...) so the discovery+balance path is genuinely guarded.

💡 Suggested change
Suggested change
// Helper XCTFails if the balance can't be parsed; reaching this
// line confirms identityDetail.balanceLabel exposed a readable
// credit count. We deliberately don't assert a non-zero floor —
// the test's scope is discovery + balance readability, not the
// external testnet-funding state of the fixture identity.
_ = readIdentityBalanceCredits(in: app)
let credits = readIdentityBalanceCredits(in: app)
XCTAssertGreaterThan(
credits,
0,
"Expected discovered identity \(expectedSenderIdentityIdBase58) to surface a non-zero balance."
)

source: ['claude-general', 'codex-general']

🤖 Fix this with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/swift-sdk/SwiftExampleApp/SwiftExampleAppUITests/CreditTransferTest.swift`:
- [SUGGESTION] lines 110-115: Discovery test never asserts a non-zero balance was actually loaded
  `_ = readIdentityBalanceCredits(in: app)` only verifies that `identityDetail.balanceLabel` exposes a parseable number, which conflicts with the PR description's stated goal of asserting that the discovered identity surfaces with a non-zero balance. Two reinforcing reasons this matters: (1) the in-file comment intentionally disclaims the floor ("deliberately don't assert a non-zero floor"), so the test's coverage and the PR description are inconsistent — anyone copying the description's claim will assume balance regression coverage that doesn't exist; (2) `IdentityDetailView.onAppear` (Views/IdentityDetailView.swift:431-459) only refreshes DPNS/DashPay/tokens — it does not refresh balance, so a regression where discovery still finds the identity but stops persisting/surfacing its balance would render `0` and silently pass this test. Either tighten the PR description and the in-file comment to match what is actually asserted (parseability only), or add an explicit `XCTAssertGreaterThan(credits, 0, ...)` so the discovery+balance path is genuinely guarded.

Comment on lines +705 to +752
// MARK: - Best-effort cleanup

/// Best-effort wallet deletion for teardown blocks — does not assert. Used
/// by relaunch tests so an aborted assertion mid-flow doesn't leave a real
/// wallet on the developer's simulator. Silent on every "not found" path,
/// because if any required element is missing there's nothing useful to
/// clean up. This mirrors `deleteWallet` step-for-step but with bailouts
/// instead of XCTAssert calls.
@MainActor
func bestEffortDeleteWallet(named walletName: String, in app: XCUIApplication) {
let walletsScreen = element(Identifier.walletsScreen, in: app)
if !walletsScreen.exists {
let walletsTab = app.tabBars.buttons
.matching(identifier: Identifier.walletsTab)
.firstMatch
if walletsTab.waitForExistence(timeout: 30) {
walletsTab.tap()
} else if app.tabBars.buttons["Wallets"].waitForExistence(timeout: 2) {
app.tabBars.buttons["Wallets"].tap()
}
guard walletsScreen.waitForExistence(timeout: 10)
|| app.navigationBars["Wallets"].waitForExistence(timeout: 1)
else { return }
}

let row = app.buttons
.matching(NSPredicate(format: "label == %@", walletName))
.firstMatch
for _ in 0..<8 where !row.exists {
app.swipeUp()
}
guard row.waitForExistence(timeout: 5) else { return }
row.tap()

let infoButton = button(Identifier.walletInfoButton, in: app)
guard infoButton.waitForExistence(timeout: 5) else { return }
infoButton.tap()

let deleteButton = button(Identifier.deleteWalletButton, in: app)
scrollUntilHittable(deleteButton, in: app)
guard deleteButton.exists, deleteButton.isHittable else { return }
deleteButton.tap()

let deleteAlert = app.alerts["Delete Wallet"]
if deleteAlert.waitForExistence(timeout: 5) {
deleteAlert.buttons["Delete"].tap()
}
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: bestEffortDeleteWallet teardown does not dismiss the orphan-mnemonic recovery alert

All three teardown blocks (CreditTransferTest.swift:93-100, WalletPersistenceTests.swift:39-48 and 82-91) launch a fresh app and call bestEffortDeleteWallet, but the helper never checks for or dismisses a "Recover Wallet?" alert. If a prior assertion fails after a partial wallet write that leaks Keychain or SwiftData state — exactly the scenario testWalletDeletionCleanupSurvivesRelaunch is designed to catch — the recovery prompt blocks the UI on the cleanup launch, the wallet row never appears, the guard row.waitForExistence exits silently, and the residue persists into the next run. The next test then trips failIfRecoveryPromptVisible with a confusing pre-existing residue rather than a clean failure attributable to its own setup. Add a best-effort dismiss of the recovery alert at the top of bestEffortDeleteWallet, or document this as a known gap.

source: ['claude-general']

🤖 Fix this with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/swift-sdk/SwiftExampleApp/SwiftExampleAppUITests/Support/WalletFlow.swift`:
- [SUGGESTION] lines 705-752: `bestEffortDeleteWallet` teardown does not dismiss the orphan-mnemonic recovery alert
  All three teardown blocks (CreditTransferTest.swift:93-100, WalletPersistenceTests.swift:39-48 and 82-91) launch a fresh app and call `bestEffortDeleteWallet`, but the helper never checks for or dismisses a `"Recover Wallet?"` alert. If a prior assertion fails after a partial wallet write that leaks Keychain or SwiftData state — exactly the scenario `testWalletDeletionCleanupSurvivesRelaunch` is designed to catch — the recovery prompt blocks the UI on the cleanup launch, the wallet row never appears, the `guard row.waitForExistence` exits silently, and the residue persists into the next run. The next test then trips `failIfRecoveryPromptVisible` with a confusing pre-existing residue rather than a clean failure attributable to its own setup. Add a best-effort dismiss of the recovery alert at the top of `bestEffortDeleteWallet`, or document this as a known gap.

Comment on lines +14 to +22
//
// Env var:
// * UI_TEST_MNEMONIC — sender wallet's 12-word phrase
//
// Note on env var forwarding: a previous run on this branch showed that
// `xcodebuild test ENV=...` did not propagate env vars to the XCUITest
// runner — only the prefix form `TEST_RUNNER_<NAME>` reached the test
// process (Xcode strips the prefix). Try the unprefixed form first; if
// the env var doesn't reach the test, use the prefixed form.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💬 Nitpick: PR description references UI_TEST_TESTNET_MNEMONIC; code uses UI_TEST_MNEMONIC

The PR description names the secret UI_TEST_TESTNET_MNEMONIC, but the test reads ProcessInfo.processInfo.environment["UI_TEST_MNEMONIC"] (line 44), the file header documents UI_TEST_MNEMONIC (lines 16, 20-22), and .github/workflows/swift-example-app-ui-smoke.yml forwards TEST_RUNNER_UI_TEST_MNEMONIC: ${{ secrets.UI_TEST_MNEMONIC }}. Code is internally consistent; only the PR description is wrong. Update the PR description so anyone configuring the secret uses the correct name.

source: ['claude-general']

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants