Skip to content

test: port Android journeys to iOS - #691

Open
jvsena42 wants to merge 20 commits into
masterfrom
chore/xcodebuildmcp-setup
Open

test: port Android journeys to iOS#691
jvsena42 wants to merge 20 commits into
masterfrom
chore/xcodebuildmcp-setup

Conversation

@jvsena42

@jvsena42 jvsena42 commented Aug 27, 2026

Copy link
Copy Markdown
Member

This PR:

  1. Ports 28 of the 30 journeys from bitkit-android/journeys to journeys/.
  2. Adds the accessibility identifiers they assert on, matching the Android testTag names.
  3. Documents the XcodeBuildMCP and android CLIs for agents, and makes porting journeys part of porting an Android feature.

Description

File names, journey names and action prose are kept identical to the Android originals so the two platforms stay diffable. Only mechanics change: adb becomes xcodebuildmcp, testTags become accessibility identifiers.

Adapted where iOS genuinely differs, each noted in its own description and suite README:

  • notification-permission — toggling off does not deep link into system settings on iOS, so that journey asserts what the toggle actually does and covers the real route in through Settings → Notifications.
  • cjit-notifications — the notification extension maps a push type to fixed copy and never formats an amount, so the thousands-separator assertions are dropped. The duplicate and wrong-type ones port directly.
  • hardware-wallet — USB reconnect becomes a Bridge reconnect, since iOS cannot do WebUSB.
  • deeplinks — not ported. There is no screen or sheet router on iOS, only web URLs, Pubky callbacks and payment URIs, so these would assert a feature that does not exist. Recorded under "Not ported".

Most identifiers already matched, including the whole number pad. The gaps were screen containers, a few fields, the three background-setup switches which shared one binding and had none, and the over-max toasts on the transfer screens.

One behavioural change: widget list tiles are tap-gesture views, so the runtime snapshot never listed them as tappable. They now combine their children and carry the button trait, which also makes a tile read as one item under VoiceOver instead of separate fragments. No changelog fragment, since nothing else here is user-facing — but that part is worth a second opinion.

Trying it out

brew tap getsentry/xcodebuildmcp && brew install xcodebuildmcp
xcodebuildmcp setup          # Bitkit.xcodeproj, scheme Bitkit, Debug, iPhone 17
xcodebuildmcp simulator build-and-run

Because a journey is a shared spec, the same file can be run on Android too — AGENTS.md has the android CLI equivalents, and says to record a disagreement between the platforms rather than rewrite the journey to match.

The widgets journeys need no backend and are the quickest way to see the loop work. The rest need regtest from a sibling bitkit-docker checkout; the hardware wallet ones also need the Trezor emulator and the Bridge build, since the simulator has no Bluetooth LE. Preconditions are in each suite README.

Linked Issues/Tasks

Screenshot / Video

No visual changes. The only UI-affecting edit is accessibility metadata on the widget list tiles.

QA Notes

Manual Tests

You should not have to name a file. Ask for the flow and let the agent pick the journey:

  • test the widgets flow
  • test the send flow
  • test the changes from this branch
  • open a channel
  • do a cross-platform payment
  • check whether any journey has drifted from the code

The last one is the interesting one: a journey that no longer matches the app should come back as a failure, not be worked around.

Automated Checks

  • No test targets changed, and nothing here is wired into CI: journeys are agent-evaluated and belong on the manual ai-device-tests workflow rather than a blocking gate.
  • Verified on an iPhone 17 simulator while authoring: both widgets journeys plus send-amount-over-balance and transfer-spending-over-max walked end to end, and every added identifier asserted present with a deliberately bogus identifier as a control.
  • swiftformat --lint reports only two pre-existing failures, in files this branch does not touch.

jvsena42 and others added 8 commits August 27, 2026 08:28
Ports the journey concept from bitkit-android: XML behaviour specs evaluated by
an agent driving a simulator through the XcodeBuildMCP CLI. Adds the runner
guide, the Android/iOS identifier mapping, and the AGENTS.md rule that an
Android port carries its journeys with it.

Adds the accessibility identifiers the ported journeys assert on, matching the
Android testTag vocabulary:

