@deeeed/metamask-harness 0.47.4 → 0.48.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 (146) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +3 -2
  3. package/adapters/extension/lib/macos-focus.cjs +2 -2
  4. package/adapters/extension/live.sh +6 -8
  5. package/adapters/mobile/bridge-runtime/lib/bridge-errors.cjs +1 -0
  6. package/adapters/shared/ensure-runner-deps.sh +6 -0
  7. package/dist/adapters/extension/runtime-decision.js +18 -1
  8. package/dist/adapters/extension/runtime.js +5 -5
  9. package/dist/adapters/extension/surface.js +1 -0
  10. package/dist/app-lifecycle.js +3 -1
  11. package/dist/command-contract.js +2 -0
  12. package/dist/commands/call.js +1 -8
  13. package/dist/commands/device-target.js +12 -3
  14. package/dist/commands/doctor.js +7 -5
  15. package/dist/commands/fixtures.js +1 -1
  16. package/dist/commands/launch/index.js +10 -0
  17. package/dist/commands/manifest.js +3 -1
  18. package/dist/commands/mobile-device-view.js +13 -3
  19. package/dist/commands/parse-args.js +2 -0
  20. package/dist/commands/run-engine.js +29 -4
  21. package/dist/commands/run.js +1 -1
  22. package/dist/commands/runtime-launch.js +10 -1
  23. package/dist/commands/status.js +8 -4
  24. package/dist/devices.js +31 -9
  25. package/dist/execution-provenance.js +8 -7
  26. package/dist/funding-execution-context.js +43 -13
  27. package/dist/heal-bounds.js +3 -3
  28. package/dist/live-adapter-contract.js +4 -0
  29. package/dist/manifest.js +2 -13
  30. package/dist/mm-harness-cli.js +2 -1
  31. package/dist/runner.js +14 -3
  32. package/docs/RECIPES.md +133 -22
  33. package/docs/RELEASE-QA-CAPABILITY-MAP.md +3 -2
  34. package/library/actions/core/perps/_controller.mjs +10 -0
  35. package/library/actions/core/perps/close_orders.mjs +13 -17
  36. package/library/actions/extension/assets/home-token-identity.mjs +30 -0
  37. package/library/actions/extension/assets/import_custom_token.mjs +61 -79
  38. package/library/actions/extension/assets/open_details.mjs +18 -7
  39. package/library/actions/extension/assets/read_details.mjs +61 -29
  40. package/library/actions/extension/assets/read_visible_state.mjs +4 -1
  41. package/library/actions/extension/assets/select_network_scope.mjs +16 -9
  42. package/library/actions/extension/assets/set_token_visibility.mjs +84 -17
  43. package/library/actions/extension/assets/verify_sorting.mjs +62 -14
  44. package/library/actions/extension/networks/add_custom.mjs +19 -17
  45. package/library/actions/extension/networks/custom_network.mjs +8 -3
  46. package/library/actions/extension/networks/read_visible_state.mjs +1 -0
  47. package/library/actions/extension/performance/_navigation-memory.mjs +35 -6
  48. package/library/actions/extension/performance/compare_idle_navigation_memory.mjs +3 -2
  49. package/library/actions/extension/performance/measure_detached_dom.mjs +3 -2
  50. package/library/actions/extension/performance/measure_navigation_memory.mjs +3 -2
  51. package/library/actions/extension/perps/assert_visible_consistency.mjs +9 -4
  52. package/library/actions/extension/perps/close_visible_position.mjs +3 -3
  53. package/library/actions/extension/perps/edit_margin.mjs +1 -1
  54. package/library/actions/extension/perps/open_balance_action.mjs +16 -7
  55. package/library/actions/extension/perps/perps.mjs +150 -45
  56. package/library/actions/extension/perps/read_funds_confirmation.mjs +8 -0
  57. package/library/actions/extension/perps/read_snapshot.mjs +2 -1
  58. package/library/actions/extension/perps/read_visible_state.mjs +10 -6
  59. package/library/actions/extension/perps/search_markets.mjs +7 -2
  60. package/library/actions/extension/perps/select_activity_filter.mjs +1 -1
  61. package/library/actions/extension/perps/select_market_filter.mjs +74 -45
  62. package/library/actions/extension/perps/set_market_favorite.mjs +6 -3
  63. package/library/actions/extension/perps/state.mjs +1 -0
  64. package/library/actions/extension/perps/update_position_tpsl.mjs +1 -1
  65. package/library/actions/extension/perps/visible-mutation-identity.mjs +4 -2
  66. package/library/actions/extension/platform/cdp.mjs +23 -142
  67. package/library/actions/extension/swap_bridge/quote-state.mjs +54 -0
  68. package/library/actions/extension/swap_bridge/read_visible_state.mjs +4 -1
  69. package/library/actions/extension/swap_bridge/select_assets.mjs +11 -1
  70. package/library/actions/extension/swap_bridge/set_amount.mjs +10 -8
  71. package/library/actions/extension/swap_bridge/set_max_amount.mjs +17 -18
  72. package/library/actions/extension/swap_bridge/set_slippage.mjs +0 -9
  73. package/library/actions/extension/ui/navigate.mjs +58 -14
  74. package/library/actions/extension/wallet/lock.mjs +2 -1
  75. package/library/actions/mobile/assets/import_custom_token.mjs +1 -0
  76. package/library/actions/mobile/assets/read_visible_state.mjs +3 -1
  77. package/library/actions/mobile/assets/set_token_visibility.mjs +13 -9
  78. package/library/actions/mobile/networks/network-management.mjs +22 -15
  79. package/library/actions/mobile/networks/remove_custom.mjs +6 -2
  80. package/library/actions/mobile/perps/perps.mjs +3 -0
  81. package/library/actions/mobile/perps/read-visible-state-loop.mjs +1 -0
  82. package/library/actions/mobile/perps/set_market_favorite.mjs +15 -19
  83. package/library/actions/mobile/platform/native-session.mjs +26 -17
  84. package/library/actions/mobile/platform/observe-ui.mjs +2 -2
  85. package/library/actions/mobile/ui/navigate.mjs +1 -1
  86. package/library/actions/shared/perps/visible-state.mjs +14 -3
  87. package/library/manifests/core.action-manifest.json +14 -2
  88. package/library/manifests/extension.action-manifest.json +49 -16
  89. package/library/manifests/mobile.action-manifest.json +8 -7
  90. package/library/recipes/core/perps/smoke.recipe.json +23 -0
  91. package/library/recipes/core/perps/snapshot.recipe.json +26 -1
  92. package/library/recipes/extension/assets/release-custom-network-token-persistence.recipe.json +25 -8
  93. package/library/recipes/extension/performance/navigation-memory-suite.recipe.json +219 -0
  94. package/library/recipes/extension/performance/navigation-memory.recipe.json +61 -156
  95. package/library/recipes/extension/wallet/smoke.recipe.json +17 -8
  96. package/library/recipes/mobile/app/lifecycle.android-smoke.recipe.json +23 -4
  97. package/library/recipes/mobile/networks/release-custom-network-persistence.recipe.json +31 -6
  98. package/library/recipes/mobile/networks/release-multichain-visibility.recipe.json +5 -4
  99. package/library/recipes/mobile/wallet/smoke.recipe.json +17 -8
  100. package/library/recipes/{assets → shared/assets}/release-token-address-import.recipe.json +7 -0
  101. package/library/recipes/{assets → shared/assets}/release-token-details.recipe.json +2 -2
  102. package/library/recipes/shared/assets/release-token-hide-readd.recipe.json +91 -0
  103. package/library/recipes/{assets → shared/assets}/release-token-persistence.recipe.json +3 -0
  104. package/library/recipes/{assets → shared/assets}/release-token-search-import.recipe.json +4 -1
  105. package/library/recipes/{assets → shared/assets}/release-token-sorting.recipe.json +13 -2
  106. package/library/recipes/{swap-bridge → shared/swap-bridge}/release-quote.recipe.json +29 -3
  107. package/package.json +3 -3
  108. package/site/assets/help-recipes.json +63 -23
  109. package/library/recipes/assets/release-token-hide-readd.recipe.json +0 -57
  110. package/library/recipes/core/perps/clean-market-testnet.recipe.json +0 -44
  111. package/library/recipes/core/perps/order-lifecycle.recipe.json +0 -82
  112. package/library/recipes/core/perps/read-markets.recipe.json +0 -36
  113. package/library/recipes/core/perps/trading-lifecycle.recipe.json +0 -80
  114. package/library/recipes/extension/perps/release-activity-filters.recipe.json +0 -51
  115. package/library/recipes/extension/perps/release-funds-flows.recipe.json +0 -76
  116. package/library/recipes/extension/perps/release-live-limit-order.recipe.json +0 -109
  117. package/library/recipes/extension/perps/release-market-filters.recipe.json +0 -97
  118. package/library/recipes/extension/perps/release-order-entry.recipe.json +0 -158
  119. package/library/recipes/extension/perps/release-order-validation.recipe.json +0 -259
  120. package/library/recipes/extension/perps/release-visible-consistency.recipe.json +0 -47
  121. package/library/recipes/extension/perps/source-dev-snapshot-consistency.recipe.json +0 -76
  122. package/library/recipes/mobile/perps/chase-assert-running.recipe.json +0 -110
  123. package/library/recipes/mobile/perps/chase-place.recipe.json +0 -145
  124. package/library/recipes/mobile/perps/chase-terminate.recipe.json +0 -98
  125. package/library/recipes/mobile/perps/performance.recipe.json +0 -722
  126. package/library/recipes/mobile/perps/pro-order-setup.recipe.json +0 -111
  127. package/library/recipes/mobile/perps/pro-order-start-state.recipe.json +0 -53
  128. package/library/recipes/mobile/perps/scale-assert-orders.recipe.json +0 -126
  129. package/library/recipes/mobile/perps/scale-place.recipe.json +0 -186
  130. package/library/recipes/mobile/perps/twap-assert-active.recipe.json +0 -97
  131. package/library/recipes/mobile/perps/twap-place.recipe.json +0 -146
  132. package/library/recipes/mobile/runner/action-validation.recipe.json +0 -336
  133. package/library/recipes/perps/clean-market-testnet.recipe.json +0 -49
  134. package/library/recipes/perps/lifecycle.recipe.json +0 -144
  135. package/library/recipes/perps/release-live-market-order.recipe.json +0 -139
  136. package/library/recipes/perps/release-watchlist.recipe.json +0 -111
  137. package/library/recipes/swap-bridge/release-max-quote.recipe.json +0 -72
  138. /package/library/recipes/{assets → shared/assets}/release-token-list.recipe.json +0 -0
  139. /package/library/recipes/{perps → shared/perps}/release-market-details.recipe.json +0 -0
  140. /package/library/recipes/{perps → shared/perps}/release-market-search.recipe.json +0 -0
  141. /package/library/recipes/{swap-bridge → shared/swap-bridge}/release-custom-slippage.recipe.json +0 -0
  142. /package/library/recipes/{swap-bridge → shared/swap-bridge}/release-live-max-swap.recipe.json +0 -0
  143. /package/library/recipes/{swap-bridge → shared/swap-bridge}/release-live-swap.recipe.json +0 -0
  144. /package/library/recipes/{swap-bridge → shared/swap-bridge}/release-surface.recipe.json +0 -0
  145. /package/library/recipes/{wallet → shared/wallet}/import.recipe.json +0 -0
  146. /package/library/recipes/{wallet → shared/wallet}/reset-import.recipe.json +0 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.48.0 - 2026-09-07
