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.
Files changed (115) hide show
  1. package/CHANGELOG.md +192 -2
  2. package/FEATURES.md +50 -7
  3. package/README.md +168 -120
  4. package/build.zig.zon +3 -3
  5. package/clients/README.md +60 -3
  6. package/clients/go/README.md +12 -0
  7. package/clients/go/zmr/client.go +142 -0
  8. package/clients/kotlin/README.md +18 -1
  9. package/clients/kotlin/build.gradle.kts +1 -1
  10. package/clients/kotlin/src/main/kotlin/dev/zmr/ZmrClient.kt +76 -1
  11. package/clients/python/README.md +19 -0
  12. package/clients/python/pyproject.toml +1 -1
  13. package/clients/python/zmr_client.py +33 -0
  14. package/clients/rust/Cargo.lock +1 -1
  15. package/clients/rust/Cargo.toml +1 -1
  16. package/clients/rust/README.md +25 -1
  17. package/clients/rust/src/lib.rs +201 -0
  18. package/clients/swift/README.md +18 -0
  19. package/clients/swift/Sources/ZMRClient/ZMRClient.swift +82 -0
  20. package/clients/typescript/README.md +16 -0
  21. package/clients/typescript/index.d.ts +12 -0
  22. package/clients/typescript/index.mjs +16 -0
  23. package/clients/typescript/package.json +1 -1
  24. package/docs/agent-discovery.md +151 -22
  25. package/docs/ai-agents.md +99 -11
  26. package/docs/benchmarking.md +49 -3
  27. package/docs/benchmarks/2026-06-09-android-workflow.md +73 -0
  28. package/docs/benchmarks/2026-06-09-android-workflow.results.jsonl +20 -0
  29. package/docs/benchmarks/2026-06-09-framework-baseline-status.md +32 -0
  30. package/docs/benchmarks/2026-06-09-ios-appium-comparison.md +115 -0
  31. package/docs/benchmarks/2026-06-09-ios-appium-comparison.results.jsonl +40 -0
  32. package/docs/benchmarks/2026-06-09-ios-demo.md +90 -0
  33. package/docs/benchmarks/2026-06-09-ios-demo.results.jsonl +20 -0
  34. package/docs/benchmarks/2026-06-09-ios-maestro-comparison.md +128 -0
  35. package/docs/benchmarks/2026-06-09-ios-maestro-comparison.results.jsonl +40 -0
  36. package/docs/benchmarks/2026-06-09-ios-workflow-comparison.md +143 -0
  37. package/docs/benchmarks/2026-06-09-ios-workflow-comparison.results.jsonl +40 -0
  38. package/docs/benchmarks/2026-06-09-ios-xctest-floor.md +106 -0
  39. package/docs/benchmarks/2026-06-09-ios-xctest-floor.results.jsonl +40 -0
  40. package/docs/benchmarks/README.md +36 -0
  41. package/docs/benchmarks/benchmark-lab-v1.json +155 -0
  42. package/docs/benchmarks/benchmark-lab-v1.md +95 -0
  43. package/docs/clients.md +26 -6
  44. package/docs/demo.md +40 -1
  45. package/docs/expo-smoke.md +8 -8
  46. package/docs/frameworks.md +10 -0
  47. package/docs/install.md +3 -2
  48. package/docs/npm.md +100 -4
  49. package/docs/production-readiness.md +123 -0
  50. package/docs/protocol-fixtures/core-session.responses.jsonl +1 -1
  51. package/docs/protocol.md +215 -16
  52. package/docs/scenario-authoring.md +18 -0
  53. package/docs/trace-privacy.md +9 -0
  54. package/docs/troubleshooting.md +7 -1
  55. package/examples/android-workflow.json +79 -0
  56. package/examples/ios-shim-workflow.json +79 -0
  57. package/examples/react-native-expo-workflow.json +75 -0
  58. package/npm/agents.mjs +16 -0
  59. package/npm/commands.mjs +9 -5
  60. package/package.json +6 -1
  61. package/prebuilds/darwin-arm64/zmr +0 -0
  62. package/prebuilds/darwin-x64/zmr +0 -0
  63. package/prebuilds/linux-arm64/zmr +0 -0
  64. package/prebuilds/linux-x64/zmr +0 -0
  65. package/schemas/README.md +4 -0
  66. package/schemas/discover-output.schema.json +83 -0
  67. package/schemas/draft-output.schema.json +58 -0
  68. package/schemas/explore-output.schema.json +94 -0
  69. package/schemas/inspect-output.schema.json +88 -0
  70. package/schemas/run-output.schema.json +2 -0
  71. package/scripts/benchmark-lab.py +253 -0
  72. package/scripts/create-android-demo-app.sh +324 -29
  73. package/scripts/create-ios-demo-app.sh +174 -7
  74. package/scripts/create-react-native-expo-demo-app.sh +727 -0
  75. package/scripts/demo.sh +3 -0
  76. package/scripts/install-ios-shim.sh +2 -2
  77. package/scripts/release-readiness.py +43 -0
  78. package/scripts/run-android-pilot.sh +35 -9
  79. package/scripts/run-ios-pilot.sh +11 -4
  80. package/shims/ios/ZMRShim.swift +10 -0
  81. package/shims/ios/ZMRShimUITestCase.swift +42 -0
  82. package/shims/ios/protocol.md +1 -0
  83. package/skills/zmr-mobile-testing/SKILL.md +28 -3
  84. package/src/cli_discover.zig +239 -0
  85. package/src/cli_draft.zig +924 -0
  86. package/src/cli_explore.zig +136 -0
  87. package/src/cli_import.zig +31 -15
  88. package/src/cli_inspect.zig +310 -0
  89. package/src/cli_output.zig +26 -2
  90. package/src/cli_run.zig +28 -0
  91. package/src/cli_trace.zig +45 -15
  92. package/src/cli_validate.zig +12 -6
  93. package/src/errors.zig +9 -0
  94. package/src/ios.zig +49 -12
  95. package/src/ios_shim.zig +36 -2
  96. package/src/json_rpc_methods.zig +85 -11
  97. package/src/json_rpc_params.zig +8 -0
  98. package/src/json_rpc_protocol.zig +1 -1
  99. package/src/json_rpc_trace.zig +112 -0
  100. package/src/main.zig +27 -2
  101. package/src/mcp.zig +209 -6
  102. package/src/mcp_protocol.zig +29 -1
  103. package/src/mcp_trace.zig +126 -4
  104. package/src/report.zig +186 -0
  105. package/src/runner.zig +26 -4
  106. package/src/runner_actions.zig +10 -0
  107. package/src/runner_diagnostics.zig +31 -1
  108. package/src/runner_events.zig +70 -7
  109. package/src/runner_native.zig +17 -1
  110. package/src/runner_waits.zig +82 -19
  111. package/src/scaffold.zig +28 -12
  112. package/src/scenario.zig +32 -4
  113. package/src/schema_registry.zig +4 -0
  114. package/src/version.zig +1 -1
  115. package/viewer/app.js +23 -3
