@deeeed/metamask-harness 0.13.0 → 0.14.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 (79) hide show
  1. package/CHANGELOG.md +17 -4
  2. package/adapters/core/cleanup.sh +0 -0
  3. package/adapters/core/inject.sh +0 -0
  4. package/adapters/extension/cleanup.mjs +0 -0
  5. package/adapters/extension/ensure-browser.sh +0 -0
  6. package/adapters/extension/inject.mjs +0 -0
  7. package/adapters/extension/launch-browser.cjs +0 -0
  8. package/adapters/extension/launch.sh +0 -0
  9. package/adapters/extension/live.sh +0 -0
  10. package/adapters/extension/readiness.mjs +0 -0
  11. package/adapters/extension/reattach.sh +0 -0
  12. package/adapters/extension/refresh-build.sh +0 -0
  13. package/adapters/extension/seed-fixture.sh +0 -0
  14. package/adapters/extension/sidepanel-toggle.sh +0 -0
  15. package/adapters/extension/snapshot-dist.sh +0 -0
  16. package/adapters/extension/start-watch.sh +0 -0
  17. package/adapters/extension/verify.sh +4 -4
  18. package/adapters/extension/wallet-fixture-state.cjs +0 -0
  19. package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +0 -0
  20. package/adapters/mobile/bridge-runtime/setup-wallet.sh +0 -0
  21. package/adapters/mobile/cleanup.sh +0 -0
  22. package/adapters/mobile/inject.sh +0 -0
  23. package/adapters/mobile/lib/metro-listener.sh +0 -0
  24. package/adapters/mobile/lib/tmux-viewer.sh +0 -0
  25. package/adapters/mobile/open-device.sh +0 -0
  26. package/adapters/mobile/prewarm-bundle.sh +0 -0
  27. package/adapters/mobile/start-metro.sh +15 -1
  28. package/adapters/mobile/stop-metro.sh +0 -0
  29. package/adapters/mobile/verify.sh +50 -13
  30. package/adapters/mobile/wait-for-bridge.sh +0 -0
  31. package/adapters/mobile/yarn-setup.sh +0 -0
  32. package/adapters/shared/activate-repo-node.sh +0 -0
  33. package/adapters/shared/activate-repo-ruby.sh +0 -0
  34. package/adapters/shared/cli-ux.sh +0 -0
  35. package/adapters/shared/ensure-runner-deps.sh +0 -0
  36. package/adapters/shared/harness-path.sh +0 -0
  37. package/adapters/shared/hash-helpers.sh +0 -0
  38. package/adapters/shared/json-field.sh +0 -0
  39. package/adapters/shared/log-tui.mjs +7 -3
  40. package/adapters/shared/open-log-window.sh +0 -0
  41. package/adapters/shared/reap-checkout-metros.sh +0 -0
  42. package/adapters/shared/resolve-farmslot-ports.mjs +0 -0
  43. package/adapters/shared/resolve-farmslot-ports.sh +0 -0
  44. package/adapters/shared/resolve-slot-ports.mjs +0 -0
  45. package/adapters/shared/resolve-slot-ports.sh +0 -0
  46. package/adapters/shared/sync-wallet-fixture.sh +0 -0
  47. package/adapters/shared/tmux-session.sh +0 -0
  48. package/dist/cli-commands.js +5 -4
  49. package/dist/commands/call.js +61 -5
  50. package/dist/commands/device-target.js +10 -3
  51. package/dist/commands/fixtures.js +2 -1
  52. package/dist/commands/run-engine.js +62 -3
  53. package/dist/commands/run.js +2 -0
  54. package/dist/commands/self-test.js +1 -1
  55. package/dist/commands/status.js +19 -4
  56. package/dist/mm-harness-cli.js +7 -5
  57. package/dist/recipe-files.js +14 -0
  58. package/docs/architecture.md +2 -2
  59. package/docs/live-adapter-contract.md +1 -1
  60. package/docs/recipe-libraries.md +21 -18
  61. package/library/recipes/{app-lifecycle-android-smoke.mobile.recipe.json → app/lifecycle.android-smoke.mobile.recipe.json} +1 -1
  62. package/library/recipes/{perps-performance.mobile.recipe.json → perps/performance.mobile.recipe.json} +1 -1
  63. package/library/recipes/perps/smoke.core.recipe.json +39 -0
  64. package/library/recipes/perps/smoke.extension.recipe.json +51 -0
  65. package/library/recipes/perps/smoke.mobile.recipe.json +51 -0
  66. package/library/recipes/{action-validation.extension.recipe.json → runner/action-validation.extension.recipe.json} +7 -7
  67. package/library/recipes/{action-validation.mobile.recipe.json → runner/action-validation.mobile.recipe.json} +7 -7
  68. package/package.json +2 -2
  69. package/scripts/completions.sh +0 -0
  70. package/scripts/install-completions.sh +0 -0
  71. /package/library/recipes/{perps-lifecycle.recipe.json → perps/lifecycle.recipe.json} +0 -0
  72. /package/library/recipes/{order-lifecycle.core.recipe.json → perps/order-lifecycle.core.recipe.json} +0 -0
  73. /package/library/recipes/{perps-performance-background-resume.mobile.recipe.json → perps/performance.background-resume.mobile.recipe.json} +0 -0
  74. /package/library/recipes/{perps-performance-cold-start.mobile.recipe.json → perps/performance.cold-start.mobile.recipe.json} +0 -0
  75. /package/library/recipes/{perps-performance-warm-start.mobile.recipe.json → perps/performance.warm-start.mobile.recipe.json} +0 -0
  76. /package/library/recipes/{read-markets.core.recipe.json → perps/read-markets.core.recipe.json} +0 -0
  77. /package/library/recipes/{trading-lifecycle.core.recipe.json → perps/trading-lifecycle.core.recipe.json} +0 -0
  78. /package/library/recipes/{smoke.extension.recipe.json → runner/smoke.extension.recipe.json} +0 -0
  79. /package/library/recipes/{smoke.mobile.recipe.json → runner/smoke.mobile.recipe.json} +0 -0