6
+
7
+ - Consolidate parameterized recipes, move specialized Perps journeys into the team library, and separate short performance checks from full coverage suites.
8
+ - Correct wallet, token, quote and Perps validation; honor network-addition opt-in and preserve identity-bound cleanup and failure evidence. Bound macOS focus calls, avoid duplicate Android preparation and native-session ownership, and refuse dependency repairs through shared directory links.
9
+ - Add compact, searchable tutorials and version-matched recipe guidance for branching, composition, performance, and diagnosing drift.
10
+
5
11
  ## 0.47.4 - 2026-09-05
6
12
 
7
13
  ### Fixed
package/README.md CHANGED
@@ -129,7 +129,7 @@ mm-harness actions --action metamask.wallet.ensure_unlocked
129
129
  mm-harness call metamask.wallet.ensure_unlocked
130
130
 
131
131
  mm-harness run --list
132
- mm-harness run perps.clean-market-testnet --describe
132
+ mm-harness run perps.smoke --describe
133
133
  mm-harness run wallet.smoke --describe
134
134
  mm-harness run wallet.import method=auto
135
135
 
@@ -145,7 +145,8 @@ absolute paths to the report, trace, executed recipe, and artifact manifest.
145
145
  Use `mm-harness last --json` to resume without repeating the last operation.
146
146
 
147
147
  For automation, `--json` emits one stable document. `run --json-stream` emits
148
- line-buffered JSONL progress and a terminal event.
148
+ line-buffered JSONL progress and a terminal event. Its status matches the recipe's
149
+ `pass`, `fail`, or `unknown` verdict; `unknown` still exits nonzero.
149
150
 
150
151
  ## Team libraries
151
152
 
@@ -8,7 +8,7 @@ function captureMacFrontmostProcess() {
8
8
  const value = execFileSync('osascript', [
9
9
  '-e',
10
10
  'tell application "System Events" to get unix id of first application process whose frontmost is true',
11
- ], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
11
+ ], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 1_000 }).trim();
12
12
  const pid = Number.parseInt(value, 10);
