zeno-mobile-runner 0.2.1 → 0.2.3

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 (76) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/FEATURES.md +1 -1
  3. package/README.md +1 -1
  4. package/build.zig +10 -0
  5. package/build.zig.zon +2 -2
  6. package/clients/kotlin/README.md +1 -1
  7. package/clients/kotlin/build.gradle.kts +1 -1
  8. package/clients/python/pyproject.toml +1 -1
  9. package/clients/rust/Cargo.lock +1 -1
  10. package/clients/rust/Cargo.toml +1 -1
  11. package/clients/typescript/package.json +1 -1
  12. package/docs/protocol-fixtures/core-session.responses.jsonl +1 -1
  13. package/docs/protocol.md +10 -10
  14. package/package.json +3 -1
  15. package/prebuilds/darwin-arm64/zmr +0 -0
  16. package/prebuilds/darwin-x64/zmr +0 -0
  17. package/prebuilds/linux-arm64/zmr +0 -0
  18. package/prebuilds/linux-x64/zmr +0 -0
  19. package/scripts/create-react-native-expo-demo-app.sh +11 -13
  20. package/shims/ios/ZMRShim.swift +40 -12
  21. package/shims/ios/ZMRShimUITestCase.swift +135 -15
  22. package/src/android.zig +10 -9
  23. package/src/android_emulator.zig +22 -11
  24. package/src/android_screen_recording.zig +11 -7
  25. package/src/bundle.zig +10 -9
  26. package/src/bundle_redaction.zig +29 -28
  27. package/src/bundle_tar.zig +15 -12
  28. package/src/cli_devices.zig +7 -3
  29. package/src/cli_discover.zig +7 -3
  30. package/src/cli_doctor.zig +7 -3
  31. package/src/cli_draft.zig +51 -47
  32. package/src/cli_explore.zig +7 -3
  33. package/src/cli_import.zig +8 -4
  34. package/src/cli_info.zig +13 -6
  35. package/src/cli_init.zig +9 -5
  36. package/src/cli_inspect.zig +8 -4
  37. package/src/cli_run.zig +22 -16
  38. package/src/cli_serve.zig +3 -3
  39. package/src/cli_trace.zig +25 -12
  40. package/src/cli_validate.zig +8 -4
  41. package/src/command.zig +81 -99
  42. package/src/config.zig +2 -1
  43. package/src/config_diagnostics.zig +2 -1
  44. package/src/config_paths.zig +2 -1
  45. package/src/doctor.zig +8 -7
  46. package/src/doctor_hints.zig +1 -1
  47. package/src/errors.zig +5 -5
  48. package/src/importer.zig +8 -7
  49. package/src/ios.zig +98 -19
  50. package/src/ios_devices.zig +6 -5
  51. package/src/ios_lifecycle.zig +4 -4
  52. package/src/ios_shim.zig +12 -0
  53. package/src/json_rpc.zig +39 -40
  54. package/src/json_rpc_methods.zig +8 -8
  55. package/src/json_rpc_observation.zig +9 -8
  56. package/src/json_rpc_params.zig +1 -1
  57. package/src/json_rpc_trace.zig +22 -21
  58. package/src/main.zig +19 -10
  59. package/src/mcp.zig +28 -19
  60. package/src/mcp_trace.zig +30 -29
  61. package/src/report.zig +39 -36
  62. package/src/report_html.zig +5 -4
  63. package/src/runner.zig +2 -1
  64. package/src/runner_actions.zig +20 -17
  65. package/src/runner_diagnostics.zig +4 -4
  66. package/src/runner_events.zig +55 -51
  67. package/src/runner_native.zig +21 -19
  68. package/src/runner_waits.zig +46 -41
  69. package/src/scaffold.zig +25 -24
  70. package/src/scenario.zig +4 -3
  71. package/src/stdio.zig +129 -0
  72. package/src/trace.zig +34 -26
  73. package/src/trace_summary.zig +3 -2
  74. package/src/trace_summary_diagnostic.zig +15 -13
  75. package/src/validation.zig +5 -4
  76. package/src/version.zig +1 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,45 @@ All notable changes to Zeno Mobile Runner are tracked here.
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.2.3 (2026-06-15)
8
+
9
+ ### Fixed
10
+
11
+ - iOS simulator `openLink` now keeps sweeping delayed XCTest shim
12
+ interruptions until the shim reports that an alert or Expo dev-client
13
+ chooser was actually accepted. This fixes Expo dev-client "Deep link
14
+ received" sheets that appear a few seconds after `simctl openurl` returns,
15
+ so app scenarios no longer need launcher-specific timing workarounds.
16
+
17
+ ## 0.2.2 (2026-06-15)
18
+
19
+ ### Fixed
20
+
21
+ - Migrated the runner to Zig 0.16 process IO and initialized the process IO
22
+ runtime before spawning child commands. This fixes `OutOfMemory` failures
23
+ when commands such as `zmr devices --platform ios --json` or XCTest shim
24
+ launches tried to call `xcrun` through an uninitialized IO context.
25
+ - The iOS XCTest shim now resumes an Expo dev-client project from the
26
+ "Development servers" home screen after an `openLink` command. This covers
27
+ the common case where iOS returns to the Expo launcher instead of immediately
28
+ dispatching a pending app deep link, by selecting the app project row rather
29
+ than requiring each app scenario to add launcher-specific workarounds.
30
+ - The generated React Native/Expo demo app no longer sets `accessibilityLabel`
31
+ to its own `testID` values. On iOS the label overrides the visible text in
32
+ the accessibility tree, which made every text selector in the generated iOS
33
+ workflow scenario unmatchable against the generated app itself (and modeled
34
+ an accessibility antipattern — screen readers announced "demo_title" instead
35
+ of the title). Found while recording real demo footage; the fixture's text
36
+ waits now pass on iOS.
37
+
38
+ ### Added
39
+
40
+ - Added `scripts/record-demo-video.sh` (maintainer-only, npm-excluded): a
41
+ reproducible pipeline that records the launch-demo footage from the
42
+ generated Expo demo app — a passing workflow run, a copy-change break with
43
+ the `zmr explain` diagnosis, and the repaired green run — plus the
44
+ storyboard in `docs/demo-video-storyboard.md`.
45
+
7
46
  ## 0.2.1 (2026-06-10)