- Screen containers: SendAmount, SpendingAmount, SpendingAdvanced,
  ExternalAmount, ReceiveCjitAmount, ReceiveCjitConfirm, ReceiveCjitLiquidity
- Fields: SpendingAmountNumberField, SpendingAmountAvailable,
  ExternalAmountAvailable, ReceiveCjitAmountNumberField
- Controls: Send, Receive, ReceiveCjitAmountContinue, NotificationsOpenSystemSettings
- Over-max toasts: SpendingAmountExceededToast, SpendingAdvancedExceededToast,
  ExternalAmountExceededToast (Send already had one)
- The three "Enable background setup" switches, which shared one binding and no ids

Widget list tiles are onTapGesture views, so the runtime UI snapshot did not
expose them as tappable targets. They now combine their children and report the
button trait, making each tile one addressable element; without the combine the
identifier fanned out to every child label.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Four journeys covering the number pad cap on every amount-entry screen: Send,
Transfer to Spending, Receiving capacity, and External node funding.

Walked end to end on an iPhone 17 simulator. The two screens cap differently and
the journeys assert "does not exceed the stated maximum" rather than a value:
Send keeps the largest all-9s value under the cap (99 999 against 297 393),
while Transfer to Spending clamps to the max exactly (296 522). Delete works at
the cap on both and Continue stays enabled throughout.

Entry is via the Savings card, not the Spending card as on Android: the iOS
Spending screen only offers "Transfer From Savings" while the spending balance
is zero, and shows "Transfer To Savings" once a balance exists.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two journeys covering the widgets intro on first run and the add-widget flow.

Both walked end to end on an iPhone 17 simulator with no backend running. The
home page "Add Widget" button sits below the fold and needs a scroll before it
can be tapped, which Android does not; the sheet scroll Android needs is
conditional on iOS since all six tiles fit on an iPhone 17.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three journeys covering the "Enable background setup" toggle on the Receive CJIT
confirm, Receive CJIT liquidity and Transfer Spending confirm screens.

Android's fourth journey does not apply: toggling off on iOS only unregisters
push and stays in place, it does not deep link into system settings. It is
replaced by toggle-off-and-system-settings-route.xml, which asserts that
behaviour and then covers the real iOS route into Settings, the button on
Settings > Notifications. That screen's identifiers were verified on simulator.

All three toggles bind to one setting, so they must be run one per app state.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three journeys covering single delivery of a CJIT payment notification and the
rule that a non-CJIT channel opening is never reported as a received payment.

Adapted rather than transcribed. NotificationService maps a Blocktank push type
to fixed copy and never formats an amount, so Android's thousands-separator
assertions have no iOS counterpart and are dropped. The duplicate-notification
and wrong-type assertions port directly against the iOS copy: cjitPaymentArrived
renders "Incoming Payment", orderPaymentConfirmed renders "Spending Balance
Ready". Android's foreground-service journey becomes an app-backgrounded one.

These need a physical device: the extension has to decrypt a real Blocktank
payload delivered over APNs. Inspection is via the extension's os_log output,
unlike the main app which writes log files into the app group.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Fifteen journeys covering pairing, the home tile and detail screen, the Hardware
Wallets settings surface, passphrase wallets, wallet-scoped activity, and the
watch-only Transfer To Spending flow.

Android's usb-reconnect becomes reconnect.xml: iOS cannot do WebUSB and has no
USB_DEVICE_ATTACHED intent to inject, so the disconnect and reconnect chain runs
through Trezor Bridge and the dev Trezor screen instead.

Verified on simulator as far as one allows. The simulator has no Bluetooth LE,
so every journey that pairs or reconnects needs the Bridge build or a physical
device; the read-only journeys run on a plain simulator build. Two Android
assumptions were corrected: iOS settings has no "Payments" section, and the
connect intro headline renders uppercase.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
#686 settles both transfer maximums and adds to SpendingAdvancedView the
snap-to-max that SpendingAmount.onMaxExceeded() already performs on master.
The existing assertion holds either way, but the disabled pad while settling,
the snap down to the settled value, and the clamp when Max is tapped early are
not covered yet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jvsena42 jvsena42 self-assigned this Aug 27, 2026
@greptile-apps

