zeno-mobile-runner 0.1.2 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/CHANGELOG.md +162 -3
  2. package/FEATURES.md +50 -7
  3. package/README.md +133 -7
  4. package/build.zig.zon +3 -3
  5. package/clients/README.md +60 -3
  6. package/clients/go/README.md +12 -0
  7. package/clients/go/zmr/client.go +142 -0
  8. package/clients/kotlin/README.md +18 -1
  9. package/clients/kotlin/build.gradle.kts +1 -1
  10. package/clients/kotlin/src/main/kotlin/dev/zmr/ZmrClient.kt +76 -1
  11. package/clients/python/README.md +19 -0
  12. package/clients/python/pyproject.toml +1 -1
  13. package/clients/python/zmr_client.py +33 -0
  14. package/clients/rust/Cargo.lock +1 -1
  15. package/clients/rust/Cargo.toml +1 -1
  16. package/clients/rust/README.md +25 -1
  17. package/clients/rust/src/lib.rs +201 -0
  18. package/clients/swift/README.md +18 -0
  19. package/clients/swift/Sources/ZMRClient/ZMRClient.swift +82 -0
  20. package/clients/typescript/README.md +16 -0
  21. package/clients/typescript/index.d.ts +12 -0
  22. package/clients/typescript/index.mjs +16 -0
  23. package/clients/typescript/package.json +1 -1
  24. package/docs/agent-discovery.md +202 -0
  25. package/docs/ai-agents.md +87 -6
  26. package/docs/benchmarking.md +10 -3
  27. package/docs/clients.md +10 -6
  28. package/docs/demo.md +4 -0
  29. package/docs/expo-smoke.md +79 -0
  30. package/docs/install.md +3 -2
  31. package/docs/npm.md +58 -4
  32. package/docs/production-readiness.md +123 -0
  33. package/docs/protocol-fixtures/core-session.responses.jsonl +1 -1
  34. package/docs/protocol.md +215 -16
  35. package/docs/scenario-authoring.md +3 -0
  36. package/docs/troubleshooting.md +1 -1
  37. package/npm/agents.mjs +16 -0
  38. package/npm/build-zmr.mjs +1 -1
  39. package/npm/commands.mjs +9 -5
  40. package/npm/postinstall.mjs +28 -2
  41. package/npm/verify-publish.mjs +36 -0
  42. package/package.json +2 -1
  43. package/prebuilds/darwin-arm64/zmr +0 -0
  44. package/prebuilds/darwin-x64/zmr +0 -0
  45. package/prebuilds/linux-arm64/zmr +0 -0
  46. package/prebuilds/linux-x64/zmr +0 -0
  47. package/schemas/README.md +4 -0
  48. package/schemas/discover-output.schema.json +83 -0
  49. package/schemas/draft-output.schema.json +58 -0
  50. package/schemas/explore-output.schema.json +94 -0
  51. package/schemas/inspect-output.schema.json +88 -0
  52. package/schemas/run-output.schema.json +2 -0
  53. package/scripts/install-ios-shim.sh +79 -14
  54. package/scripts/release-readiness.py +43 -0
  55. package/scripts/run-android-pilot.sh +35 -9
  56. package/scripts/run-ios-pilot.sh +11 -4
  57. package/shims/ios/ZMRShim.swift +3 -0
  58. package/shims/ios/ZMRShimUITestCase.swift +41 -11
  59. package/skills/zmr-mobile-testing/SKILL.md +28 -3
  60. package/src/cli_discover.zig +239 -0
  61. package/src/cli_draft.zig +924 -0
  62. package/src/cli_explore.zig +136 -0
  63. package/src/cli_inspect.zig +310 -0
  64. package/src/cli_output.zig +26 -2
  65. package/src/cli_run.zig +28 -0
  66. package/src/cli_trace.zig +8 -0
  67. package/src/errors.zig +9 -0
  68. package/src/ios.zig +11 -4
  69. package/src/ios_lifecycle.zig +36 -0
  70. package/src/ios_shim.zig +42 -0
  71. package/src/json_rpc_methods.zig +85 -11
  72. package/src/json_rpc_params.zig +8 -0
  73. package/src/json_rpc_protocol.zig +1 -1
  74. package/src/json_rpc_trace.zig +112 -0
  75. package/src/main.zig +24 -2
  76. package/src/mcp.zig +209 -6
  77. package/src/mcp_protocol.zig +29 -1
  78. package/src/mcp_trace.zig +126 -4
  79. package/src/report.zig +186 -0
  80. package/src/runner.zig +26 -4
  81. package/src/runner_actions.zig +10 -0
  82. package/src/runner_diagnostics.zig +31 -1
  83. package/src/runner_events.zig +70 -7
  84. package/src/runner_native.zig +17 -1
  85. package/src/runner_waits.zig +82 -19
  86. package/src/scaffold.zig +28 -12
  87. package/src/scenario.zig +32 -4
  88. package/src/schema_registry.zig +4 -0
  89. package/src/version.zig +1 -1
