@deeeed/metamask-harness 0.47.3 → 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 (148) hide show
  1. package/CHANGELOG.md +12 -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/mobile/bridge-runtime/lib/cdp-broker.cjs +4 -0
  7. package/adapters/mobile/reload-app.mjs +92 -28
  8. package/adapters/shared/ensure-runner-deps.sh +6 -0
  9. package/dist/adapters/extension/runtime-decision.js +18 -1
  10. package/dist/adapters/extension/runtime.js +5 -5
  11. package/dist/adapters/extension/surface.js +1 -0
  12. package/dist/app-lifecycle.js +3 -1
  13. package/dist/command-contract.js +2 -0
  14. package/dist/commands/call.js +1 -8
  15. package/dist/commands/device-target.js +12 -3
  16. package/dist/commands/doctor.js +7 -5
  17. package/dist/commands/fixtures.js +1 -1
  18. package/dist/commands/launch/index.js +10 -0
  19. package/dist/commands/manifest.js +3 -1
  20. package/dist/commands/mobile-device-view.js +13 -3
  21. package/dist/commands/parse-args.js +2 -0
  22. package/dist/commands/run-engine.js +29 -4
  23. package/dist/commands/run.js +1 -1
  24. package/dist/commands/runtime-launch.js +10 -1
  25. package/dist/commands/status.js +8 -4
  26. package/dist/devices.js +31 -9
  27. package/dist/execution-provenance.js +8 -7
  28. package/dist/funding-execution-context.js +43 -13
  29. package/dist/heal-bounds.js +3 -3
  30. package/dist/live-adapter-contract.js +4 -0
  31. package/dist/manifest.js +2 -13
  32. package/dist/mm-harness-cli.js +2 -1
  33. package/dist/runner.js +14 -3
  34. package/docs/RECIPES.md +133 -22
  35. package/docs/RELEASE-QA-CAPABILITY-MAP.md +3 -2
  36. package/library/actions/core/perps/_controller.mjs +10 -0
  37. package/library/actions/core/perps/close_orders.mjs +13 -17
  38. package/library/actions/extension/assets/home-token-identity.mjs +30 -0
  39. package/library/actions/extension/assets/import_custom_token.mjs +61 -79
  40. package/library/actions/extension/assets/open_details.mjs +18 -7
  41. package/library/actions/extension/assets/read_details.mjs +61 -29
  42. package/library/actions/extension/assets/read_visible_state.mjs +4 -1
  43. package/library/actions/extension/assets/select_network_scope.mjs +16 -9
  44. package/library/actions/extension/assets/set_token_visibility.mjs +84 -17
  45. package/library/actions/extension/assets/verify_sorting.mjs +62 -14
  46. package/library/actions/extension/networks/add_custom.mjs +19 -17
  47. package/library/actions/extension/networks/custom_network.mjs +8 -3
  48. package/library/actions/extension/networks/read_visible_state.mjs +1 -0
  49. package/library/actions/extension/performance/_navigation-memory.mjs +35 -6
  50. package/library/actions/extension/performance/compare_idle_navigation_memory.mjs +3 -2
  51. package/library/actions/extension/performance/measure_detached_dom.mjs +3 -2
  52. package/library/actions/extension/performance/measure_navigation_memory.mjs +3 -2
  53. package/library/actions/extension/perps/assert_visible_consistency.mjs +9 -4
  54. package/library/actions/extension/perps/close_visible_position.mjs +3 -3
  55. package/library/actions/extension/perps/edit_margin.mjs +1 -1
  56. package/library/actions/extension/perps/open_balance_action.mjs +16 -7
  57. package/library/actions/extension/perps/perps.mjs +150 -45
  58. package/library/actions/extension/perps/read_funds_confirmation.mjs +8 -0
  59. package/library/actions/extension/perps/read_snapshot.mjs +2 -1
  60. package/library/actions/extension/perps/read_visible_state.mjs +10 -6
  61. package/library/actions/extension/perps/search_markets.mjs +7 -2
  62. package/library/actions/extension/perps/select_activity_filter.mjs +1 -1
  63. package/library/actions/extension/perps/select_market_filter.mjs +74 -45
  64. package/library/actions/extension/perps/set_market_favorite.mjs +6 -3
  65. package/library/actions/extension/perps/state.mjs +1 -0
  66. package/library/actions/extension/perps/update_position_tpsl.mjs +1 -1
  67. package/library/actions/extension/perps/visible-mutation-identity.mjs +4 -2
  68. package/library/actions/extension/platform/cdp.mjs +23 -142
  69. package/library/actions/extension/swap_bridge/quote-state.mjs +54 -0
  70. package/library/actions/extension/swap_bridge/read_visible_state.mjs +4 -1
  71. package/library/actions/extension/swap_bridge/select_assets.mjs +11 -1
  72. package/library/actions/extension/swap_bridge/set_amount.mjs +10 -8
  73. package/library/actions/extension/swap_bridge/set_max_amount.mjs +17 -18
  74. package/library/actions/extension/swap_bridge/set_slippage.mjs +0 -9
  75. package/library/actions/extension/ui/navigate.mjs +58 -14
  76. package/library/actions/extension/wallet/lock.mjs +2 -1
  77. package/library/actions/mobile/assets/import_custom_token.mjs +1 -0
  78. package/library/actions/mobile/assets/read_visible_state.mjs +3 -1
  79. package/library/actions/mobile/assets/set_token_visibility.mjs +13 -9
  80. package/library/actions/mobile/networks/network-management.mjs +22 -15
  81. package/library/actions/mobile/networks/remove_custom.mjs +6 -2
  82. package/library/actions/mobile/perps/perps.mjs +3 -0
  83. package/library/actions/mobile/perps/read-visible-state-loop.mjs +1 -0
  84. package/library/actions/mobile/perps/set_market_favorite.mjs +15 -19
  85. package/library/actions/mobile/platform/native-session.mjs +26 -17
  86. package/library/actions/mobile/platform/observe-ui.mjs +2 -2
  87. package/library/actions/mobile/ui/navigate.mjs +1 -1
  88. package/library/actions/shared/perps/visible-state.mjs +14 -3
  89. package/library/manifests/core.action-manifest.json +14 -2
  90. package/library/manifests/extension.action-manifest.json +49 -16
  91. package/library/manifests/mobile.action-manifest.json +8 -7
  92. package/library/recipes/core/perps/smoke.recipe.json +23 -0
  93. package/library/recipes/core/perps/snapshot.recipe.json +26 -1
  94. package/library/recipes/extension/assets/release-custom-network-token-persistence.recipe.json +25 -8
  95. package/library/recipes/extension/performance/navigation-memory-suite.recipe.json +219 -0
  96. package/library/recipes/extension/performance/navigation-memory.recipe.json +61 -156
  97. package/library/recipes/extension/wallet/smoke.recipe.json +17 -8
  98. package/library/recipes/mobile/app/lifecycle.android-smoke.recipe.json +23 -4
  99. package/library/recipes/mobile/networks/release-custom-network-persistence.recipe.json +31 -6
  100. package/library/recipes/mobile/networks/release-multichain-visibility.recipe.json +5 -4
  101. package/library/recipes/mobile/wallet/smoke.recipe.json +17 -8
  102. package/library/recipes/{assets → shared/assets}/release-token-address-import.recipe.json +7 -0
  103. package/library/recipes/{assets → shared/assets}/release-token-details.recipe.json +2 -2
  104. package/library/recipes/shared/assets/release-token-hide-readd.recipe.json +91 -0
  105. package/library/recipes/{assets → shared/assets}/release-token-persistence.recipe.json +3 -0
  106. package/library/recipes/{assets → shared/assets}/release-token-search-import.recipe.json +4 -1
  107. package/library/recipes/{assets → shared/assets}/release-token-sorting.recipe.json +13 -2
  108. package/library/recipes/{swap-bridge → shared/swap-bridge}/release-quote.recipe.json +29 -3
  109. package/package.json +3 -3
  110. package/site/assets/help-recipes.json +63 -23
  111. package/library/recipes/assets/release-token-hide-readd.recipe.json +0 -57
  112. package/library/recipes/core/perps/clean-market-testnet.recipe.json +0 -44
  113. package/library/recipes/core/perps/order-lifecycle.recipe.json +0 -82
  114. package/library/recipes/core/perps/read-markets.recipe.json +0 -36
  115. package/library/recipes/core/perps/trading-lifecycle.recipe.json +0 -80
  116. package/library/recipes/extension/perps/release-activity-filters.recipe.json +0 -51
  117. package/library/recipes/extension/perps/release-funds-flows.recipe.json +0 -76
  118. package/library/recipes/extension/perps/release-live-limit-order.recipe.json +0 -109
  119. package/library/recipes/extension/perps/release-market-filters.recipe.json +0 -97
  120. package/library/recipes/extension/perps/release-order-entry.recipe.json +0 -158
  121. package/library/recipes/extension/perps/release-order-validation.recipe.json +0 -259
  122. package/library/recipes/extension/perps/release-visible-consistency.recipe.json +0 -47
  123. package/library/recipes/extension/perps/source-dev-snapshot-consistency.recipe.json +0 -76
  124. package/library/recipes/mobile/perps/chase-assert-running.recipe.json +0 -110
  125. package/library/recipes/mobile/perps/chase-place.recipe.json +0 -145
  126. package/library/recipes/mobile/perps/chase-terminate.recipe.json +0 -98
  127. package/library/recipes/mobile/perps/performance.recipe.json +0 -722
  128. package/library/recipes/mobile/perps/pro-order-setup.recipe.json +0 -111
  129. package/library/recipes/mobile/perps/pro-order-start-state.recipe.json +0 -53
  130. package/library/recipes/mobile/perps/scale-assert-orders.recipe.json +0 -126
  131. package/library/recipes/mobile/perps/scale-place.recipe.json +0 -186
  132. package/library/recipes/mobile/perps/twap-assert-active.recipe.json +0 -97
  133. package/library/recipes/mobile/perps/twap-place.recipe.json +0 -146
  134. package/library/recipes/mobile/runner/action-validation.recipe.json +0 -336
  135. package/library/recipes/perps/clean-market-testnet.recipe.json +0 -49
  136. package/library/recipes/perps/lifecycle.recipe.json +0 -144
  137. package/library/recipes/perps/release-live-market-order.recipe.json +0 -139
  138. package/library/recipes/perps/release-watchlist.recipe.json +0 -111
  139. package/library/recipes/swap-bridge/release-max-quote.recipe.json +0 -72
  140. /package/library/recipes/{assets → shared/assets}/release-token-list.recipe.json +0 -0
  141. /package/library/recipes/{perps → shared/perps}/release-market-details.recipe.json +0 -0
  142. /package/library/recipes/{perps → shared/perps}/release-market-search.recipe.json +0 -0
  143. /package/library/recipes/{swap-bridge → shared/swap-bridge}/release-custom-slippage.recipe.json +0 -0
  144. /package/library/recipes/{swap-bridge → shared/swap-bridge}/release-live-max-swap.recipe.json +0 -0
  145. /package/library/recipes/{swap-bridge → shared/swap-bridge}/release-live-swap.recipe.json +0 -0
  146. /package/library/recipes/{swap-bridge → shared/swap-bridge}/release-surface.recipe.json +0 -0
  147. /package/library/recipes/{wallet → shared/wallet}/import.recipe.json +0 -0
  148. /package/library/recipes/{wallet → shared/wallet}/reset-import.recipe.json +0 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
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
+
11
+ ## 0.47.4 - 2026-09-05
12
+
13
+ ### Fixed
14
+
15
+ - Verify Mobile reloads through execution-context replacement when React Native retains its Hermes target ID, and advance broker runtime identity on context reset for lifecycle readiness checks.
16
+
5
17
  ## 0.47.3 - 2026-09-05
