zeno-mobile-runner 0.2.0 → 0.2.1

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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,29 @@ All notable changes to Zeno Mobile Runner are tracked here.
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.2.1 (2026-06-10)
8
+
9
+ ### Fixed
10
+
11
+ - iOS simulator `openLink` now asks the XCTest shim to accept the SpringBoard
12
+ "Open in <App>?" confirmation for custom URL schemes too, not just
13
+ http/https universal links. Custom schemes are the common Expo dev-client
14
+ deep-link case (`exp+scheme://expo-development-client/...`), and the
15
+ unaccepted dialog previously blocked navigation entirely. The shim's
16
+ `acceptSystemAlert` also gained a single alert-existence probe so the
17
+ best-effort accept stays fast when no dialog appears.
18
+ - The generated Expo dev-client scenarios no longer pass when only the Expo
19
+ dev launcher rendered. The old `waitAny` markers also matched launcher
20
+ chrome ("Home", "Continue", "Sign in"), so runs exited green even though
21
+ the app's JS bundle never loaded. The scenarios now wait for the launcher's
22
+ persistent marker to be gone (`waitNotVisible` on "evelopment servers",
23
+ covering both case-sensitive spellings) — passing immediately when the deep
24
+ link navigates, and failing when the launcher is stuck — then assert no
25
+ bundle-error screen ("Unable to load" / "There was a problem loading") is
26
+ visible before `assertHealthy` and `snapshot`. Verified both directions
27
+ against a real Expo SDK 56 app: passes in ~24s with Metro serving, fails
28
+ with a wait timeout when the bundler is down.
29
+
7
30
  ## 0.2.0 (2026-06-10)
8
31
 
9
32
  ### 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.1`, 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.1` developer preview.
193
201
  Protocol version: `2026-04-28`.
194
202
 
195
203
  ## Optional protocol clients
@@ -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.1.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.1"
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.1.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.1"
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.1"
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.1",
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.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"]}}
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.1`.
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.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"]}
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.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"]}
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.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"]}
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.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"]}
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.1","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.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"]}
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.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"]}}
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.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"]}}
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.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"]}}
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.1",
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": {
Binary file
Binary file
Binary file
Binary file
@@ -241,10 +241,16 @@ final class ZMRShimUITestCase: XCTestCase {
241
241
  var acceptedCount = 0
242
242
  var lastAcceptedLabel = ""
243
243
  for _ in 0..<3 {
244
+ // One existence probe on the alert container keeps the no-dialog
245
+ // path to a single short wait instead of a per-label wait, so the
246
+ // best-effort accept after every openLink stays cheap.
247
+ guard springboard.alerts.firstMatch.waitForExistence(timeout: 2) else {
248
+ break
249
+ }
244
250
  var tapped = false
245
251
  for label in labels {
246
252
  let button = springboard.buttons[label].firstMatch
247
- if button.waitForExistence(timeout: 2), button.isHittable {
253
+ if button.exists, button.isHittable {
248
254
  button.tap()
249
255
  acceptedCount += 1
250
256
  lastAcceptedLabel = label
package/src/ios.zig CHANGED
@@ -128,9 +128,13 @@ pub const IosDevice = struct {
128
128
  const result = try self.runSimctl(&.{ "openurl", self.target(), url }, default_max_output);
129
129
  defer result.deinit(self.allocator);
130
130
  try result.ensureSuccess();
131
- if (urlMayNeedOpenConfirmation(url)) {
132
- self.acceptOpenURLConfirmationBestEffort();
133
- }
131
+ // Opening a URL on the simulator can raise a SpringBoard "Open in <App>?"
132
+ // confirmation for universal links (http/https) and, just as often, for
133
+ // custom schemes — the common Expo dev-client case
134
+ // (exp+scheme://expo-development-client/...). Attempt a best-effort accept
135
+ // whenever a shim is configured; the shim probes briefly and returns fast
136
+ // when no dialog is present, so this stays cheap on the no-prompt path.
137
+ self.acceptOpenURLConfirmationBestEffort();
134
138
  }
135
139
 
136
140
  pub fn tap(self: *IosDevice, x: i32, y: i32) !void {
@@ -406,14 +410,6 @@ fn parseShimTimeoutMs(raw: ?[]const u8) u64 {
406
410
  return parsed;
407
411
  }
408
412
 
409
- fn urlMayNeedOpenConfirmation(url: []const u8) bool {
410
- return startsWithIgnoreCase(url, "http://") or startsWithIgnoreCase(url, "https://");
411
- }
412
-
413
- fn startsWithIgnoreCase(value: []const u8, prefix: []const u8) bool {
414
- return value.len >= prefix.len and std.ascii.eqlIgnoreCase(value[0..prefix.len], prefix);
415
- }
416
-
417
413
  pub fn listDevices(allocator: std.mem.Allocator, xcrun_path: []const u8) ![]types.DeviceInfo {
418
414
  return try ios_devices.listSimulators(allocator, xcrun_path);
419
415
  }
package/src/main.zig CHANGED
@@ -16,6 +16,9 @@ const errors = @import("errors.zig");
16
16
 
17
17
  pub fn main() void {
18
18
  mainInner() catch |err| {
19
+ // stdout's consumer went away (e.g. `zmr ... | head`); exit quietly
20
+ // with the conventional SIGPIPE status instead of reporting an error.
21
+ if (err == error.BrokenPipe) std.process.exit(141);
19
22
  writeTopLevelError(err);
20
23
  std.process.exit(exitCodeForError(err));
21
24
  };
package/src/version.zig CHANGED
@@ -1,4 +1,4 @@
1
- pub const runner_version = "0.2.0";
1
+ pub const runner_version = "0.2.1";
2
2
  pub const protocol_version = "2026-04-28";
3
3
  pub const protocol_min_compatible_version = "2026-04-28";
4
4
  pub const protocol_stability = "dev-preview";