@@ -0,0 +1,136 @@
1
+ const std = @import("std");
2
+
3
+ const cli_discover = @import("cli_discover.zig");
4
+ const cli_output = @import("cli_output.zig");
5
+ const validation = @import("validation.zig");
6
+
7
+ const explore_guardrails = [_][]const u8{
8
+ "writes from existing trace evidence only",
9
+ "does not crawl the app",
10
+ "does not discover credentials or secrets",
11
+ "requires human review before commit",
12
+ };
13
+
14
+ pub const ParsedArgs = struct {
15
+ from_trace: ?[]const u8 = null,
16
+ out_path: ?[]const u8 = null,
17
+ goal: ?[]const u8 = null,
18
+ name: ?[]const u8 = null,
19
+ app_id: ?[]const u8 = null,
20
+ include_actions: bool = false,
21
+ validate: bool = false,
22
+ force: bool = false,
23
+ json: bool = false,
24
+ };
25
+
26
+ pub const ExploreSummary = struct {
27
+ ok: bool,
28
+ goal: ?[]const u8,
29
+ };
30
+
31
+ pub const OwnedExplore = struct {
32
+ discovered: cli_discover.OwnedDiscover,
33
+ summary: ExploreSummary,
34
+
35
+ pub fn deinit(self: *OwnedExplore, allocator: std.mem.Allocator) void {
36
+ self.discovered.deinit(allocator);
37
+ }
38
+ };
39
+
40
+ pub fn parseArgs(args: []const []const u8) !ParsedArgs {
41
+ var parsed = ParsedArgs{};
42
+ var index: usize = 0;
43
+ while (index < args.len) : (index += 1) {
44
+ const arg = args[index];
45
+ if (std.mem.eql(u8, arg, "--from-trace")) {
46
+ index += 1;
47
+ parsed.from_trace = if (index < args.len) args[index] else return error.MissingTraceDir;
48
+ } else if (std.mem.eql(u8, arg, "--out")) {
49
+ index += 1;
50
+ parsed.out_path = if (index < args.len) args[index] else return error.MissingDraftOut;
51
+ } else if (std.mem.eql(u8, arg, "--goal")) {
52
+ index += 1;
53
+ parsed.goal = if (index < args.len) args[index] else return error.MissingParam;
54
+ } else if (std.mem.eql(u8, arg, "--name")) {
55
+ index += 1;
56
+ parsed.name = if (index < args.len) args[index] else return error.MissingParam;
57
+ } else if (std.mem.eql(u8, arg, "--app-id")) {
58
+ index += 1;
59
+ parsed.app_id = if (index < args.len) args[index] else return error.MissingAppId;
60
+ } else if (std.mem.eql(u8, arg, "--include-actions")) {
61
+ parsed.include_actions = true;
62
+ } else if (std.mem.eql(u8, arg, "--validate")) {
63
+ parsed.validate = true;
64
+ } else if (std.mem.eql(u8, arg, "--force")) {
65
+ parsed.force = true;
66
+ } else if (std.mem.eql(u8, arg, "--json")) {
67
+ parsed.json = true;
68
+ } else {
69
+ return error.UnknownFlag;
70
+ }
71
+ }
72
+
73
+ if (parsed.from_trace == null) return error.MissingTraceDir;
74
+ if (parsed.out_path == null) return error.MissingDraftOut;
75
+ return parsed;
76
+ }
77
+
78
+ pub fn run(allocator: std.mem.Allocator, args: *std.process.ArgIterator) !void {
79
+ var raw_args = std.ArrayList([]const u8).empty;
80
+ defer raw_args.deinit(allocator);
81
+ while (args.next()) |arg| try raw_args.append(allocator, arg);
82
+
83
+ const parsed = try parseArgs(raw_args.items);
84
+ var explored = try exploreFromTrace(allocator, parsed);
85
+ defer explored.deinit(allocator);
86
+
87
+ const stdout = std.fs.File.stdout().deprecatedWriter();
88
+ if (parsed.json) {
89
+ try writeJson(stdout, explored.summary, explored.discovered.summary, explored.discovered.validation);
90
+ } else {
91
+ try stdout.print("wrote {s}\n", .{explored.discovered.summary.draft.out_path});
92
+ if (explored.summary.goal) |goal| try stdout.print("goal: {s}\n", .{goal});
93
+ try stdout.writeAll("review required: true\n");
94
+ try stdout.writeAll("next: zmr validate --json ");
95
+ try cli_output.writeShellArg(stdout, explored.discovered.summary.draft.out_path);
96
+ try stdout.writeAll("\n");
97
+ }
98
+ if (!explored.summary.ok) std.process.exit(1);
99
+ }
100
+
101
+ pub fn exploreFromTrace(allocator: std.mem.Allocator, parsed: ParsedArgs) !OwnedExplore {
102
+ var discovered = try cli_discover.discoverFromTrace(allocator, .{
103
+ .from_trace = parsed.from_trace,
104
+ .out_path = parsed.out_path,
105
+ .name = parsed.name,
106
+ .app_id = parsed.app_id,
107
+ .include_actions = parsed.include_actions,
108
+ .validate = parsed.validate,
109
+ .force = parsed.force,
110
+ .json = parsed.json,
111
+ });
112
+ errdefer discovered.deinit(allocator);
113
+
114
+ return .{
115
+ .discovered = discovered,
116
+ .summary = .{
117
+ .ok = discovered.summary.ok,
118
+ .goal = parsed.goal,
119
+ },
120
+ };
121
+ }
122
+
123
+ pub fn writeJson(
124
+ writer: anytype,
125
+ summary: ExploreSummary,
126
+ discover_summary: cli_discover.DiscoverSummary,
127
+ validation_result: ?validation.Result,
128
+ ) !void {
129
+ try cli_discover.writeJsonWithOptions(writer, discover_summary, validation_result, .{
130
+ .mode = "explore",
131
+ .goal = summary.goal,
132
+ .autonomous = false,
133
+ .review_required = true,
134
+ .guardrails = explore_guardrails[0..],
135
+ });
136
+ }
@@ -0,0 +1,310 @@
1
+ const std = @import("std");
2
+
3
+ const config = @import("config.zig");
4
+ const scaffold = @import("scaffold.zig");
5
+ const trace = @import("trace.zig");
6
+ const version = @import("version.zig");
7
+
8
+ pub const ParsedArgs = struct {
9
+ json: bool = false,
10
+ dir: []const u8 = ".",
11
+ config_path: ?[]const u8 = null,
12
+ };
13
+
14
+ pub const PlatformInspection = struct {
15
+ name: []const u8,
16
+ enabled: bool,
17
+ default_device: ?[]const u8 = null,
18
+ smoke_scenario: ?[]const u8 = null,
19
+ smoke_scenario_exists: bool = false,
20
+ trace_dir: ?[]const u8 = null,
21
+ };
22
+
23
+ pub const Inspection = struct {
24
+ ok: bool,
25
+ status: []const u8 = "ready",
26
+ dir: []const u8,
27
+ config_path: []const u8,
28
+ config_exists: bool,
29
+ agent_instructions_path: []const u8,
30
+ agent_instructions_exists: bool,
31
+ platforms: []const PlatformInspection = &.{},
32
+ };
33
+
34
+ const OwnedInspection = struct {
35
+ inspection: Inspection,
36
+ owned_strings: std.ArrayList([]const u8),
37
+ platforms: std.ArrayList(PlatformInspection),
38
+ parsed_config: ?config.Config = null,
39
+
40
+ fn deinit(self: *OwnedInspection, allocator: std.mem.Allocator) void {
41
+ if (self.parsed_config) |*cfg| cfg.deinit(allocator);
42
+ for (self.owned_strings.items) |value| allocator.free(value);
43
+ self.owned_strings.deinit(allocator);
44
+ self.platforms.deinit(allocator);
45
+ }
46
+ };
47
+
48
+ pub fn parseArgs(args: []const []const u8) !ParsedArgs {
49
+ var parsed = ParsedArgs{};
50
+ var index: usize = 0;
51
+ while (index < args.len) : (index += 1) {
52
+ const arg = args[index];
53
+ if (std.mem.eql(u8, arg, "--json")) {
54
+ parsed.json = true;
55
+ } else if (std.mem.eql(u8, arg, "--dir")) {
56
+ index += 1;
57
+ parsed.dir = if (index < args.len) args[index] else return error.MissingParam;
58
+ } else if (std.mem.eql(u8, arg, "--config")) {
59
+ index += 1;
60
+ parsed.config_path = if (index < args.len) args[index] else return error.MissingParam;
61
+ } else {
62
+ return error.UnknownFlag;
63
+ }
64
+ }
65
+ return parsed;
66
+ }
67
+
68
+ pub fn run(allocator: std.mem.Allocator, args: *std.process.ArgIterator) !void {
69
+ var raw_args = std.ArrayList([]const u8).empty;
70
+ defer raw_args.deinit(allocator);
71
+ while (args.next()) |arg| try raw_args.append(allocator, arg);
72
+
73
+ const parsed = try parseArgs(raw_args.items);
74
+ var owned = try inspect(allocator, parsed);
75
+ defer owned.deinit(allocator);
76
+
77
+ const stdout = std.fs.File.stdout().deprecatedWriter();
78
+ if (parsed.json) {
79
+ try writeJson(stdout, owned.inspection);
80
+ } else {
81
+ try writeText(stdout, owned.inspection);
82
+ }
83
+ }
84
+
85
+ fn inspect(allocator: std.mem.Allocator, parsed: ParsedArgs) !OwnedInspection {
86
+ var owned = OwnedInspection{
87
+ .inspection = .{
88
+ .ok = false,
89
+ .status = "needs-setup",
90
+ .dir = undefined,
91
+ .config_path = undefined,
92
+ .config_exists = false,
93
+ .agent_instructions_path = undefined,
94
+ .agent_instructions_exists = false,
95
+ },
96
+ .owned_strings = .empty,
97
+ .platforms = .empty,
98
+ };
99
+ errdefer owned.deinit(allocator);
100
+
101
+ owned.inspection.dir = try ownString(allocator, &owned.owned_strings, parsed.dir);
102
+ owned.inspection.config_path = if (parsed.config_path) |explicit|
103
+ try ownString(allocator, &owned.owned_strings, explicit)
104
+ else
105
+ try ownJoinedPath(allocator, &owned.owned_strings, parsed.dir, scaffold.app_config_file);
106
+ owned.inspection.agent_instructions_path = try ownJoinedPath(allocator, &owned.owned_strings, parsed.dir, scaffold.app_agents_file);
107
+
108
+ owned.inspection.config_exists = pathExists(owned.inspection.config_path);
109
+ owned.inspection.agent_instructions_exists = pathExists(owned.inspection.agent_instructions_path);
110
+ if (!owned.inspection.config_exists) return owned;
111
+
112
+ owned.parsed_config = try config.parseFile(allocator, owned.inspection.config_path);
113
+ const cfg = &owned.parsed_config.?;
114
+ try appendPlatform(allocator, &owned, "android", cfg.android);
115
+ try appendPlatform(allocator, &owned, "ios", cfg.ios);
116
+ owned.inspection.platforms = owned.platforms.items;
117
+ owned.inspection.ok = true;
118
+ owned.inspection.status = "ready";
119
+ return owned;
120
+ }
121
+
122
+ fn appendPlatform(
123
+ allocator: std.mem.Allocator,
124
+ owned: *OwnedInspection,
125
+ name: []const u8,
126
+ platform: config.PlatformConfig,
127
+ ) !void {
128
+ const smoke_scenario = if (platform.smoke_scenario) |path|
129
+ try ownResolvedPath(allocator, &owned.owned_strings, owned.inspection.dir, path)
130
+ else
131
+ null;
132
+ try owned.platforms.append(allocator, .{
133
+ .name = name,
134
+ .enabled = platform.enabled,
135
+ .default_device = platform.default_device,
136
+ .smoke_scenario = smoke_scenario,
137
+ .smoke_scenario_exists = if (smoke_scenario) |path| pathExists(path) else false,
138
+ .trace_dir = platform.trace_dir,
139
+ });
140
+ }
141
+
142
+ fn pathExists(path: []const u8) bool {
143
+ std.fs.cwd().access(path, .{}) catch return false;
144
+ return true;
145
+ }
146
+
147
+ fn ownString(allocator: std.mem.Allocator, owned_strings: *std.ArrayList([]const u8), value: []const u8) ![]const u8 {
148
+ const owned = try allocator.dupe(u8, value);
149
+ try owned_strings.append(allocator, owned);
150
+ return owned;
151
+ }
152
+
153
+ fn ownJoinedPath(
154
+ allocator: std.mem.Allocator,
155
+ owned_strings: *std.ArrayList([]const u8),
156
+ root: []const u8,
157
+ path: []const u8,
158
+ ) ![]const u8 {
159
+ const joined = try std.fs.path.join(allocator, &.{ root, path });
160
+ try owned_strings.append(allocator, joined);
161
+ return joined;
162
+ }
163
+
164
+ fn ownResolvedPath(
165
+ allocator: std.mem.Allocator,
166
+ owned_strings: *std.ArrayList([]const u8),
167
+ root: []const u8,
168
+ path: []const u8,
169
+ ) ![]const u8 {
170
+ if (std.fs.path.isAbsolute(path)) return try ownString(allocator, owned_strings, path);
171
+ return try ownJoinedPath(allocator, owned_strings, root, path);
172
+ }
173
+
174
+ pub fn writeJson(writer: anytype, inspection: Inspection) !void {
175
+ try writer.writeAll("{\"ok\":");
176
+ try writer.writeAll(if (inspection.ok) "true" else "false");
177
+ try writer.writeAll(",\"status\":");
178
+ try trace.writeJsonString(writer, inspection.status);
179
+ try writer.writeAll(",\"schemaVersion\":1");
180
+ try writer.writeAll(",\"runnerVersion\":");
181
+ try trace.writeJsonString(writer, version.runner_version);
182
+ try writer.writeAll(",\"protocolVersion\":");
183
+ try trace.writeJsonString(writer, version.protocol_version);
184
+ try writer.writeAll(",\"dir\":");
185
+ try trace.writeJsonString(writer, inspection.dir);
186
+ try writer.writeAll(",\"configPath\":");
187
+ try trace.writeJsonString(writer, inspection.config_path);
188
+ try writer.writeAll(",\"configExists\":");
189
+ try writer.writeAll(if (inspection.config_exists) "true" else "false");
190
+ try writer.writeAll(",\"agentInstructionsPath\":");
191
+ try trace.writeJsonString(writer, inspection.agent_instructions_path);
192
+ try writer.writeAll(",\"agentInstructionsExists\":");
193
+ try writer.writeAll(if (inspection.agent_instructions_exists) "true" else "false");
194
+ try writer.writeAll(",\"platforms\":[");
195
+ for (inspection.platforms, 0..) |platform, index| {
196
+ if (index > 0) try writer.writeAll(",");
197
+ try writePlatformJson(writer, platform);
198
+ }
199
+ try writer.writeAll("],\"recommendedCommands\":[");
200
+ try writeRecommendedCommandsJson(writer, inspection);
201
+ try writer.writeAll("]");
202
+ try writer.writeAll(",\"claimsPolicy\":[\"verify runs with trace evidence before making readiness claims\",\"do not claim Flutter widget-tree inspection\"]");
203
+ try writer.writeAll(",\"limitations\":[\"inspect is read-only and does not launch devices\",\"autonomous crawling is not shipped; generate or edit scenarios for human review\"]");
204
+ try writer.writeAll("}\n");
205
+ }
206
+
207
+ fn writePlatformJson(writer: anytype, platform: PlatformInspection) !void {
208
+ try writer.writeAll("{\"name\":");
209
+ try trace.writeJsonString(writer, platform.name);
210
+ try writer.writeAll(",\"enabled\":");
211
+ try writer.writeAll(if (platform.enabled) "true" else "false");
212
+ try writer.writeAll(",\"defaultDevice\":");
213
+ try writeOptionalJsonString(writer, platform.default_device);
214
+ try writer.writeAll(",\"smokeScenario\":");
215
+ try writeOptionalJsonString(writer, platform.smoke_scenario);
216
+ try writer.writeAll(",\"smokeScenarioExists\":");
217
+ try writer.writeAll(if (platform.smoke_scenario_exists) "true" else "false");
218
+ try writer.writeAll(",\"traceDir\":");
219
+ try writeOptionalJsonString(writer, platform.trace_dir);
220
+ try writer.writeAll("}");
221
+ }
222
+
223
+ fn writeOptionalJsonString(writer: anytype, value: ?[]const u8) !void {
224
+ if (value) |actual| {
225
+ try trace.writeJsonString(writer, actual);
226
+ } else {
227
+ try writer.writeAll("null");
228
+ }
229
+ }
230
+
231
+ fn writeRecommendedCommandsJson(writer: anytype, inspection: Inspection) !void {
232
+ if (!inspection.config_exists) {
233
+ try writer.writeAll("\"zmr init --app --dir ");
234
+ try writeShellArgJsonContent(writer, inspection.dir);
235
+ try writer.writeAll("\"");
236
+ return;
237
+ }
238
+
239
+ try writer.writeAll("\"zmr doctor --strict --json --config ");
240
+ try writeShellArgJsonContent(writer, inspection.config_path);
241
+ try writer.writeAll("\",");
242
+ try trace.writeJsonString(writer, "zmr schemas --json");
243
+ for (inspection.platforms) |platform| {
244
+ if (!platform.enabled) continue;
245
+ if (platform.smoke_scenario) |path| {
246
+ try writer.writeAll(",\"zmr validate --json ");
247
+ try writeShellArgJsonContent(writer, path);
248
+ try writer.writeAll("\"");
249
+ }
250
+ }
251
+ try writer.writeAll(",\"zmr serve --transport stdio --config ");
252
+ try writeShellArgJsonContent(writer, inspection.config_path);
253
+ try writer.writeAll(" --trace-dir traces/zmr-agent\"");
254
+ try writer.writeAll(",\"zmr mcp --config ");
255
+ try writeShellArgJsonContent(writer, inspection.config_path);
256
+ try writer.writeAll(" --trace-dir traces/zmr-agent\"");
257
+ }
258
+
259
+ fn writeShellArgJsonContent(writer: anytype, value: []const u8) !void {
260
+ if (value.len == 0) {
261
+ try writer.writeAll("''");
262
+ return;
263
+ }
264
+ var safe = true;
265
+ for (value) |byte| {
266
+ if (!std.ascii.isAlphanumeric(byte) and byte != '/' and byte != '.' and byte != '_' and byte != '-' and byte != ':' and byte != '=') {
267
+ safe = false;
268
+ break;
269
+ }
270
+ }
271
+ if (safe) {
272
+ try writeJsonStringContent(writer, value);
273
+ return;
274
+ }
275
+ try writer.writeAll("'");
276
+ for (value) |byte| {
277
+ if (byte == '\'') {
278
+ try writer.writeAll("'\"'\"'");
279
+ } else {
280
+ try writeJsonEscapedByte(writer, byte);
281
+ }
282
+ }
283
+ try writer.writeAll("'");
284
+ }
285
+
286
+ fn writeJsonStringContent(writer: anytype, value: []const u8) !void {
287
+ for (value) |byte| try writeJsonEscapedByte(writer, byte);
288
+ }
289
+
290
+ fn writeJsonEscapedByte(writer: anytype, byte: u8) !void {
291
+ switch (byte) {
292
+ '\\' => try writer.writeAll("\\\\"),
293
+ '"' => try writer.writeAll("\\\""),
294
+ '\n' => try writer.writeAll("\\n"),
295
+ '\r' => try writer.writeAll("\\r"),
296
+ '\t' => try writer.writeAll("\\t"),
297
+ else => try writer.writeByte(byte),
298
+ }
299
+ }
300
+
301
+ fn writeText(writer: anytype, inspection: Inspection) !void {
302
+ try writer.print("status\t{s}\n", .{inspection.status});
303
+ try writer.print("config\t{s}\t{s}\n", .{ if (inspection.config_exists) "ok" else "missing", inspection.config_path });
304
+ try writer.print("agentInstructions\t{s}\t{s}\n", .{ if (inspection.agent_instructions_exists) "ok" else "missing", inspection.agent_instructions_path });
305
+ for (inspection.platforms) |platform| {
306
+ try writer.print("{s}\t{s}", .{ platform.name, if (platform.enabled) "enabled" else "disabled" });
307
+ if (platform.smoke_scenario) |path| try writer.print("\t{s}", .{path});
308
+ try writer.writeAll("\n");
309
+ }
310
+ }
@@ -6,6 +6,11 @@ const trace = @import("trace.zig");
6
6
  const trace_summary = @import("trace_summary.zig");