8
47
 
9
48
  ### Fixed
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.1`, a public developer preview rather than
145
+ - Current release status is `0.2.3`, 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
@@ -197,7 +197,7 @@ comparisons against your current E2E tool, and multi-device matrices, see
197
197
  | Cloud device farms | Not included | ZMR focuses on local and self-managed device targets in this preview |
198
198
 
199
199
  Slow CI hardware can extend the iOS shim cold-build timeout with
200
- `ZMR_IOS_SHIM_TIMEOUT_MS`. Current release: `0.2.1` developer preview.
200
+ `ZMR_IOS_SHIM_TIMEOUT_MS`. Current release: `0.2.3` developer preview.
201
201
  Protocol version: `2026-04-28`.
202
202
 
203
203
  ## Optional protocol clients
package/build.zig CHANGED
@@ -33,6 +33,16 @@ pub fn build(b: *std.Build) void {
33
33
  });
34
34
  const run_unit_tests = b.addRunArtifact(unit_tests);
35
35
 
36
+ const ios_tests = b.addTest(.{
37
+ .root_module = b.createModule(.{
38
+ .root_source_file = b.path("src/ios.zig"),
39
+ .target = target,
40
+ .optimize = optimize,
41
+ }),
42
+ });
43
+ const run_ios_tests = b.addRunArtifact(ios_tests);
44
+
36
45
  const test_step = b.step("test", "Run unit tests");
37
46
  test_step.dependOn(&run_unit_tests.step);
47
+ test_step.dependOn(&run_ios_tests.step);
38
48
  }
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.3",
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.1.jar"))
30
+ implementation(files("path/to/zeno-mobile-runner/clients/kotlin/build/libs/zmr-client-0.2.3.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.1"
7
+ version = "0.2.3"
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.1.dev1"
7
+ version = "0.2.3.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.1"
103
+ version = "0.2.3"
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.1"
3
+ version = "0.2.3"
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.1",
3
+ "version": "0.2.3",
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.1","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.3","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.1`.
5
+ Current runner version: `0.2.3`.
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.1","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.3","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.1","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.3","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.1","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.3","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.1","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.3","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.1","protocolVersion":"2026-04-28","minimumCompatibleProtocolVersion":"2026-04-28","stability":"dev-preview","breakingChangePolicy":"version-and-changelog"}
217
+ {"name":"zmr","version":"0.2.3","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.1","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.3","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.1","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.3","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.1","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.3","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.1","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.3","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`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zeno-mobile-runner",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
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]}
@@ -46,21 +46,23 @@ enum ZMRShim {
46
46
  }
