@deeeed/metamask-harness 0.34.0 → 0.34.1

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 CHANGED
@@ -2,6 +2,22 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.34.1 - 2026-08-09
6
+
7
+ ### Fixed
8
+
9
+ - Launch Mobile Metro through the product's canonical watcher while preserving slot, build, Sentry, and Segment environment settings, applying cache clears only to the selected Metro instance, and removing the config override that could corrupt cold bundles.
10
+ - Pin native Mobile builds to the exact supported Main or Flask app identity and Android device, rejecting ambiguous physical-device names instead of building or launching on the wrong target.
11
+ - Clean up timed-out Metro process groups safely and require the cold-disk-cache Perps freshness gate to report the expected lifecycle.
12
+ - Allow performance recipes to enforce a first-fresh socket-to-subscriber latency budget for a stream and subscriber throttle, optionally scoped to a lifecycle while always reporting the observed lifecycle.
13
+ - Allow Mobile wallet unlock actions to use their declared target and unlock timeouts without the live-adapter process ending first.
14
+ - Remove Android's 180-second `screenrecord` cap so `--record-video=full-run` covers long recipe runs.
15
+ - Retry empty Metro target lists, derive the Android model pin from `ADB_SERIAL`, and use pin-aware wallet status while Hermes re-registers between recipe actions.
16
+ - Keep compound Mobile actions under one debugger lease so nested bridge calls cannot race another recipe action.
17
+ - Let Android `scroll_into_view` fall back to a scrollable ancestor when the target itself is not a scroll container.
18
+ - Leave Metro's worker count at its native default unless `METRO_MAX_WORKERS` is explicitly configured.
19
+ - Bound each Mobile wallet-status probe by its polling deadline, raise the default unlock verification window to 30 seconds, and give the Android cold-no-cache recipe an explicit cold-start allowance, preventing a successful physical-device unlock from being reported as a timeout.
20
+
5
21
  ## 0.34.0 - 2026-08-07
6
22
 
7
23
  ### Fixed
@@ -42,14 +42,6 @@
42
42
  "inputs": "--target --port --log --pid-file [--workers] [--clear]",
43
43
  "outputs": "Metro PID/launch metadata and redirected log; exit 0/1/2"
44
44
  },