@@ -18,6 +18,7 @@ import {
18
18
  runnerDir
19
19
  } from "../paths.js";
20
20
  import { captureHelperSupportsRecordSessionSnapshots } from "../recording-target.js";
21
+ import { listRecipeFiles } from "../recipe-files.js";
21
22
  import { startRecipeRecording, stopRecipeRecording } from "../run-recording.js";
22
23
  import { EXIT } from "./shared.js";
23
24
  import {
@@ -152,7 +153,7 @@ function resolveRunRecipeArg(recipeArg, adapter, librarySources) {
152
153
  const direct = path.resolve(recipeArg);
153
154
  if (isRecipeFile(direct)) return { recipeFile: direct };
154
155
  if (!recipeArg.includes("/") && !recipeArg.includes(path.sep)) {
155
- const candidates = [`${recipeArg}.${adapter}.recipe.json`, `${recipeArg}.recipe.json`, recipeArg];
156
+ const candidates = recipeNameCandidates(recipeArg, adapter);
156
157
  if (librarySources && librarySources.length > 0) {
157
158
  for (const source of librarySources) {
158
159
  for (const candidate of candidates) {
@@ -176,17 +177,75 @@ function resolveRunRecipeArg(recipeArg, adapter, librarySources) {
176
177
  function libraryRecipeNames(adapter) {
177
178
  let entries;
178
179
  try {
179
- entries = fs.readdirSync(recipePath(""));
180
+ entries = listRecipeFiles(recipePath(""));
180
181
  } catch {
181
182
  return [];
182
183
  }
183
184
  const scopes = ["mobile", "extension", "core"];
184
- const names = entries.filter((f) => f.endsWith(".recipe.json")).map((f) => f.slice(0, -".recipe.json".length)).filter((n) => {
185
+ const names = entries.filter((f) => f.endsWith(".recipe.json")).map((f) => f.slice(0, -".recipe.json".length).replace(/[\\/]/gu, ".")).filter((n) => {
185
186
  const scope = n.split(".").pop() ?? "";
186
187
  return !scopes.includes(scope) || scope === adapter;
187
188
  }).map((n) => n.endsWith(`.${adapter}`) ? n.slice(0, -(adapter.length + 1)) : n);
188
189
  return [...new Set(names)].sort();
189
190
  }
191
+ function recipeNameCandidates(recipeArg, adapter) {
192
+ const names = [recipeArg, ...LEGACY_RECIPE_ALIASES[recipeArg] ?? []];
193
+ const candidates = [];
194
+ for (const name of names) {
195
+ candidates.push(...singleRecipeNameCandidates(name, adapter));
196
+ }
197
+ return [...new Set(candidates)];
198
+ }
199
+ function singleRecipeNameCandidates(name, adapter) {
200
+ if (name.endsWith(".recipe.json")) {
201
+ return uniqueStrings([domainPathForRecipeFile(name), name]);
202
+ }
203
+ const bases = uniqueStrings([domainPathForRecipeName(name), name]);
204
+ const candidates = [];
205
+ for (const base of bases) {
206
+ candidates.push(`${base}.${adapter}.recipe.json`, `${base}.recipe.json`, base);
207
+ }
208
+ return candidates;
209
+ }
210
+ function domainPathForRecipeFile(name) {
211
+ const suffix = ".recipe.json";
212
+ const base = name.slice(0, -suffix.length);
213
+ return `${domainPathForRecipeName(base)}${suffix}`;
214
+ }
215
+ function domainPathForRecipeName(name) {
216
+ if (name.includes("/") || name.includes(path.sep)) return name;
217
+ const dot = name.indexOf(".");
218
+ return dot > 0 ? `${name.slice(0, dot)}/${name.slice(dot + 1)}` : name;
219
+ }
220
+ function uniqueStrings(values) {
221
+ return [...new Set(values)];
222
+ }
223
+ const LEGACY_RECIPE_ALIASES = {
224
+ "action-validation": ["runner.action-validation"],
225
+ "action-validation.extension.recipe.json": ["runner.action-validation.extension.recipe.json"],
226
+ "action-validation.mobile.recipe.json": ["runner.action-validation.mobile.recipe.json"],
227
+ "app-lifecycle-android-smoke": ["app.lifecycle.android-smoke"],
228
+ "app-lifecycle-android-smoke.mobile.recipe.json": ["app.lifecycle.android-smoke.mobile.recipe.json"],
229
+ "order-lifecycle": ["perps.order-lifecycle"],
230
+ "order-lifecycle.core.recipe.json": ["perps.order-lifecycle.core.recipe.json"],
231
+ "perps-lifecycle": ["perps.lifecycle"],
232
+ "perps-lifecycle.recipe.json": ["perps.lifecycle.recipe.json"],
233
+ "perps-performance": ["perps.performance"],
234
+ "perps-performance-background-resume": ["perps.performance.background-resume"],
235
+ "perps-performance-background-resume.mobile.recipe.json": ["perps.performance.background-resume.mobile.recipe.json"],
236
+ "perps-performance-cold-start": ["perps.performance.cold-start"],
237
+ "perps-performance-cold-start.mobile.recipe.json": ["perps.performance.cold-start.mobile.recipe.json"],
238
+ "perps-performance-warm-start": ["perps.performance.warm-start"],
239
+ "perps-performance-warm-start.mobile.recipe.json": ["perps.performance.warm-start.mobile.recipe.json"],
240
+ "perps-performance.mobile.recipe.json": ["perps.performance.mobile.recipe.json"],
241
+ "read-markets": ["perps.read-markets"],
242
+ "read-markets.core.recipe.json": ["perps.read-markets.core.recipe.json"],
243
+ "smoke": ["runner.smoke"],
244
+ "smoke.extension.recipe.json": ["runner.smoke.extension.recipe.json"],
245
+ "smoke.mobile.recipe.json": ["runner.smoke.mobile.recipe.json"],
246
+ "trading-lifecycle": ["perps.trading-lifecycle"],
247
+ "trading-lifecycle.core.recipe.json": ["perps.trading-lifecycle.core.recipe.json"]
248
+ };
190
249
  async function validateRunRecipeStatic(recipeArg, adapter, options) {
191
250
  const librarySources = await resolveMetaMaskLibrarySources(optionString(options, "library"));
192
251
  const resolved = resolveRunRecipeArg(recipeArg, adapter, librarySources);
@@ -20,6 +20,7 @@ import {
20
20
  } from "./run-engine.js";
21
21
  import { handleListExecutables } from "./list-executables.js";
22
22
  import { applyDeviceTargeting } from "./device-target.js";
23
+ import { getAdapterSurface } from "../adapters/surface.js";
23
24
  async function handleRun({ positional, options }) {
24
25
  if (optionFlag(options, "list")) return handleListExecutables("run", options);
25
26
  const targetRecipe = positional[0];
@@ -27,6 +28,7 @@ async function handleRun({ positional, options }) {
27
28
  if (optionFlag(options, "plan")) return handleRunPlan(targetRecipe, options);
28
29
  const { adapter, target } = resolveAdapter(options);
29
30
  const json = optionFlag(options, "json");
31
+ getAdapterSurface(adapter).resolveSlotPorts(target);
30
32
  const dtResult = applyDeviceTargeting("run", adapter, options, { gate: true, rerun: "" });
31
33
  if ("code" in dtResult) {
32
34
  return emitRunUsageError(json, adapter, targetRecipe, dtResult.code, dtResult.message);
@@ -26,7 +26,7 @@ async function runSelfTest(options) {
26
26
  const manifest = loadActionManifest(adapter);
27
27
  const manifestValidation = await validateManifest(manifest);
28
28
  const smokeRecipe = recipePath(
29
- adapter === "mobile" ? "smoke.mobile.recipe.json" : "smoke.extension.recipe.json"
29
+ adapter === "mobile" ? "runner/smoke.mobile.recipe.json" : "runner/smoke.extension.recipe.json"
30
30
  );
31
31
  const artifactsDir = path.join(root, adapter);
32
32
  const result = await runRecipe(adapter, smokeRecipe, artifactsDir, runnerDir, void 0, {
@@ -16,8 +16,10 @@ async function handleStatus({ options }) {
16
16
  return usageOut(json, "status", `could not detect the MetaMask repo type for ${target}`, ADAPTER_DETECT_NEXT);
17
17
  }
18
18
  assertAdapter(adapter);
19
- const devices = adapter === "mobile" ? listConnectedDevices().map((device) => ({ ...device, selected: deviceSelected(device) })) : [];
20
- const next = getAdapterSurface(adapter).hints.relaunch;
19
+ const surface = getAdapterSurface(adapter);
20
+ surface.resolveSlotPorts(target);
21
+ const next = surface.hints.relaunch;
22
+ const devices = adapter === "mobile" ? slotScopedDevices(listConnectedDevices()) : [];
21
23
  if (json) {
22
24
  const doProbe = !fast && adapter === "mobile" && devices.length > 0;
23
25
  if (doProbe) {
@@ -25,7 +27,7 @@ async function handleStatus({ options }) {
25
27
  const devicesWithLive = devices.map((d) => mergeDeviceLive(d, liveMap.get(d.id)));
26
28
  console.log(
27
29
  JSON.stringify(
28
- { schemaVersion: 1, command: "status", adapter, target, devices: devicesWithLive, next },
30
+ { schemaVersion: 1, command: "status", adapter, target, devices: devicesWithLive, next: nextForLive(next, liveMap) },
29
31
  null,
30
32
  2
31
33
  )
@@ -40,13 +42,26 @@ async function handleStatus({ options }) {
40
42
  const out = (style, text) => color(style, text, { stream: process.stdout });
41
43
  console.log(`${out("label", "status")} ${out("bold", adapter)} ${out("dim", target)}`);
42
44
  if (adapter === "mobile") renderDeviceList(devices, out);
43
- console.log(`${out("label", "Next:")} ${out("cmd", next)}`);
44
45
  if (!fast && adapter === "mobile" && devices.length > 0) {
45
46
  const liveMap = await probeMobileLiveState(target, devices);
46
47
  renderLiveBlock(devices, liveMap, out);
48
+ console.log(`${out("label", "Next:")} ${out("cmd", nextForLive(next, liveMap))}`);
49
+ } else {
50
+ console.log(`${out("label", "Next:")} ${out("cmd", next)}`);
47
51
  }
48
52
  return EXIT.ok;
49
53
  }
54
+ function nextForLive(fallback, liveMap) {
55
+ for (const live of liveMap.values()) {
56
+ if (live.liveState === void 0) return "mm-harness logs";
57
+ }
58
+ return fallback;
59
+ }
60
+ function slotScopedDevices(devices) {
61
+ const withSelection = devices.map((device) => ({ ...device, selected: deviceSelected(device) }));
62
+ const selected = withSelection.filter((device) => device.selected);
63
+ return selected.length > 0 ? selected : withSelection;
64
+ }
50
65
  function mergeDeviceLive(device, live) {
51
66
  if (!live) return device;
52
67
  return {
@@ -90,15 +90,16 @@ Example:
90
90
  name: "call",
91
91
  summary: "Run one action in isolation as a one-node recipe through the real engine path (fuzzy short names; --arg k=v; same trace/evidence as run).",
92
92
  example: "mm-harness call ensure_unlocked",
93
- helpText: `mm-harness call <action> [--arg k=v ...] [flags]
93
+ helpText: `mm-harness call <action> [key=value ...] [--arg k=v ...] [flags]
94
94
 
95
95
  Run one action in isolation as a one-node recipe through the real engine path.
96
96
  Fuzzy short-name: 'ensure_unlocked' resolves to 'metamask.wallet.ensure_unlocked'
97
- if unique; ambiguous = exit 2. Actions differ per adapter \u2014 list this checkout's
98
- with: mm-harness actions.
97
+ if unique; ambiguous = exit 2. Pass action fields as key=value shorthand or
98
+ with --arg k=v. Actions differ per adapter \u2014 list this checkout's with:
99
+ mm-harness actions.
99
100
 
100
101
  --list List everything invocable for the adapter (actions + flows); no <action> needed
101
- --arg k=v Action field value (repeatable)
102
+ --arg k=v Action field value (repeatable; equivalent to key=value shorthand)
102
103
  --device <udid|serial|name> Mobile only: target this device (env: IOS_SIMULATOR / ADB_SERIAL). Without it, >1 connected mobile device fails fast and lists them.
103
104
  --adapter <mobile|extension|core> Target adapter (auto-detected inside a checkout)
104
105
  --target <path> Checkout path (default: cwd)
@@ -110,7 +111,8 @@ Example:
110
111
 
111
112
  Example (real actions; run mm-harness actions for this checkout's full set):
112
113
  mm-harness call ensure_unlocked --adapter extension # a wallet action (extension/mobile)
113
- mm-harness call command --arg cmd="echo hi" --adapter core # the universal action (all adapters)`
114
+ mm-harness call navigate page=perps --adapter mobile
115
+ mm-harness call command cmd="echo hi" --adapter core # the universal action (all adapters)`
114
116
  },
115
117
  {
116
118
  name: "flows",
@@ -0,0 +1,14 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ function listRecipeFiles(root, base = "") {
4
+ const out = [];
5
+ for (const entry of fs.readdirSync(path.join(root, base), { withFileTypes: true })) {
6
+ const rel = path.join(base, entry.name);
7
+ if (entry.isDirectory()) out.push(...listRecipeFiles(root, rel));
8
+ else if (entry.isFile() && entry.name.endsWith(".recipe.json")) out.push(rel);
9
+ }
10
+ return out;
11
+ }
12
+ export {
13
+ listRecipeFiles
14
+ };
@@ -216,7 +216,7 @@ describes **how an agent should work**, it belongs in skills.
216
216
  | `adapters/extension/` | Runner-owned Extension launch/live/watch/windows/wallet-state/readiness helpers (orchestration) and verify (recipe) copied into installed harnesses. |
217
217
  | `adapters/shared/path-defaults.json` | Single source for default `recipeHarnessRoot` and `recipeRuntimeDir`. |
218
218
  | `adapters/shared/harness-path.sh`, `src/paths.ts` | Shell and TypeScript accessors for those defaults plus validation. |
219
- | `library/recipes/` | Reusable smoke/action-validation recipes only. Task-specific proof recipes stay task-local. |
219
+ | `library/recipes/` | Reusable smoke/runner.action-validation recipes only. Task-specific proof recipes stay task-local. |
220
220
  | `library/library.json`, `library/flows/` | Canonical recipe library: minimal, stable flows recipes can `call`. Personal/team libraries shadow it by default — see [Recipe Libraries](recipe-libraries.md). |
221
221
  | `docs/` | Runner architecture, contracts, and operational conventions. |
222
222
 
@@ -377,7 +377,7 @@ TypeScript or a focused `.mjs` adapter and keep shell as the launcher.
377
377
  5. Use official `ui.*` actions and screenshot claims for visible acceptance
378
378
  criteria; do not add task-specific `metamask.*` actions for exact copy,
379
379
  styling, ticket IDs, or one-off selectors.
380
- 6. Update smoke/action-validation recipes only when the capability is reusable.
380
+ 6. Update smoke/runner.action-validation recipes only when the capability is reusable.
381
381
 
382
382
  ## Standalone use first; farm later
383
383
 
@@ -85,7 +85,7 @@ domain capability useful across many tasks, such as `metamask.perps.start_state`
85
85
  `metamask.perps.place_order`, `metamask.perps.close_positions`, or
86
86
  `metamask.perps.assert_positions`.
87
87
 
88
- `ui.scroll` is part of the current executable contract and action-validation must
88
+ `ui.scroll` is part of the current executable contract and runner.action-validation must
89
89
  prove both normal scrolling and `scroll_into_view` before screenshot capture.
90
90
  `ui.gesture` is intentionally not advertised yet; drag/swipe proof must wait
91
91
  until the shared runtime and this runner expose and validate that action on both
@@ -101,19 +101,20 @@ each user-visible step takes. The runner has no benchmark verb: timings are just
101
101
  the per-node `duration`s in a passing run's `trace.json`, so a flow is comparable
102
102
  across runs only when you pin the run (same device, healing off) and keep the node
103
103
  graph stable. The repo ships one canonical example,
104
- `library/recipes/perps-performance.mobile.recipe.json` — unlock → open the Perps
104
+ `library/recipes/perps/performance.mobile.recipe.json` — unlock → open the Perps
105
105
  market list → read live state → open a market detail. This walkthrough copies it
106
106
  into a library of your own and retargets it to your journey. A peer engineer can
107
- follow it verbatim; the same steps run as the `perps-performance-recipe` contract
107
+ follow it verbatim; the same steps run as the `perps.performance-recipe` contract
108
108
  test.
109
109
 
110
110
  ### 1. Scaffold a personal library
111
111
 
112
112
  A library is a directory with a `library.json` marker. Keep reusable `flows/`
113
- (referenced via `call`) beside a `recipes/` folder for the full flows you run:
113
+ (referenced via `call`) beside a domain-grouped `recipes/` folder for the full
114
+ flows you run:
114
115
 
115
116
  ```bash
116
- mkdir -p ~/my-recipes/flows ~/my-recipes/recipes
117
+ mkdir -p ~/my-recipes/flows ~/my-recipes/recipes/my-perps
117
118
  cat > ~/my-recipes/library.json <<'JSON'
118
119
  { "kind": "recipe-library", "schema_version": 1, "name": "mydev", "owner": "mydev" }
119
120
  JSON
@@ -130,8 +131,8 @@ it works headlessly (CI, a scratch checkout) and so the path is unambiguous.
130
131
  Copy it out of your runner checkout's `library/recipes/`:
131
132
 
132
133
  ```bash
133
- cp library/recipes/perps-performance.mobile.recipe.json \
134
- ~/my-recipes/recipes/my-perps-performance.mobile.recipe.json
134
+ cp library/recipes/perps/performance.mobile.recipe.json \
135
+ ~/my-recipes/recipes/my-perps/performance.mobile.recipe.json
135
136
  ```
136
137
 
137
138
  ### 3. Edit the nodes to your journey
@@ -211,33 +212,35 @@ field on flow catalog entries — not yet in the protocol.
211
212
 
212
213
  ### 4. Run it and read the timings
213
214
 
214
- Validate statically **by name** first — `run` probes each library source's `recipes/`
215
- directory in precedence order (personal → team → canonical), so `my-perps-performance`
216
- resolves from `~/my-recipes/recipes/` without you spelling out the path:
215
+ Validate statically **by name** first — `run` probes each library source's
216
+ `recipes/` directory in precedence order (personal → team → canonical), so
217
+ `my-perps.performance` resolves from
218
+ `~/my-recipes/recipes/my-perps/performance.mobile.recipe.json` without you
219
+ spelling out the path:
217
220
 
218
221
  ```bash
219
222
  # Static validation by NAME — resolves from the personal library via --library.
220
- mm-harness run my-perps-performance \
223
+ mm-harness run my-perps.performance \
221
224
  --library mydev=~/my-recipes --plan --adapter mobile
222
225
 
223
226
  # Zero-flag personal-library: when ~/my-recipes is placed at
224
227
  # $FARMSLOT_HOME/recipe-library (default ~/.farmslot/recipe-library), the runner
225
228
  # discovers it automatically and run-by-name works without --library:
226
- mm-harness run my-perps-performance --plan --adapter mobile
229
+ mm-harness run my-perps.performance --plan --adapter mobile
227
230
 
228
231
  # Pinned live run — same device, healing OFF, so durations are comparable.
229
- mm-harness run my-perps-performance \
232
+ mm-harness run my-perps.performance \
230
233
  --library mydev=~/my-recipes \
231
234
  --adapter mobile --device <serial> --heal off --artifacts-dir artifacts
232
235
 
233
236
  # Canonical start-state variants — same installed app, no rebuild.
234
- mm-harness run app-lifecycle-android-smoke \
237
+ mm-harness run app.lifecycle.android-smoke \
235
238
  --adapter mobile --device <serial> --heal off --artifacts-dir artifacts/lifecycle-smoke
236
- mm-harness run perps-performance-warm-start \
239
+ mm-harness run perps.performance.warm-start \
237
240
  --adapter mobile --device <serial> --heal off --artifacts-dir artifacts/warm
238
- mm-harness run perps-performance-background-resume \
241
+ mm-harness run perps.performance.background-resume \
239
242
  --adapter mobile --device <serial> --heal off --artifacts-dir artifacts/background
240
- mm-harness run perps-performance-cold-start \
243
+ mm-harness run perps.performance.cold-start \
241
244
  --adapter mobile --device <serial> --heal off --artifacts-dir artifacts/cold
242
245
 
243
246
  # Per-node durations to diff across runs (trace.json is an array of entries, or
@@ -249,7 +252,7 @@ node -e 'const t=require("./artifacts/trace.json"); \
249
252
  A miss with `--library` names the sources that were searched, so you can tell at a
250
253
  glance whether a typo or a missing library entry caused the failure.
251
254
 
252
- Run `app-lifecycle-android-smoke` first when validating a new Android slot; it
255
+ Run `app.lifecycle.android-smoke` first when validating a new Android slot; it
253
256
  isolates lifecycle control from wallet setup and Perps navigation. The start-state
254
257
  variants use the standard outer `app.lifecycle` action. Every
255
258
  variant begins with `app.status` so `trace.json` records an idempotent start
@@ -294,5 +297,5 @@ mm-harness run my-perps-with-segment --plan --adapter mobile \
294
297
  ```
295
298
 
296
299
  That resolution — unresolved without the source, `pass` with it — is what the
297
- `perps-performance-recipe` contract test asserts, alongside the canonical recipe
300
+ `perps.performance-recipe` contract test asserts, alongside the canonical recipe
298
301
  resolving by name and the personal mobile copy planning by path and basename.
@@ -68,7 +68,7 @@
68
68
  },
69
69
  "screenshot-after": {
70
70
  "action": "ui.screenshot",
71
- "path": "screenshots/app-lifecycle-android-smoke.png",
71
+ "path": "screenshots/app.lifecycle.android-smoke.png",
72
72
  "intent": "Capture the app after Android lifecycle foreground",
73
73
  "detail": "Visual proof that the selected device is displaying the relaunched app.",
74
74
  "flow": "evidence",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schema_version": 1,
3
3
  "title": "MetaMask Mobile Perps performance flow",
4
- "description": "Canonical measured flow for MetaMask Mobile Perps: setup unlocks the wallet, startState records the stable status, then the measured workflow opens the Perps market list, reads live state, and opens a market detail. Timings live in trace.json as each node's duration; there is no built-in benchmark verb. Run pinned so numbers are comparable: `mm-harness run perps-performance --device <serial> --heal off`. Optional testnet order placement is intentionally omitted because a live order would make the flow destructive and non-repeatable; add metamask.perps.place_order + close in a personal copy when you want to measure the trade path.",
4
+ "description": "Canonical measured flow for MetaMask Mobile Perps: setup unlocks the wallet, startState records the stable status, then the measured workflow opens the Perps market list, reads live state, and opens a market detail. Timings live in trace.json as each node's duration; there is no built-in benchmark verb. Run pinned so numbers are comparable: `mm-harness run perps.performance --device <serial> --heal off`. Optional testnet order placement is intentionally omitted because a live order would make the flow destructive and non-repeatable; add metamask.perps.place_order + close in a personal copy when you want to measure the trade path.",
5
5
  "startState": {
6
6
  "action": "app.status",
7
7
  "intent": "Record the stable run start status after wallet setup",
@@ -0,0 +1,39 @@
1
+ {
2
+ "schema_version": 1,
3
+ "title": "MetaMask core Perps smoke",
4
+ "description": "Non-mutating Perps domain smoke for the headless core adapter: instantiates the Perps controller and reads live positions, orders, and account state.",
5
+ "validate": {
6
+ "workflow": {
7
+ "entry": "status",
8
+ "nodes": {
9
+ "status": {
10
+ "action": "app.status",
11
+ "next": "read-positions",
12
+ "intent": "Resolve the core checkout and report headless compatibility mode"
13
+ },
14
+ "read-positions": {
15
+ "action": "metamask.perps.read_positions",
16
+ "mode": "all",
17
+ "next": "read-orders",
18
+ "intent": "Read live Perps positions from the headless controller"
19
+ },
20
+ "read-orders": {
21
+ "action": "metamask.perps.read_orders",
22
+ "mode": "all",
23
+ "next": "read-account",
24
+ "intent": "Read live Perps open orders from the headless controller"
25
+ },
26
+ "read-account": {
27
+ "action": "metamask.perps.read_account",
28
+ "next": "done",
29
+ "intent": "Read live Perps account state from the headless controller"
30
+ },
31
+ "done": {
32
+ "action": "end",
33
+ "status": "pass",
34
+ "intent": "Finish the core Perps smoke"
35
+ }
36
+ }
37
+ }
38
+ }
39
+ }
@@ -0,0 +1,51 @@
1
+ {
2
+ "schema_version": 1,
3
+ "title": "MetaMask Extension Perps smoke",
4
+ "description": "Non-mutating Perps domain smoke for MetaMask Extension: verifies CDP, unlocks if needed, opens the Perps page, and reads live Perps positions and orders.",
5
+ "validate": {
6
+ "workflow": {
7
+ "entry": "status",
8
+ "nodes": {
9
+ "status": {
10
+ "action": "app.status",
11
+ "next": "cdp",
12
+ "intent": "Resolve the Extension checkout and adapter status before Perps proof"
13
+ },
14
+ "cdp": {
15
+ "action": "cdp.target",
16
+ "required": true,
17
+ "timeout_ms": 15000,
18
+ "next": "ensure-unlocked",
19
+ "intent": "Confirm the Extension CDP runtime is reachable"
20
+ },
21
+ "ensure-unlocked": {
22
+ "action": "metamask.wallet.ensure_unlocked",
23
+ "timeout_ms": 45000,
24
+ "next": "open-perps",
25
+ "intent": "Ensure the wallet is unlocked before opening Perps"
26
+ },
27
+ "open-perps": {
28
+ "action": "ui.navigate",
29
+ "page": "perps",
30
+ "next": "read-positions",
31
+ "intent": "Open the Perps page through the stable page alias"
32
+ },
33
+ "read-positions": {
34
+ "action": "metamask.perps.read_positions",
35
+ "next": "read-orders",
36
+ "intent": "Read live Perps positions without mutating account state"
37
+ },
38
+ "read-orders": {
39
+ "action": "metamask.perps.read_orders",
40
+ "next": "done",
41
+ "intent": "Read live Perps open orders without mutating account state"
42
+ },
43
+ "done": {
44
+ "action": "end",
45
+ "status": "pass",
46
+ "intent": "Finish the Extension Perps smoke"
47
+ }
48
+ }
49
+ }
50
+ }
51
+ }
@@ -0,0 +1,51 @@
1
+ {
2
+ "schema_version": 1,
3
+ "title": "MetaMask Mobile Perps smoke",
4
+ "description": "Non-mutating Perps domain smoke for MetaMask Mobile: verifies the live bridge, unlocks if needed, opens the Perps market list, and reads live Perps positions and orders.",
5
+ "validate": {
6
+ "workflow": {
7
+ "entry": "status",
8
+ "nodes": {
9
+ "status": {
10
+ "action": "app.status",
11
+ "next": "cdp",
12
+ "intent": "Resolve the Mobile checkout and adapter status before Perps proof"
13
+ },
14
+ "cdp": {
15
+ "action": "cdp.target",
16
+ "required": true,
17
+ "timeout_ms": 15000,
18
+ "next": "ensure-unlocked",
19
+ "intent": "Confirm the React Native debug bridge is reachable"
20
+ },
21
+ "ensure-unlocked": {
22
+ "action": "metamask.wallet.ensure_unlocked",
23
+ "timeout_ms": 45000,
24
+ "next": "open-perps",
25
+ "intent": "Ensure the wallet is unlocked before opening Perps"
26
+ },
27
+ "open-perps": {
28
+ "action": "ui.navigate",
29
+ "page": "perps",
30
+ "next": "read-positions",
31
+ "intent": "Open the Perps market list through the stable page alias"
32
+ },
33
+ "read-positions": {
34
+ "action": "metamask.perps.read_positions",
35
+ "next": "read-orders",
36
+ "intent": "Read live Perps positions without mutating account state"
37
+ },
38
+ "read-orders": {
39
+ "action": "metamask.perps.read_orders",
40
+ "next": "done",
41
+ "intent": "Read live Perps open orders without mutating account state"
42
+ },
43
+ "done": {
44
+ "action": "end",
45
+ "status": "pass",
46
+ "intent": "Finish the Mobile Perps smoke"
47
+ }
48
+ }
49
+ }
50
+ }
51
+ }
@@ -8,9 +8,9 @@
8
8
  "nodes": {
9
9
  "prepare-files": {
10
10
  "action": "command",
11
- "cmd": "mkdir -p temp/recipe-action-validation && printf 'E2E_ACTION_VALIDATION extension\\n' > temp/recipe-action-validation/probe.txt && printf '{\"platform\":\"extension\",\"ok\":true}\n' > temp/recipe-action-validation/probe.json && printf 'extension log E2E_ACTION_VALIDATION\\n' > temp/recipe-action-validation/validation.log && echo E2E_ACTION_VALIDATION extension",
11
+ "cmd": "mkdir -p temp/recipe-runner.action-validation && printf 'E2E_ACTION_VALIDATION extension\\n' > temp/recipe-runner.action-validation/probe.txt && printf '{\"platform\":\"extension\",\"ok\":true}\n' > temp/recipe-runner.action-validation/probe.json && printf 'extension log E2E_ACTION_VALIDATION\\n' > temp/recipe-runner.action-validation/validation.log && echo E2E_ACTION_VALIDATION extension",
12
12
  "next": "assert-command-exit",
13
- "intent": "Prepare action-validation probe files",
13
+ "intent": "Prepare runner.action-validation probe files",
14
14
  "detail": "Create local files used to prove command/assert/log/artifact actions.",
15
15
  "flow": "setup"
16
16
  },
@@ -35,7 +35,7 @@
35
35
  },
36
36
  "assert-file": {
37
37
  "action": "assert_file",
38
- "path": "temp/recipe-action-validation/probe.txt",
38
+ "path": "temp/recipe-runner.action-validation/probe.txt",
39
39
  "contains": "E2E_ACTION_VALIDATION extension",
40
40
  "next": "assert-json",
41
41
  "intent": "Verify file assertions work",
@@ -44,7 +44,7 @@
44
44
  },
45
45
  "assert-json": {
46
46
  "action": "assert_json",
47
- "path": "temp/recipe-action-validation/probe.json",
47
+ "path": "temp/recipe-runner.action-validation/probe.json",
48
48
  "assert": {
49
49
  "path": "$.platform",
50
50
  "operator": "eq",
@@ -57,7 +57,7 @@
57
57
  },
58
58
  "watch-logs": {
59
59
  "action": "watch_logs",
60
- "path": "temp/recipe-action-validation/validation.log",
60
+ "path": "temp/recipe-runner.action-validation/validation.log",
61
61
  "contains": "E2E_ACTION_VALIDATION",
62
62
  "next": "index-artifacts",
63
63
  "intent": "Verify log watching works",
@@ -68,7 +68,7 @@
68
68
  "action": "index_artifacts",
69
69
  "artifacts": [
70
70
  {
71
- "path": "temp/recipe-action-validation/probe.txt",
71
+ "path": "temp/recipe-runner.action-validation/probe.txt",
72
72
  "type": "log",
73
73
  "category": "debug",
74
74
  "label": "Extension action validation probe"
@@ -173,7 +173,7 @@
173
173
  },
174
174
  "ui-screenshot-perps": {
175
175
  "action": "ui.screenshot",
176
- "path": "screenshots/extension-perps-action-validation.png",
176
+ "path": "screenshots/extension-perps-runner.action-validation.png",
177
177
  "timeout_ms": 5000,
178
178
  "next": "wallet-navigate-perps",
179
179
  "intent": "Capture Perps visual proof",
@@ -8,9 +8,9 @@
8
8
  "nodes": {
9
9
  "prepare-files": {
10
10
  "action": "command",
11
- "cmd": "mkdir -p temp/recipe-action-validation && printf 'E2E_ACTION_VALIDATION mobile\\n' > temp/recipe-action-validation/probe.txt && printf '{\"platform\":\"mobile\",\"ok\":true}\n' > temp/recipe-action-validation/probe.json && printf 'mobile log E2E_ACTION_VALIDATION\\n' > temp/recipe-action-validation/validation.log && echo E2E_ACTION_VALIDATION mobile",
11
+ "cmd": "mkdir -p temp/recipe-runner.action-validation && printf 'E2E_ACTION_VALIDATION mobile\\n' > temp/recipe-runner.action-validation/probe.txt && printf '{\"platform\":\"mobile\",\"ok\":true}\n' > temp/recipe-runner.action-validation/probe.json && printf 'mobile log E2E_ACTION_VALIDATION\\n' > temp/recipe-runner.action-validation/validation.log && echo E2E_ACTION_VALIDATION mobile",
12
12
  "next": "assert-command-exit",
13
- "intent": "Prepare action-validation probe files",
13
+ "intent": "Prepare runner.action-validation probe files",
14
14
  "detail": "Create local files used to prove command/assert/log/artifact actions.",
15
15
  "flow": "setup"
16
16
  },
@@ -35,7 +35,7 @@
35
35
  },
36
36
  "assert-file": {
37
37
  "action": "assert_file",
38
- "path": "temp/recipe-action-validation/probe.txt",
38
+ "path": "temp/recipe-runner.action-validation/probe.txt",
39
39
  "contains": "E2E_ACTION_VALIDATION mobile",
40
40
  "next": "assert-json",
41
41
  "intent": "Verify file assertions work",
@@ -44,7 +44,7 @@
44
44
  },
45
45
  "assert-json": {
46
46
  "action": "assert_json",
47
- "path": "temp/recipe-action-validation/probe.json",
47
+ "path": "temp/recipe-runner.action-validation/probe.json",
48
48
  "assert": {
49
49
  "path": "$.platform",
50
50
  "operator": "eq",
@@ -57,7 +57,7 @@
57
57
  },
58
58
  "watch-logs": {
59
59
  "action": "watch_logs",
60
- "path": "temp/recipe-action-validation/validation.log",
60
+ "path": "temp/recipe-runner.action-validation/validation.log",
61
61
  "contains": "E2E_ACTION_VALIDATION",
62
62
  "next": "index-artifacts",
63
63
  "intent": "Verify log watching works",
@@ -68,7 +68,7 @@
68
68
  "action": "index_artifacts",
69
69
  "artifacts": [
70
70
  {
71
- "path": "temp/recipe-action-validation/probe.txt",
71
+ "path": "temp/recipe-runner.action-validation/probe.txt",
72
72
  "type": "log",
73
73
  "category": "debug",
74
74
  "label": "Mobile action validation probe"
@@ -174,7 +174,7 @@
174
174
  },
175
175
  "ui-screenshot-wallet": {
176
176
  "action": "ui.screenshot",
177
- "path": "screenshots/mobile-wallet-action-validation.png",
177
+ "path": "screenshots/mobile-wallet-runner.action-validation.png",
178
178
  "next": "wallet-navigate-perps",
179
179
  "intent": "Capture wallet visual proof",
180
180
  "detail": "Save a screenshot artifact after generic UI actions.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deeeed/metamask-harness",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mm-harness": "bin/mm-harness"
@@ -69,4 +69,4 @@
69
69
  "url": "https://github.com/MetaMask/experimental-metamask-harness/issues"
70
70
  },
71
71
  "homepage": "https://github.com/MetaMask/experimental-metamask-harness#readme"
72
- }
72
+ }
File without changes