iOS E2E
Work from the checkout root. A local .mobilebuildmcp/config.yaml (Setup) supplies the project, scheme and DerivedData, so mobilebuildmcp commands need no project flags.
Two values recur:
$sim: the simulator UDID (pnpm ios:sim:pairprints one). Pass--simulator-id "$sim"to everymobilebuildmcpcall; without it the config default targets the simulator namediPhone 17.$bundle: the bundle ID, which depends on the signing team. Read it from the build:bundle=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' apps/ios/build/DerivedData/Build/Products/Debug-iphonesimulator/OpenClaw.app/Info.plist).
Build and run
-
Build:
mobilebuildmcp simulator build. After editingapps/ios/project.yml, runpnpm ios:genfirst. -
Install and launch with
simctl, which takes launch arguments (pnpm ios:runtakes none):xcrun simctl install "$sim" apps/ios/build/DerivedData/Build/Products/Debug-iphonesimulator/OpenClaw.app xcrun simctl launch --terminate-running-process "$sim" "$bundle" --openclaw-reset-onboardingmobilebuildmcp simulator build-and-runandlaunch-appattach a log capture that never exits. -
Debug launch arguments:
grep -rhoE '"--openclaw-[a-z-]+"' apps/ios/Sources | sort -u.--openclaw-screenshot-modeplus an--openclaw-*-fixtureflag shows a fixture screen without a Gateway;-AppleLanguages "(de)" -AppleLocale de_DEruns another language. The Debug URL scheme isopenclaw-debug://.
Drive the UI
mobilebuildmcp ui-automation snapshot-ui --style minimallists tappable targets asref|action|role|label|value|id. Text that is not a control (chat bubbles, row titles) appears only with--output json.- Act on a ref with
tap,type-text,long-press(--durationis in milliseconds),swipeorgesture. Wait withwait-for-ui:--predicate exists --label '…' --role buttonfor an element,--predicate textContains --text '…'for text. A wait selector that matches several elements fails; add--roleor--identifier. Keep--timeout-msunder 30000: the daemon ends any request after 30 s withDAEMON_TRANSPORT_FAILED; repeat the wait for longer. - Refs belong to one snapshot; take a fresh one after every screen change.
- See the screen:
mobilebuildmcp simulator screenshot, then read the printed JPEG.
Gotchas:
- Onboarding and sheets also list the screens behind them; match by label or id and take the last match.
- On iPhone the sidebar overlays the content: tap
RootTabs.Sidebar.Showbefore its rows. xcrun simctl openurlqueues a system "Open in OpenClaw?" alert per call; tapOpenorCancelfor each.- System UI outside the app's process (photo picker, share sheet) is missing from snapshots. Tap it by coordinates with the AXe binary bundled in MobileBuildMCP:
"$(brew --prefix mobilebuildmcp)/libexec/bundled/axe" tap -x <x> -y <y> --udid "$sim". Coordinates are points: screenshot pixels × screen width in points ÷ screenshot width.
Logs and crashes
Act first, then read a bounded window:
xcrun simctl spawn "$sim" log show --last 2m --style compact --predicate 'subsystem BEGINSWITH "ai.openclaw"'
Crashes land in ~/Library/Logs/DiagnosticReports/OpenClaw-*.ips; the first OpenClaw frame of the triggered thread names the culprit. App preferences and databases live in the app container (xcrun simctl get_app_container "$sim" "$bundle" data); simctl spawn … defaults read cannot see them. LLDB and xctrace: DEBUG.md.
Tests
- Run the touched suites:
pnpm ios:test <Suite>(selectors andIOS_SIM:apps/ios/README.md). - Prove a regression test red on the base: commit the change, check out the base version of the changed source files (
git checkout <base> -- <paths>), run the suite, thengit checkout HEAD -- <paths>.
Done: the touched suites pass, and each new regression test fails on the base.
Prove a change end to end
- Start the isolated Gateway (one-time profile in Setup) as a long-running service:
node scripts/run-node.mjs --profile ios-sim gateway --bind loopback. It never touches the operator's Gateway or~/.openclaw. While it runs, call the CLI asnode openclaw.mjs --profile ios-sim …;run-noderefuses to rebuild under a running Gateway. - Pair a fresh simulator through the real onboarding UI:
sim=$(pnpm --silent ios:sim:pair | tail -1)(--appfor another build,--profilefor another profile). - Exercise the change through visible controls. A chat round trip: type into
chat-message-input, tapchat-send-message, then wait for a word only the reply contains. - Compare builds: the same steps on
mainand on the change, each on its own fresh simulator, with matching screenshots. - Stop the Gateway and delete the simulators.
Node commands, permissions, auth failures, outages, rejected sends, upgrades and iPad layout: PROOF.md, which also lists what only a device proves.
Done: screenshots or recorded output show the behavior on the change and its absence on main; the Gateway port is closed and no openclaw-pair-* simulator remains.
Setup
One-time per machine; each step ends on a check.
-
Xcode, simulator runtimes,
xcodegenand the Watch Rust toolchain:apps/ios/README.mdprerequisites. Check:xcrun simctl list devices availableshowsiPhone 17. -
SwiftFormat and SwiftLint at the repo pin:
scripts/install-swift-tools.sh <dir on PATH>. Check:scripts/check-swift-tools.shexits 0. -
MobileBuildMCP, Sentry's XcodeBuildMCP CLI:
brew tap getsentry/xcodebuildmcp && brew install getsentry/xcodebuildmcp/mobilebuildmcp. These steps were written against 2.7.1. Check:mobilebuildmcp --version. -
Checkout:
pnpm install --frozen-lockfile, thenpnpm ios:gen(the Xcode project is generated, not committed). Then create.mobilebuildmcp/config.yamland add/.mobilebuildmcp/to.git/info/exclude:schemaVersion: 1 enabledWorkflows: [simulator, simulator-management, ui-automation, debugging] sentryDisabled: true sessionDefaults: projectPath: ./apps/ios/OpenClaw.xcodeproj scheme: OpenClaw configuration: Debug platform: iOS simulatorName: iPhone 17 derivedDataPath: ./apps/ios/build/DerivedData # shared with pnpm ios:runCheck:
mobilebuildmcp simulator list-schemeslistsOpenClaw, andmobilebuildmcp simulator buildsucceeds. -
The
ios-simGateway profile, with the Gateway stopped (oc=node openclaw.mjs --profile ios-sim):oc config set gateway.mode local,oc config set gateway.port 19031,oc config set gateway.auth.mode token,oc config set gateway.auth.token "$(openssl rand -hex 24)". Chat replies need a model for itsmainagent:oc models auth loginoraddwith test credentials. Check: the Gateway starts, andpnpm ios:sim:pairends on the chat screen.