@deeeed/metamask-harness 0.74.0 → 0.76.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 +39 -1
- package/README.md +1 -1
- package/adapters/shared/ensure-runner-deps.sh +15 -3
- package/adapters/shared/reap-checkout-metros.sh +9 -0
- 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/overlay.js +34 -0
- package/dist/adapters/core/readiness.js +8 -0
- package/dist/adapters/core/run.js +54 -0
- package/dist/adapters/core/surface.js +12 -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/doctor-checks.js +135 -0
- package/dist/adapters/extension/fixtures.js +103 -0
- package/dist/adapters/extension/launch.js +315 -0
- package/dist/adapters/extension/launched-browser.js +51 -0
- package/dist/adapters/extension/overlay.js +55 -0
- package/dist/adapters/extension/readiness.js +32 -0
- package/dist/adapters/extension/reload.js +40 -0
- package/dist/adapters/extension/run.js +105 -0
- package/dist/adapters/extension/surface.js +31 -3
- package/dist/adapters/harness-freshness.js +4 -2
- package/dist/adapters/mobile/actions.js +54 -0
- package/dist/adapters/mobile/devices.js +21 -0
- package/dist/adapters/mobile/doctor-checks.js +40 -0
- package/dist/adapters/mobile/fixtures.js +141 -0
- package/dist/adapters/mobile/launch.js +192 -0
- package/dist/adapters/mobile/lifecycle.js +300 -0
- package/dist/adapters/mobile/overlay.js +27 -0
- package/dist/adapters/mobile/prepare-target.js +38 -0
- package/dist/adapters/mobile/readiness.js +39 -0
- package/dist/adapters/mobile/reload.js +15 -0
- package/dist/adapters/mobile/run.js +156 -0
- package/dist/adapters/mobile/runtime-identity.js +34 -0
- package/dist/adapters/mobile/surface.js +39 -3
- 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 +1 -1
- package/dist/adapters/terminal/actions.js +43 -0
- package/dist/adapters/terminal/console-capture.js +14 -1
- package/dist/adapters/terminal/doctor-checks.js +22 -0
- package/dist/adapters/terminal/overlay.js +18 -0
- package/dist/adapters/terminal/readiness.js +11 -0
- package/dist/adapters/terminal/run.js +30 -0
- package/dist/adapters/terminal/surface.js +21 -3
- package/dist/adapters.js +32 -824
- package/dist/cli-commands.js +1 -0
- package/dist/cli.js +3 -1
- package/dist/command-contract.js +3 -0
- package/dist/commands/call.js +2 -5
- package/dist/commands/debug.js +6 -25
- package/dist/commands/device-target.js +2 -1
- package/dist/commands/doctor.js +26 -137
- package/dist/commands/fixtures.js +13 -217
- package/dist/commands/launch/device.js +2 -6
- package/dist/commands/launch/extension.js +3 -303
- package/dist/commands/launch/index.js +1 -1
- package/dist/commands/launch/mobile.js +2 -190
- package/dist/commands/launch/quote.js +7 -0
- package/dist/commands/logs.js +1 -1
- package/dist/commands/parse-args.js +9 -4
- package/dist/commands/pin.js +226 -0
- package/dist/commands/prepare.js +23 -53
- package/dist/commands/provision.js +3 -7
- package/dist/commands/reload.js +8 -47
- 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/runtime-decision.js +5 -27
- package/dist/commands/self-test.js +1 -3
- package/dist/commands/status.js +20 -28
- package/dist/commands/stop.js +1 -1
- package/dist/doctor.js +12 -120
- package/dist/execution-provenance.js +3 -2
- package/dist/harness-pin.js +10 -0
- package/dist/harness.js +31 -100
- 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 +30 -4
- package/dist/network-observation.js +10 -3
- package/dist/paths.js +8 -2
- 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/dist/runtime-context.js +12 -17
- package/docs/CONTRIBUTING.md +29 -7
- package/package.json +2 -1
- package/scripts/completions.sh +2 -1
|
@@ -10,7 +10,8 @@ import { pathToFileURL } from "node:url";
|
|
|
10
10
|
import { build } from "esbuild";
|
|
11
11
|
import { init, parse } from "es-module-lexer";
|
|
12
12
|
import { resolveRequiredLocalProtocolRoot, runnerDir } from "./paths.js";
|
|
13
|
-
import {
|
|
13
|
+
import { adapterRegistry } from "./adapters/registry.js";
|
|
14
|
+
import { getAdapterSurface } from "./adapters/surface.js";
|
|
14
15
|
import { serializeTrustedMutationContext } from "./funding-execution-context.js";
|
|
15
16
|
function actionFileStem(action) {
|
|
16
17
|
return String(action).replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
@@ -213,8 +214,11 @@ function bindImportMetaUrls() {
|
|
|
213
214
|
}
|
|
214
215
|
};
|
|
215
216
|
}
|
|
217
|
+
function tsxLiveScripts(platform) {
|
|
218
|
+
return platform && adapterRegistry.has(platform) ? getAdapterSurface(platform).actions.tsxLiveScripts : void 0;
|
|
219
|
+
}
|
|
216
220
|
function commandFor(file, projectRoot, platform) {
|
|
217
|
-
const needsTsx = platform
|
|
221
|
+
const needsTsx = Boolean(tsxLiveScripts(platform)) || importsSourceTypescript(file);
|
|
218
222
|
if (!needsTsx) {
|
|
219
223
|
return { command: process.execPath, args: [file] };
|
|
220
224
|
}
|
|
@@ -268,36 +272,6 @@ function importsSourceTypescriptFrom(file, visited) {
|
|
|
268
272
|
async function resolveLiveAdapter(platform, action, runtimeEnv) {
|
|
269
273
|
return firstExecutablePath(candidatePaths(platform, action, runtimeEnv));
|
|
270
274
|
}
|
|
271
|
-
const CORE_WORKSPACE_PACKAGES = [
|
|
272
|
-
"@metamask/base-controller",
|
|
273
|
-
"@metamask/messenger",
|
|
274
|
-
"@metamask/controller-utils",
|
|
275
|
-
"@metamask/keyring-controller"
|
|
276
|
-
];
|
|
277
|
-
async function platformAdapterEnv(platform, projectRoot, tempDir) {
|
|
278
|
-
if (platform !== "core") return {};
|
|
279
|
-
const paths = {};
|
|
280
|
-
for (const pkg of CORE_WORKSPACE_PACKAGES) {
|
|
281
|
-
const packageDir = path.join(projectRoot, "packages", pkg.replace("@metamask/", ""));
|
|
282
|
-
const distEntry = path.join(packageDir, "dist/index.cjs");
|
|
283
|
-
if (existsSync(distEntry)) continue;
|
|
284
|
-
const srcDir = path.join(packageDir, "src");
|
|
285
|
-
if (!existsSync(path.join(srcDir, "index.ts"))) continue;
|
|
286
|
-
paths[pkg] = [path.join(srcDir, "index.ts")];
|
|
287
|
-
paths[`${pkg}/*`] = [path.join(srcDir, "*")];
|
|
288
|
-
}
|
|
289
|
-
if (Object.keys(paths).length === 0) return {};
|
|
290
|
-
const tsconfigPath = path.join(tempDir, "core-adapter.tsconfig.json");
|
|
291
|
-
await writeFile(
|
|
292
|
-
tsconfigPath,
|
|
293
|
-
`${JSON.stringify({ compilerOptions: { baseUrl: projectRoot, paths } }, null, 2)}
|
|
294
|
-
`
|
|
295
|
-
);
|
|
296
|
-
return {
|
|
297
|
-
TSX_TSCONFIG_PATH: tsconfigPath,
|
|
298
|
-
NODE_OPTIONS: corePnpNodeOptions(projectRoot, process.env.NODE_OPTIONS)
|
|
299
|
-
};
|
|
300
|
-
}
|
|
301
275
|
async function runLiveAdapterScript({ platform, action, node, context, prepared }) {
|
|
302
276
|
const script = prepared?.entryPath ?? await resolveLiveAdapter(platform, action, context?.env);
|
|
303
277
|
if (!script) return null;
|
|
@@ -320,7 +294,7 @@ async function runLiveAdapterScript({ platform, action, node, context, prepared
|
|
|
320
294
|
await writeFile(inputPath, `${JSON.stringify(input, null, 2)}
|
|
321
295
|
`);
|
|
322
296
|
const command = prepared ? commandForPrepared(context.projectRoot, platform) : commandFor(script, context.projectRoot, platform);
|
|
323
|
-
const platformEnv = await
|
|
297
|
+
const platformEnv = await tsxLiveScripts(platform)?.env(context.projectRoot, tempDir) ?? {};
|
|
324
298
|
const processTimeoutMs = liveAdapterProcessTimeoutMs(node);
|
|
325
299
|
const result = await runProcess(command.command, [...command.args, inputPath], {
|
|
326
300
|
cwd: context.projectRoot,
|
|
@@ -380,7 +354,7 @@ function liveAdapterProcessTimeoutMs(node) {
|
|
|
380
354
|
return 6e4 + settleAllowance;
|
|
381
355
|
}
|
|
382
356
|
function commandForPrepared(projectRoot, platform) {
|
|
383
|
-
if (platform
|
|
357
|
+
if (!tsxLiveScripts(platform)) {
|
|
384
358
|
return { command: process.execPath, args: ["--input-type=module", "-"] };
|
|
385
359
|
}
|
|
386
360
|
const tsxBin = resolveTsxBin(projectRoot);
|
package/dist/manifest.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { OFFICIAL_RECIPE_ACTIONS } from "@farmslot/protocol";
|
|
4
|
+
import { getAdapterSurface } from "./adapters/surface.js";
|
|
4
5
|
import { manifestPath, readJson, importRecipeProtocol } from "./paths.js";
|
|
5
6
|
function loadMetaMaskMobileActionManifest() {
|
|
6
7
|
return asActionManifest(readJson(manifestPath("mobile")));
|
|
@@ -16,10 +17,7 @@ function loadMetaMaskTerminalActionManifest() {
|
|
|
16
17
|
}
|
|
17
18
|
function loadActionManifest(adapter, overridePath) {
|
|
18
19
|
if (overridePath) return asActionManifest(readJson(path.resolve(overridePath)));
|
|
19
|
-
|
|
20
|
-
if (adapter === "core") return loadMetaMaskCoreActionManifest();
|
|
21
|
-
if (adapter === "terminal") return loadMetaMaskTerminalActionManifest();
|
|
22
|
-
return loadMetaMaskExtensionActionManifest();
|
|
20
|
+
return asActionManifest(readJson(getAdapterSurface(adapter).actions.manifestPath()));
|
|
23
21
|
}
|
|
24
22
|
async function resolveActionManifest(adapter, overridePath, librarySources, taskActionRoot) {
|
|
25
23
|
if (overridePath) {
|
|
@@ -40,7 +38,7 @@ async function resolveActionManifest(adapter, overridePath, librarySources, task
|
|
|
40
38
|
})
|
|
41
39
|
};
|
|
42
40
|
}
|
|
43
|
-
const canonicalPath = path.resolve(
|
|
41
|
+
const canonicalPath = path.resolve(getAdapterSurface(adapter).actions.manifestPath());
|
|
44
42
|
const canonicalRoot = path.dirname(path.dirname(canonicalPath));
|
|
45
43
|
const sources = librarySources?.length ? librarySources : [{ name: "metamask", root: canonicalRoot }];
|
|
46
44
|
const manifests = [];
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { adapterRegistry } from "./adapters/registry.js";
|
|
2
|
+
import { getAdapterSurface } from "./adapters/surface.js";
|
|
1
3
|
function isRecord(value) {
|
|
2
4
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
3
5
|
}
|
|
@@ -22,208 +24,15 @@ function validateMetaMaskActionInputs(recipe, adapter, params) {
|
|
|
22
24
|
if (!isRecord(recipe) || !isRecord(recipe.workflow)) return [];
|
|
23
25
|
const nodes = isRecord(recipe.workflow.nodes) ? recipe.workflow.nodes : void 0;
|
|
24
26
|
if (!nodes) return [];
|
|
27
|
+
const surface = adapter && adapterRegistry.has(adapter) ? getAdapterSurface(adapter) : void 0;
|
|
25
28
|
const findings = [];
|
|
26
29
|
for (const [nodeId, rawNode] of Object.entries(nodes)) {
|
|
27
30
|
const node = params ? resolveMetaMaskParameterValue(rawNode, params) : rawNode;
|
|
28
31
|
if (!isRecord(node)) continue;
|
|
29
32
|
const action = node.action;
|
|
30
|
-
|
|
31
|
-
if (typeof node[name] !== "number" || node[name] <= limit) return;
|
|
32
|
-
findings.push({
|
|
33
|
-
severity: "error",
|
|
34
|
-
code: "recipe.invalid_param",
|
|
35
|
-
path: `workflow.nodes.${nodeId}.${name}`,
|
|
36
|
-
message: `${action} ${name} must not exceed ${limit}`
|
|
37
|
-
});
|
|
38
|
-
};
|
|
39
|
-
if (action === "metamask.perps.read_snapshot" && adapter === "extension") {
|
|
40
|
-
if (node.market !== void 0 && node.markets !== void 0) {
|
|
41
|
-
findings.push({
|
|
42
|
-
severity: "error",
|
|
43
|
-
code: "recipe.invalid_param",
|
|
44
|
-
path: `workflow.nodes.${nodeId}.markets`,
|
|
45
|
-
message: `${action} accepts market or markets, not both`
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
if (isRecord(node.history)) {
|
|
49
|
-
const start = node.history.start_time_ms;
|
|
50
|
-
const end = node.history.end_time_ms;
|
|
51
|
-
if (typeof start === "number" && typeof end === "number" && start > end) {
|
|
52
|
-
findings.push({
|
|
53
|
-
severity: "error",
|
|
54
|
-
code: "recipe.invalid_param",
|
|
55
|
-
path: `workflow.nodes.${nodeId}.history.start_time_ms`,
|
|
56
|
-
message: `${action} history.start_time_ms must not exceed history.end_time_ms`
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
if (typeof node.history.limit === "number" && node.history.limit > 500) {
|
|
60
|
-
findings.push({
|
|
61
|
-
severity: "error",
|
|
62
|
-
code: "recipe.invalid_param",
|
|
63
|
-
path: `workflow.nodes.${nodeId}.history.limit`,
|
|
64
|
-
message: `${action} history.limit must not exceed 500`
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
if (typeof node.max_items_per_section === "number" && node.max_items_per_section > 500) {
|
|
69
|
-
findings.push({
|
|
70
|
-
severity: "error",
|
|
71
|
-
code: "recipe.invalid_param",
|
|
72
|
-
path: `workflow.nodes.${nodeId}.max_items_per_section`,
|
|
73
|
-
message: `${action} max_items_per_section must not exceed 500`
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
if (typeof node.timeout_ms === "number" && node.timeout_ms > 6e4) {
|
|
77
|
-
findings.push({
|
|
78
|
-
severity: "error",
|
|
79
|
-
code: "recipe.invalid_param",
|
|
80
|
-
path: `workflow.nodes.${nodeId}.timeout_ms`,
|
|
81
|
-
message: `${action} timeout_ms must not exceed 60000`
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
if (isRecord(node.sampling)) {
|
|
85
|
-
if (typeof node.sampling.count === "number" && node.sampling.count > 10) {
|
|
86
|
-
findings.push({
|
|
87
|
-
severity: "error",
|
|
88
|
-
code: "recipe.invalid_param",
|
|
89
|
-
path: `workflow.nodes.${nodeId}.sampling.count`,
|
|
90
|
-
message: `${action} sampling.count must not exceed 10`
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
if (typeof node.sampling.interval_ms === "number" && node.sampling.interval_ms > 3e4) {
|
|
94
|
-
findings.push({
|
|
95
|
-
severity: "error",
|
|
96
|
-
code: "recipe.invalid_param",
|
|
97
|
-
path: `workflow.nodes.${nodeId}.sampling.interval_ms`,
|
|
98
|
-
message: `${action} sampling.interval_ms must not exceed 30000`
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
if (isRecord(node.expect)) {
|
|
103
|
-
if (typeof node.expect.minimum_changed_market_count === "number" && node.expect.minimum_changed_market_count > 100) {
|
|
104
|
-
findings.push({
|
|
105
|
-
severity: "error",
|
|
106
|
-
code: "recipe.invalid_param",
|
|
107
|
-
path: `workflow.nodes.${nodeId}.expect.minimum_changed_market_count`,
|
|
108
|
-
message: `${action} expect.minimum_changed_market_count must not exceed 100`
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
if (typeof node.expect.minimum_history_count === "number" && node.expect.minimum_history_count > 500) {
|
|
112
|
-
findings.push({
|
|
113
|
-
severity: "error",
|
|
114
|
-
code: "recipe.invalid_param",
|
|
115
|
-
path: `workflow.nodes.${nodeId}.expect.minimum_history_count`,
|
|
116
|
-
message: `${action} expect.minimum_history_count must not exceed 500`
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
if (action === "metamask.perps.assert_visible_consistency" && adapter === "extension") {
|
|
122
|
-
maximum("sample_count", 10);
|
|
123
|
-
maximum("sample_interval_ms", 3e4);
|
|
124
|
-
maximum("minimum_changed_count", 200);
|
|
125
|
-
maximum("minimum_recent_activity_count", 50);
|
|
126
|
-
maximum("timeout_ms", 12e4);
|
|
127
|
-
maximum("max_items", 200);
|
|
128
|
-
}
|
|
129
|
-
const fundedVisibleMutation = adapter === "extension" && (action === "metamask.perps.close_visible_position" || action === "metamask.perps.close_orders" || action === "metamask.perps.edit_margin" || action === "metamask.perps.update_position_tpsl" || action === "metamask.perps.place_order" && node.confirm_live === true);
|
|
130
|
-
if (fundedVisibleMutation) {
|
|
131
|
-
const required = (name, pattern, message) => {
|
|
132
|
-
if (typeof node[name] === "string" && pattern.test(node[name])) return;
|
|
133
|
-
findings.push({
|
|
134
|
-
severity: "error",
|
|
135
|
-
code: "recipe.invalid_param",
|
|
136
|
-
path: `workflow.nodes.${nodeId}.${name}`,
|
|
137
|
-
message
|
|
138
|
-
});
|
|
139
|
-
};
|
|
140
|
-
const decimal = action === "metamask.perps.close_visible_position" || action === "metamask.perps.update_position_tpsl" ? node.funding_amount : node.amount ?? node.notional;
|
|
141
|
-
const decimalText = String(decimal ?? "");
|
|
142
|
-
const match = /^([1-9][0-9]*)(?:\.([0-9]{1,6}))?$/u.exec(decimalText);
|
|
143
|
-
if (!match) {
|
|
144
|
-
findings.push({
|
|
145
|
-
severity: "error",
|
|
146
|
-
code: "recipe.invalid_param",
|
|
147
|
-
path: `workflow.nodes.${nodeId}.${action === "metamask.perps.close_visible_position" || action === "metamask.perps.update_position_tpsl" ? "funding_amount" : "amount"}`,
|
|
148
|
-
message: `${action} funded decimal amount must be positive plain decimal text with at most six places`
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
if (action === "metamask.perps.place_order" && node.submit_attempts !== void 0 && node.submit_attempts !== 1) {
|
|
152
|
-
findings.push({
|
|
153
|
-
severity: "error",
|
|
154
|
-
code: "recipe.invalid_param",
|
|
155
|
-
path: `workflow.nodes.${nodeId}.submit_attempts`,
|
|
156
|
-
message: `${action} funded visible submission requires submit_attempts=1`
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
if (action === "metamask.perps.close_visible_position" || action === "metamask.perps.edit_margin" || action === "metamask.perps.update_position_tpsl") {
|
|
160
|
-
required("funding_side", /^(?:long|short)$/u, `${action} requires the authorized position side`);
|
|
161
|
-
}
|
|
162
|
-
if (action === "metamask.perps.edit_margin" || action === "metamask.perps.update_position_tpsl") {
|
|
163
|
-
required(
|
|
164
|
-
"funding_position_size",
|
|
165
|
-
/^(?=[0-9.]*[1-9])[0-9]+(?:\.[0-9]+)? [A-Z0-9:_-]{2,20}$/u,
|
|
166
|
-
`${action} requires the exact visible position size`
|
|
167
|
-
);
|
|
168
|
-
}
|
|
169
|
-
if (action === "metamask.perps.close_visible_position" && node.cleanup_only === true) {
|
|
170
|
-
required(
|
|
171
|
-
"funding_position_size",
|
|
172
|
-
/^(?=[0-9.]*[1-9])[0-9]+(?:\.[0-9]+)? [A-Z0-9:_-]{2,20}$/u,
|
|
173
|
-
`${action} cleanup requires the exact visible position size`
|
|
174
|
-
);
|
|
175
|
-
if (node.amount != null) {
|
|
176
|
-
findings.push({
|
|
177
|
-
severity: "error",
|
|
178
|
-
code: "recipe.invalid_param",
|
|
179
|
-
path: `workflow.nodes.${nodeId}.amount`,
|
|
180
|
-
message: `${action} cleanup must close the full exact position`
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
if (action === "metamask.perps.edit_margin" && !/^[1-9][0-9]*(?:\.[0-9]{1,6})?$/u.test(String(node.funding_amount ?? ""))) {
|
|
185
|
-
findings.push({
|
|
186
|
-
severity: "error",
|
|
187
|
-
code: "recipe.invalid_param",
|
|
188
|
-
path: `workflow.nodes.${nodeId}.funding_amount`,
|
|
189
|
-
message: `${action} requires the authorized position amount`
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
if (action === "metamask.perps.close_orders") {
|
|
193
|
-
required("side", /^(?:long|short)$/u, `${action} requires the exact order side`);
|
|
194
|
-
const outputPrice = /^\{\{outputs\.[A-Za-z0-9_.-]+\}\}$/u.test(String(node.limit_price ?? ""));
|
|
195
|
-
if (!outputPrice && (!/^(?:0|[1-9][0-9]*)(?:\.[0-9]{1,6})?$/u.test(String(node.limit_price ?? "")) || !(Number(node.limit_price) > 0))) {
|
|
196
|
-
findings.push({
|
|
197
|
-
severity: "error",
|
|
198
|
-
code: "recipe.invalid_param",
|
|
199
|
-
path: `workflow.nodes.${nodeId}.limit_price`,
|
|
200
|
-
message: `${action} requires an explicit positive limit_price for resumable cancellation`
|
|
201
|
-
});
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
if (action === "metamask.perps.place_order" && String(node.order_type ?? "market") === "limit" && (!/^(?:0|[1-9][0-9]*)(?:\.[0-9]{1,6})?$/u.test(String(node.limit_price ?? "")) || !(Number(node.limit_price) > 0))) {
|
|
205
|
-
findings.push({
|
|
206
|
-
severity: "error",
|
|
207
|
-
code: "recipe.invalid_param",
|
|
208
|
-
path: `workflow.nodes.${nodeId}.limit_price`,
|
|
209
|
-
message: `${action} funded limit submission requires explicit limit_price; offset-only pricing is not resumable`
|
|
210
|
-
});
|
|
211
|
-
}
|
|
212
|
-
for (const name of ["identity_rounding_tolerance_base_units", "balance_reconciliation_tolerance_base_units"]) {
|
|
213
|
-
if (node[name] === void 0) continue;
|
|
214
|
-
const value = String(node[name]);
|
|
215
|
-
if (!/^(?:0|[1-9][0-9]{0,6})$/u.test(value) || BigInt(value) > 1000000n) {
|
|
216
|
-
findings.push({
|
|
217
|
-
severity: "error",
|
|
218
|
-
code: "recipe.invalid_param",
|
|
219
|
-
path: `workflow.nodes.${nodeId}.${name}`,
|
|
220
|
-
message: `${action} ${name} must be an integer string from 0 through 1000000`
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
}
|
|
33
|
+
findings.push(...surface?.actions.inputFindings?.(nodeId, node) ?? []);
|
|
225
34
|
const state = typeof node.state === "string" ? node.state.toLowerCase() : void 0;
|
|
226
|
-
const createsOrder = action === "metamask.perps.place_order" ||
|
|
35
|
+
const createsOrder = action === "metamask.perps.place_order" || !surface?.actions.ensureChecksOnly && (action === "metamask.perps.ensure_positions" || action === "metamask.perps.ensure_orders") && (state === "open" || state === "present");
|
|
227
36
|
if (!createsOrder) continue;
|
|
228
37
|
const requireOneOf = (names, message) => {
|
|
229
38
|
const present = names.some((name) => {
|
package/dist/mm-harness-cli.js
CHANGED
|
@@ -7,6 +7,7 @@ import { color } from "./cli-color.js";
|
|
|
7
7
|
import { withCommandJournal } from "./command-journal.js";
|
|
8
8
|
import { JsonStreamWriter } from "./json-stream.js";
|
|
9
9
|
import { handleUpdate, maybeNudge } from "./commands/update.js";
|
|
10
|
+
import { handlePin } from "./commands/pin.js";
|
|
10
11
|
import { handleCallHelp } from "./commands/call.js";
|
|
11
12
|
import { handleHelp } from "./commands/help.js";
|
|
12
13
|
import { handleTutorial } from "./commands/tutorial.js";
|
|
@@ -834,6 +835,27 @@ Example:
|
|
|
834
835
|
Example:
|
|
835
836
|
mm-harness reload`
|
|
836
837
|
},
|
|
838
|
+
{
|
|
839
|
+
name: "pin",
|
|
840
|
+
summary: "Copy this installed mm-harness into an immutable, versioned store and print the copy for a task to run, so a later global upgrade cannot change a running task.",
|
|
841
|
+
example: "mm-harness pin",
|
|
842
|
+
helpText: `mm-harness pin [flags]
|
|
843
|
+
|
|
844
|
+
Copies the installed mm-harness running this command into an immutable,
|
|
845
|
+
versioned store (~/.local/share/mm-harness/pins/<version>-<hash>/; APFS clone
|
|
846
|
+
where available) and prints the copy's executable. A task records that path,
|
|
847
|
+
so \`npm i -g\` upgrading the global install never changes the harness a running
|
|
848
|
+
task runs. Idempotent: the same install returns the same pin. A source checkout
|
|
849
|
+
(MM_HARNESS_BIN) or an existing pin is printed as is.
|
|
850
|
+
|
|
851
|
+
--json Machine-readable { version, source, executable, pinned, created }
|
|
852
|
+
|
|
853
|
+
MM_HARNESS_PIN_ROOT overrides the store location.
|
|
854
|
+
|
|
855
|
+
Example:
|
|
856
|
+
mm-harness pin
|
|
857
|
+
mm-harness pin --json`
|
|
858
|
+
},
|
|
837
859
|
{
|
|
838
860
|
name: "update",
|
|
839
861
|
summary: "Update the installed mm-harness to the published latest (--check reports only; --json = {current, latest, updateAvailable}).",
|
|
@@ -980,7 +1002,7 @@ const HELP_GROUPS = [
|
|
|
980
1002
|
{
|
|
981
1003
|
title: "MAINTAIN",
|
|
982
1004
|
blurb: "keep the installed harness current with the npm registry",
|
|
983
|
-
commands: ["setup-base", "update"]
|
|
1005
|
+
commands: ["setup-base", "update", "pin"]
|
|
984
1006
|
}
|
|
985
1007
|
];
|
|
986
1008
|
function commandMeta(name) {
|
|
@@ -1010,8 +1032,9 @@ function detectedSlotLine(out) {
|
|
|
1010
1032
|
if (ctx.slotId) parts.push(`slot ${out("ok", String(ctx.slotId))}`);
|
|
1011
1033
|
if (ctx.simulator) parts.push(`device ${out("ok", String(ctx.simulator))}`);
|
|
1012
1034
|
const devServerPort = ctx.watcherPort ?? ctx.devServerPort ?? ctx.metroPort;
|
|
1013
|
-
|
|
1014
|
-
|
|
1035
|
+
const surface = adapter ? getAdapterSurface(adapter) : void 0;
|
|
1036
|
+
if (surface && !surface.headless && devServerPort) {
|
|
1037
|
+
parts.push(`${surface.devServer.label} :${out("ok", String(devServerPort))}`);
|
|
1015
1038
|
}
|
|
1016
1039
|
if (ctx.gitBranch) parts.push(`branch ${out("info", String(ctx.gitBranch))}`);
|
|
1017
1040
|
if (parts.length === 0) return null;
|
|
@@ -1088,6 +1111,9 @@ for (const command of REAL) {
|
|
|
1088
1111
|
if (command.name === "update") {
|
|
1089
1112
|
process.exit(await handleUpdate(rawArgv.slice(1)));
|
|
1090
1113
|
}
|
|
1114
|
+
if (command.name === "pin") {
|
|
1115
|
+
process.exit(handlePin(rawArgv.slice(1)));
|
|
1116
|
+
}
|
|
1091
1117
|
if (command.name === "help") {
|
|
1092
1118
|
process.exitCode = handleHelp(rawArgv.slice(1), { packageRoot, harnessVersion: pkgVersion });
|
|
1093
1119
|
return;
|
|
@@ -1116,7 +1142,7 @@ program.command("completions").description("Install/print bundled shell tab-comp
|
|
|
1116
1142
|
const result = spawnSync("bash", [script, ...rawArgv.slice(1)], { stdio: "inherit" });
|
|
1117
1143
|
process.exit(result.status ?? 1);
|
|
1118
1144
|
});
|
|
1119
|
-
const NUDGE_SKIP = ["help", "tutorial", "setup-base", "update", "completions", "completion-candidates"];
|
|
1145
|
+
const NUDGE_SKIP = ["help", "tutorial", "setup-base", "update", "pin", "completions", "completion-candidates"];
|
|
1120
1146
|
if (rawArgv.length > 0 && !NUDGE_SKIP.includes(rawArgv[0])) {
|
|
1121
1147
|
maybeNudge();
|
|
1122
1148
|
}
|
|
@@ -4,6 +4,7 @@ import configModule from "../adapters/mobile/bridge-runtime/lib/config.cjs";
|
|
|
4
4
|
import {
|
|
5
5
|
createExtensionNetworkObserver
|
|
6
6
|
} from "./adapters/extension/network-observer.js";
|
|
7
|
+
import { getAdapterSurface } from "./adapters/surface.js";
|
|
7
8
|
import {
|
|
8
9
|
indexArtifactManifest,
|
|
9
10
|
writeContainedArtifact
|
|
@@ -14,7 +15,8 @@ const { brokerSocketPath, createBrokerClient } = brokerModule;
|
|
|
14
15
|
const { resolvePort } = configModule;
|
|
15
16
|
const sessions = /* @__PURE__ */ new Map();
|
|
16
17
|
async function startRunNetworkObservation(adapter, target, artifactsDir, env, ports = {}) {
|
|
17
|
-
|
|
18
|
+
const network = getAdapterSurface(adapter).observation?.network;
|
|
19
|
+
if (!network) return void 0;
|
|
18
20
|
const runtimeEnv = {
|
|
19
21
|
...env,
|
|
20
22
|
...ports.cdpPort ? { CDP_PORT: ports.cdpPort, RECIPE_CDP_PORT: ports.cdpPort } : {},
|
|
@@ -29,7 +31,7 @@ async function startRunNetworkObservation(adapter, target, artifactsDir, env, po
|
|
|
29
31
|
};
|
|
30
32
|
sessions.set(key, session);
|
|
31
33
|
try {
|
|
32
|
-
session.backend =
|
|
34
|
+
session.backend = await network.backend(target, runtimeEnv, key);
|
|
33
35
|
} catch (error) {
|
|
34
36
|
session.setupError = boundedError(error);
|
|
35
37
|
}
|
|
@@ -76,7 +78,7 @@ async function startRunNetworkObservation(adapter, target, artifactsDir, env, po
|
|
|
76
78
|
};
|
|
77
79
|
}
|
|
78
80
|
async function handleRunNetworkAction(platform, action, node, context) {
|
|
79
|
-
if (platform
|
|
81
|
+
if (!getAdapterSurface(platform).observation?.network?.actions) return null;
|
|
80
82
|
if (action === "app.network_assert") {
|
|
81
83
|
const assertion = await import("../library/actions/shared/app/network-assert.mjs");
|
|
82
84
|
return {
|
|
@@ -261,6 +263,9 @@ async function indexArtifact(artifactManifestPath, relativePath, label) {
|
|
|
261
263
|
]
|
|
262
264
|
);
|
|
263
265
|
}
|
|
266
|
+
async function createExtensionNetworkBackend(env, artifactsDir) {
|
|
267
|
+
return createExtensionNetworkObserver(extensionCdpPort(env), artifactsDir);
|
|
268
|
+
}
|
|
264
269
|
function extensionCdpPort(env) {
|
|
265
270
|
const port = Number(
|
|
266
271
|
env.CDP_PORT ?? env.RECIPE_CDP_PORT ?? process.env.CDP_PORT
|
|
@@ -278,6 +283,8 @@ function asRecord(value) {
|
|
|
278
283
|
}
|
|
279
284
|
export {
|
|
280
285
|
connectMobileBroker,
|
|
286
|
+
createExtensionNetworkBackend,
|
|
287
|
+
createMobileNetworkBackend,
|
|
281
288
|
handleRunNetworkAction,
|
|
282
289
|
startRunNetworkObservation
|
|
283
290
|
};
|
package/dist/paths.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
4
|
+
import { adapterRegistry } from "./adapters/registry.js";
|
|
4
5
|
const runnerDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
5
6
|
const pathDefaults = readPathDefaults();
|
|
6
7
|
const DEFAULT_RECIPE_RUNTIME_DIR = pathDefaults.recipeRuntimeDir;
|
|
@@ -88,8 +89,13 @@ function isProtocolRoot(candidate) {
|
|
|
88
89
|
return fs.existsSync(path.join(candidate, "packages/recipe-harness/package.json")) && fs.existsSync(path.join(candidate, "packages/protocol/package.json"));
|
|
89
90
|
}
|
|
90
91
|
function assertAdapter(adapter) {
|
|
91
|
-
|
|
92
|
-
|
|
92
|
+
const ids = adapterRegistry.list();
|
|
93
|
+
if (ids.length === 0) {
|
|
94
|
+
throw new Error("internal: no adapters are registered yet; import src/adapters/surface.ts before validating an adapter");
|
|
95
|
+
}
|
|
96
|
+
if (typeof adapter !== "string" || !adapterRegistry.has(adapter)) {
|
|
97
|
+
const choices = ids.length > 1 ? `${ids.slice(0, -1).join(", ")}, or ${ids.at(-1)}` : ids.join("");
|
|
98
|
+
throw new Error(`Adapter must be ${choices}.`);
|
|
93
99
|
}
|
|
94
100
|
}
|
|
95
101
|
function manifestPath(adapter) {
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
3
|
import { gzip } from "node:zlib";
|
|
4
|
-
import {
|
|
5
|
-
createMobilePerformanceBackend
|
|
6
|
-
} from "./adapters/mobile/performance-observer.js";
|
|
4
|
+
import { getAdapterSurface } from "./adapters/surface.js";
|
|
7
5
|
import { createExtensionPerformanceBackend } from "./adapters/extension/performance-observer.js";
|
|
6
|
+
import { TRACE_KINDS } from "./adapters/performance/cdp-trace.js";
|
|
8
7
|
import { summarizeFrames } from "./adapters/mobile/frame-metrics.js";
|
|
9
8
|
import { summarizeJavaScriptTasks } from "./adapters/performance/js-task-metrics.js";
|
|
10
9
|
import {
|
|
@@ -17,10 +16,11 @@ const DEFAULT_MAX_DURATION_MS = 5 * 60 * 1e3;
|
|
|
17
16
|
const MAX_DURATION_MS = 60 * 60 * 1e3;
|
|
18
17
|
const gzipAsync = promisify(gzip);
|
|
19
18
|
function startRunPerformanceObservation(adapter, target, artifactsDir, env, ports = {}) {
|
|
20
|
-
|
|
19
|
+
const observer = getAdapterSurface(adapter).observation?.performance;
|
|
20
|
+
if (!observer) return void 0;
|
|
21
21
|
const key = path.resolve(artifactsDir);
|
|
22
22
|
const session = {
|
|
23
|
-
|
|
23
|
+
observer,
|
|
24
24
|
artifactsDir: key,
|
|
25
25
|
env: {
|
|
26
26
|
...env,
|
|
@@ -84,7 +84,7 @@ function startRunPerformanceObservation(adapter, target, artifactsDir, env, port
|
|
|
84
84
|
};
|
|
85
85
|
}
|
|
86
86
|
async function handleRunPerformanceAction(platform, action, node, context) {
|
|
87
|
-
if (platform
|
|
87
|
+
if (!getAdapterSurface(platform).observation?.performance) return null;
|
|
88
88
|
if (action === "app.performance_assert") {
|
|
89
89
|
return { output: await assertPerformance(node, context) };
|
|
90
90
|
}
|
|
@@ -154,10 +154,7 @@ async function performanceBackend(session) {
|
|
|
154
154
|
if (session.backend) return session.backend;
|
|
155
155
|
if (session.backendError) throw new Error(session.backendError);
|
|
156
156
|
try {
|
|
157
|
-
session.backend = session.
|
|
158
|
-
extensionCdpPort(session),
|
|
159
|
-
session.target
|
|
160
|
-
);
|
|
157
|
+
session.backend = await session.observer.backend(session.target, session.env, session.ports);
|
|
161
158
|
return session.backend;
|
|
162
159
|
} catch (error) {
|
|
163
160
|
session.backendError = errorMessage(error);
|
|
@@ -182,7 +179,7 @@ function finishCaptureWindow(session, window) {
|
|
|
182
179
|
return window.result;
|
|
183
180
|
}
|
|
184
181
|
function buildPerformanceArtifact(window, endedAtEpochMs, result, allEvents) {
|
|
185
|
-
const javascriptTasksApplicable = result.platform
|
|
182
|
+
const javascriptTasksApplicable = TRACE_KINDS[result.platform].javascriptTasks;
|
|
186
183
|
const javascript = javascriptTasksApplicable ? result.javascript : {
|
|
187
184
|
...result.javascript,
|
|
188
185
|
applicability: "not_applicable",
|
|
@@ -436,7 +433,7 @@ function chromeTracePath(jsonPath) {
|
|
|
436
433
|
return jsonPath.endsWith(".json") ? `${jsonPath.slice(0, -".json".length)}-trace.json.gz` : `${jsonPath}-trace.json.gz`;
|
|
437
434
|
}
|
|
438
435
|
function renderPerformanceHtml(artifact) {
|
|
439
|
-
const javascriptTasksApplicable = artifact.platform
|
|
436
|
+
const javascriptTasksApplicable = TRACE_KINDS[artifact.platform].javascriptTasks;
|
|
440
437
|
const iosCadence = artifact.platform === "ios";
|
|
441
438
|
const maxNodeDurationMs = Math.max(
|
|
442
439
|
1,
|
|
@@ -503,7 +500,7 @@ function unavailableCapture(session, window, reason) {
|
|
|
503
500
|
window,
|
|
504
501
|
Date.now(),
|
|
505
502
|
{
|
|
506
|
-
platform:
|
|
503
|
+
platform: session.observer.platform(session.env),
|
|
507
504
|
javascript: unavailableJavaScript,
|
|
508
505
|
nativeUi: unavailableNativeUi,
|
|
509
506
|
trace: {
|
|
@@ -520,17 +517,12 @@ function unavailableCapture(session, window, reason) {
|
|
|
520
517
|
session.nodeEvents
|
|
521
518
|
);
|
|
522
519
|
}
|
|
523
|
-
function
|
|
524
|
-
|
|
525
|
-
const explicit = String(
|
|
526
|
-
session.env.MM_HARNESS_EXPLICIT_PLATFORM ?? session.env.RECIPE_HARNESS_PLATFORM ?? ""
|
|
527
|
-
).toLowerCase();
|
|
528
|
-
if (explicit === "android" || explicit === "ios") return explicit;
|
|
529
|
-
return session.env.ADB_SERIAL || session.env.ANDROID_SERIAL || session.env.ANDROID_DEVICE ? "android" : "ios";
|
|
520
|
+
async function createExtensionPerformanceObserver(target, env, ports) {
|
|
521
|
+
return createExtensionPerformanceBackend(extensionCdpPort(ports, env), target);
|
|
530
522
|
}
|
|
531
|
-
function extensionCdpPort(
|
|
523
|
+
function extensionCdpPort(ports, env) {
|
|
532
524
|
const port = Number(
|
|
533
|
-
|
|
525
|
+
ports.cdpPort ?? env.CDP_PORT ?? env.RECIPE_CDP_PORT
|
|
534
526
|
);
|
|
535
527
|
if (!Number.isInteger(port) || port <= 0) {
|
|
536
528
|
throw new Error("Extension performance observation requires CDP_PORT.");
|
|
@@ -572,6 +564,7 @@ function errorMessage(error) {
|
|
|
572
564
|
}
|
|
573
565
|
export {
|
|
574
566
|
buildPerformanceArtifact,
|
|
567
|
+
createExtensionPerformanceObserver,
|
|
575
568
|
handleRunPerformanceAction,
|
|
576
569
|
renderPerformanceHtml,
|
|
577
570
|
startRunPerformanceObservation
|
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
|
}
|