zeno-mobile-runner 0.1.3 → 0.2.0
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 +192 -2
- package/FEATURES.md +50 -7
- package/README.md +168 -120
- package/build.zig.zon +3 -3
- package/clients/README.md +60 -3
- package/clients/go/README.md +12 -0
- package/clients/go/zmr/client.go +142 -0
- package/clients/kotlin/README.md +18 -1
- package/clients/kotlin/build.gradle.kts +1 -1
- package/clients/kotlin/src/main/kotlin/dev/zmr/ZmrClient.kt +76 -1
- package/clients/python/README.md +19 -0
- package/clients/python/pyproject.toml +1 -1
- package/clients/python/zmr_client.py +33 -0
- package/clients/rust/Cargo.lock +1 -1
- package/clients/rust/Cargo.toml +1 -1
- package/clients/rust/README.md +25 -1
- package/clients/rust/src/lib.rs +201 -0
- package/clients/swift/README.md +18 -0
- package/clients/swift/Sources/ZMRClient/ZMRClient.swift +82 -0
- package/clients/typescript/README.md +16 -0
- package/clients/typescript/index.d.ts +12 -0
- package/clients/typescript/index.mjs +16 -0
- package/clients/typescript/package.json +1 -1
- package/docs/agent-discovery.md +151 -22
- package/docs/ai-agents.md +99 -11
- package/docs/benchmarking.md +49 -3
- package/docs/benchmarks/2026-06-09-android-workflow.md +73 -0
- package/docs/benchmarks/2026-06-09-android-workflow.results.jsonl +20 -0
- package/docs/benchmarks/2026-06-09-framework-baseline-status.md +32 -0
- package/docs/benchmarks/2026-06-09-ios-appium-comparison.md +115 -0
- package/docs/benchmarks/2026-06-09-ios-appium-comparison.results.jsonl +40 -0
- package/docs/benchmarks/2026-06-09-ios-demo.md +90 -0
- package/docs/benchmarks/2026-06-09-ios-demo.results.jsonl +20 -0
- package/docs/benchmarks/2026-06-09-ios-maestro-comparison.md +128 -0
- package/docs/benchmarks/2026-06-09-ios-maestro-comparison.results.jsonl +40 -0
- package/docs/benchmarks/2026-06-09-ios-workflow-comparison.md +143 -0
- package/docs/benchmarks/2026-06-09-ios-workflow-comparison.results.jsonl +40 -0
- package/docs/benchmarks/2026-06-09-ios-xctest-floor.md +106 -0
- package/docs/benchmarks/2026-06-09-ios-xctest-floor.results.jsonl +40 -0
- package/docs/benchmarks/README.md +36 -0
- package/docs/benchmarks/benchmark-lab-v1.json +155 -0
- package/docs/benchmarks/benchmark-lab-v1.md +95 -0
- package/docs/clients.md +26 -6
- package/docs/demo.md +40 -1
- package/docs/expo-smoke.md +8 -8
- package/docs/frameworks.md +10 -0
- package/docs/install.md +3 -2
- package/docs/npm.md +100 -4
- package/docs/production-readiness.md +123 -0
- package/docs/protocol-fixtures/core-session.responses.jsonl +1 -1
- package/docs/protocol.md +215 -16
- package/docs/scenario-authoring.md +18 -0
- package/docs/trace-privacy.md +9 -0
- package/docs/troubleshooting.md +7 -1
- package/examples/android-workflow.json +79 -0
- package/examples/ios-shim-workflow.json +79 -0
- package/examples/react-native-expo-workflow.json +75 -0
- package/npm/agents.mjs +16 -0
- package/npm/commands.mjs +9 -5
- package/package.json +6 -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/schemas/README.md +4 -0
- package/schemas/discover-output.schema.json +83 -0
- package/schemas/draft-output.schema.json +58 -0
- package/schemas/explore-output.schema.json +94 -0
- package/schemas/inspect-output.schema.json +88 -0
- package/schemas/run-output.schema.json +2 -0
- package/scripts/benchmark-lab.py +253 -0
- package/scripts/create-android-demo-app.sh +324 -29
- package/scripts/create-ios-demo-app.sh +174 -7
- package/scripts/create-react-native-expo-demo-app.sh +727 -0
- package/scripts/demo.sh +3 -0
- package/scripts/install-ios-shim.sh +2 -2
- package/scripts/release-readiness.py +43 -0
- package/scripts/run-android-pilot.sh +35 -9
- package/scripts/run-ios-pilot.sh +11 -4
- package/shims/ios/ZMRShim.swift +10 -0
- package/shims/ios/ZMRShimUITestCase.swift +42 -0
- package/shims/ios/protocol.md +1 -0
- package/skills/zmr-mobile-testing/SKILL.md +28 -3
- package/src/cli_discover.zig +239 -0
- package/src/cli_draft.zig +924 -0
- package/src/cli_explore.zig +136 -0
- package/src/cli_import.zig +31 -15
- package/src/cli_inspect.zig +310 -0
- package/src/cli_output.zig +26 -2
- package/src/cli_run.zig +28 -0
- package/src/cli_trace.zig +45 -15
- package/src/cli_validate.zig +12 -6
- package/src/errors.zig +9 -0
- package/src/ios.zig +49 -12
- package/src/ios_shim.zig +36 -2
- package/src/json_rpc_methods.zig +85 -11
- package/src/json_rpc_params.zig +8 -0
- package/src/json_rpc_protocol.zig +1 -1
- package/src/json_rpc_trace.zig +112 -0
- package/src/main.zig +27 -2
- package/src/mcp.zig +209 -6
- package/src/mcp_protocol.zig +29 -1
- package/src/mcp_trace.zig +126 -4
- package/src/report.zig +186 -0
- package/src/runner.zig +26 -4
- package/src/runner_actions.zig +10 -0
- package/src/runner_diagnostics.zig +31 -1
- package/src/runner_events.zig +70 -7
- package/src/runner_native.zig +17 -1
- package/src/runner_waits.zig +82 -19
- package/src/scaffold.zig +28 -12
- package/src/scenario.zig +32 -4
- package/src/schema_registry.zig +4 -0
- package/src/version.zig +1 -1
- package/viewer/app.js +23 -3
package/README.md
CHANGED
|
@@ -1,182 +1,230 @@
|
|
|
1
1
|
# Zeno Mobile Runner
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> The verification loop for AI coding agents building Expo, React Native,
|
|
4
|
+
> Flutter, and native Android/iOS apps.
|
|
4
5
|
|
|
5
6
|
[](https://github.com/johnmikel/zeno-mobile-runner/actions/workflows/ci.yml)
|
|
6
7
|
[](https://github.com/johnmikel/zeno-mobile-runner/releases)
|
|
7
8
|
[](https://www.npmjs.com/package/zeno-mobile-runner)
|
|
8
9
|
[](LICENSE)
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
Your coding agent can write mobile code, but it cannot see the phone. ZMR is
|
|
12
|
+
its eyes and hands: a typed mobile control plane that installs and launches
|
|
13
|
+
apps, observes the UI, taps and types, waits for the screen to settle, asserts
|
|
14
|
+
state, and exports a replayable trace as proof. The runner does not embed an
|
|
15
|
+
LLM. Agents stay outside and drive ZMR through MCP, JSON-RPC, CLI JSON, or
|
|
16
|
+
JSON scenarios.
|
|
17
|
+
|
|
18
|
+

|
|
19
|
+
|
|
20
|
+
<p align="center">
|
|
21
|
+
<img src="docs/assets/device-ios-demo.png" width="260" alt="iOS simulator screenshot captured by ZMR during a scenario run" />
|
|
22
|
+
|
|
23
|
+
<img src="docs/assets/device-android-demo.png" width="260" alt="Android emulator screenshot captured by ZMR during a scenario run" />
|
|
24
|
+
</p>
|
|
25
|
+
|
|
26
|
+
<p align="center"><em>Real on-device screenshots from ZMR traces: the same demo flow
|
|
27
|
+
driven on an iOS simulator and an Android emulator.</em></p>
|
|
28
|
+
|
|
29
|
+
## Why agents need this
|
|
30
|
+
|
|
31
|
+
- **Agents can't verify what they can't observe.** ZMR returns semantic UI
|
|
32
|
+
trees with stable selectors, screenshots, and typed action results an agent
|
|
33
|
+
can reason about — not raw pixels it has to guess at.
|
|
34
|
+
- **Evidence, not vibes.** Every session can write a deterministic trace:
|
|
35
|
+
events, screenshots, UI hierarchies, timings, assertion results, HTML and
|
|
36
|
+
JUnit reports, and a redacted shareable bundle.
|
|
37
|
+
- **Tests fall out for free.** After a live agent session, `zmr discover`
|
|
38
|
+
turns the trace into a reviewable JSON scenario that replays in CI without
|
|
39
|
+
an LLM in the loop.
|
|
40
|
+
|
|
41
|
+
## How it works
|
|
42
|
+
|
|
43
|
+
```mermaid
|
|
44
|
+
flowchart LR
|
|
45
|
+
A["AI coding agent<br/>Claude Code · Cursor · custom harness"]
|
|
46
|
+
subgraph zmr["ZMR — one small Zig binary"]
|
|
47
|
+
MCP["MCP server<br/><code>zmr mcp</code>"]
|
|
48
|
+
RPC["JSON-RPC stdio/TCP<br/><code>zmr serve</code>"]
|
|
49
|
+
CLI["CLI + JSON scenarios<br/><code>zmr run</code>"]
|
|
50
|
+
CORE["Core engine<br/>selectors · waits · assertions<br/>scenario runner · trace writer"]
|
|
51
|
+
MCP --> CORE
|
|
52
|
+
RPC --> CORE
|
|
53
|
+
CLI --> CORE
|
|
54
|
+
end
|
|
55
|
+
subgraph devices["Devices"]
|
|
56
|
+
AND["Android emulator/device<br/>ADB · UI Automator · optional shim"]
|
|
57
|
+
IOS["iOS simulator/device<br/>simctl · devicectl · XCTest shim"]
|
|
58
|
+
end
|
|
59
|
+
TRACE["Trace<br/>events.jsonl · screenshots · UI trees<br/>report.html · junit.xml · .zmrtrace"]
|
|
60
|
+
A -- "MCP tools" --> MCP
|
|
61
|
+
A -- "JSON-RPC" --> RPC
|
|
62
|
+
A -- "CLI JSON" --> CLI
|
|
63
|
+
CORE --> AND
|
|
64
|
+
CORE --> IOS
|
|
65
|
+
CORE --> TRACE
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
No app instrumentation is required on Android. iOS selector actions use an
|
|
69
|
+
app-local XCTest shim that the wizard scaffolds. ZMR works below the
|
|
70
|
+
JavaScript/Dart layer, so React Native, Expo, Flutter, and fully native apps
|
|
71
|
+
are all driven the same way. See [docs/frameworks.md](docs/frameworks.md).
|
|
15
72
|
|
|
16
|
-
##
|
|
73
|
+
## Five-minute start
|
|
17
74
|
|
|
18
75
|
Inside a mobile app repo:
|
|
19
76
|
|
|
20
77
|
```bash
|
|
21
|
-
npm install --save-dev zeno-mobile-runner
|
|
78
|
+
npm install --save-dev zeno-mobile-runner # bun add --dev zeno-mobile-runner
|
|
22
79
|
npx zmr-wizard --app-id com.example.mobiletest --package-json
|
|
23
80
|
npx zmr doctor --strict --json --config .zmr/config.json
|
|
24
81
|
```
|
|
25
82
|
|
|
26
|
-
|
|
83
|
+
Hook it up to your coding agent (Claude Code shown; any MCP client works):
|
|
27
84
|
|
|
28
85
|
```bash
|
|
29
|
-
|
|
30
|
-
npm run zmr:android
|
|
31
|
-
npm run zmr:ios
|
|
86
|
+
claude mcp add zmr -- npx zmr mcp --config .zmr/config.json --trace-dir traces/zmr-agent
|
|
32
87
|
```
|
|
33
88
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
89
|
+
Or in an `.mcp.json` / MCP client config:
|
|
90
|
+
|
|
91
|
+
```json
|
|
92
|
+
{
|
|
93
|
+
"mcpServers": {
|
|
94
|
+
"zmr": {
|
|
95
|
+
"command": "npx",
|
|
96
|
+
"args": ["zmr", "mcp", "--config", ".zmr/config.json", "--trace-dir", "traces/zmr-agent"]
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Then ask the agent to verify its own work: *"launch the app, walk through
|
|
103
|
+
onboarding, and show me the trace."*
|
|
104
|
+
|
|
105
|
+
## The agent verification loop
|
|
106
|
+
|
|
107
|
+
```mermaid
|
|
108
|
+
sequenceDiagram
|
|
109
|
+
participant Agent as AI agent
|
|
110
|
+
participant ZMR
|
|
111
|
+
participant Device as Emulator / simulator
|
|
112
|
+
Agent->>ZMR: semantic_snapshot
|
|
113
|
+
ZMR->>Device: capture UI + screenshot
|
|
114
|
+
ZMR-->>Agent: roles, stable selectors, bounds
|
|
115
|
+
Agent->>ZMR: tap / type / swipe / open_link
|
|
116
|
+
ZMR->>Device: execute + settle
|
|
117
|
+
Agent->>ZMR: wait_visible / assert_visible
|
|
118
|
+
ZMR-->>Agent: typed result + trace events
|
|
119
|
+
Agent->>ZMR: trace_discover
|
|
120
|
+
ZMR-->>Agent: reviewable replay scenario
|
|
121
|
+
Agent->>ZMR: trace_export --redact
|
|
122
|
+
ZMR-->>Agent: .zmrtrace evidence bundle
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
The MCP server exposes the full loop as mobile-native tools:
|
|
126
|
+
|
|
127
|
+
| Group | Tools |
|
|
128
|
+
| --- | --- |
|
|
129
|
+
| Observe | `snapshot`, `semantic_snapshot` |
|
|
130
|
+
| App lifecycle | `install_app`, `launch_app`, `stop_app`, `clear_state`, `open_link` |
|
|
131
|
+
| Act | `tap`, `type`, `erase_text`, `hide_keyboard`, `swipe`, `press_back` |
|
|
132
|
+
| Wait | `wait_visible`, `wait_not_visible`, `wait_any`, `scroll_until_visible` |
|
|
133
|
+
| Assert | `assert_visible`, `assert_not_visible`, `assert_healthy` |
|
|
134
|
+
| Evidence | `trace_events`, `trace_explain`, `trace_discover`, `trace_explore`, `trace_export`, `scenario_validate` |
|
|
135
|
+
|
|
136
|
+
The same surface is available over JSON-RPC for harnesses that embed ZMR
|
|
137
|
+
directly — see [docs/protocol.md](docs/protocol.md) and
|
|
138
|
+
[docs/ai-agents.md](docs/ai-agents.md). When a run fails, `zmr explain`
|
|
139
|
+
diagnoses the trace for humans and agents alike:
|
|
140
|
+
|
|
141
|
+

|
|
142
|
+
|
|
143
|
+
## Deterministic scenarios for CI
|
|
144
|
+
|
|
145
|
+
Scenarios are plain JSON — agents and build scripts generate, validate, and
|
|
146
|
+
mutate them without a second DSL, and they replay in CI with no LLM cost:
|
|
72
147
|
|
|
73
148
|
```json
|
|
74
149
|
{
|
|
75
150
|
"name": "Login smoke",
|
|
76
151
|
"appId": "com.example.mobiletest",
|
|
77
152
|
"steps": [
|
|
153
|
+
{ "action": "clearState" },
|
|
78
154
|
{ "action": "launch" },
|
|
79
155
|
{ "action": "assertHealthy", "timeoutMs": 5000 },
|
|
80
156
|
{ "action": "tap", "selector": { "resourceId": "email" } },
|
|
81
157
|
{ "action": "typeText", "text": "user@example.com" },
|
|
82
|
-
{ "action": "tap", "selector": { "resourceId": "password" } },
|
|
83
|
-
{ "action": "typeText", "text": "password" },
|
|
84
158
|
{ "action": "tap", "selector": { "text": "Login" } },
|
|
85
159
|
{ "action": "waitVisible", "selector": { "text": "Welcome" }, "timeoutMs": 30000 }
|
|
86
160
|
]
|
|
87
161
|
}
|
|
88
162
|
```
|
|
89
163
|
|
|
90
|
-
Useful commands:
|
|
91
|
-
|
|
92
164
|
```bash
|
|
93
|
-
zmr version --json
|
|
94
|
-
zmr schemas --json
|
|
95
|
-
zmr devices --json
|
|
96
|
-
zmr init --app --json --dir . --app-id com.example.mobiletest
|
|
97
165
|
zmr validate --json .zmr/login-smoke.json
|
|
98
166
|
zmr run .zmr/login-smoke.json --json --trace-dir traces/login-smoke
|
|
99
|
-
zmr
|
|
100
|
-
zmr
|
|
101
|
-
zmr export traces/login-smoke --out traces/login-smoke-redacted.zmrtrace --redact
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
See [docs/scenario-authoring.md](docs/scenario-authoring.md) for selector and
|
|
105
|
-
wait guidance.
|
|
106
|
-
|
|
107
|
-
## Agent Workflow
|
|
108
|
-
|
|
109
|
-
Agents can use the CLI, JSON-RPC, or MCP surface. Start JSON-RPC over stdio:
|
|
110
|
-
|
|
111
|
-
```bash
|
|
112
|
-
zmr serve --transport stdio --config .zmr/config.json --trace-dir traces/zmr-agent
|
|
167
|
+
zmr report traces/login-smoke --out traces/login-smoke/report.html --junit traces/login-smoke/junit.xml
|
|
168
|
+
zmr export traces/login-smoke --out login-smoke-redacted.zmrtrace --redact
|
|
113
169
|
```
|
|
114
170
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
```
|
|
171
|
+
Traced `zmr run --json` responses include executable `nextCommands` so agents
|
|
172
|
+
can continue to reporting, explanation, discovery, or export without guessing.
|
|
173
|
+
Open any exported bundle in the static [trace viewer](viewer/index.html) — or
|
|
174
|
+
serve it and link straight to it with `viewer/index.html?bundle=<url>`.
|
|
120
175
|
|
|
121
|
-
|
|
122
|
-
|
|
176
|
+
For repeat-run reliability gates, p95 duration thresholds, baseline
|
|
177
|
+
comparisons against your current E2E tool, and multi-device matrices, see
|
|
178
|
+
[docs/benchmarking.md](docs/benchmarking.md) and the public
|
|
179
|
+
[Benchmark Lab](docs/benchmarks/README.md) evidence.
|
|
123
180
|
|
|
124
|
-
|
|
125
|
-
[docs/agent-discovery.md](docs/agent-discovery.md). ZMR supports that loop
|
|
126
|
-
through MCP and JSON-RPC today; a built-in autonomous crawler is not shipped in
|
|
127
|
-
this preview.
|
|
128
|
-
|
|
129
|
-
## Optional Protocol Clients
|
|
130
|
-
|
|
131
|
-
Clients are thin wrappers around `zmr serve --transport stdio`. They do not
|
|
132
|
-
replace the runner; they make it easier for agents and test code to call the
|
|
133
|
-
same JSON-RPC protocol.
|
|
134
|
-
|
|
135
|
-
TypeScript and Python are the most common starting points for app teams and
|
|
136
|
-
agent harnesses. Go, Rust, Swift, and Kotlin clients are reference integrations
|
|
137
|
-
for teams that want to embed the protocol from those ecosystems.
|
|
138
|
-
|
|
139
|
-
| Language | Entry point | Example |
|
|
140
|
-
| --- | --- | --- |
|
|
141
|
-
| TypeScript | `clients/typescript/index.mjs` + `index.d.ts` | `node clients/typescript/examples/fake-session.mjs` |
|
|
142
|
-
| Python | `clients/python/zmr_client.py` + `pyproject.toml` | `python3 clients/python/examples/fake_session.py` |
|
|
143
|
-
| Go | `clients/go/zmr/client.go` | `go run ./clients/go/examples/fake-session` |
|
|
144
|
-
| Rust | `clients/rust/src/lib.rs` | `cargo run --manifest-path clients/rust/Cargo.toml --example fake_session` |
|
|
145
|
-
| Swift | `clients/swift/Sources/ZMRClient` | `swift build --package-path clients/swift` |
|
|
146
|
-
| Kotlin | `clients/kotlin/src/main/kotlin/dev/zmr` | `gradle -p clients/kotlin build` |
|
|
147
|
-
|
|
148
|
-
See [clients/README.md](clients/README.md), [docs/clients.md](docs/clients.md),
|
|
149
|
-
and [docs/client-installation.md](docs/client-installation.md).
|
|
150
|
-
|
|
151
|
-
## Platform Support
|
|
181
|
+
## Platform support
|
|
152
182
|
|
|
153
183
|
| Target | Status | Notes |
|
|
154
184
|
| --- | --- | --- |
|
|
155
185
|
| Android emulator | Supported | ADB/UI Automator, optional Android shim, emulator lifecycle helpers |
|
|
156
186
|
| Android physical device | Supported | Requires ADB connection and app build/install surface |
|
|
157
|
-
| iOS simulator | Supported | `simctl` plus app-local XCTest/XCUIAutomation shim for native selector actions
|
|
158
|
-
| iOS physical device | Supported, validate locally | `devicectl` lifecycle plus
|
|
159
|
-
| Cloud device farms | Not included | ZMR
|
|
187
|
+
| iOS simulator | Supported | `simctl` plus app-local XCTest/XCUIAutomation shim for native selector actions |
|
|
188
|
+
| iOS physical device | Supported, validate locally | `devicectl` lifecycle plus XCTest shim; pilot on your own app/device before relying on it in CI |
|
|
189
|
+
| Cloud device farms | Not included | ZMR focuses on local and self-managed device targets in this preview |
|
|
190
|
+
|
|
191
|
+
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.
|
|
193
|
+
Protocol version: `2026-04-28`.
|
|
194
|
+
|
|
195
|
+
## Optional protocol clients
|
|
160
196
|
|
|
161
|
-
|
|
162
|
-
|
|
197
|
+
TypeScript and Python clients are the common starting points; Go, Rust, Swift,
|
|
198
|
+
and Kotlin reference clients embed the same JSON-RPC protocol from those
|
|
199
|
+
ecosystems. All are thin wrappers around `zmr serve --transport stdio`. See
|
|
200
|
+
[docs/clients.md](docs/clients.md) and
|
|
201
|
+
[docs/client-installation.md](docs/client-installation.md).
|
|
163
202
|
|
|
164
203
|
## Documentation
|
|
165
204
|
|
|
166
|
-
|
|
205
|
+
**For agents**
|
|
206
|
+
|
|
207
|
+
- [docs/ai-agents.md](docs/ai-agents.md): JSON-RPC and MCP agent workflows
|
|
208
|
+
- [docs/agent-discovery.md](docs/agent-discovery.md): agent-led discovery, `zmr explore`/`discover`/`draft`, and the trace-to-test loop
|
|
209
|
+
- [skills/zmr-mobile-testing/SKILL.md](skills/zmr-mobile-testing/SKILL.md): reusable agent skill
|
|
210
|
+
|
|
211
|
+
**For test authors**
|
|
212
|
+
|
|
167
213
|
- [docs/install.md](docs/install.md): source, npm, Homebrew, and app setup
|
|
168
214
|
- [docs/frameworks.md](docs/frameworks.md): React Native, Expo, Flutter, and native app guidance
|
|
169
|
-
- [docs/expo-smoke.md](docs/expo-smoke.md): reproducible Expo and iOS smoke test
|
|
170
|
-
- [docs/app-integration.md](docs/app-integration.md): app-side Android/iOS shims
|
|
171
215
|
- [docs/scenario-authoring.md](docs/scenario-authoring.md): selectors, waits, and scenario design
|
|
172
|
-
- [docs/
|
|
216
|
+
- [docs/app-integration.md](docs/app-integration.md): app-side Android/iOS shims
|
|
217
|
+
- [docs/expo-smoke.md](docs/expo-smoke.md): reproducible Expo and iOS smoke test
|
|
218
|
+
- [docs/benchmarking.md](docs/benchmarking.md): repeat-run gates, reports, device matrix, baselines
|
|
219
|
+
|
|
220
|
+
**Reference**
|
|
221
|
+
|
|
222
|
+
- [FEATURES.md](FEATURES.md): complete feature list and limitations
|
|
173
223
|
- [docs/protocol.md](docs/protocol.md): JSON-RPC methods and schemas
|
|
174
|
-
- [docs/ai-agents.md](docs/ai-agents.md): JSON-RPC and MCP agent workflows
|
|
175
|
-
- [docs/clients.md](docs/clients.md): language client guide
|
|
176
|
-
- [docs/client-installation.md](docs/client-installation.md): npm, Homebrew, TS, Python, Go, Rust, Swift, and Kotlin setup
|
|
177
224
|
- [docs/trace-privacy.md](docs/trace-privacy.md): safe trace export
|
|
225
|
+
- [docs/production-readiness.md](docs/production-readiness.md): release, reliability, and agent-readiness gates
|
|
178
226
|
- [docs/troubleshooting.md](docs/troubleshooting.md): common setup and runtime issues
|
|
179
|
-
- [
|
|
227
|
+
- [docs/benchmarks](docs/benchmarks/README.md): public-safe benchmark evidence
|
|
180
228
|
|
|
181
229
|
## License
|
|
182
230
|
|
package/build.zig.zon
CHANGED
package/clients/README.md
CHANGED
|
@@ -19,9 +19,12 @@ actions instead of raw platform hierarchy classes.
|
|
|
19
19
|
The TypeScript and Python clients expose the broadest app-facing control
|
|
20
20
|
surface: session lifecycle, app launch/stop/link/state, snapshot and semantic
|
|
21
21
|
snapshot, tap/type/erase/hide-keyboard/swipe/back/scroll, waits, assertions,
|
|
22
|
-
trace event polling,
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
scenario validation, trace event polling, trace explanation, trace discovery,
|
|
23
|
+
trace exploration, and trace export.
|
|
24
|
+
The Go and Rust clients also include typed scenario validation and trace
|
|
25
|
+
explanation/exploration/discovery helpers. Swift and Kotlin include lightweight
|
|
26
|
+
trace explanation, validation, exploration, and discovery helpers for
|
|
27
|
+
host-side agents in those ecosystems.
|
|
25
28
|
Use the `assertHealthy`/`assert_healthy` helper after launches, links, and major
|
|
26
29
|
navigation steps to catch native crash overlays and development-client failures
|
|
27
30
|
without hand-maintaining negative selectors in every client.
|
|
@@ -45,6 +48,7 @@ const zmr = createZmrClient({
|
|
|
45
48
|
command: "zmr",
|
|
46
49
|
args: ["serve", "--transport", "stdio", "--config", ".zmr/config.json"],
|
|
47
50
|
});
|
|
51
|
+
const explored = await zmr.exploreTrace(".zmr/discovered/agent-goal.json", "find a stable login smoke", { includeActions: true, validate: true, force: true });
|
|
48
52
|
```
|
|
49
53
|
|
|
50
54
|
## Python
|
|
@@ -64,6 +68,8 @@ from zmr_client import ZmrClient
|
|
|
64
68
|
|
|
65
69
|
with ZmrClient("zmr", ["serve", "--transport", "stdio", "--config", ".zmr/config.json"]) as zmr:
|
|
66
70
|
snapshot = zmr.snapshot()
|
|
71
|
+
explanation = zmr.explain_trace()
|
|
72
|
+
explored = zmr.explore_trace(".zmr/discovered/agent-goal.json", "find a stable login smoke", include_actions=True, validate=True, force=True)
|
|
67
73
|
```
|
|
68
74
|
|
|
69
75
|
## Go
|
|
@@ -83,6 +89,10 @@ go run ./clients/go/examples/fake-session \
|
|
|
83
89
|
|
|
84
90
|
```go
|
|
85
91
|
client, err := zmr.Start(ctx, "zmr", "serve", "--transport", "stdio", "--config", ".zmr/config.json")
|
|
92
|
+
discovered, err := client.DiscoverTrace(ctx, ".zmr/discovered/go-agent.json", zmr.TraceDiscoverOptions{IncludeActions: true, Validate: true, Force: true})
|
|
93
|
+
explored, err := client.ExploreTrace(ctx, ".zmr/discovered/go-goal.json", "find a stable login smoke", zmr.TraceDiscoverOptions{IncludeActions: true, Validate: true, Force: true})
|
|
94
|
+
validation, err := client.ValidateScenario(ctx, discovered.Out)
|
|
95
|
+
explanation, err := client.ExplainTrace(ctx)
|
|
86
96
|
```
|
|
87
97
|
|
|
88
98
|
## Rust
|
|
@@ -111,6 +121,20 @@ cargo run --manifest-path clients/rust/Cargo.toml --example fake_session -- \
|
|
|
111
121
|
```rust
|
|
112
122
|
let mut client = zmr_client::Client::start("zmr", ["serve", "--transport", "stdio", "--config", ".zmr/config.json"])?;
|
|
113
123
|
let snapshot = client.snapshot()?;
|
|
124
|
+
let discovered = client.discover_trace(".zmr/discovered/rust-agent.json", zmr_client::TraceDiscoverOptions {
|
|
125
|
+
include_actions: true,
|
|
126
|
+
validate: true,
|
|
127
|
+
force: true,
|
|
128
|
+
..Default::default()
|
|
129
|
+
})?;
|
|
130
|
+
let explored = client.explore_trace(".zmr/discovered/rust-goal.json", "find a stable login smoke", zmr_client::TraceDiscoverOptions {
|
|
131
|
+
include_actions: true,
|
|
132
|
+
validate: true,
|
|
133
|
+
force: true,
|
|
134
|
+
..Default::default()
|
|
135
|
+
})?;
|
|
136
|
+
let validation = client.validate_scenario(&discovered.out)?;
|
|
137
|
+
let explanation = client.explain_trace()?;
|
|
114
138
|
```
|
|
115
139
|
|
|
116
140
|
## Swift
|
|
@@ -128,6 +152,24 @@ git submodule add https://github.com/johnmikel/zeno-mobile-runner.git vendor/zen
|
|
|
128
152
|
.package(path: "vendor/zeno-mobile-runner/clients/swift")
|
|
129
153
|
```
|
|
130
154
|
|
|
155
|
+
```swift
|
|
156
|
+
let client = ZMRClient(arguments: ["serve", "--transport", "stdio", "--config", ".zmr/config.json"])
|
|
157
|
+
try client.start()
|
|
158
|
+
let out = ".zmr/discovered/swift-agent.json"
|
|
159
|
+
let discovered = try client.discoverTrace(
|
|
160
|
+
out: out,
|
|
161
|
+
options: TraceDiscoverOptions(includeActions: true, validate: true, force: true)
|
|
162
|
+
)
|
|
163
|
+
let explored = try client.exploreTrace(
|
|
164
|
+
out: ".zmr/discovered/swift-goal.json",
|
|
165
|
+
goal: "find a stable login smoke",
|
|
166
|
+
options: TraceDiscoverOptions(includeActions: true, validate: true, force: true)
|
|
167
|
+
)
|
|
168
|
+
let validation = try client.validateScenario(path: out)
|
|
169
|
+
let explanation = try client.explainTrace()
|
|
170
|
+
client.close()
|
|
171
|
+
```
|
|
172
|
+
|
|
131
173
|
Swift is useful for macOS host-side automation next to iOS app code. It is not
|
|
132
174
|
an SDK embedded in the app under test.
|
|
133
175
|
|
|
@@ -140,6 +182,21 @@ git submodule add https://github.com/johnmikel/zeno-mobile-runner.git vendor/zen
|
|
|
140
182
|
gradle -p vendor/zeno-mobile-runner/clients/kotlin build
|
|
141
183
|
```
|
|
142
184
|
|
|
185
|
+
```kotlin
|
|
186
|
+
val out = ".zmr/discovered/kotlin-agent.json"
|
|
187
|
+
val discovered = client.discoverTrace(
|
|
188
|
+
out,
|
|
189
|
+
TraceDiscoverOptions(includeActions = true, validate = true, force = true)
|
|
190
|
+
)
|
|
191
|
+
val explored = client.exploreTrace(
|
|
192
|
+
".zmr/discovered/kotlin-goal.json",
|
|
193
|
+
"find a stable login smoke",
|
|
194
|
+
TraceDiscoverOptions(includeActions = true, validate = true, force = true)
|
|
195
|
+
)
|
|
196
|
+
val validation = client.validateScenario(out)
|
|
197
|
+
val explanation = client.explainTrace()
|
|
198
|
+
```
|
|
199
|
+
|
|
143
200
|
Kotlin is useful for Android teams that want host-side orchestration in Kotlin.
|
|
144
201
|
It still drives the external `zmr` binary.
|
|
145
202
|
|
package/clients/go/README.md
CHANGED
|
@@ -12,6 +12,18 @@ defer client.Close()
|
|
|
12
12
|
|
|
13
13
|
snapshot, err := client.Snapshot(ctx)
|
|
14
14
|
healthy, err := client.AssertHealthy(ctx, 1000)
|
|
15
|
+
explanation, err := client.ExplainTrace(ctx)
|
|
16
|
+
discovered, err := client.DiscoverTrace(ctx, ".zmr/discovered/go-agent.json", zmr.TraceDiscoverOptions{
|
|
17
|
+
IncludeActions: true,
|
|
18
|
+
Validate: true,
|
|
19
|
+
Force: true,
|
|
20
|
+
})
|
|
21
|
+
explored, err := client.ExploreTrace(ctx, ".zmr/discovered/go-goal.json", "find a stable login smoke", zmr.TraceDiscoverOptions{
|
|
22
|
+
IncludeActions: true,
|
|
23
|
+
Validate: true,
|
|
24
|
+
Force: true,
|
|
25
|
+
})
|
|
26
|
+
validation, err := client.ValidateScenario(ctx, discovered.Out)
|
|
15
27
|
```
|
|
16
28
|
|
|
17
29
|
Run the fake-session example from the repository root:
|
package/clients/go/zmr/client.go
CHANGED
|
@@ -134,6 +134,39 @@ type TraceEvents struct {
|
|
|
134
134
|
Events []map[string]interface{} `json:"events"`
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
type TraceDiagnostic struct {
|
|
138
|
+
Kind string `json:"kind"`
|
|
139
|
+
Status string `json:"status,omitempty"`
|
|
140
|
+
SnapshotID string `json:"snapshotId,omitempty"`
|
|
141
|
+
ArtifactStatus string `json:"artifactStatus,omitempty"`
|
|
142
|
+
SemanticStatus string `json:"semanticStatus,omitempty"`
|
|
143
|
+
Error string `json:"error,omitempty"`
|
|
144
|
+
ScreenshotArtifact string `json:"screenshotArtifact,omitempty"`
|
|
145
|
+
Source string `json:"source,omitempty"`
|
|
146
|
+
ActivePackage string `json:"activePackage,omitempty"`
|
|
147
|
+
ActiveActivity string `json:"activeActivity,omitempty"`
|
|
148
|
+
VisibleTexts []string `json:"visibleTexts,omitempty"`
|
|
149
|
+
NearestTextMatches []string `json:"nearestTextMatches,omitempty"`
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
type TraceExplain struct {
|
|
153
|
+
OK bool `json:"ok"`
|
|
154
|
+
TraceDir string `json:"traceDir"`
|
|
155
|
+
Scenario string `json:"scenario"`
|
|
156
|
+
Status string `json:"status"`
|
|
157
|
+
AppID string `json:"appId,omitempty"`
|
|
158
|
+
DurationMS int64 `json:"durationMs,omitempty"`
|
|
159
|
+
EventCount int64 `json:"eventCount,omitempty"`
|
|
160
|
+
SnapshotCount int64 `json:"snapshotCount,omitempty"`
|
|
161
|
+
PartialFailureCount int64 `json:"partialFailureCount,omitempty"`
|
|
162
|
+
FailedStepIndex int64 `json:"failedStepIndex,omitempty"`
|
|
163
|
+
Error string `json:"error,omitempty"`
|
|
164
|
+
Diagnostic *TraceDiagnostic `json:"diagnostic,omitempty"`
|
|
165
|
+
PartialFailure *TraceDiagnostic `json:"partialFailure,omitempty"`
|
|
166
|
+
LastEvent string `json:"lastEvent,omitempty"`
|
|
167
|
+
NextCommands []string `json:"nextCommands"`
|
|
168
|
+
}
|
|
169
|
+
|
|
137
170
|
type TraceExport struct {
|
|
138
171
|
TraceDir string `json:"traceDir"`
|
|
139
172
|
Out string `json:"out"`
|
|
@@ -141,6 +174,59 @@ type TraceExport struct {
|
|
|
141
174
|
OmitScreenshots bool `json:"omitScreenshots"`
|
|
142
175
|
}
|
|
143
176
|
|
|
177
|
+
type ValidationResult struct {
|
|
178
|
+
OK bool `json:"ok"`
|
|
179
|
+
Path string `json:"path"`
|
|
180
|
+
Name string `json:"name,omitempty"`
|
|
181
|
+
AppID string `json:"appId,omitempty"`
|
|
182
|
+
StepCount int `json:"stepCount,omitempty"`
|
|
183
|
+
ErrorCode string `json:"errorCode,omitempty"`
|
|
184
|
+
Message string `json:"message,omitempty"`
|
|
185
|
+
FieldPath string `json:"fieldPath,omitempty"`
|
|
186
|
+
Line int `json:"line,omitempty"`
|
|
187
|
+
Column int `json:"column,omitempty"`
|
|
188
|
+
NextCommands []string `json:"nextCommands,omitempty"`
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
type ReplaySummary struct {
|
|
192
|
+
Enabled bool `json:"enabled"`
|
|
193
|
+
EventCount int `json:"eventCount"`
|
|
194
|
+
StepCount int `json:"stepCount"`
|
|
195
|
+
SkippedEventCount int `json:"skippedEventCount"`
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
type TraceDiscoverOptions struct {
|
|
199
|
+
IncludeActions bool
|
|
200
|
+
Validate bool
|
|
201
|
+
Force bool
|
|
202
|
+
Name string
|
|
203
|
+
AppID string
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
type TraceDiscover struct {
|
|
207
|
+
OK bool `json:"ok"`
|
|
208
|
+
Mode string `json:"mode"`
|
|
209
|
+
SchemaVersion int `json:"schemaVersion"`
|
|
210
|
+
RunnerVersion string `json:"runnerVersion"`
|
|
211
|
+
ProtocolVersion string `json:"protocolVersion"`
|
|
212
|
+
Out string `json:"out"`
|
|
213
|
+
TraceDir string `json:"traceDir"`
|
|
214
|
+
SourceSnapshot string `json:"sourceSnapshot"`
|
|
215
|
+
Name string `json:"name"`
|
|
216
|
+
AppID string `json:"appId,omitempty"`
|
|
217
|
+
SelectorCount int `json:"selectorCount"`
|
|
218
|
+
StepCount int `json:"stepCount"`
|
|
219
|
+
Replay ReplaySummary `json:"replay"`
|
|
220
|
+
Warnings []string `json:"warnings"`
|
|
221
|
+
Validated bool `json:"validated"`
|
|
222
|
+
Validation *ValidationResult `json:"validation"`
|
|
223
|
+
NextCommands []string `json:"nextCommands"`
|
|
224
|
+
Goal string `json:"goal,omitempty"`
|
|
225
|
+
Autonomous bool `json:"autonomous,omitempty"`
|
|
226
|
+
ReviewRequired bool `json:"reviewRequired,omitempty"`
|
|
227
|
+
Guardrails []string `json:"guardrails,omitempty"`
|
|
228
|
+
}
|
|
229
|
+
|
|
144
230
|
func Start(ctx context.Context, command string, args ...string) (*Client, error) {
|
|
145
231
|
cmd := exec.CommandContext(ctx, command, args...)
|
|
146
232
|
stdin, err := cmd.StdinPipe()
|
|
@@ -415,6 +501,12 @@ func (c *Client) AssertHealthy(ctx context.Context, timeoutMS int64) (bool, erro
|
|
|
415
501
|
return out, err
|
|
416
502
|
}
|
|
417
503
|
|
|
504
|
+
func (c *Client) ValidateScenario(ctx context.Context, path string) (ValidationResult, error) {
|
|
505
|
+
var out ValidationResult
|
|
506
|
+
err := c.Request(ctx, "scenario.validate", map[string]interface{}{"path": path}, &out)
|
|
507
|
+
return out, err
|
|
508
|
+
}
|
|
509
|
+
|
|
418
510
|
func (c *Client) ExportTrace(ctx context.Context, outPath string, redact bool, omitScreenshots bool) (TraceExport, error) {
|
|
419
511
|
var out TraceExport
|
|
420
512
|
err := c.Request(ctx, "trace.export", map[string]interface{}{"out": outPath, "redact": redact, "omitScreenshots": omitScreenshots}, &out)
|
|
@@ -430,3 +522,53 @@ func (c *Client) TraceEvents(ctx context.Context, afterSeq int64, limit int64) (
|
|
|
430
522
|
err := c.Request(ctx, "trace.events", params, &out)
|
|
431
523
|
return out, err
|
|
432
524
|
}
|
|
525
|
+
|
|
526
|
+
func (c *Client) ExplainTrace(ctx context.Context) (TraceExplain, error) {
|
|
527
|
+
var out TraceExplain
|
|
528
|
+
err := c.Request(ctx, "trace.explain", map[string]interface{}{}, &out)
|
|
529
|
+
return out, err
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
func (c *Client) DiscoverTrace(ctx context.Context, outPath string, options TraceDiscoverOptions) (TraceDiscover, error) {
|
|
533
|
+
var out TraceDiscover
|
|
534
|
+
params := map[string]interface{}{"out": outPath}
|
|
535
|
+
if options.IncludeActions {
|
|
536
|
+
params["includeActions"] = true
|
|
537
|
+
}
|
|
538
|
+
if options.Validate {
|
|
539
|
+
params["validate"] = true
|
|
540
|
+
}
|
|
541
|
+
if options.Force {
|
|
542
|
+
params["force"] = true
|
|
543
|
+
}
|
|
544
|
+
if options.Name != "" {
|
|
545
|
+
params["name"] = options.Name
|
|
546
|
+
}
|
|
547
|
+
if options.AppID != "" {
|
|
548
|
+
params["appId"] = options.AppID
|
|
549
|
+
}
|
|
550
|
+
err := c.Request(ctx, "trace.discover", params, &out)
|
|
551
|
+
return out, err
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
func (c *Client) ExploreTrace(ctx context.Context, outPath string, goal string, options TraceDiscoverOptions) (TraceDiscover, error) {
|
|
555
|
+
var out TraceDiscover
|
|
556
|
+
params := map[string]interface{}{"out": outPath, "goal": goal}
|
|
557
|
+
if options.IncludeActions {
|
|
558
|
+
params["includeActions"] = true
|
|
559
|
+
}
|
|
560
|
+
if options.Validate {
|
|
561
|
+
params["validate"] = true
|
|
562
|
+
}
|
|
563
|
+
if options.Force {
|
|
564
|
+
params["force"] = true
|
|
565
|
+
}
|
|
566
|
+
if options.Name != "" {
|
|
567
|
+
params["name"] = options.Name
|
|
568
|
+
}
|
|
569
|
+
if options.AppID != "" {
|
|
570
|
+
params["appId"] = options.AppID
|
|
571
|
+
}
|
|
572
|
+
err := c.Request(ctx, "trace.explore", params, &out)
|
|
573
|
+
return out, err
|
|
574
|
+
}
|