greptile-apps Bot commented Aug 27, 2026

Copy link
Copy Markdown

Greptile Summary

The PR ports Android journey specifications to iOS and adds the accessibility identifiers needed to execute them manually.

  • Adds 28 journey specifications covering amount limits, CJIT notifications, hardware wallets, notification permissions, and widgets.
  • Adds shared accessibility identifiers to the corresponding SwiftUI screens and controls.
  • Updates agent documentation with journey conventions and simulator tooling.
  • Corrects the widget-tile accessibility state and clarifies that CJIT notification journeys require a physical device.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
Bitkit/Views/Widgets/WidgetsListSheetView.swift Groups widget-tile accessibility content and applies the button trait only while tiles are enabled, resolving the prior accessibility finding.
journeys/cjit-notifications/README.md Documents the physical-device and APNs requirements while explicitly distinguishing the simulator-only UI-test workflow from manual journeys.
AGENTS.md Adds XcodeBuildMCP usage and cross-platform journey authoring and execution guidance.
Bitkit/Views/Transfer/FundManualAmountView.swift Adds journey-facing accessibility identifiers to the amount screen, controls, and over-limit toast.
Bitkit/Views/Transfer/SpendingAdvancedView.swift Adds accessibility identifiers for journey automation while preserving the transfer amount flow.

Reviews (3): Last reviewed commit: "docs: note that lsp mine is silent on su..." | Re-trigger Greptile

Comment thread Bitkit/Views/Widgets/WidgetsListSheetView.swift Outdated
Comment thread journeys/cjit-notifications/README.md Outdated
@jvsena42
jvsena42 marked this pull request as draft August 27, 2026 12:27
jvsena42 and others added 2 commits August 27, 2026 09:27
A journey is a shared spec, so a behaviour meant to match Android can be checked
by running the same file on both sides. Documents the android CLI equivalents of
the XcodeBuildMCP commands, and what to do when the two platforms disagree:
record an intentional difference on both sides, or report a divergence as the
bug it is, rather than rewriting the journey to match current behaviour.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Disabled widget tiles announced themselves as actionable buttons while their tap
handler returns early, so VoiceOver offered a double-tap that did nothing. The
button trait is now only added when the tile is enabled; the tile stays present
by identifier, so journeys can still assert on it, but it is no longer an
actionable target and WidgetEnableInSettings is the only control offered.

The cjit-notifications suite requires a physical device but the README pointed
at ai-device-tests.yml, which runs on the self-hosted macOS runner yet builds
for platform=iOS Simulator, and gave a simctl log-stream command and a
simulator snapshot step. Corrected to device commands, and the missing CI home
is now stated rather than implied.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jvsena42
jvsena42 marked this pull request as ready for review August 27, 2026 12:40
@jvsena42

Copy link
Copy Markdown
Member Author

This will help us review and test PRs faster and also reproduce bugs more reliably

jvsena42 and others added 3 commits August 27, 2026 09:50
Ran the documented commands against a live emulator. Two were wrong:
`android emulator start` takes a required AVD name rather than defaulting,
and `android layout` emits hyphenated JSON keys (`resource-id`, `content-desc`)
rather than camelCase.

Also records that the Settings screen agrees on nine identifiers across the two
platforms, so a comparison run is mostly signal.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Running the same Settings screen on both platforms surfaced two rows that
resolved to the identifier "Value" on iOS, where Android has LanguageSettings
and TransactionSpeedSettings. "Value" is the hardcoded identifier on the row's
right-hand label in SettingsRow, so several rows were competing for it and
neither row could be targeted.

The Language row had no identifier at all. Transaction Speed had one, but
`.accessibilityElement(children: .contain)` stopped the NavigationLink being the
addressable element and let the inner value label surface instead — every
sibling row that works uses a plain identifier with no container modifier.
Dropped the modifier on both and added the missing identifier.

Verified on simulator: the rows now resolve as LanguageSettings and
TransactionSpeedSettings, matching Android.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Paid a Lightning invoice generated on the iOS simulator from the Android
emulator, end to end against the staging regtest backend. Two things worth
writing down:

