@deeeed/metamask-harness 0.17.3 → 0.17.5

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 (114) hide show
  1. package/CHANGELOG.md +6 -96
  2. package/README.md +136 -96
  3. package/adapters/extension/console-tail.mjs +24 -51
  4. package/adapters/extension/ensure-browser.sh +7 -1
  5. package/adapters/extension/inject.mjs +15 -5
  6. package/adapters/extension/launch-browser.cjs +4 -3
  7. package/adapters/extension/launch.sh +11 -8
  8. package/adapters/extension/lib/chrome-args.cjs +24 -0
  9. package/adapters/extension/live.sh +1 -2
  10. package/adapters/extension/reattach.sh +0 -1
  11. package/adapters/extension/seed-fixture.sh +12 -4
  12. package/adapters/extension/sidepanel-toggle.sh +1 -4
  13. package/adapters/extension/start-watch.sh +4 -17
  14. package/adapters/extension/stop-viewers.sh +1 -1
  15. package/adapters/extension/verify.sh +1 -1
  16. package/adapters/manifest.json +45 -53
  17. package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +0 -16
  18. package/adapters/mobile/bridge-runtime/console-forwarder.cjs +1 -1
  19. package/adapters/mobile/bridge-runtime/lib/match-bridge-target.cjs +1 -3
  20. package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +3 -2
  21. package/adapters/mobile/cleanup.sh +176 -16
  22. package/adapters/mobile/inject.sh +671 -90
  23. package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +185 -0
  24. package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgenticService.ts.patch +1662 -0
  25. package/adapters/mobile/overlay/compat/README.md +28 -0
  26. package/adapters/mobile/overlay/compat/rn81-message-event-source.patch +42 -0
  27. package/adapters/mobile/start-metro.sh +41 -0
  28. package/adapters/mobile/verify.sh +61 -4
  29. package/adapters/mobile/wait-for-bridge.sh +8 -22
  30. package/adapters/shared/harness-source-fingerprint.mjs +49 -0
  31. package/adapters/shared/install-repo-deps.sh +1 -5
  32. package/adapters/shared/open-debug.mjs +184 -103
  33. package/adapters/shared/resolve-farmslot-ports-core.mjs +3 -0
  34. package/adapters/shared/resolve-farmslot-ports.mjs +5 -0
  35. package/adapters/shared/resolve-farmslot-ports.sh +7 -0
  36. package/adapters/shared/resolve-slot-ports-core.mjs +6 -0
  37. package/adapters/shared/resolve-slot-ports.sh +7 -2
  38. package/bin/mm-harness +0 -7
  39. package/dist/adapters/extension/harness-freshness.js +39 -0
  40. package/dist/adapters/extension/runtime-decision.js +4 -73
  41. package/dist/adapters/extension/surface.js +2 -3
  42. package/dist/adapters/mobile/perps-env.js +3 -43
  43. package/dist/adapters/mobile/prepare.js +7 -22
  44. package/dist/adapters/mobile/source-freshness.js +26 -41
  45. package/dist/adapters/mobile/surface.js +1 -1
  46. package/dist/adapters/resolve-slot-ports.js +2 -0
  47. package/dist/adapters/slot-ports.js +0 -16
  48. package/dist/adapters.js +5 -12
  49. package/dist/cli-commands.js +1 -1
  50. package/dist/cli.js +12 -6
  51. package/dist/commands/call.js +22 -56
  52. package/dist/commands/check.js +3 -9
  53. package/dist/commands/checklist.js +4 -1
  54. package/dist/commands/debug.js +31 -38
  55. package/dist/commands/device-target.js +12 -27
  56. package/dist/commands/doctor.js +23 -77
  57. package/dist/commands/fixtures.js +17 -27
  58. package/dist/commands/launch/extension.js +2 -0
  59. package/dist/commands/launch/index.js +59 -167
  60. package/dist/commands/logs.js +6 -8
  61. package/dist/commands/manifest.js +25 -151
  62. package/dist/commands/parse-args.js +0 -2
  63. package/dist/commands/provision.js +3 -10
  64. package/dist/commands/run-engine.js +69 -71
  65. package/dist/commands/run-report.js +3 -12
  66. package/dist/commands/run.js +35 -245
  67. package/dist/commands/shared.js +3 -55
  68. package/dist/commands/status-probe.js +1 -1
  69. package/dist/commands/status.js +1 -1
  70. package/dist/commands/stop.js +2 -7
  71. package/dist/commands/update.js +28 -16
  72. package/dist/doctor.js +4 -1
  73. package/dist/harness.js +134 -43
  74. package/dist/heal-bounds.js +1 -1
  75. package/dist/live-adapter-contract.js +20 -6
  76. package/dist/mm-harness-cli.js +48 -123
  77. package/dist/paths.js +5 -2
  78. package/dist/runner.js +2 -37
  79. package/dist/runtime-context.js +8 -26
  80. package/docs/ADAPTER-SURFACE.md +112 -19
  81. package/docs/CHEATSHEET.md +52 -39
  82. package/docs/CLI-SPEC.md +1098 -0
  83. package/docs/CODE-MAP.md +1 -1
  84. package/docs/DEBUG-HANDOVER.md +36 -0
  85. package/docs/MENTAL-MODEL.md +295 -0
  86. package/docs/UX-PRINCIPLES.md +1 -3
  87. package/docs/architecture.md +375 -65
  88. package/docs/live-adapter-contract.md +2 -4
  89. package/docs/recipe-libraries.md +275 -45
  90. package/library/README.md +0 -4
  91. package/library/actions/core/perps/_controller.mjs +55 -10
  92. package/library/actions/mobile/platform/bridge.mjs +1 -5
  93. package/library/manifests/core.action-manifest.json +7 -7
  94. package/library/manifests/mobile.action-manifest.json +3 -28
  95. package/package.json +1 -4
  96. package/adapters/extension/launch-webpack.cjs +0 -83
  97. package/adapters/extension/stamp-runtime-title.cjs +0 -49
  98. package/adapters/extension/sync-webpack-dist.cjs +0 -105
  99. package/adapters/mobile/launch-console-forwarder.cjs +0 -39
  100. package/adapters/mobile/start-console-forwarder.sh +0 -70
  101. package/adapters/shared/update-check-worker.mjs +0 -35
  102. package/dist/adapters/extension/console-capture.js +0 -93
  103. package/dist/command-contract.js +0 -441
  104. package/dist/command-journal.js +0 -225
  105. package/dist/commands/last.js +0 -52
  106. package/dist/json-stream.js +0 -57
  107. package/dist/run-diagnostics.js +0 -261
  108. package/docs/CLI-ERGONOMICS-AUDIT.md +0 -32
  109. package/docs/CLI-ERGONOMICS-HUMAN-QA.md +0 -64
  110. package/docs/VIDEO-DEMO-VALIDATION.md +0 -74
  111. package/library/recipes/runner/smoke.core.recipe.json +0 -27
  112. package/library/recipes/wallet/smoke.extension.recipe.json +0 -42
  113. package/library/recipes/wallet/smoke.mobile.recipe.json +0 -42
  114. package/scripts/validate-human-outcomes.mjs +0 -169
