zeno-mobile-runner 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/FEATURES.md +1 -1
  3. package/README.md +9 -1
  4. package/build.zig.zon +2 -2
  5. package/clients/kotlin/README.md +1 -1
  6. package/clients/kotlin/build.gradle.kts +1 -1
  7. package/clients/python/pyproject.toml +1 -1
  8. package/clients/rust/Cargo.lock +1 -1
  9. package/clients/rust/Cargo.toml +1 -1
  10. package/clients/typescript/package.json +1 -1
  11. package/docs/protocol-fixtures/core-session.responses.jsonl +1 -1
  12. package/docs/protocol.md +10 -10
  13. package/examples/ios-dev-client-open-link.json +24 -13
  14. package/examples/ios-dev-client-route-snapshot.json +33 -8
  15. package/npm/scenarios.mjs +15 -8
  16. package/npm/wizard.mjs +1 -1
  17. package/package.json +3 -1
  18. package/prebuilds/darwin-arm64/zmr +0 -0
  19. package/prebuilds/darwin-x64/zmr +0 -0
  20. package/prebuilds/linux-arm64/zmr +0 -0
  21. package/prebuilds/linux-x64/zmr +0 -0
  22. package/scripts/create-react-native-expo-demo-app.sh +11 -13
  23. package/shims/ios/ZMRShim.swift +40 -12
  24. package/shims/ios/ZMRShimUITestCase.swift +142 -16
  25. package/src/android.zig +10 -9
  26. package/src/android_emulator.zig +22 -11
  27. package/src/android_screen_recording.zig +11 -7
  28. package/src/bundle.zig +10 -9
  29. package/src/bundle_redaction.zig +29 -28
  30. package/src/bundle_tar.zig +15 -12
  31. package/src/cli_devices.zig +7 -3
  32. package/src/cli_discover.zig +7 -3
  33. package/src/cli_doctor.zig +7 -3
  34. package/src/cli_draft.zig +51 -47
  35. package/src/cli_explore.zig +7 -3
  36. package/src/cli_import.zig +8 -4
  37. package/src/cli_info.zig +13 -6
  38. package/src/cli_init.zig +9 -5
  39. package/src/cli_inspect.zig +8 -4
  40. package/src/cli_run.zig +22 -16
  41. package/src/cli_serve.zig +3 -3
  42. package/src/cli_trace.zig +25 -12
  43. package/src/cli_validate.zig +8 -4
  44. package/src/command.zig +81 -99
  45. package/src/config.zig +2 -1
  46. package/src/config_diagnostics.zig +2 -1
  47. package/src/config_paths.zig +2 -1
  48. package/src/doctor.zig +8 -7
  49. package/src/doctor_hints.zig +1 -1
  50. package/src/errors.zig +5 -5
  51. package/src/importer.zig +8 -7
  52. package/src/ios.zig +26 -29
  53. package/src/ios_devices.zig +6 -5
  54. package/src/ios_lifecycle.zig +4 -4
  55. package/src/json_rpc.zig +39 -40
  56. package/src/json_rpc_methods.zig +8 -8
  57. package/src/json_rpc_observation.zig +9 -8
  58. package/src/json_rpc_params.zig +1 -1
  59. package/src/json_rpc_trace.zig +22 -21
  60. package/src/main.zig +22 -10
  61. package/src/mcp.zig +28 -19
  62. package/src/mcp_trace.zig +30 -29
  63. package/src/report.zig +39 -36
  64. package/src/report_html.zig +5 -4
  65. package/src/runner.zig +2 -1
  66. package/src/runner_actions.zig +20 -17
  67. package/src/runner_diagnostics.zig +4 -4
  68. package/src/runner_events.zig +55 -51
  69. package/src/runner_native.zig +21 -19
  70. package/src/runner_waits.zig +46 -41
  71. package/src/scaffold.zig +25 -24
  72. package/src/scenario.zig +4 -3
  73. package/src/stdio.zig +129 -0
  74. package/src/trace.zig +34 -26
  75. package/src/trace_summary.zig +3 -2
  76. package/src/trace_summary_diagnostic.zig +15 -13
  77. package/src/validation.zig +5 -4
  78. package/src/version.zig +1 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,58 @@ All notable changes to Zeno Mobile Runner are tracked here.
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.2.2 (2026-06-15)
8
+
9
+ ### Fixed
10
+
11
+ - Migrated the runner to Zig 0.16 process IO and initialized the process IO
12
+ runtime before spawning child commands. This fixes `OutOfMemory` failures
13
+ when commands such as `zmr devices --platform ios --json` or XCTest shim
14
+ launches tried to call `xcrun` through an uninitialized IO context.
15
+ - The iOS XCTest shim now resumes an Expo dev-client project from the
16
+ "Development servers" home screen after an `openLink` command. This covers
17
+ the common case where iOS returns to the Expo launcher instead of immediately
18
+ dispatching a pending app deep link, by selecting the app project row rather
19
+ than requiring each app scenario to add launcher-specific workarounds.
20
+ - The generated React Native/Expo demo app no longer sets `accessibilityLabel`
21
+ to its own `testID` values. On iOS the label overrides the visible text in
22
+ the accessibility tree, which made every text selector in the generated iOS
23
+ workflow scenario unmatchable against the generated app itself (and modeled
24
+ an accessibility antipattern — screen readers announced "demo_title" instead
25
+ of the title). Found while recording real demo footage; the fixture's text
26
+ waits now pass on iOS.
27
+
28
+ ### Added
29
+
30
+ - Added `scripts/record-demo-video.sh` (maintainer-only, npm-excluded): a
31
+ reproducible pipeline that records the launch-demo footage from the
32
+ generated Expo demo app — a passing workflow run, a copy-change break with
33
+ the `zmr explain` diagnosis, and the repaired green run — plus the
34
+ storyboard in `docs/demo-video-storyboard.md`.
35
+
36
+ ## 0.2.1 (2026-06-10)
37
+
38
+ ### Fixed
39
+
40
+ - iOS simulator `openLink` now asks the XCTest shim to accept the SpringBoard
41
+ "Open in <App>?" confirmation for custom URL schemes too, not just
42
+ http/https universal links. Custom schemes are the common Expo dev-client
43
+ deep-link case (`exp+scheme://expo-development-client/...`), and the
44
+ unaccepted dialog previously blocked navigation entirely. The shim's
45
+ `acceptSystemAlert` also gained a single alert-existence probe so the
46
+ best-effort accept stays fast when no dialog appears.
47
+ - The generated Expo dev-client scenarios no longer pass when only the Expo
48
+ dev launcher rendered. The old `waitAny` markers also matched launcher
49
+ chrome ("Home", "Continue", "Sign in"), so runs exited green even though
50
+ the app's JS bundle never loaded. The scenarios now wait for the launcher's
51
+ persistent marker to be gone (`waitNotVisible` on "evelopment servers",
52
+ covering both case-sensitive spellings) — passing immediately when the deep
53
+ link navigates, and failing when the launcher is stuck — then assert no
54
+ bundle-error screen ("Unable to load" / "There was a problem loading") is
55
+ visible before `assertHealthy` and `snapshot`. Verified both directions
56
+ against a real Expo SDK 56 app: passes in ~24s with Metro serving, fails
57
+ with a wait timeout when the bundler is down.
58
+
7
59
  ## 0.2.0 (2026-06-10)
