zig-mobile-runner 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (225) hide show
  1. package/CHANGELOG.md +484 -0
  2. package/CONTRIBUTING.md +42 -0
  3. package/FEATURES.md +112 -0
  4. package/LICENSE +21 -0
  5. package/README.md +255 -0
  6. package/SECURITY.md +34 -0
  7. package/build.zig +38 -0
  8. package/build.zig.zon +7 -0
  9. package/clients/README.md +144 -0
  10. package/clients/go/README.md +24 -0
  11. package/clients/go/examples/fake-session/main.go +93 -0
  12. package/clients/go/go.mod +3 -0
  13. package/clients/go/zmr/client.go +432 -0
  14. package/clients/kotlin/README.md +35 -0
  15. package/clients/kotlin/build.gradle.kts +35 -0
  16. package/clients/kotlin/settings.gradle.kts +15 -0
  17. package/clients/kotlin/src/main/kotlin/dev/zmr/FakeSession.kt +86 -0
  18. package/clients/kotlin/src/main/kotlin/dev/zmr/ZmrClient.kt +67 -0
  19. package/clients/python/README.md +29 -0
  20. package/clients/python/examples/fake_session.py +48 -0
  21. package/clients/python/pyproject.toml +13 -0
  22. package/clients/python/zmr_client.py +202 -0
  23. package/clients/rust/Cargo.lock +107 -0
  24. package/clients/rust/Cargo.toml +10 -0
  25. package/clients/rust/README.md +19 -0
  26. package/clients/rust/examples/fake_session.rs +70 -0
  27. package/clients/rust/src/lib.rs +461 -0
  28. package/clients/swift/Package.swift +16 -0
  29. package/clients/swift/README.md +36 -0
  30. package/clients/swift/Sources/ZMRClient/ZMRClient.swift +114 -0
  31. package/clients/swift/Sources/ZMRFakeSession/main.swift +86 -0
  32. package/clients/typescript/README.md +34 -0
  33. package/clients/typescript/examples/fake-session.mjs +36 -0
  34. package/clients/typescript/index.d.ts +144 -0
  35. package/clients/typescript/index.mjs +192 -0
  36. package/clients/typescript/package.json +8 -0
  37. package/docs/adr/0001-agent-native-runner-boundary.md +31 -0
  38. package/docs/adr/0002-app-local-zmr-contract.md +39 -0
  39. package/docs/adr/0003-ios-simulator-xctest-shim.md +41 -0
  40. package/docs/adr/0004-benchmark-claims-and-baseline-collection.md +37 -0
  41. package/docs/adr/README.md +12 -0
  42. package/docs/ai-agents.md +156 -0
  43. package/docs/app-integration.md +316 -0
  44. package/docs/benchmarking.md +275 -0
  45. package/docs/client-installation.md +141 -0
  46. package/docs/clients.md +98 -0
  47. package/docs/config.md +175 -0
  48. package/docs/demo.md +259 -0
  49. package/docs/dsl.md +57 -0
  50. package/docs/install.md +233 -0
  51. package/docs/market-positioning.md +70 -0
  52. package/docs/npm.md +359 -0
  53. package/docs/protocol-fixtures/README.md +8 -0
  54. package/docs/protocol-fixtures/core-session.requests.jsonl +8 -0
  55. package/docs/protocol-fixtures/core-session.responses.jsonl +8 -0
  56. package/docs/protocol-versioning.md +65 -0
  57. package/docs/protocol.md +560 -0
  58. package/docs/publication.md +77 -0
  59. package/docs/release-audit.md +99 -0
  60. package/docs/release-candidate.md +111 -0
  61. package/docs/release-evidence.md +188 -0
  62. package/docs/release-notes-template.md +58 -0
  63. package/docs/roadmap.md +334 -0
  64. package/docs/scenario-authoring.md +88 -0
  65. package/docs/shipping.md +170 -0
  66. package/docs/trace-privacy.md +88 -0
  67. package/docs/troubleshooting.md +256 -0
  68. package/examples/android-app-auth-probe.json +89 -0
  69. package/examples/android-app-error-state.json +13 -0
  70. package/examples/android-app-login-smoke.json +192 -0
  71. package/examples/android-app-onboarding.json +12 -0
  72. package/examples/android-app-referral-deep-link.json +12 -0
  73. package/examples/android-shim-smoke.json +19 -0
  74. package/examples/demo-failure.json +12 -0
  75. package/examples/demo-fake.json +14 -0
  76. package/examples/ios-dev-client-open-link.json +26 -0
  77. package/examples/ios-dev-client-route-snapshot.json +24 -0
  78. package/examples/ios-shim-smoke.json +23 -0
  79. package/examples/ios-smoke.json +9 -0
  80. package/go.work +3 -0
  81. package/npm/agents.mjs +183 -0
  82. package/npm/app-config.mjs +95 -0
  83. package/npm/build-zmr.mjs +21 -0
  84. package/npm/commands.mjs +104 -0
  85. package/npm/generated-files.mjs +50 -0
  86. package/npm/index.mjs +75 -0
  87. package/npm/init-app.mjs +80 -0
  88. package/npm/package-scripts.mjs +72 -0
  89. package/npm/postinstall.mjs +21 -0
  90. package/npm/scaffold.mjs +179 -0
  91. package/npm/scenarios.mjs +93 -0
  92. package/npm/setup.mjs +69 -0
  93. package/npm/wizard.mjs +117 -0
  94. package/npm/zmr.mjs +23 -0
  95. package/package.json +114 -0
  96. package/prebuilds/darwin-arm64/zmr +0 -0
  97. package/prebuilds/darwin-x64/zmr +0 -0
  98. package/prebuilds/linux-arm64/zmr +0 -0
  99. package/prebuilds/linux-x64/zmr +0 -0
  100. package/schemas/README.md +26 -0
  101. package/schemas/action-result.schema.json +27 -0
  102. package/schemas/capabilities-output.schema.json +98 -0
  103. package/schemas/devices-output.schema.json +25 -0
  104. package/schemas/doctor-output.schema.json +51 -0
  105. package/schemas/explain-output.schema.json +51 -0
  106. package/schemas/import-output.schema.json +23 -0
  107. package/schemas/init-output.schema.json +71 -0
  108. package/schemas/json-rpc.schema.json +55 -0
  109. package/schemas/release-manifest.schema.json +43 -0
  110. package/schemas/release-readiness-output.schema.json +127 -0
  111. package/schemas/run-output.schema.json +43 -0
  112. package/schemas/scenario.schema.json +128 -0
  113. package/schemas/schemas-output.schema.json +26 -0
  114. package/schemas/semantic-snapshot.schema.json +116 -0
  115. package/schemas/snapshot.schema.json +60 -0
  116. package/schemas/trace-event.schema.json +14 -0
  117. package/schemas/trace-manifest.schema.json +59 -0
  118. package/schemas/validate-output.schema.json +42 -0
  119. package/schemas/version-output.schema.json +23 -0
  120. package/schemas/zmr-config.schema.json +75 -0
  121. package/scripts/android-emulator.sh +126 -0
  122. package/scripts/assert-ios-physical-ready.sh +213 -0
  123. package/scripts/benchmark-command.sh +307 -0
  124. package/scripts/benchmark.sh +359 -0
  125. package/scripts/benchmark_gate.py +117 -0
  126. package/scripts/benchmark_result_row.py +88 -0
  127. package/scripts/compare-benchmarks.py +288 -0
  128. package/scripts/create-android-demo-app.sh +342 -0
  129. package/scripts/create-ios-demo-app.sh +261 -0
  130. package/scripts/demo-android-real.sh +232 -0
  131. package/scripts/demo-ios-real.sh +270 -0
  132. package/scripts/demo.sh +464 -0
  133. package/scripts/device-matrix.sh +338 -0
  134. package/scripts/ensure-ios-shim-target.rb +237 -0
  135. package/scripts/install-android-shim.sh +281 -0
  136. package/scripts/install-ios-shim.sh +589 -0
  137. package/scripts/pilot-gate.sh +560 -0
  138. package/scripts/release-readiness.py +838 -0
  139. package/scripts/release-readiness.sh +91 -0
  140. package/scripts/run-android-pilot.sh +561 -0
  141. package/scripts/run-ios-pilot.sh +509 -0
  142. package/shims/android/README.md +21 -0
  143. package/shims/android/ZMRShimInstrumentedTest.java +152 -0
  144. package/shims/android/protocol.md +18 -0
  145. package/shims/ios/README.md +50 -0
  146. package/shims/ios/ZMRShim.swift +110 -0
  147. package/shims/ios/ZMRShimUITestCase.swift +475 -0
  148. package/shims/ios/protocol.md +74 -0
  149. package/skills/zmr-mobile-testing/SKILL.md +127 -0
  150. package/src/android.zig +344 -0
  151. package/src/android_device_info.zig +99 -0
  152. package/src/android_emulator.zig +154 -0
  153. package/src/android_screen_recording.zig +112 -0
  154. package/src/android_shell.zig +112 -0
  155. package/src/bundle.zig +124 -0
  156. package/src/bundle_redaction.zig +272 -0
  157. package/src/bundle_tar.zig +123 -0
  158. package/src/cli_devices.zig +97 -0
  159. package/src/cli_doctor.zig +114 -0
  160. package/src/cli_import.zig +70 -0
  161. package/src/cli_info.zig +39 -0
  162. package/src/cli_init.zig +72 -0
  163. package/src/cli_output.zig +467 -0
  164. package/src/cli_run.zig +259 -0
  165. package/src/cli_serve.zig +287 -0
  166. package/src/cli_trace.zig +111 -0
  167. package/src/cli_validate.zig +41 -0
  168. package/src/command.zig +211 -0
  169. package/src/config.zig +305 -0
  170. package/src/config_diagnostics.zig +212 -0
  171. package/src/config_paths.zig +49 -0
  172. package/src/device_registry.zig +37 -0
  173. package/src/doctor.zig +412 -0
  174. package/src/doctor_hints.zig +52 -0
  175. package/src/errors.zig +55 -0
  176. package/src/fake_device.zig +163 -0
  177. package/src/health.zig +28 -0
  178. package/src/importer.zig +343 -0
  179. package/src/importer_json.zig +100 -0
  180. package/src/importer_model.zig +103 -0
  181. package/src/ios.zig +399 -0
  182. package/src/ios_devices.zig +219 -0
  183. package/src/ios_lifecycle.zig +72 -0
  184. package/src/ios_shim.zig +242 -0
  185. package/src/ios_snapshot.zig +20 -0
  186. package/src/json_fields.zig +80 -0
  187. package/src/json_rpc.zig +150 -0
  188. package/src/json_rpc_methods.zig +318 -0
  189. package/src/json_rpc_observation.zig +31 -0
  190. package/src/json_rpc_params.zig +52 -0
  191. package/src/json_rpc_protocol.zig +110 -0
  192. package/src/json_rpc_trace.zig +73 -0
  193. package/src/main.zig +135 -0
  194. package/src/mcp.zig +234 -0
  195. package/src/mcp_protocol.zig +64 -0
  196. package/src/mcp_trace.zig +83 -0
  197. package/src/report.zig +346 -0
  198. package/src/report_html.zig +63 -0
  199. package/src/report_values.zig +27 -0
  200. package/src/run_options.zig +152 -0
  201. package/src/runner.zig +280 -0
  202. package/src/runner_actions.zig +109 -0
  203. package/src/runner_config.zig +6 -0
  204. package/src/runner_diagnostics.zig +268 -0
  205. package/src/runner_events.zig +170 -0
  206. package/src/runner_native.zig +88 -0
  207. package/src/runner_waits.zig +300 -0
  208. package/src/scaffold.zig +472 -0
  209. package/src/scenario.zig +346 -0
  210. package/src/scenario_fields.zig +50 -0
  211. package/src/schema_registry.zig +53 -0
  212. package/src/selector.zig +84 -0
  213. package/src/semantic.zig +171 -0
  214. package/src/trace.zig +315 -0
  215. package/src/trace_json.zig +340 -0
  216. package/src/trace_summary.zig +218 -0
  217. package/src/trace_summary_diagnostic.zig +202 -0
  218. package/src/types.zig +120 -0
  219. package/src/uiautomator.zig +164 -0
  220. package/src/validation.zig +187 -0
  221. package/src/version.zig +22 -0
  222. package/viewer/app.js +373 -0
  223. package/viewer/index.html +126 -0
  224. package/viewer/parser.js +233 -0
  225. package/viewer/styles.css +585 -0
