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.
- package/CHANGELOG.md +162 -3
- package/FEATURES.md +50 -7
- package/README.md +133 -7
- 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 +202 -0
- package/docs/ai-agents.md +87 -6
- package/docs/benchmarking.md +10 -3
- package/docs/clients.md +10 -6
- package/docs/demo.md +4 -0
- package/docs/expo-smoke.md +79 -0
- package/docs/install.md +3 -2
- package/docs/npm.md +58 -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 +3 -0
- package/docs/troubleshooting.md +1 -1
- package/npm/agents.mjs +16 -0
- package/npm/build-zmr.mjs +1 -1
- package/npm/commands.mjs +9 -5
- package/npm/postinstall.mjs +28 -2
- package/npm/verify-publish.mjs +36 -0
- package/package.json +2 -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/install-ios-shim.sh +79 -14
- 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 +3 -0
- package/shims/ios/ZMRShimUITestCase.swift +41 -11
- 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_inspect.zig +310 -0
- package/src/cli_output.zig +26 -2
- package/src/cli_run.zig +28 -0
- package/src/cli_trace.zig +8 -0
- package/src/errors.zig +9 -0
- package/src/ios.zig +11 -4
- package/src/ios_lifecycle.zig +36 -0
- package/src/ios_shim.zig +42 -0
- 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 +24 -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
|
@@ -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
|
+
}
|
package/src/cli_output.zig
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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.
|
|
311
|
-
|
|
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 >=
|
|
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
|
}
|