@deeeed/metamask-harness 0.34.4 → 0.36.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 (65) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +9 -1
  3. package/adapters/core/inject.sh +3 -3
  4. package/adapters/extension/check-infura-readiness.cjs +102 -0
  5. package/adapters/extension/inject.mjs +4 -4
  6. package/adapters/extension/live.sh +25 -10
  7. package/adapters/extension/start-watch.sh +15 -0
  8. package/adapters/extension/wallet-fixture-state.cjs +3 -1
  9. package/adapters/manifest.json +16 -0
  10. package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +73 -42
  11. package/adapters/mobile/inject.sh +4 -3
  12. package/adapters/mobile/reset-app-data.sh +154 -0
  13. package/adapters/mobile/verify.sh +37 -0
  14. package/dist/adapters/extension/product-config.js +29 -1
  15. package/dist/adapters/extension/runtime.js +60 -15
  16. package/dist/adapters/extension/surface.js +6 -1
  17. package/dist/adapters/mobile/surface.js +6 -1
  18. package/dist/adapters.js +18 -0
  19. package/dist/cli-commands.js +1 -1
  20. package/dist/cli.js +2 -2
  21. package/dist/command-contract.js +1 -1
  22. package/dist/commands/device-target.js +5 -0
  23. package/dist/commands/fixtures.js +106 -31
  24. package/dist/commands/launch/extension.js +39 -3
  25. package/dist/commands/launch/index.js +13 -0
  26. package/dist/doctor.js +30 -1
  27. package/dist/mm-harness-cli.js +6 -3
  28. package/dist/recipe-security.js +3 -0
  29. package/docs/RECIPES.md +29 -0
  30. package/library/actions/extension/wallet/import.mjs +234 -0
  31. package/library/actions/extension/wallet/reset.mjs +98 -0
  32. package/library/actions/extension/wallet/state.mjs +1 -0
  33. package/library/actions/mobile/analytics/consent-settings.mjs +112 -0
  34. package/library/actions/mobile/analytics/set_consent.mjs +4 -112
  35. package/library/actions/mobile/platform/bridge.mjs +8 -0
  36. package/library/actions/mobile/wallet/import.mjs +259 -0
  37. package/library/actions/mobile/wallet/reset-helper.mjs +99 -0
  38. package/library/actions/mobile/wallet/reset.mjs +7 -0
  39. package/library/actions/shared/wallet/import-source.mjs +101 -0
  40. package/library/manifests/extension.action-manifest.json +112 -0
  41. package/library/manifests/mobile.action-manifest.json +108 -0
  42. package/library/recipes/wallet/import.recipe.json +83 -0
  43. package/library/recipes/wallet/reset-import.recipe.json +88 -0
  44. package/package.json +1 -1
  45. package/scripts/completions.sh +2 -2
  46. package/scripts/site-contrast.mjs +7 -0
  47. package/site/architecture.html +14 -7
  48. package/site/assets/metamask-fox.svg +24 -0
  49. package/site/assets/progress.mjs +6 -0
  50. package/site/assets/style.css +88 -29
  51. package/site/cheatsheet.html +5 -6
  52. package/site/ecosystem.html +162 -0
  53. package/site/how-it-works.html +8 -9
  54. package/site/index.html +17 -12
  55. package/site/perps.html +103 -45
  56. package/site/recipes.html +5 -6
  57. package/site/reviewers.html +5 -6
  58. package/site/tutorials/index.html +5 -6
  59. package/site/tutorials/v1.html +5 -6
  60. package/site/tutorials/v2.html +6 -8
  61. package/site/tutorials/v3.html +5 -6
  62. package/site/tutorials/v4.html +5 -6
  63. package/site/tutorials/v5.html +5 -6
  64. package/site/tutorials/v6.html +5 -6
  65. package/site/tutorials/v7.html +5 -6
package/CHANGELOG.md CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.36.0 - 2026-08-13
6
+
7
+ ### Added
8
+
9
+ - Add a composable Mobile and Extension wallet import recipe that either verifies an existing fixture-backed profile or imports the primary mnemonic through visible onboarding UI, with secret-safe inputs, privacy-preserving defaults, and configurable MetaMetrics and Mobile interests.
10
+ - Add `fixtures reset` plus a shared Mobile/Extension reset-import recipe. Extension resets its owned browser profile; Mobile clears only the selected app's data while preserving its installed build. The visible recipe separately proves the real user reset and import flow.
11
+
12
+ ### Fixed
13
+
14
+ - Restart a harness-owned Extension watcher when its effective Infura credential differs from the current product configuration, and preflight compiled Ethereum and Linea credentials before Chrome starts, preventing a forced-red validation from poisoning later launches or opening a native HTTP authentication dialog.
15
+ - Make normal Extension launch and runtime recovery use the product's `yarn start` development build, while keeping production-like LavaMoat builds behind explicit `launch --build`.
16
+ - Require wallet profile reuse to match the requested account, wait for Mobile onboarding to reach a terminal route, and resolve the exact Mobile reset device and prewarm its bundle before relaunching after app-data reset.
17
+ - Refuse ambiguous installed Mobile app variants during reset, prefer complete environment wallet credentials without parsing a stale fixture, and reject bad Extension RPC configuration before replacing the loaded runtime snapshot.
18
+ - Validate recovery credentials, fixture structure, and platform-specific onboarding choices before destructive resets; prove Mobile reset by the visible import control; honor persisted MetaMetrics consent through Security settings; and keep file-backed wallet secrets out of bridge errors.
19
+
20
+ ## 0.35.0 - 2026-08-12
21
+
22
+ ### Fixed
23
+
24
+ - Make Extension and Mobile launch verification fail when their in-app EVM network client cannot complete a live RPC, with exact Infura configuration and retry guidance.
25
+ - Make Extension doctor require checkout-pinned Playwright Chromium and report the exact install command when it is missing.
26
+ - Make doctor report the exact dependency-install command when `--fix` finds stale or missing dependencies instead of implying they were repaired.
27
+ - Make the setup prompt reuse configured skill sources and require consent before cloning or editing shell startup files.
28
+ - Keep installed overlays delegated to the source runner that installed them before falling back to a potentially stale global runner.
29
+
5
30
  ## 0.34.4 - 2026-08-12