6
18
 
7
19
  ### 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],
@@ -862,6 +862,10 @@ function createCdpBroker({
862
862
  }
863
863
  },
864
864
  onCdpEvent(deviceId, method, params) {
865
+ if (method === 'Runtime.executionContextsCleared') {
866
+ const target = knownTargets.get(deviceId);
867
+ if (target) knownTargets.set(deviceId, { ...target, generation: target.generation + 1 });
868
+ }
865
869
  for (const socket of clients) {
866
870
  if (subscriptions.get(socket)?.has(`${deviceId}\u0000${method}`)) {
867
871
  writeMessage(socket, { type: 'event', deviceId, method, params });
@@ -1,12 +1,54 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { spawnSync } from 'node:child_process';
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
4
6
  import { createRequire } from 'node:module';
5
7
  import { resolveMobileToolPath } from '../../library/actions/mobile/platform/tool-paths.mjs';
6
8
 
7
9
  const require = createRequire(import.meta.url);
8
10
  const WebSocket = require('ws');
9
- const { discoverTarget } = require('./bridge-runtime/lib/target-discovery.cjs');
11
+ const { discoverTarget, rankRuntimeCandidates } = require('./bridge-runtime/lib/target-discovery.cjs');
12
+ const { brokerSocketPath, createBrokerClient, deviceIdFromUrl } = require('./bridge-runtime/lib/cdp-broker.cjs');
13
+ const { createWSClient } = require('./bridge-runtime/lib/ws-client.cjs');
14
+
15
+ async function observeRuntimeReset(port, target) {
16
+ const socketPath = brokerSocketPath(
17
+ process.env.RECIPE_RUNTIME_DIR || path.join(process.cwd(), 'temp/recipe/runtime'),
18
+ port,
19
+ );
20
+ const client = fs.existsSync(socketPath)
21
+ ? await createBrokerClient(socketPath, deviceIdFromUrl(target.wsUrl), 5000)
22
+ : await createWSClient(target.wsUrl, 5000);
23
+ let armed = false;
24
+ let cleared = false;
25
+ let recreated = false;
26
+ client.on('Runtime.executionContextsCleared', () => {
27
+ if (armed) cleared = true;
28
+ });
29
+ client.on('Runtime.executionContextCreated', () => {
30
+ if (armed && cleared) recreated = true;
31
+ });
32
+ try {
33
+ await client.send('Runtime.enable');
34
+ // Finish subscription/bootstrap before the reload request can generate events.
35
+ await client.send('Runtime.evaluate', { expression: 'true', returnByValue: true });
36
+ armed = true;
37
+ } catch (error) {
38
+ client.close();
39
+ throw error;
40
+ }
41
+ return {
42
+ get recreated() { return recreated; },
43
+ async responsive(timeoutMs) {
44
+ const result = await client.send('Runtime.evaluate', {
45
+ expression: 'true', returnByValue: true,
46
+ }, timeoutMs);
47
+ return result?.result?.value === true;
48
+ },
49
+ close() { client.close(); },
50
+ };
51
+ }
10
52
 
11
53
  function parseArgs(argv) {
12
54
  const args = {
@@ -46,30 +88,38 @@ async function reload(port) {
46
88
  'The Android Hermes sampling profiler is active, so a runtime reload can abort the app. Use an explicit app.lifecycle restart.',
47
89
  );
48
90
  }
49
- await new Promise((resolve, reject) => {
50
- const socket = new WebSocket(`ws://127.0.0.1:${numericPort}/message`);
51
- const timeout = setTimeout(() => {
52
- socket.close();
53
- reject(new Error(`Metro reload timed out on port ${numericPort}`));
54
- }, 5000);
55
- socket.on('open', () => {
56
- socket.send(JSON.stringify({ method: 'reload', version: 2 }));
57
- setTimeout(() => {
58
- clearTimeout(timeout);
91
+ const observer = await observeRuntimeReset(numericPort, targetBefore);
92
+ let verification;
93
+ try {
94
+ await new Promise((resolve, reject) => {
95
+ const socket = new WebSocket(`ws://127.0.0.1:${numericPort}/message`);
96
+ const timeout = setTimeout(() => {
59
97
  socket.close();
60
- resolve();
61
- }, 50);
62
- });
63
- socket.on('error', () => {
64
- clearTimeout(timeout);
65
- reject(new Error(`Metro is not reachable on port ${numericPort}`));
98
+ reject(new Error(`Metro reload timed out on port ${numericPort}`));
99
+ }, 5000);
100
+ socket.on('open', () => {
101
+ socket.send(JSON.stringify({ method: 'reload', version: 2 }));
102
+ setTimeout(() => {
103
+ clearTimeout(timeout);
104
+ socket.close();
105
+ resolve();
106
+ }, 50);
107
+ });
108
+ socket.on('error', () => {
109
+ clearTimeout(timeout);
110
+ reject(new Error(`Metro is not reachable on port ${numericPort}`));
111
+ });
66
112
  });
67
- });
68
- const targetAfter = await waitForReload(
69
- numericPort,
70
- targetBefore.id,
71
- processBefore,
72
- );
113
+ verification = await waitForReload(
114
+ numericPort,
115
+ targetBefore,
116
+ processBefore,
117
+ observer,
118
+ );
119
+ } finally {
120
+ observer.close();
121
+ }
122
+ const { target: targetAfter, proof } = verification;
73
123
  const processAfter = processBefore ? readAndroidPid() : null;
74
124
  if (processBefore && processAfter !== processBefore) {
75
125
  throw new Error(
@@ -84,6 +134,7 @@ async function reload(port) {
84
134
  port: numericPort,
85
135
  targetBefore: targetBefore.id,
86
136
  targetAfter: targetAfter.id,
137
+ verification: proof,
87
138
  ...(processBefore
88
139
  ? {
89
140
  processBefore,
@@ -94,20 +145,33 @@ async function reload(port) {
94
145
  };
95
146
  }
96
147
 
97
- async function waitForReload(port, targetBefore, processBefore) {
148
+ async function waitForReload(port, targetBefore, processBefore, observer) {
98
149
  const deadline = Date.now() + 30_000;
150
+ let lastError;
99
151
  while (Date.now() < deadline) {
100
152
  if (processBefore && readAndroidPid() !== processBefore) {
101
153
  throw new Error(
102
154
  `Android app process ${processBefore} exited during reload. Inspect the native crash before using an explicit app.lifecycle restart.`,
103
155
  );
104
156
  }
105
- const target = await readHermesTarget(port).catch(() => null);
106
- if (target && target.id !== targetBefore) return target;
157
+ try {
158
+ const response = await fetch(`http://127.0.0.1:${port}/json/list`, {
159
+ signal: AbortSignal.timeout(Math.max(1, Math.min(1000, deadline - Date.now()))),
160
+ });
161
+ const targets = rankRuntimeCandidates(await response.json());
162
+ const target = targets.find((item) => item.deviceName === targetBefore.deviceName);
163
+ if (target && target.id !== targetBefore.id) return { target, proof: 'target-replaced' };
164
+ if (target && observer.recreated && await observer.responsive(Math.max(1, Math.min(1000, deadline - Date.now())))) {
165
+ return { target, proof: 'execution-context-recreated' };
166
+ }
167
+ } catch (error) {
168
+ // A reload temporarily removes the runtime; preserve the last failure if it never returns.
169
+ lastError = error;
170
+ }
107
171
  await new Promise((resolve) => setTimeout(resolve, 250));
108
172
  }
109
173
  throw new Error(
110
- `React Native did not expose a new Hermes target after reload on port ${port}`,
174
+ `React Native reload was not verified on port ${port}: no target replacement or responsive new execution context${lastError ? ` (${lastError.message})` : ''}.\nNext: mm-harness status --json`,
111
175
  );
112
176
  }
113
177
 
@@ -155,7 +219,7 @@ const args = parseArgs(process.argv.slice(2));
155
219
  reload(args.port)
156
220
  .then((result) => {
157
221
  if (args.json) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
158
- else console.log(`Reload requested through Metro :${result.port}`);
222
+ else console.log(`Reload verified through Metro :${result.port} (${result.verification})`);
159
223
  })
160
224
  .catch((error) => {
161
225
  const result = { ok: false, adapter: 'mobile', error: String(error?.message || error) };
@@ -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",