@deeeed/metamask-harness 0.35.0 → 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 (38) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +9 -1
  3. package/adapters/extension/check-infura-readiness.cjs +102 -0
  4. package/adapters/extension/inject.mjs +1 -0
  5. package/adapters/extension/live.sh +25 -10
  6. package/adapters/extension/start-watch.sh +15 -0
  7. package/adapters/extension/wallet-fixture-state.cjs +3 -1
  8. package/adapters/manifest.json +16 -0
  9. package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +73 -42
  10. package/adapters/mobile/reset-app-data.sh +154 -0
  11. package/dist/adapters/extension/product-config.js +29 -1
  12. package/dist/adapters.js +18 -0
  13. package/dist/cli-commands.js +1 -1
  14. package/dist/cli.js +2 -2
  15. package/dist/command-contract.js +1 -1
  16. package/dist/commands/device-target.js +5 -0
  17. package/dist/commands/fixtures.js +106 -31
  18. package/dist/commands/launch/extension.js +39 -3
  19. package/dist/commands/launch/index.js +13 -0
  20. package/dist/mm-harness-cli.js +6 -3
  21. package/dist/recipe-security.js +3 -0
  22. package/docs/RECIPES.md +29 -0
  23. package/library/actions/extension/wallet/import.mjs +234 -0
  24. package/library/actions/extension/wallet/reset.mjs +98 -0
  25. package/library/actions/extension/wallet/state.mjs +1 -0
  26. package/library/actions/mobile/analytics/consent-settings.mjs +112 -0
  27. package/library/actions/mobile/analytics/set_consent.mjs +4 -112
  28. package/library/actions/mobile/platform/bridge.mjs +8 -0
  29. package/library/actions/mobile/wallet/import.mjs +259 -0
  30. package/library/actions/mobile/wallet/reset-helper.mjs +99 -0
  31. package/library/actions/mobile/wallet/reset.mjs +7 -0
  32. package/library/actions/shared/wallet/import-source.mjs +101 -0
  33. package/library/manifests/extension.action-manifest.json +112 -0
  34. package/library/manifests/mobile.action-manifest.json +108 -0
  35. package/library/recipes/wallet/import.recipe.json +83 -0
  36. package/library/recipes/wallet/reset-import.recipe.json +88 -0
  37. package/package.json +1 -1
  38. package/scripts/completions.sh +2 -2
package/CHANGELOG.md CHANGED
@@ -2,6 +2,21 @@
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
+
5
20
  ## 0.35.0 - 2026-08-12
6
21
 