6
31
 
7
32
  ### Changed
package/README.md CHANGED
@@ -88,10 +88,14 @@ mm-harness stop
88
88
  mm-harness logs
89
89
  mm-harness debug
90
90
  mm-harness fixtures set
91
+
92
+ # Delete the current wallet, then reapply the canonical fixture from clean state.
93
+ mm-harness fixtures reset
91
94
  ```
92
95
 
93
96
  Extension `launch` keeps its incremental watcher running. Refresh the active
94
- page after a successful rebuild; use `launch --build` only for a clean rebuild.
97
+ page after a successful rebuild; use `launch --build` only for an explicit
98
+ production-like LavaMoat rebuild.
95
99
 
96
100
  Log sources stay separate:
97
101
 
@@ -115,6 +119,10 @@ mm-harness call metamask.wallet.ensure_unlocked
115
119
  mm-harness run --list
116
120
  mm-harness run perps.clean-market-testnet --describe
117
121
  mm-harness run wallet.smoke --describe
122
+ mm-harness run wallet.import method=auto
123
+
124
+ # Use visible client UI to delete and import the wallet again.
125
+ mm-harness run wallet.reset-import
118
126
  mm-harness run path/to/recipe.json market=ETH --plan
119
127
  mm-harness run path/to/recipe.json
120
128
  ```
@@ -88,11 +88,11 @@ install_v1_runner_assets() {
88
88
  if [ -n "$METAMASK_RUNNER_PROTOCOL_ROOT" ]; then
89
89
  printf 'export FARMSLOT_ROOT=${FARMSLOT_ROOT:-%s}\n' "$runner_protocol_root_q"
90
90
  fi
91
- # Resolve the runner at run time (override > global install > install-time
92
- # record); the recorded path is last resort so a moved checkout is not pinned.
91
+ # The recorded runner owns the installed overlay and must remain ahead of an
92
+ # unrelated global version. MM_HARNESS_BIN is the explicit override.
93
93
  printf '%s\n' 'if [ -n "${MM_HARNESS_BIN:-}" ] && [ -x "${MM_HARNESS_BIN}" ]; then exec "${MM_HARNESS_BIN}" "$@"; fi'
94
- printf '%s\n' 'if command -v mm-harness >/dev/null 2>&1; then exec mm-harness "$@"; fi'
95
94
  printf 'if [ -x %s ]; then exec %s "$@"; fi\n' "$runner_exec_q" "$runner_exec_q"
95
+ printf '%s\n' 'if command -v mm-harness >/dev/null 2>&1; then exec mm-harness "$@"; fi'
96
96
  printf '%s\n' 'echo "mm-harness not found. Next: npm i -g @deeeed/metamask-harness" >&2'
97
97
  printf '%s\n' 'exit 127'
98
98
  } > "$HARNESS_DIR/runner/bin/mm-harness"
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('node:fs');
5
+ const https = require('node:https');
6
+ const path = require('node:path');
7
+
8
+ const args = {};
9
+ for (let index = 2; index < process.argv.length; index += 1) {
10
+ const key = process.argv[index];
11
+ if (key === '--help' || key === '-h') {
12
+ process.stdout.write('Usage: check-infura-readiness.cjs --target <metamask-extension> --runtime-dist <dist>\n');
13
+ process.exit(0);
14
+ }
15
+ if (!key.startsWith('--') || index + 1 >= process.argv.length) {
16
+ throw new Error(`invalid argument: ${key}`);
17
+ }
18
+ args[key.slice(2)] = process.argv[++index];
19
+ }
20
+
21
+ for (const key of ['target', 'runtime-dist']) {
22
+ if (!args[key]) throw new Error(`--${key} is required`);
23
+ }
24
+
25
+ const target = path.resolve(args.target);
26
+ const runtimeDist = path.resolve(args['runtime-dist']);
27
+ const config = require('./lib/product-config.cjs');
28
+
29
+ async function main() {
30
+ const resolution = config.resolveExtensionInfuraProjectId(target, process.env);
31
+ if (resolution.kind !== 'configured') {
32
+ fail('the effective Extension product configuration has no usable Infura project ID');
33
+ }
34
+ if (!compiledScriptsContain(runtimeDist, resolution.value)) {
35
+ fail('the compiled Extension scripts do not match the effective Infura project ID');
36
+ }
37
+ for (const host of ['mainnet.infura.io', 'linea-mainnet.infura.io']) {
38
+ const response = await rpcProbe(host, resolution.value);
39
+ if (response.statusCode < 200 || response.statusCode >= 300 || response.jsonRpcResult !== true) {
40
+ fail(`${host} rejected the effective Infura project ID (HTTP ${response.statusCode})`);
41
+ }
42
+ }
43
+ process.stdout.write('Infura readiness preflight passed for Ethereum and Linea.\n');
44
+ }
45
+
46
+ function compiledScriptsContain(root, value) {
47
+ const pending = [root];
48
+ while (pending.length > 0) {
49
+ const current = pending.pop();
50
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
51
+ const file = path.join(current, entry.name);
52
+ if (entry.isDirectory()) pending.push(file);
53
+ else if (entry.isFile() && entry.name.endsWith('.js') && fs.readFileSync(file, 'utf8').includes(value)) {
54
+ return true;
55
+ }
56
+ }
57
+ }
58
+ return false;
59
+ }
60
+
61
+ function rpcProbe(host, projectId) {
62
+ const body = JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_chainId', params: [] });
63
+ return new Promise((resolve, reject) => {
64
+ const request = https.request({
65
+ hostname: host,
66
+ path: `/v3/${encodeURIComponent(projectId)}`,
67
+ method: 'POST',
68
+ timeout: 10_000,
69
+ headers: {
70
+ 'content-type': 'application/json',
71
+ 'content-length': Buffer.byteLength(body),
72
+ },
73
+ }, (response) => {
74
+ let text = '';
75
+ response.setEncoding('utf8');
76
+ response.on('data', (chunk) => { text += chunk; });
77
+ response.on('end', () => {
78
+ let jsonRpcResult = false;
79
+ try {
80
+ const parsed = JSON.parse(text);
81
+ jsonRpcResult = typeof parsed.result === 'string' && parsed.result.startsWith('0x');
82
+ } catch {
83
+ jsonRpcResult = false;
84
+ }
85
+ resolve({ statusCode: response.statusCode || 0, jsonRpcResult });
86
+ });
87
+ });
88
+ request.on('timeout', () => request.destroy(new Error(`${host} timed out`)));
89
+ request.on('error', reject);
90
+ request.end(body);
91
+ });
92
+ }
93
+
94
+ function fail(detail) {
95
+ process.stderr.write(
96
+ `EVM_RPC_UNREACHABLE: Infura readiness preflight failed before browser launch: ${detail}.\n` +
97
+ `Next: stop the stale watcher, confirm ${path.join(target, '.metamaskrc')} has a real INFURA_PROJECT_ID, then rerun mm-harness launch --verify --target ${JSON.stringify(target)}.\n`,
98
+ );
99
+ process.exit(1);
100
+ }
101
+
102
+ main().catch((error) => fail(error instanceof Error ? error.message : String(error)));
@@ -69,17 +69,16 @@ fs.mkdirSync(path.join(harnessDir, 'runner/recipes'), { recursive: true });
69
69
  fs.rmSync(path.join(harnessDir, 'scripts'), { recursive: true, force: true });