@@ -1,129 +1,57 @@
1
1
  import { loadActionManifest, validateManifest } from "../manifest.js";
2
2
  import { EXIT } from "./shared.js";
3
- import { isRecord, optionFlag, optionString, resolveAdapter } from "./parse-args.js";
4
- async function handleActions({ options, positional }) {
3
+ import {
4
+ actionManifestPathOption,
5
+ isRecord,
6
+ optionFlag,
7
+ optionString,
8
+ resolveAdapter
9
+ } from "./parse-args.js";
10
+ async function handleManifest({ options }) {
11
+ const { adapter } = resolveAdapter(options);
12
+ const actionManifestPath = actionManifestPathOption(options, adapter);
13
+ const manifest = loadActionManifest(adapter, optionString(options, "actionManifest"));
14
+ await validateManifest(manifest);
15
+ if (optionFlag(options, "json")) console.log(JSON.stringify(manifest, null, 2));
16
+ else console.log(actionManifestPath);
17
+ return 0;
18
+ }
19
+ async function handleActions({ options }) {
5
20
  const { adapter } = resolveAdapter(options);
6
21
  const manifest = loadActionManifest(adapter, optionString(options, "actionManifest"));
7
22
  await validateManifest(manifest);
8
- if (optionFlag(options, "raw")) {
9
- console.log(JSON.stringify(manifest, null, 2));
10
- return EXIT.ok;
11
- }
12
23
  const json = optionFlag(options, "json");
13
24
  const action = optionString(options, "action");
14
- const query = positional[0]?.trim();
15
- const category = optionString(options, "category")?.toLowerCase();
16
- const categoriesOnly = optionFlag(options, "categories");
17
25
  const all = describeManifestActions(manifest);
18
- const categories = summarizeActionCategories(all);
19
- if (categoriesOnly && (action || category)) {
20
- const message = "--categories cannot be combined with --action or --category.";
21
- const userAction = `mm-harness actions --adapter ${adapter} --categories`;
22
- if (json) {
23
- console.log(JSON.stringify({ schemaVersion: 1, command: "actions", adapter, error: { code: "ACTION_FILTER_CONFLICT", message, userAction } }, null, 2));
24
- } else {
25
- console.error(`\u2717 mm-harness actions: ${message}
26
- Next: ${userAction}`);
27
- }
28
- return EXIT.usage;
29
- }
30
- if (categoriesOnly) {
31
- if (json) console.log(JSON.stringify({ schemaVersion: 1, command: "actions", adapter, categories }, null, 2));
32
- else for (const entry of categories) console.log(`${entry.name} (${entry.count})`);
33
- return EXIT.ok;
34
- }
35
- const categoryActions = category ? all.filter((entry) => entry.category === category) : all;
36
- if (category && categoryActions.length === 0) {
37
- const message = `no action category matches "${category}" for the ${adapter} adapter.`;
38
- const userAction = `mm-harness actions --adapter ${adapter} --categories`;
39
- if (json) {
40
- console.log(JSON.stringify({ schemaVersion: 1, command: "actions", adapter, category, availableCategories: categories, error: { code: "ACTION_CATEGORY_UNKNOWN", message, userAction } }, null, 2));
41
- } else {
42
- console.error(`\u2717 mm-harness actions: ${message}
43
- Next: ${userAction}`);
44
- }
45
- return EXIT.usage;
46
- }
47
- const actions = action ? fuzzyResolveActions(categoryActions, action) : query ? searchActions(categoryActions, query) : categoryActions;
26
+ const actions = action ? fuzzyResolveActions(all, action) : all;
48
27
  if (action && actions.length === 0) {
49
28
  const message = `no action matches "${action}" for the ${adapter} adapter.`;
50
- const userAction = `mm-harness actions --adapter ${adapter}`;
51
- if (json) {
52
- console.log(JSON.stringify({ schemaVersion: 1, command: "actions", adapter, action, category, error: { code: "ACTION_UNKNOWN", message, userAction } }, null, 2));
53
- } else {
54
- console.error(`\u2717 mm-harness actions: ${message}
55
- Next: ${userAction}`);
56
- }
57
- return EXIT.usage;
58
- }
59
- if (query && actions.length === 0) {
60
- const message = `no action matches search "${query}" for the ${adapter} adapter.`;
61
- const userAction = `mm-harness actions --adapter ${adapter} --categories`;
29
+ const userAction = `mm-harness actions --adapter ${adapter} # list the vocabulary`;
62
30
  if (json) {
63
- console.log(JSON.stringify({
64
- schemaVersion: 1,
65
- command: "actions",
66
- adapter,
67
- query,
68
- category,
69
- error: { code: "ACTION_SEARCH_EMPTY", message, userAction }
70
- }, null, 2));
31
+ console.log(JSON.stringify({ schemaVersion: 1, command: "actions", adapter, action, error: { code: "ACTION_UNKNOWN", message, userAction } }, null, 2));
71
32
  } else {
72
33
  console.error(`\u2717 mm-harness actions: ${message}
73
34
  Next: ${userAction}`);
74
35
  }
75
36
  return EXIT.usage;
76
37
  }
77
- const relatedActions = action && actions.length === 1 ? findRelatedActions(all, actions[0]) : void 0;
78
38
  if (json) {
79
- console.log(JSON.stringify({
80
- schemaVersion: 1,
81
- command: "actions",
82
- adapter,
83
- ...query ? { query } : {},
84
- category,
85
- actions,
86
- ...relatedActions ? { relatedActions } : {}
87
- }, null, 2));
39
+ console.log(JSON.stringify({ adapter, actions }, null, 2));
88
40
  } else {
89
41
  for (const entry of actions) {
90
42
  const fields = entry.fields.length ? ` fields=${entry.fields.join(",")}` : "";
91
43
  console.log(`${entry.name} (${entry.kind})${fields}${entry.description ? ` \u2014 ${entry.description}` : ""}`);
92
44
  }
93
- if (relatedActions) console.log(`Related: ${relatedActions.join(", ")}`);
94
45
  }
95
46
  return 0;
96
47
  }
97
48
  function fuzzyResolveActions(entries, query) {
98
49
  const exactFull = entries.filter((e) => e.name === query);
99
50
  if (exactFull.length > 0) return exactFull;
100
- const finalSegment2 = (name) => name.split(".").pop() ?? name;
101
- const exactSegment = entries.filter((e) => finalSegment2(e.name) === query);
51
+ const finalSegment = (name) => name.split(".").pop() ?? name;
52
+ const exactSegment = entries.filter((e) => finalSegment(e.name) === query);
102
53
  if (exactSegment.length > 0) return exactSegment;
103
- return entries.filter((e) => finalSegment2(e.name).includes(query));
104
- }
105
- function searchActions(entries, query) {
106
- const terms = searchTerms(query);
107
- if (terms.length === 0) return [];
108
- return entries.map((entry) => {
109
- const scores = terms.map((term) => actionSearchScore(entry, term));
110
- return { entry, scores, score: scores.reduce((total, value) => total + value, 0) };
111
- }).filter(({ score, scores }) => score > 0 && scores.every((value) => value > 0)).sort((left, right) => right.score - left.score || left.entry.name.localeCompare(right.entry.name)).map(({ entry }) => entry);
112
- }
113
- function findRelatedActions(entries, selected, limit = 5) {
114
- const selectedNameTerms = searchTerms(finalSegment(selected.name).replaceAll("_", " "));
115
- return entries.filter((entry) => entry.name !== selected.name).map((entry) => {
116
- const nameTerms = new Set(searchTerms(finalSegment(entry.name).replaceAll("_", " ")));
117
- const sharedScore = selectedNameTerms.filter((term) => nameTerms.has(term)).reduce((score2, term) => score2 + (GENERIC_OPERATION_TERMS.has(term) ? 5 : 30), 0);
118
- const score = (entry.category === selected.category ? 100 : 0) + sharedScore;
119
- return { name: entry.name, score };
120
- }).filter(({ score }) => score > 0).sort((left, right) => right.score - left.score || left.name.localeCompare(right.name)).slice(0, limit).map(({ name }) => name);
121
- }
122
- const GENERIC_OPERATION_TERMS = /* @__PURE__ */ new Set(["assert", "call", "close", "ensure", "place", "read", "start", "teardown"]);
123
- function summarizeActionCategories(actions) {
124
- const counts = /* @__PURE__ */ new Map();
125
- for (const action of actions) counts.set(action.category, (counts.get(action.category) ?? 0) + 1);
126
- return [...counts.entries()].map(([name, count]) => ({ name, count })).sort((left, right) => left.name.localeCompare(right.name));
54
+ return entries.filter((e) => finalSegment(e.name).includes(query));
127
55
  }
128
56
  function describeManifestActions(manifest) {
129
57
  const manifestRecord = isRecord(manifest) ? manifest : {};
@@ -152,69 +80,15 @@ function describeManifestAction(name, kind, metadata) {
152
80
  return {
153
81
  name,
154
82
  kind,
155
- category: actionCategory(name, record.category),
156
83
  description: typeof record.description === "string" ? record.description : "",
157
84
  fields: properties,
158
85
  schema,
159
86
  examples: record.examples
160
87
  };
161
88
  }
162
- function actionCategory(name, configured) {
163
- if (typeof configured === "string" && configured.trim()) return configured.trim().toLowerCase();
164
- const segments = name.split(".");
165
- if (segments[0] === "metamask" && segments.length > 2) return segments[1] ?? "metamask";
166
- if (segments[0] === "app" || segments[0] === "cdp") return "runtime";
167
- if (segments[0] === "ui") return "ui";
168
- if (name.startsWith("assert_")) return "assertion";
169
- if (name === "watch_logs" || name === "index_artifacts") return "evidence";
170
- if (name === "command" || name === "wait" || name === "call" || name === "end") return "control";
171
- return segments.length > 1 ? segments[0] || "utility" : "utility";
172
- }
173
- function actionSearchScore(entry, term) {
174
- const name = entry.name.toLowerCase();
175
- const segment = finalSegment(name);
176
- const nameTerms = searchTerms(name.replaceAll(".", " ").replaceAll("_", " "));
177
- const fields = entry.fields.map((field) => field.toLowerCase());
178
- const description = entry.description.toLowerCase();
179
- if (name === term) return 120;
180
- if (segment === term) return 110;
181
- if (entry.category === term) return 100;
182
- if (fields.includes(term)) return 90;
183
- if (nameTerms.includes(term)) return 80;
184
- if (name.includes(term)) return 70;
185
- if (fields.some((field) => field.includes(term))) return 60;
186
- if (description.includes(term)) return 50;
187
- if ([...nameTerms, ...fields].some((candidate) => fuzzyTermMatch(term, candidate))) return 40;
188
- return 0;
189
- }
190
- function fuzzyTermMatch(term, candidate) {
191
- if (term.length < 4 || candidate.length < 4) return false;
192
- const threshold = Math.max(1, Math.floor(Math.max(term.length, candidate.length) / 4));
193
- return levenshtein(term, candidate) <= threshold;
194
- }
195
- function searchTerms(value) {
196
- return value.toLowerCase().match(/[a-z0-9]+/gu) ?? [];
197
- }
198
- function finalSegment(name) {
199
- return name.split(".").pop() ?? name;
200
- }
201
- function levenshtein(left, right) {
202
- const prior = Array.from({ length: right.length + 1 }, (_, index) => index);
203
- for (let leftIndex = 1; leftIndex <= left.length; leftIndex += 1) {
204
- const current = [leftIndex];
205
- for (let rightIndex = 1; rightIndex <= right.length; rightIndex += 1) {
206
- const substitution = (prior[rightIndex - 1] ?? 0) + (left[leftIndex - 1] === right[rightIndex - 1] ? 0 : 1);
207
- current[rightIndex] = Math.min((current[rightIndex - 1] ?? 0) + 1, (prior[rightIndex] ?? 0) + 1, substitution);
208
- }
209
- for (let index = 0; index < current.length; index += 1) prior[index] = current[index] ?? 0;
210
- }
211
- return prior[right.length] ?? right.length;
212
- }
213
89
  export {
214
90
  describeManifestActions,
215
- findRelatedActions,
216
91
  fuzzyResolveActions,
217
92
  handleActions,
218
- searchActions,
219
- summarizeActionCategories
93
+ handleManifest
220
94
  };
@@ -19,14 +19,12 @@ function parseArgs(argv, command) {
19
19
  const options = {};
20
20
  const booleanOptions = /* @__PURE__ */ new Set([
21
21
  "json",
22
- "jsonStream",
23
22
  "launchExistingDist",
24
23
  "startWatch",
25
24
  "record",
26
25
  "plan",
27
26
  "list",
28
27
  "raw",
29
- "categories",
30
28
  "fix",
31
29
  "force",
32
30
  "resolveOnly",
@@ -1,7 +1,7 @@
1
1
  import { detectAdapter } from "../harness.js";
2
2
  import { assertAdapter } from "../paths.js";
3
3
  import { getAdapterSurface } from "../adapters/surface.js";
4
- import { ADAPTER_DETECT_NEXT, usageOut, writeInteractiveProgress } from "./shared.js";
4
+ import { ADAPTER_DETECT_NEXT, usageOut } from "./shared.js";
5
5
  import {
6
6
  applyWatcherPortOption,
7
7
  optionFlag,
@@ -25,13 +25,9 @@ async function handleProvision({ positional, options, rawArgv }) {
25
25
  }
26
26
  const surface = getAdapterSurface(adapter);
27
27
  const rerunCommand = provisionRerunCommand(rawArgv, options, adapter, target);
28
- const platform = optionString(options, "platform") ?? optionString(options, "devicePlatform") ?? (positional[0] === "runway" ? positional[1] : positional[0]) ?? "ios";
29
- if (adapter === "mobile") {
30
- writeInteractiveProgress(json, `\u2192 provision mobile ${platform} \u2014 resolving simulator and Runway artifact`);
31
- }
32
28
  const result = await surface.runwayProvision.run(target, {
33
29
  json,
34
- platform,
30
+ platform: optionString(options, "platform") ?? optionString(options, "devicePlatform") ?? (positional[0] === "runway" ? positional[1] : positional[0]) ?? "ios",
35
31
  branch: optionString(options, "branch"),
36
32
  defaultBranch: optionString(options, "defaultBranch"),
37
33
  run: optionString(options, "run"),
@@ -46,10 +42,8 @@ async function handleProvision({ positional, options, rawArgv }) {
46
42
  resolveOnly: optionFlag(options, "resolveOnly"),
47
43
  rerunCommand
48
44
  });
49
- const next = result.status === "pass" && adapter === "mobile" && result.resolveOnly !== true ? `mm-harness launch ${String(result.platform ?? platform)} --adapter mobile --target ${shellQuote(target)}` : void 0;
50
- const output = next ? { ...result, next } : result;
51
45
  if (json) {
52
- console.log(JSON.stringify(output, null, 2));
46
+ console.log(JSON.stringify(result, null, 2));
53
47
  } else if (result.status === "pass") {
54
48
  const cache = typeof result.cache === "object" && result.cache ? result.cache : void 0;
55
49
  const simulator = typeof result.simulator === "object" && result.simulator ? result.simulator : void 0;
@@ -60,7 +54,6 @@ async function handleProvision({ positional, options, rawArgv }) {
60
54
  const action = result.skipped ? "already provisioned" : "provisioned";
61
55
  console.error(`\u2713 ${action} ${adapter} ${result.platform ?? ""} simulator=${simulator?.name ?? "unknown"} cache=${cache?.status ?? "unknown"}`);
62
56
  }
63
- if (next) console.error(` Next: ${next}`);
64
57
  } else {
65
58
  console.error(`\u2717 mm-harness provision: ${result.error?.message ?? "provision failed"}
66
59
  Next: ${result.error?.userAction ?? rerunCommand}`);
@@ -20,17 +20,12 @@ import {
20
20
  import { captureHelperSupportsRecordSessionSnapshots } from "../recording-target.js";
21
21
  import { listRecipeFiles } from "../recipe-files.js";
22
22
  import { startRecipeRecording, stopRecipeRecording } from "../run-recording.js";
23
- import {
24
- beginRunDiagnostics,
25
- finishRunDiagnostics
26
- } from "../run-diagnostics.js";
27
- import { EXIT } from "./shared.js";
23
+ import { EXIT, spawnScriptStreaming } from "./shared.js";
28
24
  import {
29
25
  actionManifestPathOption,
30
26
  optionString,
31
27
  isRecord,
32
28
  shellQuoteArg,
33
- targetPath,
34
29
  usageError
35
30
  } from "./parse-args.js";
36
31
  async function runRecipe(adapter, recipe, artifactsDir, projectRoot, actionManifestPath, runtimeOptions = {}) {
@@ -54,7 +49,6 @@ async function runRecipe(adapter, recipe, artifactsDir, projectRoot, actionManif
54
49
  try {
55
50
  await prepareRuntimeIfNeeded(adapter, projectRoot, runtimeOptions);
56
51
  const absoluteArtifactsDir = path.resolve(artifactsDir);
57
- const diagnosticBaseline = await beginRunDiagnostics(adapter, projectRoot);
58
52
  const recordVideo = runtimeOptions.recordVideo ?? false;
59
53
  const useFramedExtensionRecording = adapter === "extension" && recordVideo === "full-run" && captureHelperSupportsRecordSessionSnapshots(projectRoot);
60
54
  const recording = useFramedExtensionRecording ? await startRecipeRecording(adapter, projectRoot, absoluteArtifactsDir, {
@@ -66,8 +60,7 @@ async function runRecipe(adapter, recipe, artifactsDir, projectRoot, actionManif
66
60
  await validateManifest(manifest);
67
61
  const { createMetaMaskRunner } = await import("../runner.js");
68
62
  const runner = await createMetaMaskRunner(adapter, manifest, {
69
- quietStdout: runtimeOptions.stdoutIsMachineContract === true,
70
- onActionEvent: runtimeOptions.onActionEvent
63
+ quietStdout: runtimeOptions.stdoutIsMachineContract === true
71
64
  });
72
65
  const runRequest = {
73
66
  recipePath: path.resolve(recipe),
@@ -87,7 +80,7 @@ async function runRecipe(adapter, recipe, artifactsDir, projectRoot, actionManif
87
80
  }
88
81
  }
89
82
  await stopRecipeRecording(recording, result);
90
- return finishRunDiagnostics(diagnosticBaseline, result);
83
+ return result;
91
84
  } finally {
92
85
  await stopRecipeRecording(recording);
93
86
  }
@@ -198,7 +191,12 @@ function libraryRecipeNames(adapter) {
198
191
  return [...new Set(names)].sort();
199
192
  }
200
193
  function recipeNameCandidates(recipeArg, adapter) {
201
- return singleRecipeNameCandidates(recipeArg, adapter);
194
+ const names = [recipeArg, ...LEGACY_RECIPE_ALIASES[recipeArg] ?? []];
195
+ const candidates = [];
196
+ for (const name of names) {
197
+ candidates.push(...singleRecipeNameCandidates(name, adapter));
198
+ }
199
+ return [...new Set(candidates)];
202
200
  }
203
201
  function singleRecipeNameCandidates(name, adapter) {
204
202
  if (name.endsWith(".recipe.json")) {
@@ -224,6 +222,32 @@ function domainPathForRecipeName(name) {
224
222
  function uniqueStrings(values) {
225
223
  return [...new Set(values)];
226
224
  }
225
+ const LEGACY_RECIPE_ALIASES = {
226
+ "action-validation": ["runner.action-validation"],
227
+ "action-validation.extension.recipe.json": ["runner.action-validation.extension.recipe.json"],
228
+ "action-validation.mobile.recipe.json": ["runner.action-validation.mobile.recipe.json"],
229
+ "app-lifecycle-android-smoke": ["app.lifecycle.android-smoke"],
230
+ "app-lifecycle-android-smoke.mobile.recipe.json": ["app.lifecycle.android-smoke.mobile.recipe.json"],
231
+ "order-lifecycle": ["perps.order-lifecycle"],
232
+ "order-lifecycle.core.recipe.json": ["perps.order-lifecycle.core.recipe.json"],
233
+ "perps-lifecycle": ["perps.lifecycle"],
234
+ "perps-lifecycle.recipe.json": ["perps.lifecycle.recipe.json"],
235
+ "perps-performance": ["perps.performance"],
236
+ "perps-performance-background-resume": ["perps.performance.background-resume"],
237
+ "perps-performance-background-resume.mobile.recipe.json": ["perps.performance.background-resume.mobile.recipe.json"],
238
+ "perps-performance-cold-start": ["perps.performance.cold-start"],
239
+ "perps-performance-cold-start.mobile.recipe.json": ["perps.performance.cold-start.mobile.recipe.json"],
240
+ "perps-performance-warm-start": ["perps.performance.warm-start"],
241
+ "perps-performance-warm-start.mobile.recipe.json": ["perps.performance.warm-start.mobile.recipe.json"],
242
+ "perps-performance.mobile.recipe.json": ["perps.performance.mobile.recipe.json"],
243
+ "read-markets": ["perps.read-markets"],
244
+ "read-markets.core.recipe.json": ["perps.read-markets.core.recipe.json"],
245
+ "smoke": ["runner.smoke"],
246
+ "smoke.extension.recipe.json": ["runner.smoke.extension.recipe.json"],
247
+ "smoke.mobile.recipe.json": ["runner.smoke.mobile.recipe.json"],
248
+ "trading-lifecycle": ["perps.trading-lifecycle"],
249
+ "trading-lifecycle.core.recipe.json": ["perps.trading-lifecycle.core.recipe.json"]
250
+ };
227
251
  async function validateRunRecipeStatic(recipeArg, adapter, options) {
228
252
  const librarySources = await resolveMetaMaskLibrarySources(optionString(options, "library"));
229
253
  const resolved = resolveRunRecipeArg(recipeArg, adapter, librarySources);
@@ -317,7 +341,6 @@ async function actionInsteadOfRecipeHint(recipeArg, adapter, options) {
317
341
  const device = optionString(options, "device");
318
342
  const actionManifest = optionString(options, "actionManifest");
319
343
  const parts = ["mm-harness", "call", matches[0]];
320
- parts.push("--adapter", adapter, "--target", shellQuoteArg(targetPath(options)));
321
344
  if (device) parts.push("--device", shellQuoteArg(device));
322
345
  if (actionManifest) parts.push("--action-manifest", shellQuoteArg(actionManifest));
323
346
  return ` This is an action, not a recipe. Use: ${parts.join(" ")}.`;
@@ -375,12 +398,10 @@ async function runOneNode(adapter, action, args, target, actionManifest) {
375
398
  async function prepareHeal(adapter, target, options, json, prepareOptions = {}) {
376
399
  if (recipeRunning(target)) {
377
400
  const msg = "a recipe is currently running \u2014 refusing to start while another recipe executes.";
378
- const userAction = `inspect the checkout state with: mm-harness status --target ${shellQuoteArg(target)} --json; retry after the active recipe finishes`;
379
401
  if (json) {
380
- console.log(JSON.stringify({ schemaVersion: 1, status: "fail", recoverable: false, error: { code: "RECIPE_RUNNING", message: msg, userAction } }, null, 2));
402
+ console.log(JSON.stringify({ schemaVersion: 1, status: "fail", recoverable: false, error: { code: "RECIPE_RUNNING", message: msg } }, null, 2));
381
403
  } else {
382
- console.error(`\u2717 mm-harness: ${msg}
383
- Next: ${userAction}`);
404
+ console.error(`\u2717 mm-harness: ${msg}`);
384
405
  }
385
406
  return EXIT.bounded;
386
407
  }
@@ -393,25 +414,21 @@ async function prepareHeal(adapter, target, options, json, prepareOptions = {})
393
414
  return EXIT.usage;
394
415
  }
395
416
  const state = newHealState();
396
- prepareOptions.onPhase?.("install");
397
417
  const ensured = await ensureOverlay(adapter, target, heal, state, json);
398
418
  if (!ensured.ok) {
399
- const userAction = `mm-harness install --adapter ${adapter} --target ${shellQuoteArg(target)}`;
400
419
  if (json) {
401
420
  console.log(
402
421
  JSON.stringify(
403
- { schemaVersion: 1, status: "fail", recoverable: false, mutations: state.mutations, error: { code: "OVERLAY_INSTALL_FAILED", message: ensured.error, userAction } },
422
+ { schemaVersion: 1, status: "fail", recoverable: false, mutations: state.mutations, error: { code: "OVERLAY_INSTALL_FAILED", message: ensured.error } },
404
423
  null,
405
424
  2
406
425
  )
407
426
  );
408
427
  } else {
409
- console.error(`\u2717 overlay auto-ensure failed: ${ensured.error}
410
- Next: ${userAction}`);
428
+ console.error(`\u2717 overlay auto-ensure failed: ${ensured.error}`);
411
429
  }
412
430
  return EXIT.infra;
413
431
  }
414
- prepareOptions.onPhase?.("healthcheck");
415
432
  if (adapter === "extension") {
416
433
  const previousCdpPort = process.env.CDP_PORT;
417
434
  const previousRecipeCdpPort = process.env.RECIPE_CDP_PORT;
@@ -424,9 +441,7 @@ async function prepareHeal(adapter, target, options, json, prepareOptions = {})
424
441
  }
425
442
  if (explicitWatcherPort) process.env.WATCHER_PORT = explicitWatcherPort;
426
443
  try {
427
- const current = await ensureExtensionProofRuntime(target, heal, state, json, {
428
- onRecovery: (code) => prepareOptions.onPhase?.("recover", { code })
429
- });
444
+ const current = await ensureExtensionProofRuntime(target, heal, state, json);
430
445
  if (current !== null) return current;
431
446
  } finally {
432
447
  restoreEnv("CDP_PORT", previousCdpPort);
@@ -435,9 +450,7 @@ async function prepareHeal(adapter, target, options, json, prepareOptions = {})
435
450
  }
436
451
  }
437
452
  if (adapter === "mobile" && prepareOptions.skipMobileSourceFreshness !== true) {
438
- const current = await ensureMobileProofRuntime(target, heal, state, json, {
439
- onRecovery: (code) => prepareOptions.onPhase?.("recover", { code })
440
- });
453
+ const current = await ensureMobileProofRuntime(target, heal, state, json);
441
454
  if (current !== null) return current;
442
455
  }
443
456
  return { state, heal };
@@ -447,20 +460,6 @@ async function ensureMobileProofRuntime(target, heal, state, json, deps = {}) {
447
460
  const check = deps.check ?? (() => source.mobileSourceCheck(target));
448
461
  const record = deps.record ?? ((fingerprint) => source.recordMobileSourceBaseline(target, fingerprint));
449
462
  const initial = check();
450
- if (initial.status === "unavailable") {
451
- if (json) {
452
- console.log(JSON.stringify({
453
- schemaVersion: 1,
454
- status: "fail",
455
- recoverable: false,
456
- error: initial.error
457
- }, null, 2));
458
- } else {
459
- console.error(`\u2717 ${initial.error.message}`);
460
- console.error(` Next: ${initial.error.userAction}`);
461
- }
462
- return EXIT.usage;
463
- }
464
463
  if (initial.status === "current") return null;
465
464
  const targetArg = JSON.stringify(path.resolve(target));
466
465
  const userAction = `mm-harness call app.lifecycle --arg command=restart --adapter mobile --target ${targetArg}`;
@@ -482,7 +481,6 @@ async function ensureMobileProofRuntime(target, heal, state, json, deps = {}) {
482
481
  }
483
482
  const recoveryCode = "mobile.source_reloaded";
484
483
  state.attemptedRecoveries.push(recoveryCode);
485
- deps.onRecovery?.(recoveryCode);
486
484
  if (!json) console.error(`\u2192 Mobile proof preflight: source ${initial.status}; restarting the app before execution`);
487
485
  const restart = deps.restart ?? (async () => {
488
486
  const result2 = await runOneNode(
@@ -495,21 +493,27 @@ async function ensureMobileProofRuntime(target, heal, state, json, deps = {}) {
495
493
  return { status: result2.status === "pass" ? 0 : EXIT.runtime, output: result2.status };
496
494
  });
497
495
  const result = await restart();
498
- const recorded = result.status === 0 && record(initial.fingerprint);
496
+ const platform = resolveRunMobilePlatform();
497
+ const bridge = result.status === 0 ? await (deps.waitForBridge ? deps.waitForBridge() : spawnScriptStreaming(
498
+ path.join(runnerDir, "adapters/mobile/wait-for-bridge.sh"),
499
+ ["--target", target, "--platform", platform],
500
+ target
501
+ )) : { status: result.status, output: result.output };
502
+ const recorded = result.status === 0 && bridge.status === 0 && record(initial.fingerprint);
499
503
  const verified = recorded ? check() : initial;
500
- if (result.status !== 0 || !recorded || verified.status !== "current") {
501
- const message = "Mobile source restart did not produce a current loaded-source baseline.";
504
+ if (result.status !== 0 || bridge.status !== 0 || !recorded || verified.status !== "current") {
505
+ const message = bridge.status !== 0 ? "Mobile source restart completed but the platform-matched bridge did not become ready." : "Mobile source restart did not produce a current loaded-source baseline.";
502
506
  if (json) {
503
507
  console.log(JSON.stringify({
504
508
  schemaVersion: 1,
505
509
  status: "fail",
506
510
  recoverable: false,
507
511
  attemptedRecoveries: state.attemptedRecoveries,
508
- error: { code: "MOBILE_SOURCE_RELOAD_FAILED", message, userAction, originalError: result.output }
512
+ error: { code: bridge.status !== 0 ? "MOBILE_BRIDGE_NOT_READY" : "MOBILE_SOURCE_RELOAD_FAILED", message, userAction, originalError: bridge.output || result.output }
509
513
  }, null, 2));
510
514
  } else {
511
515
  console.error(`\u2717 ${message}`);
512
- console.error(` Next: ${userAction}`);
516
+ console.error(` Next: ${bridge.status !== 0 ? "inspect the bridge-status log and rerun mm-harness launch <platform>" : userAction}`);
513
517
  }
514
518
  return EXIT.infra;
515
519
  }
@@ -528,22 +532,6 @@ async function ensureExtensionProofRuntime(target, heal, state, json, deps = {})
528
532
  });
529
533
  const initial = await decide();
530
534
  if (initial.decision === "ready") return null;
531
- if (initial.reasonCode === "runtime-dist-check-failed") {
532
- const message = `Extension proof runtime freshness could not be checked: ${initial.reasons[0] ?? initial.decision}`;
533
- const userAction2 = `mm-harness doctor --adapter extension --target ${JSON.stringify(path.resolve(target))}`;
534
- if (json) {
535
- console.log(JSON.stringify({
536
- schemaVersion: 1,
537
- status: "fail",
538
- recoverable: false,
539
- error: { code: "EXTENSION_RUNTIME_CHECK_FAILED", message, userAction: userAction2 }
540
- }, null, 2));
541
- } else {
542
- console.error(`\u2717 ${message}`);
543
- console.error(` Next: ${userAction2}`);
544
- }
545
- return EXIT.infra;
546
- }
547
535
  if (!["dist-stale", "dist-missing", "runtime-dist-stale", "runtime-dist-missing"].includes(initial.reasonCode)) {
548
536
  return null;
549
537
  }
@@ -569,14 +557,19 @@ async function ensureExtensionProofRuntime(target, heal, state, json, deps = {})
569
557
  }
570
558
  const recoveryCode = tier === "build" ? "extension.runtime_rebuilt" : "extension.runtime_reloaded";
571
559
  state.attemptedRecoveries.push(recoveryCode);
572
- deps.onRecovery?.(recoveryCode);
573
560
  if (!json) console.error(`\u2192 Extension proof preflight: ${initial.reasonCode}; ${tier === "build" ? "rebuilding" : "reloading"} before execution`);
574
561
  const launch = deps.launch ?? (async (requestedTier) => {
575
562
  const { launchExtension } = await import("./launch/extension.js");
576
563
  return launchExtension(target, requestedTier, false);
577
564
  });
578
565
  const result = await launch(tier);
579
- const verified = result.status === 0 ? await decide() : initial;
566
+ let verified = result.status === 0 ? await decide() : initial;
567
+ if (result.status === 0 && verified.decision !== "ready") {
568
+ for (let attempt = 0; attempt < 8 && verified.decision !== "ready"; attempt += 1) {
569
+ await new Promise((resolve) => setTimeout(resolve, 250));
570
+ verified = await decide();
571
+ }
572
+ }
580
573
  if (result.status !== 0 || verified.decision !== "ready") {
581
574
  const message = `Extension runtime recovery did not produce a current healthy runtime (${verified.reasonCode}).`;
582
575
  if (json) {
@@ -610,11 +603,17 @@ async function recoverRunInfra(adapter, target, json) {
610
603
  }
611
604
  if (adapter === "mobile") {
612
605
  const { launchMobile } = await import("./launch/mobile.js");
613
- const platform = process.env.PLATFORM === "android" ? "android" : "ios";
606
+ const platform = resolveRunMobilePlatform();
614
607
  return launchMobile(target, platform, "quick", json);
615
608
  }
616
609
  return { status: 0, output: "headless run retry" };
617
610
  }
611
+ function resolveRunMobilePlatform() {
612
+ if (process.env.PLATFORM === "android" || process.env.PLATFORM === "ios") {
613
+ return process.env.PLATFORM;
614
+ }
615
+ return process.env.ADB_SERIAL || process.env.ANDROID_SERIAL ? "android" : "ios";
616
+ }
618
617
  function readRunFailureText(result) {
619
618
  try {
620
619
  const trace = JSON.parse(fs.readFileSync(result.tracePath, "utf8"));
@@ -624,7 +623,7 @@ function readRunFailureText(result) {
624
623
  return "";
625
624
  }
626
625
  }
627
- async function executeWithHealBounds(exec, adapter, target, heal, state, recover = () => recoverRunInfra(adapter, target, false), onRecovery = () => void 0) {
626
+ async function executeWithHealBounds(exec, adapter, target, heal, state, recover = () => recoverRunInfra(adapter, target, false)) {
628
627
  let result = await exec();
629
628
  for (; ; ) {
630
629
  if (result.status === "pass") return { result, violation: null };
@@ -633,7 +632,6 @@ async function executeWithHealBounds(exec, adapter, target, heal, state, recover
633
632
  if (heal === "off") return { result, violation: null };
634
633
  const recoveryCode = RUN_RECOVERY_CODE[adapter];
635
634
  state.attemptedRecoveries.push(recoveryCode);
636
- onRecovery(recoveryCode);
637
635
  await recover();
638
636
  result = await exec();
639
637
  if (result.status === "pass") {
@@ -643,7 +641,7 @@ async function executeWithHealBounds(exec, adapter, target, heal, state, recover
643
641
  }
644
642
  }
645
643
  function emitHealViolation(json, command, result, violation, state, adapter) {
646
- const userAction = adapter === "core" && violation.code === "WALLET_STATE_REQUIRED" ? 'create temp/recipe/runtime/wallet-fixture.json, or add "account": "<0x\u2026>" to the node block in the recipe' : violation.userAction ?? `inspect ${shellQuoteArg(result.summaryPath)} and ${shellQuoteArg(result.tracePath)}; fix the application or recipe failure before retrying`;
644
+ const userAction = adapter === "core" && violation.code === "WALLET_STATE_REQUIRED" ? 'set MM_TEST_ACCOUNT_ADDRESS=<0x\u2026> in env, or add "account": "<0x\u2026>" to the node block in the recipe' : violation.userAction;
647
645
  if (json) {
648
646
  console.log(
649
647
  JSON.stringify(
@@ -663,7 +661,7 @@ function emitHealViolation(json, command, result, violation, state, adapter) {
663
661
  code: violation.code,
664
662
  message: violation.message,
665
663
  retryable: false,
666
- userAction,
664
+ userAction: userAction ?? null,
667
665
  originalError: violation.originalError ?? null
668
666
  }
669
667
  },
@@ -25,19 +25,10 @@ function renderRunReport(summary, entries) {
25
25
  "",
26
26
  `Status: ${String(summary.status ?? "unknown")}`,
27
27
  `Duration: ${formatDuration(Number(summary.durationMs ?? 0))}`,
28
- `Nodes: ${Number(summary.passed ?? 0)}/${Number(summary.total ?? entries.length)} passed`
28
+ `Nodes: ${Number(summary.passed ?? 0)}/${Number(summary.total ?? entries.length)} passed`,
29
+ "",
30
+ "## Steps"
29
31
  ];
30
- const sideFindings = isRecord(summary.sideFindings) ? summary.sideFindings : void 0;
31
- const sideFindingCounts = sideFindings && isRecord(sideFindings.counts) ? sideFindings.counts : void 0;
32
- const sideFindingTotal = Number(sideFindingCounts?.total ?? 0);
33
- if (sideFindingTotal > 0) {
34
- lines.push(
35
- "",
36
- "## Side findings",
37
- `- REVIEW ${sideFindingTotal} distinct application warning/error event(s) (non-blocking; expanded below and stored in diagnostics.json)`
38
- );
39
- }
40
- lines.push("", "## Steps");
41
32
  for (const entry of entries) {
42
33
  if (!isRecord(entry)) continue;
43
34
  const mark = entry.ok === false ? "FAIL" : "PASS";