13
13
  return Number.isInteger(pid) && pid > 0 ? pid : null;
14
14
  } catch {
@@ -22,7 +22,7 @@ function restoreMacFrontmostProcess(pid) {
22
22
  execFileSync('osascript', [
23
23
  '-e',
24
24
  `tell application "System Events" to set frontmost of first application process whose unix id is ${pid} to true`,
25
- ], { stdio: 'ignore' });
25
+ ], { stdio: 'ignore', timeout: 1_000 });
26
26
  return true;
27
27
  } catch {
28
28
  return false;
@@ -45,7 +45,7 @@ START_WATCH=false
45
45
  BUILD_LAVAMOAT=false
46
46
  EXTERNAL_ARTIFACT=false
47
47
  ARTIFACT_PROVENANCE=""
48
- RESET_PROFILE=true
48
+ RESET_PROFILE=false
49
49
  DIST_DIR="dist/chrome"
50
50
  CHROME_USER_DATA_DIR="${CHROME_USER_DATA_DIR:-}"
51
51
  REMOTE_FLAGS=""
@@ -62,6 +62,7 @@ while [ "$#" -gt 0 ]; do
62
62
  --external-artifact) EXTERNAL_ARTIFACT=true; LAUNCH_EXISTING_DIST=true; shift ;;
63
63
  --artifact-provenance) [ "$#" -ge 2 ] || { echo "Missing value for $1" >&2; exit 2; }; ARTIFACT_PROVENANCE="$2"; shift 2 ;;
64
64
  --preserve-profile) RESET_PROFILE=false; shift ;;
65
+ --reset-profile) RESET_PROFILE=true; shift ;;
65
66
  --start-url) [ "$#" -ge 2 ] || { echo "Missing value for $1" >&2; exit 2; }; START_URL="$2"; shift 2 ;;
66
67
  --start-watch|--start-test-watch) START_WATCH=true; LAUNCH_EXISTING_DIST=true; shift ;;
67
68
  --build-lavamoat) BUILD_LAVAMOAT=true; LAUNCH_EXISTING_DIST=true; shift ;;
@@ -69,7 +70,7 @@ while [ "$#" -gt 0 ]; do
69
70
  --dist-dir) [ "$#" -ge 2 ] || { echo "Missing value for $1" >&2; exit 2; }; DIST_DIR="$2"; shift 2 ;;
70
71
  --chrome-user-data-dir) [ "$#" -ge 2 ] || { echo "Missing value for $1" >&2; exit 2; }; CHROME_USER_DATA_DIR="$2"; shift 2 ;;
71
72
  --remote-flag) [ "$#" -ge 2 ] || { echo "Missing value for $1" >&2; exit 2; }; REMOTE_FLAGS="$2"; shift 2 ;;
72
- -h|--help) echo "Usage: live.sh [--target <metamask-extension>] [--out <recipes-dir>] [--cdp-port <port>] [--launch-existing-dist|--start-watch|--build-lavamoat|--external-artifact --dist-dir <absolute-dir> --artifact-provenance <absolute-file> [--preserve-profile]|--prepare-cmd <cmd>] [--launch-only] [--dist-dir dist/chrome] [--remote-flag KEY=VARIANT] [--artifacts-dir <dir>]"; exit 0 ;;
73
+ -h|--help) echo "Usage: live.sh [--target <metamask-extension>] [--out <recipes-dir>] [--cdp-port <port>] [--launch-existing-dist|--start-watch|--build-lavamoat|--external-artifact --dist-dir <absolute-dir> --artifact-provenance <absolute-file>|--prepare-cmd <cmd>] [--preserve-profile|--reset-profile] [--launch-only] [--dist-dir dist/chrome] [--remote-flag KEY=VARIANT] [--artifacts-dir <dir>]"; exit 0 ;;
73
74
  *) echo "Unknown arg: $1" >&2; exit 2 ;;
74
75
  esac
75
76
  done
@@ -86,10 +87,6 @@ if $EXTERNAL_ARTIFACT && [ ! -f "$ARTIFACT_PROVENANCE" ]; then
86
87
  echo "--external-artifact requires --artifact-provenance <file>" >&2
87
88
  exit 2
88
89
  fi
89
- if ! $RESET_PROFILE && ! $EXTERNAL_ARTIFACT; then
90
- echo "--preserve-profile is only valid for a verified external artifact" >&2
91
- exit 2
92
- fi
93
90
  if $EXTERNAL_ARTIFACT; then
94
91
  case "$ARTIFACT_PROVENANCE" in