70
70
  fs.mkdirSync(path.join(harnessDir, 'scripts/lib'), { recursive: true });
71
71
 
72
- // Resolve the runner at run time so a moved or shared checkout is never bound to
73
- // an install-time absolute path: explicit override, then a global install, then
74
- // the runner recorded at install time as a last resort; teach install on miss.
72
+ // Prefer the runner that installed the overlay so its runtime checks match the
73
+ // installed scripts. Explicit overrides still support controlled delegation.
75
74
  const pinnedBin = shellQuote(path.join(runnerDir, 'bin/mm-harness'));
76
75
  const delegate = [
77
76
  '#!/usr/bin/env bash',
78
77
  'set -euo pipefail',
79
78
  protocolRoot ? `export FARMSLOT_ROOT=\${FARMSLOT_ROOT:-${shellQuote(protocolRoot)}}` : null,
80
79
  'if [ -n "${MM_HARNESS_BIN:-}" ] && [ -x "${MM_HARNESS_BIN}" ]; then exec "${MM_HARNESS_BIN}" "$@"; fi',
81
- 'if command -v mm-harness >/dev/null 2>&1; then exec mm-harness "$@"; fi',
82
80
  `if [ -x ${pinnedBin} ]; then exec ${pinnedBin} "$@"; fi`,
81
+ 'if command -v mm-harness >/dev/null 2>&1; then exec mm-harness "$@"; fi',
83
82
  'echo "mm-harness not found. Next: npm i -g @deeeed/metamask-harness" >&2',
84
83
  'exit 127',
85
84
  ].filter(Boolean).join('\n') + '\n';
