zeno-mobile-runner 0.1.2 → 0.1.8

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 (89) hide show
  1. package/CHANGELOG.md +162 -3
  2. package/FEATURES.md +50 -7
  3. package/README.md +133 -7
  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 +202 -0
  25. package/docs/ai-agents.md +87 -6
  26. package/docs/benchmarking.md +10 -3
  27. package/docs/clients.md +10 -6
  28. package/docs/demo.md +4 -0
  29. package/docs/expo-smoke.md +79 -0
  30. package/docs/install.md +3 -2
  31. package/docs/npm.md +58 -4
  32. package/docs/production-readiness.md +123 -0
  33. package/docs/protocol-fixtures/core-session.responses.jsonl +1 -1
  34. package/docs/protocol.md +215 -16
  35. package/docs/scenario-authoring.md +3 -0
  36. package/docs/troubleshooting.md +1 -1
  37. package/npm/agents.mjs +16 -0
  38. package/npm/build-zmr.mjs +1 -1
  39. package/npm/commands.mjs +9 -5
  40. package/npm/postinstall.mjs +28 -2
  41. package/npm/verify-publish.mjs +36 -0
  42. package/package.json +2 -1
  43. package/prebuilds/darwin-arm64/zmr +0 -0
  44. package/prebuilds/darwin-x64/zmr +0 -0
  45. package/prebuilds/linux-arm64/zmr +0 -0
  46. package/prebuilds/linux-x64/zmr +0 -0
  47. package/schemas/README.md +4 -0
  48. package/schemas/discover-output.schema.json +83 -0
  49. package/schemas/draft-output.schema.json +58 -0
  50. package/schemas/explore-output.schema.json +94 -0
  51. package/schemas/inspect-output.schema.json +88 -0
  52. package/schemas/run-output.schema.json +2 -0
  53. package/scripts/install-ios-shim.sh +79 -14
  54. package/scripts/release-readiness.py +43 -0
  55. package/scripts/run-android-pilot.sh +35 -9
  56. package/scripts/run-ios-pilot.sh +11 -4
  57. package/shims/ios/ZMRShim.swift +3 -0
  58. package/shims/ios/ZMRShimUITestCase.swift +41 -11
  59. package/skills/zmr-mobile-testing/SKILL.md +28 -3
  60. package/src/cli_discover.zig +239 -0
  61. package/src/cli_draft.zig +924 -0
  62. package/src/cli_explore.zig +136 -0
  63. package/src/cli_inspect.zig +310 -0
  64. package/src/cli_output.zig +26 -2
  65. package/src/cli_run.zig +28 -0
  66. package/src/cli_trace.zig +8 -0
  67. package/src/errors.zig +9 -0
  68. package/src/ios.zig +11 -4
  69. package/src/ios_lifecycle.zig +36 -0
  70. package/src/ios_shim.zig +42 -0
  71. package/src/json_rpc_methods.zig +85 -11
  72. package/src/json_rpc_params.zig +8 -0
  73. package/src/json_rpc_protocol.zig +1 -1
  74. package/src/json_rpc_trace.zig +112 -0
  75. package/src/main.zig +24 -2
  76. package/src/mcp.zig +209 -6
  77. package/src/mcp_protocol.zig +29 -1
  78. package/src/mcp_trace.zig +126 -4
  79. package/src/report.zig +186 -0
  80. package/src/runner.zig +26 -4
  81. package/src/runner_actions.zig +10 -0
  82. package/src/runner_diagnostics.zig +31 -1
  83. package/src/runner_events.zig +70 -7
  84. package/src/runner_native.zig +17 -1
  85. package/src/runner_waits.zig +82 -19
  86. package/src/scaffold.zig +28 -12
  87. package/src/scenario.zig +32 -4
  88. package/src/schema_registry.zig +4 -0
  89. package/src/version.zig +1 -1
@@ -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
+ }
@@ -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.2.jar"))
30
+ implementation(files("path/to/zeno-mobile-runner/clients/kotlin/build/libs/zmr-client-0.1.8.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.2"
7
+ version = "0.1.8"
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.2.dev1"
7
+ version = "0.1.8.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.2"
103
+ version = "0.1.8"
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.2"
3
+ version = "0.1.8"
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