@deeeed/metamask-harness 0.73.0 → 0.75.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 (62) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +1 -1
  3. package/adapters/shared/ensure-runner-deps.sh +15 -3
  4. package/dist/action-values.js +15 -0
  5. package/dist/adapters/core/actions.js +20 -0
  6. package/dist/adapters/core/live-script-env.js +36 -0
  7. package/dist/adapters/core/run.js +54 -0
  8. package/dist/adapters/core/surface.js +8 -1
  9. package/dist/adapters/extension/action-inputs.js +206 -0
  10. package/dist/adapters/extension/actions.js +51 -0
  11. package/dist/adapters/extension/console-capture.js +13 -1
  12. package/dist/adapters/extension/launch.js +315 -0
  13. package/dist/adapters/extension/launched-browser.js +51 -0
  14. package/dist/adapters/extension/run.js +105 -0
  15. package/dist/adapters/extension/surface.js +17 -1
  16. package/dist/adapters/mobile/actions.js +54 -0
  17. package/dist/adapters/mobile/launch.js +192 -0
  18. package/dist/adapters/mobile/lifecycle.js +300 -0
  19. package/dist/adapters/mobile/run.js +156 -0
  20. package/dist/adapters/mobile/runtime-identity.js +34 -0
  21. package/dist/adapters/mobile/surface.js +19 -1
  22. package/dist/adapters/mobile/ui.js +701 -0
  23. package/dist/adapters/performance/cdp-trace.js +24 -6
  24. package/dist/adapters/performance/device-platform.js +10 -0
  25. package/dist/adapters/registry.js +17 -0
  26. package/dist/adapters/surface.js +6 -11
  27. package/dist/adapters/terminal/actions.js +43 -0
  28. package/dist/adapters/terminal/console-capture.js +14 -1
  29. package/dist/adapters/terminal/launch.js +85 -0
  30. package/dist/adapters/terminal/run.js +30 -0
  31. package/dist/adapters/terminal/surface.js +9 -1
  32. package/dist/adapters.js +32 -824
  33. package/dist/cli-commands.js +1 -0
  34. package/dist/command-contract.js +3 -0
  35. package/dist/commands/call.js +2 -5
  36. package/dist/commands/launch/device.js +342 -0
  37. package/dist/commands/launch/index.js +9 -942
  38. package/dist/commands/launch/quote.js +7 -0
  39. package/dist/commands/pin.js +226 -0
  40. package/dist/commands/review.js +3 -1
  41. package/dist/commands/run-engine.js +38 -138
  42. package/dist/commands/run-report.js +4 -48
  43. package/dist/commands/run.js +6 -48
  44. package/dist/commands/shared.js +2 -2
  45. package/dist/doctor.js +3 -1
  46. package/dist/execution-provenance.js +3 -2
  47. package/dist/harness-pin.js +10 -0
  48. package/dist/heal-bounds.js +3 -2
  49. package/dist/live-adapter-contract.js +8 -34
  50. package/dist/manifest.js +3 -5
  51. package/dist/metamask-action-validation.js +5 -196
  52. package/dist/mm-harness-cli.js +27 -2
  53. package/dist/network-observation.js +10 -3
  54. package/dist/performance-observation.js +15 -22
  55. package/dist/pr-body/render.js +3 -1
  56. package/dist/recording-target.js +4 -39
  57. package/dist/run-diagnostics.js +21 -60
  58. package/dist/run-recording.js +5 -4
  59. package/dist/runner.js +20 -368
  60. package/docs/CONTRIBUTING.md +20 -3
  61. package/package.json +2 -1
  62. package/scripts/completions.sh +2 -1
@@ -18,6 +18,22 @@ const EXTENSION_TRACE_CATEGORIES = [
18
18
  "disabled-by-default-devtools.timeline.frame",
19
19
  "v8.execute"
20
20
  ];