7
22
  ### Fixed
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
  ```
@@ -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)));
@@ -102,6 +102,7 @@ copyFile(path.join(runnerDir, 'adapters/extension/build-lavamoat.sh'), path.join
102
102
  copyFile(path.join(runnerDir, 'adapters/extension/launch-webpack.cjs'), path.join(harnessDir, 'scripts/launch-webpack.cjs'));
103
103
  copyFile(path.join(runnerDir, 'adapters/extension/sync-webpack-dist.cjs'), path.join(harnessDir, 'scripts/sync-webpack-dist.cjs'));
104
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'));
105
106
  copyFile(path.join(runnerDir, 'adapters/extension/stamp-runtime-title.cjs'), path.join(harnessDir, 'scripts/stamp-runtime-title.cjs'));
106
107
  copyFile(path.join(runnerDir, 'adapters/extension/stop-viewers.sh'), path.join(harnessDir, 'scripts/stop-viewers.sh'));
107
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
@@ -0,0 +1,154 @@
1
+ #!/usr/bin/env bash
2
+ # Reset one installed MetaMask Mobile app without rebuilding it.
3
+ set -euo pipefail
4
+
5
+ PLATFORM="${PLATFORM:-ios}"
6
+ TARGET="$PWD"
7
+ SIMULATOR="${SIM_UDID:-${IOS_SIMULATOR:-booted}}"
8
+ ADB_SERIAL_ARG="${ADB_SERIAL:-${ANDROID_SERIAL:-}}"
9
+ ADB_BIN="${MM_HARNESS_ADB_PATH:-}"
10
+ MM_RESET_TEMP_ROOT=""
11
+ MM_RESET_STAGING_DIR=""
12
+
13
+ while [ "$#" -gt 0 ]; do
14
+ case "$1" in
15
+ --platform) PLATFORM="$2"; shift 2 ;;
16
+ --target) TARGET="$2"; shift 2 ;;
17
+ --simulator) SIMULATOR="$2"; shift 2 ;;
18
+ --adb-serial) ADB_SERIAL_ARG="$2"; shift 2 ;;
19
+ -h|--help)
20
+ printf 'Usage: reset-app-data.sh --platform ios|android [--target <dir>] [--simulator <udid|name>] [--adb-serial <serial>]\n'
21
+ exit 0
22
+ ;;
23
+ *) printf 'reset-app-data: unknown arg: %s\n' "$1" >&2; exit 2 ;;
24
+ esac
25
+ done
26
+
27
+ TARGET="$(cd "$TARGET" && pwd -P)"
28
+
29
+ reset_ios() {
30
+ local bundle_id="" app_source="" candidate="" staged_app=""
31
+ local temp_root="${TMPDIR:-/tmp}" staging_dir=""
32
+ local -a candidates installed_ids installed_paths
33
+
34
+ temp_root="${temp_root%/}"
35
+ [ -n "$temp_root" ] && [ "$temp_root" != "/" ] && [ -d "$temp_root" ] && [ ! -L "$temp_root" ] || {
36
+ printf 'reset-app-data: unsafe temporary directory: %s\n' "$temp_root" >&2
37
+ return 1
38
+ }
39
+ if [ -n "${IOS_BUNDLE_ID:-}" ]; then
40
+ candidates=("$IOS_BUNDLE_ID")
41
+ else
42
+ candidates=("io.metamask.MetaMask" "io.metamask" "io.metamask.MetaMask-Flask" "io.metamask.qa")
43
+ fi
44
+ for candidate in "${candidates[@]}"; do
45
+ if app_source="$(xcrun simctl get_app_container "$SIMULATOR" "$candidate" app 2>/dev/null)"; then
46
+ installed_ids+=("$candidate")
47
+ installed_paths+=("$app_source")
48
+ fi
49
+ done
50
+ if [ "${#installed_ids[@]}" -gt 1 ]; then
51
+ printf 'reset-app-data: multiple MetaMask iOS variants are installed on simulator %s: %s.\n' "$SIMULATOR" "${installed_ids[*]}" >&2
52
+ printf 'Next: set IOS_BUNDLE_ID to the exact variant and retry.\n' >&2
53
+ return 1
54
+ fi
55
+ bundle_id="${installed_ids[0]:-}"
56
+ app_source="${installed_paths[0]:-}"
57
+ [ -n "$bundle_id" ] && [ -n "$app_source" ] || {
58
+ printf 'reset-app-data: no installed MetaMask app was found on iOS simulator %s.\n' "$SIMULATOR" >&2
59
+ printf "Next: mm-harness launch --adapter mobile --platform ios --target '%s'\n" "$TARGET" >&2
60
+ return 1
61
+ }
62
+ case "$app_source" in
63
+ */Library/Developer/CoreSimulator/Devices/*/data/Containers/Bundle/Application/*/*.app) ;;
64
+ *) printf 'reset-app-data: refusing unexpected iOS app path: %s\n' "$app_source" >&2; return 1 ;;
65
+ esac
66
+ [ -d "$app_source" ] && [ ! -L "$app_source" ] || {
67
+ printf 'reset-app-data: iOS app is not a real directory: %s\n' "$app_source" >&2
68
+ return 1
69
+ }
70
+
71
+ staging_dir="$(mktemp -d "$temp_root/mm-harness-ios-reset.XXXXXX")"
72
+ case "$staging_dir" in
73
+ "$temp_root"/mm-harness-ios-reset.*) ;;
74
+ *) printf 'reset-app-data: refusing unexpected staging path: %s\n' "$staging_dir" >&2; return 1 ;;
75
+ esac
76
+ [ -d "$staging_dir" ] && [ ! -L "$staging_dir" ] || return 1
77
+ staged_app="$staging_dir/$(basename "$app_source")"
78
+ MM_RESET_TEMP_ROOT="$temp_root"
79
+ MM_RESET_STAGING_DIR="$staging_dir"
80
+ cleanup_staging() {
81
+ case "$MM_RESET_STAGING_DIR" in
82
+ "$MM_RESET_TEMP_ROOT"/mm-harness-ios-reset.*)
83
+ [ -d "$MM_RESET_STAGING_DIR" ] && [ ! -L "$MM_RESET_STAGING_DIR" ] && rm -r -- "$MM_RESET_STAGING_DIR"
84
+ ;;
85
+ *) printf 'reset-app-data: staging cleanup refused: %s\n' "$MM_RESET_STAGING_DIR" >&2 ;;
86
+ esac
87
+ }
88
+ trap cleanup_staging EXIT HUP INT TERM
89
+
90
+ ditto "$app_source" "$staged_app"
91
+ [ -d "$staged_app" ] && [ ! -L "$staged_app" ] || {
92
+ printf 'reset-app-data: failed to preserve the installed iOS app.\n' >&2
93
+ return 1
94
+ }
95
+ printf 'Resetting iOS app data: simulator=%s bundle=%s\n' "$SIMULATOR" "$bundle_id" >&2
96
+ xcrun simctl terminate "$SIMULATOR" "$bundle_id" >/dev/null 2>&1 || true
97
+ xcrun simctl uninstall "$SIMULATOR" "$bundle_id"
98
+ xcrun simctl install "$SIMULATOR" "$staged_app"
99
+ xcrun simctl get_app_container "$SIMULATOR" "$bundle_id" app >/dev/null
100
+ printf 'Mobile app data reset; preserved installed iOS build %s.\n' "$bundle_id" >&2
101
+ }
102
+
103
+ reset_android() {
104
+ local package_id="" candidate="" clear_output=""
105
+ local -a candidates adb_target installed_ids
106
+ if [ -z "$ADB_BIN" ]; then
107
+ ADB_BIN="$(command -v adb 2>/dev/null || true)"
108
+ fi
109
+ [ -n "$ADB_BIN" ] && [ "${ADB_BIN#/}" != "$ADB_BIN" ] && [ -x "$ADB_BIN" ] || {
110
+ printf 'reset-app-data: Android SDK Platform-Tools (adb) is required.\n' >&2
111
+ printf 'Next: brew install android-platform-tools\n' >&2
112
+ return 1
113
+ }
114
+ [ -n "$ADB_SERIAL_ARG" ] || {
115
+ printf 'reset-app-data: an Android serial is required.\n' >&2
116
+ printf 'Next: mm-harness devices --adapter mobile --json\n' >&2
117
+ return 1
118
+ }
119
+ adb_target=(-s "$ADB_SERIAL_ARG")
120
+ if [ -n "${ANDROID_PACKAGE_ID:-}" ]; then
121
+ candidates=("$ANDROID_PACKAGE_ID")
122
+ else
123
+ candidates=("io.metamask" "io.metamask.flask" "io.metamask.qa")
124
+ fi
125
+ for candidate in "${candidates[@]}"; do
126
+ if "$ADB_BIN" "${adb_target[@]}" shell pm path "$candidate" 2>/dev/null | grep -q '^package:'; then
127
+ installed_ids+=("$candidate")
128
+ fi
129
+ done
130
+ if [ "${#installed_ids[@]}" -gt 1 ]; then
131
+ printf 'reset-app-data: multiple MetaMask Android variants are installed on device %s: %s.\n' "$ADB_SERIAL_ARG" "${installed_ids[*]}" >&2
132
+ printf 'Next: set ANDROID_PACKAGE_ID to the exact variant and retry.\n' >&2
133
+ return 1
134
+ fi
135
+ package_id="${installed_ids[0]:-}"
136
+ [ -n "$package_id" ] || {
137
+ printf 'reset-app-data: no installed MetaMask app was found on Android device %s.\n' "$ADB_SERIAL_ARG" >&2
138
+ printf "Next: mm-harness launch --adapter mobile --platform android --target '%s' --device '%s'\n" "$TARGET" "$ADB_SERIAL_ARG" >&2
139
+ return 1
140
+ }
141
+ printf 'Resetting Android app data: device=%s package=%s\n' "$ADB_SERIAL_ARG" "$package_id" >&2
142
+ clear_output="$("$ADB_BIN" "${adb_target[@]}" shell pm clear "$package_id")"
143
+ [ "$clear_output" = "Success" ] || {
144
+ printf 'reset-app-data: Android package clear failed: %s\n' "$clear_output" >&2
145
+ return 1
146
+ }
147
+ printf 'Mobile app data reset; preserved installed Android build %s.\n' "$package_id" >&2
148
+ }
149
+
150
+ case "$PLATFORM" in
151
+ ios) reset_ios ;;
152
+ android) reset_android ;;
153
+ *) printf 'reset-app-data: --platform must be ios or android (got: %s)\n' "$PLATFORM" >&2; exit 2 ;;
154
+ esac