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,560 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ SOURCE="${BASH_SOURCE[0]}"
5
+ while [[ -h "$SOURCE" ]]; do
6
+ SOURCE_DIR="$(cd -P "$(dirname "$SOURCE")" && pwd)"
7
+ SOURCE="$(readlink "$SOURCE")"
8
+ if [[ "$SOURCE" != /* ]]; then
9
+ SOURCE="$SOURCE_DIR/$SOURCE"
10
+ fi
11
+ done
12
+
13
+ SCRIPT_DIR="$(cd -P "$(dirname "$SOURCE")" && pwd)"
14
+ ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
15
+ CALLER_CWD="$(pwd -P)"
16
+
17
+ # Some sandboxed environments do not allow writing to the default temp directory
18
+ # (/var/folders, /tmp). Use a repo-local TMPDIR so adb/xcrun/mktemp/heredocs work.
19
+ if [[ -z "${TMPDIR:-}" || ! -w "${TMPDIR:-/nonexistent}" ]]; then
20
+ TMPDIR="$ROOT/traces/tmp"
21
+ mkdir -p "$TMPDIR"
22
+ export TMPDIR
23
+ fi
24
+
25
+ ANDROID_SELECTED=0
26
+ IOS_SELECTED=0
27
+ ANDROID_APP_ROOT="${ANDROID_APP_ROOT:-}"
28
+ ANDROID_APP_ID="${ANDROID_APP_ID:-}"
29
+ ANDROID_DEVICE="${ANDROID_DEVICE:-emulator-5554}"
30
+ ANDROID_APK="${ANDROID_APK:-}"
31
+ ANDROID_SCENARIO="${ANDROID_SCENARIO:-}"
32
+ ANDROID_SKIP_EMULATOR=0
33
+ ANDROID_SKIP_METRO=0
34
+ ADB="${ADB:-}"
35
+ IOS_APP_ROOT="${IOS_APP_ROOT:-}"
36
+ IOS_APP_ID="${IOS_APP_ID:-}"
37
+ IOS_DEVICE="${IOS_DEVICE:-booted}"
38
+ IOS_DEVICE_TYPE="${IOS_DEVICE_TYPE:-simulator}"
39
+ IOS_APP_PATH="${IOS_APP_PATH:-}"
40
+ IOS_SHIM="${IOS_SHIM:-}"
41
+ XCRUN="${XCRUN:-}"
42
+ ZMR_BIN="${ZMR_BIN:-$(command -v zmr 2>/dev/null || true)}"
43
+ TRACE_ROOT="${TRACE_ROOT:-$CALLER_CWD/traces/pilot-gate-$(date +%Y%m%d-%H%M%S)}"
44
+ EVIDENCE_OUT="${EVIDENCE_OUT:-}"
45
+ RUNS="${RUNS:-20}"
46
+ MIN_PASS_RATE="${MIN_PASS_RATE:-100}"
47
+ MAX_FAILURES="${MAX_FAILURES:-0}"
48
+ MAX_MEAN_MS="${MAX_MEAN_MS:-}"
49
+ ANDROID_MAX_P95_MS="${ANDROID_MAX_P95_MS:-30000}"
50
+ IOS_MAX_P95_MS="${IOS_MAX_P95_MS:-45000}"
51
+ DRY_RUN=0
52
+
53
+ usage() {
54
+ cat <<'USAGE'
55
+ Usage:
56
+ scripts/pilot-gate.sh [--android] [--ios] [options]
57
+
58
+ Runs the external real-device release gate by delegating to the Android and iOS
59
+ pilot wrappers with repeated-run thresholds. If neither --android nor --ios is
60
+ passed, both platforms are selected.
61
+
62
+ Android options:
63
+ --android-app-root <dir> App repo root for the Android pilot. Required when Android is selected.
64
+ --android-app-id <id> Android application id. Defaults to the pilot wrapper default.
65
+ --android-device <serial> Android serial. Default: emulator-5554.
66
+ --android-apk <path> APK path. Defaults to the pilot wrapper default.
67
+ --android-scenario <path> ZMR scenario JSON to run for the Android pilot.
68
+ --adb <path> adb path forwarded to the Android pilot.
69
+ --skip-emulator Require/use an already booted Android device.
70
+ --skip-metro Do not start the app test server for Android.
71
+
72
+ iOS options:
73
+ --ios-app-root <dir> App repo root for iOS pilot evidence. Required when iOS is selected.
74
+ --ios-app-path <path> Built .app/.ipa. Required when iOS is selected.
75
+ --ios-app-id <id> iOS bundle id. Defaults to the pilot wrapper default.
76
+ --ios-device <udid> Simulator UDID or booted. Default: booted.
77
+ --ios-device-type <simulator|physical>
78
+ iOS target type. Default: simulator.
79
+ --ios-shim <path> XCTest shim command for selector-grade iOS runs.
80
+ --xcrun <path> xcrun path forwarded to the iOS pilot.
81
+ --zmr-bin <path> zmr binary path forwarded to pilot wrappers.
82
+
83
+ Gate options:
84
+ --trace-root <dir> Output root. Default: traces/pilot-gate-<timestamp>.
85
+ --evidence-out <path> Optional JSONL evidence file for zmr-release-readiness.
86
+ --runs <n> Repeated run count. Default: 20.
87
+ --min-pass-rate <pct> Minimum pass rate. Default: 100.
88
+ --max-failures <n> Maximum failed runs. Default: 0.
89
+ --max-mean-ms <ms> Optional mean duration maximum for both platforms.
90
+ --android-max-p95-ms <ms> Android p95 duration maximum. Default: 30000.
91
+ --ios-max-p95-ms <ms> iOS p95 duration maximum. Default: 45000.
92
+ --dry-run Print commands without executing them.
93
+ -h, --help Show this help.
94
+
95
+ Environment defaults mirror the upper-case option names, for example
96
+ ANDROID_APP_ROOT, IOS_APP_PATH, ADB, XCRUN, ZMR_BIN, RUNS, TRACE_ROOT, and EVIDENCE_OUT.
97
+ USAGE
98
+ }
99
+
100
+ die() {
101
+ echo "error: $*" >&2
102
+ exit 2
103
+ }
104
+
105
+ quote_cmd() {
106
+ local quoted=()
107
+ local arg
108
+ for arg in "$@"; do
109
+ quoted+=("$(printf '%q' "$arg")")
110
+ done
111
+ printf '%s\n' "${quoted[*]}"
112
+ }
113
+
114
+ resolve_path_from_cwd() {
115
+ local value="$1"
116
+ local absolute dir base probe suffix
117
+ local resolved_root
118
+ if [[ -z "$value" ]]; then
119
+ printf '\n'
120
+ return 0
121
+ fi
122
+ if [[ "$value" == /* ]]; then
123
+ absolute="$value"
124
+ else
125
+ absolute="$CALLER_CWD/$value"
126
+ fi
127
+ if [[ -d "$absolute" ]]; then
128
+ cd "$absolute" && pwd -P
129
+ return 0
130
+ fi
131
+ if [[ -e "$absolute" ]]; then
132
+ dir="$(dirname "$absolute")"
133
+ base="$(basename "$absolute")"
134
+ printf '%s/%s\n' "$(cd "$dir" && pwd -P)" "$base"
135
+ return 0
136
+ fi
137
+ dir="$(dirname "$absolute")"
138
+ base="$(basename "$absolute")"
139
+ if [[ -d "$dir" ]]; then
140
+ printf '%s/%s\n' "$(cd "$dir" && pwd -P)" "$base"
141
+ return 0
142
+ fi
143
+ probe="$absolute"
144
+ suffix=""
145
+ while [[ "$probe" != "/" && ! -e "$probe" ]]; do
146
+ suffix="/$(basename "$probe")$suffix"
147
+ probe="$(dirname "$probe")"
148
+ done
149
+ if [[ -d "$probe" ]]; then
150
+ resolved_root="$(cd "$probe" && pwd -P)"
151
+ if [[ "$resolved_root" == "/" ]]; then
152
+ printf '%s\n' "$suffix"
153
+ else
154
+ printf '%s%s\n' "$resolved_root" "$suffix"
155
+ fi
156
+ return 0
157
+ fi
158
+ while [[ "$absolute" == *"/./"* ]]; do
159
+ absolute="${absolute//\/.\//\/}"
160
+ done
161
+ printf '%s\n' "$absolute"
162
+ }
163
+
164
+ resolve_command_path_from_cwd() {
165
+ local value="$1"
166
+ if [[ -z "$value" || "$value" != */* ]]; then
167
+ printf '%s\n' "$value"
168
+ else
169
+ resolve_path_from_cwd "$value"
170
+ fi
171
+ }
172
+
173
+ run() {
174
+ echo "+ $(quote_cmd "$@")"
175
+ if [[ "$DRY_RUN" -eq 0 ]]; then
176
+ "$@"
177
+ fi
178
+ }
179
+
180
+ json_escape() {
181
+ python3 -c 'import json,sys; print(json.dumps(sys.argv[1]))' "$1"
182
+ }
183
+
184
+ write_evidence() {
185
+ local name="$1"
186
+ local status="$2"
187
+ local command="$3"
188
+ local duration_ms="$4"
189
+ local trace_root="$5"
190
+
191
+ if [[ -z "$EVIDENCE_OUT" ]]; then
192
+ return 0
193
+ fi
194
+
195
+ local metadata_json=""
196
+ if [[ "$name" == "Android hardware pilot" && -n "$ANDROID_APP_ID" ]]; then
197
+ metadata_json+=",\"androidAppId\":$(json_escape "$ANDROID_APP_ID")"
198
+ fi
199
+ if [[ "$name" == "Android hardware pilot" && -n "$ANDROID_APP_ROOT" ]]; then
200
+ metadata_json+=",\"androidAppRoot\":$(json_escape "$ANDROID_APP_ROOT")"
201
+ fi
202
+ if [[ "$name" == "Android hardware pilot" && -n "$ANDROID_DEVICE" ]]; then
203
+ metadata_json+=",\"androidDeviceId\":$(json_escape "$ANDROID_DEVICE")"
204
+ fi
205
+ if [[ "$name" == "physical iOS readiness" && -n "$IOS_DEVICE" ]]; then
206
+ metadata_json+=",\"iosDeviceId\":$(json_escape "$IOS_DEVICE")"
207
+ fi
208
+ if [[ "$name" == "iOS simulator hardware pilot" && -n "$IOS_APP_ID" ]]; then
209
+ metadata_json+=",\"iosAppId\":$(json_escape "$IOS_APP_ID")"
210
+ fi
211
+ if [[ "$name" == "iOS simulator hardware pilot" && -n "$IOS_APP_ROOT" ]]; then
212
+ metadata_json+=",\"iosAppRoot\":$(json_escape "$IOS_APP_ROOT")"
213
+ fi
214
+ if [[ "$name" == "iOS simulator hardware pilot" && -n "$IOS_APP_PATH" ]]; then
215
+ metadata_json+=",\"iosAppPath\":$(json_escape "$IOS_APP_PATH")"
216
+ fi
217
+ if [[ "$name" == "iOS simulator hardware pilot" && -n "$IOS_DEVICE" ]]; then
218
+ metadata_json+=",\"iosDeviceId\":$(json_escape "$IOS_DEVICE")"
219
+ fi
220
+ if [[ "$name" == "iOS physical hardware pilot" ]]; then
221
+ if [[ -n "$IOS_APP_ID" ]]; then
222
+ metadata_json+=",\"iosAppId\":$(json_escape "$IOS_APP_ID")"
223
+ fi
224
+ if [[ -n "$IOS_APP_ROOT" ]]; then
225
+ metadata_json+=",\"iosAppRoot\":$(json_escape "$IOS_APP_ROOT")"
226
+ fi
227
+ if [[ -n "$IOS_APP_PATH" ]]; then
228
+ metadata_json+=",\"iosAppPath\":$(json_escape "$IOS_APP_PATH")"
229
+ fi
230
+ if [[ -n "$IOS_DEVICE" ]]; then
231
+ metadata_json+=",\"iosDeviceId\":$(json_escape "$IOS_DEVICE")"
232
+ fi
233
+ fi
234
+
235
+ printf '{"name":%s,"mode":"pilot-gate","status":%s,"durationMs":%s,"command":%s,"traceRoot":%s,"runs":%s,"minPassRate":%s,"maxFailures":%s%s}\n' \
236
+ "$(json_escape "$name")" \
237
+ "$(json_escape "$status")" \
238
+ "$duration_ms" \
239
+ "$(json_escape "$command")" \
240
+ "$(json_escape "$trace_root")" \
241
+ "$RUNS" \
242
+ "$MIN_PASS_RATE" \
243
+ "$MAX_FAILURES" \
244
+ "$metadata_json" >> "$EVIDENCE_OUT"
245
+ }
246
+
247
+ run_step() {
248
+ local name="$1"
249
+ local trace_root="$2"
250
+ shift 2
251
+
252
+ local start end duration status command
253
+ command="$(quote_cmd "$@")"
254
+ echo "+ $command"
255
+ start="$(date +%s)"
256
+ if [[ "$DRY_RUN" -eq 1 ]]; then
257
+ status="planned"
258
+ else
259
+ set +e
260
+ "$@"
261
+ local command_status=$?
262
+ set -e
263
+ if [[ "$command_status" -eq 0 ]]; then
264
+ status="passed"
265
+ else
266
+ status="failed"
267
+ fi
268
+ fi
269
+ end="$(date +%s)"
270
+ duration="$(( (end - start) * 1000 ))"
271
+ write_evidence "$name" "$status" "$command" "$duration" "$trace_root"
272
+ [[ "$status" != "failed" ]] || exit 1
273
+ }
274
+
275
+ validate_number() {
276
+ local name="$1"
277
+ local value="$2"
278
+ if [[ ! "$value" =~ ^[0-9]+([.][0-9]+)?$ ]]; then
279
+ die "$name must be a non-negative number"
280
+ fi
281
+ }
282
+
283
+ validate_integer() {
284
+ local name="$1"
285
+ local value="$2"
286
+ if [[ ! "$value" =~ ^[0-9]+$ ]]; then
287
+ die "$name must be a non-negative integer"
288
+ fi
289
+ }
290
+
291
+ validate_optional_integer() {
292
+ local name="$1"
293
+ local value="$2"
294
+ if [[ -n "$value" && ! "$value" =~ ^[0-9]+$ ]]; then
295
+ die "$name must be a non-negative integer"
296
+ fi
297
+ }
298
+
299
+ require_value() {
300
+ local flag="$1"
301
+ local value="${2-}"
302
+ if [[ -z "$value" || "$value" == --* ]]; then
303
+ die "$flag requires a value"
304
+ fi
305
+ printf '%s\n' "$value"
306
+ }
307
+
308
+ while [[ $# -gt 0 ]]; do
309
+ case "$1" in
310
+ --android)
311
+ ANDROID_SELECTED=1
312
+ shift
313
+ ;;
314
+ --ios)
315
+ IOS_SELECTED=1
316
+ shift
317
+ ;;
318
+ --android-app-root)
319
+ ANDROID_APP_ROOT="$(require_value "$1" "${2-}")"
320
+ shift 2
321
+ ;;
322
+ --android-app-id)
323
+ ANDROID_APP_ID="$(require_value "$1" "${2-}")"
324
+ shift 2
325
+ ;;
326
+ --android-device)
327
+ ANDROID_DEVICE="$(require_value "$1" "${2-}")"
328
+ shift 2
329
+ ;;
330
+ --android-apk)
331
+ ANDROID_APK="$(require_value "$1" "${2-}")"
332
+ shift 2
333
+ ;;
334
+ --android-scenario)
335
+ ANDROID_SCENARIO="$(require_value "$1" "${2-}")"
336
+ shift 2
337
+ ;;
338
+ --adb)
339
+ ADB="$(require_value "$1" "${2-}")"
340
+ shift 2
341
+ ;;
342
+ --skip-emulator)
343
+ ANDROID_SKIP_EMULATOR=1
344
+ shift
345
+ ;;
346
+ --skip-metro)
347
+ ANDROID_SKIP_METRO=1
348
+ shift
349
+ ;;
350
+ --ios-app-root)
351
+ IOS_APP_ROOT="$(require_value "$1" "${2-}")"
352
+ shift 2
353
+ ;;
354
+ --ios-app-id)
355
+ IOS_APP_ID="$(require_value "$1" "${2-}")"
356
+ shift 2
357
+ ;;
358
+ --ios-device)
359
+ IOS_DEVICE="$(require_value "$1" "${2-}")"
360
+ shift 2
361
+ ;;
362
+ --ios-device-type)
363
+ IOS_DEVICE_TYPE="$(require_value "$1" "${2-}")"
364
+ shift 2
365
+ ;;
366
+ --ios-app-path)
367
+ IOS_APP_PATH="$(require_value "$1" "${2-}")"
368
+ shift 2
369
+ ;;
370
+ --ios-shim)
371
+ IOS_SHIM="$(require_value "$1" "${2-}")"
372
+ shift 2
373
+ ;;
374
+ --xcrun)
375
+ XCRUN="$(require_value "$1" "${2-}")"
376
+ shift 2
377
+ ;;
378
+ --zmr-bin)
379
+ ZMR_BIN="$(require_value "$1" "${2-}")"
380
+ shift 2
381
+ ;;
382
+ --trace-root)
383
+ TRACE_ROOT="$(require_value "$1" "${2-}")"
384
+ shift 2
385
+ ;;
386
+ --evidence-out)
387
+ EVIDENCE_OUT="$(require_value "$1" "${2-}")"
388
+ shift 2
389
+ ;;
390
+ --runs)
391
+ RUNS="$(require_value "$1" "${2-}")"
392
+ shift 2
393
+ ;;
394
+ --min-pass-rate)
395
+ MIN_PASS_RATE="$(require_value "$1" "${2-}")"
396
+ shift 2
397
+ ;;
398
+ --max-failures)
399
+ MAX_FAILURES="$(require_value "$1" "${2-}")"
400
+ shift 2
401
+ ;;
402
+ --max-mean-ms)
403
+ MAX_MEAN_MS="$(require_value "$1" "${2-}")"
404
+ shift 2
405
+ ;;
406
+ --android-max-p95-ms)
407
+ ANDROID_MAX_P95_MS="$(require_value "$1" "${2-}")"
408
+ shift 2
409
+ ;;
410
+ --ios-max-p95-ms)
411
+ IOS_MAX_P95_MS="$(require_value "$1" "${2-}")"
412
+ shift 2
413
+ ;;
414
+ --dry-run)
415
+ DRY_RUN=1
416
+ shift
417
+ ;;
418
+ -h|--help)
419
+ usage
420
+ exit 0
421
+ ;;
422
+ *)
423
+ die "unknown argument: $1"
424
+ ;;
425
+ esac
426
+ done
427
+
428
+ if [[ "$ANDROID_SELECTED" -eq 0 && "$IOS_SELECTED" -eq 0 ]]; then
429
+ ANDROID_SELECTED=1
430
+ IOS_SELECTED=1
431
+ fi
432
+
433
+ validate_integer "--runs" "$RUNS"
434
+ [[ "$RUNS" -ge 1 ]] || die "--runs must be a positive integer"
435
+ validate_number "--min-pass-rate" "$MIN_PASS_RATE"
436
+ validate_integer "--max-failures" "$MAX_FAILURES"
437
+ validate_optional_integer "--max-mean-ms" "$MAX_MEAN_MS"
438
+ validate_integer "--android-max-p95-ms" "$ANDROID_MAX_P95_MS"
439
+ validate_integer "--ios-max-p95-ms" "$IOS_MAX_P95_MS"
440
+ [[ "$IOS_DEVICE_TYPE" == "simulator" || "$IOS_DEVICE_TYPE" == "physical" ]] || die "--ios-device-type must be simulator or physical"
441
+
442
+ if [[ "$ANDROID_SELECTED" -eq 1 && -z "$ANDROID_APP_ROOT" ]]; then
443
+ die "--android-app-root is required when --android is selected"
444
+ fi
445
+
446
+ if [[ "$IOS_SELECTED" -eq 1 && -z "$IOS_APP_PATH" ]]; then
447
+ die "--ios-app-path is required when --ios is selected"
448
+ fi
449
+ if [[ "$IOS_SELECTED" -eq 1 && -z "$IOS_APP_ROOT" ]]; then
450
+ die "--ios-app-root is required when --ios is selected"
451
+ fi
452
+
453
+ TRACE_ROOT="$(resolve_path_from_cwd "$TRACE_ROOT")"
454
+ if [[ -n "$EVIDENCE_OUT" ]]; then
455
+ EVIDENCE_OUT="$(resolve_path_from_cwd "$EVIDENCE_OUT")"
456
+ fi
457
+ if [[ -n "$ANDROID_APP_ROOT" ]]; then
458
+ ANDROID_APP_ROOT="$(resolve_path_from_cwd "$ANDROID_APP_ROOT")"
459
+ fi
460
+ if [[ -n "$ANDROID_APK" ]]; then
461
+ ANDROID_APK="$(resolve_path_from_cwd "$ANDROID_APK")"
462
+ fi
463
+ if [[ -n "$ANDROID_SCENARIO" ]]; then
464
+ ANDROID_SCENARIO="$(resolve_path_from_cwd "$ANDROID_SCENARIO")"
465
+ fi
466
+ if [[ -n "$ADB" ]]; then
467
+ ADB="$(resolve_command_path_from_cwd "$ADB")"
468
+ fi
469
+ if [[ -n "$IOS_APP_ROOT" ]]; then
470
+ IOS_APP_ROOT="$(resolve_path_from_cwd "$IOS_APP_ROOT")"
471
+ fi
472
+ if [[ -n "$IOS_APP_PATH" ]]; then
473
+ IOS_APP_PATH="$(resolve_path_from_cwd "$IOS_APP_PATH")"
474
+ fi
475
+ if [[ -n "$IOS_SHIM" ]]; then
476
+ IOS_SHIM="$(resolve_command_path_from_cwd "$IOS_SHIM")"
477
+ fi
478
+ if [[ -n "$XCRUN" ]]; then
479
+ XCRUN="$(resolve_command_path_from_cwd "$XCRUN")"
480
+ fi
481
+ if [[ -n "$ZMR_BIN" ]]; then
482
+ ZMR_BIN="$(resolve_command_path_from_cwd "$ZMR_BIN")"
483
+ fi
484
+
485
+ echo "Pilot gate output: $TRACE_ROOT"
486
+ if [[ -n "$EVIDENCE_OUT" ]]; then
487
+ echo "pilot evidence: $EVIDENCE_OUT"
488
+ mkdir -p "$(dirname "$EVIDENCE_OUT")"
489
+ : > "$EVIDENCE_OUT"
490
+ fi
491
+ if [[ "$DRY_RUN" -eq 1 ]]; then
492
+ echo "DRY RUN: commands will be printed but not executed"
493
+ fi
494
+
495
+ common_gate_args=(--runs "$RUNS" --min-pass-rate "$MIN_PASS_RATE" --max-failures "$MAX_FAILURES")
496
+ if [[ -n "$MAX_MEAN_MS" ]]; then
497
+ common_gate_args+=(--max-mean-ms "$MAX_MEAN_MS")
498
+ fi
499
+
500
+ if [[ "$ANDROID_SELECTED" -eq 1 ]]; then
501
+ android_cmd=("$ROOT/scripts/run-android-pilot.sh" --app-root "$ANDROID_APP_ROOT" --device "$ANDROID_DEVICE" --trace-root "$TRACE_ROOT/android" --max-p95-ms "$ANDROID_MAX_P95_MS")
502
+ if [[ -n "$ANDROID_APP_ID" ]]; then
503
+ android_cmd+=(--app-id "$ANDROID_APP_ID")
504
+ fi
505
+ if [[ -n "$ANDROID_APK" ]]; then
506
+ android_cmd+=(--apk "$ANDROID_APK")
507
+ fi
508
+ if [[ -n "$ANDROID_SCENARIO" ]]; then
509
+ android_cmd+=(--scenario "$ANDROID_SCENARIO")
510
+ fi
511
+ if [[ -n "$ADB" ]]; then
512
+ android_cmd+=(--adb "$ADB")
513
+ fi
514
+ if [[ -n "$ZMR_BIN" ]]; then
515
+ android_cmd+=(--zmr-bin "$ZMR_BIN")
516
+ fi
517
+ if [[ "$ANDROID_SKIP_EMULATOR" -eq 1 ]]; then
518
+ android_cmd+=(--skip-emulator)
519
+ fi
520
+ if [[ "$ANDROID_SKIP_METRO" -eq 1 ]]; then
521
+ android_cmd+=(--skip-metro)
522
+ fi
523
+ android_cmd+=("${common_gate_args[@]}")
524
+ run_step "Android hardware pilot" "$TRACE_ROOT/android" "${android_cmd[@]}"
525
+ fi
526
+
527
+ if [[ "$IOS_SELECTED" -eq 1 ]]; then
528
+ if [[ "$IOS_DEVICE_TYPE" == "physical" ]]; then
529
+ readiness_cmd=("$ROOT/scripts/assert-ios-physical-ready.sh" --device "$IOS_DEVICE")
530
+ if [[ -n "$ZMR_BIN" ]]; then
531
+ readiness_cmd+=(--zmr "$ZMR_BIN")
532
+ fi
533
+ if [[ -n "$XCRUN" ]]; then
534
+ readiness_cmd+=(--xcrun "$XCRUN")
535
+ fi
536
+ run_step "physical iOS readiness" "$TRACE_ROOT/ios" "${readiness_cmd[@]}"
537
+ fi
538
+ ios_cmd=("$ROOT/scripts/run-ios-pilot.sh" --app-path "$IOS_APP_PATH" --device "$IOS_DEVICE" --ios-device-type "$IOS_DEVICE_TYPE" --trace-root "$TRACE_ROOT/ios" --max-p95-ms "$IOS_MAX_P95_MS")
539
+ if [[ -n "$IOS_APP_ROOT" ]]; then
540
+ ios_cmd+=(--app-root "$IOS_APP_ROOT")
541
+ fi
542
+ if [[ -n "$IOS_APP_ID" ]]; then
543
+ ios_cmd+=(--app-id "$IOS_APP_ID")
544
+ fi
545
+ if [[ -n "$IOS_SHIM" ]]; then
546
+ ios_cmd+=(--ios-shim "$IOS_SHIM")
547
+ fi
548
+ if [[ -n "$XCRUN" ]]; then
549
+ ios_cmd+=(--xcrun "$XCRUN")
550
+ fi
551
+ if [[ -n "$ZMR_BIN" ]]; then
552
+ ios_cmd+=(--zmr-bin "$ZMR_BIN")
553
+ fi
554
+ ios_cmd+=("${common_gate_args[@]}")
555
+ if [[ "$IOS_DEVICE_TYPE" == "physical" ]]; then
556
+ run_step "iOS physical hardware pilot" "$TRACE_ROOT/ios" "${ios_cmd[@]}"
557
+ else
558
+ run_step "iOS simulator hardware pilot" "$TRACE_ROOT/ios" "${ios_cmd[@]}"
559
+ fi
560
+ fi