@@ -103,6 +102,7 @@ copyFile(path.join(runnerDir, 'adapters/extension/build-lavamoat.sh'), path.join
103
102
  copyFile(path.join(runnerDir, 'adapters/extension/launch-webpack.cjs'), path.join(harnessDir, 'scripts/launch-webpack.cjs'));
104
103
  copyFile(path.join(runnerDir, 'adapters/extension/sync-webpack-dist.cjs'), path.join(harnessDir, 'scripts/sync-webpack-dist.cjs'));
105
104
  copyFile(path.join(runnerDir, 'adapters/extension/configure-runtime-manifest.cjs'), path.join(harnessDir, 'scripts/configure-runtime-manifest.cjs'));
105
+ copyFile(path.join(runnerDir, 'adapters/extension/check-infura-readiness.cjs'), path.join(harnessDir, 'scripts/check-infura-readiness.cjs'));
106
106
  copyFile(path.join(runnerDir, 'adapters/extension/stamp-runtime-title.cjs'), path.join(harnessDir, 'scripts/stamp-runtime-title.cjs'));
107
107
  copyFile(path.join(runnerDir, 'adapters/extension/stop-viewers.sh'), path.join(harnessDir, 'scripts/stop-viewers.sh'));
108
108
  copyFile(path.join(runnerDir, 'adapters/extension/snapshot-dist.sh'), path.join(harnessDir, 'scripts/snapshot-dist.sh'));
@@ -6,13 +6,16 @@
6
6
  # Purpose:
7
7
  # Builds the per-run prepare command from the named feature scripts
8
8
  # (watch, dist snapshot, fixture seed, detached chrome, CDP poll), then
9
- # runs launch.sh and verify.sh under a timestamped artifact dir.
9
+ # runs launch.sh and, unless --launch-only is set, verify.sh under a
10
+ # timestamped artifact dir.
10
11
  #
11
12
  # Inputs (flags / env):
12
13
  # --target <metamask-extension> (default $PWD)
13
14
  # --cdp-port <port> (optional; inferred from checkout context/pool when omitted)
14
15
  # --launch-existing-dist | --start-watch | --build-lavamoat | --prepare-cmd <cmd>
15
16
  # (env RECIPE_HARNESS_EXTENSION_LAUNCH_CMD)
17
+ # --launch-only (skip recipe verification; used by fixture application while
18
+ # its checkout lock is held)
16
19
  # --dist-dir <rel> (default dist/chrome), --chrome-user-data-dir <dir>,
17
20
  # --remote-flag <KEY=VARIANT[,KEY=VARIANT...]> (pins manifest _flags into the
18
21
  # ephemeral runtime-dist snapshot before launch; omit for default behavior)
@@ -44,6 +47,7 @@ DIST_DIR="dist/chrome"
44
47
  CHROME_USER_DATA_DIR="${CHROME_USER_DATA_DIR:-}"
45
48
  REMOTE_FLAGS=""
46
49
  REMOTE_FLAGS_APPLIED=false
50
+ RUN_VERIFY=true
47
51
  while [ "$#" -gt 0 ]; do
48
52
  case "$1" in
49
53
  --target) [ "$#" -ge 2 ] || { echo "Missing value for $1" >&2; exit 2; }; TARGET="$2"; shift 2 ;;
@@ -55,10 +59,11 @@ while [ "$#" -gt 0 ]; do
55
59
  --start-url) [ "$#" -ge 2 ] || { echo "Missing value for $1" >&2; exit 2; }; START_URL="$2"; shift 2 ;;
56
60
  --start-watch|--start-test-watch) START_WATCH=true; LAUNCH_EXISTING_DIST=true; shift ;;
57
61
  --build-lavamoat) BUILD_LAVAMOAT=true; LAUNCH_EXISTING_DIST=true; shift ;;
62
+ --launch-only) RUN_VERIFY=false; shift ;;
58
63
  --dist-dir) [ "$#" -ge 2 ] || { echo "Missing value for $1" >&2; exit 2; }; DIST_DIR="$2"; shift 2 ;;
59
64
  --chrome-user-data-dir) [ "$#" -ge 2 ] || { echo "Missing value for $1" >&2; exit 2; }; CHROME_USER_DATA_DIR="$2"; shift 2 ;;
60
65
  --remote-flag) [ "$#" -ge 2 ] || { echo "Missing value for $1" >&2; exit 2; }; REMOTE_FLAGS="$2"; shift 2 ;;
61
- -h|--help) echo "Usage: live.sh [--target <metamask-extension>] [--out <recipes-dir>] [--cdp-port <port>] [--launch-existing-dist|--start-watch|--build-lavamoat|--prepare-cmd <cmd>] [--dist-dir dist/chrome] [--remote-flag KEY=VARIANT] [--artifacts-dir <dir>]"; exit 0 ;;
66
+ -h|--help) echo "Usage: live.sh [--target <metamask-extension>] [--out <recipes-dir>] [--cdp-port <port>] [--launch-existing-dist|--start-watch|--build-lavamoat|--prepare-cmd <cmd>] [--launch-only] [--dist-dir dist/chrome] [--remote-flag KEY=VARIANT] [--artifacts-dir <dir>]"; exit 0 ;;
62
67
  *) echo "Unknown arg: $1" >&2; exit 2 ;;
63
68
  esac
64
69
  done
@@ -177,6 +182,7 @@ NODE
177
182
  quoted_build_lavamoat="$(printf '%q' "$SCRIPT_DIR/build-lavamoat.sh")"
178
183
  quoted_snapshot_dist="$(printf '%q' "$SCRIPT_DIR/snapshot-dist.sh")"
179
184
  quoted_configure_manifest="$(printf '%q' "$SCRIPT_DIR/configure-runtime-manifest.cjs")"
185
+ quoted_check_infura="$(printf '%q' "$SCRIPT_DIR/check-infura-readiness.cjs")"
180
186
  quoted_stamp_title="$(printf '%q' "$SCRIPT_DIR/stamp-runtime-title.cjs")"
181
187
  quoted_chrome_launcher="$(printf '%q' "$SCRIPT_DIR/launch-browser.cjs")"