47
47
 
48
48
  static func snapshot(app: XCUIApplication) -> [ZMRShimNode] {
49
- let queries: [(XCUIElement.ElementType, XCUIElementQuery)] = [
50
- (.button, app.buttons),
51
- (.staticText, app.staticTexts),
52
- (.textField, app.textFields),
53
- (.secureTextField, app.secureTextFields),
54
- (.textView, app.textViews),
55
- (.image, app.images),
56
- (.switch, app.switches),
57
- (.cell, app.cells),
58
- (.scrollView, app.scrollViews),
59
- (.table, app.tables),
60
- (.collectionView, app.collectionViews)
49
+ let types: [XCUIElement.ElementType] = [
50
+ .button,
51
+ .staticText,
52
+ .textField,
53
+ .secureTextField,
54
+ .textView,
55
+ .image,
56
+ .switch,
57
+ .cell,
58
+ .scrollView,
59
+ .table,
60
+ .collectionView
61
61
  ]
62
+ let queries = types.flatMap { type in snapshotQueries(app: app, type: type) }
62
63
 
63
64
  var nodes: [ZMRShimNode] = []
65
+ var seen = Set<String>()
64
66
  nodes.reserveCapacity(128)
65
67
 
66
68
  for (type, query) in queries {
@@ -71,6 +73,11 @@ enum ZMRShim {
71
73
  guard element.exists else {
72
74
  continue
73
75
  }
76
+ let key = elementKey(type: type, element: element)
77
+ guard !seen.contains(key) else {
78
+ continue
79
+ }
80
+ seen.insert(key)
74
81
  nodes.append(node(index: nodes.count, type: type, element: element))
75
82
  }
76
83
  }
@@ -81,6 +88,13 @@ enum ZMRShim {
81
88
  return nodes
82
89
  }
83
90
 
91
+ private static func snapshotQueries(app: XCUIApplication, type: XCUIElement.ElementType) -> [(XCUIElement.ElementType, XCUIElementQuery)] {
92
+ [
93
+ (type, app.windows.descendants(matching: type)),
94
+ (type, app.descendants(matching: type))
95
+ ]
96
+ }
97
+
84
98
  private static func node(index: Int, type: XCUIElement.ElementType, element: XCUIElement) -> ZMRShimNode {
85
99
  let frame = element.frame
86
100
  return ZMRShimNode(
@@ -120,4 +134,18 @@ enum ZMRShim {
120
134
  }
121
135
  return "index:\(index)"
122
136
  }
137
+
138
+ private static func elementKey(type: XCUIElement.ElementType, element: XCUIElement) -> String {
139
+ let frame = element.frame
140
+ return [
141
+ String(describing: type),
142
+ element.identifier,
143
+ element.label,
144
+ elementValue(element),
145
+ String(Int(frame.origin.x)),
146
+ String(Int(frame.origin.y)),
147
+ String(Int(frame.size.width)),
148
+ String(Int(frame.size.height))
149
+ ].joined(separator: "|")
150
+ }
123
151
  }