@deeeed/metamask-harness 0.40.1 → 0.41.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/adapters/manifest.json +25 -1
  3. package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +5 -4
  4. package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +24 -5
  5. package/adapters/mobile/coalesce-metro-log.cjs +24 -0
  6. package/adapters/mobile/launch-metro.cjs +9 -8
  7. package/adapters/mobile/metro-config.cjs +94 -0
  8. package/adapters/mobile/metro-log-generation.cjs +106 -0
  9. package/adapters/mobile/start-metro.sh +30 -8
  10. package/adapters/mobile/stop-metro.sh +5 -6
  11. package/adapters/shared/reap-checkout-metros.sh +17 -0
  12. package/dist/adapters/mobile/runtime-decision.js +0 -21
  13. package/dist/commands/device-target.js +3 -0
  14. package/dist/commands/launch/index.js +25 -5
  15. package/dist/commands/run-engine.js +4 -14
  16. package/dist/heal-bounds.js +1 -0
  17. package/dist/live-adapter-contract.js +2 -1
  18. package/dist/mm-harness-cli.js +1 -1
  19. package/docs/QA.md +2 -0
  20. package/docs/RECIPES.md +25 -19
  21. package/library/actions/mobile/perps/capture_performance.mjs +2 -2
  22. package/library/actions/mobile/perps/performance-capture.mjs +375 -17
  23. package/library/actions/mobile/perps/perps.mjs +56 -10
  24. package/library/actions/mobile/platform/bridge.mjs +59 -1
  25. package/library/actions/mobile/wallet/ensure_unlocked.mjs +60 -26
  26. package/library/actions/mobile/wallet/import.mjs +2 -0
  27. package/library/actions/mobile/wallet/select_account.mjs +21 -2
  28. package/library/manifests/mobile.action-manifest.json +47 -6
  29. package/library/recipes/mobile/perps/performance.recipe.json +680 -26
  30. package/package.json +1 -1
  31. package/library/recipes/mobile/perps/performance.background-resume.recipe.json +0 -56
  32. package/library/recipes/mobile/perps/performance.cold-start.recipe.json +0 -56
  33. package/library/recipes/mobile/perps/performance.homepage.android-background-reconnect.recipe.json +0 -159
  34. package/library/recipes/mobile/perps/performance.homepage.android-background-short.recipe.json +0 -157
  35. package/library/recipes/mobile/perps/performance.homepage.android-cold-disk-cache.recipe.json +0 -165
  36. package/library/recipes/mobile/perps/performance.homepage.android-cold-no-cache.recipe.json +0 -139
  37. package/library/recipes/mobile/perps/performance.homepage.android-network-recovery.recipe.json +0 -149
  38. package/library/recipes/mobile/perps/performance.homepage.cold-position-sample.recipe.json +0 -120
  39. package/library/recipes/mobile/perps/performance.homepage.ios-background-reconnect.recipe.json +0 -111
  40. package/library/recipes/mobile/perps/performance.homepage.ios-background-short.recipe.json +0 -108
  41. package/library/recipes/mobile/perps/performance.homepage.ios-cold-disk-cache.recipe.json +0 -122
  42. package/library/recipes/mobile/perps/performance.homepage.ios-cold-no-cache.recipe.json +0 -95
  43. package/library/recipes/mobile/perps/performance.warm-start.recipe.json +0 -49
@@ -53,6 +53,21 @@ const LAUNCH_BOOLEANS = /* @__PURE__ */ new Set([
53
53
  "jsonStream"
54
54
  ]);
55
55
  const DEFAULT_EXTENSION_DAPP_URL = "https://metamask.github.io/test-dapp/";
56
+ function metroLaunchEvidence(adapter, target) {
57
+ if (adapter !== "mobile") return void 0;
58
+ const evidencePath = recipeRuntimePath(target, "metro-generation.json");
59
+ let descriptor;
60
+ try {
61
+ descriptor = fs.openSync(evidencePath, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
62
+ const stat = fs.fstatSync(descriptor);
63
+ if (!stat.isFile() || stat.size > 256 * 1024) return void 0;
64
+ return JSON.parse(fs.readFileSync(descriptor, "utf8"));
65
+ } catch {
66
+ return void 0;
67
+ } finally {
68
+ if (descriptor !== void 0) fs.closeSync(descriptor);
69
+ }
70
+ }
56
71
  async function handleLaunch(argv) {
57
72
  const { options } = parseFlags(argv, LAUNCH_BOOLEANS);
58
73
  const json = flag(options, "json");
@@ -627,14 +642,15 @@ async function finishLaunch(jsonOutput, machine, stream, adapter, mobileTarget,
627
642
  platform: mobileTarget ?? (adapter === "extension" ? displayMode : null),
628
643
  tier,
629
644
  recovered: state.recovered,
630
- mutations: state.mutations
645
+ mutations: state.mutations,
646
+ metro: metroLaunchEvidence(adapter, target)
631
647
  });
632
648
  }