`adb shell input text` drops characters — it lost 54 of a 397-character invoice
— and `adb shell cmd clipboard` is not implemented on the emulator image. The
reliable route is handing the app a lightning: URI via `am start`, which also
skips the recipient screen.

Also corrects the identifier table: Android emits both AvailableAmount and
available_balance on the send amount screen, not only the latter.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@ovitrif ovitrif left a comment

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.

The hardware-wallet activity-tags journey binds ActivityTag and ActivityTags to the wrong controls, so an identifier-first run can treat the Add Tag button as the chip and then miss the All Activity filter. This is iOS-only: Android's original journey does not attach those ids to those steps, and the shared app vocabulary already matches on both platforms.

Comment thread journeys/hardware-wallet/activity-detail-hw-tags.xml Outdated
Traced against ActivityItemView and ActivityListFilter: ActivityTag is the
Add Tag button and ActivityTags is the detail screen's chip list, so the chip
assertion could pass merely because the Add Tag button was still on screen, and
the All Activity filter lookup would miss entirely — that control is TagsPrompt,
same as Android.

Add Tag now names ActivityTag, the chip assertion names ActivityTags, and the
filter names TagsPrompt. Suite README updated to spell out the distinction.

Reported by @ovitrif in review.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@ben-kaufman ben-kaufman left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The Swift changes build cleanly, but several journey definitions cannot run or validate the intended behavior as written. These harness issues should be corrected before merge so agents do not get deterministic false failures or false passes.

Comment thread AGENTS.md Outdated
Comment thread journeys/README.md Outdated
Comment thread journeys/cjit-notifications/README.md Outdated
Comment thread journeys/hardware-wallet/passphrase-pairing.xml Outdated
Comment thread journeys/hardware-wallet/passphrase-settings-remove.xml Outdated
Comment thread journeys/cjit-notifications/non-cjit-channel-no-payment-notification.xml Outdated
Comment thread journeys/hardware-wallet/passphrase-duplicate.xml Outdated
ovitrif
ovitrif previously approved these changes Aug 27, 2026

@ovitrif ovitrif left a comment

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.

@jvsena42, I rechecked the follow-up to my identifier comment. ActivityTag, ActivityTags, and TagsPrompt now point to the intended controls and preserve Android parity, so that issue is resolved and I’m approving.

I think two pieces of guidance added here should be reconsidered before they turn every journey detail into a merge requirement. Android does not have the new iOS journeys/README.md rule that a journey is the source of truth and any disagreement means the app failed. The referenced ai-device-tests workflow is also something I added earlier; it only runs TrezorBridgeDashboardUITests and is not a journey runner. Rather than expanding that workflow, I think this PR should remove or reword the claim that journeys run there and describe journeys more modestly as agent-driven developer-assistance specs, not QA gates.

@ben-kaufman surfaced useful details, and I appreciate the care behind them, but I would triage them against Android before making them blockers: fix iOS-specific mistakes introduced by the port, preserve or document inherited Android limitations, and leave broader runner or hardening work for separate follow-ups. That keeps this PR focused on Android parity without dismissing the review.

@jvsena42
jvsena42 marked this pull request as draft August 27, 2026 17:07
@jvsena42

Copy link
Copy Markdown
Member Author

Draft for check all comments

Reframing, per @ovitrif: drop the claim that a journey is the source of truth
and that any disagreement means the app failed. Android carries no such rule,
and the corpus is new enough here that a drift is far more likely to be a stale
spec than a bug. Journeys are now described as agent-driven developer-assistance
specs — they gate nothing, and a disagreement is a prompt to look rather than a
bug report. Also removes the claim that journeys run on ai-device-tests.yml:
that workflow runs TrezorBridgeDashboardUITests and never reads journeys/.

Harness errors, per @ben-kaufman, each verified against the source:

- The passphrase leak check used `grep ... || echo NO_PASSPHRASE_LEAK`, which
  prints the clean result for every nonzero status — a missing container or an
  unresolved simulator passed a security check without scanning anything. Now
  resolves the simulator explicitly and treats only grep status 1 as clean,
  reporting LEAK_CHECK_INVALID otherwise. Inherited from Android; noted as such.