7
7
  const validation = @import("validation.zig");
8
8
 
9
+ pub const RunDiscovery = struct {
10
+ json: ?[]const u8 = null,
11
+ error_name: ?[]const u8 = null,
12
+ };
13
+
9
14
  pub fn writeImportJson(writer: anytype, format: []const u8, source_path: []const u8, result: importer.ImportResult) !void {
10
15
  try writer.writeAll("{\"ok\":true,\"format\":");
11
16
  try trace.writeJsonString(writer, format);
@@ -131,12 +136,13 @@ pub fn writeRunSummaryJson(
131
136
  fallback_scenario: []const u8,
132
137
  fallback_app_id: []const u8,
133
138
  run_error: ?anyerror,
139
+ discovery: RunDiscovery,
134
140
  ) !void {
135
141
  if (trace_dir) |dir| {
136
142
  if (trace_summary.read(allocator, dir)) |summary_value| {
137
143
  var summary = summary_value;
138
144
  defer summary.deinit(allocator);
139
- return try writeRunSummaryFromTraceSummary(writer, dir, summary, run_error);
145
+ return try writeRunSummaryFromTraceSummary(writer, dir, summary, run_error, discovery);
140
146
  } else |_| {}
141
147
  }
142
148
 
@@ -153,6 +159,7 @@ pub fn writeRunSummaryJson(
153
159
  try writer.writeAll(",\"error\":");
154
160
  try trace.writeJsonString(writer, @errorName(err));
155
161
  }
162
+ try writeRunDiscoveryJson(writer, discovery);
156
163
  try writer.writeAll("}\n");
157
164
  }
158
165
 
@@ -258,6 +265,7 @@ fn writeRunSummaryFromTraceSummary(
258
265
  trace_dir: []const u8,
259
266
  summary: trace_summary.Summary,
260
267
  run_error: ?anyerror,
268
+ discovery: RunDiscovery,
261
269
  ) !void {
262
270
  try writer.writeAll("{\"ok\":");
263
271
  try writer.writeAll(if (std.mem.eql(u8, summary.status, "passed")) "true" else "false");
@@ -295,18 +303,34 @@ fn writeRunSummaryFromTraceSummary(
295
303
  try writer.writeAll(",\"reportPath\":");
296
304
  try trace.writeJsonString(writer, value);
297
305
  }
306
+ try writeRunDiscoveryJson(writer, discovery);
298
307
  try writeRunNextCommandsJson(writer, trace_dir);
299
308
  try writer.writeAll("}\n");
300
309
  }
301
310
 
311
+ fn writeRunDiscoveryJson(writer: anytype, discovery: RunDiscovery) !void {
312
+ if (discovery.json) |json| {
313
+ try writer.writeAll(",\"discovery\":");
314
+ try writer.writeAll(json);
315
+ }
316
+ if (discovery.error_name) |error_name| {
317
+ try writer.writeAll(",\"discoveryError\":");
318
+ try trace.writeJsonString(writer, error_name);
319
+ }
320
+ }
321
+
302
322
  fn writeRunNextCommandsJson(writer: anytype, trace_dir: []const u8) !void {
303
323
  try writer.writeAll(",\"nextCommands\":[\"zmr report ");
304
324
  try writeShellArgJsonContent(writer, trace_dir);
305
325
  try writer.writeAll(" --out ");
306
326
  try writeJoinedPathShellArgJsonContent(writer, trace_dir, "report.html");
327
+ try writer.writeAll(" --junit ");
328
+ try writeJoinedPathShellArgJsonContent(writer, trace_dir, "junit.xml");
307
329
  try writer.writeAll("\",\"zmr explain ");
308
330
  try writeShellArgJsonContent(writer, trace_dir);
309
- try writer.writeAll(" --json\",\"zmr export ");
331
+ try writer.writeAll(" --json\",\"zmr discover --from-trace ");
332
+ try writeShellArgJsonContent(writer, trace_dir);
333
+ try writer.writeAll(" --out .zmr/discovered/replay-smoke.json --include-actions --validate --force --json\",\"zmr export ");
310
334
  try writeShellArgJsonContent(writer, trace_dir);
311
335
  try writer.writeAll(" --out ");
312
336
  try writePathWithSuffixShellArgJsonContent(writer, trace_dir, ".zmrtrace");
package/src/cli_run.zig CHANGED
@@ -2,6 +2,7 @@ const std = @import("std");
2
2
 
3
3
  const android = @import("android.zig");
4
4
  const android_emulator = @import("android_emulator.zig");
5
+ const cli_discover = @import("cli_discover.zig");
5
6
  const cli_output = @import("cli_output.zig");
6
7
  const config_paths = @import("config_paths.zig");
7
8
  const ios = @import("ios.zig");
@@ -21,6 +22,7 @@ pub const ParsedArgs = struct {
21
22
  avdmanager_path_set: bool = false,
22
23
  xcrun_path_set: bool = false,
23
24
  config_path: ?[]const u8 = null,
25
+ discover_out: ?[]const u8 = null,
24
26
  json: bool = false,
25
27
  };
26
28
 
@@ -69,6 +71,9 @@ pub fn parseArgs(args: []const []const u8) !ParsedArgs {
69
71
  } else if (std.mem.eql(u8, arg, "--config")) {
70
72
  index += 1;
71
73
  parsed.config_path = if (index < args.len) args[index] else return error.MissingConfigPath;
74
+ } else if (std.mem.eql(u8, arg, "--discover-out")) {
75
+ index += 1;
76
+ parsed.discover_out = if (index < args.len) args[index] else return error.MissingDiscoverOut;
72
77
  } else if (std.mem.eql(u8, arg, "--screen-record")) {
73
78
  parsed.raw.screen_recording = true;
74
79
  } else if (std.mem.eql(u8, arg, "--no-screen-record")) {
@@ -153,6 +158,7 @@ pub fn run(allocator: std.mem.Allocator, args: *std.process.ArgIterator) !void {
153
158
  try config_paths.ownFilePath(allocator, &owned_config_paths, config_root.?, resolved.trace_dir.?)
154
159
  else
155
160
  resolved.trace_dir;
161
+ if (parsed.discover_out != null and trace_dir == null) return error.MissingTraceDir;
156
162
  const android_shim_path = if (raw.android_shim_path == null and config_root != null and resolved.android_shim_path != null)
157
163
  try config_paths.ownFilePath(allocator, &owned_config_paths, config_root.?, resolved.android_shim_path.?)
158
164
  else
@@ -185,6 +191,27 @@ pub fn run(allocator: std.mem.Allocator, args: *std.process.ArgIterator) !void {
185
191
  break :blk null;
186
192
  };
187
193
 
194
+ var discovery_payload = std.ArrayList(u8).empty;
195
+ defer discovery_payload.deinit(allocator);
196
+ var run_discovery = cli_output.RunDiscovery{};
197
+ if (parsed.discover_out) |out_path| {
198
+ if (cli_discover.discoverFromTrace(allocator, .{
199
+ .from_trace = trace_dir,
200
+ .out_path = out_path,
201
+ .include_actions = true,
202
+ .validate = true,
203
+ .force = true,
204
+ .json = true,
205
+ })) |discovered_value| {
206
+ var discovered = discovered_value;
207
+ defer discovered.deinit(allocator);
208
+ try cli_discover.writeJson(discovery_payload.writer(allocator), discovered.summary, discovered.validation);
209
+ run_discovery = .{ .json = std.mem.trimRight(u8, discovery_payload.items, " \t\r\n") };
210
+ } else |err| {
211
+ run_discovery = .{ .error_name = @errorName(err) };
212
+ }
213
+ }
214
+
188
215
  if (parsed.json) try cli_output.writeRunSummaryJson(
189
216
  allocator,
190
217
  std.fs.File.stdout().deprecatedWriter(),
@@ -192,6 +219,7 @@ pub fn run(allocator: std.mem.Allocator, args: *std.process.ArgIterator) !void {
192
219
  script.name,
193
220
  app_id,
194
221
  run_error,
222
+ run_discovery,
195
223
  );
196
224
  if (run_error) |err| return err;
197
225
  }
package/src/cli_trace.zig CHANGED
@@ -6,6 +6,7 @@ const report = @import("report.zig");
6
6
  pub const ReportArgs = struct {
7
7
  input_path: []const u8,
8
8
  out_path: ?[]const u8 = null,
9
+ junit_path: ?[]const u8 = null,
9
10
  };
10
11
 
11
12
  pub const ExplainArgs = struct {
@@ -30,6 +31,9 @@ pub fn parseReportArgs(args: []const []const u8) !ReportArgs {
30
31
  if (std.mem.eql(u8, arg, "--out")) {
31
32
  index += 1;
32
33
  parsed.out_path = if (index < args.len) args[index] else return error.MissingReportOutput;
34
+ } else if (std.mem.eql(u8, arg, "--junit")) {
35
+ index += 1;
36
+ parsed.junit_path = if (index < args.len) args[index] else return error.MissingJUnitOutput;
33
37
  } else {
34
38
  return error.UnknownFlag;
35
39
  }
@@ -84,6 +88,10 @@ pub fn runReport(allocator: std.mem.Allocator, args: *std.process.ArgIterator) !
84
88
  const parsed = try parseReportArgs(raw_args.items);
85
89
  try report.writeHtmlReport(allocator, parsed.input_path, parsed.out_path.?);
86
90
  try std.fs.File.stdout().deprecatedWriter().print("wrote {s}\n", .{parsed.out_path.?});
91
+ if (parsed.junit_path) |junit_path| {
92
+ try report.writeJUnitReport(allocator, parsed.input_path, junit_path);
93
+ try std.fs.File.stdout().deprecatedWriter().print("wrote {s}\n", .{junit_path});
94
+ }
87
95
  }
88
96
 
89
97
  pub fn runExplain(allocator: std.mem.Allocator, args: *std.process.ArgIterator) !void {
package/src/errors.zig CHANGED
@@ -10,6 +10,12 @@ pub fn classify(err: anyerror) PublicError {
10
10
  error.MissingScenarioPath => .{ .code = "cli.missing_scenario", .message = "missing scenario path" },
11
11
  error.MissingDeviceSerial => .{ .code = "cli.missing_device", .message = "missing device serial" },
12
12
  error.MissingTraceDir => .{ .code = "cli.missing_trace_dir", .message = "missing trace directory" },
13
+ error.MissingReportInput => .{ .code = "cli.missing_report_input", .message = "missing report input path" },
14
+ error.MissingReportOutput => .{ .code = "cli.missing_report_output", .message = "missing report output path" },
15
+ error.MissingDraftOut => .{ .code = "cli.missing_draft_out", .message = "missing draft output path" },
16
+ error.MissingDiscoverOut => .{ .code = "cli.missing_discover_out", .message = "missing discover output path" },
17
+ error.MissingJUnitOutput => .{ .code = "cli.missing_junit_output", .message = "missing JUnit output path" },
18
+ error.MissingTraceBundleOutput => .{ .code = "cli.missing_trace_bundle_output", .message = "missing trace bundle output path" },
13
19
  error.MissingAppId => .{ .code = "cli.missing_app_id", .message = "missing app id" },
14
20
  error.MissingAdbPath => .{ .code = "cli.missing_adb_path", .message = "missing adb path" },
15
21
  error.MissingXcrunPath => .{ .code = "cli.missing_xcrun_path", .message = "missing xcrun path" },
@@ -19,6 +25,9 @@ pub fn classify(err: anyerror) PublicError {
19
25
  error.UnknownFlag => .{ .code = "cli.unknown_flag", .message = "unknown flag" },
20
26
  error.MissingParam => .{ .code = "cli.missing_param", .message = "missing parameter" },
21
27
  error.FileNotFound => .{ .code = "scenario.file_not_found", .message = "scenario file was not found" },
28
+ error.SemanticSnapshotMissing => .{ .code = "draft.no_snapshot", .message = "no semantic snapshot was found in the trace" },
29
+ error.InvalidTraceManifest => .{ .code = "trace.invalid_manifest", .message = "trace manifest is invalid" },
30
+ error.InvalidSemanticSnapshot => .{ .code = "trace.invalid_semantic_snapshot", .message = "semantic snapshot is invalid" },
22
31
  error.UnsupportedPlatform => .{ .code = "cli.unsupported_platform", .message = "unsupported platform" },
23
32
  error.UnsupportedTransport => .{ .code = "cli.unsupported_transport", .message = "unsupported transport" },
24
33
  error.ScenarioMustBeObject,
package/src/ios.zig CHANGED
@@ -9,7 +9,9 @@ const trace = @import("trace.zig");
9
9
  const types = @import("types.zig");
10
10
 
11
11
  const default_max_output = 32 * 1024 * 1024;
12
- const shim_timeout_ms = 600_000;
12
+ // Clean iOS prebuilds can force the XCTest shim script through a full native
13
+ // dependency build before it can answer the first selector query.
14
+ const shim_timeout_ms = 5_400_000;
13
15
  const shim_best_effort_timeout_ms = 10_000;
14
16
  const shim_command_attempts = 2;
15
17
  const shim_bootstrap_retry_delay_ms = 500;
@@ -107,6 +109,7 @@ pub const IosDevice = struct {
107
109
  if (self.target_kind == .physical) return try self.stopPhysicalBestEffort();
108
110
  const result = try self.runSimctl(&.{ "terminate", self.target(), self.app_id }, default_max_output);
109
111
  defer result.deinit(self.allocator);
112
+ if (ios_lifecycle.isAppNotRunning(result)) return;
110
113
  try result.ensureSuccess();
111
114
  }
112
115
 
@@ -307,8 +310,12 @@ pub const IosDevice = struct {
307
310
 
308
311
  var command_with_selector = shim_command;
309
312
  command_with_selector.selector = shim_selector;
310
- try self.runShimAction(command_with_selector);
311
- return true;
313
+ const response = try self.runShim(command_with_selector);
314
+ defer self.allocator.free(response);
315
+ return switch (try ios_shim.parseSelectorActionResponse(response)) {
316
+ .ok => true,
317
+ .selector_unavailable => false,
318
+ };
312
319
  }
313
320
 
314
321
  fn runShim(self: *IosDevice, shim_command: ios_shim.Command) ![]u8 {
@@ -394,6 +401,6 @@ pub fn parsePhysicalDevicesJson(allocator: std.mem.Allocator, content: []const u
394
401
  }
395
402
 
396
403
  test "ios xctest shim timeout allows cold xcodebuild startup" {
397
- try std.testing.expect(shim_timeout_ms >= 300_000);
404
+ try std.testing.expect(shim_timeout_ms >= 5_400_000);
398
405
  try std.testing.expect(shim_best_effort_timeout_ms <= 15_000);
399
406
  }