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.
- package/CHANGELOG.md +484 -0
- package/CONTRIBUTING.md +42 -0
- package/FEATURES.md +112 -0
- package/LICENSE +21 -0
- package/README.md +255 -0
- package/SECURITY.md +34 -0
- package/build.zig +38 -0
- package/build.zig.zon +7 -0
- package/clients/README.md +144 -0
- package/clients/go/README.md +24 -0
- package/clients/go/examples/fake-session/main.go +93 -0
- package/clients/go/go.mod +3 -0
- package/clients/go/zmr/client.go +432 -0
- package/clients/kotlin/README.md +35 -0
- package/clients/kotlin/build.gradle.kts +35 -0
- package/clients/kotlin/settings.gradle.kts +15 -0
- package/clients/kotlin/src/main/kotlin/dev/zmr/FakeSession.kt +86 -0
- package/clients/kotlin/src/main/kotlin/dev/zmr/ZmrClient.kt +67 -0
- package/clients/python/README.md +29 -0
- package/clients/python/examples/fake_session.py +48 -0
- package/clients/python/pyproject.toml +13 -0
- package/clients/python/zmr_client.py +202 -0
- package/clients/rust/Cargo.lock +107 -0
- package/clients/rust/Cargo.toml +10 -0
- package/clients/rust/README.md +19 -0
- package/clients/rust/examples/fake_session.rs +70 -0
- package/clients/rust/src/lib.rs +461 -0
- package/clients/swift/Package.swift +16 -0
- package/clients/swift/README.md +36 -0
- package/clients/swift/Sources/ZMRClient/ZMRClient.swift +114 -0
- package/clients/swift/Sources/ZMRFakeSession/main.swift +86 -0
- package/clients/typescript/README.md +34 -0
- package/clients/typescript/examples/fake-session.mjs +36 -0
- package/clients/typescript/index.d.ts +144 -0
- package/clients/typescript/index.mjs +192 -0
- package/clients/typescript/package.json +8 -0
- package/docs/adr/0001-agent-native-runner-boundary.md +31 -0
- package/docs/adr/0002-app-local-zmr-contract.md +39 -0
- package/docs/adr/0003-ios-simulator-xctest-shim.md +41 -0
- package/docs/adr/0004-benchmark-claims-and-baseline-collection.md +37 -0
- package/docs/adr/README.md +12 -0
- package/docs/ai-agents.md +156 -0
- package/docs/app-integration.md +316 -0
- package/docs/benchmarking.md +275 -0
- package/docs/client-installation.md +141 -0
- package/docs/clients.md +98 -0
- package/docs/config.md +175 -0
- package/docs/demo.md +259 -0
- package/docs/dsl.md +57 -0
- package/docs/install.md +233 -0
- package/docs/market-positioning.md +70 -0
- package/docs/npm.md +359 -0
- package/docs/protocol-fixtures/README.md +8 -0
- package/docs/protocol-fixtures/core-session.requests.jsonl +8 -0
- package/docs/protocol-fixtures/core-session.responses.jsonl +8 -0
- package/docs/protocol-versioning.md +65 -0
- package/docs/protocol.md +560 -0
- package/docs/publication.md +77 -0
- package/docs/release-audit.md +99 -0
- package/docs/release-candidate.md +111 -0
- package/docs/release-evidence.md +188 -0
- package/docs/release-notes-template.md +58 -0
- package/docs/roadmap.md +334 -0
- package/docs/scenario-authoring.md +88 -0
- package/docs/shipping.md +170 -0
- package/docs/trace-privacy.md +88 -0
- package/docs/troubleshooting.md +256 -0
- package/examples/android-app-auth-probe.json +89 -0
- package/examples/android-app-error-state.json +13 -0
- package/examples/android-app-login-smoke.json +192 -0
- package/examples/android-app-onboarding.json +12 -0
- package/examples/android-app-referral-deep-link.json +12 -0
- package/examples/android-shim-smoke.json +19 -0
- package/examples/demo-failure.json +12 -0
- package/examples/demo-fake.json +14 -0
- package/examples/ios-dev-client-open-link.json +26 -0
- package/examples/ios-dev-client-route-snapshot.json +24 -0
- package/examples/ios-shim-smoke.json +23 -0
- package/examples/ios-smoke.json +9 -0
- package/go.work +3 -0
- package/npm/agents.mjs +183 -0
- package/npm/app-config.mjs +95 -0
- package/npm/build-zmr.mjs +21 -0
- package/npm/commands.mjs +104 -0
- package/npm/generated-files.mjs +50 -0
- package/npm/index.mjs +75 -0
- package/npm/init-app.mjs +80 -0
- package/npm/package-scripts.mjs +72 -0
- package/npm/postinstall.mjs +21 -0
- package/npm/scaffold.mjs +179 -0
- package/npm/scenarios.mjs +93 -0
- package/npm/setup.mjs +69 -0
- package/npm/wizard.mjs +117 -0
- package/npm/zmr.mjs +23 -0
- package/package.json +114 -0
- 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 +26 -0
- package/schemas/action-result.schema.json +27 -0
- package/schemas/capabilities-output.schema.json +98 -0
- package/schemas/devices-output.schema.json +25 -0
- package/schemas/doctor-output.schema.json +51 -0
- package/schemas/explain-output.schema.json +51 -0
- package/schemas/import-output.schema.json +23 -0
- package/schemas/init-output.schema.json +71 -0
- package/schemas/json-rpc.schema.json +55 -0
- package/schemas/release-manifest.schema.json +43 -0
- package/schemas/release-readiness-output.schema.json +127 -0
- package/schemas/run-output.schema.json +43 -0
- package/schemas/scenario.schema.json +128 -0
- package/schemas/schemas-output.schema.json +26 -0
- package/schemas/semantic-snapshot.schema.json +116 -0
- package/schemas/snapshot.schema.json +60 -0
- package/schemas/trace-event.schema.json +14 -0
- package/schemas/trace-manifest.schema.json +59 -0
- package/schemas/validate-output.schema.json +42 -0
- package/schemas/version-output.schema.json +23 -0
- package/schemas/zmr-config.schema.json +75 -0
- package/scripts/android-emulator.sh +126 -0
- package/scripts/assert-ios-physical-ready.sh +213 -0
- package/scripts/benchmark-command.sh +307 -0
- package/scripts/benchmark.sh +359 -0
- package/scripts/benchmark_gate.py +117 -0
- package/scripts/benchmark_result_row.py +88 -0
- package/scripts/compare-benchmarks.py +288 -0
- package/scripts/create-android-demo-app.sh +342 -0
- package/scripts/create-ios-demo-app.sh +261 -0
- package/scripts/demo-android-real.sh +232 -0
- package/scripts/demo-ios-real.sh +270 -0
- package/scripts/demo.sh +464 -0
- package/scripts/device-matrix.sh +338 -0
- package/scripts/ensure-ios-shim-target.rb +237 -0
- package/scripts/install-android-shim.sh +281 -0
- package/scripts/install-ios-shim.sh +589 -0
- package/scripts/pilot-gate.sh +560 -0
- package/scripts/release-readiness.py +838 -0
- package/scripts/release-readiness.sh +91 -0
- package/scripts/run-android-pilot.sh +561 -0
- package/scripts/run-ios-pilot.sh +509 -0
- package/shims/android/README.md +21 -0
- package/shims/android/ZMRShimInstrumentedTest.java +152 -0
- package/shims/android/protocol.md +18 -0
- package/shims/ios/README.md +50 -0
- package/shims/ios/ZMRShim.swift +110 -0
- package/shims/ios/ZMRShimUITestCase.swift +475 -0
- package/shims/ios/protocol.md +74 -0
- package/skills/zmr-mobile-testing/SKILL.md +127 -0
- package/src/android.zig +344 -0
- package/src/android_device_info.zig +99 -0
- package/src/android_emulator.zig +154 -0
- package/src/android_screen_recording.zig +112 -0
- package/src/android_shell.zig +112 -0
- package/src/bundle.zig +124 -0
- package/src/bundle_redaction.zig +272 -0
- package/src/bundle_tar.zig +123 -0
- package/src/cli_devices.zig +97 -0
- package/src/cli_doctor.zig +114 -0
- package/src/cli_import.zig +70 -0
- package/src/cli_info.zig +39 -0
- package/src/cli_init.zig +72 -0
- package/src/cli_output.zig +467 -0
- package/src/cli_run.zig +259 -0
- package/src/cli_serve.zig +287 -0
- package/src/cli_trace.zig +111 -0
- package/src/cli_validate.zig +41 -0
- package/src/command.zig +211 -0
- package/src/config.zig +305 -0
- package/src/config_diagnostics.zig +212 -0
- package/src/config_paths.zig +49 -0
- package/src/device_registry.zig +37 -0
- package/src/doctor.zig +412 -0
- package/src/doctor_hints.zig +52 -0
- package/src/errors.zig +55 -0
- package/src/fake_device.zig +163 -0
- package/src/health.zig +28 -0
- package/src/importer.zig +343 -0
- package/src/importer_json.zig +100 -0
- package/src/importer_model.zig +103 -0
- package/src/ios.zig +399 -0
- package/src/ios_devices.zig +219 -0
- package/src/ios_lifecycle.zig +72 -0
- package/src/ios_shim.zig +242 -0
- package/src/ios_snapshot.zig +20 -0
- package/src/json_fields.zig +80 -0
- package/src/json_rpc.zig +150 -0
- package/src/json_rpc_methods.zig +318 -0
- package/src/json_rpc_observation.zig +31 -0
- package/src/json_rpc_params.zig +52 -0
- package/src/json_rpc_protocol.zig +110 -0
- package/src/json_rpc_trace.zig +73 -0
- package/src/main.zig +135 -0
- package/src/mcp.zig +234 -0
- package/src/mcp_protocol.zig +64 -0
- package/src/mcp_trace.zig +83 -0
- package/src/report.zig +346 -0
- package/src/report_html.zig +63 -0
- package/src/report_values.zig +27 -0
- package/src/run_options.zig +152 -0
- package/src/runner.zig +280 -0
- package/src/runner_actions.zig +109 -0
- package/src/runner_config.zig +6 -0
- package/src/runner_diagnostics.zig +268 -0
- package/src/runner_events.zig +170 -0
- package/src/runner_native.zig +88 -0
- package/src/runner_waits.zig +300 -0
- package/src/scaffold.zig +472 -0
- package/src/scenario.zig +346 -0
- package/src/scenario_fields.zig +50 -0
- package/src/schema_registry.zig +53 -0
- package/src/selector.zig +84 -0
- package/src/semantic.zig +171 -0
- package/src/trace.zig +315 -0
- package/src/trace_json.zig +340 -0
- package/src/trace_summary.zig +218 -0
- package/src/trace_summary_diagnostic.zig +202 -0
- package/src/types.zig +120 -0
- package/src/uiautomator.zig +164 -0
- package/src/validation.zig +187 -0
- package/src/version.zig +22 -0
- package/viewer/app.js +373 -0
- package/viewer/index.html +126 -0
- package/viewer/parser.js +233 -0
- package/viewer/styles.css +585 -0
package/viewer/parser.js
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
(function (root, factory) {
|
|
2
|
+
const api = factory();
|
|
3
|
+
if (typeof module === "object" && module.exports) module.exports = api;
|
|
4
|
+
root.ZmrTraceParser = api;
|
|
5
|
+
})(typeof globalThis !== "undefined" ? globalThis : window, function () {
|
|
6
|
+
const decoder = new TextDecoder();
|
|
7
|
+
|
|
8
|
+
function parseTarArchive(buffer) {
|
|
9
|
+
const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
|
|
10
|
+
const entries = [];
|
|
11
|
+
let offset = 0;
|
|
12
|
+
|
|
13
|
+
while (offset + 512 <= bytes.length) {
|
|
14
|
+
const header = bytes.subarray(offset, offset + 512);
|
|
15
|
+
if (isZeroBlock(header)) break;
|
|
16
|
+
|
|
17
|
+
const name = readString(header, 0, 100);
|
|
18
|
+
const prefix = readString(header, 345, 155);
|
|
19
|
+
const path = prefix ? `${prefix}/${name}` : name;
|
|
20
|
+
const sizeText = readString(header, 124, 12).trim();
|
|
21
|
+
const size = sizeText ? Number.parseInt(sizeText, 8) : 0;
|
|
22
|
+
if (!Number.isFinite(size) || size < 0) {
|
|
23
|
+
throw new Error(`Invalid tar size for ${path || "<unknown>"}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const dataStart = offset + 512;
|
|
27
|
+
const dataEnd = dataStart + size;
|
|
28
|
+
if (dataEnd > bytes.length) {
|
|
29
|
+
throw new Error(`Unexpected end of archive while reading ${path}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const entryBytes = bytes.slice(dataStart, dataEnd);
|
|
33
|
+
entries.push({
|
|
34
|
+
path,
|
|
35
|
+
bytes: entryBytes,
|
|
36
|
+
text: () => decoder.decode(entryBytes),
|
|
37
|
+
mime: mimeForPath(path),
|
|
38
|
+
});
|
|
39
|
+
offset = dataStart + align512(size);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return entries;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function parseEventsJsonl(content) {
|
|
46
|
+
const events = [];
|
|
47
|
+
for (const rawLine of content.split(/\r?\n/)) {
|
|
48
|
+
const line = rawLine.trim();
|
|
49
|
+
if (!line) continue;
|
|
50
|
+
try {
|
|
51
|
+
events.push(JSON.parse(line));
|
|
52
|
+
} catch (error) {
|
|
53
|
+
events.push({
|
|
54
|
+
seq: events.length + 1,
|
|
55
|
+
timestampMs: null,
|
|
56
|
+
kind: "parse.error",
|
|
57
|
+
payload: {
|
|
58
|
+
message: error instanceof Error ? error.message : String(error),
|
|
59
|
+
line,
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return events;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function buildTraceModel(entries) {
|
|
68
|
+
const byPath = new Map(entries.map((entry) => [entry.path, entry]));
|
|
69
|
+
const manifestEntry = byPath.get("trace.json");
|
|
70
|
+
if (!manifestEntry) throw new Error("trace.json is missing from this bundle");
|
|
71
|
+
const manifest = JSON.parse(manifestEntry.text());
|
|
72
|
+
const eventsEntry = byPath.get(manifest.eventsPath || "events.jsonl");
|
|
73
|
+
const events = eventsEntry ? parseEventsJsonl(eventsEntry.text()) : [];
|
|
74
|
+
const artifacts = entries
|
|
75
|
+
.filter((entry) => entry.path.startsWith(`${manifest.artifactsDir || "artifacts"}/`))
|
|
76
|
+
.map((entry) => ({
|
|
77
|
+
path: entry.path,
|
|
78
|
+
name: entry.path.split("/").at(-1),
|
|
79
|
+
mime: entry.mime,
|
|
80
|
+
entry,
|
|
81
|
+
}));
|
|
82
|
+
const snapshots = new Map();
|
|
83
|
+
for (const artifact of artifacts) {
|
|
84
|
+
if (!artifact.path.endsWith(".json")) continue;
|
|
85
|
+
try {
|
|
86
|
+
const snapshot = JSON.parse(artifact.entry.text());
|
|
87
|
+
if (snapshot && typeof snapshot.id === "string") snapshots.set(snapshot.id, snapshot);
|
|
88
|
+
} catch {
|
|
89
|
+
// Non-snapshot JSON artifacts stay visible in the artifact list.
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const snapshotInspections = buildSnapshotInspections(snapshots, artifacts);
|
|
93
|
+
const replayFrames = buildReplayFrames(events, snapshotInspections);
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
manifest,
|
|
97
|
+
events,
|
|
98
|
+
artifacts,
|
|
99
|
+
snapshots,
|
|
100
|
+
snapshotInspections,
|
|
101
|
+
replayFrames,
|
|
102
|
+
entriesByPath: byPath,
|
|
103
|
+
summary: {
|
|
104
|
+
scenarioName: manifest.scenarioName || "",
|
|
105
|
+
appId: manifest.appId || "",
|
|
106
|
+
status: manifest.status || inferStatus(events),
|
|
107
|
+
durationMs: manifest.durationMs ?? null,
|
|
108
|
+
eventCount: manifest.eventCount ?? events.length,
|
|
109
|
+
snapshotCount: manifest.snapshotCount ?? snapshots.size,
|
|
110
|
+
failedStepIndex: manifest.failedStepIndex ?? null,
|
|
111
|
+
error: manifest.error ?? null,
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function buildReplayFrames(events, snapshotInspections) {
|
|
117
|
+
const startTimestampMs = firstTimestamp(events);
|
|
118
|
+
const frames = [];
|
|
119
|
+
for (const event of events) {
|
|
120
|
+
const snapshotId = snapshotIdForEvent(event);
|
|
121
|
+
if (!snapshotId) continue;
|
|
122
|
+
const inspection = snapshotInspections.get(snapshotId);
|
|
123
|
+
if (!inspection) continue;
|
|
124
|
+
const timestampMs = typeof event.timestampMs === "number" ? event.timestampMs : null;
|
|
125
|
+
frames.push({
|
|
126
|
+
index: frames.length,
|
|
127
|
+
seq: event.seq ?? null,
|
|
128
|
+
timestampMs,
|
|
129
|
+
elapsedMs: timestampMs == null || startTimestampMs == null ? null : Math.max(0, timestampMs - startTimestampMs),
|
|
130
|
+
kind: event.kind ?? "unknown",
|
|
131
|
+
status: event.payload?.status ?? (event.payload?.error ? "error" : "event"),
|
|
132
|
+
snapshotId,
|
|
133
|
+
event,
|
|
134
|
+
inspection,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
return frames;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function snapshotIdForEvent(event) {
|
|
141
|
+
if (event.payload?.snapshotId) return event.payload.snapshotId;
|
|
142
|
+
if (event.payload?.afterSnapshotId) return event.payload.afterSnapshotId;
|
|
143
|
+
if (event.payload?.beforeSnapshotId) return event.payload.beforeSnapshotId;
|
|
144
|
+
if (typeof event.payload?.value === "string") {
|
|
145
|
+
const match = event.payload.value.match(/(?:^|\/)(snapshot-[^/.]+)\.json$/);
|
|
146
|
+
if (match) return match[1];
|
|
147
|
+
}
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function firstTimestamp(events) {
|
|
152
|
+
for (const event of events) {
|
|
153
|
+
if (typeof event.timestampMs === "number") return event.timestampMs;
|
|
154
|
+
}
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function buildSnapshotInspections(snapshots, artifacts) {
|
|
159
|
+
const byId = new Map();
|
|
160
|
+
for (const [id, snapshot] of snapshots) {
|
|
161
|
+
const screenshot = artifactForSnapshot(artifacts, snapshot.screenshotArtifact, id, ".png");
|
|
162
|
+
const tree = artifactForSnapshot(artifacts, snapshot.treeArtifact, id, ".xml");
|
|
163
|
+
byId.set(id, {
|
|
164
|
+
id,
|
|
165
|
+
snapshot,
|
|
166
|
+
screenshot,
|
|
167
|
+
tree,
|
|
168
|
+
nodes: Array.isArray(snapshot.nodes) ? snapshot.nodes.map(normalizeNode) : [],
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
return byId;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function artifactForSnapshot(artifacts, artifactPath, snapshotId, extension) {
|
|
175
|
+
if (typeof artifactPath === "string" && artifactPath) {
|
|
176
|
+
const byExact = artifacts.find((artifact) => artifact.path === artifactPath || artifactPath.endsWith(artifact.path));
|
|
177
|
+
if (byExact) return byExact;
|
|
178
|
+
}
|
|
179
|
+
return artifacts.find((artifact) => artifact.path.endsWith(`${snapshotId}${extension}`)) ?? null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function normalizeNode(node) {
|
|
183
|
+
return {
|
|
184
|
+
stableId: node.stableId ?? node.id ?? "",
|
|
185
|
+
label: node.text ?? node.contentDesc ?? node.label ?? node.identifier ?? "",
|
|
186
|
+
className: node.className ?? node.type ?? "",
|
|
187
|
+
resourceId: node.resourceId ?? node.identifier ?? "",
|
|
188
|
+
enabled: node.enabled !== false,
|
|
189
|
+
visible: node.visible !== false,
|
|
190
|
+
selected: node.selected === true,
|
|
191
|
+
bounds: node.bounds ?? null,
|
|
192
|
+
raw: node,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function inferStatus(events) {
|
|
197
|
+
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
198
|
+
const event = events[index];
|
|
199
|
+
if (event.kind === "scenario.end" && event.payload?.status) return event.payload.status;
|
|
200
|
+
}
|
|
201
|
+
return "unknown";
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function align512(value) {
|
|
205
|
+
return Math.ceil(value / 512) * 512;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function isZeroBlock(bytes) {
|
|
209
|
+
return bytes.every((byte) => byte === 0);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function readString(bytes, offset, length) {
|
|
213
|
+
const slice = bytes.subarray(offset, offset + length);
|
|
214
|
+
const nul = slice.indexOf(0);
|
|
215
|
+
const actual = nul === -1 ? slice : slice.subarray(0, nul);
|
|
216
|
+
return decoder.decode(actual);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function mimeForPath(path) {
|
|
220
|
+
if (path.endsWith(".png")) return "image/png";
|
|
221
|
+
if (path.endsWith(".jpg") || path.endsWith(".jpeg")) return "image/jpeg";
|
|
222
|
+
if (path.endsWith(".json")) return "application/json";
|
|
223
|
+
if (path.endsWith(".xml")) return "application/xml";
|
|
224
|
+
if (path.endsWith(".html")) return "text/html";
|
|
225
|
+
return "application/octet-stream";
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
buildTraceModel,
|
|
230
|
+
parseEventsJsonl,
|
|
231
|
+
parseTarArchive,
|
|
232
|
+
};
|
|
233
|
+
});
|