8
60
 
9
61
  ### Added
package/FEATURES.md CHANGED
@@ -142,7 +142,7 @@ state, and writes deterministic traces. It does not embed an LLM.
142
142
 
143
143
  ## Current Limitations
144
144
 
145
- - Current release status is `0.2.0`, a public developer preview rather than
145
+ - Current release status is `0.2.2`, a public developer preview rather than
146
146
  a production-stable `1.0.0`.
147
147
  - Physical iOS log capture is still simulator-first. Physical iOS screenshots
148
148
  are available when the XCTest/XCUIAutomation shim is configured.
package/README.md CHANGED
@@ -86,6 +86,14 @@ Hook it up to your coding agent (Claude Code shown; any MCP client works):
86
86
  claude mcp add zmr -- npx zmr mcp --config .zmr/config.json --trace-dir traces/zmr-agent
87
87
  ```
88
88
 
89
+ Claude Code users can instead install the plugin, which bundles the MCP server
90
+ and a mobile-testing skill:
91
+
92
+ ```text
93
+ /plugin marketplace add johnmikel/zeno-mobile-runner
94
+ /plugin install zmr@zmr-marketplace
95
+ ```
96
+
89
97
  Or in an `.mcp.json` / MCP client config:
90
98
 
91
99
  ```json
@@ -189,7 +197,7 @@ comparisons against your current E2E tool, and multi-device matrices, see
189
197
  | Cloud device farms | Not included | ZMR focuses on local and self-managed device targets in this preview |
190
198
 
191
199
  Slow CI hardware can extend the iOS shim cold-build timeout with
192
- `ZMR_IOS_SHIM_TIMEOUT_MS`. Current release: `0.2.0` developer preview.
200
+ `ZMR_IOS_SHIM_TIMEOUT_MS`. Current release: `0.2.2` developer preview.
193
201
  Protocol version: `2026-04-28`.
194
202
 
195
203
  ## Optional protocol clients
package/build.zig.zon CHANGED
@@ -1,7 +1,7 @@
1
1
  .{
2
2
  .name = .zeno_mobile_runner,
3
- .version = "0.1.7",
4
- .minimum_zig_version = "0.15.2",
3
+ .version = "0.2.2",
4
+ .minimum_zig_version = "0.16.0",
5
5
  .paths = .{""},
6
6
  .fingerprint = 0xcc2c8187874868fc,
7
7
  }
@@ -27,7 +27,7 @@ gradle -p clients/kotlin runFakeSession \
27
27
  ```
28
28
 
29
29
  ```kotlin
30
- implementation(files("path/to/zeno-mobile-runner/clients/kotlin/build/libs/zmr-client-0.2.0.jar"))
30
+ implementation(files("path/to/zeno-mobile-runner/clients/kotlin/build/libs/zmr-client-0.2.2.jar"))
31
31
  ```
32
32
 
33
33
  ```kotlin
@@ -4,7 +4,7 @@ plugins {
4
4
  }
5
5
 
6
6
  group = "dev.zmr"
7
- version = "0.2.0"
7
+ version = "0.2.2"
8
8
 