@@ -27,7 +27,24 @@ 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.1.3.jar"))
30
+ implementation(files("path/to/zeno-mobile-runner/clients/kotlin/build/libs/zmr-client-0.2.0.jar"))
31
+ ```
32
+
33
+ ```kotlin
34
+ val client = ZmrClient(listOf("zmr", "serve", "--transport", "stdio", "--config", ".zmr/config.json"))
35
+ val out = ".zmr/discovered/kotlin-agent.json"
36
+ val discovered = client.discoverTrace(
37
+ out,
38
+ TraceDiscoverOptions(includeActions = true, validate = true, force = true)
39
+ )
40
+ val explored = client.exploreTrace(
41
+ ".zmr/discovered/kotlin-goal.json",
42
+ "find a stable login smoke",
43
+ TraceDiscoverOptions(includeActions = true, validate = true, force = true)
44
+ )
45
+ val validation = client.validateScenario(out)
46
+ val explanation = client.explainTrace()
47
+ client.close()
31
48
  ```
32
49
 
33
50
  The Kotlin client is host-side. It is useful for Android teams that want test
@@ -4,7 +4,7 @@ plugins {
4
4
  }
5
5
 
6
6
  group = "dev.zmr"
7
- version = "0.1.3"
7
+ version = "0.2.0"
8
8
 
9
9
  kotlin {
10
10
  jvmToolchain(17)
@@ -12,6 +12,14 @@ class ZmrRpcException(
12
12
  val publicCode: String? = null
13
13
  ) : RuntimeException(message)
14
14
 
15
+ data class TraceDiscoverOptions(
16
+ val includeActions: Boolean = false,
17
+ val validate: Boolean = false,
18
+ val force: Boolean = false,
19
+ val name: String? = null,
20
+ val appId: String? = null
21
+ )
22
+
15
23
  class ZmrClient(
16
24
  private val command: List<String> = listOf("zmr", "serve", "--transport", "stdio")
17
25
  ) : Closeable {
@@ -31,6 +39,34 @@ class ZmrClient(
31
39
  return call("assert.healthy", params)
32
40
  }
33
41
 
42
+ fun validateScenario(path: String): String =
43
+ call("scenario.validate", """{"path":"${escapeJson(path)}"}""")
44
+
45
+ fun explainTrace(): String = call("trace.explain")
46
+
47
+ fun discoverTrace(out: String, options: TraceDiscoverOptions = TraceDiscoverOptions()): String {
48
+ val fields = mutableListOf(""""out":"${escapeJson(out)}"""")
49
+ if (options.includeActions) fields.add(""""includeActions":true""")
50
+ if (options.validate) fields.add(""""validate":true""")
51
+ if (options.force) fields.add(""""force":true""")
52
+ options.name?.let { fields.add(""""name":"${escapeJson(it)}"""") }
53
+ options.appId?.let { fields.add(""""appId":"${escapeJson(it)}"""") }
54
+ return call("trace.discover", "{${fields.joinToString(",")}}")
55
+ }
56
+
57
+ fun exploreTrace(out: String, goal: String, options: TraceDiscoverOptions = TraceDiscoverOptions()): String {
58
+ val fields = mutableListOf(
59
+ """"out":"${escapeJson(out)}"""",
60
+ """"goal":"${escapeJson(goal)}""""
61
+ )
62
+ if (options.includeActions) fields.add(""""includeActions":true""")
63
+ if (options.validate) fields.add(""""validate":true""")
64
+ if (options.force) fields.add(""""force":true""")
65
+ options.name?.let { fields.add(""""name":"${escapeJson(it)}"""") }
66
+ options.appId?.let { fields.add(""""appId":"${escapeJson(it)}"""") }
67
+ return call("trace.explore", "{${fields.joinToString(",")}}")
68
+ }
69
+
34
70
  @Synchronized
35
71
  fun call(method: String, paramsJson: String? = null): String {
36
72
  val id = nextId++
@@ -39,7 +75,7 @@ class ZmrClient(
39
75
  input.newLine()
40
76
  input.flush()
41
77
  val response = output.readLine() ?: error("zmr closed stdout")
42
- if (response.contains(""""error"""")) {
78
+ if (hasTopLevelKey(response, "error")) {
43
79
  throw ZmrRpcException(
44
80
  code = extractNumber(response, "code") ?: -32000,
45
81
  message = extractString(response, "message").ifEmpty { "ZMR JSON-RPC error" },
@@ -65,3 +101,42 @@ private fun extractNumber(json: String, key: String): Int? {
65
101
  val pattern = """"$key"\s*:\s*(-?[0-9]+)""".toRegex()
66
102
  return pattern.find(json)?.groupValues?.get(1)?.toIntOrNull()
67
103
  }
104
+
105
+ private fun hasTopLevelKey(json: String, key: String): Boolean {
106
+ var depth = 0
107
+ var inString = false
108
+ var escaped = false
109
+ var stringStart = 0
110
+ var i = 0
111
+ while (i < json.length) {
112
+ val ch = json[i]
113
+ if (inString) {
114
+ when {
115
+ escaped -> escaped = false
116
+ ch == '\\' -> escaped = true
117
+ ch == '"' -> {
118
+ inString = false
119
+ if (depth == 1 && json.substring(stringStart, i) == key) {
120
+ var j = i + 1
121
+ while (j < json.length && json[j].isWhitespace()) j += 1
122
+ if (j < json.length && json[j] == ':') return true
123
+ }
124
+ }
125
+ }
126
+ } else {
127
+ when (ch) {
128
+ '"' -> {
129
+ inString = true
130
+ stringStart = i + 1
131
+ }
132
+ '{', '[' -> depth += 1
133
+ '}', ']' -> depth -= 1
134
+ }
135
+ }
136
+ i += 1
137
+ }
138
+ return false
139
+ }
140
+
141
+ private fun escapeJson(value: String): String =
142
+ value.replace("\\", "\\\\").replace("\"", "\\\"")
@@ -20,8 +20,27 @@ with ZmrClient(
20
20
  zmr.wait_until({"text": "E2E auth probe"}, timeout_ms=30000)
21
21
  snapshot = zmr.snapshot()
22
22
  events = zmr.trace_events(0, limit=100)
23
+ explanation = zmr.explain_trace()
24
+ discovered = zmr.discover_trace(
25
+ ".zmr/discovered/agent-session.json",
26
+ include_actions=True,
27
+ validate=True,
28
+ force=True,
29
+ )
30
+ explored = zmr.explore_trace(
31
+ ".zmr/discovered/agent-goal.json",
32
+ "find a stable login smoke",
33
+ include_actions=True,
34
+ validate=True,
35
+ force=True,
36
+ )
37
+ validation = zmr.validate_scenario(discovered["out"])
23
38
  print(snapshot["nodes"])
24
39
  print(len(events["events"]))
40
+ print(explanation["status"])
41
+ print(discovered["out"])
42
+ print(explored["reviewRequired"])
43
+ print(validation["ok"])
25
44
  zmr.export_trace("traces/agent-session-redacted.zmrtrace", redact=True, omit_screenshots=True)
26
45
  ```