633
649
  return exitCode;
634
650
  }
635
- return launchPass(jsonOutput, stream, adapter, mobileTarget, tier, displayMode, state);
651
+ return launchPass(jsonOutput, stream, adapter, mobileTarget, tier, displayMode, target, state);
636
652
  }
637
- function launchPass(json, stream, adapter, mobileTarget, tier, displayMode, state) {
653
+ function launchPass(json, stream, adapter, mobileTarget, tier, displayMode, target, state) {
638
654
  if (stream.enabled) {
639
655
  for (const mutation of state.mutations) stream.mutation(mutation);
640
656
  for (const recovery of state.recovered) stream.recovery(recovery);
@@ -643,7 +659,8 @@ function launchPass(json, stream, adapter, mobileTarget, tier, displayMode, stat
643
659
  platform: mobileTarget ?? (adapter === "extension" ? displayMode : null),
644
660
  tier,
645
661
  recovered: state.recovered,
646
- mutations: state.mutations
662
+ mutations: state.mutations,
663
+ metro: metroLaunchEvidence(adapter, target)
647
664
  });
648
665
  } else if (json) {
649
666
  console.log(
@@ -658,6 +675,7 @@ function launchPass(json, stream, adapter, mobileTarget, tier, displayMode, stat
658
675
  phase: "launch",
659
676
  recovered: state.recovered,
660
677
  mutations: state.mutations,
678
+ metro: metroLaunchEvidence(adapter, target),
661
679
  exitCode: EXIT.ok
662
680
  },
663
681
  null,
@@ -706,7 +724,8 @@ function launchFail(json, stream, adapter, mobileTarget, tier, state, target, fa
706
724
  recovered: state.recovered,
707
725
  mutations: state.mutations,
708
726
  recoverable: failure.recoverable,
709
- attemptedRecoveries: state.attemptedRecoveries
727
+ attemptedRecoveries: state.attemptedRecoveries,
728
+ metro: metroLaunchEvidence(adapter, target)
710
729
  });
711
730
  } else if (json) {
712
731
  console.log(
@@ -723,6 +742,7 @@ function launchFail(json, stream, adapter, mobileTarget, tier, state, target, fa
723
742
  mutations: state.mutations,
724
743
  recoverable: failure.recoverable,
725
744
  attemptedRecoveries: state.attemptedRecoveries,
745
+ metro: metroLaunchEvidence(adapter, target),
726
746
  exitCode: failure.exitCode,
727
747
  error: {
728
748
  code: failure.code,
@@ -155,12 +155,11 @@ async function resolveRecipeExecution(adapter, recipe, artifactsDir, projectRoot
155
155
  projectRoot,
156
156
  rootRecipe: recipeDocument
157
157
  });
158
- const suppressHudForLifecycleTiming = adapter === "mobile" && mobileRecipeRequiresUninterruptedLifecycleTiming(recipeDocument);
159
158
  const suppressHudForOpaqueMobile = adapter === "mobile" && process.env.METAMASK_RECIPE_MOBILE_OPAQUE_RUNTIME === "1";
160
159
  const runnerOptions = {
161
160
  quietStdout: runtimeOptions.stdoutIsMachineContract === true,
162
161
  suppressLibraryResolutionLogs: runtimeOptions.suppressLibraryResolutionLogs,
163
- autoHud: suppressHudForLifecycleTiming || suppressHudForOpaqueMobile ? false : trust.source && trust.source.trust !== "trusted" ? false : runtimeOptions.autoHud,
162
+ autoHud: suppressHudForOpaqueMobile ? false : trust.source && trust.source.trust !== "trusted" ? false : runtimeOptions.autoHud,
164
163
  onActionEvent: runtimeOptions.onActionEvent,
165
164
  actionSources,
166
165
  // A direct engineer invocation is the explicit trust boundary for a
@@ -213,16 +212,6 @@ function runtimeRecipeDocument(recipe, absoluteRecipePath) {
213
212
  return void 0;
214
213
  }
215
214
  }
216
- function mobileRecipeRequiresUninterruptedLifecycleTiming(recipe) {
217
- if (!isRecord(recipe)) return false;
218
- const workflow = isRecord(recipe.workflow) ? recipe.workflow : void 0;
219
- const nodes = workflow && isRecord(workflow.nodes) ? workflow.nodes : void 0;
220
- if (!nodes) return false;
221
- const actions = new Set(
222
- Object.values(nodes).filter(isRecord).map((node) => node.action).filter((action) => typeof action === "string")
223
- );
224
- return actions.has("app.lifecycle") && actions.has("metamask.perps.capture_performance");
225
- }
226
215
  function recipeProcessEnvironment() {
227
216
  const env = { ...process.env };
228
217
  delete env.MM_HARNESS_CHECKOUT_LOCK_TOKEN;
@@ -314,7 +303,8 @@ function recipeRunEnv(adapter, runtimeOptions = {}) {
314
303
  RECIPE_CDP_PORT: runtimeOptions.cdpPort ?? process.env.RECIPE_CDP_PORT,
315
304
  FARMSLOT_SLOT_ID: runtimeOptions.slot ?? process.env.FARMSLOT_SLOT_ID,
316
305
  SLOT_ID: runtimeOptions.slot ?? process.env.SLOT_ID,
317
- PLATFORM: process.env.PLATFORM
306
+ PLATFORM: process.env.PLATFORM,
307
+ MM_HARNESS_EXPLICIT_PLATFORM: process.env.MM_HARNESS_EXPLICIT_PLATFORM
318
308
  };
319
309
  if (adapter !== "mobile") return base;
320
310
  return {
@@ -322,6 +312,7 @@ function recipeRunEnv(adapter, runtimeOptions = {}) {
322
312
  WATCHER_PORT: process.env.WATCHER_PORT ?? base.CDP_PORT ?? base.RECIPE_CDP_PORT,
323
313
  METRO_PORT: process.env.METRO_PORT ?? process.env.WATCHER_PORT ?? base.CDP_PORT ?? base.RECIPE_CDP_PORT,
324
314
  IOS_SIMULATOR: process.env.IOS_SIMULATOR,
315
+ SIM_UDID: process.env.SIM_UDID,
325
316
  ANDROID_DEVICE: process.env.ANDROID_DEVICE,
326
317
  ANDROID_TARGET_DEVICE_NAME: process.env.ANDROID_TARGET_DEVICE_NAME,
327
318
  ADB_SERIAL: process.env.ADB_SERIAL,
@@ -1378,7 +1369,6 @@ export {
1378
1369
  ensureMobileProofRuntime,
1379
1370
  executeWithHealBounds,
1380
1371
  listRunnableRecipes,
1381
- mobileRecipeRequiresUninterruptedLifecycleTiming,
1382
1372
  preflightRecipe,
1383
1373
  prepareHeal,
1384
1374
  prepareRuntimeIfNeeded,
@@ -62,6 +62,7 @@ async function ensureOverlay(adapter, target, heal, state, json) {
62
62
  }
63
63
  function classifyFailure(output) {
64
64
  if (/SCREENSHOT_PROTECTED|screenshot is protected by FLAG_SECURE/iu.test(output)) return "screenshot-protected";
65
+ if (/CDP_TIMEOUT|Mobile CDP bridge command timed out|NO_TARGET|WS_CLOSED/iu.test(output)) return "infra";
65
66
  if (/wallet|fixture|keyring|not seeded|\bsrp\b|password|onboard/iu.test(output)) return "wallet";
66
67
  if (/metro|cdp|chrome|bridge|bundle|packager|port\b|econnrefused|not reachable|watcher|dev client|websocket/iu.test(output)) {
67
68
  return "infra";
@@ -359,7 +359,8 @@ function liveAdapterProcessTimeoutMs(node) {
359
359
  const actionTimeouts = [
360
360
  node.target_timeout_ms,
361
361
  node.unlock_timeout_ms,
362
- node.timeout_ms
362
+ node.timeout_ms,
363
+ node.wait_timeout_ms
363
364
  ].map(Number).filter((value) => Number.isFinite(value) && value > 0);
364
365
  if (actionTimeouts.length > 0) {
365
366
  return Math.max(
@@ -243,7 +243,7 @@ Common overrides:
243
243
  --json Machine-readable output
244
244
  --json-stream Line-flushed JSONL progress + terminal event
245
245
  --record-video=full-run Record a video of the run
246
- --hud <auto|show|hide> Recipe HUD policy (default: auto)
246
+ --hud <auto|show|hide> Recipe HUD policy (auto shows normal Mobile recipes; hide is explicit)
247
247
 
248
248
  Advanced integrations:
249
249
  runtime: --cdp-port, --watcher-port/--metro-port, --runtime-dir, --slot,
package/docs/QA.md CHANGED
@@ -124,6 +124,8 @@ mm-harness debug
124
124
  - [ ] Status matches verified product routes: `Login`/`LockScreen` are locked;
125
125
  `WalletView` is unlocked.
126
126
  - [ ] App and Metro logs are separate.
127
+ - [ ] A restarted Metro rotates the previous `metro.log`, records its generation and retention actions in launch JSON, and classifies bundle failures only from the current log.
128
+ - [ ] `mm-harness logs --full` preserves errors, completion, and meaningful timestamped bundle progress without repeated same-percentage module counts.
127
129
  - [ ] A JS edit rebuilds through Metro and appears after reload without a native
128
130
  rebuild; revert restores a clean tree.
129
131
 
package/docs/RECIPES.md CHANGED
@@ -176,30 +176,36 @@ For visual PR proof, prefer a short simulator recording or GIF over still
176
176
  screenshots when the claim is a UI flow (timing / transitions). Use screenshots
177
177
  as supporting stills.
178
178
 
179
- ### Perps Homepage visible performance
179
+ ### Perps loading lifecycle validation
180
180
 
181
- The bundled `perps.performance.homepage.*` family preserves the device-proven
182
- Homepage TTC/DFD journeys. Run every recipe with an explicit device pin,
183
- healing disabled, and full-run video:
181
+ One platform-neutral recipe owns the reusable Perps lifecycle proof. The
182
+ harness adapter supplies Android or iOS behavior; the graph and lifecycle
183
+ parameters remain identical. Run it with an explicit device pin:
184
184
 
185
185
  ```bash
186
- mm-harness run perps.performance.homepage.android-background-short --device <serial> --heal off --record-video=full-run
187
- mm-harness run perps.performance.homepage.android-background-reconnect --device <serial> --heal off --record-video=full-run
188
- mm-harness run perps.performance.homepage.android-cold-no-cache --device <serial> --heal off --record-video=full-run
189
- mm-harness run perps.performance.homepage.android-cold-disk-cache --device <serial> --heal off --record-video=full-run
190
- mm-harness run perps.performance.homepage.android-network-recovery --device <serial> --heal off --record-video=full-run
191
- mm-harness run perps.performance.homepage.ios-background-short --device <udid> --heal off --record-video=full-run
192
- mm-harness run perps.performance.homepage.ios-background-reconnect --device <udid> --heal off --record-video=full-run
193
- mm-harness run perps.performance.homepage.ios-cold-no-cache --device <udid> --heal off --record-video=full-run
194
- mm-harness run perps.performance.homepage.ios-cold-disk-cache --device <udid> --heal off --record-video=full-run
186
+ mm-harness run perps.performance \
187
+ --device <android-serial-or-ios-udid> \
188
+ account=<fixture-account-name> \
189
+ content_variant=trending \
190
+ market=BTC \
191
+ lifecycle=cold_no_cache \
192
+ --heal off --record-video=full-run
195
193
  ```
196
194
 
197
- The Android variants use a deterministic Hyperliquid testnet order and cancel
198
- it after capture. The iOS variants use the existing fixture wallet and validate
199
- trending content without fabricating position/order state. `app.network` is
200
- Android-only because host-wide iOS network mutation cannot isolate one
201
- simulator safely. These recipes validate viewport-demand-to-visible TTC/DFD;
202
- they do not relabel process-start duration as TTC.
195
+ The Mobile recipe HUD is shown by default, including during performance
196
+ validation. Use `--hud hide` only when a specific opaque/release proof requires
197
+ an unobstructed product surface; do not hide it merely to collect timings.
198
+
199
+ Supported lifecycle values are `navigate_return`, `cold_disk_cache`,
200
+ `cold_no_cache`, `background_short`, `background_reconnect`, `account_switch`,
201
+ and `network_switch`. The shared graph uses an existing fixture position and
202
+ never fabricates exchange state inside the measurement. It proves native
203
+ visible content with `ui.wait_for`, captures production `[PerpsLoadProof]` and
204
+ WebSocket milestones, and leaves detailed TTC/DFD to the app's Sentry traces.
205
+ It never relabels Metro, fixture, or process-start duration as TTC.
206
+ The recipe does not label a checkout as main or candidate; pin and record the
207
+ exact checkout commit externally, then use captured source milestones to verify
208
+ which data path actually ran.
203
209
 
204
210
  Rules:
205
211
 
@@ -1,4 +1,4 @@
1
1
  import { runAdapter } from '../platform/bridge.mjs';
2
- import { captureHomepagePerformance } from './performance-capture.mjs';
2
+ import { capturePerpsPerformance } from './performance-capture.mjs';
3
3
 
4
- runAdapter(captureHomepagePerformance);
4
+ runAdapter(capturePerpsPerformance);