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,91 @@
1
+ #!/bin/sh
2
+ if [ -z "${ZMR_BASH_BOOTSTRAP:-}" ]; then
3
+ ZMR_BASH_BOOTSTRAP=1
4
+ export ZMR_BASH_BOOTSTRAP
5
+ SCRIPT_DIR="$(cd -P "$(dirname "$0")" && pwd -P)"
6
+ ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
7
+ if [ -z "${TMPDIR:-}" ] || [ ! -w "${TMPDIR:-/nonexistent}" ]; then
8
+ TMPDIR="$ROOT/traces/tmp"
9
+ fi
10
+ mkdir -p "$TMPDIR"
11
+ export TMPDIR
12
+ exec /usr/bin/env bash "$0" "$@"
13
+ fi
14
+
15
+ set -euo pipefail
16
+
17
+ CALLER_CWD="$(pwd -P)"
18
+ # Some sandboxed environments do not allow writing to the default temp directory
19
+ # (/var/folders, /tmp). Use a caller-local TMPDIR so heredocs/mktemp work.
20
+ if [[ -z "${TMPDIR:-}" || ! -w "${TMPDIR:-/nonexistent}" ]]; then
21
+ TMPDIR="$CALLER_CWD/traces/tmp"
22
+ mkdir -p "$TMPDIR"
23
+ export TMPDIR
24
+ fi
25
+
26
+ EVIDENCE_FILES=()
27
+ TARGET="dev-preview"
28
+ JSON=0
29
+
30
+ usage() {
31
+ printf '%s\n' 'Usage:'
32
+ printf '%s\n' ' scripts/release-readiness.sh --evidence <evidence.jsonl> [--evidence <more.jsonl> ...] [--target dev-preview|production|market-claim] [--json]'
33
+ printf '%s\n' ''
34
+ printf '%s\n' 'Reads one or more release/pilot evidence JSONL files and reports whether the'
35
+ printf '%s\n' 'requested release claim is supported by concrete passed evidence.'
36
+ printf '%s\n' ''
37
+ printf '%s\n' 'Targets:'
38
+ printf '%s\n' ' dev-preview Requires local release gate plus public Android and iOS demos.'
39
+ printf '%s\n' ' production Requires dev-preview evidence plus repeated real app/device pilots.'
40
+ printf '%s\n' ' market-claim Requires production evidence plus same-device/app/build benchmark comparison.'
41
+ }
42
+
43
+ die() {
44
+ echo "error: $*" >&2
45
+ exit 2
46
+ }
47
+
48
+ require_value() {
49
+ local flag="$1"
50
+ local value="${2-}"
51
+ if [[ -z "$value" || "$value" == --* ]]; then
52
+ die "$flag requires a value"
53
+ fi
54
+ printf '%s\n' "$value"
55
+ }
56
+
57
+ while [[ $# -gt 0 ]]; do
58
+ case "$1" in
59
+ --evidence)
60
+ EVIDENCE_FILES+=("$(require_value "$1" "${2-}")")
61
+ shift 2
62
+ ;;
63
+ --target)
64
+ TARGET="$(require_value "$1" "${2-}")"
65
+ shift 2
66
+ ;;
67
+ --json)
68
+ JSON=1
69
+ shift
70
+ ;;
71
+ -h|--help)
72
+ usage
73
+ exit 0
74
+ ;;
75
+ *)
76
+ die "unknown argument: $1"
77
+ ;;
78
+ esac
79
+ done
80
+
81
+ [[ "${#EVIDENCE_FILES[@]}" -gt 0 ]] || die "--evidence is required"
82
+ for evidence_file in "${EVIDENCE_FILES[@]}"; do
83
+ [[ -n "$evidence_file" ]] || die "--evidence requires a path"
84
+ if [[ ! -f "$evidence_file" && "$JSON" -eq 0 ]]; then
85
+ die "evidence file not found: $evidence_file"
86
+ fi
87
+ done
88
+ [[ "$TARGET" == "dev-preview" || "$TARGET" == "production" || "$TARGET" == "market-claim" ]] || die "--target must be dev-preview, production, or market-claim"
89
+
90
+ SCRIPT_DIR="$(cd -P "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
91
+ python3 "$SCRIPT_DIR/release-readiness.py" "$TARGET" "$JSON" "${EVIDENCE_FILES[@]}"
@@ -0,0 +1,561 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
+ CALLER_CWD="$(pwd -P)"
6
+ cd "$ROOT"
7
+
8
+ # Some sandboxed environments do not allow writing to the default temp directory
9
+ # (/var/folders, /tmp). Use a repo-local TMPDIR so adb/mktemp/heredocs work.
10
+ if [[ -z "${TMPDIR:-}" || ! -w "${TMPDIR:-/nonexistent}" ]]; then
11
+ TMPDIR="$ROOT/traces/tmp"
12
+ mkdir -p "$TMPDIR"
13
+ export TMPDIR
14
+ fi
15
+
16
+ APP_ROOT="${APP_ROOT:-}"
17
+ DEVICE="${DEVICE:-emulator-5554}"
18
+ AVD="${AVD:-}"
19
+ TRACE_ROOT="${TRACE_ROOT:-$CALLER_CWD/traces/android-app-pilot-$(date +%Y%m%d-%H%M%S)}"
20
+ ZMR_BIN="${ZMR_BIN:-$(command -v zmr 2>/dev/null || printf '%s' "$ROOT/zig-out/bin/zmr")}"
21
+ ADB="${ADB:-adb}"
22
+ APK="${APK:-}"
23
+ SCENARIO="${SCENARIO:-}"
24
+ APP_ID="${APP_ID:-com.example.mobiletest}"
25
+ RUNS="${RUNS:-1}"
26
+ MIN_PASS_RATE="${MIN_PASS_RATE:-100}"
27
+ MAX_FAILURES="${MAX_FAILURES:-0}"
28
+ MAX_MEAN_MS="${MAX_MEAN_MS:-}"
29
+ MAX_P95_MS="${MAX_P95_MS:-}"
30
+ RESTORE_SNAPSHOT="${RESTORE_SNAPSHOT:-}"
31
+ DRY_RUN=0
32
+ SKIP_EMULATOR=0
33
+ SKIP_METRO=0
34
+ KEEP_RUNNING=0
35
+ RESET_EMULATOR=0
36
+ SCREEN_RECORD=0
37
+ STARTED_EMULATOR=0
38
+ METRO_PID=""
39
+ SCREEN_RECORD_PID=""
40
+ SCREEN_RECORD_REMOTE="/sdcard/zmr-pilot-screenrecord.mp4"
41
+ SCREEN_RECORD_LOCAL=""
42
+
43
+ usage() {
44
+ cat <<'USAGE'
45
+ Usage:
46
+ scripts/run-android-pilot.sh [options]
47
+
48
+ Runs a configurable Android sample-app pilot end to end:
49
+ 1. build/validate zmr
50
+ 2. boot or use an emulator
51
+ 3. install the sample app test APK
52
+ 4. optionally start the sample app's Metro environment
53
+ 5. run auth and login smoke scenarios
54
+ 6. generate reports and normal/redacted .zmrtrace bundles
55
+
56
+ Options:
57
+ --app-root <dir> Sample app repo containing .env.test and the debug APK.
58
+ --app-id <bundle> Application id. Default: com.example.mobiletest.
59
+ --apk <path> APK to install. Defaults to android/app/build/outputs/apk/debug/app-debug.apk.
60
+ --scenario <path> Optional ZMR scenario JSON to run. Defaults to the built-in Sample App flows.
61
+ --device <serial> Android serial. Default: emulator-5554.
62
+ --avd <name> AVD to boot when no device is attached. Defaults to first local AVD.
63
+ --trace-root <dir> Output directory. Default: traces/android-app-pilot-<timestamp>.
64
+ --zmr-bin <path> zmr binary. Default: ZMR_BIN, PATH zmr, then zig-out/bin/zmr.
65
+ --adb <path> adb path. Default: adb.
66
+ --runs <n> Run each flow n times. n=1 writes trace bundles; n>1 writes benchmark reports.
67
+ --min-pass-rate <pct> Repeated-run gate minimum. Default: 100.
68
+ --max-failures <n> Repeated-run gate maximum. Default: 0.
69
+ --max-mean-ms <ms> Optional repeated-run mean duration maximum.
70
+ --max-p95-ms <ms> Optional repeated-run p95 duration maximum.
71
+ --reset-emulator Kill the target emulator before booting/restoring it.
72
+ --restore-snapshot <name>
73
+ Boot the AVD from a named emulator snapshot.
74
+ --screen-record Capture a pilot-level MP4 with adb shell screenrecord.
75
+ --skip-emulator Require/use an already booted device.
76
+ --skip-metro Do not start Metro; assume it is already running.
77
+ --keep-running Leave Metro/emulator running when this script exits.
78
+ --dry-run Print the commands without executing them.
79
+ -h, --help Show this help.
80
+
81
+ Environment:
82
+ APP_ROOT, APP_ID, APK, DEVICE, AVD, TRACE_ROOT, ZMR_BIN, ADB, RUNS,
83
+ RESTORE_SNAPSHOT, MIN_PASS_RATE, MAX_FAILURES, MAX_MEAN_MS, MAX_P95_MS.
84
+
85
+ Notes:
86
+ Metro output is written to <trace-root>/metro.log and may contain app secrets.
87
+ Share the generated *-redacted.zmrtrace bundles, not raw Metro logs.
88
+ USAGE
89
+ }
90
+
91
+ die() {
92
+ echo "error: $*" >&2
93
+ exit 2
94
+ }
95
+
96
+ require_value() {
97
+ local flag="$1"
98
+ local value="${2-}"
99
+ if [[ -z "$value" || "$value" == --* ]]; then
100
+ die "$flag requires a value"
101
+ fi
102
+ printf '%s\n' "$value"
103
+ }
104
+
105
+ resolve_path_from_cwd() {
106
+ local value="$1"
107
+ local absolute dir base probe suffix
108
+ if [[ -z "$value" ]]; then
109
+ printf '\n'
110
+ return 0
111
+ fi
112
+ if [[ "$value" == /* ]]; then
113
+ absolute="$value"
114
+ else
115
+ absolute="$CALLER_CWD/$value"
116
+ fi
117
+ if [[ -d "$absolute" ]]; then
118
+ cd "$absolute" && pwd -P
119
+ return 0
120
+ fi
121
+ if [[ -e "$absolute" ]]; then
122
+ dir="$(dirname "$absolute")"
123
+ base="$(basename "$absolute")"
124
+ printf '%s/%s\n' "$(cd "$dir" && pwd -P)" "$base"
125
+ return 0
126
+ fi
127
+ dir="$(dirname "$absolute")"
128
+ base="$(basename "$absolute")"
129
+ if [[ -d "$dir" ]]; then
130
+ printf '%s/%s\n' "$(cd "$dir" && pwd -P)" "$base"
131
+ return 0
132
+ fi
133
+ probe="$absolute"
134
+ suffix=""
135
+ while [[ "$probe" != "/" && ! -e "$probe" ]]; do
136
+ suffix="/$(basename "$probe")$suffix"
137
+ probe="$(dirname "$probe")"
138
+ done
139
+ if [[ -d "$probe" ]]; then
140
+ printf '%s%s\n' "$(cd "$probe" && pwd -P)" "$suffix"
141
+ return 0
142
+ fi
143
+ while [[ "$absolute" == *"/./"* ]]; do
144
+ absolute="${absolute//\/.\//\/}"
145
+ done
146
+ printf '%s\n' "$absolute"
147
+ }
148
+
149
+ resolve_command_path_from_cwd() {
150
+ local value="$1"
151
+ if [[ -z "$value" || "$value" != */* ]]; then
152
+ printf '%s\n' "$value"
153
+ else
154
+ resolve_path_from_cwd "$value"
155
+ fi
156
+ }
157
+
158
+ quote_cmd() {
159
+ local quoted=()
160
+ local arg
161
+ for arg in "$@"; do
162
+ quoted+=("$(printf '%q' "$arg")")
163
+ done
164
+ printf '%s\n' "${quoted[*]}"
165
+ }
166
+
167
+ run() {
168
+ echo "+ $(quote_cmd "$@")"
169
+ if [[ "$DRY_RUN" -eq 0 ]]; then
170
+ "$@"
171
+ fi
172
+ }
173
+
174
+ capture() {
175
+ if [[ "$DRY_RUN" -eq 1 ]]; then
176
+ echo ""
177
+ else
178
+ "$@"
179
+ fi
180
+ }
181
+
182
+ wait_for_device() {
183
+ echo "+ wait for adb device $DEVICE"
184
+ if [[ "$DRY_RUN" -eq 1 ]]; then
185
+ return
186
+ fi
187
+ for _ in $(seq 1 120); do
188
+ if "$ADB" devices | awk -v serial="$DEVICE" '$1 == serial && $2 == "device" { found = 1 } END { exit found ? 0 : 1 }'; then
189
+ return
190
+ fi
191
+ sleep 2
192
+ done
193
+ die "Android device did not appear: $DEVICE"
194
+ }
195
+
196
+ wait_for_boot() {
197
+ echo "+ wait for Android boot completion on $DEVICE"
198
+ if [[ "$DRY_RUN" -eq 1 ]]; then
199
+ return
200
+ fi
201
+ for _ in $(seq 1 120); do
202
+ booted="$("$ADB" -s "$DEVICE" shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')"
203
+ if [[ "$booted" == "1" ]]; then
204
+ return
205
+ fi
206
+ sleep 2
207
+ done
208
+ die "Android device did not finish booting: $DEVICE"
209
+ }
210
+
211
+ cleanup() {
212
+ if [[ "$DRY_RUN" -eq 1 ]]; then
213
+ return
214
+ fi
215
+ if [[ -n "$SCREEN_RECORD_PID" ]]; then
216
+ kill "$SCREEN_RECORD_PID" >/dev/null 2>&1 || true
217
+ wait "$SCREEN_RECORD_PID" >/dev/null 2>&1 || true
218
+ "$ADB" -s "$DEVICE" pull "$SCREEN_RECORD_REMOTE" "$SCREEN_RECORD_LOCAL" >/dev/null 2>&1 || true
219
+ "$ADB" -s "$DEVICE" shell rm -f "$SCREEN_RECORD_REMOTE" >/dev/null 2>&1 || true
220
+ fi
221
+ if [[ -n "$METRO_PID" ]]; then
222
+ kill "$METRO_PID" >/dev/null 2>&1 || true
223
+ wait "$METRO_PID" >/dev/null 2>&1 || true
224
+ fi
225
+ if [[ "$KEEP_RUNNING" -eq 1 ]]; then
226
+ return
227
+ fi
228
+ if [[ "$STARTED_EMULATOR" -eq 1 ]]; then
229
+ "$ADB" -s "$DEVICE" emu kill >/dev/null 2>&1 || true
230
+ fi
231
+ }
232
+ trap cleanup EXIT
233
+
234
+ start_screen_recording() {
235
+ if [[ "$SCREEN_RECORD" -ne 1 ]]; then
236
+ return 0
237
+ fi
238
+ SCREEN_RECORD_LOCAL="$TRACE_ROOT/screenrecord.mp4"
239
+ run "$ADB" -s "$DEVICE" shell rm -f "$SCREEN_RECORD_REMOTE"
240
+ echo "+ $(quote_cmd "$ADB" -s "$DEVICE" shell screenrecord "$SCREEN_RECORD_REMOTE")"
241
+ if [[ "$DRY_RUN" -eq 0 ]]; then
242
+ "$ADB" -s "$DEVICE" shell screenrecord "$SCREEN_RECORD_REMOTE" > "$TRACE_ROOT/screenrecord.log" 2>&1 &
243
+ SCREEN_RECORD_PID="$!"
244
+ fi
245
+ }
246
+
247
+ stop_screen_recording() {
248
+ if [[ "$SCREEN_RECORD" -ne 1 ]]; then
249
+ return 0
250
+ fi
251
+ if [[ "$DRY_RUN" -eq 1 ]]; then
252
+ echo "+ stop adb screenrecord"
253
+ run "$ADB" -s "$DEVICE" pull "$SCREEN_RECORD_REMOTE" "$TRACE_ROOT/screenrecord.mp4"
254
+ run "$ADB" -s "$DEVICE" shell rm -f "$SCREEN_RECORD_REMOTE"
255
+ return
256
+ fi
257
+ if [[ -n "$SCREEN_RECORD_PID" ]]; then
258
+ kill "$SCREEN_RECORD_PID" >/dev/null 2>&1 || true
259
+ wait "$SCREEN_RECORD_PID" >/dev/null 2>&1 || true
260
+ SCREEN_RECORD_PID=""
261
+ fi
262
+ run "$ADB" -s "$DEVICE" pull "$SCREEN_RECORD_REMOTE" "$SCREEN_RECORD_LOCAL"
263
+ run "$ADB" -s "$DEVICE" shell rm -f "$SCREEN_RECORD_REMOTE"
264
+ }
265
+
266
+ preflight_android_device() {
267
+ if [[ "$DRY_RUN" -eq 1 ]]; then
268
+ return 0
269
+ fi
270
+ if "$ADB" devices | awk -v serial="$DEVICE" '$1 == serial && $2 == "device" { found = 1 } END { exit found ? 0 : 1 }'; then
271
+ return 0
272
+ fi
273
+ echo "error: no Android device found: $DEVICE" >&2
274
+ echo "errorCode: setup.android.no_devices" >&2
275
+ echo "hint: run zmr doctor --json --adb $(printf '%q' "$ADB") and start/connect the requested device." >&2
276
+ "$ZMR_BIN" doctor --json --adb "$ADB" >&2 || true
277
+ exit 2
278
+ }
279
+
280
+ while [[ $# -gt 0 ]]; do
281
+ case "$1" in
282
+ --app-root)
283
+ APP_ROOT="$(require_value "$1" "${2-}")"
284
+ shift 2
285
+ ;;
286
+ --app-id)
287
+ APP_ID="$(require_value "$1" "${2-}")"
288
+ shift 2
289
+ ;;
290
+ --apk)
291
+ APK="$(require_value "$1" "${2-}")"
292
+ shift 2
293
+ ;;
294
+ --scenario)
295
+ SCENARIO="$(require_value "$1" "${2-}")"
296
+ shift 2
297
+ ;;
298
+ --device)
299
+ DEVICE="$(require_value "$1" "${2-}")"
300
+ shift 2
301
+ ;;
302
+ --avd)
303
+ AVD="$(require_value "$1" "${2-}")"
304
+ shift 2
305
+ ;;
306
+ --trace-root)
307
+ TRACE_ROOT="$(require_value "$1" "${2-}")"
308
+ shift 2
309
+ ;;
310
+ --zmr-bin)
311
+ ZMR_BIN="$(require_value "$1" "${2-}")"
312
+ shift 2
313
+ ;;
314
+ --adb)
315
+ ADB="$(require_value "$1" "${2-}")"
316
+ shift 2
317
+ ;;
318
+ --runs)
319
+ RUNS="$(require_value "$1" "${2-}")"
320
+ shift 2
321
+ ;;
322
+ --min-pass-rate)
323
+ MIN_PASS_RATE="$(require_value "$1" "${2-}")"
324
+ shift 2
325
+ ;;
326
+ --max-failures)
327
+ MAX_FAILURES="$(require_value "$1" "${2-}")"
328
+ shift 2
329
+ ;;
330
+ --max-mean-ms)
331
+ MAX_MEAN_MS="$(require_value "$1" "${2-}")"
332
+ shift 2
333
+ ;;
334
+ --max-p95-ms)
335
+ MAX_P95_MS="$(require_value "$1" "${2-}")"
336
+ shift 2
337
+ ;;
338
+ --reset-emulator)
339
+ RESET_EMULATOR=1
340
+ shift
341
+ ;;
342
+ --restore-snapshot)
343
+ RESTORE_SNAPSHOT="$(require_value "$1" "${2-}")"
344
+ shift 2
345
+ ;;
346
+ --screen-record)
347
+ SCREEN_RECORD=1
348
+ shift
349
+ ;;
350
+ --skip-emulator)
351
+ SKIP_EMULATOR=1
352
+ shift
353
+ ;;
354
+ --skip-metro)
355
+ SKIP_METRO=1
356
+ shift
357
+ ;;
358
+ --keep-running)
359
+ KEEP_RUNNING=1
360
+ shift
361
+ ;;
362
+ --dry-run)
363
+ DRY_RUN=1
364
+ shift
365
+ ;;
366
+ -h|--help)
367
+ usage
368
+ exit 0
369
+ ;;
370
+ *)
371
+ die "unknown argument: $1"
372
+ ;;
373
+ esac
374
+ done
375
+
376
+ [[ -n "$DEVICE" ]] || die "--device cannot be empty"
377
+ [[ "$RUNS" =~ ^[0-9]+$ && "$RUNS" -ge 1 ]] || die "--runs must be a positive integer"
378
+ [[ "$MIN_PASS_RATE" =~ ^[0-9]+([.][0-9]+)?$ ]] || die "--min-pass-rate must be a non-negative number"
379
+ [[ "$MAX_FAILURES" =~ ^[0-9]+$ ]] || die "--max-failures must be a non-negative integer"
380
+ [[ -z "$MAX_MEAN_MS" || "$MAX_MEAN_MS" =~ ^[0-9]+$ ]] || die "--max-mean-ms must be a non-negative integer"
381
+ [[ -z "$MAX_P95_MS" || "$MAX_P95_MS" =~ ^[0-9]+$ ]] || die "--max-p95-ms must be a non-negative integer"
382
+
383
+ APP_ROOT="$(resolve_path_from_cwd "$APP_ROOT")"
384
+ TRACE_ROOT="$(resolve_path_from_cwd "$TRACE_ROOT")"
385
+ ZMR_BIN="$(resolve_command_path_from_cwd "$ZMR_BIN")"
386
+ ADB="$(resolve_command_path_from_cwd "$ADB")"
387
+ if [[ -n "$APK" ]]; then
388
+ APK="$(resolve_path_from_cwd "$APK")"
389
+ fi
390
+ if [[ -n "$SCENARIO" ]]; then
391
+ SCENARIO="$(resolve_path_from_cwd "$SCENARIO")"
392
+ fi
393
+
394
+ [[ -n "$APP_ROOT" ]] || die "--app-root is required"
395
+ [[ -d "$APP_ROOT" ]] || die "app repo not found: $APP_ROOT"
396
+ if [[ "$SKIP_METRO" -eq 0 ]]; then
397
+ [[ -f "$APP_ROOT/.env.test" ]] || die "app test env file not found: $APP_ROOT/.env.test"
398
+ fi
399
+
400
+ if [[ -z "$APK" ]]; then
401
+ APK="$APP_ROOT/android/app/build/outputs/apk/debug/app-debug.apk"
402
+ fi
403
+ [[ -f "$APK" ]] || die "APK not found: $APK"
404
+
405
+ echo "Android pilot output: $TRACE_ROOT"
406
+ echo "App test env: $APP_ROOT/.env.test"
407
+ if [[ "$DRY_RUN" -eq 1 ]]; then
408
+ echo "DRY RUN: commands will be printed but not executed"
409
+ fi
410
+
411
+ run mkdir -p "$TRACE_ROOT" "$(dirname "$ZMR_BIN")"
412
+
413
+ if [[ ! -x "$ZMR_BIN" ]]; then
414
+ target_args=()
415
+ if [[ "$(uname -s)" == "Darwin" && "$(uname -m)" == "arm64" ]]; then
416
+ target_args=(-target aarch64-macos.15.0)
417
+ fi
418
+ run zig build-exe src/main.zig "${target_args[@]}" -O Debug -femit-bin="$ZMR_BIN"
419
+ fi
420
+
421
+ run "$ZMR_BIN" version
422
+ if [[ -n "$SCENARIO" ]]; then
423
+ run "$ZMR_BIN" validate "$SCENARIO"
424
+ else
425
+ run "$ZMR_BIN" validate examples/android-app-auth-probe.json
426
+ run "$ZMR_BIN" validate examples/android-app-login-smoke.json
427
+ fi
428
+
429
+ run_zmr_android_scenario() {
430
+ if [[ "$ADB" == "adb" ]]; then
431
+ run "$ZMR_BIN" run "$@"
432
+ else
433
+ run "$ZMR_BIN" run "$@" --adb "$ADB"
434
+ fi
435
+ }
436
+
437
+ run_android_benchmark() {
438
+ if [[ "$ADB" == "adb" ]]; then
439
+ ZMR_BIN="$ZMR_BIN" run "$ROOT/scripts/benchmark.sh" "$@"
440
+ else
441
+ ZMR_BIN="$ZMR_BIN" run "$ROOT/scripts/benchmark.sh" "$@" --adb "$ADB"
442
+ fi
443
+ }
444
+
445
+ if [[ "$SKIP_EMULATOR" -eq 0 ]]; then
446
+ if [[ "$RESET_EMULATOR" -eq 1 ]]; then
447
+ run "$ADB" -s "$DEVICE" emu kill
448
+ STARTED_EMULATOR=1
449
+ fi
450
+ attached="$(capture "$ADB" devices | awk -v serial="$DEVICE" '$1 == serial && $2 == "device" { print $1 }')"
451
+ if [[ -z "$attached" || "$RESET_EMULATOR" -eq 1 ]]; then
452
+ if [[ -z "$AVD" ]]; then
453
+ AVD="$(find "$HOME/.android/avd" -maxdepth 1 -name '*.ini' -print 2>/dev/null | sed -n '1s#.*/##;s#\.ini$##p')"
454
+ fi
455
+ [[ -n "$AVD" ]] || die "no AVD found; pass --avd or --skip-emulator"
456
+ emulator_bin="${ANDROID_HOME:-$HOME/Library/Android/sdk}/emulator/emulator"
457
+ [[ -x "$emulator_bin" ]] || die "emulator binary not found: $emulator_bin"
458
+ emulator_args=(-avd "$AVD" -no-window -gpu swiftshader_indirect -no-snapshot-save -no-audio -no-boot-anim)
459
+ if [[ -n "$RESTORE_SNAPSHOT" ]]; then
460
+ emulator_args+=(-snapshot "$RESTORE_SNAPSHOT")
461
+ else
462
+ emulator_args+=(-no-snapshot-load)
463
+ fi
464
+ echo "+ $(quote_cmd "$emulator_bin" "${emulator_args[@]}")"
465
+ if [[ "$DRY_RUN" -eq 0 ]]; then
466
+ "$emulator_bin" "${emulator_args[@]}" > "$TRACE_ROOT/emulator.log" 2>&1 &
467
+ STARTED_EMULATOR=1
468
+ fi
469
+ fi
470
+ fi
471
+
472
+ if [[ "$SKIP_EMULATOR" -eq 1 ]]; then
473
+ preflight_android_device
474
+ fi
475
+ wait_for_device
476
+ wait_for_boot
477
+
478
+ run "$ADB" -s "$DEVICE" install -r "$APK"
479
+ run "$ADB" -s "$DEVICE" reverse tcp:8081 tcp:8081
480
+ run "$ADB" -s "$DEVICE" shell settings put global window_animation_scale 0
481
+ run "$ADB" -s "$DEVICE" shell settings put global transition_animation_scale 0
482
+ run "$ADB" -s "$DEVICE" shell settings put global animator_duration_scale 0
483
+
484
+ if [[ "$SKIP_METRO" -eq 0 ]]; then
485
+ command -v bun >/dev/null 2>&1 || die "bun is required to start Metro"
486
+ echo "+ (cd $(printf '%q' "$APP_ROOT") && bun run test:start > $(printf '%q' "$TRACE_ROOT/metro.log") 2>&1 &)"
487
+ if [[ "$DRY_RUN" -eq 0 ]]; then
488
+ (cd "$APP_ROOT" && bun run test:start > "$TRACE_ROOT/metro.log" 2>&1) &
489
+ METRO_PID="$!"
490
+ for _ in $(seq 1 90); do
491
+ if lsof -nP -iTCP:8081 -sTCP:LISTEN >/dev/null 2>&1; then
492
+ break
493
+ fi
494
+ sleep 1
495
+ done
496
+ fi
497
+ else
498
+ echo "Skipping Metro start; assuming app test server is already running"
499
+ fi
500
+
501
+ start_screen_recording
502
+
503
+ if [[ "$RUNS" -eq 1 ]]; then
504
+ if [[ -n "$SCENARIO" ]]; then
505
+ SINGLE_TRACE="$TRACE_ROOT/scenario"
506
+ run rm -rf "$SINGLE_TRACE"
507
+ run_zmr_android_scenario "$SCENARIO" --device "$DEVICE" --app-id "$APP_ID" --trace-dir "$SINGLE_TRACE"
508
+ run "$ZMR_BIN" report "$SINGLE_TRACE" --out "$SINGLE_TRACE/report.html"
509
+ run "$ZMR_BIN" export "$SINGLE_TRACE" --out "$TRACE_ROOT/scenario.zmrtrace"
510
+ run "$ZMR_BIN" export "$SINGLE_TRACE" --out "$TRACE_ROOT/scenario-redacted.zmrtrace" --redact
511
+ else
512
+ AUTH_TRACE="$TRACE_ROOT/auth"
513
+ LOGIN_TRACE="$TRACE_ROOT/login-smoke"
514
+ run rm -rf "$AUTH_TRACE" "$LOGIN_TRACE"
515
+ run_zmr_android_scenario examples/android-app-auth-probe.json --device "$DEVICE" --app-id "$APP_ID" --trace-dir "$AUTH_TRACE"
516
+ run "$ZMR_BIN" report "$AUTH_TRACE" --out "$AUTH_TRACE/report.html"
517
+ run "$ZMR_BIN" export "$AUTH_TRACE" --out "$TRACE_ROOT/auth.zmrtrace"
518
+ run "$ZMR_BIN" export "$AUTH_TRACE" --out "$TRACE_ROOT/auth-redacted.zmrtrace" --redact
519
+ run_zmr_android_scenario examples/android-app-login-smoke.json --device "$DEVICE" --app-id "$APP_ID" --trace-dir "$LOGIN_TRACE"
520
+ run "$ZMR_BIN" report "$LOGIN_TRACE" --out "$LOGIN_TRACE/report.html"
521
+ run "$ZMR_BIN" export "$LOGIN_TRACE" --out "$TRACE_ROOT/login-smoke.zmrtrace"
522
+ run "$ZMR_BIN" export "$LOGIN_TRACE" --out "$TRACE_ROOT/login-smoke-redacted.zmrtrace" --redact
523
+ fi
524
+ else
525
+ benchmark_gate_args=(--min-pass-rate "$MIN_PASS_RATE" --max-failures "$MAX_FAILURES")
526
+ if [[ -n "$MAX_MEAN_MS" ]]; then
527
+ benchmark_gate_args+=(--max-mean-ms "$MAX_MEAN_MS")
528
+ fi
529
+ if [[ -n "$MAX_P95_MS" ]]; then
530
+ benchmark_gate_args+=(--max-p95-ms "$MAX_P95_MS")
531
+ fi
532
+ if [[ -n "$SCENARIO" ]]; then
533
+ run_android_benchmark --zmr "$SCENARIO" --device "$DEVICE" --app-id "$APP_ID" --runs "$RUNS" --trace-root "$TRACE_ROOT/bench-scenario" "${benchmark_gate_args[@]}"
534
+ run "$ZMR_BIN" report "$TRACE_ROOT/bench-scenario" --out "$TRACE_ROOT/bench-scenario/report.html"
535
+ else
536
+ run_android_benchmark --zmr examples/android-app-auth-probe.json --device "$DEVICE" --app-id "$APP_ID" --runs "$RUNS" --trace-root "$TRACE_ROOT/bench-auth" "${benchmark_gate_args[@]}"
537
+ run "$ZMR_BIN" report "$TRACE_ROOT/bench-auth" --out "$TRACE_ROOT/bench-auth/report.html"
538
+ run_android_benchmark --zmr examples/android-app-login-smoke.json --device "$DEVICE" --app-id "$APP_ID" --runs "$RUNS" --trace-root "$TRACE_ROOT/bench-login-smoke" "${benchmark_gate_args[@]}"
539
+ run "$ZMR_BIN" report "$TRACE_ROOT/bench-login-smoke" --out "$TRACE_ROOT/bench-login-smoke/report.html"
540
+ fi
541
+ fi
542
+
543
+ stop_screen_recording
544
+
545
+ cat <<EOF
546
+
547
+ Android pilot complete.
548
+ Output directory: $TRACE_ROOT
549
+ Shareable bundles:
550
+ $TRACE_ROOT/auth-redacted.zmrtrace
551
+ $TRACE_ROOT/login-smoke-redacted.zmrtrace
552
+ Viewer:
553
+ $ROOT/viewer/index.html
554
+ EOF
555
+
556
+ if [[ "$SCREEN_RECORD" -eq 1 ]]; then
557
+ cat <<EOF
558
+ Screen recording:
559
+ $TRACE_ROOT/screenrecord.mp4
560
+ EOF
561
+ fi