182
188
  quoted_fixture_state="$(printf '%q' "$FIXTURE_STATE_ABS")"
@@ -257,9 +263,10 @@ NODE
257
263
  if $BUILD_LAVAMOAT; then
258
264
  prepare_parts+=("bash ${quoted_build_lavamoat} --target ${quoted_target} --runtime-dir ${quoted_runtime_dir}")
259
265
  fi
260
- prepare_parts+=("node ${quoted_chrome_launcher} --stop-only 1 --reset-profile 1 --chrome-bin ${quoted_chrome} --profile ${quoted_profile} --cdp-port ${CDP_PORT} --extension-dir ${quoted_runtime_dist} --chrome-log ${quoted_chrome_log} --chrome-pid ${quoted_chrome_pid}")
266
+ prepare_parts+=("node ${quoted_check_infura} --target ${quoted_target} --runtime-dist ${quoted_dist}")
261
267
  prepare_parts+=("bash ${quoted_snapshot_dist} --dist ${quoted_dist} --runtime-dist ${quoted_runtime_dist}")
262
268
  prepare_parts+=("node ${quoted_configure_manifest} --target ${quoted_target} --manifest ${quoted_runtime_dist}/manifest.json")
269
+ prepare_parts+=("node ${quoted_chrome_launcher} --stop-only 1 --reset-profile 1 --chrome-bin ${quoted_chrome} --profile ${quoted_profile} --cdp-port ${CDP_PORT} --extension-dir ${quoted_runtime_dist} --chrome-log ${quoted_chrome_log} --chrome-pid ${quoted_chrome_pid}")
263
270
  prepare_parts+=("node ${quoted_stamp_title} --target ${quoted_target} --runtime-dist ${quoted_runtime_dist} --runtime-dir ${quoted_runtime_dir}")
264
271
  # Optional A/B feature-flag pinning: patch the ephemeral snapshot manifest so
265
272
  # manifest._flags wins over the fetched ClientConfigApi value. No-op unless
@@ -301,7 +308,7 @@ printf ' '
301
308
  printf '%q ' "${display_args[@]}"
302
309
  printf '\n'
303
310
  echo "Launch artifacts: $ARTIFACTS/launch"
304
- echo "Verify artifacts: $ARTIFACTS/verify"
311
+ $RUN_VERIFY && echo "Verify artifacts: $ARTIFACTS/verify"
305
312
 
306
313
  launch_args=(--target "$TARGET" --cdp-port "$CDP_PORT" --artifacts-dir "$ARTIFACTS/launch")
307
314
  [ -n "$PREPARE_CMD" ] && launch_args+=(--prepare-cmd "$PREPARE_CMD")
@@ -312,18 +319,21 @@ set +e
312
319
  launch_status=$?
313
320
  set -e
314
321
 
315
- verify_status=1
316
- if [ "$launch_status" -eq 0 ]; then
322
+ verify_status=0
323
+ if [ "$launch_status" -eq 0 ] && $RUN_VERIFY; then
317
324
  echo "[recipe-harness] phase 2/2: live verify (readiness + smoke recipe)" >&2
318
325
  set +e
319
326
  "$VERIFY_SH" --target "$TARGET" --out "$OUT" --cdp-port "$CDP_PORT" --artifacts-dir "$ARTIFACTS/verify"
320
327
  verify_status=$?
321
328
  set -e
322
- else
329
+ elif [ "$launch_status" -ne 0 ]; then
330
+ verify_status=1
323
331
  echo "Skipping Extension live verify because launch failed; see $ARTIFACTS/launch/summary.json" >&2
332
+ else
333
+ echo "Skipping Extension live verify for launch-only fixture application." >&2
324
334
  fi
325
335
 
326
- TARGET_FOR_SUMMARY="$TARGET" ARTIFACTS_FOR_SUMMARY="$ARTIFACTS" CDP_PORT_FOR_SUMMARY="$CDP_PORT" LAUNCH_STATUS="$launch_status" VERIFY_STATUS="$verify_status" LAUNCH_EXISTING_DIST="$LAUNCH_EXISTING_DIST" START_WATCH="$START_WATCH" BUILD_LAVAMOAT="$BUILD_LAVAMOAT" node <<'NODE'
336
+ TARGET_FOR_SUMMARY="$TARGET" ARTIFACTS_FOR_SUMMARY="$ARTIFACTS" CDP_PORT_FOR_SUMMARY="$CDP_PORT" LAUNCH_STATUS="$launch_status" VERIFY_STATUS="$verify_status" RUN_VERIFY="$RUN_VERIFY" LAUNCH_EXISTING_DIST="$LAUNCH_EXISTING_DIST" START_WATCH="$START_WATCH" BUILD_LAVAMOAT="$BUILD_LAVAMOAT" node <<'NODE'
327
337
  const fs = require('fs');
328
338
  const path = require('path');
329
339
  const artifacts = process.env.ARTIFACTS_FOR_SUMMARY;
@@ -331,6 +341,7 @@ const launchSummary = path.join(artifacts, 'launch', 'summary.json');
331
341
  const verifySummary = path.join(artifacts, 'verify', 'summary.json');
332
342
  const launchStatus = Number(process.env.LAUNCH_STATUS);
333
343
  const verifyStatus = Number(process.env.VERIFY_STATUS);