27
46
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "zmr-client"
7
- version = "0.1.3.dev1"
7
+ version = "0.2.0.dev1"
8
8
  description = "Python JSON-RPC client for Zeno Mobile Runner."
9
9
  requires-python = ">=3.9"
10
10
  license = { text = "MIT" }
@@ -160,6 +160,9 @@ class ZmrClient:
160
160
  params["timeoutMs"] = timeout_ms
161
161
  return self.request("assert.healthy", params)
162
162
 
163
+ def validate_scenario(self, path):
164
+ return self.request("scenario.validate", {"path": path})
165
+
163
166
  def export_trace(self, out, redact=False, omit_screenshots=False):
164
167
  return self.request(
165
168
  "trace.export",
@@ -176,6 +179,36 @@ class ZmrClient:
176
179
  params["limit"] = limit
177
180
  return self.request("trace.events", params)
178
181
 
182
+ def explain_trace(self):
183
+ return self.request("trace.explain")
184
+
185
+ def discover_trace(self, out, include_actions=False, validate=False, force=False, name=None, app_id=None):
186
+ params = {
187
+ "out": out,
188
+ "includeActions": include_actions,
189
+ "validate": validate,
190
+ "force": force,
191
+ }
192
+ if name is not None:
193
+ params["name"] = name
194
+ if app_id is not None:
195
+ params["appId"] = app_id
196
+ return self.request("trace.discover", params)
197
+
198
+ def explore_trace(self, out, goal, include_actions=False, validate=False, force=False, name=None, app_id=None):
199
+ params = {
200
+ "out": out,
201
+ "goal": goal,
202
+ "includeActions": include_actions,
203
+ "validate": validate,
204
+ "force": force,
205
+ }
206
+ if name is not None:
207
+ params["name"] = name
208
+ if app_id is not None:
209
+ params["appId"] = app_id
210
+ return self.request("trace.explore", params)
211
+
179
212
  def close(self):
180
213
  if self._closed:
181
214
  return
@@ -100,7 +100,7 @@ checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
100
100
 
101
101
  [[package]]
102
102
  name = "zmr-client"
103
- version = "0.1.3"
103
+ version = "0.2.0"
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.1.3"
3
+ version = "0.2.0"
4
4
  edition = "2021"
5
5
  license = "MIT"
6
6
  description = "Rust JSON-RPC client for Zeno Mobile Runner."
@@ -4,9 +4,33 @@ Small synchronous JSON-RPC client for driving `zmr serve --transport stdio`
4
4
  from Rust agents and test harnesses.
5
5
 
6
6
  ```rust
7
- let mut client = zmr_client::Client::start("zmr", ["serve", "--transport", "stdio"])?;
7
+ let mut client = zmr_client::Client::start(
8
+ "zmr",
9
+ ["serve", "--transport", "stdio", "--config", ".zmr/config.json"],
10
+ )?;
8
11
  let snapshot = client.snapshot()?;
9
12
  let healthy = client.assert_healthy(Some(1000))?;
13
+ let explanation = client.explain_trace()?;
14
+ let discovered = client.discover_trace(
15
+ ".zmr/discovered/rust-agent.json",
16
+ zmr_client::TraceDiscoverOptions {
17
+ include_actions: true,
18
+ validate: true,
19
+ force: true,
20
+ ..Default::default()
21
+ },
22
+ )?;
23
+ let explored = client.explore_trace(
24
+ ".zmr/discovered/rust-goal.json",
25
+ "find a stable login smoke",
26
+ zmr_client::TraceDiscoverOptions {
27
+ include_actions: true,
28
+ validate: true,
29
+ force: true,
30
+ ..Default::default()
31
+ },
32
+ )?;
33
+ let validation = client.validate_scenario(&discovered.out)?;
10
34
  ```
11
35
 
12
36
  Run the fake-session example from the repository root:
@@ -204,6 +204,150 @@ pub struct TraceEvents {
204
204
  pub events: Vec<Value>,
205
205
  }
206
206
 
207
+ #[derive(Debug, Deserialize)]
208
+ pub struct TraceDiagnostic {
209
+ pub kind: String,
210
+ #[serde(default)]
211
+ pub status: Option<String>,
212
+ #[serde(default, rename = "snapshotId")]
213
+ pub snapshot_id: Option<String>,
214
+ #[serde(default, rename = "artifactStatus")]
215
+ pub artifact_status: Option<String>,
216
+ #[serde(default, rename = "semanticStatus")]
217
+ pub semantic_status: Option<String>,
218
+ #[serde(default)]
219
+ pub error: Option<String>,
220
+ #[serde(default, rename = "screenshotArtifact")]
221
+ pub screenshot_artifact: Option<String>,
222
+ #[serde(default)]
223
+ pub source: Option<String>,
224
+ #[serde(default, rename = "activePackage")]
225
+ pub active_package: Option<String>,
226
+ #[serde(default, rename = "activeActivity")]
227
+ pub active_activity: Option<String>,
228
+ #[serde(default, rename = "visibleTexts")]
229
+ pub visible_texts: Vec<String>,
230
+ #[serde(default, rename = "nearestTextMatches")]
231
+ pub nearest_text_matches: Vec<String>,
232
+ }
233
+
234
+ #[derive(Debug, Deserialize)]
235
+ pub struct TraceExplain {
236
+ pub ok: bool,
237
+ #[serde(rename = "traceDir")]
238
+ pub trace_dir: String,
239
+ pub scenario: String,
240
+ pub status: String,
241
+ #[serde(default, rename = "appId")]
242
+ pub app_id: Option<String>,
243
+ #[serde(default, rename = "durationMs")]
244
+ pub duration_ms: Option<i64>,
245
+ #[serde(default, rename = "eventCount")]
246
+ pub event_count: Option<i64>,
247
+ #[serde(default, rename = "snapshotCount")]
248
+ pub snapshot_count: Option<i64>,
249
+ #[serde(default, rename = "partialFailureCount")]
250
+ pub partial_failure_count: Option<i64>,
251
+ #[serde(default, rename = "failedStepIndex")]
252
+ pub failed_step_index: Option<i64>,
253
+ #[serde(default)]
254
+ pub error: Option<String>,
255
+ #[serde(default)]
256
+ pub diagnostic: Option<TraceDiagnostic>,
257
+ #[serde(default, rename = "partialFailure")]
258
+ pub partial_failure: Option<TraceDiagnostic>,
259
+ #[serde(default, rename = "lastEvent")]
260
+ pub last_event: Option<String>,
261
+ #[serde(default, rename = "nextCommands")]
262
+ pub next_commands: Vec<String>,
263
+ }
264
+
265
+ #[derive(Debug, Deserialize)]
266
+ pub struct ValidationResult {
267
+ pub ok: bool,
268
+ pub path: String,
269
+ #[serde(default)]
270
+ pub name: Option<String>,
271
+ #[serde(default, rename = "appId")]
272
+ pub app_id: Option<String>,
273
+ #[serde(default, rename = "stepCount")]
274
+ pub step_count: i64,
275
+ #[serde(default, rename = "errorCode")]
276
+ pub error_code: Option<String>,
277
+ #[serde(default)]
278
+ pub message: Option<String>,
279
+ #[serde(default, rename = "fieldPath")]
280
+ pub field_path: Option<String>,
281
+ #[serde(default)]
282
+ pub line: Option<i64>,
283
+ #[serde(default)]
284
+ pub column: Option<i64>,
285
+ #[serde(default, rename = "nextCommands")]
286
+ pub next_commands: Vec<String>,
287
+ }
288
+
289
+ #[derive(Debug, Deserialize)]
290
+ pub struct ReplaySummary {
291
+ #[serde(default)]
292
+ pub enabled: bool,
293
+ #[serde(default, rename = "eventCount")]
294
+ pub event_count: i64,
295
+ #[serde(default, rename = "stepCount")]
296
+ pub step_count: i64,
297
+ #[serde(default, rename = "skippedEventCount")]
298
+ pub skipped_event_count: i64,
299
+ }
300
+
301
+ #[derive(Debug, Default)]
302
+ pub struct TraceDiscoverOptions {
303
+ pub include_actions: bool,
304
+ pub validate: bool,
305
+ pub force: bool,
306
+ pub name: Option<String>,
307
+ pub app_id: Option<String>,
308
+ }
309
+
310
+ #[derive(Debug, Deserialize)]
311
+ pub struct TraceDiscover {
312
+ pub ok: bool,
313
+ pub mode: String,
314
+ #[serde(rename = "schemaVersion")]
315
+ pub schema_version: i64,
316
+ #[serde(rename = "runnerVersion")]
317
+ pub runner_version: String,
318
+ #[serde(rename = "protocolVersion")]
319
+ pub protocol_version: String,
320
+ pub out: String,
321
+ #[serde(rename = "traceDir")]
322
+ pub trace_dir: String,
323
+ #[serde(rename = "sourceSnapshot")]
324
+ pub source_snapshot: String,
325
+ pub name: String,
326
+ #[serde(default, rename = "appId")]
327
+ pub app_id: Option<String>,
328
+ #[serde(rename = "selectorCount")]
329
+ pub selector_count: i64,
330
+ #[serde(rename = "stepCount")]
331
+ pub step_count: i64,
332
+ pub replay: ReplaySummary,
333
+ #[serde(default)]
334
+ pub warnings: Vec<String>,
335
+ #[serde(default)]
336
+ pub validated: bool,
337
+ #[serde(default)]
338
+ pub validation: Option<ValidationResult>,
339
+ #[serde(default, rename = "nextCommands")]
340
+ pub next_commands: Vec<String>,
341
+ #[serde(default)]
342
+ pub goal: Option<String>,
343
+ #[serde(default)]
344
+ pub autonomous: bool,
345
+ #[serde(default, rename = "reviewRequired")]
346
+ pub review_required: bool,
347
+ #[serde(default)]
348
+ pub guardrails: Vec<String>,
349
+ }
350
+
207
351
  impl Client {
208
352
  pub fn start<I, S>(command: &str, args: I) -> Result<Self, Error>
209
353
  where
@@ -420,6 +564,10 @@ impl Client {
420
564
  self.request("assert.healthy", params)
421
565
  }
422
566
 
567
+ pub fn validate_scenario(&mut self, path: &str) -> Result<ValidationResult, Error> {
568
+ self.request("scenario.validate", json!({ "path": path }))
569
+ }
570
+
423
571
  pub fn export_trace(
424
572
  &mut self,
425
573
  out: &str,
@@ -443,6 +591,59 @@ impl Client {
443
591
  }
444
592
  self.request("trace.events", params)
445
593
  }
594
+
595
+ pub fn explain_trace(&mut self) -> Result<TraceExplain, Error> {
596
+ self.request("trace.explain", json!({}))
597
+ }
598
+
599
+ pub fn discover_trace(
600
+ &mut self,
601
+ out: &str,
602
+ options: TraceDiscoverOptions,
603
+ ) -> Result<TraceDiscover, Error> {
604
+ let mut params = json!({ "out": out });
605
+ if options.include_actions {
606
+ params["includeActions"] = json!(true);
607
+ }
608
+ if options.validate {
609
+ params["validate"] = json!(true);
610
+ }
611
+ if options.force {
612
+ params["force"] = json!(true);
613
+ }
614
+ if let Some(name) = options.name {
615
+ params["name"] = json!(name);
616
+ }
617
+ if let Some(app_id) = options.app_id {
618
+ params["appId"] = json!(app_id);
619
+ }
620
+ self.request("trace.discover", params)
621
+ }
622
+
623
+ pub fn explore_trace(
624
+ &mut self,
625
+ out: &str,
626
+ goal: &str,
627
+ options: TraceDiscoverOptions,
628
+ ) -> Result<TraceDiscover, Error> {
629
+ let mut params = json!({ "out": out, "goal": goal });
630
+ if options.include_actions {
631
+ params["includeActions"] = json!(true);
632
+ }
633
+ if options.validate {
634
+ params["validate"] = json!(true);
635
+ }
636
+ if options.force {
637
+ params["force"] = json!(true);
638
+ }
639
+ if let Some(name) = options.name {
640
+ params["name"] = json!(name);
641
+ }
642
+ if let Some(app_id) = options.app_id {
643
+ params["appId"] = json!(app_id);
644
+ }
645
+ self.request("trace.explore", params)
646
+ }
446
647
  }
447
648
 
448
649
  impl Drop for Client {
@@ -16,6 +16,24 @@ git submodule add https://github.com/johnmikel/zeno-mobile-runner.git vendor/zen
16
16
 
17
17
  Then depend on the `ZMRClient` product from `clients/swift`.
18
18
 
19
+ ```swift
20
+ let client = ZMRClient(arguments: ["serve", "--transport", "stdio", "--config", ".zmr/config.json"])
21
+ try client.start()
22
+ let out = ".zmr/discovered/swift-agent.json"
23
+ let discovered = try client.discoverTrace(
24
+ out: out,
25
+ options: TraceDiscoverOptions(includeActions: true, validate: true, force: true)
26
+ )
27
+ let explored = try client.exploreTrace(
28
+ out: ".zmr/discovered/swift-goal.json",
29
+ goal: "find a stable login smoke",
30
+ options: TraceDiscoverOptions(includeActions: true, validate: true, force: true)
31
+ )
32
+ let validation = try client.validateScenario(path: out)
33
+ let explanation = try client.explainTrace()
34
+ client.close()
35
+ ```
36
+
19
37
  Run the package test from this directory:
20
38
 
21
39
  ```bash
@@ -6,6 +6,28 @@ public enum ZMRError: Error {
6
6
  case rpcError([String: Any])
7
7
  }
8
8
 
9
+ public struct TraceDiscoverOptions {
10
+ public var includeActions: Bool
11
+ public var validate: Bool
12
+ public var force: Bool
13
+ public var name: String?
14
+ public var appId: String?
15
+
16
+ public init(
17
+ includeActions: Bool = false,
18
+ validate: Bool = false,
19
+ force: Bool = false,
20
+ name: String? = nil,
21
+ appId: String? = nil
22
+ ) {
23
+ self.includeActions = includeActions
24
+ self.validate = validate
25
+ self.force = force
26
+ self.name = name
27
+ self.appId = appId
28
+ }
29
+ }
30
+
9
31
  public final class ZMRClient {
10
32
  private let process: Process
11
33
  private let input: FileHandle
@@ -98,6 +120,66 @@ public final class ZMRClient {
98
120
  return result
99
121
  }
100
122
 
123
+ public func validateScenario(path: String) throws -> [String: Any] {
124
+ guard let result = try call("scenario.validate", params: ["path": path]) as? [String: Any] else {
125
+ throw ZMRError.invalidResponse
126
+ }
127
+ return result
128
+ }
129
+
130
+ public func discoverTrace(out: String, options: TraceDiscoverOptions = TraceDiscoverOptions()) throws -> [String: Any] {
131
+ var params: [String: Any] = ["out": out]
132
+ if options.includeActions {
133
+ params["includeActions"] = true
134
+ }
135
+ if options.validate {
136
+ params["validate"] = true
137
+ }
138
+ if options.force {
139
+ params["force"] = true
140
+ }
141
+ if let name = options.name {
142
+ params["name"] = name
143
+ }
144
+ if let appId = options.appId {
145
+ params["appId"] = appId
146
+ }
147
+ guard let result = try call("trace.discover", params: params) as? [String: Any] else {
148
+ throw ZMRError.invalidResponse
149
+ }
150
+ return result
151
+ }
152
+
153
+ public func exploreTrace(out: String, goal: String, options: TraceDiscoverOptions = TraceDiscoverOptions()) throws -> [String: Any] {
154
+ var params: [String: Any] = ["out": out, "goal": goal]
155
+ if options.includeActions {
156
+ params["includeActions"] = true
157
+ }
158
+ if options.validate {
159
+ params["validate"] = true
160
+ }
161
+ if options.force {
162
+ params["force"] = true
163
+ }
164
+ if let name = options.name {
165
+ params["name"] = name
166
+ }
167
+ if let appId = options.appId {
168
+ params["appId"] = appId
169
+ }
170
+ guard let result = try call("trace.explore", params: params) as? [String: Any] else {
171
+ throw ZMRError.invalidResponse
172
+ }
173
+ return result
174
+ }
175
+
176
+ public func explainTrace() throws -> [String: Any] {
177
+ guard let result = try call("trace.explain", params: [:]) as? [String: Any] else {
178
+ throw ZMRError.invalidResponse
179
+ }
180
+ return result
181
+ }
182
+
101
183
  private func readLineData() throws -> Data {
102
184
  var data = Data()
103
185
  while true {
@@ -22,8 +22,24 @@ try {
22
22
  await zmr.waitUntil({ text: "E2E auth probe" }, { timeoutMs: 30000 });
23
23
  const snapshot = await zmr.snapshot();
24
24
  const events = await zmr.traceEvents(0, { limit: 100 });
25
+ const explanation = await zmr.explainTrace();
26
+ const discovered = await zmr.discoverTrace(".zmr/discovered/agent-session.json", {
27
+ includeActions: true,
28
+ validate: true,
29
+ force: true,
30
+ });
31
+ const explored = await zmr.exploreTrace(".zmr/discovered/agent-goal.json", "find a stable login smoke", {
32
+ includeActions: true,
33
+ validate: true,
34
+ force: true,
35
+ });
36
+ const validation = await zmr.validateScenario(discovered.out);
25
37
  console.log(snapshot.nodes);
26
38
  console.log(events.events.length);
39
+ console.log(explanation.status);
40
+ console.log(discovered.out);
41
+ console.log(explored.reviewRequired);
42
+ console.log(validation.ok);
27
43
  await zmr.exportTrace("traces/agent-session-redacted.zmrtrace", { redact: true, omitScreenshots: true });
28
44
  } finally {
29
45
  await zmr.close();
@@ -99,6 +99,14 @@ export interface Capabilities {
99
99
  methods: string[];
100
100
  }
101
101
 
102
+ export interface TraceDiscoverOptions {
103
+ includeActions?: boolean;
104
+ validate?: boolean;
105
+ force?: boolean;
106
+ name?: string;
107
+ appId?: string;
108
+ }
109
+
102
110
  export interface DeviceInfo {
103
111
  serial: string;
104
112
  state: string;
@@ -130,8 +138,12 @@ export interface ZmrClient {
130
138
  assertVisible(selector: Selector, options?: { timeoutMs?: number }): Promise<boolean>;
131
139
  assertNotVisible(selector: Selector, options?: { timeoutMs?: number }): Promise<boolean>;
132
140
  assertHealthy(options?: { timeoutMs?: number }): Promise<boolean>;
141
+ validateScenario(path: string): Promise<Record<string, unknown>>;
133
142
  exportTrace(out: string, options?: { redact?: boolean; omitScreenshots?: boolean }): Promise<Record<string, unknown>>;
134
143
  traceEvents(afterSeq?: number, options?: { limit?: number }): Promise<Record<string, unknown>>;
144
+ explainTrace(): Promise<Record<string, unknown>>;
145
+ discoverTrace(out: string, options?: TraceDiscoverOptions): Promise<Record<string, unknown>>;
146
+ exploreTrace(out: string, goal: string, options?: TraceDiscoverOptions): Promise<Record<string, unknown>>;
135
147
  close(): Promise<void>;
136
148
  }
137
149