@@ -0,0 +1,318 @@
1
+ const std = @import("std");
2
+ const bundle = @import("bundle.zig");
3
+ const observation = @import("json_rpc_observation.zig");
4
+ const params_parser = @import("json_rpc_params.zig");
5
+ const protocol = @import("json_rpc_protocol.zig");
6
+ const rpc_trace = @import("json_rpc_trace.zig");
7
+ const runner = @import("runner.zig");
8
+ const selector = @import("selector.zig");
9
+ const trace = @import("trace.zig");
10
+
11
+ pub fn dispatchMethod(
12
+ allocator: std.mem.Allocator,
13
+ device: anytype,
14
+ method: []const u8,
15
+ params: ?std.json.Value,
16
+ id: ?std.json.Value,
17
+ writer: anytype,
18
+ live_trace: ?*trace.TraceWriter,
19
+ ) !void {
20
+ if (try dispatchCoreMethod(allocator, device, method, id, writer)) return;
21
+ if (try dispatchAppMethod(device, method, params, id, writer)) return;
22
+ if (try dispatchObserveMethod(device, method, id, writer, live_trace)) return;
23
+ if (try dispatchUiMethod(allocator, device, method, params, id, writer, live_trace)) return;
24
+ if (try dispatchWaitMethod(allocator, device, method, params, id, writer, live_trace)) return;
25
+ if (try dispatchAssertMethod(allocator, device, method, params, id, writer, live_trace)) return;
26
+ if (try dispatchTraceMethod(allocator, method, params, id, writer, live_trace)) return;
27
+
28
+ try protocol.writeError(writer, id, -32601, "method not found");
29
+ }
30
+
31
+ fn dispatchCoreMethod(
32
+ allocator: std.mem.Allocator,
33
+ device: anytype,
34
+ method: []const u8,
35
+ id: ?std.json.Value,
36
+ writer: anytype,
37
+ ) !bool {
38
+ if (std.mem.eql(u8, method, "runner.capabilities")) {
39
+ try protocol.writeCapabilitiesResult(writer, id);
40
+ return true;
41
+ }
42
+ if (std.mem.eql(u8, method, "device.list")) {
43
+ const devices = try device.listDevices();
44
+ defer {
45
+ for (devices) |info| info.deinit(allocator);
46
+ allocator.free(devices);
47
+ }
48
+ try protocol.writeDevicesResult(writer, id, devices);
49
+ return true;
50
+ }
51
+ if (std.mem.eql(u8, method, "session.create")) {
52
+ try protocol.writeResultRaw(writer, id, "{\"sessionId\":\"default\"}");
53
+ return true;
54
+ }
55
+ if (std.mem.eql(u8, method, "session.close")) {
56
+ try protocol.writeResultRaw(writer, id, "true");
57
+ return true;
58
+ }
59
+ return false;
60
+ }
61
+
62
+ fn dispatchAppMethod(
63
+ device: anytype,
64
+ method: []const u8,
65
+ params: ?std.json.Value,
66
+ id: ?std.json.Value,
67
+ writer: anytype,
68
+ ) !bool {
69
+ if (std.mem.eql(u8, method, "app.install")) {
70
+ const path = try params_parser.requiredString(params, "path");
71
+ try device.install(path);
72
+ try protocol.writeResultRaw(writer, id, "true");
73
+ return true;
74
+ }
75
+ if (std.mem.eql(u8, method, "app.launch")) {
76
+ try device.launch();
77
+ try protocol.writeResultRaw(writer, id, "true");
78
+ return true;
79
+ }
80
+ if (std.mem.eql(u8, method, "app.stop")) {
81
+ try device.stop();
82
+ try protocol.writeResultRaw(writer, id, "true");
83
+ return true;
84
+ }
85
+ if (std.mem.eql(u8, method, "app.clearState")) {
86
+ try device.clearState();
87
+ try protocol.writeResultRaw(writer, id, "true");
88
+ return true;
89
+ }
90
+ if (std.mem.eql(u8, method, "app.openLink")) {
91
+ const url = try params_parser.requiredString(params, "url");
92
+ try device.openLink(url);
93
+ try protocol.writeResultRaw(writer, id, "true");
94
+ return true;
95
+ }
96
+ return false;
97
+ }
98
+
99
+ fn dispatchObserveMethod(
100
+ device: anytype,
101
+ method: []const u8,
102
+ id: ?std.json.Value,
103
+ writer: anytype,
104
+ live_trace: ?*trace.TraceWriter,
105
+ ) !bool {
106
+ if (std.mem.eql(u8, method, "observe.snapshot")) {
107
+ var snap = try device.snapshot(live_trace);
108
+ defer snap.deinit(device.allocator);
109
+ if (live_trace) |tw| try observation.recordArtifact(tw, "observe.snapshot", snap);
110
+ try observation.writeResult(writer, id, snap, .raw);
111
+ return true;
112
+ }
113
+ if (std.mem.eql(u8, method, "observe.semanticSnapshot")) {
114
+ var snap = try device.snapshot(live_trace);
115
+ defer snap.deinit(device.allocator);
116
+ if (live_trace) |tw| try observation.recordArtifact(tw, "observe.semanticSnapshot", snap);
117
+ try observation.writeResult(writer, id, snap, .semantic);
118
+ return true;
119
+ }
120
+ return false;
121
+ }
122
+
123
+ fn dispatchUiMethod(
124
+ allocator: std.mem.Allocator,
125
+ device: anytype,
126
+ method: []const u8,
127
+ params: ?std.json.Value,
128
+ id: ?std.json.Value,
129
+ writer: anytype,
130
+ live_trace: ?*trace.TraceWriter,
131
+ ) !bool {
132
+ if (std.mem.eql(u8, method, "ui.tap")) {
133
+ const wanted = try params_parser.selectorParam(allocator, params);
134
+ defer wanted.deinit(allocator);
135
+ try runner.tapSelector(device, wanted, live_trace, .{});
136
+ try protocol.writeResultRaw(writer, id, "true");
137
+ return true;
138
+ }
139
+ if (std.mem.eql(u8, method, "ui.type")) {
140
+ const text = try params_parser.requiredString(params, "text");
141
+ if (params_parser.field(params, "selector")) |selector_value| {
142
+ const wanted = try selector.parseFromJson(allocator, selector_value);
143
+ defer wanted.deinit(allocator);
144
+ try runner.typeTextSelector(device, wanted, text, live_trace, .{});
145
+ try protocol.writeResultRaw(writer, id, "true");
146
+ return true;
147
+ }
148
+ try device.typeText(text);
149
+ if (live_trace) |tw| try rpc_trace.recordSimplePayload(tw, "ui.type", "text", text);
150
+ try protocol.writeResultRaw(writer, id, "true");
151
+ return true;
152
+ }
153
+ if (std.mem.eql(u8, method, "ui.eraseText")) {
154
+ const max_chars = @as(u32, @intCast(try params_parser.optionalU64(params, "maxChars", 80)));
155
+ if (params_parser.field(params, "selector")) |selector_value| {
156
+ const wanted = try selector.parseFromJson(allocator, selector_value);
157
+ defer wanted.deinit(allocator);
158
+ try runner.eraseTextSelector(device, wanted, max_chars, live_trace, .{});
159
+ try protocol.writeResultRaw(writer, id, "true");
160
+ return true;
161
+ }
162
+ try device.eraseText(max_chars);
163
+ if (live_trace) |tw| {
164
+ const payload = try std.fmt.allocPrint(tw.allocator, "{{\"maxChars\":{d}}}", .{max_chars});
165
+ defer tw.allocator.free(payload);
166
+ try tw.recordEvent("ui.eraseText", payload);
167
+ }
168
+ try protocol.writeResultRaw(writer, id, "true");
169
+ return true;
170
+ }
171
+ if (std.mem.eql(u8, method, "ui.hideKeyboard")) {
172
+ try device.hideKeyboard();
173
+ if (live_trace) |tw| try tw.recordEvent("ui.hideKeyboard", "{\"status\":\"ok\"}");
174
+ try protocol.writeResultRaw(writer, id, "true");
175
+ return true;
176
+ }
177
+ if (std.mem.eql(u8, method, "ui.swipe")) {
178
+ try device.swipe(
179
+ try params_parser.requiredI32(params, "x1"),
180
+ try params_parser.requiredI32(params, "y1"),
181
+ try params_parser.requiredI32(params, "x2"),
182
+ try params_parser.requiredI32(params, "y2"),
183
+ @as(u32, @intCast(try params_parser.optionalU64(params, "durationMs", 300))),
184
+ );
185
+ if (live_trace) |tw| try tw.recordEvent("ui.swipe", "{\"status\":\"ok\"}");
186
+ try protocol.writeResultRaw(writer, id, "true");
187
+ return true;
188
+ }
189
+ if (std.mem.eql(u8, method, "ui.pressBack")) {
190
+ try device.pressBack();
191
+ if (live_trace) |tw| try tw.recordEvent("ui.pressBack", "{\"status\":\"ok\"}");
192
+ try protocol.writeResultRaw(writer, id, "true");
193
+ return true;
194
+ }
195
+ if (std.mem.eql(u8, method, "ui.scrollUntilVisible")) {
196
+ const wanted = try params_parser.selectorParam(allocator, params);
197
+ defer wanted.deinit(allocator);
198
+ const ok = try runner.scrollUntilVisible(
199
+ device,
200
+ wanted,
201
+ try params_parser.optionalU64(params, "timeoutMs", 5000),
202
+ try params_parser.optionalDirection(params, "direction", .down),
203
+ live_trace,
204
+ .{},
205
+ );
206
+ try protocol.writeResultRaw(writer, id, if (ok) "true" else "false");
207
+ return true;
208
+ }
209
+ return false;
210
+ }
211
+
212
+ fn dispatchWaitMethod(
213
+ allocator: std.mem.Allocator,
214
+ device: anytype,
215
+ method: []const u8,
216
+ params: ?std.json.Value,
217
+ id: ?std.json.Value,
218
+ writer: anytype,
219
+ live_trace: ?*trace.TraceWriter,
220
+ ) !bool {
221
+ if (std.mem.eql(u8, method, "wait.until")) {
222
+ const visible_value = params_parser.field(params, "visible") orelse return error.WaitUntilNeedsVisibleSelector;
223
+ const wanted = try selector.parseFromJson(allocator, visible_value);
224
+ defer wanted.deinit(allocator);
225
+ const timeout_ms = try params_parser.optionalU64(params, "timeoutMs", 5000);
226
+ const ok = try runner.waitUntilVisible(device, wanted, timeout_ms, live_trace, .{});
227
+ try protocol.writeResultRaw(writer, id, if (ok) "true" else "false");
228
+ return true;
229
+ }
230
+ if (std.mem.eql(u8, method, "wait.any")) {
231
+ const selectors = try params_parser.selectors(allocator, params);
232
+ defer {
233
+ for (selectors) |wanted| wanted.deinit(allocator);
234
+ allocator.free(selectors);
235
+ }
236
+ const matched = try runner.waitUntilAnyVisible(device, selectors, try params_parser.optionalU64(params, "timeoutMs", 5000), live_trace, .{});
237
+ if (matched) |index| {
238
+ try protocol.writeMatchedIndexResult(writer, id, index);
239
+ } else {
240
+ try protocol.writeResultRaw(writer, id, "false");
241
+ }
242
+ return true;
243
+ }
244
+ if (std.mem.eql(u8, method, "wait.gone")) {
245
+ const wanted = try params_parser.selectorParam(allocator, params);
246
+ defer wanted.deinit(allocator);
247
+ const ok = try runner.waitUntilNotVisible(device, wanted, try params_parser.optionalU64(params, "timeoutMs", 5000), live_trace, .{});
248
+ try protocol.writeResultRaw(writer, id, if (ok) "true" else "false");
249
+ return true;
250
+ }
251
+ return false;
252
+ }
253
+
254
+ fn dispatchAssertMethod(
255
+ allocator: std.mem.Allocator,
256
+ device: anytype,
257
+ method: []const u8,
258
+ params: ?std.json.Value,
259
+ id: ?std.json.Value,
260
+ writer: anytype,
261
+ live_trace: ?*trace.TraceWriter,
262
+ ) !bool {
263
+ if (std.mem.eql(u8, method, "assert.visible")) {
264
+ const wanted = try params_parser.selectorParam(allocator, params);
265
+ defer wanted.deinit(allocator);
266
+ if (!try runner.waitUntilVisible(device, wanted, try params_parser.optionalU64(params, "timeoutMs", 5000), live_trace, .{})) return error.AssertionFailed;
267
+ try protocol.writeResultRaw(writer, id, "true");
268
+ return true;
269
+ }
270
+ if (std.mem.eql(u8, method, "assert.notVisible")) {
271
+ const wanted = try params_parser.selectorParam(allocator, params);
272
+ defer wanted.deinit(allocator);
273
+ if (!try runner.waitUntilNotVisible(device, wanted, try params_parser.optionalU64(params, "timeoutMs", 5000), live_trace, .{})) return error.AssertionFailed;
274
+ try protocol.writeResultRaw(writer, id, "true");
275
+ return true;
276
+ }
277
+ if (std.mem.eql(u8, method, "assert.healthy")) {
278
+ if (!try runner.assertHealthy(device, try params_parser.optionalU64(params, "timeoutMs", 0), live_trace, .{})) return error.AssertionFailed;
279
+ try protocol.writeResultRaw(writer, id, "true");
280
+ return true;
281
+ }
282
+ return false;
283
+ }
284
+
285
+ fn dispatchTraceMethod(
286
+ allocator: std.mem.Allocator,
287
+ method: []const u8,
288
+ params: ?std.json.Value,
289
+ id: ?std.json.Value,
290
+ writer: anytype,
291
+ live_trace: ?*trace.TraceWriter,
292
+ ) !bool {
293
+ if (std.mem.eql(u8, method, "trace.events")) {
294
+ const after_seq = try params_parser.optionalU64(params, "afterSeq", 0);
295
+ const limit = @min(try params_parser.optionalU64(params, "limit", 100), 1000);
296
+ try rpc_trace.writeEventsResult(allocator, writer, id, live_trace, after_seq, limit);
297
+ return true;
298
+ }
299
+ if (std.mem.eql(u8, method, "trace.export")) {
300
+ const tw = live_trace orelse {
301
+ try protocol.writeTraceDisabledResult(writer, id);
302
+ return true;
303
+ };
304
+ const out_path = try params_parser.requiredString(params, "out");
305
+ const redact = try params_parser.optionalBool(params, "redact", false);
306
+ const omit_screenshots = try params_parser.optionalBool(params, "omitScreenshots", false);
307
+ const effective_redact = redact or omit_screenshots;
308
+ try tw.recordEvent("trace.export", "{\"status\":\"started\"}");
309
+ try tw.flushManifest();
310
+ try bundle.exportTraceBundleWithOptions(allocator, tw.root_dir, out_path, .{
311
+ .redact = effective_redact,
312
+ .omit_screenshots = omit_screenshots,
313
+ });
314
+ try protocol.writeTraceExportResult(writer, id, tw.root_dir, out_path, effective_redact, omit_screenshots);
315
+ return true;
316
+ }
317
+ return false;
318
+ }
@@ -0,0 +1,31 @@
1
+ const std = @import("std");
2
+ const protocol = @import("json_rpc_protocol.zig");
3
+ const semantic = @import("semantic.zig");
4
+ const trace = @import("trace.zig");
5
+ const types = @import("types.zig");
6
+
7
+ pub const Format = enum { raw, semantic };
8
+
9
+ pub fn writeResult(writer: anytype, id: ?std.json.Value, snap: types.ObservationSnapshot, format: Format) !void {
10
+ try writer.writeAll("{\"jsonrpc\":\"2.0\",\"id\":");
11
+ try protocol.writeId(writer, id);
12
+ try writer.writeAll(",\"result\":");
13
+ switch (format) {
14
+ .raw => try trace.writeSnapshotJson(writer, snap),
15
+ .semantic => try semantic.writeSemanticSnapshotJson(writer, snap),
16
+ }
17
+ try writer.writeAll("}\n");
18
+ }
19
+
20
+ pub fn recordArtifact(tw: *trace.TraceWriter, kind: []const u8, snap: types.ObservationSnapshot) !void {
21
+ const path = try tw.writeSnapshot(snap);
22
+ defer tw.allocator.free(path);
23
+ var payload = std.ArrayList(u8).empty;
24
+ defer payload.deinit(tw.allocator);
25
+ try payload.writer(tw.allocator).writeAll("{\"path\":");
26
+ try trace.writeJsonString(payload.writer(tw.allocator), path);
27
+ try payload.writer(tw.allocator).writeAll(",\"snapshotId\":");
28
+ try trace.writeJsonString(payload.writer(tw.allocator), snap.id);
29
+ try payload.writer(tw.allocator).writeAll("}");
30
+ try tw.recordEvent(kind, payload.items);
31
+ }
@@ -0,0 +1,52 @@
1
+ const std = @import("std");
2
+ const json_fields = @import("json_fields.zig");
3
+ const scenario = @import("scenario.zig");
4
+ const selector = @import("selector.zig");
5
+
6
+ pub fn field(params: ?std.json.Value, key: []const u8) ?std.json.Value {
7
+ return json_fields.field(params, key);
8
+ }
9
+
10
+ pub fn selectorParam(allocator: std.mem.Allocator, params: ?std.json.Value) !selector.Selector {
11
+ const selector_value = field(params, "selector") orelse return error.MissingSelector;
12
+ return try selector.parseFromJson(allocator, selector_value);
13
+ }
14
+
15
+ pub fn selectors(allocator: std.mem.Allocator, params: ?std.json.Value) ![]selector.Selector {
16
+ const selectors_value = field(params, "selectors") orelse return error.MissingSelectors;
17
+ if (selectors_value != .array) return error.SelectorsMustBeArray;
18
+ var parsed_selectors = std.ArrayList(selector.Selector).empty;
19
+ errdefer {
20
+ for (parsed_selectors.items) |wanted| wanted.deinit(allocator);
21
+ parsed_selectors.deinit(allocator);
22
+ }
23
+ for (selectors_value.array.items) |selector_value| {
24
+ try parsed_selectors.append(allocator, try selector.parseFromJson(allocator, selector_value));
25
+ }
26
+ if (parsed_selectors.items.len == 0) return error.SelectorsMustNotBeEmpty;
27
+ return try parsed_selectors.toOwnedSlice(allocator);
28
+ }
29
+
30
+ pub fn requiredString(params: ?std.json.Value, key: []const u8) ![]const u8 {
31
+ return try json_fields.requiredString(params, key, error.MissingParam, error.ParamMustBeString);
32
+ }
33
+
34
+ pub fn requiredI32(params: ?std.json.Value, key: []const u8) !i32 {
35
+ return try json_fields.requiredI32(params, key, error.MissingParam, error.ParamMustBeInteger);
36
+ }
37
+
38
+ pub fn optionalU64(params: ?std.json.Value, key: []const u8, default_value: u64) !u64 {
39
+ return try json_fields.optionalU64(params, key, default_value, error.ParamMustBeInteger);
40
+ }
41
+
42
+ pub fn optionalBool(params: ?std.json.Value, key: []const u8, default_value: bool) !bool {
43
+ return try json_fields.optionalBool(params, key, default_value, error.ParamMustBeBool);
44
+ }
45
+
46
+ pub fn optionalDirection(params: ?std.json.Value, key: []const u8, default_value: scenario.ScrollDirection) !scenario.ScrollDirection {
47
+ const value = field(params, key) orelse return default_value;
48
+ if (value != .string) return error.ParamMustBeString;
49
+ if (std.mem.eql(u8, value.string, "down")) return .down;
50
+ if (std.mem.eql(u8, value.string, "up")) return .up;
51
+ return error.UnknownScrollDirection;
52
+ }
@@ -0,0 +1,110 @@
1
+ const std = @import("std");
2
+ const device_registry = @import("device_registry.zig");
3
+ const trace = @import("trace.zig");
4
+ const types = @import("types.zig");
5
+ const version = @import("version.zig");
6
+
7
+ pub const capabilities_json =
8
+ "{\"name\":\"zmr\",\"version\":\"" ++ version.runner_version ++
9
+ "\",\"protocolVersion\":\"" ++ version.protocol_version ++
10
+ "\",\"protocol\":{\"version\":\"" ++ version.protocol_version ++
11
+ "\",\"minimumCompatibleVersion\":\"" ++ version.protocol_min_compatible_version ++
12
+ "\",\"stability\":\"" ++ version.protocol_stability ++
13
+ "\",\"breakingChangePolicy\":\"" ++ version.protocol_breaking_change_policy ++
14
+ "\"},\"platforms\":[\"android\",\"ios\"],\"platformSupport\":{\"android\":{\"status\":\"supported\",\"deviceTypes\":[\"emulator\",\"physical\"],\"automation\":[\"adb\",\"uiautomator\",\"android-shim\"]},\"ios\":{\"status\":\"supported\",\"deviceTypes\":[\"simulator\",\"physical\"],\"automation\":[\"simctl\",\"devicectl\",\"xctest-shim\"],\"physicalDevices\":true}},\"iosPreview\":false,\"transports\":[\"stdio\",\"tcp\"],\"methods\":[\"runner.capabilities\",\"device.list\",\"session.create\",\"session.close\",\"app.install\",\"app.launch\",\"app.stop\",\"app.openLink\",\"app.clearState\",\"observe.snapshot\",\"observe.semanticSnapshot\",\"ui.tap\",\"ui.type\",\"ui.eraseText\",\"ui.hideKeyboard\",\"ui.swipe\",\"ui.pressBack\",\"ui.scrollUntilVisible\",\"wait.until\",\"wait.any\",\"wait.gone\",\"assert.visible\",\"assert.notVisible\",\"assert.healthy\",\"trace.events\",\"trace.export\"]}";
15
+
16
+ pub fn writeCapabilitiesResult(writer: anytype, id: ?std.json.Value) !void {
17
+ try writeResultRaw(writer, id, capabilities_json);
18
+ }
19
+
20
+ pub fn writeDevicesResult(writer: anytype, id: ?std.json.Value, devices: []const types.DeviceInfo) !void {
21
+ try writer.writeAll("{\"jsonrpc\":\"2.0\",\"id\":");
22
+ try writeId(writer, id);
23
+ try writer.writeAll(",\"result\":[");
24
+ for (devices, 0..) |info, index| {
25
+ if (index > 0) try writer.writeAll(",");
26
+ try writer.writeAll("{\"serial\":");
27
+ try trace.writeJsonString(writer, info.serial);
28
+ try writer.writeAll(",\"state\":");
29
+ try trace.writeJsonString(writer, info.state);
30
+ try writer.print(",\"ready\":{}", .{device_registry.isKnownReadyState(info.state)});
31
+ try writer.writeAll("}");
32
+ }
33
+ try writer.writeAll("]}\n");
34
+ }
35
+
36
+ pub fn writeTraceDisabledResult(writer: anytype, id: ?std.json.Value) !void {
37
+ try writeResultRaw(writer, id, "{\"traceDir\":null,\"message\":\"start zmr serve with --trace-dir to enable live RPC trace export\"}");
38
+ }
39
+
40
+ pub fn writeTraceExportResult(
41
+ writer: anytype,
42
+ id: ?std.json.Value,
43
+ trace_dir: []const u8,
44
+ out_path: []const u8,
45
+ redacted: bool,
46
+ omit_screenshots: bool,
47
+ ) !void {
48
+ try writer.writeAll("{\"jsonrpc\":\"2.0\",\"id\":");
49
+ try writeId(writer, id);
50
+ try writer.writeAll(",\"result\":{\"traceDir\":");
51
+ try trace.writeJsonString(writer, trace_dir);
52
+ try writer.writeAll(",\"out\":");
53
+ try trace.writeJsonString(writer, out_path);
54
+ try writer.writeAll(",\"redacted\":");
55
+ try writer.writeAll(if (redacted) "true" else "false");
56
+ try writer.writeAll(",\"omitScreenshots\":");
57
+ try writer.writeAll(if (omit_screenshots) "true" else "false");
58
+ try writer.writeAll("}}\n");
59
+ }
60
+
61
+ pub fn writeMatchedIndexResult(writer: anytype, id: ?std.json.Value, index: usize) !void {
62
+ try writer.writeAll("{\"jsonrpc\":\"2.0\",\"id\":");
63
+ try writeId(writer, id);
64
+ try writer.writeAll(",\"result\":{\"matchedIndex\":");
65
+ try writer.print("{d}", .{index});
66
+ try writer.writeAll("}}\n");
67
+ }
68
+
69
+ pub fn writeResultRaw(writer: anytype, id: ?std.json.Value, raw_json: []const u8) !void {
70
+ try writer.writeAll("{\"jsonrpc\":\"2.0\",\"id\":");
71
+ try writeId(writer, id);
72
+ try writer.writeAll(",\"result\":");
73
+ try writer.writeAll(raw_json);
74
+ try writer.writeAll("}\n");
75
+ }
76
+
77
+ pub fn writeError(writer: anytype, id: ?std.json.Value, code: i32, message: []const u8) !void {
78
+ try writeErrorWithPublicCode(writer, id, code, message, null);
79
+ }
80
+
81
+ pub fn writeErrorWithPublicCode(
82
+ writer: anytype,
83
+ id: ?std.json.Value,
84
+ code: i32,
85
+ message: []const u8,
86
+ public_code: ?[]const u8,
87
+ ) !void {
88
+ try writer.writeAll("{\"jsonrpc\":\"2.0\",\"id\":");
89
+ try writeId(writer, id);
90
+ try writer.print(",\"error\":{{\"code\":{d},\"message\":", .{code});
91
+ try trace.writeJsonString(writer, message);
92
+ if (public_code) |value| {
93
+ try writer.writeAll(",\"publicCode\":");
94
+ try trace.writeJsonString(writer, value);
95
+ }
96
+ try writer.writeAll("}}\n");
97
+ }
98
+
99
+ pub fn writeId(writer: anytype, id: ?std.json.Value) !void {
100
+ const value = id orelse {
101
+ try writer.writeAll("null");
102
+ return;
103
+ };
104
+ switch (value) {
105
+ .null => try writer.writeAll("null"),
106
+ .string => |actual| try trace.writeJsonString(writer, actual),
107
+ .integer => |actual| try writer.print("{d}", .{actual}),
108
+ else => try writer.writeAll("null"),
109
+ }
110
+ }
@@ -0,0 +1,73 @@
1
+ const std = @import("std");
2
+ const protocol = @import("json_rpc_protocol.zig");
3
+ const trace = @import("trace.zig");
4
+
5
+ pub fn writeEventsResult(
6
+ allocator: std.mem.Allocator,
7
+ writer: anytype,
8
+ id: ?std.json.Value,
9
+ live_trace: ?*trace.TraceWriter,
10
+ after_seq: u64,
11
+ limit: u64,
12
+ ) !void {
13
+ const tw = live_trace orelse {
14
+ try writer.writeAll("{\"jsonrpc\":\"2.0\",\"id\":");
15
+ try protocol.writeId(writer, id);
16
+ try writer.print(",\"result\":{{\"traceDir\":null,\"afterSeq\":{d},\"nextSeq\":{d},\"latestSeq\":0,\"events\":[]}}}}\n", .{ after_seq, after_seq });
17
+ return;
18
+ };
19
+
20
+ const events_path = try std.fs.path.join(allocator, &.{ tw.root_dir, "events.jsonl" });
21
+ defer allocator.free(events_path);
22
+ const content = std.fs.cwd().readFileAlloc(allocator, events_path, 64 * 1024 * 1024) catch |err| switch (err) {
23
+ error.FileNotFound => try allocator.dupe(u8, ""),
24
+ else => return err,
25
+ };
26
+ defer allocator.free(content);
27
+
28
+ try writer.writeAll("{\"jsonrpc\":\"2.0\",\"id\":");
29
+ try protocol.writeId(writer, id);
30
+ try writer.writeAll(",\"result\":{\"traceDir\":");
31
+ try trace.writeJsonString(writer, tw.root_dir);
32
+ try writer.print(",\"afterSeq\":{d},\"nextSeq\":", .{after_seq});
33
+
34
+ var events_json = std.ArrayList(u8).empty;
35
+ defer events_json.deinit(allocator);
36
+ var events_writer = events_json.writer(allocator);
37
+ var next_seq = after_seq;
38
+ var emitted: u64 = 0;
39
+
40
+ var lines = std.mem.splitScalar(u8, content, '\n');
41
+ while (lines.next()) |raw_line| {
42
+ if (emitted >= limit) break;
43
+ const line = std.mem.trim(u8, raw_line, " \t\r\n");
44
+ if (line.len == 0) continue;
45
+ const parsed = std.json.parseFromSlice(std.json.Value, allocator, line, .{}) catch continue;
46
+ defer parsed.deinit();
47
+ if (parsed.value != .object) continue;
48
+ const seq_value = parsed.value.object.get("seq") orelse continue;
49
+ if (seq_value != .integer or seq_value.integer <= 0) continue;
50
+ const seq = @as(u64, @intCast(seq_value.integer));
51
+ if (seq <= after_seq) continue;
52
+ if (emitted > 0) try events_writer.writeAll(",");
53
+ try events_writer.writeAll(line);
54
+ next_seq = seq;
55
+ emitted += 1;
56
+ }
57
+
58
+ try writer.print("{d},\"latestSeq\":{d},\"events\":[", .{ next_seq, tw.event_count });
59
+ try writer.writeAll(events_json.items);
60
+ try writer.writeAll("]}}\n");
61
+ }
62
+
63
+ pub fn recordSimplePayload(tw: *trace.TraceWriter, kind: []const u8, key: []const u8, value: []const u8) !void {
64
+ var payload = std.ArrayList(u8).empty;
65
+ defer payload.deinit(tw.allocator);
66
+ const writer = payload.writer(tw.allocator);
67
+ try writer.writeAll("{");
68
+ try trace.writeJsonString(writer, key);
69
+ try writer.writeAll(":");
70
+ try trace.writeJsonString(writer, value);
71
+ try writer.writeAll("}");
72
+ try tw.recordEvent(kind, payload.items);
73
+ }