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.
- package/CHANGELOG.md +52 -0
- package/FEATURES.md +1 -1
- package/README.md +9 -1
- package/build.zig.zon +2 -2
- package/clients/kotlin/README.md +1 -1
- package/clients/kotlin/build.gradle.kts +1 -1
- package/clients/python/pyproject.toml +1 -1
- package/clients/rust/Cargo.lock +1 -1
- package/clients/rust/Cargo.toml +1 -1
- package/clients/typescript/package.json +1 -1
- package/docs/protocol-fixtures/core-session.responses.jsonl +1 -1
- package/docs/protocol.md +10 -10
- package/examples/ios-dev-client-open-link.json +24 -13
- package/examples/ios-dev-client-route-snapshot.json +33 -8
- package/npm/scenarios.mjs +15 -8
- package/npm/wizard.mjs +1 -1
- package/package.json +3 -1
- package/prebuilds/darwin-arm64/zmr +0 -0
- package/prebuilds/darwin-x64/zmr +0 -0
- package/prebuilds/linux-arm64/zmr +0 -0
- package/prebuilds/linux-x64/zmr +0 -0
- package/scripts/create-react-native-expo-demo-app.sh +11 -13
- package/shims/ios/ZMRShim.swift +40 -12
- package/shims/ios/ZMRShimUITestCase.swift +142 -16
- package/src/android.zig +10 -9
- package/src/android_emulator.zig +22 -11
- package/src/android_screen_recording.zig +11 -7
- package/src/bundle.zig +10 -9
- package/src/bundle_redaction.zig +29 -28
- package/src/bundle_tar.zig +15 -12
- package/src/cli_devices.zig +7 -3
- package/src/cli_discover.zig +7 -3
- package/src/cli_doctor.zig +7 -3
- package/src/cli_draft.zig +51 -47
- package/src/cli_explore.zig +7 -3
- package/src/cli_import.zig +8 -4
- package/src/cli_info.zig +13 -6
- package/src/cli_init.zig +9 -5
- package/src/cli_inspect.zig +8 -4
- package/src/cli_run.zig +22 -16
- package/src/cli_serve.zig +3 -3
- package/src/cli_trace.zig +25 -12
- package/src/cli_validate.zig +8 -4
- package/src/command.zig +81 -99
- package/src/config.zig +2 -1
- package/src/config_diagnostics.zig +2 -1
- package/src/config_paths.zig +2 -1
- package/src/doctor.zig +8 -7
- package/src/doctor_hints.zig +1 -1
- package/src/errors.zig +5 -5
- package/src/importer.zig +8 -7
- package/src/ios.zig +26 -29
- package/src/ios_devices.zig +6 -5
- package/src/ios_lifecycle.zig +4 -4
- package/src/json_rpc.zig +39 -40
- package/src/json_rpc_methods.zig +8 -8
- package/src/json_rpc_observation.zig +9 -8
- package/src/json_rpc_params.zig +1 -1
- package/src/json_rpc_trace.zig +22 -21
- package/src/main.zig +22 -10
- package/src/mcp.zig +28 -19
- package/src/mcp_trace.zig +30 -29
- package/src/report.zig +39 -36
- package/src/report_html.zig +5 -4
- package/src/runner.zig +2 -1
- package/src/runner_actions.zig +20 -17
- package/src/runner_diagnostics.zig +4 -4
- package/src/runner_events.zig +55 -51
- package/src/runner_native.zig +21 -19
- package/src/runner_waits.zig +46 -41
- package/src/scaffold.zig +25 -24
- package/src/scenario.zig +4 -3
- package/src/stdio.zig +129 -0
- package/src/trace.zig +34 -26
- package/src/trace_summary.zig +3 -2
- package/src/trace_summary_diagnostic.zig +15 -13
- package/src/validation.zig +5 -4
- 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.
|
|
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.
|
|
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
package/clients/kotlin/README.md
CHANGED
|
@@ -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.
|
|
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
|
package/clients/rust/Cargo.lock
CHANGED
package/clients/rust/Cargo.toml
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
{"jsonrpc":"2.0","id":1,"result":{"name":"zmr","version":"0.2.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
{
|
|
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": "
|
|
12
|
-
"
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
{
|
|
24
|
-
|
|
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
|
-
{
|
|
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
|
-
{
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
{
|
|
17
|
-
|
|
22
|
+
{
|
|
23
|
+
"textContains": "Settings"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"textContains": "Manage"
|
|
27
|
+
}
|
|
18
28
|
],
|
|
19
29
|
"timeoutMs": 60000
|
|
20
30
|
},
|
|
21
|
-
{
|
|
22
|
-
|
|
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: "
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zeno-mobile-runner",
|
|
3
|
-
"version": "0.2.
|
|
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
|
package/prebuilds/darwin-x64/zmr
CHANGED
|
Binary file
|
|
Binary file
|
package/prebuilds/linux-x64/zmr
CHANGED
|
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"
|
|
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"
|
|
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
|
-
|
|
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
|
-
|
|
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"
|
|
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
|
-
|
|
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"
|
|
317
|
+
<Text style={styles.heading} testID="detail_title">
|
|
319
318
|
{selectedItem.title}
|
|
320
319
|
</Text>
|
|
321
|
-
<Text style={styles.copy} testID="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"
|
|
336
|
+
<Text style={styles.heading} testID="review_title">
|
|
338
337
|
Review
|
|
339
338
|
</Text>
|
|
340
|
-
<Text style={styles.copy} testID="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"
|
|
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]}
|