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/clients/kotlin/README.md
CHANGED
|
@@ -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.
|
|
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
|
|
@@ -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
|
|
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("\"", "\\\"")
|
package/clients/python/README.md
CHANGED
|
@@ -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
|
|
|
@@ -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
|
package/clients/rust/Cargo.lock
CHANGED
package/clients/rust/Cargo.toml
CHANGED
package/clients/rust/README.md
CHANGED
|
@@ -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(
|
|
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:
|
package/clients/rust/src/lib.rs
CHANGED
|
@@ -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 {
|
package/clients/swift/README.md
CHANGED
|
@@ -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
|
|