344
+ const runVerify = process.env.RUN_VERIFY === 'true';
334
345
  fs.writeFileSync(path.join(artifacts, 'summary.json'), `${JSON.stringify({
335
346
  adapter: 'extension',
336
347
  action: 'live',
@@ -341,9 +352,13 @@ fs.writeFileSync(path.join(artifacts, 'summary.json'), `${JSON.stringify({
341
352
  startWatch: process.env.START_WATCH === 'true',
342
353
  buildLavaMoat: process.env.BUILD_LAVAMOAT === 'true',
343
354
  launch: { exitCode: launchStatus, summaryPath: fs.existsSync(launchSummary) ? launchSummary : null },
344
- verify: { exitCode: verifyStatus, summaryPath: fs.existsSync(verifySummary) ? verifySummary : null },
355
+ verify: runVerify
356
+ ? { status: verifyStatus === 0 ? 'pass' : 'fail', exitCode: verifyStatus, summaryPath: fs.existsSync(verifySummary) ? verifySummary : null }
357
+ : { status: 'skipped', exitCode: null, summaryPath: null },
345
358
  easyCommand: `mm-harness launch --verify --target <repo>`,
346
- note: 'Runs launch then live verify so a developer can validate browser startup, CDP readiness, recipe bridge, screenshots/fallback classification, and sample recipes from one runner-owned command.',
359
+ note: runVerify
360
+ ? 'Runs launch then live verify so a developer can validate browser startup, CDP readiness, recipe bridge, screenshots/fallback classification, and sample recipes from one runner-owned command.'
361
+ : 'Runs launch validation only for fixture application while the checkout lock is held.',
347
362
  generatedAt: new Date().toISOString(),
348
363
  }, null, 2)}\n`);
349
364
  NODE
@@ -142,6 +142,21 @@ trap finish EXIT
142
142
 
143
143
  mkdir -p "$RUNTIME_DIR"
144
144
 
145
+ PRODUCT_CONFIG_MODULE="$SCRIPT_DIR/lib/product-config.cjs" \
146
+ node - "$PWD" "$RUNTIME_DIR/extension-product-config.sha256" <<'NODE'
147
+ const crypto = require('node:crypto');
148
+ const fs = require('node:fs');
149
+ const config = require(process.env.PRODUCT_CONFIG_MODULE);
150
+ const [target, destination] = process.argv.slice(2);
151
+ const resolution = config.resolveExtensionInfuraProjectId(target, process.env);
152
+ if (resolution.kind !== 'configured') {
153
+ fs.rmSync(destination, { force: true });
154
+ process.exit(0);
155
+ }
156
+ const fingerprint = crypto.createHash('sha256').update(resolution.value).digest('hex');
157
+ fs.writeFileSync(destination, `${fingerprint}\n`);
158
+ NODE
159
+
145
160
  watcher_pids() {
146
161
  ps -axo pid=,command= | while read -r current_pid command; do
147
162
  [ -n "${current_pid:-}" ] || continue
@@ -658,7 +658,9 @@ async function detectExtension(context, extensionDir, extensionIdFile) {
658
658
  }
659
659
 
660
660
  for (const candidate of candidates) {
661
- const page = await context.newPage();
661
+ const page = context.pages().find(
662
+ (candidatePage) => extensionIdFromUrl(candidatePage.url()) === candidate.id,
663
+ ) || await context.newPage();
662
664
  try {
663
665
  await page.goto(`chrome-extension://${candidate.id}/home.html`, {
664
666
  waitUntil: 'domcontentloaded',
@@ -58,6 +58,14 @@
58
58
  "inputs": "--platform ios|android --target --port; env MOBILE_BUNDLE_PREWARM, MOBILE_BUNDLE_PREWARM_TIMEOUT",
59
59
  "outputs": "progress on stderr; exit 0 bundle ready / 1 timeout / 2 bad args"
60
60
  },
61
+ {
62
+ "id": "mobile/reset-app-data",
63
+ "entry": "adapters/mobile/reset-app-data.sh",
64
+ "kind": "bash",
65
+ "purpose": "Reset exactly one installed MetaMask Mobile app while preserving its install for fixture reapplication.",
66
+ "inputs": "--platform ios|android --target --simulator --adb-serial; env IOS_BUNDLE_ID, ANDROID_PACKAGE_ID",
67
+ "outputs": "selected app data cleared and app restored on iOS / cleared on Android; exit 0/1/2"
68
+ },
61
69
  {
62
70
  "id": "mobile/open-device",
63
71
  "entry": "adapters/mobile/open-device.sh",
@@ -162,6 +170,14 @@
162
170
  "inputs": "--target --manifest",
163
171
  "outputs": "runtime manifest _flags.testing.infuraProjectId; exit 0/1"
164
172
  },