95
92
  /*) ;;
@@ -200,11 +197,11 @@ if $LAUNCH_EXISTING_DIST && [ -z "$PREPARE_CMD" ]; then
200
197
  FIXTURE_STATE_ABS="$ARTIFACTS/fixture-state.json"
201
198
  FIXTURE_VALIDATION_ABS="$ARTIFACTS/logs/fixture-account-parity.json"
202
199
  WALLET_FIXTURE_ABS=""
203
- if ! $EXTERNAL_ARTIFACT; then
200
+ if $RESET_PROFILE && ! $EXTERNAL_ARTIFACT; then
204
201
  # Wallet fixture resolution chain + provenance live in seed-fixture.sh.
205
202
  WALLET_FIXTURE_ABS="$(bash "$SCRIPT_DIR/seed-fixture.sh" resolve --target "$TARGET" --cdp-port "$CDP_PORT" --source-out "$ARTIFACTS/logs/fixture-source.json")"
206
203
  fi
207
- if ! $EXTERNAL_ARTIFACT && [ ! -f "$ARTIFACTS/logs/fixture-source.json" ]; then
204
+ if $RESET_PROFILE && ! $EXTERNAL_ARTIFACT && [ ! -f "$ARTIFACTS/logs/fixture-source.json" ]; then
208
205
  # resolve failed before writing provenance (invalid RECIPE_WALLET_FIXTURE);
209
206
  # keep the documented missing-provenance artifact.
210
207
  node - "$ARTIFACTS/logs/fixture-source.json" "" <<'NODE'
@@ -370,6 +367,7 @@ display_args=(mm-harness runtime-launch --adapter extension --target "$TARGET" -
370
367
  $START_WATCH && display_args+=(--start-watch)
371
368
  $BUILD_LAVAMOAT && display_args+=(--build-lavamoat)
372
369
  $EXTERNAL_ARTIFACT && display_args+=(--external-artifact --dist-dir "$DIST_DIR" --artifact-provenance "$ARTIFACT_PROVENANCE")
370
+ $RESET_PROFILE && display_args+=(--reset-profile)
373
371
  $RESET_PROFILE || display_args+=(--preserve-profile)
374
372
  [ -n "$REMOTE_FLAGS" ] && display_args+=(--remote-flag "$REMOTE_FLAGS")
375
373
  [ -n "$CHROME_USER_DATA_DIR" ] && display_args+=(--chrome-user-data-dir "$CHROME_USER_DATA_DIR")
@@ -64,6 +64,7 @@ const NEEDLE_CODES = [
64
64
  ['pinned android device', BRIDGE_ERROR_CODES.NO_TARGET],
65
65
  ['no react native bridge target', BRIDGE_ERROR_CODES.NO_TARGET],
66
66
  ['cdp broker target unavailable', BRIDGE_ERROR_CODES.NO_TARGET],
67
+ ['cdp broker never observed the requested target', BRIDGE_ERROR_CODES.NO_TARGET],
67
68
  ['websocket closed', BRIDGE_ERROR_CODES.WS_CLOSED],
68
69
  ['websocket error', BRIDGE_ERROR_CODES.WS_CLOSED],
69
70
  ['cdp connection timeout', BRIDGE_ERROR_CODES.CDP_TIMEOUT],
@@ -60,6 +60,12 @@ if [ ! -f "$RUNNER_DIR/package.json" ]; then
60
60
  exit 1
61
61
  fi
62
62
 
63
+ if [ -L "$RUNNER_DIR/node_modules" ]; then
64
+ echo "mm-harness: cannot repair dependencies through a shared node_modules symlink." >&2
65
+ echo " Next: readlink '$RUNNER_DIR/node_modules' # inspect the shared target, then prepare a private dependency directory" >&2
66
+ exit 1
67
+ fi
68
+
63
69
  if [ -n "$PROTOCOL_ROOT" ] && [ -f "$RUNNER_DIR/scripts/link-local-farmslot.mjs" ]; then
64
70
  if [ -f "$PROTOCOL_ROOT/packages/recipe-harness/package.json" ]; then
65
71
  FARMSLOT_ROOT="$(cd "$PROTOCOL_ROOT" && pwd -P)" \
@@ -356,7 +356,7 @@ async function cdpCheck(target, cdpPort, pageMode) {
356
356
  try {
357
357
  const { checkExtensionRuntimeHealth } = await import("./runtime.js");
358
358
  const report = await checkExtensionRuntimeHealth(target, cdpPort, { pageMode });
359
- return report.status === "PASS" ? { status: "pass" } : { status: "fail", findings: report.findings };
359
+ return report.status === "PASS" ? { status: "pass" } : { status: "fail", findings: report.findings, errorCode: report.errorCode };
360
360
  } catch (error) {
361
361
  return { status: "fail", findings: [error instanceof Error ? error.message : String(error)] };
362
362
  }
@@ -441,6 +441,16 @@ async function decideExtensionReadiness(target, options = {}) {
441
441
  reasons: ["Build is fresh and the extension runtime is healthy over CDP."],
442
442
  actions: []
443
443
  },
444
+ {
445
+ when: cdp.errorCode === "EVM_RPC_EXECUTION_FAILED",
446
+ decision: "blocked",
447
+ reasonCode: "rpc-execution-failed",
448
+ reasons: [
449
+ "Background RPC fetch failed before an HTTP response; inspect its JavaScript stack before relaunching.",
450
+ ...cdp.findings?.slice(0, 3) ?? []
451
+ ],
452
+ actions: []
453
+ },
444
454
  {
445
455
  when: cdp.status === "fail",
446
456
  decision: "relaunch",
@@ -482,6 +492,13 @@ async function decideExtensionReadiness(target, options = {}) {
482
492
  reasons: [`Release artifact ${releaseArtifact.expectedVersion} is intact and healthy over CDP.`],
483
493
  actions: []
484
494
  },
495
+ {
496
+ when: cdp.errorCode === "EVM_RPC_EXECUTION_FAILED",
497
+ decision: "blocked",
498
+ reasonCode: "rpc-execution-failed",
499
+ reasons: ["Background RPC fetch failed before an HTTP response; inspect its JavaScript stack before relaunching.", ...cdp.findings?.slice(0, 3) ?? []],
500
+ actions: []
501
+ },
485
502
  {
486
503
  when: cdp.status === "fail",
487
504
  decision: "relaunch",
@@ -166,10 +166,11 @@ async function checkExtensionRuntimeHealth(projectRoot, cdpPort, options = {}) {
166
166
  if (runtime.ethereumConnectionUnavailable === true) {
167
167
  findings.push('Extension UI reports "Unable to connect to Ethereum".');
168
168
  }
169
+ const rpcExecutionFailed = runtime.evmRpcProbeOk !== true && /Failed to execute 'fetch' on 'WorkerGlobalScope': Illegal invocation/u.test(String(runtime.evmRpcProbeError));
169
170
  if (runtime.evmRpcProbeOk !== true) {
170
171
  const probeError = String(runtime.evmRpcProbeError ?? "unknown error").replace(/[.\s]+$/u, "");
171
172
  findings.push(
172
- `EVM RPC readiness probe failed: ${probeError}. Infura configured \u2260 RPC reachable.`
173
+ rpcExecutionFailed ? `EVM_RPC_EXECUTION_FAILED: background RPC fetch threw before an HTTP response: ${probeError}.` : `EVM RPC readiness probe failed: ${probeError}. Infura configured \u2260 RPC reachable.`
173
174
  );
174
175
  }
175
176
  if (runtime.hasSubmitRequest === true && runtime.backgroundProbeOk !== true) {
@@ -180,8 +181,8 @@ async function checkExtensionRuntimeHealth(projectRoot, cdpPort, options = {}) {
180
181
  return {
181
182
  status: findings.length === 0 ? "PASS" : "FAIL",
182
183
  ...evmRpcUnreachable ? {
183
- errorCode: "EVM_RPC_UNREACHABLE",
184
- userAction: evmRpcRecoveryAction(projectRoot)
184
+ errorCode: rpcExecutionFailed ? "EVM_RPC_EXECUTION_FAILED" : "EVM_RPC_UNREACHABLE",
185
+ userAction: rpcExecutionFailed ? `mm-harness logs --target ${shellQuote(projectRoot)} --full # inspect the background fetch stack; credential resync is not indicated by this error` : evmRpcRecoveryAction(projectRoot)
185
186
  } : {},
186
187
  warnings,
187
188
  cdpPort,
@@ -497,8 +498,7 @@ function formatHealthFailure(report, projectRoot) {
497
498
  ].join("\n");
498
499
  }
499
500
  function compositorRecoveryAction(projectRoot) {
500
- const recovery = `mm-harness launch --verify --target ${JSON.stringify(projectRoot)}`;
501
- return process.platform === "darwin" ? `Unlock the macOS session, then rerun: ${recovery}` : `Restore an active display compositor, then rerun: ${recovery}`;
501
+ return `Make the MetaMask tab visible in an unlocked desktop session, then rerun: mm-harness verify --adapter extension --target ${shellQuote(projectRoot)}`;
502
502
  }
503
503
  function evmRpcRecoveryAction(projectRoot) {
504
504
  return `Confirm ${shellQuote(path.join(projectRoot, ".metamaskrc"))} has a real INFURA_PROJECT_ID (not 00000000000), then rerun: mm-harness launch --verify --target ${shellQuote(projectRoot)}`;
@@ -16,6 +16,7 @@ function watcherStatus(buildLog) {
16
16
  }
17
17
  function extensionRuntimeNextAction(reasonCode, target) {
18
18
  const resolved = shellQuote(path.resolve(target));
19
+ if (reasonCode === "rpc-execution-failed") return `mm-harness logs --target ${resolved} --full`;
19
20
  return reasonCode === "deps-missing" || reasonCode === "deps-stale" ? `cd ${resolved} && yarn install --immutable` : `mm-harness launch --adapter extension --target ${resolved}`;
20
21
  }
21
22
  const extensionSurface = {
@@ -1,6 +1,7 @@
1
1
  import { execFile } from "node:child_process";
2
2
  import { AsyncLocalStorage } from "node:async_hooks";
3
3
  import { promisify } from "node:util";
4
+ const MOBILE_LIFECYCLE_PREPARED = /* @__PURE__ */ Symbol("mobileLifecyclePrepared");
4
5
  const MOBILE_LIFECYCLE_DEADLINE_FIELD = "_mobile_lifecycle_deadline_epoch_ms";
