@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.
- package/CHANGELOG.md +22 -0
- package/README.md +1 -1
- package/adapters/shared/ensure-runner-deps.sh +15 -3
- package/dist/action-values.js +15 -0
- package/dist/adapters/core/actions.js +20 -0
- package/dist/adapters/core/live-script-env.js +36 -0
- package/dist/adapters/core/run.js +54 -0
- package/dist/adapters/core/surface.js +8 -1
- package/dist/adapters/extension/action-inputs.js +206 -0
- package/dist/adapters/extension/actions.js +51 -0
- package/dist/adapters/extension/console-capture.js +13 -1
- package/dist/adapters/extension/launch.js +315 -0
- package/dist/adapters/extension/launched-browser.js +51 -0
- package/dist/adapters/extension/run.js +105 -0
- package/dist/adapters/extension/surface.js +17 -1
- package/dist/adapters/mobile/actions.js +54 -0
- package/dist/adapters/mobile/launch.js +192 -0
- package/dist/adapters/mobile/lifecycle.js +300 -0
- package/dist/adapters/mobile/run.js +156 -0
- package/dist/adapters/mobile/runtime-identity.js +34 -0
- package/dist/adapters/mobile/surface.js +19 -1
- package/dist/adapters/mobile/ui.js +701 -0
- package/dist/adapters/performance/cdp-trace.js +24 -6
- package/dist/adapters/performance/device-platform.js +10 -0
- package/dist/adapters/registry.js +17 -0
- package/dist/adapters/surface.js +6 -11
- package/dist/adapters/terminal/actions.js +43 -0
- package/dist/adapters/terminal/console-capture.js +14 -1
- package/dist/adapters/terminal/launch.js +85 -0
- package/dist/adapters/terminal/run.js +30 -0
- package/dist/adapters/terminal/surface.js +9 -1
- package/dist/adapters.js +32 -824
- package/dist/cli-commands.js +1 -0
- package/dist/command-contract.js +3 -0
- package/dist/commands/call.js +2 -5
- package/dist/commands/launch/device.js +342 -0
- package/dist/commands/launch/index.js +9 -942
- package/dist/commands/launch/quote.js +7 -0
- package/dist/commands/pin.js +226 -0
- package/dist/commands/review.js +3 -1
- package/dist/commands/run-engine.js +38 -138
- package/dist/commands/run-report.js +4 -48
- package/dist/commands/run.js +6 -48
- package/dist/commands/shared.js +2 -2
- package/dist/doctor.js +3 -1
- package/dist/execution-provenance.js +3 -2
- package/dist/harness-pin.js +10 -0
- package/dist/heal-bounds.js +3 -2
- package/dist/live-adapter-contract.js +8 -34
- package/dist/manifest.js +3 -5
- package/dist/metamask-action-validation.js +5 -196
- package/dist/mm-harness-cli.js +27 -2
- package/dist/network-observation.js +10 -3
- package/dist/performance-observation.js +15 -22
- package/dist/pr-body/render.js +3 -1
- package/dist/recording-target.js +4 -39
- package/dist/run-diagnostics.js +21 -60
- package/dist/run-recording.js +5 -4
- package/dist/runner.js +20 -368
- package/docs/CONTRIBUTING.md +20 -3
- package/package.json +2 -1
- package/scripts/completions.sh +2 -1
package/dist/pr-body/render.js
CHANGED
|
@@ -37,6 +37,8 @@ function countRecipeNodes(recipe) {
|
|
|
37
37
|
if (Array.isArray(record.steps)) return record.steps.length;
|
|
38
38
|
const workflow = record.workflow;
|
|
39
39
|
if (!workflow || typeof workflow !== "object") return 0;
|
|
40
|
+
const graph = workflow.nodes;
|
|
41
|
+
if (graph && typeof graph === "object" && !Array.isArray(graph)) return Object.keys(graph).length;
|
|
40
42
|
return Object.values(workflow).reduce(
|
|
41
43
|
(total, value) => total + (Array.isArray(value) ? value.length : 0),
|
|
42
44
|
0
|
|
@@ -47,7 +49,7 @@ function recipeSection(file) {
|
|
|
47
49
|
const nodes = countRecipeNodes(recipe);
|
|
48
50
|
const record = recipe && typeof recipe === "object" ? recipe : {};
|
|
49
51
|
const title = typeof record.title === "string" ? record.title : typeof record.description === "string" ? record.description : "";
|
|
50
|
-
const summary = escapeHtml(`recipe.json (${nodes}
|
|
52
|
+
const summary = escapeHtml(`recipe.json (${nodes} nodes${title ? ` \u2014 ${title.slice(0, 120)}` : ""})`);
|
|
51
53
|
return {
|
|
52
54
|
nodes,
|
|
53
55
|
body: [`<details><summary>${summary}</summary>`, "", fenced(JSON.stringify(recipe, null, 2), "json"), "</details>"].join("\n")
|
package/dist/recording-target.js
CHANGED
|
@@ -6,45 +6,10 @@ const IOS_SIMULATOR_RECORDING_TARGET_PREFIX = "ios-simulator:";
|
|
|
6
6
|
function createMetaMaskRecordingTargetProvider(adapter) {
|
|
7
7
|
return {
|
|
8
8
|
async resolveRecordingTarget(context) {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
context.env.CDP_PORT ?? context.env.RECIPE_CDP_PORT ?? process.env.CDP_PORT ?? process.env.RECIPE_CDP_PORT
|
|
14
|
-
);
|
|
15
|
-
if (!pid) {
|
|
16
|
-
throw new Error(
|
|
17
|
-
"Could not resolve the extension browser PID for --record-video. Ensure the slot browser is running and CDP_PORT is set."
|
|
18
|
-
);
|
|
19
|
-
}
|
|
20
|
-
return { kind: "pid", pid };
|
|
21
|
-
}
|
|
22
|
-
if (adapter === "mobile") {
|
|
23
|
-
const platform = (context.env.PLATFORM ?? process.env.PLATFORM ?? "ios").trim().toLowerCase();
|
|
24
|
-
const androidSerial = resolveAndroidDeviceSerial(context.env);
|
|
25
|
-
if (platform === "android" || androidSerial) {
|
|
26
|
-
if (!androidSerial) {
|
|
27
|
-
throw new Error(
|
|
28
|
-
"Could not resolve an Android device for --record-video. Set ADB_SERIAL or pass --device to mm-harness run."
|
|
29
|
-
);
|
|
30
|
-
}
|
|
31
|
-
return {
|
|
32
|
-
kind: "android-device",
|
|
33
|
-
serial: androidSerial
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
const simulator = resolveMobileIosSimulatorName(context.env);
|
|
37
|
-
if (!simulator) {
|
|
38
|
-
throw new Error(
|
|
39
|
-
"Could not resolve an iOS simulator name for --record-video. Set IOS_SIMULATOR or pass --simulator to the recipe hook."
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
return {
|
|
43
|
-
kind: "window-id",
|
|
44
|
-
windowId: `${IOS_SIMULATOR_RECORDING_TARGET_PREFIX}${simulator}`
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
throw new Error(`--record-video is not implemented for the ${adapter} adapter.`);
|
|
9
|
+
const { getAdapterSurface } = await import("./adapters/surface.js");
|
|
10
|
+
const recording = getAdapterSurface(adapter).recording;
|
|
11
|
+
if (!recording) throw new Error(`--record-video is not implemented for the ${adapter} adapter.`);
|
|
12
|
+
return recording.target(context);
|
|
48
13
|
}
|
|
49
14
|
};
|
|
50
15
|
}
|
package/dist/run-diagnostics.js
CHANGED
|
@@ -1,20 +1,10 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
|
-
import { spawnSync } from "node:child_process";
|
|
5
4
|
import { getAdapterSurface } from "./adapters/surface.js";
|
|
6
|
-
import { recipeRuntimePath, runnerDir } from "./paths.js";
|
|
7
|
-
import { ensureExtensionConsoleCapture, extensionCaptureFiles, extensionCdpPort } from "./adapters/extension/console-capture.js";
|
|
8
|
-
import {
|
|
9
|
-
ensureTerminalConsoleCapture,
|
|
10
|
-
terminalCaptureFiles,
|
|
11
|
-
terminalCdpPort,
|
|
12
|
-
terminalRuntimeFile
|
|
13
|
-
} from "./adapters/terminal/console-capture.js";
|
|
14
5
|
import {
|
|
15
6
|
collectorOwnerMarker,
|
|
16
|
-
collectorStatus
|
|
17
|
-
verifyCapture
|
|
7
|
+
collectorStatus
|
|
18
8
|
} from "../library/actions/shared/console-collector.mjs";
|
|
19
9
|
import {
|
|
20
10
|
allowlistMatch,
|
|
@@ -39,7 +29,7 @@ function readRunDiagnosticsDocument(diagnosticsPath) {
|
|
|
39
29
|
}
|
|
40
30
|
function formatRunDiagnosticsForHuman(diagnostics, adapter) {
|
|
41
31
|
if (!diagnostics) {
|
|
42
|
-
return [adapter
|
|
32
|
+
return [getAdapterSurface(adapter).headless ? `N/A \u2014 ${adapter.charAt(0).toUpperCase()}${adapter.slice(1)} is headless and has no application log source.` : "UNAVAILABLE \u2014 application diagnostics could not be collected for this run."];
|
|
43
33
|
}
|
|
44
34
|
const status = diagnostics.status === "clean" ? "CLEAN" : diagnostics.status === "review" ? "REVIEW" : "UNAVAILABLE";
|
|
45
35
|
const lines = [`${status} \u2014 ${diagnostics.note}`];
|
|
@@ -52,25 +42,27 @@ function formatRunDiagnosticsForHuman(diagnostics, adapter) {
|
|
|
52
42
|
return lines;
|
|
53
43
|
}
|
|
54
44
|
async function beginRunDiagnostics(adapter, projectRoot) {
|
|
55
|
-
const
|
|
45
|
+
const surface = getAdapterSurface(adapter);
|
|
46
|
+
const source = surface.appLogSource(projectRoot);
|
|
56
47
|
if (!source) return null;
|
|
57
|
-
const
|
|
48
|
+
const diagnostics = surface.diagnostics;
|
|
49
|
+
const issueBuffer = diagnostics?.issueBuffer?.arm(projectRoot) ? { adapter, projectRoot } : void 0;
|
|
58
50
|
let capture;
|
|
59
|
-
if (
|
|
60
|
-
const
|
|
61
|
-
const startError = await start(projectRoot).then(() => void 0, (error) => error instanceof Error ? error.message : String(error));
|
|
62
|
-
const { extensionLog, pageLog, pidFile } =
|
|
63
|
-
const cdpPort =
|
|
51
|
+
if (diagnostics?.console) {
|
|
52
|
+
const collector = diagnostics.console;
|
|
53
|
+
const startError = await collector.start(projectRoot).then(() => void 0, (error) => error instanceof Error ? error.message : String(error));
|
|
54
|
+
const { extensionLog, pageLog, pidFile } = collector.files(projectRoot);
|
|
55
|
+
const cdpPort = collector.cdpPort();
|
|
64
56
|
capture = { adapter, projectRoot, extensionLog, pageLog, pidFile, ...cdpPort ? { cdpPort } : {}, ...startError ? { startError } : {} };
|
|
65
57
|
}
|
|
66
|
-
const walletLogPath =
|
|
58
|
+
const walletLogPath = diagnostics?.walletLog?.(projectRoot) ?? null;
|
|
67
59
|
const walletStat = walletLogPath ? safeStat(walletLogPath) : null;
|
|
68
60
|
const stat = safeStat(source.path);
|
|
69
61
|
return {
|
|
70
62
|
source,
|
|
71
63
|
offset: stat?.size ?? 0,
|
|
72
64
|
...stat ? { inode: stat.ino } : {},
|
|
73
|
-
...
|
|
65
|
+
...issueBuffer ? { issueBuffer } : {},
|
|
74
66
|
...capture ? { capture } : {},
|
|
75
67
|
...walletLogPath ? { walletLog: { path: walletLogPath, offset: walletStat?.size ?? 0, ...walletStat ? { inode: walletStat.ino } : {} } } : {}
|
|
76
68
|
};
|
|
@@ -85,20 +77,13 @@ async function verifyConsoleCapture(capture) {
|
|
|
85
77
|
cdpPort: capture.cdpPort
|
|
86
78
|
});
|
|
87
79
|
if (!status.ok) return { verified: false, detail: status.detail };
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
control = await verifyCapture({ cdpPort: capture.cdpPort, log: capture.extensionLog, target: "extension", extensionId });
|
|
97
|
-
} else {
|
|
98
|
-
const browser = readJsonRecord(terminalRuntimeFile(capture.projectRoot, "browser.json"));
|
|
99
|
-
if (typeof browser?.appOrigin !== "string") return { verified: false, detail: "no slot browser state (browser.json) to verify the page console capture against" };
|
|
100
|
-
control = await verifyCapture({ cdpPort: capture.cdpPort, log: capture.pageLog, target: "page", appOrigin: browser.appOrigin });
|
|
101
|
-
}
|
|
80
|
+
const control = await getAdapterSurface(capture.adapter).diagnostics.console.verifyControl({
|
|
81
|
+
projectRoot: capture.projectRoot,
|
|
82
|
+
cdpPort: capture.cdpPort,
|
|
83
|
+
extensionLog: capture.extensionLog,
|
|
84
|
+
pageLog: capture.pageLog,
|
|
85
|
+
pidFile: capture.pidFile
|
|
86
|
+
});
|
|
102
87
|
return { verified: control.ok, detail: control.ok ? `${status.detail}; ${control.detail}` : control.detail };
|
|
103
88
|
}
|
|
104
89
|
function isRunDiagnosticsDocument(value) {
|
|
@@ -109,7 +94,7 @@ function isRunDiagnosticsDocument(value) {
|
|
|
109
94
|
async function finishRunDiagnostics(baseline, result) {
|
|
110
95
|
if (!baseline) return result;
|
|
111
96
|
try {
|
|
112
|
-
const bufferedIssues = baseline.
|
|
97
|
+
const bufferedIssues = baseline.issueBuffer ? getAdapterSurface(baseline.issueBuffer.adapter).diagnostics?.issueBuffer?.collect(baseline.issueBuffer.projectRoot) : void 0;
|
|
113
98
|
const allowlist = loadConsoleAllowlist(recipeLibraryRoots(result.summaryPath));
|
|
114
99
|
const capture = baseline.capture ? await verifyConsoleCapture(baseline.capture) : void 0;
|
|
115
100
|
const { diagnostics, rawAppLog } = collectRunDiagnosticsCapture(baseline, bufferedIssues, allowlist, capture);
|
|
@@ -271,30 +256,6 @@ function applyConsoleAllowlist(findings, allowlist) {
|
|
|
271
256
|
finding.fingerprint = createHash("sha256").update(`${finding.fingerprint}|info|${match.file}#${match.index}`).digest("hex").slice(0, 12);
|
|
272
257
|
}
|
|
273
258
|
}
|
|
274
|
-
function armMobileIssueBuffer(projectRoot) {
|
|
275
|
-
const armed = runMobileIssueCommand(projectRoot, "issues-arm");
|
|
276
|
-
if (!armed) return false;
|
|
277
|
-
return runMobileIssueCommand(projectRoot, "issues-collect") !== null;
|
|
278
|
-
}
|
|
279
|
-
function collectMobileIssueBuffer(projectRoot) {
|
|
280
|
-
const result = runMobileIssueCommand(projectRoot, "issues-collect");
|
|
281
|
-
return isRecord(result) && Array.isArray(result.entries) ? result.entries : null;
|
|
282
|
-
}
|
|
283
|
-
function runMobileIssueCommand(projectRoot, command) {
|
|
284
|
-
const bridge = path.join(runnerDir, "adapters", "mobile", "bridge-runtime", "cdp-bridge.cjs");
|
|
285
|
-
const result = spawnSync(process.execPath, [bridge, command], {
|
|
286
|
-
cwd: projectRoot,
|
|
287
|
-
env: { ...process.env, APP_ROOT: projectRoot, CDP_TIMEOUT: "5000" },
|
|
288
|
-
encoding: "utf8",
|
|
289
|
-
timeout: 1e4
|
|
290
|
-
});
|
|
291
|
-
if (result.status !== 0) return null;
|
|
292
|
-
try {
|
|
293
|
-
return JSON.parse(result.stdout.trim());
|
|
294
|
-
} catch {
|
|
295
|
-
return null;
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
259
|
function redactPreview(value) {
|
|
299
260
|
return redactDiagnosticText(value).slice(0, MAX_PREVIEW_CHARS);
|
|
300
261
|
}
|
package/dist/run-recording.js
CHANGED
|
@@ -2,10 +2,10 @@ import { spawn, spawnSync } from "node:child_process";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { optionalVideoTiming, readCaptureHelperTiming, writeRecordingTimeline } from "@farmslot/recipe-harness";
|
|
5
|
+
import { getAdapterSurface } from "./adapters/surface.js";
|
|
5
6
|
import {
|
|
6
7
|
captureHelperSupportsRecordSessionSnapshots,
|
|
7
|
-
captureHelperSupportsCapability
|
|
8
|
-
resolveExtensionBrowserPid
|
|
8
|
+
captureHelperSupportsCapability
|
|
9
9
|
} from "./recording-target.js";
|
|
10
10
|
const activeRecordingsByPid = /* @__PURE__ */ new Map();
|
|
11
11
|
const ACTIVE_EXTENSION_RECORDING_PID = "METAMASK_RECIPE_EXTENSION_ACTIVE_RECORDING_PID";
|
|
@@ -17,7 +17,8 @@ async function startRecipeRecording(adapter, projectRoot, artifactsDir, options)
|
|
|
17
17
|
);
|
|
18
18
|
return void 0;
|
|
19
19
|
}
|
|
20
|
-
|
|
20
|
+
const framed = getAdapterSurface(adapter).recording?.framed;
|
|
21
|
+
if (!framed) return void 0;
|
|
21
22
|
const cdpPort = options.cdpPort ?? process.env.CDP_PORT ?? process.env.RECIPE_CDP_PORT;
|
|
22
23
|
if (!captureHelperSupportsRecordSessionSnapshots(projectRoot)) {
|
|
23
24
|
console.error(
|
|
@@ -25,7 +26,7 @@ async function startRecipeRecording(adapter, projectRoot, artifactsDir, options)
|
|
|
25
26
|
);
|
|
26
27
|
return void 0;
|
|
27
28
|
}
|
|
28
|
-
const pid =
|
|
29
|
+
const pid = framed.browserPid(projectRoot, artifactsDir, cdpPort);
|
|
29
30
|
if (!pid) {
|
|
30
31
|
console.error(
|
|
31
32
|
`WARN: framed extension recording could not resolve browser PID from CDP port ${cdpPort ?? "<unset>"}; falling back to harness --record-video.`
|