173
+ {
174
+ "id": "extension/check-infura-readiness",
175
+ "entry": "adapters/extension/check-infura-readiness.cjs",
176
+ "kind": "node",
177
+ "purpose": "Reject compiled Infura credentials that are stale or unreachable on Ethereum or Linea before Chromium starts.",
178
+ "inputs": "--target --runtime-dist",
179
+ "outputs": "readiness result without credential values; exit 0/1/2"
180
+ },
165
181
  {
166
182
  "id": "extension/stamp-runtime-title",
167
183
  "entry": "adapters/extension/stamp-runtime-title.cjs",
@@ -84,6 +84,65 @@ const NESTED_ROUTE_PARENTS = {
84
84
  DeveloperOptions: 'Settings', NotificationsSettings: 'Settings',
85
85
  };
86
86
 
87
+ async function setInput(client, testId, value, { deviceName } = {}, redact = false) {
88
+ if (!testId) {
89
+ throw new Error('Usage: set-input <testId> <value>');
90
+ }
91
+ const expr = `(function() {
92
+ if (globalThis.__AGENTIC__?.setInput) return globalThis.__AGENTIC__.setInput(${JSON.stringify(testId)}, ${JSON.stringify(value)});
93
+ var hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
94
+ if (!hook) return { ok: false, error: 'No React DevTools hook' };
95
+ var renderers = hook.renderers;
96
+ if (!renderers) return { ok: false, error: 'No renderers' };
97
+ var getFiberRoots = hook.getFiberRoots;
98
+ function findByTestId(fiber) {
99
+ if (!fiber) return null;
100
+ var props = fiber.memoizedProps;
101
+ if (props && props.testID === ${JSON.stringify(testId)}) return fiber;
102
+ return findByTestId(fiber.child) || findByTestId(fiber.sibling);
103
+ }
104
+ for (var [id] of renderers) {
105
+ var roots = getFiberRoots ? getFiberRoots(id) : undefined;
106
+ if (!roots) continue;
107
+ var result = null;
108
+ roots.forEach(function(r) {
109
+ if (result) return;
110
+ var fiber = findByTestId(r.current);
111
+ if (!fiber) return;
112
+ var cur = fiber;
113
+ while (cur) {
114
+ if (cur.memoizedProps && typeof cur.memoizedProps.onChangeText === 'function') {
115
+ cur.memoizedProps.onChangeText(${JSON.stringify(value)});
116
+ result = { ok: true, testId: ${JSON.stringify(testId)} };
117
+ return;
118
+ }
119
+ cur = cur.return || null;
120
+ }
121
+ });
122
+ if (result) return result;
123
+ }
124
+ return { ok: false, error: 'No component with testID=' + ${JSON.stringify(testId)} + ' found or no onChangeText' };
125
+ })()`;
126
+ let result;
127
+ try {
128
+ result = await cdpEval(client, expr);
129
+ } catch (error) {
130
+ if (redact) {
131
+ throw new Error(`Secret input could not be applied for testID ${testId}.`);
132
+ }
133
+ throw error;
134
+ }
135
+ if (redact && result?.ok === false) {
136
+ result = { ok: false, error: 'Secret input could not be applied.' };
137
+ }
138
+ return {
139
+ ...result,
140
+ testId,
141
+ value: redact ? '<redacted>' : value,
142
+ deviceName,
143
+ };
144
+ }
145
+
87
146
  const COMMANDS = {
88
147
  async navigate(client, args, { deviceName, platform } = {}) {
89
148
  const routeName = ROUTE_ALIASES[args[0]] || args[0];
@@ -608,50 +667,21 @@ const COMMANDS = {
608
667
  };
609
668
  },
610
669
 
611
- async 'set-input'(client, args, { deviceName } = {}) {
670
+ async 'set-input'(client, args, context = {}) {
671
+ return setInput(client, args[0], args.slice(1).join(' '), context);
672
+ },
673
+
674
+ async 'set-input-file'(client, args, context = {}) {
612
675
  const testId = args[0];
613
- const value = args.slice(1).join(' ');
614
- if (!testId) {
615
- throw new Error('Usage: set-input <testId> <value>');
676
+ const file = args[1];
677
+ if (!testId || !file) {
678
+ throw new Error('Usage: set-input-file <testId> <value-file>');
616
679
  }
617
- // Try __AGENTIC__ bridge first, fall back to inline fiber walking
618
- const expr = `(function() {
619
- if (globalThis.__AGENTIC__?.setInput) return globalThis.__AGENTIC__.setInput(${JSON.stringify(testId)}, ${JSON.stringify(value)});
620
- var hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
621
- if (!hook) return { ok: false, error: 'No React DevTools hook' };
622
- var renderers = hook.renderers;
623
- if (!renderers) return { ok: false, error: 'No renderers' };
624
- var getFiberRoots = hook.getFiberRoots;
625
- function findByTestId(fiber) {
626
- if (!fiber) return null;
627
- var props = fiber.memoizedProps;
628
- if (props && props.testID === ${JSON.stringify(testId)}) return fiber;
629
- return findByTestId(fiber.child) || findByTestId(fiber.sibling);
630
- }
631
- for (var [id] of renderers) {
632
- var roots = getFiberRoots ? getFiberRoots(id) : undefined;
633
- if (!roots) continue;
634
- var result = null;
635
- roots.forEach(function(r) {
636
- if (result) return;
637
- var fiber = findByTestId(r.current);
638
- if (!fiber) return;
639
- var cur = fiber;
640
- while (cur) {
641
- if (cur.memoizedProps && typeof cur.memoizedProps.onChangeText === 'function') {
642
- cur.memoizedProps.onChangeText(${JSON.stringify(value)});
643
- result = { ok: true, testId: ${JSON.stringify(testId)}, value: ${JSON.stringify(value)} };
644
- return;
645
- }
646
- cur = cur.return || null;
647
- }
648
- });
649
- if (result) return result;
650
- }
651
- return { ok: false, error: 'No component with testID=' + ${JSON.stringify(testId)} + ' found or no onChangeText' };
652
- })()`;
653
- const result = await cdpEval(client, expr);
654
- return { ...result, testId, value, deviceName };
680
+ const stat = fs.lstatSync(file);
681
+ if (!stat.isFile() || stat.isSymbolicLink()) {
682
+ throw new Error('set-input-file requires a regular value file.');
683
+ }
684
+ return setInput(client, testId, fs.readFileSync(file, 'utf8'), context, true);
655
685
  },
656
686
 
657
687
  async 'sentry-debug'(client, args, { deviceName } = {}) {
@@ -977,6 +1007,7 @@ Commands:
977
1007
  Scroll a ScrollView/FlatList
978
1008
  measure-scroll-transition <json> Scroll and measure a visible target using one CDP session
979
1009
  set-input <testId> <value> Set text input value by testID (calls onChangeText)
1010
+ set-input-file <testId> <value-file> Set text input from a regular file and redact the value
980
1011
  sentry-debug [enable|disable] Patch Sentry to log errors to console with [SENTRY-DEBUG] prefix
981
1012
  unlock <password> Unlock wallet (inject password + press login button via fiber tree)
982
1013
  profiler-start Start Hermes sampling profiler
@@ -44,7 +44,7 @@ for rel in "$HARNESS_REL" "$HARNESS_REL/runner" "$HARNESS_REL/scripts"; do
44
44
  done
45
45
  done
46
46
 
47
- rm -rf "$HARNESS_DIR/runner" "$HARNESS_DIR/scripts"
47
+ rm -rf "$HARNESS_DIR/runner" "$HARNESS_DIR/scripts" "$HARNESS_DIR/bridge-runtime"
48
48
  mkdir -p "$HARNESS_DIR/runner/bin" "$HARNESS_DIR/runner/manifests" "$HARNESS_DIR/runner/recipes" "$HARNESS_DIR/scripts/lib"
49
49
 
50
50
  RUNNER_EXEC_Q="$(printf '%q' "$RUNNER_DIR/bin/mm-harness")"
@@ -62,8 +62,8 @@ PROTOCOL_ROOT_Q="$(printf '%q' "$PROTOCOL_ROOT")"
62
62
  printf 'export METAMASK_RUNNER_PROTOCOL_ROOT=${METAMASK_RUNNER_PROTOCOL_ROOT:-%s}\n' "$PROTOCOL_ROOT_Q"
63
63
  fi
64
64
  printf '%s\n' 'if [ -n "${MM_HARNESS_BIN:-}" ] && [ -x "$MM_HARNESS_BIN" ]; then exec "$MM_HARNESS_BIN" "$@"; fi'
65
- printf '%s\n' 'if command -v mm-harness >/dev/null 2>&1; then exec mm-harness "$@"; fi'
66
65
  printf 'if [ -x %s ]; then exec %s "$@"; fi\n' "$RUNNER_EXEC_Q" "$RUNNER_EXEC_Q"
66
+ printf '%s\n' 'if command -v mm-harness >/dev/null 2>&1; then exec mm-harness "$@"; fi'
67
67
  printf '%s\n' 'echo "mm-harness not found. Next: npm i -g @deeeed/metamask-harness@latest" >&2' 'exit 127'
68
68
  } > "$HARNESS_DIR/runner/bin/mm-harness"
69
69
  chmod +x "$HARNESS_DIR/runner/bin/mm-harness"
@@ -77,6 +77,7 @@ rsync -a --delete "$RUNNER_DIR/library/recipes/" "$HARNESS_DIR/runner/recipes/"
77
77
  rm -rf "$HARNESS_DIR/runner/flows"
78
78
  rm -f "$HARNESS_DIR/runner/library.json"
79
79
  cp "$RUNNER_DIR/adapters/mobile/verify.sh" "$HARNESS_DIR/scripts/verify.sh"
80
+ cp -R "$RUNNER_DIR/adapters/mobile/bridge-runtime" "$HARNESS_DIR/bridge-runtime"
80
81
  cp "$RUNNER_DIR/adapters/shared/harness-path.sh" "$HARNESS_DIR/scripts/lib/harness-path.sh"
81
82
  cp "$RUNNER_DIR/adapters/shared/path-defaults.json" "$HARNESS_DIR/scripts/lib/path-defaults.json"
82
83
  chmod -R u+rwX,go+rX "$HARNESS_DIR"
@@ -114,7 +115,7 @@ fs.writeFileSync(manifestPath, JSON.stringify({
114
115
  actionManifestPath: `${harnessRel}/action-manifest.json`,
115
116
  runnerEntrypoint: `${harnessRel}/runner/bin/mm-harness`,
116
117
  runtimeHelpers: { verify: `${harnessRel}/scripts/verify.sh` },
117
- harnessInstalledPaths: [`${harnessRel}/runner`, `${harnessRel}/scripts`, `${harnessRel}/action-manifest.json`],
118
+ harnessInstalledPaths: [`${harnessRel}/runner`, `${harnessRel}/scripts`, `${harnessRel}/bridge-runtime`, `${harnessRel}/action-manifest.json`],
118
119
  productOwnedPaths: ['app/dev-tools/AgenticService', 'app/core/NavigationService/NavigationService.ts'],
119
120
  cleanupCommand,
120
121
  productDiffExcludes: [`:(exclude)${harnessRoot}`],