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/src/runner_waits.zig
CHANGED
|
@@ -15,34 +15,62 @@ pub fn waitUntilVisible(
|
|
|
15
15
|
timeout_ms: u64,
|
|
16
16
|
writer: ?*trace.TraceWriter,
|
|
17
17
|
options: RunOptions,
|
|
18
|
+
) !bool {
|
|
19
|
+
return try untilVisibleKind(device, wanted, timeout_ms, writer, options, "wait.visible");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
pub fn assertVisible(
|
|
23
|
+
device: anytype,
|
|
24
|
+
wanted: selector.Selector,
|
|
25
|
+
timeout_ms: u64,
|
|
26
|
+
writer: ?*trace.TraceWriter,
|
|
27
|
+
options: RunOptions,
|
|
28
|
+
) !bool {
|
|
29
|
+
return try untilVisibleKind(device, wanted, timeout_ms, writer, options, "assert.visible");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
fn untilVisibleKind(
|
|
33
|
+
device: anytype,
|
|
34
|
+
wanted: selector.Selector,
|
|
35
|
+
timeout_ms: u64,
|
|
36
|
+
writer: ?*trace.TraceWriter,
|
|
37
|
+
options: RunOptions,
|
|
38
|
+
kind: []const u8,
|
|
18
39
|
) !bool {
|
|
19
40
|
const deadline = std.time.milliTimestamp() + @as(i64, @intCast(timeout_ms));
|
|
20
41
|
while (true) {
|
|
21
42
|
if (try nativeVisibleBySelector(device, wanted)) |visible| {
|
|
22
43
|
if (visible) {
|
|
23
|
-
if (writer) |tw| try runner_events.recordNativeWait(tw,
|
|
44
|
+
if (writer) |tw| try runner_events.recordNativeWait(tw, kind, wanted, null, timeout_ms);
|
|
24
45
|
return true;
|
|
25
46
|
}
|
|
26
47
|
if (std.time.milliTimestamp() >= deadline) {
|
|
27
|
-
if (writer) |tw| try runner_events.recordNativeWaitTimeoutWithDiagnostics(device, tw,
|
|
48
|
+
if (writer) |tw| try runner_events.recordNativeWaitTimeoutWithDiagnostics(device, tw, kind, &[_]selector.Selector{wanted}, timeout_ms);
|
|
28
49
|
return false;
|
|
29
50
|
}
|
|
30
51
|
try sleepMs(options.poll_ms);
|
|
31
52
|
continue;
|
|
32
53
|
}
|
|
33
54
|
var snap = device.snapshot(writer) catch |err| {
|
|
34
|
-
if (try retryTransientObservation(err,
|
|
55
|
+
if (try retryTransientObservation(err, kind, writer, deadline, options)) continue;
|
|
35
56
|
return err;
|
|
36
57
|
};
|
|
37
58
|
defer snap.deinit(device.allocator);
|
|
38
|
-
if (selector.find(snap.nodes, wanted)
|
|
39
|
-
if (writer) |tw|
|
|
59
|
+
if (selector.find(snap.nodes, wanted)) |node| {
|
|
60
|
+
if (writer) |tw| {
|
|
61
|
+
var payload = std.ArrayList(u8).empty;
|
|
62
|
+
defer payload.deinit(tw.allocator);
|
|
63
|
+
try payload.writer(tw.allocator).print("{{\"status\":\"ok\",\"target\":\"{s}\",\"selector\":", .{node.stable_id});
|
|
64
|
+
try trace.writeSelectorJson(payload.writer(tw.allocator), wanted);
|
|
65
|
+
try payload.writer(tw.allocator).print(",\"timeoutMs\":{d}}}", .{timeout_ms});
|
|
66
|
+
try tw.recordEvent(kind, payload.items);
|
|
67
|
+
}
|
|
40
68
|
return true;
|
|
41
69
|
}
|
|
42
70
|
if (std.time.milliTimestamp() >= deadline) {
|
|
43
71
|
if (writer) |tw| {
|
|
44
72
|
const selectors = [_]selector.Selector{wanted};
|
|
45
|
-
try runner_events.
|
|
73
|
+
try runner_events.recordDiagnosticWithStrategyAndTimeout(tw, kind, "timeout", null, selectors[0..], snap, timeout_ms);
|
|
46
74
|
}
|
|
47
75
|
return false;
|
|
48
76
|
}
|
|
@@ -56,34 +84,62 @@ pub fn waitUntilNotVisible(
|
|
|
56
84
|
timeout_ms: u64,
|
|
57
85
|
writer: ?*trace.TraceWriter,
|
|
58
86
|
options: RunOptions,
|
|
87
|
+
) !bool {
|
|
88
|
+
return try untilNotVisibleKind(device, wanted, timeout_ms, writer, options, "wait.notVisible");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
pub fn assertNotVisible(
|
|
92
|
+
device: anytype,
|
|
93
|
+
wanted: selector.Selector,
|
|
94
|
+
timeout_ms: u64,
|
|
95
|
+
writer: ?*trace.TraceWriter,
|
|
96
|
+
options: RunOptions,
|
|
97
|
+
) !bool {
|
|
98
|
+
return try untilNotVisibleKind(device, wanted, timeout_ms, writer, options, "assert.notVisible");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
fn untilNotVisibleKind(
|
|
102
|
+
device: anytype,
|
|
103
|
+
wanted: selector.Selector,
|
|
104
|
+
timeout_ms: u64,
|
|
105
|
+
writer: ?*trace.TraceWriter,
|
|
106
|
+
options: RunOptions,
|
|
107
|
+
kind: []const u8,
|
|
59
108
|
) !bool {
|
|
60
109
|
const deadline = std.time.milliTimestamp() + @as(i64, @intCast(timeout_ms));
|
|
61
110
|
while (true) {
|
|
62
111
|
if (try nativeVisibleBySelector(device, wanted)) |visible| {
|
|
63
112
|
if (!visible) {
|
|
64
|
-
if (writer) |tw| try runner_events.recordNativeWait(tw,
|
|
113
|
+
if (writer) |tw| try runner_events.recordNativeWait(tw, kind, wanted, null, timeout_ms);
|
|
65
114
|
return true;
|
|
66
115
|
}
|
|
67
116
|
if (std.time.milliTimestamp() >= deadline) {
|
|
68
|
-
if (writer) |tw| try runner_events.recordNativeWaitTimeoutWithDiagnostics(device, tw,
|
|
117
|
+
if (writer) |tw| try runner_events.recordNativeWaitTimeoutWithDiagnostics(device, tw, kind, &[_]selector.Selector{wanted}, timeout_ms);
|
|
69
118
|
return false;
|
|
70
119
|
}
|
|
71
120
|
try sleepMs(options.poll_ms);
|
|
72
121
|
continue;
|
|
73
122
|
}
|
|
74
123
|
var snap = device.snapshot(writer) catch |err| {
|
|
75
|
-
if (try retryTransientObservation(err,
|
|
124
|
+
if (try retryTransientObservation(err, kind, writer, deadline, options)) continue;
|
|
76
125
|
return err;
|
|
77
126
|
};
|
|
78
127
|
defer snap.deinit(device.allocator);
|
|
79
128
|
if (selector.find(snap.nodes, wanted) == null) {
|
|
80
|
-
if (writer) |tw|
|
|
129
|
+
if (writer) |tw| {
|
|
130
|
+
var payload = std.ArrayList(u8).empty;
|
|
131
|
+
defer payload.deinit(tw.allocator);
|
|
132
|
+
try payload.writer(tw.allocator).writeAll("{\"status\":\"ok\",\"selector\":");
|
|
133
|
+
try trace.writeSelectorJson(payload.writer(tw.allocator), wanted);
|
|
134
|
+
try payload.writer(tw.allocator).print(",\"timeoutMs\":{d}}}", .{timeout_ms});
|
|
135
|
+
try tw.recordEvent(kind, payload.items);
|
|
136
|
+
}
|
|
81
137
|
return true;
|
|
82
138
|
}
|
|
83
139
|
if (std.time.milliTimestamp() >= deadline) {
|
|
84
140
|
if (writer) |tw| {
|
|
85
141
|
const selectors = [_]selector.Selector{wanted};
|
|
86
|
-
try runner_events.
|
|
142
|
+
try runner_events.recordDiagnosticWithStrategyAndTimeout(tw, kind, "timeout", null, selectors[0..], snap, timeout_ms);
|
|
87
143
|
}
|
|
88
144
|
return false;
|
|
89
145
|
}
|
|
@@ -104,7 +160,7 @@ pub fn waitUntilAnyVisible(
|
|
|
104
160
|
for (selectors, 0..) |wanted, index| {
|
|
105
161
|
if (try nativeVisibleBySelector(device, wanted)) |visible| {
|
|
106
162
|
if (visible) {
|
|
107
|
-
if (writer) |tw| try runner_events.recordNativeWait(tw, "wait.any", wanted, index);
|
|
163
|
+
if (writer) |tw| try runner_events.recordNativeWait(tw, "wait.any", wanted, index, timeout_ms);
|
|
108
164
|
return index;
|
|
109
165
|
}
|
|
110
166
|
} else {
|
|
@@ -114,7 +170,7 @@ pub fn waitUntilAnyVisible(
|
|
|
114
170
|
}
|
|
115
171
|
if (all_native) {
|
|
116
172
|
if (std.time.milliTimestamp() >= deadline) {
|
|
117
|
-
if (writer) |tw| try runner_events.recordNativeWaitTimeoutWithDiagnostics(device, tw, "wait.any", selectors);
|
|
173
|
+
if (writer) |tw| try runner_events.recordNativeWaitTimeoutWithDiagnostics(device, tw, "wait.any", selectors, timeout_ms);
|
|
118
174
|
return null;
|
|
119
175
|
}
|
|
120
176
|
try sleepMs(options.poll_ms);
|
|
@@ -132,7 +188,7 @@ pub fn waitUntilAnyVisible(
|
|
|
132
188
|
defer payload.deinit(tw.allocator);
|
|
133
189
|
try payload.writer(tw.allocator).print("{{\"status\":\"ok\",\"matchedIndex\":{d},\"target\":\"{s}\",\"selector\":", .{ index, node.stable_id });
|
|
134
190
|
try trace.writeSelectorJson(payload.writer(tw.allocator), wanted);
|
|
135
|
-
try payload.writer(tw.allocator).
|
|
191
|
+
try payload.writer(tw.allocator).print(",\"timeoutMs\":{d}}}", .{timeout_ms});
|
|
136
192
|
try tw.recordEvent("wait.any", payload.items);
|
|
137
193
|
}
|
|
138
194
|
return index;
|
|
@@ -170,12 +226,12 @@ pub fn assertNoneVisible(
|
|
|
170
226
|
}
|
|
171
227
|
|
|
172
228
|
if (!matched) {
|
|
173
|
-
if (writer) |tw| try
|
|
229
|
+
if (writer) |tw| try runner_events.recordSelectorArrayStatus(tw, "assert.noneVisible", "ok", selectors, timeout_ms);
|
|
174
230
|
return true;
|
|
175
231
|
}
|
|
176
232
|
|
|
177
233
|
if (std.time.milliTimestamp() >= deadline) {
|
|
178
|
-
if (writer) |tw| try runner_events.
|
|
234
|
+
if (writer) |tw| try runner_events.recordDiagnosticWithStrategyAndTimeout(tw, "assert.noneVisible", "visible", null, selectors, snap, timeout_ms);
|
|
179
235
|
return false;
|
|
180
236
|
}
|
|
181
237
|
|
|
@@ -199,12 +255,16 @@ pub fn assertHealthy(
|
|
|
199
255
|
defer snap.deinit(device.allocator);
|
|
200
256
|
|
|
201
257
|
if (!health.hasUnhealthyOverlay(snap.nodes)) {
|
|
202
|
-
if (writer) |tw|
|
|
258
|
+
if (writer) |tw| {
|
|
259
|
+
const payload = try std.fmt.allocPrint(tw.allocator, "{{\"status\":\"ok\",\"timeoutMs\":{d}}}", .{timeout_ms});
|
|
260
|
+
defer tw.allocator.free(payload);
|
|
261
|
+
try tw.recordEvent("assert.healthy", payload);
|
|
262
|
+
}
|
|
203
263
|
return true;
|
|
204
264
|
}
|
|
205
265
|
|
|
206
266
|
if (std.time.milliTimestamp() >= deadline) {
|
|
207
|
-
if (writer) |tw| try runner_events.
|
|
267
|
+
if (writer) |tw| try runner_events.recordDiagnosticWithStrategyAndTimeout(tw, "assert.healthy", "unhealthy", null, health_selectors, snap, timeout_ms);
|
|
208
268
|
return false;
|
|
209
269
|
}
|
|
210
270
|
|
|
@@ -233,7 +293,10 @@ pub fn scrollUntilVisible(
|
|
|
233
293
|
defer payload.deinit(tw.allocator);
|
|
234
294
|
try payload.writer(tw.allocator).print("{{\"status\":\"ok\",\"target\":\"{s}\",\"selector\":", .{node.stable_id});
|
|
235
295
|
try trace.writeSelectorJson(payload.writer(tw.allocator), wanted);
|
|
236
|
-
try payload.writer(tw.allocator).
|
|
296
|
+
try payload.writer(tw.allocator).print(",\"direction\":\"{s}\",\"timeoutMs\":{d}}}", .{
|
|
297
|
+
if (direction == .down) "down" else "up",
|
|
298
|
+
timeout_ms,
|
|
299
|
+
});
|
|
237
300
|
try tw.recordEvent("ui.scrollUntilVisible", payload.items);
|
|
238
301
|
}
|
|
239
302
|
return true;
|
package/src/scaffold.zig
CHANGED
|
@@ -149,21 +149,21 @@ fn writeAppConfig(path: []const u8, app_id: []const u8, force: bool) !void {
|
|
|
149
149
|
\\ "schemas": "zmr schemas --json",
|
|
150
150
|
\\ "validate": "zmr validate --json .zmr/android-smoke.json && zmr validate --json .zmr/ios-smoke.json",
|
|
151
151
|
\\ "android": "zmr run .zmr/android-smoke.json --device emulator-5554 --trace-dir traces/zmr-android",
|
|
152
|
-
\\ "androidReport": "zmr report traces/zmr-android --out traces/zmr-android/report.html",
|
|
152
|
+
\\ "androidReport": "zmr report traces/zmr-android --out traces/zmr-android/report.html --junit traces/zmr-android/junit.xml",
|
|
153
153
|
\\ "androidReliability": "export ZMR_BIN=\"${ZMR_BIN:-zmr}\"; zmr-benchmark --zmr .zmr/android-smoke.json --device emulator-5554 --app-id
|
|
154
154
|
);
|
|
155
155
|
try writer.writeAll(" ");
|
|
156
156
|
try writeJsonShellArg(writer, app_id);
|
|
157
157
|
try writer.writeAll(
|
|
158
|
-
\\ --runs 20 --trace-root traces/zmr-android-reliability --min-pass-rate 100 --max-failures 0 --max-p95-ms 30000 && \"$ZMR_BIN\" report traces/zmr-android-reliability --out traces/zmr-android-reliability/report.html",
|
|
158
|
+
\\ --runs 20 --trace-root traces/zmr-android-reliability --min-pass-rate 100 --max-failures 0 --max-p95-ms 30000 && \"$ZMR_BIN\" report traces/zmr-android-reliability --out traces/zmr-android-reliability/report.html --junit traces/zmr-android-reliability/junit.xml",
|
|
159
159
|
\\ "ios": "zmr run .zmr/ios-smoke.json --platform ios --device booted --trace-dir traces/zmr-ios",
|
|
160
|
-
\\ "iosReport": "zmr report traces/zmr-ios --out traces/zmr-ios/report.html",
|
|
160
|
+
\\ "iosReport": "zmr report traces/zmr-ios --out traces/zmr-ios/report.html --junit traces/zmr-ios/junit.xml",
|
|
161
161
|
\\ "iosReliability": "export ZMR_BIN=\"${ZMR_BIN:-zmr}\"; zmr-benchmark --zmr .zmr/ios-smoke.json --platform ios --device booted --app-id
|
|
162
162
|
);
|
|
163
163
|
try writer.writeAll(" ");
|
|
164
164
|
try writeJsonShellArg(writer, app_id);
|
|
165
165
|
try writer.writeAll(
|
|
166
|
-
\\ --xcrun xcrun --runs 20 --trace-root traces/zmr-ios-reliability --min-pass-rate 100 --max-failures 0 --max-p95-ms 45000 && \"$ZMR_BIN\" report traces/zmr-ios-reliability --out traces/zmr-ios-reliability/report.html",
|
|
166
|
+
\\ --xcrun xcrun --runs 20 --trace-root traces/zmr-ios-reliability --min-pass-rate 100 --max-failures 0 --max-p95-ms 45000 && \"$ZMR_BIN\" report traces/zmr-ios-reliability --out traces/zmr-ios-reliability/report.html --junit traces/zmr-ios-reliability/junit.xml",
|
|
167
167
|
\\ "matrix": "ZMR_BIN=${ZMR_BIN:-zmr} zmr-device-matrix --matrix .zmr/device-matrix.json --trace-root traces/zmr-matrix --min-pass-rate 100 --max-failures 0",
|
|
168
168
|
\\ "pilotGate": "zmr-pilot-gate --android --ios --android-app-root . --android-app-id
|
|
169
169
|
);
|
|
@@ -318,6 +318,7 @@ fn writeAgentInstructions(path: []const u8, app_id: []const u8, force: bool) !vo
|
|
|
318
318
|
\\## Setup Checks
|
|
319
319
|
\\
|
|
320
320
|
\\```bash
|
|
321
|
+
\\zmr inspect --json --dir .
|
|
321
322
|
\\zmr doctor --strict --json --config .zmr/config.json
|
|
322
323
|
\\zmr schemas --json
|
|
323
324
|
\\zmr validate --json .zmr/android-smoke.json && zmr validate --json .zmr/ios-smoke.json
|
|
@@ -332,6 +333,19 @@ fn writeAgentInstructions(path: []const u8, app_id: []const u8, force: bool) !vo
|
|
|
332
333
|
\\
|
|
333
334
|
\\Use `semantic_snapshot` before choosing tap or type actions. Prefer selectors from accessibility identifiers, resource ids, labels, or exact text before coordinates. Export redacted traces before sharing artifacts.
|
|
334
335
|
\\
|
|
336
|
+
\\## Discover From Trace
|
|
337
|
+
\\
|
|
338
|
+
\\```bash
|
|
339
|
+
\\zmr explore --from-trace traces/zmr-agent --out .zmr/discovered/login-smoke.json --goal "find a stable login smoke" --include-actions --validate --json
|
|
340
|
+
\\zmr discover --from-trace traces/zmr-agent --out .zmr/discovered/replay-smoke.json --include-actions --validate --json
|
|
341
|
+
\\zmr draft --from-trace traces/zmr-agent --out .zmr/discovered/surface-smoke.json --json
|
|
342
|
+
\\zmr validate --json .zmr/discovered/surface-smoke.json
|
|
343
|
+
\\zmr draft --from-trace traces/zmr-agent --out .zmr/discovered/replay-smoke.json --include-actions --json
|
|
344
|
+
\\zmr validate --json .zmr/discovered/replay-smoke.json
|
|
345
|
+
\\```
|
|
346
|
+
\\
|
|
347
|
+
\\Prefer `zmr explore` for CLI agent loops when the goal should travel with the generated candidate. Its JSON includes `autonomous:false`, `reviewRequired:true`, `guardrails`, replay coverage, validation, and deterministic next commands. Treat discover output as a lower-level reviewable starting point. It writes from trace evidence and validates the generated file when `--validate` is present, but it does not crawl, invent missing actions, discover credentials, or commit tests. Treat draft output as a reviewable starting point when using the lower-level split workflow. The default draft contains only `launch`, `snapshot`, and conservative `assertVisible` checks. Use `--include-actions` only when the trace came from a reviewed agent session; unsupported events are skipped with warnings instead of guessed. Do not commit a discovered or drafted scenario until a human has reviewed and rerun it.
|
|
348
|
+
\\
|
|
335
349
|
\\## Failure Triage
|
|
336
350
|
\\
|
|
337
351
|
\\```bash
|
|
@@ -352,21 +366,21 @@ fn writeAgentInstructions(path: []const u8, app_id: []const u8, force: bool) !vo
|
|
|
352
366
|
\\
|
|
353
367
|
\\```bash
|
|
354
368
|
\\zmr run .zmr/android-smoke.json --device emulator-5554 --trace-dir traces/zmr-android
|
|
355
|
-
\\zmr report traces/zmr-android --out traces/zmr-android/report.html
|
|
369
|
+
\\zmr report traces/zmr-android --out traces/zmr-android/report.html --junit traces/zmr-android/junit.xml
|
|
356
370
|
\\export ZMR_BIN="${ZMR_BIN:-zmr}"; zmr-benchmark --zmr .zmr/android-smoke.json --device emulator-5554 --app-id
|
|
357
371
|
);
|
|
358
372
|
try writer.writeAll(" ");
|
|
359
373
|
try writeShellArg(writer, app_id);
|
|
360
374
|
try writer.writeAll(
|
|
361
|
-
\\ --runs 20 --trace-root traces/zmr-android-reliability --min-pass-rate 100 --max-failures 0 --max-p95-ms 30000 && "$ZMR_BIN" report traces/zmr-android-reliability --out traces/zmr-android-reliability/report.html
|
|
375
|
+
\\ --runs 20 --trace-root traces/zmr-android-reliability --min-pass-rate 100 --max-failures 0 --max-p95-ms 30000 && "$ZMR_BIN" report traces/zmr-android-reliability --out traces/zmr-android-reliability/report.html --junit traces/zmr-android-reliability/junit.xml
|
|
362
376
|
\\zmr run .zmr/ios-smoke.json --platform ios --device booted --trace-dir traces/zmr-ios
|
|
363
|
-
\\zmr report traces/zmr-ios --out traces/zmr-ios/report.html
|
|
377
|
+
\\zmr report traces/zmr-ios --out traces/zmr-ios/report.html --junit traces/zmr-ios/junit.xml
|
|
364
378
|
\\export ZMR_BIN="${ZMR_BIN:-zmr}"; zmr-benchmark --zmr .zmr/ios-smoke.json --platform ios --device booted --app-id
|
|
365
379
|
);
|
|
366
380
|
try writer.writeAll(" ");
|
|
367
381
|
try writeShellArg(writer, app_id);
|
|
368
382
|
try writer.writeAll(
|
|
369
|
-
\\ --xcrun xcrun --runs 20 --trace-root traces/zmr-ios-reliability --min-pass-rate 100 --max-failures 0 --max-p95-ms 45000 && "$ZMR_BIN" report traces/zmr-ios-reliability --out traces/zmr-ios-reliability/report.html
|
|
383
|
+
\\ --xcrun xcrun --runs 20 --trace-root traces/zmr-ios-reliability --min-pass-rate 100 --max-failures 0 --max-p95-ms 45000 && "$ZMR_BIN" report traces/zmr-ios-reliability --out traces/zmr-ios-reliability/report.html --junit traces/zmr-ios-reliability/junit.xml
|
|
370
384
|
\\```
|
|
371
385
|
\\
|
|
372
386
|
\\## Release Claims
|
|
@@ -380,25 +394,26 @@ fn writeAgentInstructions(path: []const u8, app_id: []const u8, force: bool) !vo
|
|
|
380
394
|
\\## App Commands
|
|
381
395
|
\\
|
|
382
396
|
\\```bash
|
|
397
|
+
\\zmr inspect --json --dir .
|
|
383
398
|
\\zmr doctor --strict --json --config .zmr/config.json
|
|
384
399
|
\\zmr schemas --json
|
|
385
400
|
\\zmr validate --json .zmr/android-smoke.json && zmr validate --json .zmr/ios-smoke.json
|
|
386
401
|
\\zmr run .zmr/android-smoke.json --device emulator-5554 --trace-dir traces/zmr-android
|
|
387
|
-
\\zmr report traces/zmr-android --out traces/zmr-android/report.html
|
|
402
|
+
\\zmr report traces/zmr-android --out traces/zmr-android/report.html --junit traces/zmr-android/junit.xml
|
|
388
403
|
\\export ZMR_BIN="${ZMR_BIN:-zmr}"; zmr-benchmark --zmr .zmr/android-smoke.json --device emulator-5554 --app-id
|
|
389
404
|
);
|
|
390
405
|
try writer.writeAll(" ");
|
|
391
406
|
try writeShellArg(writer, app_id);
|
|
392
407
|
try writer.writeAll(
|
|
393
|
-
\\ --runs 20 --trace-root traces/zmr-android-reliability --min-pass-rate 100 --max-failures 0 --max-p95-ms 30000 && "$ZMR_BIN" report traces/zmr-android-reliability --out traces/zmr-android-reliability/report.html
|
|
408
|
+
\\ --runs 20 --trace-root traces/zmr-android-reliability --min-pass-rate 100 --max-failures 0 --max-p95-ms 30000 && "$ZMR_BIN" report traces/zmr-android-reliability --out traces/zmr-android-reliability/report.html --junit traces/zmr-android-reliability/junit.xml
|
|
394
409
|
\\zmr run .zmr/ios-smoke.json --platform ios --device booted --trace-dir traces/zmr-ios
|
|
395
|
-
\\zmr report traces/zmr-ios --out traces/zmr-ios/report.html
|
|
410
|
+
\\zmr report traces/zmr-ios --out traces/zmr-ios/report.html --junit traces/zmr-ios/junit.xml
|
|
396
411
|
\\export ZMR_BIN="${ZMR_BIN:-zmr}"; zmr-benchmark --zmr .zmr/ios-smoke.json --platform ios --device booted --app-id
|
|
397
412
|
);
|
|
398
413
|
try writer.writeAll(" ");
|
|
399
414
|
try writeShellArg(writer, app_id);
|
|
400
415
|
try writer.writeAll(
|
|
401
|
-
\\ --xcrun xcrun --runs 20 --trace-root traces/zmr-ios-reliability --min-pass-rate 100 --max-failures 0 --max-p95-ms 45000 && "$ZMR_BIN" report traces/zmr-ios-reliability --out traces/zmr-ios-reliability/report.html
|
|
416
|
+
\\ --xcrun xcrun --runs 20 --trace-root traces/zmr-ios-reliability --min-pass-rate 100 --max-failures 0 --max-p95-ms 45000 && "$ZMR_BIN" report traces/zmr-ios-reliability --out traces/zmr-ios-reliability/report.html --junit traces/zmr-ios-reliability/junit.xml
|
|
402
417
|
\\ZMR_BIN=${ZMR_BIN:-zmr} zmr-device-matrix --matrix .zmr/device-matrix.json --trace-root traces/zmr-matrix --min-pass-rate 100 --max-failures 0
|
|
403
418
|
);
|
|
404
419
|
try writer.writeAll("zmr-pilot-gate --android --ios --android-app-root . --android-app-id ");
|
|
@@ -410,6 +425,7 @@ fn writeAgentInstructions(path: []const u8, app_id: []const u8, force: bool) !vo
|
|
|
410
425
|
\\zmr-release-readiness --evidence traces/zmr-pilots/evidence.jsonl --target production --json
|
|
411
426
|
\\zmr serve --transport stdio --config .zmr/config.json --trace-dir traces/zmr-agent
|
|
412
427
|
\\zmr mcp --config .zmr/config.json --trace-dir traces/zmr-agent
|
|
428
|
+
\\zmr discover --from-trace traces/zmr-agent --out .zmr/discovered/replay-smoke.json --include-actions --validate --json
|
|
413
429
|
\\zmr explain traces/zmr-agent --json
|
|
414
430
|
\\zmr export traces/zmr-agent --out traces/zmr-agent-redacted.zmrtrace --redact
|
|
415
431
|
\\```
|
package/src/scenario.zig
CHANGED
|
@@ -25,6 +25,15 @@ pub const WaitAny = struct {
|
|
|
25
25
|
}
|
|
26
26
|
};
|
|
27
27
|
|
|
28
|
+
pub const VisibilityAssertion = struct {
|
|
29
|
+
selector: selector.Selector,
|
|
30
|
+
timeout_ms: ?u64 = null,
|
|
31
|
+
|
|
32
|
+
pub fn deinit(self: VisibilityAssertion, allocator: std.mem.Allocator) void {
|
|
33
|
+
self.selector.deinit(allocator);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
28
37
|
pub const TypeText = struct {
|
|
29
38
|
selector: ?selector.Selector = null,
|
|
30
39
|
text: []const u8,
|
|
@@ -105,8 +114,8 @@ pub const Step = union(enum) {
|
|
|
105
114
|
wait_visible: WaitVisible,
|
|
106
115
|
wait_not_visible: WaitVisible,
|
|
107
116
|
wait_any: WaitAny,
|
|
108
|
-
assert_visible:
|
|
109
|
-
assert_not_visible:
|
|
117
|
+
assert_visible: VisibilityAssertion,
|
|
118
|
+
assert_not_visible: VisibilityAssertion,
|
|
110
119
|
assert_none_visible: WaitAny,
|
|
111
120
|
assert_healthy_timeout_ms: u64,
|
|
112
121
|
optional: *Step,
|
|
@@ -265,8 +274,22 @@ fn parseRawStep(allocator: std.mem.Allocator, object: std.json.ObjectMap) anyerr
|
|
|
265
274
|
.timeout_ms = try fields.optionalU64(object, "timeoutMs", 5000),
|
|
266
275
|
} };
|
|
267
276
|
}
|
|
268
|
-
if (std.mem.eql(u8, action, "assertVisible"))
|
|
269
|
-
|
|
277
|
+
if (std.mem.eql(u8, action, "assertVisible")) {
|
|
278
|
+
const wanted = try fields.parseSelectorField(allocator, object);
|
|
279
|
+
errdefer wanted.deinit(allocator);
|
|
280
|
+
return .{ .assert_visible = .{
|
|
281
|
+
.selector = wanted,
|
|
282
|
+
.timeout_ms = try optionalTimeoutMs(object),
|
|
283
|
+
} };
|
|
284
|
+
}
|
|
285
|
+
if (std.mem.eql(u8, action, "assertNotVisible")) {
|
|
286
|
+
const wanted = try fields.parseSelectorField(allocator, object);
|
|
287
|
+
errdefer wanted.deinit(allocator);
|
|
288
|
+
return .{ .assert_not_visible = .{
|
|
289
|
+
.selector = wanted,
|
|
290
|
+
.timeout_ms = try optionalTimeoutMs(object),
|
|
291
|
+
} };
|
|
292
|
+
}
|
|
270
293
|
if (std.mem.eql(u8, action, "assertHealthy")) return .{ .assert_healthy_timeout_ms = try fields.optionalU64(object, "timeoutMs", 0) };
|
|
271
294
|
if (std.mem.eql(u8, action, "assertNoneVisible")) {
|
|
272
295
|
const selectors = try fields.parseSelectorArrayField(allocator, object);
|
|
@@ -344,3 +367,8 @@ fn optionalDirection(object: std.json.ObjectMap, key: []const u8, default_value:
|
|
|
344
367
|
if (std.mem.eql(u8, value.string, "up")) return .up;
|
|
345
368
|
return error.UnknownScrollDirection;
|
|
346
369
|
}
|
|
370
|
+
|
|
371
|
+
fn optionalTimeoutMs(object: std.json.ObjectMap) !?u64 {
|
|
372
|
+
if (object.get("timeoutMs") == null) return null;
|
|
373
|
+
return try fields.optionalU64(object, "timeoutMs", 0);
|
|
374
|
+
}
|
package/src/schema_registry.zig
CHANGED
|
@@ -26,6 +26,10 @@ const public_schemas = [_]PublicSchema{
|
|
|
26
26
|
.{ .name = "capabilities-output", .path = "schemas/capabilities-output.schema.json", .id = "https://zmr.dev/schemas/capabilities-output.schema.json", .description = "Machine-readable runner.capabilities JSON-RPC result" },
|
|
27
27
|
.{ .name = "explain-output", .path = "schemas/explain-output.schema.json", .id = "https://zmr.dev/schemas/explain-output.schema.json", .description = "Machine-readable zmr explain --json failure triage output" },
|
|
28
28
|
.{ .name = "run-output", .path = "schemas/run-output.schema.json", .id = "https://zmr.dev/schemas/run-output.schema.json", .description = "Machine-readable zmr run --json terminal summary output" },
|
|
29
|
+
.{ .name = "inspect-output", .path = "schemas/inspect-output.schema.json", .id = "https://zmr.dev/schemas/inspect-output.schema.json", .description = "Machine-readable zmr inspect --json app and agent handoff output" },
|
|
30
|
+
.{ .name = "discover-output", .path = "schemas/discover-output.schema.json", .id = "https://zmr.dev/schemas/discover-output.schema.json", .description = "Machine-readable zmr discover --json trace-backed scenario discovery output" },
|
|
31
|
+
.{ .name = "explore-output", .path = "schemas/explore-output.schema.json", .id = "https://zmr.dev/schemas/explore-output.schema.json", .description = "Machine-readable zmr explore --json review-first trace exploration output" },
|
|
32
|
+
.{ .name = "draft-output", .path = "schemas/draft-output.schema.json", .id = "https://zmr.dev/schemas/draft-output.schema.json", .description = "Machine-readable zmr draft --json scenario draft output" },
|
|
29
33
|
.{ .name = "release-manifest", .path = "schemas/release-manifest.schema.json", .id = "https://zmr.dev/schemas/release-manifest.schema.json", .description = "Machine-readable RELEASE_MANIFEST.json emitted with release archives" },
|
|
30
34
|
.{ .name = "release-readiness-output", .path = "schemas/release-readiness-output.schema.json", .id = "https://zmr.dev/schemas/release-readiness-output.schema.json", .description = "Machine-readable zmr-release-readiness --json release evidence gate output" },
|
|
31
35
|
.{ .name = "schemas-output", .path = "schemas/schemas-output.schema.json", .id = "https://zmr.dev/schemas/schemas-output.schema.json", .description = "Machine-readable zmr schemas --json public schema index" },
|
package/src/version.zig
CHANGED
package/viewer/app.js
CHANGED
|
@@ -73,22 +73,42 @@ els.dropTarget.addEventListener("drop", async (event) => {
|
|
|
73
73
|
});
|
|
74
74
|
|
|
75
75
|
async function loadBundleFile(file) {
|
|
76
|
-
|
|
76
|
+
await loadBundle(file.name, () => file.arrayBuffer());
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function loadBundleFromUrl(url) {
|
|
80
|
+
const name = url.split("/").pop() || url;
|
|
81
|
+
await loadBundle(name, async () => {
|
|
82
|
+
const response = await fetch(url);
|
|
83
|
+
if (!response.ok) {
|
|
84
|
+
throw new Error(`Failed to fetch ${url}: HTTP ${response.status}`);
|
|
85
|
+
}
|
|
86
|
+
return await response.arrayBuffer();
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function loadBundle(name, getBuffer) {
|
|
91
|
+
setStatus(`Loading ${name}`);
|
|
77
92
|
try {
|
|
78
93
|
stopReplay();
|
|
79
94
|
revokeObjectUrls();
|
|
80
|
-
const entries = parseTarArchive(await
|
|
95
|
+
const entries = parseTarArchive(await getBuffer());
|
|
81
96
|
const model = buildTraceModel(entries);
|
|
82
97
|
state.model = model;
|
|
83
98
|
state.selectedFrameIndex = 0;
|
|
84
99
|
state.selectedEvent = model.replayFrames[0]?.event ?? model.events[0] ?? null;
|
|
85
|
-
renderModel(
|
|
100
|
+
renderModel(name);
|
|
86
101
|
} catch (error) {
|
|
87
102
|
console.error(error);
|
|
88
103
|
setStatus(error instanceof Error ? error.message : String(error), true);
|
|
89
104
|
}
|
|
90
105
|
}
|
|
91
106
|
|
|
107
|
+
const bundleParam = new URLSearchParams(window.location.search).get("bundle");
|
|
108
|
+
if (bundleParam) {
|
|
109
|
+
loadBundleFromUrl(bundleParam);
|
|
110
|
+
}
|
|
111
|
+
|
|
92
112
|
function renderModel(fileName) {
|
|
93
113
|
const { summary } = state.model;
|
|
94
114
|
els.emptyState.classList.add("hidden");
|