- passphrase-settings-remove counted files in a trezor-thp-credentials
  directory that does not exist. TrezorCredentialStorage keeps credentials as
  Keychain items under service to.bitkit.trezor.thp, so the step now validates
  the surviving pairing through observable behaviour.
- The non-CJIT foreground journey asserted an extension log line that cannot be
  produced: NotificationService returns on the .lightning process lock while the
  node is running, before it decrypts or logs anything.
- passphrase-duplicate expected an error toast; HwPassphraseView renders the
  failure inline as HwPassphraseError.
- The cjit log-stream command used a --device flag /usr/bin/log does not have,
  and omitted that the line is logged at .info. Notification Center is now the
  supported assertion path.
- The ./lsp funding commands are a bitkit-android helper, not something
  bitkit-docker ships, so they cannot run here as written.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jvsena42
jvsena42 force-pushed the chore/xcodebuildmcp-setup branch from 4e03171 to 77ba525 Compare August 28, 2026 10:28
@jvsena42

Copy link
Copy Markdown
Member Author

Reframed the journeys as developer-assistance specs instead of source of true

An audit pass in the spirit of @ben-kaufman's review — cross-checking every
identifier the journeys name against the app, running the reachable ones, and
checking every shell command actually executes.

Deterministic false failures found and fixed:

- `xcodebuildmcp simulator stop` and `launch-app` were written bare; both
  require --bundle-id and exit with "Missing required argument" as written.
- Two cjit journeys still carried `log stream --device` after the README was
  corrected. /usr/bin/log has no such flag, so the step could never run.
- Four journeys still invoked `./lsp`, which is a bitkit-android helper and is
  not checked in here.
- The activity tag button is labelled "Tag", not "Add Tag"; the field is
  TagInput and the submit is ActivityTagsSubmit. Verified by running the flow.

Traps documented rather than worked around:

- Activity rows use two id schemes: ActivityShort-<index> in the home recent
  list, Activity-<index> in All Activity and on the hardware wallet screen. The
  suite README conflated them.
- Gesture-based controls never appear as snapshot targets — the All Activity
  tag filter TagsPrompt resolves by identifier but is absent from the target
  list, so a missing control needs an --identifier check before concluding it
  is gone.
- SegmentedControl builds its identifier from localized text, so Tab-all is
  Tab-todas in Spanish. Journeys naming a Tab-* id assume an English device.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
jvsena42 and others added 2 commits August 28, 2026 08:00
Interim fix suggested by @ovitrif on #694: rather than leaving the backend
setup steps as prose, point them at ../bitkit-android/lsp. Most of us keep a
clone of the sibling app at that path, and the hardware-wallet journeys already
reach for ../bitkit-docker the same way.

Restores the real commands in the amount-limits funding steps, the three cjit
journeys, and the external-node peer lookup, each noting there is no iOS copy of
the helper yet and pointing at #694 for the proper port.

Not a permanent answer — the skill can drift if one platform updates it — but it
beats a manual step the agent cannot perform.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ran the borrowed helper end to end against staging regtest: GET /info returns
the node the external-node journey references, and deposit + mine funded the
simulator wallet from 297 623 to 397 623 sats. deposit prints a txid but mine
prints nothing and signals only through its exit status, which reads like a
failure if you are not expecting it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jvsena42

Copy link
Copy Markdown
Member Author
  • Reframed the journeys as developer-assistance specs instead of source of true
  • Asked the agent to re-run all journeys searching and fixing issues like Ben found
  • Added the instruction for using Bitkit-android LSP skill and tested it

@jvsena42
jvsena42 marked this pull request as ready for review August 28, 2026 11:38
jvsena42 and others added 2 commits August 28, 2026 08:39
Short Development subsection covering install, `xcodebuildmcp setup`, and
build-and-run, with the two things that bite here: the config is gitignored
because it holds a machine-local simulator UDID, and there is no standalone
.xcworkspace so it is --project-path. Points at AGENTS.md for the rest.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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