45
- {
46
- "id": "mobile/metro-config",
47
- "entry": "adapters/mobile/metro-config.cjs",
48
- "kind": "module",
49
- "purpose": "Preserve the product Metro config while excluding harness-owned checkout temp artifacts.",
50
- "inputs": "base Metro config; env MM_HARNESS_METRO_PROJECT_ROOT, MM_HARNESS_METRO_BASE_CONFIG",
51
- "outputs": "merged Metro config with resolver.blockList for <projectRoot>/temp"
52
- },
53
45
  {
54
46
  "id": "mobile/stop-metro",
55
47
  "entry": "adapters/mobile/stop-metro.sh",
@@ -322,7 +322,11 @@ const COMMANDS = {
322
322
  const optsJson = JSON.stringify({ testId, offset, animated, intoView });
323
323
  // Try __AGENTIC__ bridge first, fall back to inline fiber walking
324
324
  const expr = `(function() {
325
- if (globalThis.__AGENTIC__?.scrollView) return globalThis.__AGENTIC__.scrollView(${optsJson});
325
+ // The app helper implements generic scrolling by searching inside the
326
+ // selector fiber. For into-view requests the scroll container is commonly
327
+ // an ancestor (for example, a Wallet Home section row), so use the
328
+ // ancestor-aware bridge fallback below instead.
329
+ if (!${intoView} && globalThis.__AGENTIC__?.scrollView) return globalThis.__AGENTIC__.scrollView(${optsJson});
326
330
  var hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
327
331
  if (!hook) return { ok: false, error: 'No React DevTools hook' };
328
332
  var renderers = hook.renderers;
@@ -681,6 +685,11 @@ const BRIDGE_LOCK_FILE = path.join(
681
685
  process.env.RECIPE_RUNTIME_DIR || path.join('temp', 'recipe', 'runtime'),
682
686
  'cdp-bridge.lock',
683
687
  );
688
+ const BRIDGE_LOCK_OWNER_PID = /^\d+$/.test(
689
+ process.env.CDP_BRIDGE_LOCK_OWNER_PID || '',
690
+ )
691
+ ? process.env.CDP_BRIDGE_LOCK_OWNER_PID
692
+ : String(process.pid);
684
693
 
685
694
  // Ownership semantics: last writer wins. Each bridge stamps its own pid; the
686
695
  // forwarder honors the lock while the pid IN THE FILE is alive. Overlapping
@@ -691,7 +700,7 @@ let bridgeLockHeld = false;
691
700
 
692
701
  function acquireBridgeLock() {
693
702
  try {
694
- fs.writeFileSync(BRIDGE_LOCK_FILE, String(process.pid));
703
+ fs.writeFileSync(BRIDGE_LOCK_FILE, BRIDGE_LOCK_OWNER_PID);
695
704
  bridgeLockHeld = true;
696
705
  } catch {
697
706
  // Lock is best-effort: a missing runtime dir must not break bridge commands.
@@ -765,7 +774,7 @@ Environment:
765
774
  process.exit(0);
766
775
  }
767
776
 
768
- const handler = COMMANDS[command];
777
+ const handler = COMMANDS[command === 'status-selected' ? 'status' : command];
769
778
  if (!handler) {
770
779
  console.error(`Unknown command: ${command}`);
771
780
  console.error(`Available: ${Object.keys(COMMANDS).join(', ')}`);
@@ -780,6 +789,8 @@ Environment:
780
789
  const timeout = Number.parseInt(process.env.CDP_TIMEOUT || '5000', 10);
781
790
 
782
791
  // `status` probes ALL connected targets so both platforms are visible.
792
+ // `status-selected` falls through to the normal pinned discovery path for
793
+ // action code that must survive one device's Hermes runtime rotation.
783
794
  if (command === 'status') {
784
795
  const { discoverAllTargets } = require('./lib/target-discovery.cjs');
785
796
  const allTargets = await discoverAllTargets(port);
@@ -6,7 +6,12 @@ const { createWSClient } = require('./ws-client.cjs');
6
6
  const { BRIDGE_ERROR_CODES, coded } = require('./bridge-errors.cjs');
7
7
 
8
8
  const FETCH_TIMEOUT_MS = Number.parseInt(process.env.CDP_TIMEOUT || '30000', 10);
9
- const FETCH_RETRIES = Number.parseInt(process.env.CDP_DISCOVERY_RETRIES || '3', 10);
9
+ const DISCOVERY_RETRY_DELAY_MS = 500;
10
+ const FETCH_RETRIES = Number.parseInt(
11
+ process.env.CDP_DISCOVERY_RETRIES ||
12
+ String(Math.max(3, Math.ceil(FETCH_TIMEOUT_MS / DISCOVERY_RETRY_DELAY_MS))),
13
+ 10,
14
+ );
10
15
 
11
16
  /** Fetch JSON from a URL (http only, no external deps) */
12
17
  function fetchJSONOnce(url) {
@@ -43,7 +48,9 @@ async function fetchJSON(url, acceptResponse) {
43
48
  lastError = error;
44
49
  }
45
50
  if (attempt === FETCH_RETRIES) break;
46
- await new Promise((resolve) => setTimeout(resolve, attempt * 1000));
51
+ await new Promise((resolve) =>
52
+ setTimeout(resolve, DISCOVERY_RETRY_DELAY_MS),
53
+ );
47
54
  }
48
55
  if (lastError !== undefined) throw lastError;
49
56
  if (lastResponse !== undefined) return lastResponse;
@@ -146,21 +153,21 @@ async function discoverTarget(port) {
146
153
  let acceptedPinnedCandidate;
147
154
  let targets;
148
155
  try {
149
- targets = await fetchJSON(
150
- listUrl,
151
- androidPinned
152
- ? async (response) => {
153
- const pinnedCandidates = rankRuntimeCandidates(response).filter(matchesAndroidPin);
154
- for (const candidate of pinnedCandidates) {
155
- if (await probeTarget(candidate.webSocketDebuggerUrl)) {
156
- acceptedPinnedCandidate = candidate;
157
- return true;
158
- }
156
+ targets = await fetchJSON(listUrl, async (response) => {
157
+ const runtimeCandidates = rankRuntimeCandidates(response);
158
+ if (runtimeCandidates.length === 0) return false;
159
+ if (androidPinned) {
160
+ const pinnedCandidates = runtimeCandidates.filter(matchesAndroidPin);
161
+ for (const candidate of pinnedCandidates) {
162
+ if (await probeTarget(candidate.webSocketDebuggerUrl)) {
163
+ acceptedPinnedCandidate = candidate;
164
+ return true;
159
165
  }
160
- return false;
161
166
  }
162
- : undefined,
163
- );
167
+ return false;
168
+ }
169
+ return true;
170
+ });
164
171
  } catch (e) {
165
172
  throw coded(
166
173
  new Error(`Cannot reach Metro at ${listUrl}. Is Metro running?\n ${e.message}`),
@@ -26,10 +26,10 @@ function parseArgs(argv) {
26
26
  async function main() {
27
27
  const args = parseArgs(process.argv.slice(2));
28
28
  if (args.help) {
29
- console.log('Usage: launch-metro.cjs --target <path> --port <port> --log <path> --pid-file <path> [--workers <n>] [--clear]');
29
+ console.log('Usage: launch-metro.cjs --target <path> --port <port> --log <path> --pid-file <path> --build-env <path> [--workers <n>] [--clear]');
30
30
  return;
31
31
  }
32
- for (const required of ['target', 'port', 'log', 'pid-file']) {
32
+ for (const required of ['target', 'port', 'log', 'pid-file', 'build-env']) {
33
33
  if (!args[required]) throw new Error(`--${required} is required`);
34
34
  }
35
35
  const target = path.resolve(args.target);
@@ -38,9 +38,9 @@ async function main() {
38
38
  fs.mkdirSync(path.dirname(log), { recursive: true });
39
39
  fs.mkdirSync(path.dirname(pidFile), { recursive: true });
40
40
  const logFd = fs.openSync(log, 'a');
41
- const childArgs = ['expo', 'start', '--port', String(args.port)];
42
- if (args.clear) childArgs.push('--clear');
41
+ const childArgs = ['watch'];
43
42
  const env = { ...process.env };
43
+ env.BASH_ENV = args['build-env'];
44
44
  if (args.workers) env.METRO_MAX_WORKERS = String(args.workers);
45
45
  const child = spawn('yarn', childArgs, {
46
46
  cwd: target,
@@ -59,10 +59,12 @@ async function main() {
59
59
  `${JSON.stringify({
60
60
  schemaVersion: 1,
61
61
  pid: child.pid,
62
+ pgid: child.pid,
62
63
  startedAt: new Date().toISOString(),
63
64
  target,
64
65
  port: Number(args.port),
65
66
  clear: Boolean(args.clear),
67
+ command: `yarn ${childArgs[0]}`,
66
68
  }, null, 2)}\n`,
67
69
  );
68
70
  child.unref();
@@ -90,19 +90,29 @@ ios_app_installed() {
90
90
  }
91
91
 
92
92
  run_ios_native_build() (
93
- local sim_target="$1" reason="$2" build_env=""
94
- printf '%s; installing with yarn start:ios (WATCHER_PORT=%s)\n' "$reason" "$PORT" >&2
93
+ local sim_target="$1" bundle_id="$2" reason="$3" build_env="" build_script="" build_type=""
94
+ case "$bundle_id" in
95
+ io.metamask.MetaMask|io.metamask) build_script="start:ios"; build_type="main" ;;
96
+ io.metamask.MetaMask-Flask) build_script="start:ios:flask"; build_type="flask" ;;
97
+ *)
98
+ printf 'open-device: no supported local build script for iOS bundle %s\n' "$bundle_id" >&2
99
+ return 1
100
+ ;;
101
+ esac
102
+ printf '%s; installing with yarn %s (WATCHER_PORT=%s)\n' "$reason" "$build_script" "$PORT" >&2
95
103
  build_env="$(mktemp "${TMPDIR:-/tmp}/mm-harness-ios-build-env.XXXXXX")" || return 1
96
104
  chmod 600 "$build_env" || { rm -f "$build_env"; return 1; }
97
105
  trap 'rm -f "$build_env"' EXIT HUP INT TERM
98
106
  {
99
107
  printf 'export MM_HARNESS_BUILD_WATCHER_PORT=%q\n' "$PORT"
100
108
  printf 'export MM_HARNESS_BUILD_IOS_SIMULATOR=%q\n' "$sim_target"
109
+ printf 'export MM_HARNESS_BUILD_TYPE=%q\n' "$build_type"
101
110
  cat <<'BUILD_ENV'
102
111
  mm_harness_restore_build_target() {
103
112
  export WATCHER_PORT="$MM_HARNESS_BUILD_WATCHER_PORT"
104
113
  export METRO_PORT="$MM_HARNESS_BUILD_WATCHER_PORT"
105
114
  export IOS_SIMULATOR="$MM_HARNESS_BUILD_IOS_SIMULATOR"
115
+ export METAMASK_BUILD_TYPE="$MM_HARNESS_BUILD_TYPE"
106
116
  }
107
117
  eval() {
108
118
  builtin eval "$@"
@@ -122,7 +132,7 @@ BUILD_ENV
122
132
  (
123
133
  cd "$TARGET"
124
134
  command -v activate_repo_ruby_best_effort >/dev/null 2>&1 && activate_repo_ruby_best_effort "$TARGET"
125
- BASH_ENV="$build_env" WATCHER_PORT="$PORT" METRO_PORT="$PORT" IOS_SIMULATOR="$sim_target" EXPO_NO_TYPESCRIPT_SETUP=1 yarn start:ios 2>&1
135
+ BASH_ENV="$build_env" WATCHER_PORT="$PORT" METRO_PORT="$PORT" IOS_SIMULATOR="$sim_target" METAMASK_BUILD_TYPE="$build_type" EXPO_NO_TYPESCRIPT_SETUP=1 yarn "$build_script" 2>&1
126
136
  )
127
137
  )
128
138
 
@@ -138,10 +148,16 @@ ensure_ios_app_for_mode() {
138
148
  ;;
139
149
  auto|default)
140
150
  if ios_app_installed "$sim_target" "$bundle_id"; then return 0; fi
141
- run_ios_native_build "$sim_target" "iOS dev client ${bundle_id} not installed on ${sim_target}"
151
+ if ! run_ios_native_build "$sim_target" "$bundle_id" "iOS dev client ${bundle_id} not installed on ${sim_target}"; then
152
+ printf 'open-device: iOS native build failed\n' >&2
153
+ return 1
154
+ fi
142
155
  ;;
143
156
  rebuild-native|clean)
144
- run_ios_native_build "$sim_target" "Rebuilding iOS dev client ${bundle_id} (--preflight-mode ${mode})"
157
+ if ! run_ios_native_build "$sim_target" "$bundle_id" "Rebuilding iOS dev client ${bundle_id} (--preflight-mode ${mode})"; then
158
+ printf 'open-device: iOS native build failed\n' >&2
159
+ return 1
160
+ fi
145
161
  ;;
146
162
  *)
147
163
  printf 'open-device: unknown --preflight-mode: %s\n' "$mode" >&2; return 2 ;;
@@ -264,14 +280,113 @@ android_app_installed() {
264
280
  "$ADB_BIN" "$@" shell pm list packages 2>/dev/null | grep -q "package:${pkg}"
265
281
  }
266
282
 
267
- run_android_native_build() {
268
- local reason="$1"
269
- printf '%s; installing with yarn start:android (WATCHER_PORT=%s)\n' "$reason" "$PORT" >&2
270
- (cd "$TARGET" && WATCHER_PORT="$PORT" yarn start:android 2>&1)
283
+ run_android_native_build() (
284
+ local serial="$1" expo_device="$2" pkg="$3" reason="$4" build_env="" build_script=""
285
+ case "$pkg" in
286
+ io.metamask) build_script="start:android" ;;
287
+ io.metamask.flask) build_script="start:android:flask" ;;
288
+ *)
289
+ printf 'open-device: no supported local build script for Android package %s\n' "$pkg" >&2
290
+ return 1
291
+ ;;
292
+ esac
293
+ printf '%s; installing with yarn %s (WATCHER_PORT=%s)\n' "$reason" "$build_script" "$PORT" >&2
294
+ build_env="$(mktemp "${TMPDIR:-/tmp}/mm-harness-android-build-env.XXXXXX")" || return 1
295
+ chmod 600 "$build_env" || { rm -f "$build_env"; return 1; }
296
+ trap 'rm -f "$build_env"' EXIT HUP INT TERM
297
+ {
298
+ printf 'export MM_HARNESS_BUILD_ANDROID_SERIAL=%q\n' "$serial"
299
+ printf 'export MM_HARNESS_BUILD_ANDROID_EXPO_DEVICE=%q\n' "$expo_device"
300
+ printf 'export MM_HARNESS_BUILD_WATCHER_PORT=%q\n' "$PORT"
301
+ cat <<'BUILD_ENV'
302
+ mm_harness_restore_android_build_target() {
303
+ export WATCHER_PORT="$MM_HARNESS_BUILD_WATCHER_PORT"
304
+ export METRO_PORT="$MM_HARNESS_BUILD_WATCHER_PORT"
305
+ export ADB_SERIAL="$MM_HARNESS_BUILD_ANDROID_SERIAL"
306
+ export ANDROID_SERIAL="$MM_HARNESS_BUILD_ANDROID_SERIAL"
307
+ }
308
+ eval() {
309
+ builtin eval "$@"
310
+ local status=$?
311
+ mm_harness_restore_android_build_target
312
+ return "$status"
313
+ }
314
+ source() {
315
+ builtin source "$@"
316
+ local status=$?
317
+ mm_harness_restore_android_build_target
318
+ return "$status"
319
+ }
320
+ yarn() {
321
+ if [ "${1:-}" = expo ] && [ "${2:-}" = run:android ]; then
322
+ command yarn "$@" "$MM_HARNESS_BUILD_ANDROID_EXPO_DEVICE"
323
+ else
324
+ command yarn "$@"
325
+ fi
326
+ }
327
+ mm_harness_restore_android_build_target
328
+ BUILD_ENV
329
+ } > "$build_env"
330
+ (
331
+ cd "$TARGET"
332
+ BASH_ENV="$build_env" \
333
+ WATCHER_PORT="$PORT" METRO_PORT="$PORT" \
334
+ ADB_SERIAL="$serial" ANDROID_SERIAL="$serial" \
335
+ EXPO_NO_TYPESCRIPT_SETUP=1 yarn "$build_script" 2>&1
336
+ )
337
+ )
338
+
339
+ resolve_android_expo_device() {
340
+ local serial="$1" rows="" selected_line="" expo_device="" row_serial="" row_state="" row_name="" matches=0
341
+ [ -n "$serial" ] || return 0
342
+ rows="$($ADB_BIN devices -l 2>/dev/null || true)"
343
+ selected_line="$(printf '%s\n' "$rows" | awk -v wanted="$serial" '$1 == wanted { print; exit }')"
344
+ if [ -z "$selected_line" ]; then
345
+ printf 'open-device: configured Android device %s is not attached\n' "$serial" >&2
346
+ return 1
347
+ fi
348
+ row_state="$(printf '%s\n' "$selected_line" | awk '{print $2}')"
349
+ if [ "$row_state" != "device" ]; then
350
+ printf 'open-device: configured Android device %s is not ready (state: %s)\n' "$serial" "$row_state" >&2
351
+ return 1
352
+ fi
353
+ if printf '%s' "$serial" | grep -q '^emulator-'; then
354
+ expo_device="$($ADB_BIN -s "$serial" emu avd name 2>/dev/null | tr -d '\r' | sed '/^OK$/d' | head -1 || true)"
355
+ else
356
+ expo_device="$(printf '%s\n' "$selected_line" | awk '{ for (i = 3; i <= NF; i += 1) if ($i ~ /^model:/) { sub(/^model:/, "", $i); print $i; exit } }')"
357
+ fi
358
+ [ -n "$expo_device" ] || expo_device="${ANDROID_TARGET_DEVICE_NAME:-}"
359
+ expo_device="${expo_device// /_}"
360
+ if [ -z "$expo_device" ]; then
361
+ printf 'open-device: could not resolve Expo device name for Android serial %s\n' "$serial" >&2
362
+ return 1
363
+ fi
364
+
365
+ while IFS= read -r selected_line; do
366
+ row_serial="$(printf '%s\n' "$selected_line" | awk '{print $1}')"
367
+ row_state="$(printf '%s\n' "$selected_line" | awk '{print $2}')"
368
+ [ "$row_state" = "device" ] || continue
369
+ if printf '%s' "$row_serial" | grep -q '^emulator-'; then
370
+ row_name="$($ADB_BIN -s "$row_serial" emu avd name 2>/dev/null | tr -d '\r' | sed '/^OK$/d' | head -1 || true)"
371
+ else
372
+ row_name="$(printf '%s\n' "$selected_line" | awk '{ for (i = 3; i <= NF; i += 1) if ($i ~ /^model:/) { sub(/^model:/, "", $i); print $i; exit } }')"
373
+ fi
374
+ row_name="${row_name// /_}"
375
+ [ "$row_name" = "$expo_device" ] && matches=$((matches + 1))
376
+ done <<EOF
377
+ $rows
378
+ EOF
379
+ if [ "$matches" -ne 1 ]; then
380
+ printf 'open-device: Expo device name %s is ambiguous across %s attached Android devices; Expo cannot select serial %s safely\n' \
381
+ "$expo_device" "$matches" "$serial" >&2
382
+ return 1
383
+ fi
384
+ printf '%s\n' "$expo_device"
271
385
  }
272
386
 
273
387
  ensure_android_app_for_mode() {
274
388
  local pkg="$1" mode="$2"
389
+ local expo_device=""
275
390
  shift 2
276
391
  case "$mode" in
277
392
  fast)
@@ -281,10 +396,26 @@ ensure_android_app_for_mode() {
281
396
  ;;
282
397
  auto|default)
283
398
  if android_app_installed "$pkg" "$@"; then return 0; fi
284
- run_android_native_build "Android dev client ${pkg} not installed"
399
+ if [ -n "$ADB_SERIAL_ARG" ]; then
400
+ expo_device="$(resolve_android_expo_device "$ADB_SERIAL_ARG")" || return 1
401
+ fi
402
+ [ -n "$expo_device" ] || expo_device="${ANDROID_TARGET_DEVICE_NAME:-}"
403
+ expo_device="${expo_device// /_}"
404
+ if ! run_android_native_build "$ADB_SERIAL_ARG" "$expo_device" "$pkg" "Android dev client ${pkg} not installed"; then
405
+ printf 'open-device: Android native build failed\n' >&2
406
+ return 1
407
+ fi
285
408
  ;;
286
409
  rebuild-native|clean)
287
- run_android_native_build "Rebuilding Android dev client ${pkg} (--preflight-mode ${mode})"
410
+ if [ -n "$ADB_SERIAL_ARG" ]; then
411
+ expo_device="$(resolve_android_expo_device "$ADB_SERIAL_ARG")" || return 1
412
+ fi
413
+ [ -n "$expo_device" ] || expo_device="${ANDROID_TARGET_DEVICE_NAME:-}"
414
+ expo_device="${expo_device// /_}"
415
+ if ! run_android_native_build "$ADB_SERIAL_ARG" "$expo_device" "$pkg" "Rebuilding Android dev client ${pkg} (--preflight-mode ${mode})"; then
416
+ printf 'open-device: Android native build failed\n' >&2
417
+ return 1
418
+ fi
288
419
  ;;
289
420
  *)
290
421
  printf 'open-device: unknown --preflight-mode: %s\n' "$mode" >&2; return 2 ;;
@@ -301,8 +432,16 @@ PY
301
432
 
302
433
  # --- dispatch -----------------------------------------------------------------
303
434
 
304
- BUNDLE_IDS=("${IOS_BUNDLE_ID:-io.metamask.MetaMask}" "io.metamask" "io.metamask.flask" "io.metamask.qa")
305
- ANDROID_PKGS=("${ANDROID_PACKAGE_ID:-io.metamask}" "io.metamask.flask" "io.metamask.qa")
435
+ if [ -n "${IOS_BUNDLE_ID:-}" ]; then
436
+ BUNDLE_IDS=("$IOS_BUNDLE_ID")
437
+ else
438
+ BUNDLE_IDS=("io.metamask.MetaMask" "io.metamask" "io.metamask.MetaMask-Flask" "io.metamask.qa")
439
+ fi
440
+ if [ -n "${ANDROID_PACKAGE_ID:-}" ]; then
441
+ ANDROID_PKGS=("$ANDROID_PACKAGE_ID")
442
+ else
443
+ ANDROID_PKGS=("io.metamask" "io.metamask.flask" "io.metamask.qa")
444
+ fi
306
445
 
307
446
  if [ "$PLATFORM" = "ios" ]; then
308
447
  SIM_TARGET="$(sim_udid_for_target "$SIMULATOR")"
@@ -330,7 +469,12 @@ if [ "$PLATFORM" = "ios" ]; then
330
469
 
331
470
  LAUNCHED=false
332
471
  for BUNDLE_ID in "${BUNDLE_IDS[@]}"; do
333
- ensure_ios_app_for_mode "$SIM_TARGET" "$BUNDLE_ID" "$PREFLIGHT_MODE" || continue
472
+ if ! ensure_ios_app_for_mode "$SIM_TARGET" "$BUNDLE_ID" "$PREFLIGHT_MODE"; then
473
+ case "$PREFLIGHT_MODE" in
474
+ fast) continue ;;
475
+ *) exit 1 ;;
476
+ esac
477
+ fi
334
478
  if $RESTART; then
335
479
  xcrun simctl terminate "$SIM_TARGET" "$BUNDLE_ID" >/dev/null 2>&1 || true
336
480
  printf 'Restarting iOS dev client %s on %s\n' "$BUNDLE_ID" "$SIM_TARGET" >&2
@@ -384,7 +528,12 @@ else
384
528
 
385
529
  LAUNCHED=false
386
530
  for PKG in "${ANDROID_PKGS[@]}"; do
387
- ensure_android_app_for_mode "$PKG" "$PREFLIGHT_MODE" "${ADB_ARGS[@]}" || continue
531
+ if ! ensure_android_app_for_mode "$PKG" "$PREFLIGHT_MODE" "${ADB_ARGS[@]}"; then
532
+ case "$PREFLIGHT_MODE" in
533
+ fast) continue ;;
534
+ *) exit 1 ;;
535
+ esac
536
+ fi
388
537
  if $RESTART; then
389
538
  "$ADB_BIN" "${ADB_ARGS[@]}" shell am force-stop "$PKG" >/dev/null 2>&1 || true
390
539
  printf 'Restarting Android dev client %s\n' "$PKG" >&2