21
+ const MOBILE_TRACE_KIND = {
22
+ categories: MOBILE_TRACE_CATEGORIES,
23
+ rendererScoped: false,
24
+ nativeSource: "react-native-cdp-frame-timings",
25
+ javascriptTasks: false
26
+ };
27
+ const TRACE_KINDS = {
28
+ extension: {
29
+ categories: EXTENSION_TRACE_CATEGORIES,
30
+ rendererScoped: true,
31
+ nativeSource: "chromium-cdp-frame-timings",
32
+ javascriptTasks: true
33
+ },
34
+ ios: MOBILE_TRACE_KIND,
35
+ android: MOBILE_TRACE_KIND
36
+ };
21
37
  function createCdpTraceCollector(client, platform, marker) {
22
38
  let active;
23
39
  const offData = client.on("Tracing.dataCollected", (params) => {
@@ -60,7 +76,7 @@ function createCdpTraceCollector(client, platform, marker) {
60
76
  await client.send(
61
77
  "Tracing.start",
62
78
  {
63
- categories: (platform === "extension" ? EXTENSION_TRACE_CATEGORIES : MOBILE_TRACE_CATEGORIES).join(","),
79
+ categories: TRACE_KINDS[platform].categories.join(","),
64
80
  transferMode: "ReportEvents"
65
81
  },
66
82
  15e3
@@ -126,7 +142,8 @@ function createCdpTraceCollector(client, platform, marker) {
126
142
  }
127
143
  function parseTraceCapture(capture) {
128
144
  const marker = findClockMarker(capture.events, capture.markerName);
129
- if (capture.platform === "extension" && !finite(marker?.pid)) {
145
+ const kind = TRACE_KINDS[capture.platform];
146
+ if (kind.rendererScoped && !finite(marker?.pid)) {
130
147
  const reason = "Extension trace marker did not identify a renderer process.";
131
148
  return {
132
149
  platform: capture.platform,
@@ -135,12 +152,12 @@ function parseTraceCapture(capture) {
135
152
  trace: traceEvidence(capture, "unresolved")
136
153
  };
137
154
  }
138
- const scopedEvents = capture.platform === "extension" && finite(marker?.pid) ? capture.events.filter((event) => event.pid === marker.pid) : capture.events;
139
- const scope = capture.platform !== "extension" ? "mobile-host" : finite(marker?.pid) ? "extension-renderer" : "unresolved";
155
+ const scopedEvents = kind.rendererScoped && finite(marker?.pid) ? capture.events.filter((event) => event.pid === marker.pid) : capture.events;
156
+ const scope = !kind.rendererScoped ? "mobile-host" : finite(marker?.pid) ? "extension-renderer" : "unresolved";
140
157
  const trace = traceEvidence(
141
158
  { ...capture, events: scopedEvents },
142
159
  scope,
143
- capture.platform === "extension" && finite(marker?.pid) ? marker.pid : void 0
160
+ kind.rendererScoped && finite(marker?.pid) ? marker.pid : void 0
144
161
  );
145
162
  if (!finite(marker?.ts) || capture.markerHostEpochMs === void 0 || capture.markerUncertaintyMs === void 0) {
146
163
  const reason = "CDP trace clock marker was unavailable.";
@@ -181,7 +198,7 @@ function parseTraceCapture(capture) {
181
198
  capture.markerUncertaintyMs
182
199
  ),
183
200
  nativeUi: nativeUiSourceResult(
184
- capture.platform === "extension" ? "chromium-cdp-frame-timings" : "react-native-cdp-frame-timings",
201
+ kind.nativeSource,
185
202
  nativeSamples,
186
203
  status,
187
204
  gapReasons,
@@ -383,6 +400,7 @@ function asRecord(value) {
383
400
  return value && typeof value === "object" && !Array.isArray(value) ? value : {};
384
401
  }
385
402
  export {
403
+ TRACE_KINDS,
386
404
  createCdpTraceCollector,
387
405
  parseTraceCapture
388
406
  };
@@ -0,0 +1,10 @@
1
+ function devicePlatformFromEnv(env) {
2
+ const explicit = String(
3
+ env.MM_HARNESS_EXPLICIT_PLATFORM ?? env.RECIPE_HARNESS_PLATFORM ?? ""
4
+ ).toLowerCase();
5
+ if (explicit === "android" || explicit === "ios") return explicit;
6
+ return env.ADB_SERIAL || env.ANDROID_SERIAL || env.ANDROID_DEVICE ? "android" : "ios";
7
+ }
8
+ export {
9
+ devicePlatformFromEnv
10
+ };
@@ -0,0 +1,17 @@
1
+ const surfaces = /* @__PURE__ */ new Map();
2
+ const adapterRegistry = {
3
+ register(surface) {
4
+ if (surfaces.has(surface.adapter)) throw new Error(`adapter '${surface.adapter}' is already registered`);
5
+ surfaces.set(surface.adapter, surface);
6
+ },
7
+ get(id) {
8
+ const surface = surfaces.get(id);
9
+ if (!surface) throw new Error(`unknown adapter '${id}' (registered: ${adapterRegistry.list().join(", ")})`);
10
+ return surface;
11
+ },
12
+ has: (id) => surfaces.has(id),
13
+ list: () => [...surfaces.keys()]
14
+ };
15
+ export {
16
+ adapterRegistry
17
+ };
@@ -1,15 +1,10 @@
1
- import { coreSurface } from "./core/surface.js";
2
- import { extensionSurface } from "./extension/surface.js";
3
- import { mobileSurface } from "./mobile/surface.js";
4
- import { terminalSurface } from "./terminal/surface.js";
5
- const SURFACES = {
6
- mobile: mobileSurface,
7
- extension: extensionSurface,
8
- core: coreSurface,
9
- terminal: terminalSurface
10
- };
1
+ import { adapterRegistry } from "./registry.js";
2
+ import "./mobile/surface.js";
3
+ import "./extension/surface.js";
4
+ import "./core/surface.js";
5
+ import "./terminal/surface.js";
11
6
  function getAdapterSurface(adapter) {
12
- return SURFACES[adapter];
7
+ return adapterRegistry.get(adapter);
13
8
  }
14
9
  export {
15
10
  getAdapterSurface
@@ -0,0 +1,43 @@
1
+ import { manifestPath } from "../../paths.js";
2
+ const terminalActions = {
3
+ manifestPath: () => manifestPath("terminal"),
4
+ semantic: [
5
+ "app.network_capture",
6
+ "app.network_assert",
7
+ "app.performance_capture",
8
+ "app.performance_assert"
9
+ ],
10
+ cdpTarget: { transport: "chrome-cdp", probePath: "json/version" },
11
+ ui: (harness, { hudPolicy }) => ({
12
+ base: harness.createCdpWebUiTransport({
13
+ async withPage(input, callback) {
14
+ const { withTerminalPage } = await import("../../../library/actions/terminal/platform/page.mjs");
15
+ return withTerminalPage(
16
+ {
17
+ action: input.action,
18
+ node: input.node,
19
+ context: {
20
+ nodeId: input.context.nodeId,
21
+ projectRoot: input.context.projectRoot,
22
+ artifactsDir: input.context.artifactsDir
23
+ }
24
+ },
25
+ callback
26
+ );
27
+ }
28
+ }),
29
+ async execute(action, node, context, standard) {
30
+ if (action === "app.hud") {
31
+ const { showTerminalHud } = await import("../../../library/actions/terminal/platform/hud.mjs");
32
+ return showTerminalHud(
33
+ { action, node, context: { nodeId: context.nodeId, projectRoot: context.projectRoot } },
34
+ { hudPolicy }
35
+ );
36
+ }
37
+ return standard();
38
+ }
39
+ })
40
+ };
41
+ export {
42
+ terminalActions
43
+ };
@@ -1,5 +1,6 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
+ import { verifyCapture } from "../../../library/actions/shared/console-collector.mjs";
3
4
  import { recipeRuntimePath } from "../../paths.js";
4
5
  import { ensureConsoleCollector } from "../console-capture.js";
5
6
  const TERMINAL_CONSOLE_FILES = Object.freeze({
@@ -37,10 +38,22 @@ async function ensureTerminalConsoleCapture(target) {
37
38
  cwd: target
38
39
  });
39
40
  }
41
+ async function verifyTerminalConsoleControl(capture) {
42
+ let browser = null;
43
+ try {
44
+ const value = JSON.parse(fs.readFileSync(terminalRuntimeFile(capture.projectRoot, "browser.json"), "utf8"));
45
+ browser = typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
46
+ } catch {
47
+ browser = null;
48
+ }
49
+ if (typeof browser?.appOrigin !== "string") return { ok: false, detail: "no slot browser state (browser.json) to verify the page console capture against" };
50
+ return verifyCapture({ cdpPort: capture.cdpPort, log: capture.pageLog, target: "page", appOrigin: browser.appOrigin });
51
+ }
40
52
  export {
41
53
  TERMINAL_CONSOLE_FILES,
42
54
  ensureTerminalConsoleCapture,
43
55
  terminalCaptureFiles,
44
56
  terminalCdpPort,
45
- terminalRuntimeFile
57
+ terminalRuntimeFile,
58
+ verifyTerminalConsoleControl
46
59
  };
@@ -0,0 +1,85 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import path from "node:path";
3
+ import { flag, str } from "../../commands/shared.js";
4
+ import { runnerDir } from "../../paths.js";
5
+ const TERMINAL_ONLY_LAUNCH_OPTIONS = Object.freeze({
6
+ signer: "--signer",
7
+ account: "--account",
8
+ freshProfile: "--fresh-profile",
9
+ headless: "--headless",
10
+ headful: "--headful",
11
+ slowMo: "--slow-mo",
12
+ network: "--network",
13
+ mainnetConfirmation: "--mainnet-confirmation"
14
+ });
15
+ function terminalOnlyLaunchFlags(options) {
16
+ return Object.entries(TERMINAL_ONLY_LAUNCH_OPTIONS).filter(([key]) => options[key] !== void 0 && options[key] !== false).map(([, name]) => name);
17
+ }
18
+ function parseTerminalLauncherOutput(output) {
19
+ const text = output.trim();
20
+ if (!text) return { status: "fail", error: "the terminal launcher printed no summary" };
21
+ try {
22
+ return JSON.parse(text);
23
+ } catch {
24
+ const last = text.split("\n").filter((line) => line.trim()).pop() ?? "";
25
+ try {
26
+ return JSON.parse(last);
27
+ } catch {
28
+ return { status: "fail", error: `the terminal launcher printed no JSON summary: ${last.slice(0, 200)}` };
29
+ }
30
+ }
31
+ }
32
+ async function launchTerminal({ options, target, jsonOutput, stream, usage }) {
33
+ const cdpPort = str(options, "cdpPort") ?? process.env.RECIPE_CDP_PORT ?? process.env.CDP_PORT;
34
+ const appPort = str(options, "watcherPort") ?? process.env.TERMINAL_APP_PORT ?? process.env.RECIPE_WATCHER_PORT ?? process.env.WATCHER_PORT;
35
+ if (!cdpPort || !appPort) {
36
+ return usage(
37
+ "terminal launch needs the slot browser CDP port and the slot dev-server port.",
38
+ "mm-harness launch --adapter terminal --cdp-port <port> --watcher-port <app port>"
39
+ );
40
+ }
41
+ const args = [
42
+ path.join(runnerDir, "adapters", "terminal", "launch.mjs"),
43
+ "--target",
44
+ target,
45
+ "--cdp-port",
46
+ cdpPort,
47
+ "--app-port",
48
+ appPort,
49
+ "--signer",
50
+ str(options, "signer") ?? process.env.TERMINAL_SIGNER ?? "extension",
51
+ "--account",
52
+ str(options, "account") ?? process.env.TERMINAL_ACCOUNT ?? "dev1",
53
+ "--json"
54
+ ];
55
+ if (flag(options, "freshProfile")) args.push("--fresh-profile");
56
+ if (flag(options, "headless")) args.push("--headless");
57
+ if (flag(options, "headful")) args.push("--headful");
58
+ const slowMo = str(options, "slowMo");
59
+ if (slowMo) args.push("--slow-mo", slowMo);
60
+ const network = str(options, "network");
61
+ if (network) args.push("--network", network);
62
+ const mainnetConfirmation = str(options, "mainnetConfirmation");
63
+ if (mainnetConfirmation) args.push("--mainnet-confirmation", mainnetConfirmation);
64
+ stream.phase("launch", { adapter: "terminal", cdpPort, appPort });
65
+ let output = "";
66
+ let status = 0;
67
+ try {
68
+ output = execFileSync(process.execPath, args, { encoding: "utf8", stdio: ["ignore", "pipe", "inherit"] });
69
+ } catch (error) {
70
+ status = 1;
71
+ output = String(error.stdout ?? "");
72
+ }
73
+ const result = parseTerminalLauncherOutput(output);
74
+ if (jsonOutput) console.log(JSON.stringify({ schemaVersion: 1, command: "launch", adapter: "terminal", target, ...result }, null, 2));
75
+ else if (result.status === "pass") console.log(`\u2713 terminal browser ${result.reused ? "reused" : "started"} on CDP ${result.cdpPort} (${result.signer}, ${result.account?.name})`);
76
+ else console.error(`\u2717 terminal launch failed: ${result.error ?? "unknown error"}`);
77
+ stream.complete(result.status === "pass" ? "pass" : "fail", status);
78
+ return result.status === "pass" ? 0 : 1;
79
+ }
80
+ export {
81
+ TERMINAL_ONLY_LAUNCH_OPTIONS,
82
+ launchTerminal,
83
+ parseTerminalLauncherOutput,
84
+ terminalOnlyLaunchFlags
85
+ };
@@ -0,0 +1,30 @@
1
+ import { devicePlatformFromEnv } from "../performance/device-platform.js";
2
+ import {
3
+ ensureTerminalConsoleCapture,
4
+ terminalCaptureFiles,
5
+ terminalCdpPort,
6
+ terminalRuntimeFile,
7
+ verifyTerminalConsoleControl
8
+ } from "./console-capture.js";
9
+ const terminalDiagnostics = {
10
+ console: {
11
+ start: (projectRoot) => ensureTerminalConsoleCapture(projectRoot),
12
+ files: (projectRoot) => terminalCaptureFiles(projectRoot),
13
+ cdpPort: () => terminalCdpPort(),
14
+ verifyControl: (capture) => verifyTerminalConsoleControl(capture)
15
+ },
16
+ walletLog: (projectRoot) => terminalRuntimeFile(projectRoot, "wallet-requests.jsonl")
17
+ };
18
+ const terminalObservation = {
19
+ network: {
20
+ backend: async (_target, env, artifactsDir) => (await import("../../network-observation.js")).createExtensionNetworkBackend(env, artifactsDir)
21
+ },
22
+ performance: {
23
+ backend: async (target, env, ports) => (await import("../../performance-observation.js")).createExtensionPerformanceObserver(target, env, ports),
24
+ platform: (env) => devicePlatformFromEnv(env)
25
+ }
26
+ };
27
+ export {
28
+ terminalDiagnostics,
29
+ terminalObservation
30
+ };
@@ -3,6 +3,9 @@ import { shellQuote } from "../../commands/parse-args.js";
3
3
  import fs from "node:fs";
4
4
  import { TERMINAL_CONSOLE_FILES, terminalRuntimeFile } from "./console-capture.js";
5
5
  import { stopTerminalBrowserSync } from "./stop.js";
6
+ import { adapterRegistry } from "../registry.js";
7
+ import { terminalActions } from "./actions.js";
8
+ import { terminalDiagnostics, terminalObservation } from "./run.js";
6
9
  function portFromEnv(...names) {
7
10
  for (const name of names) {
8
11
  const raw = process.env[name];
@@ -95,8 +98,13 @@ const terminalSurface = {
95
98
  launch: "mm-harness launch --adapter terminal --cdp-port <port> --watcher-port <app port>",
96
99
  relaunch: "mm-harness launch --adapter terminal --fresh-profile",
97
100
  runtimeProbeRecovery: (target) => `mm-harness doctor --adapter terminal --target ${shellQuote(path.resolve(target))}`
98
- }
101
+ },
102
+ launch: async (context) => (await import("./launch.js")).launchTerminal(context),
103
+ actions: terminalActions,
104
+ diagnostics: terminalDiagnostics,
105
+ observation: terminalObservation
99
106
  };
107
+ adapterRegistry.register(terminalSurface);
100
108
  export {
101
109
  terminalAppPort,
102
110
  terminalCdpPort,