9
9
  kotlin {
10
10
  jvmToolchain(17)
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "zmr-client"
7
- version = "0.2.0.dev1"
7
+ version = "0.2.2.dev1"
8
8
  description = "Python JSON-RPC client for Zeno Mobile Runner."
9
9
  requires-python = ">=3.9"
10
10
  license = { text = "MIT" }
@@ -100,7 +100,7 @@ checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
100
100
 
101
101
  [[package]]
102
102
  name = "zmr-client"
103
- version = "0.2.0"
103
+ version = "0.2.2"
104
104
  dependencies = [
105
105
  "serde",
106
106
  "serde_json",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "zmr-client"
3
- version = "0.2.0"
3
+ version = "0.2.2"
4
4
  edition = "2021"
5
5
  license = "MIT"
6
6
  description = "Rust JSON-RPC client for Zeno Mobile Runner."
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zmr/client",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "type": "module",
5
5
  "main": "index.mjs",
6
6
  "types": "index.d.ts",
@@ -1,4 +1,4 @@
1
- {"jsonrpc":"2.0","id":1,"result":{"name":"zmr","version":"0.2.0","protocolVersion":"2026-04-28","protocol":{"version":"2026-04-28","minimumCompatibleVersion":"2026-04-28","stability":"dev-preview","breakingChangePolicy":"version-and-changelog"},"platforms":["android","ios"],"platformSupport":{"android":{"status":"supported","deviceTypes":["emulator","physical"],"automation":["adb","uiautomator","android-shim"]},"ios":{"status":"supported","deviceTypes":["simulator","physical"],"automation":["simctl","devicectl","xctest-shim"],"physicalDevices":true}},"iosPreview":false,"transports":["stdio","tcp"],"methods":["runner.capabilities","device.list","session.create","session.close","app.install","app.launch","app.stop","app.openLink","app.clearState","observe.snapshot","observe.semanticSnapshot","ui.tap","ui.type","ui.eraseText","ui.hideKeyboard","ui.swipe","ui.pressBack","ui.scrollUntilVisible","wait.until","wait.any","wait.gone","assert.visible","assert.notVisible","assert.healthy","scenario.validate","trace.events","trace.explore","trace.discover","trace.explain","trace.export"]}}
1
+ {"jsonrpc":"2.0","id":1,"result":{"name":"zmr","version":"0.2.2","protocolVersion":"2026-04-28","protocol":{"version":"2026-04-28","minimumCompatibleVersion":"2026-04-28","stability":"dev-preview","breakingChangePolicy":"version-and-changelog"},"platforms":["android","ios"],"platformSupport":{"android":{"status":"supported","deviceTypes":["emulator","physical"],"automation":["adb","uiautomator","android-shim"]},"ios":{"status":"supported","deviceTypes":["simulator","physical"],"automation":["simctl","devicectl","xctest-shim"],"physicalDevices":true}},"iosPreview":false,"transports":["stdio","tcp"],"methods":["runner.capabilities","device.list","session.create","session.close","app.install","app.launch","app.stop","app.openLink","app.clearState","observe.snapshot","observe.semanticSnapshot","ui.tap","ui.type","ui.eraseText","ui.hideKeyboard","ui.swipe","ui.pressBack","ui.scrollUntilVisible","wait.until","wait.any","wait.gone","assert.visible","assert.notVisible","assert.healthy","scenario.validate","trace.events","trace.explore","trace.discover","trace.explain","trace.export"]}}
2
2
  {"jsonrpc":"2.0","id":2,"result":[{"serial":"fake-device-1","state":"device","ready":true}]}
3
3
  {"jsonrpc":"2.0","id":3,"result":{"sessionId":"default"}}
4
4
  {"jsonrpc":"2.0","id":4,"result":true}
package/docs/protocol.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  ZMR exposes newline-delimited JSON-RPC 2.0 over stdio or localhost TCP in v1. Each request is one JSON object followed by `\n`. Each response is one JSON object followed by `\n`.
4
4
 
5
- Current runner version: `0.2.0`.
5
+ Current runner version: `0.2.2`.
6
6
 
7
7
  Current protocol version: `2026-04-28`.
8
8
 
@@ -47,7 +47,7 @@ and protocol versions. The response is covered by
47
47
  `schemas/inspect-output.schema.json`:
48
48
 
49
49
  ```json
50
- {"ok":true,"status":"ready","schemaVersion":1,"runnerVersion":"0.2.0","protocolVersion":"2026-04-28","dir":".","configPath":".zmr/config.json","configExists":true,"agentInstructionsPath":".zmr/AGENTS.md","agentInstructionsExists":true,"platforms":[{"name":"android","enabled":true,"defaultDevice":"emulator-5554","smokeScenario":".zmr/android-smoke.json","smokeScenarioExists":true,"traceDir":"traces/zmr-android"},{"name":"ios","enabled":true,"defaultDevice":"booted","smokeScenario":".zmr/ios-smoke.json","smokeScenarioExists":true,"traceDir":"traces/zmr-ios"}],"recommendedCommands":["zmr doctor --strict --json --config .zmr/config.json","zmr schemas --json","zmr validate --json .zmr/android-smoke.json","zmr validate --json .zmr/ios-smoke.json","zmr serve --transport stdio --config .zmr/config.json --trace-dir traces/zmr-agent","zmr mcp --config .zmr/config.json --trace-dir traces/zmr-agent"],"claimsPolicy":["verify runs with trace evidence before making readiness claims","do not claim Flutter widget-tree inspection"],"limitations":["inspect is read-only and does not launch devices","autonomous crawling is not shipped; generate or edit scenarios for human review"]}
50
+ {"ok":true,"status":"ready","schemaVersion":1,"runnerVersion":"0.2.2","protocolVersion":"2026-04-28","dir":".","configPath":".zmr/config.json","configExists":true,"agentInstructionsPath":".zmr/AGENTS.md","agentInstructionsExists":true,"platforms":[{"name":"android","enabled":true,"defaultDevice":"emulator-5554","smokeScenario":".zmr/android-smoke.json","smokeScenarioExists":true,"traceDir":"traces/zmr-android"},{"name":"ios","enabled":true,"defaultDevice":"booted","smokeScenario":".zmr/ios-smoke.json","smokeScenarioExists":true,"traceDir":"traces/zmr-ios"}],"recommendedCommands":["zmr doctor --strict --json --config .zmr/config.json","zmr schemas --json","zmr validate --json .zmr/android-smoke.json","zmr validate --json .zmr/ios-smoke.json","zmr serve --transport stdio --config .zmr/config.json --trace-dir traces/zmr-agent","zmr mcp --config .zmr/config.json --trace-dir traces/zmr-agent"],"claimsPolicy":["verify runs with trace evidence before making readiness claims","do not claim Flutter widget-tree inspection"],"limitations":["inspect is read-only and does not launch devices","autonomous crawling is not shipped; generate or edit scenarios for human review"]}
51
51
  ```
52
52
 
53
53
  `zmr discover --from-trace <trace-dir> --out <scenario.json> --validate --json`
@@ -60,7 +60,7 @@ invent credentials, or commit files. The response is covered by
60
60
  `schemas/discover-output.schema.json`:
61
61
 
62
62
  ```json
63
- {"ok":true,"mode":"discover","schemaVersion":1,"runnerVersion":"0.2.0","protocolVersion":"2026-04-28","out":".zmr/discovered/replay-smoke.json","traceDir":"traces/zmr-agent","sourceSnapshot":"traces/zmr-agent/artifacts/snapshot-2.json","name":"draft from login smoke","appId":"com.example.mobiletest","selectorCount":2,"stepCount":6,"replay":{"enabled":true,"eventCount":4,"stepCount":3,"skippedEventCount":1},"warnings":["draft requires human review before commit"],"validated":true,"validation":{"ok":true,"path":".zmr/discovered/replay-smoke.json","name":"draft from login smoke","appId":"com.example.mobiletest","stepCount":6},"nextCommands":["zmr validate --json .zmr/discovered/replay-smoke.json","zmr run .zmr/discovered/replay-smoke.json --json --trace-dir traces/zmr-agent"]}
63
+ {"ok":true,"mode":"discover","schemaVersion":1,"runnerVersion":"0.2.2","protocolVersion":"2026-04-28","out":".zmr/discovered/replay-smoke.json","traceDir":"traces/zmr-agent","sourceSnapshot":"traces/zmr-agent/artifacts/snapshot-2.json","name":"draft from login smoke","appId":"com.example.mobiletest","selectorCount":2,"stepCount":6,"replay":{"enabled":true,"eventCount":4,"stepCount":3,"skippedEventCount":1},"warnings":["draft requires human review before commit"],"validated":true,"validation":{"ok":true,"path":".zmr/discovered/replay-smoke.json","name":"draft from login smoke","appId":"com.example.mobiletest","stepCount":6},"nextCommands":["zmr validate --json .zmr/discovered/replay-smoke.json","zmr run .zmr/discovered/replay-smoke.json --json --trace-dir traces/zmr-agent"]}
64
64
  ```
65
65
 
66
66
  `zmr explore --from-trace <trace-dir> --out <scenario.json> --goal <goal>
@@ -71,7 +71,7 @@ launch devices, crawl the app, invent missing actions, discover credentials, or
71
71
  commit files. The response is covered by `schemas/explore-output.schema.json`:
72
72
 
73
73
  ```json
74
- {"ok":true,"mode":"explore","schemaVersion":1,"runnerVersion":"0.2.0","protocolVersion":"2026-04-28","goal":"find a stable login smoke","autonomous":false,"reviewRequired":true,"guardrails":["writes from existing trace evidence only","does not crawl the app","does not discover credentials or secrets","requires human review before commit"],"out":".zmr/discovered/login-smoke.json","traceDir":"traces/zmr-agent","sourceSnapshot":"traces/zmr-agent/artifacts/snapshot-2.json","name":"draft from login smoke","appId":"com.example.mobiletest","selectorCount":2,"stepCount":6,"replay":{"enabled":true,"eventCount":4,"stepCount":3,"skippedEventCount":1},"warnings":["draft requires human review before commit"],"validated":true,"validation":{"ok":true,"path":".zmr/discovered/login-smoke.json","name":"draft from login smoke","appId":"com.example.mobiletest","stepCount":6},"nextCommands":["zmr validate --json .zmr/discovered/login-smoke.json","zmr run .zmr/discovered/login-smoke.json --json --trace-dir traces/zmr-agent"]}
74
+ {"ok":true,"mode":"explore","schemaVersion":1,"runnerVersion":"0.2.2","protocolVersion":"2026-04-28","goal":"find a stable login smoke","autonomous":false,"reviewRequired":true,"guardrails":["writes from existing trace evidence only","does not crawl the app","does not discover credentials or secrets","requires human review before commit"],"out":".zmr/discovered/login-smoke.json","traceDir":"traces/zmr-agent","sourceSnapshot":"traces/zmr-agent/artifacts/snapshot-2.json","name":"draft from login smoke","appId":"com.example.mobiletest","selectorCount":2,"stepCount":6,"replay":{"enabled":true,"eventCount":4,"stepCount":3,"skippedEventCount":1},"warnings":["draft requires human review before commit"],"validated":true,"validation":{"ok":true,"path":".zmr/discovered/login-smoke.json","name":"draft from login smoke","appId":"com.example.mobiletest","stepCount":6},"nextCommands":["zmr validate --json .zmr/discovered/login-smoke.json","zmr run .zmr/discovered/login-smoke.json --json --trace-dir traces/zmr-agent"]}
75
75
  ```
76
76
 
77
77
  `zmr draft --from-trace <trace-dir> --out <scenario.json> --json` is the lower
@@ -84,7 +84,7 @@ into fields, or commit files. The response is covered by
84
84
  `schemas/draft-output.schema.json`:
85
85
 
86
86
  ```json
87
- {"ok":true,"mode":"draft","schemaVersion":1,"runnerVersion":"0.2.0","protocolVersion":"2026-04-28","out":".zmr/discovered/surface-smoke.json","traceDir":"traces/zmr-agent","sourceSnapshot":"traces/zmr-agent/artifacts/snapshot-2.json","name":"draft from login smoke","appId":"com.example.mobiletest","selectorCount":2,"stepCount":4,"replay":{"enabled":false,"eventCount":0,"stepCount":0,"skippedEventCount":0},"warnings":["draft requires human review before commit"],"nextCommands":["zmr validate --json .zmr/discovered/surface-smoke.json","zmr run .zmr/discovered/surface-smoke.json --json --trace-dir traces/zmr-agent"]}
87
+ {"ok":true,"mode":"draft","schemaVersion":1,"runnerVersion":"0.2.2","protocolVersion":"2026-04-28","out":".zmr/discovered/surface-smoke.json","traceDir":"traces/zmr-agent","sourceSnapshot":"traces/zmr-agent/artifacts/snapshot-2.json","name":"draft from login smoke","appId":"com.example.mobiletest","selectorCount":2,"stepCount":4,"replay":{"enabled":false,"eventCount":0,"stepCount":0,"skippedEventCount":0},"warnings":["draft requires human review before commit"],"nextCommands":["zmr validate --json .zmr/discovered/surface-smoke.json","zmr run .zmr/discovered/surface-smoke.json --json --trace-dir traces/zmr-agent"]}
88
88
  ```
89
89
 
90
90
  `zmr draft --include-actions` additionally parses `events.jsonl` and prepends
@@ -214,7 +214,7 @@ installers, setup scripts, and generated clients. The response is covered by
214
214
  `schemas/version-output.schema.json`:
215
215
 
216
216
  ```json
217
- {"name":"zmr","version":"0.2.0","protocolVersion":"2026-04-28","minimumCompatibleProtocolVersion":"2026-04-28","stability":"dev-preview","breakingChangePolicy":"version-and-changelog"}
217
+ {"name":"zmr","version":"0.2.2","protocolVersion":"2026-04-28","minimumCompatibleProtocolVersion":"2026-04-28","stability":"dev-preview","breakingChangePolicy":"version-and-changelog"}
218
218
  ```
219
219
 
220
220
  ## Capabilities Output Contract
@@ -226,7 +226,7 @@ and method inventory for JSON-RPC clients. The result object is covered by
226
226
  iOS simulator, or physical iOS workflows are available.
227
227
 
228
228
  ```json
229
- {"name":"zmr","version":"0.2.0","protocolVersion":"2026-04-28","protocol":{"version":"2026-04-28","minimumCompatibleVersion":"2026-04-28","stability":"dev-preview","breakingChangePolicy":"version-and-changelog"},"platforms":["android","ios"],"platformSupport":{"android":{"status":"supported","deviceTypes":["emulator","physical"],"automation":["adb","uiautomator","android-shim"]},"ios":{"status":"supported","deviceTypes":["simulator","physical"],"automation":["simctl","devicectl","xctest-shim"],"physicalDevices":true}},"iosPreview":false,"transports":["stdio","tcp"],"methods":["runner.capabilities","device.list","session.create","session.close","app.install","app.launch","app.stop","app.openLink","app.clearState","observe.snapshot","observe.semanticSnapshot","ui.tap","ui.type","ui.eraseText","ui.hideKeyboard","ui.swipe","ui.pressBack","ui.scrollUntilVisible","wait.until","wait.any","wait.gone","assert.visible","assert.notVisible","assert.healthy","scenario.validate","trace.events","trace.explore","trace.discover","trace.explain","trace.export"]}
229
+ {"name":"zmr","version":"0.2.2","protocolVersion":"2026-04-28","protocol":{"version":"2026-04-28","minimumCompatibleVersion":"2026-04-28","stability":"dev-preview","breakingChangePolicy":"version-and-changelog"},"platforms":["android","ios"],"platformSupport":{"android":{"status":"supported","deviceTypes":["emulator","physical"],"automation":["adb","uiautomator","android-shim"]},"ios":{"status":"supported","deviceTypes":["simulator","physical"],"automation":["simctl","devicectl","xctest-shim"],"physicalDevices":true}},"iosPreview":false,"transports":["stdio","tcp"],"methods":["runner.capabilities","device.list","session.create","session.close","app.install","app.launch","app.stop","app.openLink","app.clearState","observe.snapshot","observe.semanticSnapshot","ui.tap","ui.type","ui.eraseText","ui.hideKeyboard","ui.swipe","ui.pressBack","ui.scrollUntilVisible","wait.until","wait.any","wait.gone","assert.visible","assert.notVisible","assert.healthy","scenario.validate","trace.events","trace.explore","trace.discover","trace.explain","trace.export"]}
230
230
  ```
231
231
 
232
232
  ## Doctor Output Contract
@@ -432,7 +432,7 @@ Request:
432
432
  Response:
433
433
 
434
434
  ```json
435
- {"jsonrpc":"2.0","id":1,"result":{"name":"zmr","version":"0.2.0","protocolVersion":"2026-04-28","protocol":{"version":"2026-04-28","minimumCompatibleVersion":"2026-04-28","stability":"dev-preview","breakingChangePolicy":"version-and-changelog"},"platforms":["android","ios"],"platformSupport":{"android":{"status":"supported","deviceTypes":["emulator","physical"],"automation":["adb","uiautomator","android-shim"]},"ios":{"status":"supported","deviceTypes":["simulator","physical"],"automation":["simctl","devicectl","xctest-shim"],"physicalDevices":true}},"iosPreview":false,"transports":["stdio","tcp"],"methods":["runner.capabilities","device.list","session.create","session.close","app.install","app.launch","app.stop","app.openLink","app.clearState","observe.snapshot","observe.semanticSnapshot","ui.tap","ui.type","ui.eraseText","ui.hideKeyboard","ui.swipe","ui.pressBack","ui.scrollUntilVisible","wait.until","wait.any","wait.gone","assert.visible","assert.notVisible","assert.healthy","scenario.validate","trace.events","trace.explore","trace.discover","trace.explain","trace.export"]}}
435
+ {"jsonrpc":"2.0","id":1,"result":{"name":"zmr","version":"0.2.2","protocolVersion":"2026-04-28","protocol":{"version":"2026-04-28","minimumCompatibleVersion":"2026-04-28","stability":"dev-preview","breakingChangePolicy":"version-and-changelog"},"platforms":["android","ios"],"platformSupport":{"android":{"status":"supported","deviceTypes":["emulator","physical"],"automation":["adb","uiautomator","android-shim"]},"ios":{"status":"supported","deviceTypes":["simulator","physical"],"automation":["simctl","devicectl","xctest-shim"],"physicalDevices":true}},"iosPreview":false,"transports":["stdio","tcp"],"methods":["runner.capabilities","device.list","session.create","session.close","app.install","app.launch","app.stop","app.openLink","app.clearState","observe.snapshot","observe.semanticSnapshot","ui.tap","ui.type","ui.eraseText","ui.hideKeyboard","ui.swipe","ui.pressBack","ui.scrollUntilVisible","wait.until","wait.any","wait.gone","assert.visible","assert.notVisible","assert.healthy","scenario.validate","trace.events","trace.explore","trace.discover","trace.explain","trace.export"]}}
436
436
  ```
437
437
 
438
438
  ### `trace.events`
@@ -514,7 +514,7 @@ Request:
514
514
  Response:
515
515
 
516
516
  ```json
517
- {"jsonrpc":"2.0","id":25,"result":{"ok":true,"mode":"discover","schemaVersion":1,"runnerVersion":"0.2.0","protocolVersion":"2026-04-28","out":".zmr/discovered/agent-smoke.json","traceDir":"traces/agent-session","sourceSnapshot":"traces/agent-session/artifacts/snapshot-1.json","name":"agent smoke","appId":"com.example.mobiletest","selectorCount":1,"stepCount":4,"replay":{"enabled":true,"eventCount":2,"stepCount":1,"skippedEventCount":1},"warnings":["draft requires human review before commit"],"validated":true,"validation":{"ok":true,"path":".zmr/discovered/agent-smoke.json","name":"agent smoke","appId":"com.example.mobiletest","stepCount":4},"nextCommands":["zmr validate --json .zmr/discovered/agent-smoke.json","zmr run .zmr/discovered/agent-smoke.json --json --trace-dir traces/agent-session"]}}
517
+ {"jsonrpc":"2.0","id":25,"result":{"ok":true,"mode":"discover","schemaVersion":1,"runnerVersion":"0.2.2","protocolVersion":"2026-04-28","out":".zmr/discovered/agent-smoke.json","traceDir":"traces/agent-session","sourceSnapshot":"traces/agent-session/artifacts/snapshot-1.json","name":"agent smoke","appId":"com.example.mobiletest","selectorCount":1,"stepCount":4,"replay":{"enabled":true,"eventCount":2,"stepCount":1,"skippedEventCount":1},"warnings":["draft requires human review before commit"],"validated":true,"validation":{"ok":true,"path":".zmr/discovered/agent-smoke.json","name":"agent smoke","appId":"com.example.mobiletest","stepCount":4},"nextCommands":["zmr validate --json .zmr/discovered/agent-smoke.json","zmr run .zmr/discovered/agent-smoke.json --json --trace-dir traces/agent-session"]}}
518
518
  ```
519
519
 
520
520
  Without `--trace-dir`, it returns `ok: false` with `traceDir: null`. Generated
@@ -537,7 +537,7 @@ Request:
537
537
  Response:
538
538
 
539
539
  ```json
540
- {"jsonrpc":"2.0","id":27,"result":{"ok":true,"mode":"explore","schemaVersion":1,"runnerVersion":"0.2.0","protocolVersion":"2026-04-28","out":".zmr/discovered/agent-goal.json","traceDir":"traces/agent-session","sourceSnapshot":"traces/agent-session/artifacts/snapshot-1.json","name":"agent goal smoke","appId":"com.example.mobiletest","selectorCount":1,"stepCount":4,"replay":{"enabled":true,"eventCount":2,"stepCount":1,"skippedEventCount":1},"warnings":["draft requires human review before commit"],"validated":true,"validation":{"ok":true,"path":".zmr/discovered/agent-goal.json","name":"agent goal smoke","appId":"com.example.mobiletest","stepCount":4},"nextCommands":["zmr validate --json .zmr/discovered/agent-goal.json","zmr run .zmr/discovered/agent-goal.json --json --trace-dir traces/agent-session"],"goal":"find a stable login smoke","autonomous":false,"reviewRequired":true,"guardrails":["writes from existing trace evidence only","does not crawl the app","does not discover credentials or secrets","requires human review before commit"]}}
540
+ {"jsonrpc":"2.0","id":27,"result":{"ok":true,"mode":"explore","schemaVersion":1,"runnerVersion":"0.2.2","protocolVersion":"2026-04-28","out":".zmr/discovered/agent-goal.json","traceDir":"traces/agent-session","sourceSnapshot":"traces/agent-session/artifacts/snapshot-1.json","name":"agent goal smoke","appId":"com.example.mobiletest","selectorCount":1,"stepCount":4,"replay":{"enabled":true,"eventCount":2,"stepCount":1,"skippedEventCount":1},"warnings":["draft requires human review before commit"],"validated":true,"validation":{"ok":true,"path":".zmr/discovered/agent-goal.json","name":"agent goal smoke","appId":"com.example.mobiletest","stepCount":4},"nextCommands":["zmr validate --json .zmr/discovered/agent-goal.json","zmr run .zmr/discovered/agent-goal.json --json --trace-dir traces/agent-session"],"goal":"find a stable login smoke","autonomous":false,"reviewRequired":true,"guardrails":["writes from existing trace evidence only","does not crawl the app","does not discover credentials or secrets","requires human review before commit"]}}
541
541
  ```
542
542
 
543
543
  Without `--trace-dir`, it returns `ok: false` with `traceDir: null`.
@@ -2,25 +2,36 @@
2
2
  "name": "iOS Expo dev-client open-link smoke",
3
3
  "appId": "com.example.mobiletest",
4
4
  "steps": [
5
- { "action": "stop" },
5
+ {
6
+ "action": "stop"
7
+ },
6
8
  {
7
9
  "action": "openLink",
8
10
  "url": "exp+mobiletest://expo-development-client/?url=http%3A%2F%2F127.0.0.1%3A8081"
9
11
  },
10
12
  {
11
- "action": "waitAny",
12
- "selectors": [
13
- { "textContains": "Downloading" },
14
- { "textContains": "Connected to:" },
15
- { "textContains": "Reload" },
16
- { "textContains": "Continue" },
17
- { "textContains": "Sign in" },
18
- { "textContains": "Home" },
19
- { "textContains": "Unable to load" }
20
- ],
13
+ "action": "waitNotVisible",
14
+ "selector": {
15
+ "textContains": "evelopment servers"
16
+ },
21
17
  "timeoutMs": 120000
22
18
  },
23
- { "action": "assertHealthy" },
24
- { "action": "snapshot" }
19
+ {
20
+ "action": "assertNoneVisible",
21
+ "selectors": [
22
+ {
23
+ "textContains": "Unable to load"
24
+ },
25
+ {
26
+ "textContains": "There was a problem loading"
27
+ }
28
+ ]
29
+ },
30
+ {
31
+ "action": "assertHealthy"
32
+ },
33
+ {
34
+ "action": "snapshot"
35
+ }
25
36
  ]
26
37
  }
@@ -2,23 +2,48 @@
2
2
  "name": "iOS Expo dev-client route snapshot",
3
3
  "appId": "com.example.mobiletest",
4
4
  "steps": [
5
- { "action": "stop" },
5
+ {
6
+ "action": "stop"
7
+ },
6
8
  {
7
9
  "action": "openLink",
8
10
  "url": "exampleapp:///settings?mode=manage&source=zmr"
9
11
  },
12
+ {
13
+ "action": "waitNotVisible",
14
+ "selector": {
15
+ "textContains": "evelopment servers"
16
+ },
17
+ "timeoutMs": 60000
18
+ },
10
19
  {
11
20
  "action": "waitAny",
12
21
  "selectors": [
13
- { "textContains": "Settings" },
14
- { "textContains": "Manage" },
15
- { "textContains": "Home" },
16
- { "textContains": "Sign in" },
17
- { "textContains": "Unable to load" }
22
+ {
23
+ "textContains": "Settings"
24
+ },
25
+ {
26
+ "textContains": "Manage"
27
+ }
18
28
  ],
19
29
  "timeoutMs": 60000
20
30
  },
21
- { "action": "assertHealthy" },
22
- { "action": "snapshot" }
31
+ {
32
+ "action": "assertNoneVisible",
33
+ "selectors": [
34
+ {
35
+ "textContains": "Unable to load"
36
+ },
37
+ {
38
+ "textContains": "There was a problem loading"
39
+ }
40
+ ]
41
+ },
42
+ {
43
+ "action": "assertHealthy"
44
+ },
45
+ {
46
+ "action": "snapshot"
47
+ }
23
48
  ]
24
49
  }
package/npm/scenarios.mjs CHANGED
@@ -64,6 +64,14 @@ export function scenarioFiles(appId, { android = true, ios = true, expoDevClient
64
64
  }
65
65
 
66
66
  export function devClientScenario(name, appId, scheme, metroUrl) {
67
+ // The Expo dev-launcher chrome shares tokens with common app copy ("Home",
68
+ // "Continue", "Sign in"), and bundle-loading overlays ("Downloading") can
69
+ // flash faster than a snapshot poll, so neither can prove the app's JS
70
+ // bundle loaded. Instead wait for the launcher's persistent marker to be
71
+ // GONE: "evelopment servers" matches both "Development servers" and "No
72
+ // development servers found" (matching is case-sensitive), passes
73
+ // immediately when the deep link navigates without showing the launcher,
74
+ // and times out — failing the run — when the launcher is stuck on screen.
67
75
  return {
68
76
  name,
69
77
  appId,
@@ -74,17 +82,16 @@ export function devClientScenario(name, appId, scheme, metroUrl) {
74
82
  url: `exp+${scheme}://expo-development-client/?url=${encodeURIComponent(metroUrl)}`,
75
83
  },
76
84
  {
77
- action: "waitAny",
85
+ action: "waitNotVisible",
86
+ selector: { textContains: "evelopment servers" },
87
+ timeoutMs: 120000,
88
+ },
89
+ {
90
+ action: "assertNoneVisible",
78
91
  selectors: [
79
- { textContains: "Downloading" },
80
- { textContains: "Connected to:" },
81
- { textContains: "Reload" },
82
- { textContains: "Continue" },
83
- { textContains: "Sign in" },
84
- { textContains: "Home" },
85
92
  { textContains: "Unable to load" },
93
+ { textContains: "There was a problem loading" },
86
94
  ],
87
- timeoutMs: 120000,
88
95
  },
89
96
  { action: "assertHealthy" },
90
97
  { action: "snapshot" },
package/npm/wizard.mjs CHANGED
@@ -23,7 +23,7 @@ if (!options.json) {
23
23
  console.log("================");
24
24
  }
25
25
 
26
- if (!options.yes && !options.json) {
26
+ if (!options.yes && !options.json && input.isTTY) {
27
27
  await promptForMissingOptions(options);
28
28
  }
29
29
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zeno-mobile-runner",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Agent-native mobile app test runner for React Native, Expo, Flutter, and native Android/iOS.",
5
5
  "main": "npm/index.mjs",
6
6
  "repository": {
@@ -66,11 +66,13 @@
66
66
  "viewer/",
67
67
  "docs/",
68
68
  "!docs/assets/",
69
+ "!docs/demo-video-storyboard.md",
69
70
  "scripts/",
70
71
  "!scripts/build-npm-package.sh",
71
72
  "!scripts/build-release.sh",
72
73
  "!scripts/capture-screenshots.sh",
73
74
  "!scripts/capture-viewer-shots.mjs",
75
+ "!scripts/record-demo-video.sh",
74
76
  "!scripts/ci-gate.sh",
75
77
  "!scripts/coverage.sh",
76
78
  "!scripts/generate-homebrew-formula.mjs",
Binary file
Binary file
Binary file
Binary file
@@ -229,7 +229,7 @@ export default function App() {
229
229
  <View style={styles.shell}>
230
230
  {screen === "welcome" ? (
231
231
  <View style={styles.centered}>
232
- <Text style={styles.title} testID="demo_title" accessibilityLabel="demo_title">
232
+ <Text style={styles.title} testID="demo_title">
233
233
  Zeno Expo Demo
234
234
  </Text>
235
235
  <Text style={styles.copy}>A generated React Native and Expo workflow surface.</Text>
@@ -246,7 +246,7 @@ export default function App() {
246
246
 
247
247
  {screen === "profile" ? (
248
248
  <View style={styles.form}>
249
- <Text style={styles.heading} testID="profile_title" accessibilityLabel="profile_title">
249
+ <Text style={styles.heading} testID="profile_title">
250
250
  Profile
251
251
  </Text>
252
252
  <TextInput
@@ -257,7 +257,7 @@ export default function App() {
257
257
  autoCorrect={false}
258
258
  style={styles.input}
259
259
  testID="profile_name_input"
260
- accessibilityLabel="profile_name_input"
260
+
261
261
  />
262
262
  <TextInput
263
263
  value={profileEmail}
@@ -268,7 +268,7 @@ export default function App() {
268
268
  keyboardType="email-address"
269
269
  style={styles.input}
270
270
  testID="profile_email_input"
271
- accessibilityLabel="profile_email_input"
271
+
272
272
  />
273
273
  <PrimaryButton
274
274
  label="Save profile"
@@ -283,20 +283,19 @@ export default function App() {
283
283
 
284
284
  {screen === "catalog" ? (
285
285
  <View style={styles.flex}>
286
- <Text style={styles.heading} testID="catalog_title" accessibilityLabel="catalog_title">
286
+ <Text style={styles.heading} testID="catalog_title">
287
287
  Catalog
288
288
  </Text>
289
289
  <ScrollView
290
290
  style={styles.list}
291
291
  contentContainerStyle={styles.listContent}
292
292
  testID="catalog_list"
293
- accessibilityLabel="catalog_list"
293
+
294
294
  >
295
295
  {catalogItems.map((item) => (
296
296
  <Pressable
297
297
  key={item.id}
298
298
  testID={\`catalog_item_\${item.id}\`}
299
- accessibilityLabel={\`catalog_item_\${item.id}\`}
300
299
  accessibilityRole="button"
301
300
  style={styles.row}
302
301
  onPress={() => {
@@ -315,10 +314,10 @@ export default function App() {
315
314
 
316
315
  {screen === "detail" ? (
317
316
  <View style={styles.form}>
318
- <Text style={styles.heading} testID="detail_title" accessibilityLabel="detail_title">
317
+ <Text style={styles.heading} testID="detail_title">
319
318
  {selectedItem.title}
320
319
  </Text>
321
- <Text style={styles.copy} testID="detail_subtitle" accessibilityLabel="detail_subtitle">
320
+ <Text style={styles.copy} testID="detail_subtitle">
322
321
  {selectedItem.subtitle}
323
322
  </Text>
324
323
  <PrimaryButton
@@ -334,10 +333,10 @@ export default function App() {
334
333
 
335
334
  {screen === "review" ? (
336
335
  <View style={styles.form}>
337
- <Text style={styles.heading} testID="review_title" accessibilityLabel="review_title">
336
+ <Text style={styles.heading} testID="review_title">
338
337
  Review
339
338
  </Text>
340
- <Text style={styles.copy} testID="review_summary" accessibilityLabel="review_summary">
339
+ <Text style={styles.copy} testID="review_summary">
341
340
  {profileName || "Riley"} saved {selectedItem.title}
342
341
  </Text>
343
342
  <PrimaryButton
@@ -348,7 +347,7 @@ export default function App() {
348
347
  </View>
349
348
  ) : null}
350
349
 
351
- <Text style={styles.status} testID="workflow_status" accessibilityLabel="workflow_status">
350
+ <Text style={styles.status} testID="workflow_status">
352
351
  {status}
353
352
  </Text>
354
353
  </View>
@@ -368,7 +367,6 @@ function PrimaryButton({
368
367
  return (
369
368
  <Pressable
370
369
  accessibilityRole="button"
371
- accessibilityLabel={testID}
372
370
  testID={testID}
373
371
  onPress={onPress}
374
372
  style={({ pressed }) => [styles.button, pressed && styles.buttonPressed]}