5
6
  const lifecycleDeadline = new AsyncLocalStorage();
6
7
  const execFileAsync = promisify(execFile);
@@ -64,7 +65,7 @@ function resolveMetaMaskMobileLifecycleTarget(node, rawContext) {
64
65
  appId: scalar(node.package_id ?? node.packageName ?? node.app_id, "app.lifecycle.package_id") ?? context.env.ANDROID_PACKAGE_ID ?? process.env.ANDROID_PACKAGE_ID ?? "io.metamask",
65
66
  launchUrl: opaqueRuntime ? void 0 : launchUrl,
66
67
  metroPort: opaqueRuntime ? void 0 : port,
67
- prelaunchCalls: opaqueRuntime ? [] : expoBundlePrewarmCalls(port, "android")
68
+ prelaunchCalls: opaqueRuntime || node[MOBILE_LIFECYCLE_PREPARED] ? [] : expoBundlePrewarmCalls(port, "android")
68
69
  };
69
70
  }
70
71
  return {
@@ -123,6 +124,7 @@ function scalar(value, label) {
123
124
  }
124
125
  export {
125
126
  MOBILE_LIFECYCLE_DEADLINE_FIELD,
127
+ MOBILE_LIFECYCLE_PREPARED,
126
128
  bindMetaMaskMobileLifecycleDeadline,
127
129
  createMetaMaskMobileLifecycleCommandRunner,
128
130
  resolveMetaMaskMobileLifecycleTarget
@@ -258,6 +258,8 @@ const PUBLIC_COMMAND_CONTRACTS = {
258
258
  options: options(HELP, JSON, TARGET, ADAPTER, DEVICE, MOBILE_PLATFORM, {
259
259
  "--cdp-port": value(),
260
260
  "--chrome-user-data-dir": value(),
261
+ "--reset-profile": bool(),
262
+ "--preserve-profile": bool(),
261
263
  "--artifacts-dir": value(),
262
264
  "--start-watch": bool(),
263
265
  "--build-lavamoat": bool(),
@@ -377,14 +377,7 @@ async function handleCall(argv) {
377
377
  persistRunEffects(result.summaryPath, result.artifactManifestPath, state);
378
378
  if (violation !== null) {
379
379
  const conciseFailure = violation.originalError ? conciseFailureForHuman(violation.originalError) : "";
380
- const example = describedAction ? actionExampleCommand(
381
- describedAction,
382
- adapter,
383
- target,
384
- process.env.MM_HARNESS_INVOKED_AS ?? process.env.MM_HARNESS_EXECUTABLE ?? "mm-harness",
385
- args
386
- ) : void 0;
387
- const taughtViolation = violation.code === "APP_LOGIC_FAILURE" && conciseFailure.includes(" requires ") && example ? { ...violation, userAction: example } : violation;
380
+ const taughtViolation = violation.code === "APP_LOGIC_FAILURE" && conciseFailure.includes(" requires ") && describedAction ? { ...violation, userAction: `mm-harness actions ${resolvedAction} --adapter ${adapter} --json` } : violation;
388
381
  return emitHealViolation(json, "call", result, taughtViolation, state, adapter);
389
382
  }
390
383
  const callOutput = readCallOutput(result.tracePath);
@@ -109,8 +109,13 @@ function applyDeviceTargeting(command, adapter, options, opts) {
109
109
  }
110
110
  return { ok: true };
111
111
  }
112
+ const discoveryErrors = [];
113
+ const discoveryFailure = () => {
114
+ const error = discoveryErrors.find((error2) => !platformPreference || error2.platform === platformPreference);
115
+ return error ? { ok: false, code: "DEVICE_DISCOVERY_FAILED", message: error.message, userAction: error.userAction } : void 0;
116
+ };
112
117
  if (device !== void 0) {
113
- const connected2 = listConnectedDevices();
118
+ const connected2 = listConnectedDevices(void 0, discoveryErrors);
114
119
  const setResolvedDevice = (matched) => {
115
120
  if (platformPreference && matched.platform !== platformPreference) {
116
121
  return {
@@ -130,10 +135,12 @@ function applyDeviceTargeting(command, adapter, options, opts) {
130
135
  if (androidById) {
131
136
  return setResolvedDevice(androidById);
132
137
  }
133
- const iosById = connected2.find((d) => d.platform === "ios" && d.id === device) ?? (command === "launch" && platformPreference === "ios" ? findAvailableIosSimulatorById(device) : void 0);
138
+ const iosById = connected2.find((d) => d.platform === "ios" && d.id === device) ?? (command === "launch" && platformPreference === "ios" ? findAvailableIosSimulatorById(device, discoveryErrors) : void 0);
134
139
  if (iosById) {
135
140
  return setResolvedDevice(iosById);
136
141
  }
142
+ const probeFailure2 = discoveryFailure();
143
+ if (probeFailure2) return probeFailure2;
137
144
  const normalizedDevice = normalizeDeviceName(device);
138
145
  const byName = connected2.filter((d) => {
139
146
  const name = d.name ?? "";
@@ -161,7 +168,9 @@ ${formatConnectedDevices(connected2)}` : ` No devices are currently connected (
161
168
  userAction: deviceRecovery(options, opts.rerun)
162
169
  };
163
170
  }
164
- const connected = listConnectedDevices();
171
+ const connected = listConnectedDevices(void 0, discoveryErrors);
172
+ const probeFailure = discoveryFailure();
173
+ if (probeFailure) return opts.gate || command === "launch" ? probeFailure : { ok: true };
165
174
  const targetable = connected.filter(isTargetable);
166
175
  if (platformPreference) {
167
176
  const selectedTargetable2 = targetable.filter(deviceSelected);
@@ -188,6 +188,7 @@ async function handleDoctor({ options }) {
188
188
  const liveView = deviceView ? await mobileDeviceLiveView(target, deviceView) : null;
189
189
  const devices = liveView?.devicesWithLive ?? deviceView?.devices ?? [];
190
190
  const additionalReachableDevices = liveView?.additionalReachableDevices ?? [];
191
+ const discovery = deviceView?.deviceDiscoveryErrors.length ? { deviceDiscoveryErrors: deviceView.deviceDiscoveryErrors } : {};
191
192
  if (expectLive) {
192
193
  return emitExpectLive(
193
194
  adapter,
@@ -199,15 +200,16 @@ async function handleDoctor({ options }) {
199
200
  devices,
200
201
  additionalReachableDevices,
201
202
  json,
202
- printReady
203
+ printReady,
204
+ discovery
203
205
  );
204
206
  }
205
207
  const doctorUserAction = doctorResult.checks.find(
206
208
  (check) => check.required && check.status === "fail" && check.userAction
207
209
  )?.userAction ?? `mm-harness doctor --fix --adapter ${adapter} --target ${shellQuote(target)} --json`;
208
210
  const doctorError = doctorResult.status === "fail" ? { code: "DOCTOR_CHECKS_FAILED", message: "one or more required doctor checks failed", userAction: doctorUserAction } : void 0;
209
- const next = doctorError?.userAction ?? (runtime?.decision === "ready" ? void 0 : runtime?.nextAction);
210
- if (json) console.log(JSON.stringify({ ...doctorResult, ready: runtime?.decision === "ready", runtime, orphanMetros, capture, devices, additionalReachableDevices, ...next ? { next } : {}, ...doctorError ? { error: doctorError } : {} }, null, 2));
211
+ const next = doctorError?.userAction ?? discovery.deviceDiscoveryErrors?.[0]?.userAction ?? (runtime?.decision === "ready" ? void 0 : runtime?.nextAction);
212
+ if (json) console.log(JSON.stringify({ ...doctorResult, ready: runtime?.decision === "ready", runtime, orphanMetros, capture, devices, additionalReachableDevices, ...discovery, ...next ? { next } : {}, ...doctorError ? { error: doctorError } : {} }, null, 2));
211
213
  else {
212
214
  const out = (style, text) => color(style, text, { stream: process.stdout });
213
215
  const stateStyle = (value, good) => value === good ? "ok" : "warn";
@@ -298,7 +300,7 @@ function emitExpectLiveVerdict(adapter, target, runtime, verdict) {
298
300
  for (const reason of runtime?.reasons ?? []) console.error(` ${out("dim", reason)}`);
299
301
  console.error(` Next: ${runtime?.nextAction ?? getAdapterSurface(adapter).hints.runtimeProbeRecovery(target)}`);
300
302
  }
301
- function emitExpectLive(adapter, target, runtime, result, orphanMetros, capture, devices, additionalReachableDevices, json, printReady) {
303
+ function emitExpectLive(adapter, target, runtime, result, orphanMetros, capture, devices, additionalReachableDevices, json, printReady, discovery) {
302
304
  const live = runtime?.decision === "ready";
303
305
  if (printReady) {
304
306
  if (live) {
@@ -316,7 +318,7 @@ function emitExpectLive(adapter, target, runtime, result, orphanMetros, capture,
316
318
  if (json) {
317
319
  const userAction = runtime?.nextAction ?? getAdapterSurface(adapter).hints.runtimeProbeRecovery(target);
318
320
  const error = live ? void 0 : { code: "RUNTIME_NOT_LIVE", message: `${adapter} runtime is not live`, userAction };
319
- console.log(JSON.stringify({ ...result, ready: live, runtime, orphanMetros, capture, devices, additionalReachableDevices, ...error ? { error } : {} }, null, 2));
321
+ console.log(JSON.stringify({ ...result, ready: live, runtime, orphanMetros, capture, devices, additionalReachableDevices, ...discovery, ...error ? { error } : {} }, null, 2));
320
322
  return live ? EXIT.ok : EXIT.runtime;
321
323
  }
322
324
  const out = (style, text) => color(style, text, { stream: process.stdout });
@@ -328,7 +328,7 @@ async function applyExtensionFixture(target, fixturePath, canonicalFixture, json
328
328
  process.stderr.write(`${colorHumanMessage(`\u2192 fixtures ${action} extension \u2014 resetting and seeding the slot-owned profile from the existing build\u2026`)}
329
329
  `);
330
330
  const liveScript = path.join(runnerDir, "adapters/extension/live.sh");
331
- const args = ["--target", target, "--launch-existing-dist", "--launch-only"];
331
+ const args = ["--target", target, "--launch-existing-dist", "--launch-only", "--reset-profile"];
332
332
  if (process.env.CDP_PORT) args.push("--cdp-port", process.env.CDP_PORT);
333
333
  const result = await spawnScriptStreaming(liveScript, args, target, {
334
334
  env: { RECIPE_WALLET_FIXTURE: fixturePath },
@@ -310,6 +310,16 @@ async function handleLaunchLocked(argv, stream) {
310
310
  exitCode: EXIT.infra
311
311
  });
312
312
  }
313
+ if (adapter === "extension" && attempt.output.includes("EVM_RPC_EXECUTION_FAILED:")) {
314
+ return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
315
+ code: "EVM_RPC_EXECUTION_FAILED",
316
+ message: "Background RPC fetch failed during JavaScript execution; this does not establish an Infura authentication or connectivity failure.",
317
+ recoverable: false,
318
+ userAction: `mm-harness logs --target ${shellQuote(target)} --full`,
319
+ exitCode: EXIT.runtime,
320
+ originalError: attempt.output.trim() || void 0
321
+ });
322
+ }
313
323
  if (adapter === "extension" && extensionEvmRpcUnreachable(attempt.output)) {
314
324
  return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
315
325
  code: "EVM_RPC_UNREACHABLE",
@@ -358,10 +358,11 @@ function renderHumanActionCatalog(actions, options) {
358
358
  for (const entry of entries) {
359
359
  const fieldNames = humanFieldNames(entry);
360
360
  const fields = fieldNames.length ? ` ${out("dim", `fields=${fieldNames.join(",")}`)}` : "";
361
+ const cases = entry.result_cases?.length ? ` ${out("dim", `cases=${entry.result_cases.join(",")}`)}` : "";
361
362
  const description = entry.description ? ` ${out("comment", `\u2014 ${entry.description}`)}` : "";
362
363
  const source = ` ${out("dim", `[${entry.source}]`)}`;
363
364
  const risk = entry.capabilities.length ? ` ${out("dim", `risk=${entry.capabilities.join(",")}`)}` : "";
364
- lines.push(` ${out("cmd", options.displayName?.(entry) ?? entry.name)}${source}${fields}${risk}${description}`);
365
+ lines.push(` ${out("cmd", options.displayName?.(entry) ?? entry.name)}${source}${fields}${cases}${risk}${description}`);
365
366
  }
366
367
  }
367
368
  return lines.join("\n");
@@ -519,6 +520,7 @@ function describeManifestAction(name, kind, metadata, source) {
519
520
  fields: properties,
520
521
  schema,
521
522
  examples: record.examples,
523
+ ...Array.isArray(record.result_cases) ? { result_cases: record.result_cases.filter((value) => typeof value === "string") } : {},
522
524
  source: source?.name ?? (kind === "official" ? "official" : "metamask"),
523
525
  sourceTier: source?.tier ?? (kind === "official" ? "official" : "canonical"),
524
526
  sourceManifest: source?.manifestPath ?? "",
@@ -4,10 +4,12 @@ import { listConnectedDevices } from "../devices.js";
4
4
  import { renderDeviceList, scopedDevices } from "./device-target.js";
5
5
  import { probeMobileLiveState } from "./status-probe.js";
6
6
  function mobileDeviceView(allDevices) {
7
- const allConnectedDevices = listConnectedDevices();
7
+ const deviceDiscoveryErrors = [];
8
+ const allConnectedDevices = listConnectedDevices(void 0, deviceDiscoveryErrors);
8
9
  return {
9
10
  allConnectedDevices,
10
- devices: scopedDevices(allConnectedDevices, allDevices)
11
+ devices: scopedDevices(allConnectedDevices, allDevices),
12
+ deviceDiscoveryErrors
11
13
  };
12
14
  }
13
15
  async function mobileDeviceLiveView(target, view) {
@@ -39,7 +41,15 @@ function detectAdditionalReachableDevices(scoped, allConnected, liveMap) {
39
41
  }).filter((device) => device !== null);
40
42
  }
41
43
  function renderMobileDeviceList(view, out) {
42
- renderDeviceList(view.devices, out);
44
+ if (view.devices.length === 0 && view.deviceDiscoveryErrors.length > 0) {
45
+ console.log(`${out("label", "devices:")} ${out("warn", "discovery incomplete")}`);
46
+ } else {
47
+ renderDeviceList(view.devices, out);
48
+ }
49
+ for (const error of view.deviceDiscoveryErrors) {
50
+ console.log(`${out("warn", error.message)}
51
+ Next: ${error.userAction}`);
52
+ }
43
53
  }
44
54
  function renderAdditionalReachableDevices(devices, out) {
45
55
  console.log(`${out("warn", "additional reachable target:")} ${devices.length} device(s) answered on this Metro but are not the active target for this command`);
@@ -24,6 +24,8 @@ function parseArgs(argv, command) {
24
24
  "launchExistingDist",
25
25
  "startWatch",
26
26
  "buildLavamoat",
27
+ "resetProfile",
28
+ "preserveProfile",
27
29
  "record",
28
30
  "plan",
29
31
  "list",
@@ -582,6 +582,11 @@ async function validateRecipeAdapterAware(adapter, recipe, manifest, librarySour
582
582
  );
583
583
  const findings = [
584
584
  ...withManifest.findings,
585
+ ...withManifest.status === "valid" && params ? protocol.validateRecipeWithManifest(
586
+ resolveMetaMaskParameterValue(recipe, params),
587
+ manifest,
588
+ validationOptions
589
+ ).findings : [],
585
590
  ...validateMetaMaskActionInputs(recipe, adapter, params)
586
591
  ];
587
592
  if (withManifest.status === "valid" && isRecord(recipe) && libraryResolution) {
@@ -626,6 +631,11 @@ async function validateRecipeAdapterAware(adapter, recipe, manifest, librarySour
626
631
  supplied,
627
632
  isRecord(dependency.document) ? dependency.document.paramsSchema : void 0
628
633
  );
634
+ findings.push(...protocol.validateRecipeWithManifest(
635
+ resolveMetaMaskParameterValue(dependency.document, childParams),
636
+ manifest,
637
+ validationOptions
638
+ ).findings);
629
639
  findings.push(...validateMetaMaskActionInputs(dependency.document, adapter, childParams));
630
640
  validateDependencyInstances(dependency.document, childParams);
631
641
  }
@@ -690,9 +700,24 @@ async function listRunnableRecipes(adapter, librarySources) {
690
700
  const protocol = await importRecipeProtocol();
691
701
  const { manifest } = await resolveActionManifest(adapter, void 0, sources);
692
702
  const externalRecipeIds = new Set(resolution.recipes.keys());
693
- return [...resolution.recipes.values()].filter((recipe) => protocol.validateRecipeWithManifest(recipe.document, manifest, {
703
+ const validRefs = new Set([...resolution.recipes.values()].filter((recipe) => protocol.validateRecipeWithManifest(recipe.document, manifest, {
694
704
  externalRecipeIds
695
- }).status === "valid").map(
705
+ }).status === "valid").map((recipe) => recipe.ref));
706
+ return [...resolution.recipes.values()].filter((recipe) => {
707
+ if (!validRefs.has(recipe.ref)) return false;
708
+ try {
709
+ const dependencies = harness.resolveRecipeDependencies({
710
+ rootRef: recipe.ref,
711
+ root: recipe.document,
712
+ rootSource: recipe.provenance,
713
+ recipes: resolution.recipes
714
+ });
715
+ return [...dependencies.recipes.keys()].every((ref) => validRefs.has(ref));
716
+ } catch (error) {
717
+ if (error instanceof harness.RecipeResolutionError) return false;
718
+ throw error;
719
+ }
720
+ }).map(
696
721
  (recipe) => recipeSummary(recipe.ref, adapter, recipe.document, recipe)
697
722
  ).sort((left, right) => left.name.localeCompare(right.name));
698
723
  }
@@ -1371,14 +1396,14 @@ function readRunFailureText(result) {
1371
1396
  }
1372
1397
  async function executeWithHealBounds(exec, target, state) {
1373
1398
  const result = await exec();
1374
- if (result.status === "pass") return { result, violation: null };
1399
+ if (result.status === "pass" || result.status === "unknown") return { result, violation: null };
1375
1400
  return {
1376
1401
  result,
1377
1402
  violation: checkHealBounds(target, readRunFailureText(result), state)
1378
1403
  };
1379
1404
  }
1380
1405
  function emitHealViolation(json, command, result, violation, state, adapter) {
1381
- const userAction = adapter === "core" && violation.code === "WALLET_STATE_REQUIRED" ? 'create temp/recipe/runtime/wallet-fixture.json, or add "account": "<0x\u2026>" to the node block in the recipe' : violation.userAction ?? `inspect ${shellQuoteArg(result.summaryPath)} and ${shellQuoteArg(result.tracePath)}; fix the application or recipe failure before retrying`;
1406
+ const userAction = adapter === "core" && violation.code === "WALLET_STATE_REQUIRED" ? "for signing, configure a reserved wallet-fixture.json account and pass its non-empty name; an explicit 0x address is supported only by read-only actions" : violation.userAction ?? `inspect ${shellQuoteArg(result.summaryPath)} and ${shellQuoteArg(result.tracePath)}; fix the application or recipe failure before retrying`;
1382
1407
  if (json) {
1383
1408
  console.log(
1384
1409
  JSON.stringify(
@@ -396,7 +396,7 @@ async function handleRunInner({ positional, options }, stream) {
396
396
  userAction: failureUserAction
397
397
  });
398
398
  }
399
- stream.complete(result.status === "pass" ? "pass" : "fail", exitCode, {
399
+ stream.complete(result.status, exitCode, {
400
400
  adapter,
401
401
  reportPath: report.path,
402
402
  summaryPath: result.summaryPath,
@@ -34,15 +34,20 @@ async function handleRuntimeLaunch({ options }) {
34
34
  --device <serial> Explicit Android device serial
35
35
  --cdp-port <port> Extension CDP port
36
36
  --chrome-user-data-dir Isolated Chrome profile path
37
+ --reset-profile Explicitly clear the selected Extension profile
37
38
  --artifacts-dir <dir> Validation evidence directory
38
39
  --json Machine-readable terminal report
39
40
 
40
41
  The harness verifies and runs a local artifact but never downloads one. Extension
41
- launch resets an isolated profile. Android install requires an explicit serial.`);
42
+ launch preserves the selected profile unless --reset-profile is requested.
43
+ Android install requires an explicit serial.`);
42
44
  return 0;
43
45
  }
44
46
  const adapter = adapterOption(options);
45
47
  const target = targetPath(options);
48
+ if (adapter !== "extension" && (optionFlag(options, "resetProfile") || optionFlag(options, "preserveProfile"))) {
49
+ throw usageError("Profile options apply only to Extension.\nNext: mm-harness runtime-launch --help");
50
+ }
46
51
  const artifactFile = optionString(options, "artifactFile");
47
52
  const artifactVersion = optionString(options, "artifactVersion");
48
53
  const artifactSha256 = optionString(options, "artifactSha256");
@@ -68,6 +73,9 @@ async function handleRuntimeLaunch({ options }) {
68
73
  });
69
74
  }
70
75
  if (adapter !== "extension") throw new Error("runtime-launch currently applies to the extension adapter.");
76
+ if (optionFlag(options, "resetProfile") && optionFlag(options, "preserveProfile")) {
77
+ throw usageError("Choose either --reset-profile or --preserve-profile, not both.\nNext: mm-harness runtime-launch --help");
78
+ }
71
79
  if (artifactBuildId || artifactPackageId || optionString(options, "platform") || optionString(options, "device")) {
72
80
  throw usageError("Extension runtime-launch does not accept Android artifact options.");
73
81
  }
@@ -119,6 +127,7 @@ async function handleRuntimeLaunch({ options }) {
119
127
  );
120
128
  }
121
129
  if (chromeUserDataDir) command.push("--chrome-user-data-dir", chromeUserDataDir);
130
+ command.push(optionFlag(options, "resetProfile") ? "--reset-profile" : "--preserve-profile");
122
131
  const remoteFlag = optionString(options, "remoteFlag");
123
132
  if (remoteFlag) command.push("--remote-flag", remoteFlag);
124
133
  if (!fs.existsSync(liveScript)) {
@@ -37,6 +37,8 @@ async function handleStatus({ options }) {
37
37
  const next = surface.hints.relaunch;
38
38
  const deviceView = adapter === "mobile" ? mobileDeviceView(allDevices) : null;
39
39
  const devices = deviceView?.devices ?? [];
40
+ const discovery = deviceView?.deviceDiscoveryErrors.length ? { deviceDiscoveryErrors: deviceView.deviceDiscoveryErrors } : {};
41
+ const discoveryNext = discovery.deviceDiscoveryErrors?.[0]?.userAction;
40
42
  if (json) {
41
43
  const doProbe = !fast && adapter === "mobile" && deviceView !== null && deviceView.allConnectedDevices.length > 0;
42
44
  if (doProbe) {
@@ -49,9 +51,10 @@ async function handleStatus({ options }) {
49
51
  adapter,
50
52
  target,
51
53
  devices: liveView.devicesWithLive,
54
+ ...discovery,
52
55
  ...liveView.additionalReachableDevices.length > 0 ? { additionalReachableDevices: liveView.additionalReachableDevices } : {},
53
56
  ...liveView.androidPortHints.length > 0 ? { androidPortHints: liveView.androidPortHints } : {},
54
- next: nextForLive(next, liveView.liveMap)
57
+ next: discoveryNext ?? nextForLive(next, liveView.liveMap)
55
58
  },
56
59
  null,
57
60
  2
@@ -67,8 +70,9 @@ async function handleStatus({ options }) {
67
70
  adapter,
68
71
  target,
69
72
  devices,
73
+ ...discovery,
70
74
  ...runtime ? { runtime } : {},
71
- next: runtime ? nextForRuntime(surface.hints.launch, surface.hints.relaunch, runtime) : next
75
+ next: discoveryNext ?? (runtime ? nextForRuntime(surface.hints.launch, surface.hints.relaunch, runtime) : next)
72
76
  },
73
77
  null,
74
78
  2
@@ -85,13 +89,13 @@ async function handleStatus({ options }) {
85
89
  if (liveView.additionalReachableDevices.length > 0) renderAdditionalReachableDevices(liveView.additionalReachableDevices, out);
86
90
  renderMobileLiveBlock(devices, liveView.liveMap, out);
87
91
  renderAndroidPortHints(liveView.androidPortHints, out);
88
- console.log(`${out("label", "Next:")} ${out("cmd", nextForLive(next, liveView.liveMap))}`);
92
+ console.log(`${out("label", "Next:")} ${out("cmd", discoveryNext ?? nextForLive(next, liveView.liveMap))}`);
89
93
  } else if (!fast && adapter === "extension") {
90
94
  const runtime = await surface.runtimeStatus(target);
91
95
  renderRuntimeStatus(runtime, out);
92
96
  console.log(`${out("label", "Next:")} ${out("cmd", nextForRuntime(surface.hints.launch, surface.hints.relaunch, runtime))}`);
93
97
  } else {
94
- console.log(`${out("label", "Next:")} ${out("cmd", next)}`);
98
+ console.log(`${out("label", "Next:")} ${out("cmd", discoveryNext ?? next)}`);
95
99
  }
96
100
  return EXIT.ok;
97
101
  }