@deeeed/metamask-harness 0.84.0 → 0.86.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 +65 -0
- package/README.md +5 -3
- package/adapters/extension/console-tail.mjs +11 -7
- package/adapters/extension/ensure-browser.sh +17 -18
- package/adapters/extension/refresh-build.sh +1 -1
- package/adapters/extension/start-watch.sh +17 -8
- package/adapters/manifest.json +1 -1
- package/adapters/mobile/reload-app.mjs +2 -1
- package/adapters/mobile/start-console-forwarder.sh +8 -0
- package/adapters/shared/resolve-slot-ports-core.mjs +48 -6
- package/dist/adapters/console-capture.js +3 -1
- package/dist/adapters/extension/actions.js +5 -0
- package/dist/adapters/extension/console-capture.js +18 -0
- package/dist/adapters/extension/fixtures.js +2 -0
- package/dist/adapters/extension/overlay.js +5 -4
- package/dist/adapters/extension/runtime.js +2 -0
- package/dist/adapters/mobile/launch.js +19 -2
- package/dist/adapters/mobile/provision.js +4 -2
- package/dist/adapters/mobile/run.js +3 -2
- package/dist/adapters/mobile/verify.js +15 -7
- package/dist/adapters/web-dapp/actions.js +7 -5
- package/dist/adapters.js +5 -3
- package/dist/command-contract.js +32 -20
- package/dist/commands/check.js +5 -7
- package/dist/commands/debug.js +2 -2
- package/dist/commands/doctor.js +2 -3
- package/dist/commands/domain.js +4 -3
- package/dist/commands/fixtures.js +3 -3
- package/dist/commands/help.js +6 -5
- package/dist/commands/launch/extension.js +9 -7
- package/dist/commands/logs.js +2 -2
- package/dist/commands/prepare.js +4 -4
- package/dist/commands/provision.js +2 -2
- package/dist/commands/review.js +8 -10
- package/dist/commands/runtime-launch.js +2 -0
- package/dist/commands/status-probe.js +8 -1
- package/dist/commands/status.js +4 -2
- package/dist/commands/task-init.js +2 -3
- package/dist/console-classifier.js +3 -2
- package/dist/doctor.js +6 -2
- package/dist/mm-harness-cli.js +19 -6
- package/dist/paths.js +9 -4
- package/dist/recipe-security.js +1 -1
- package/dist/review/knowledge.js +16 -4
- package/dist/runner.js +18 -3
- package/docs/CONTRIBUTING.md +11 -0
- package/docs/QA.md +1 -0
- package/docs/RECIPES.md +18 -2
- package/docs/cli-contract.md +8 -6
- package/library/actions/core/perps/_controller.mjs +2 -2
- package/library/actions/extension/app/assert_no_console_errors.mjs +95 -0
- package/library/actions/extension/platform/cdp.mjs +72 -2
- package/library/actions/extension/ui/navigate.mjs +3 -1
- package/library/actions/extension/wallet/select_account.mjs +17 -17
- package/library/actions/extension/wallet/state.mjs +75 -21
- package/library/actions/mobile/app/assert_no_console_errors.mjs +129 -0
- package/library/actions/mobile/wallet/select_account.mjs +34 -18
- package/library/actions/shared/console-assert.mjs +174 -0
- package/library/actions/shared/console-collector.mjs +10 -3
- package/library/actions/shared/console-findings.mjs +108 -7
- package/library/actions/web-dapp/app/assert_no_console_errors.mjs +10 -54
- package/library/actions/web-dapp/platform/hud.mjs +2 -9
- package/library/actions/web-dapp/platform/page.mjs +62 -16
- package/library/actions/web-dapp/platform/runtime.mjs +1 -1
- package/library/diagnostics/console-allowlist.json +13 -0
- package/library/manifests/core.action-manifest.json +6 -6
- package/library/manifests/extension.action-manifest.json +37 -1
- package/library/manifests/mobile.action-manifest.json +40 -4
- package/library/manifests/web-dapp.action-manifest.json +1 -1
- package/library/recipes/core/health/check.recipe.json +40 -0
- package/library/recipes/extension/health/check.recipe.json +71 -0
- package/library/recipes/mobile/health/check.recipe.json +71 -0
- package/package.json +9 -9
- package/scripts/completions.sh +2 -2
- package/site/assets/help-recipes.json +3 -2
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
writeMobileReleaseArtifactState
|
|
11
11
|
} from "../adapters/mobile/release-artifact-state.js";
|
|
12
12
|
import { extensionDepsBlock } from "./launch/extension.js";
|
|
13
|
+
import { recordExtensionConsoleLaunch } from "../adapters/extension/console-capture.js";
|
|
13
14
|
import { isExtensionDistStale } from "../adapters/extension/runtime-decision.js";
|
|
14
15
|
import { FeatureFlagSpecError, parseRemoteFlagSpec } from "../feature-flags.js";
|
|
15
16
|
async function handleRuntimeLaunch(args) {
|
|
@@ -186,6 +187,7 @@ async function handleRuntimeLaunchLocked({ options }) {
|
|
|
186
187
|
cdpPort
|
|
187
188
|
}));
|
|
188
189
|
}
|
|
190
|
+
recordExtensionConsoleLaunch(target);
|
|
189
191
|
const result = await spawnScriptStreaming(command[0], command.slice(1), target, {
|
|
190
192
|
forward: jsonMode ? "stderr" : "original",
|
|
191
193
|
stdin: jsonMode ? "ignore" : "inherit",
|
|
@@ -52,6 +52,13 @@ function parseBridgeEntries(raw) {
|
|
|
52
52
|
...e.featureFlags !== void 0 ? { featureFlags: e.featureFlags } : {}
|
|
53
53
|
}));
|
|
54
54
|
}
|
|
55
|
+
function androidModelMatches(deviceName, model, device, devices) {
|
|
56
|
+
if (deviceName === model || deviceName.startsWith(`${model} -`)) return true;
|
|
57
|
+
if (!deviceName.endsWith(` ${model}`) && !deviceName.includes(` ${model} -`)) return false;
|
|
58
|
+
const physical = (d) => d.platform === "android" && !d.id.startsWith("emulator-");
|
|
59
|
+
if (!physical(device)) return false;
|
|
60
|
+
return devices.filter((d) => physical(d) && d.name === device.name).length === 1;
|
|
61
|
+
}
|
|
55
62
|
function matchBridgeEntry(device, entries, devices) {
|
|
56
63
|
const byId = entries.find((e) => e.deviceName === device.id);
|
|
57
64
|
if (byId) return byId;
|
|
@@ -60,7 +67,7 @@ function matchBridgeEntry(device, entries, devices) {
|
|
|
60
67
|
if (byName) return byName;
|
|
61
68
|
if (device.platform === "android") {
|
|
62
69
|
const model = device.name.replace(/_/gu, " ");
|
|
63
|
-
const byModel = entries.find((e) => e.deviceName
|
|
70
|
+
const byModel = entries.find((e) => androidModelMatches(e.deviceName, model, device, devices));
|
|
64
71
|
if (byModel) return byModel;
|
|
65
72
|
}
|
|
66
73
|
}
|
package/dist/commands/status.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { featureFlagReport } from "../feature-flags.js";
|
|
4
|
-
import { color,
|
|
4
|
+
import { color, contextAdapter, harnessContextField, adapterDetectNext, EXIT, usageOut, optionFlag, optionString, targetPath } from "@farmslot/recipe-cli/harness";
|
|
5
5
|
import { collectCheckoutView, renderCheckoutView } from "../checkout-view.js";
|
|
6
6
|
import { assertAdapter } from "../paths.js";
|
|
7
7
|
import { getAdapterSurface } from "../adapters/surface.js";
|
|
@@ -24,7 +24,7 @@ async function handleStatus({ options }) {
|
|
|
24
24
|
}
|
|
25
25
|
const fast = optionFlag(options, "fast");
|
|
26
26
|
const allDevices = optionFlag(options, "allDevices");
|
|
27
|
-
const adapter =
|
|
27
|
+
const adapter = contextAdapter(target);
|
|
28
28
|
if (!adapter) {
|
|
29
29
|
return usageOut(json, "status", `could not detect the MetaMask repo type for ${target}`, adapterDetectNext());
|
|
30
30
|
}
|
|
@@ -58,6 +58,7 @@ async function handleStatus({ options }) {
|
|
|
58
58
|
command: "status",
|
|
59
59
|
adapter,
|
|
60
60
|
target,
|
|
61
|
+
...harnessContextField(),
|
|
61
62
|
view,
|
|
62
63
|
devices: liveView.devicesWithLive,
|
|
63
64
|
featureFlags: featureFlagReport(liveView.featureFlagOverrides),
|
|
@@ -79,6 +80,7 @@ async function handleStatus({ options }) {
|
|
|
79
80
|
command: "status",
|
|
80
81
|
adapter,
|
|
81
82
|
target,
|
|
83
|
+
...harnessContextField(),
|
|
82
84
|
view,
|
|
83
85
|
devices,
|
|
84
86
|
featureFlags: staticFeatureFlags,
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { harnessExecutable,
|
|
1
|
+
import { harnessExecutable, contextAdapter, EXIT } from "@farmslot/recipe-cli/harness";
|
|
2
2
|
import { spawnSync } from "node:child_process";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
|
-
import "../adapters/surface.js";
|
|
5
4
|
const require2 = createRequire(import.meta.url);
|
|
6
5
|
const USAGE = "mm-harness task init <task-dir> --flow f --template id --title t [options]";
|
|
7
6
|
function hasOption(argv, name) {
|
|
@@ -14,7 +13,7 @@ function metamaskDefaults(argv) {
|
|
|
14
13
|
defaults.push("--mark-command", `${harnessExecutable()} checklist mark`);
|
|
15
14
|
}
|
|
16
15
|
if (!hasOption(argv, "--platform")) {
|
|
17
|
-
const adapter =
|
|
16
|
+
const adapter = contextAdapter(process.cwd());
|
|
18
17
|
if (adapter) defaults.push("--platform", adapter);
|
|
19
18
|
}
|
|
20
19
|
return defaults;
|
|
@@ -4,13 +4,14 @@ import {
|
|
|
4
4
|
consoleRecords,
|
|
5
5
|
libraryRootsFromEnv,
|
|
6
6
|
loadConsoleAllowlist,
|
|
7
|
-
messageSignature
|
|
7
|
+
messageSignature,
|
|
8
|
+
withBundledLibraryRoot
|
|
8
9
|
} from "../library/actions/shared/console-findings.mjs";
|
|
9
10
|
const consoleClassifier = {
|
|
10
11
|
records: (text) => consoleRecords(text),
|
|
11
12
|
classify: (record) => classifyConsoleRecord(record),
|
|
12
13
|
signature: (message) => messageSignature(message),
|
|
13
|
-
loadAllowlist: (libraryRoots) => loadConsoleAllowlist([...libraryRoots]),
|
|
14
|
+
loadAllowlist: (libraryRoots) => loadConsoleAllowlist(withBundledLibraryRoot([...libraryRoots])),
|
|
14
15
|
match: (event, allowlist) => allowlistMatch(event, allowlist),
|
|
15
16
|
libraryRoots: () => libraryRootsFromEnv()
|
|
16
17
|
};
|
package/dist/doctor.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { RECIPE_CLI_VERSION } from "@farmslot/recipe-cli";
|
|
4
|
-
import { color, harnessExecutable, recipeHarnessRoot, recipeRuntimeDir, readRuntimeContextField, resolveRuntimeContextPath } from "@farmslot/recipe-cli/harness";
|
|
4
|
+
import { color, harnessContext, harnessContextField, harnessExecutable, recipeHarnessRoot, recipeRuntimeDir, readRuntimeContextField, resolveRuntimeContextPath } from "@farmslot/recipe-cli/harness";
|
|
5
5
|
import { normalizeFeatureFlagRecord } from "./feature-flags.js";
|
|
6
6
|
import { getAdapterSurface } from "./adapters/surface.js";
|
|
7
7
|
import { isPin } from "./harness-pin.js";
|
|
@@ -107,10 +107,13 @@ function runtimeContextSummary(target, adapter) {
|
|
|
107
107
|
const file = envOverride ?? path.relative(target, contextPath);
|
|
108
108
|
const fields = {};
|
|
109
109
|
const specs = adapter ? RUNTIME_CONTEXT_FIELDS.filter((spec) => spec.adapters.includes(adapter)) : RUNTIME_CONTEXT_FIELDS;
|
|
110
|
+
const filled = new Set(
|
|
111
|
+
Object.values(harnessContext()?.ports ?? {}).flatMap((port) => port?.filled ? port.names ?? [] : [])
|
|
112
|
+
);
|
|
110
113
|
for (const spec of specs) {
|
|
111
114
|
const envName = spec.envVars.find((name) => {
|
|
112
115
|
const value = process.env[name];
|
|
113
|
-
return value !== void 0 && value !== "";
|
|
116
|
+
return value !== void 0 && value !== "" && !filled.has(name);
|
|
114
117
|
});
|
|
115
118
|
if (envName !== void 0) {
|
|
116
119
|
fields[spec.key] = { value: process.env[envName], source: "env", envVar: envName, customize: spec.customize };
|
|
@@ -165,6 +168,7 @@ function createDoctorReport(adapter, target, manifestValidation, actionManifestP
|
|
|
165
168
|
requiredChecks,
|
|
166
169
|
adapter,
|
|
167
170
|
target,
|
|
171
|
+
...harnessContextField(),
|
|
168
172
|
runner: runnerProvenance(actionManifestPath),
|
|
169
173
|
compatibilityMode: mode,
|
|
170
174
|
shape: repoShape(target),
|
package/dist/mm-harness-cli.js
CHANGED
|
@@ -5,8 +5,8 @@ import fs from "node:fs";
|
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import {
|
|
8
|
+
contextAdapter,
|
|
8
9
|
createHarnessCli,
|
|
9
|
-
detectAdapter,
|
|
10
10
|
handleActions,
|
|
11
11
|
handleCall,
|
|
12
12
|
handleHarness,
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
handleRun,
|
|
17
17
|
handleStop,
|
|
18
18
|
harnessHost,
|
|
19
|
+
optionValues,
|
|
19
20
|
parseArgs,
|
|
20
21
|
targetPath
|
|
21
22
|
} from "@farmslot/recipe-cli/harness";
|
|
@@ -26,7 +27,9 @@ import {
|
|
|
26
27
|
explainMissingAdapterLibrary,
|
|
27
28
|
HIDDEN_COMMANDS,
|
|
28
29
|
PUBLIC_COMMANDS,
|
|
29
|
-
REPLACED_OPTIONS
|
|
30
|
+
REPLACED_OPTIONS,
|
|
31
|
+
selectableAdapters,
|
|
32
|
+
withAdapterList
|
|
30
33
|
} from "./command-contract.js";
|
|
31
34
|
import { handleCheck } from "./commands/check.js";
|
|
32
35
|
import { handleChecklist } from "./commands/checklist.js";
|
|
@@ -60,7 +63,7 @@ import { handleTutorial } from "./commands/tutorial.js";
|
|
|
60
63
|
import { handleUpdate, maybeNudge } from "./commands/update.js";
|
|
61
64
|
import { metaMaskEngine } from "./engine.js";
|
|
62
65
|
import { ActionManifestError } from "./manifest.js";
|
|
63
|
-
import { walletFixturePath } from "./paths.js";
|
|
66
|
+
import { harnessSlotPoolDir, walletFixturePath } from "./paths.js";
|
|
64
67
|
import { configPath, hydrateLibraryPathEnv, readConfig } from "./review/knowledge.js";
|
|
65
68
|
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
66
69
|
function hostVersion() {
|
|
@@ -99,7 +102,7 @@ function overlay(command) {
|
|
|
99
102
|
try {
|
|
100
103
|
const { options } = parseArgs(args);
|
|
101
104
|
const target = targetPath(options);
|
|
102
|
-
const adapterRaw = (
|
|
105
|
+
const adapterRaw = contextAdapter(target);
|
|
103
106
|
if (adapterRaw && adapterRegistry.has(adapterRaw)) {
|
|
104
107
|
await handleHarness(["install", "--platform", adapterRaw, "--target", target, "--quiet"]);
|
|
105
108
|
}
|
|
@@ -116,6 +119,9 @@ function overlay(command) {
|
|
|
116
119
|
return code;
|
|
117
120
|
};
|
|
118
121
|
}
|
|
122
|
+
function invocationLibraries() {
|
|
123
|
+
return optionValues(process.argv.slice(2), "--library");
|
|
124
|
+
}
|
|
119
125
|
function withManifestEnvelope(command, run) {
|
|
120
126
|
return async (args) => {
|
|
121
127
|
try {
|
|
@@ -169,7 +175,8 @@ const PUBLIC_RUN = {
|
|
|
169
175
|
fixtures: { run: handleFixtures }
|
|
170
176
|
};
|
|
171
177
|
const HIDDEN_RUN = {
|
|
172
|
-
|
|
178
|
+
// Shell completion answers for the checkout's adapter, so it resolves one.
|
|
179
|
+
"completion-candidates": { run: handleCompletionCandidates, nudge: false, context: "adapter" },
|
|
173
180
|
"runtime-health": { run: (args) => handleRuntimeHealth(parseArgs(args)) },
|
|
174
181
|
"runtime-decision": { run: (args) => handleRuntimeDecision(parseArgs(args, { recordIsFlag: true })) },
|
|
175
182
|
"resolve-extension": { run: (args) => handleResolveExtension(parseArgs(args)) },
|
|
@@ -227,9 +234,15 @@ await createHarnessCli({
|
|
|
227
234
|
afterAdapterLoad,
|
|
228
235
|
// `mm-harness config set libraries.<name>` libraries, read before hydration.
|
|
229
236
|
configuredLibraries: configuredLibrarySources,
|
|
237
|
+
slotPoolDir: () => harnessSlotPoolDir(),
|
|
230
238
|
commands: [
|
|
231
239
|
...PUBLIC_COMMANDS.map((command) => ({
|
|
232
240
|
...command,
|
|
241
|
+
// Rendered on read: the adapter list includes the operator's libraries
|
|
242
|
+
// and this invocation's --library entries.
|
|
243
|
+
get helpText() {
|
|
244
|
+
return withAdapterList(command.helpText, invocationLibraries());
|
|
245
|
+
},
|
|
233
246
|
...PUBLIC_RUN[command.name],
|
|
234
247
|
run: withManifestEnvelope(command.name, PUBLIC_RUN[command.name].run)
|
|
235
248
|
})),
|
|
@@ -244,7 +257,7 @@ await createHarnessCli({
|
|
|
244
257
|
description: "the MetaMask recipe harness: launch the app, prove behavior, manage the runtime overlay",
|
|
245
258
|
intro: (out) => [
|
|
246
259
|
`${out("bold", "mm-harness")} \u2014 one front door for the MetaMask recipe loop: launch the app, prove behavior, manage the runtime overlay.`,
|
|
247
|
-
|
|
260
|
+
`Run it from inside a MetaMask checkout; the platform (${selectableAdapters(invocationLibraries()).join(" | ")}) is auto-detected.`,
|
|
248
261
|
`Grammar: ${out("cmd", "mm-harness <command> [target] [flags]")} (target is a positional: ios | android; flags add agent depth; --json is the agent contract)`
|
|
249
262
|
],
|
|
250
263
|
groups: HELP_GROUPS,
|
package/dist/paths.js
CHANGED
|
@@ -17,12 +17,12 @@ function recipeWatchLogCandidates() {
|
|
|
17
17
|
path.join(recipeRuntimeDir(), "recipe-harness-webpack.log")
|
|
18
18
|
];
|
|
19
19
|
}
|
|
20
|
-
function resolveLocalProtocolRoot() {
|
|
21
|
-
const root =
|
|
20
|
+
function resolveLocalProtocolRoot(env = process.env) {
|
|
21
|
+
const root = env.METAMASK_RUNNER_PROTOCOL_ROOT || readConfiguredProtocolRoot();
|
|
22
22
|
return root ? path.resolve(root) : void 0;
|
|
23
23
|
}
|
|
24
|
-
function resolveFarmslotPoolDir(starts = [runnerDir, process.cwd()]) {
|
|
25
|
-
const configured = resolveLocalProtocolRoot();
|
|
24
|
+
function resolveFarmslotPoolDir(starts = [runnerDir, process.cwd()], env = process.env) {
|
|
25
|
+
const configured = resolveLocalProtocolRoot(env);
|
|
26
26
|
if (configured) return path.join(configured, "pool");
|
|
27
27
|
for (const start of starts) {
|
|
28
28
|
const pool = findPoolDir(start);
|
|
@@ -30,6 +30,10 @@ function resolveFarmslotPoolDir(starts = [runnerDir, process.cwd()]) {
|
|
|
30
30
|
}
|
|
31
31
|
return void 0;
|
|
32
32
|
}
|
|
33
|
+
function harnessSlotPoolDir(env = process.env, starts) {
|
|
34
|
+
if (env.FARMSLOT_POOL_DIR || env.FARMSLOT_ROOT) return void 0;
|
|
35
|
+
return resolveFarmslotPoolDir(starts, env);
|
|
36
|
+
}
|
|
33
37
|
const LIBRARY_ACTION_NAMESPACE = "metamask";
|
|
34
38
|
function readConfiguredProtocolRoot() {
|
|
35
39
|
const configPath = path.join(runnerDir, ".farmslot-root");
|
|
@@ -156,6 +160,7 @@ export {
|
|
|
156
160
|
PREPARE_PROGRESS_ARTIFACT,
|
|
157
161
|
assertAdapter,
|
|
158
162
|
extensionIdPath,
|
|
163
|
+
harnessSlotPoolDir,
|
|
159
164
|
importMetaMaskNativeUiProvider,
|
|
160
165
|
importRecipeHarness,
|
|
161
166
|
importRecipeHarnessAppLifecycle,
|
package/dist/recipe-security.js
CHANGED
|
@@ -32,7 +32,7 @@ const READ_ONLY_CUSTOM_ACTIONS = /* @__PURE__ */ new Set([
|
|
|
32
32
|
// Terminal wallet request log reads.
|
|
33
33
|
"metamask.wallet.read_signatures",
|
|
34
34
|
"metamask.wallet.assert_signatures",
|
|
35
|
-
//
|
|
35
|
+
// Console capture read (web-dapp, Mobile, Extension).
|
|
36
36
|
"metamask.app.assert_no_console_errors"
|
|
37
37
|
]);
|
|
38
38
|
const APP_MUTATION_CUSTOM_ACTIONS = /* @__PURE__ */ new Set([
|
package/dist/review/knowledge.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { detectAdapter, usageError } from "@farmslot/recipe-cli/harness";
|
|
1
|
+
import { AdapterAmbiguousError, detectAdapter, usageError } from "@farmslot/recipe-cli/harness";
|
|
2
2
|
import { execFileSync } from "node:child_process";
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import os from "node:os";
|
|
@@ -95,6 +95,7 @@ function withRevision(location) {
|
|
|
95
95
|
return revision ? { ...location, revision } : location;
|
|
96
96
|
}
|
|
97
97
|
function siblingRoots(context) {
|
|
98
|
+
if ((context.env ?? process.env).MM_HARNESS_SIBLING_LIBRARIES === "0") return [];
|
|
98
99
|
const home = context.homeDir ?? os.homedir();
|
|
99
100
|
const parents = [path.dirname(path.resolve(context.target)), path.join(home, "shared-library")];
|
|
100
101
|
const roots = [];
|
|
@@ -173,6 +174,9 @@ function resolveLibrary(name, options) {
|
|
|
173
174
|
return void 0;
|
|
174
175
|
}
|
|
175
176
|
function discoverLibraries(context) {
|
|
177
|
+
return discoverLibraryLocations(context).map(withRevision);
|
|
178
|
+
}
|
|
179
|
+
function discoverLibraryLocations(context) {
|
|
176
180
|
const env = context.env ?? process.env;
|
|
177
181
|
const seenRoots = /* @__PURE__ */ new Set();
|
|
178
182
|
const seenNames = /* @__PURE__ */ new Set();
|
|
@@ -184,7 +188,7 @@ function discoverLibraries(context) {
|
|
|
184
188
|
if (seenNames.has(libraryName)) return;
|
|
185
189
|
seenRoots.add(resolved);
|
|
186
190
|
seenNames.add(libraryName);
|
|
187
|
-
found.push(
|
|
191
|
+
found.push({ name: libraryName, root: resolved, source });
|
|
188
192
|
};
|
|
189
193
|
for (const entry of parseLibraryPathEnv(declaredLibraryPathFor(env))) add(entry.name, entry.root, "env");
|
|
190
194
|
for (const [name, root] of Object.entries(context.config?.libraries ?? {})) add(name, root, "config");
|
|
@@ -215,7 +219,7 @@ async function hydrateLibraryPathEnv(context) {
|
|
|
215
219
|
(entry) => entry.name ?? loadOwnedPaths(entry.root)?.domain ?? path.basename(entry.root)
|
|
216
220
|
)
|
|
217
221
|
);
|
|
218
|
-
const discovered =
|
|
222
|
+
const discovered = discoverLibraryLocations(context).filter(
|
|
219
223
|
(library) => library.source !== "env" && !declared.has(library.name)
|
|
220
224
|
);
|
|
221
225
|
if (discovered.length === 0) return;
|
|
@@ -234,7 +238,7 @@ function resolveReference(adapter, context) {
|
|
|
234
238
|
const fromConfig = context.config?.references?.[adapter];
|
|
235
239
|
if (fromConfig && isDir(path.resolve(fromConfig))) return { adapter, root: path.resolve(fromConfig), source: "config" };
|
|
236
240
|
const target = path.resolve(context.target);
|
|
237
|
-
const siblings = siblingRoots(context).filter((root) => root !== target &&
|
|
241
|
+
const siblings = siblingRoots(context).filter((root) => root !== target && siblingAdapter(root) === adapter).sort((a, b) => {
|
|
238
242
|
const refA = path.basename(a).endsWith("-ref") ? 0 : 1;
|
|
239
243
|
const refB = path.basename(b).endsWith("-ref") ? 0 : 1;
|
|
240
244
|
return refA - refB || path.basename(a).length - path.basename(b).length || a.localeCompare(b);
|
|
@@ -242,6 +246,14 @@ function resolveReference(adapter, context) {
|
|
|
242
246
|
if (siblings[0]) return { adapter, root: siblings[0], source: "sibling" };
|
|
243
247
|
return void 0;
|
|
244
248
|
}
|
|
249
|
+
function siblingAdapter(root) {
|
|
250
|
+
try {
|
|
251
|
+
return detectAdapter(root);
|
|
252
|
+
} catch (error) {
|
|
253
|
+
if (error instanceof AdapterAmbiguousError) return void 0;
|
|
254
|
+
throw error;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
245
257
|
function referenceHint(adapter) {
|
|
246
258
|
return `set ${referenceEnvName(adapter)} or run: mm-harness config set references.${adapter} <path>`;
|
|
247
259
|
}
|
package/dist/runner.js
CHANGED
|
@@ -78,7 +78,7 @@ async function createMetaMaskRunner(adapter, actionManifest, options = {}) {
|
|
|
78
78
|
const { createAgentDeviceUiTransport } = await importMetaMaskNativeUiProvider();
|
|
79
79
|
return createAgentDeviceUiTransport(options2);
|
|
80
80
|
}
|
|
81
|
-
}, preparedLiveAdapters
|
|
81
|
+
}, preparedLiveAdapters)
|
|
82
82
|
});
|
|
83
83
|
const existing = new Set([...core, ...ui].map((entry) => entry.action));
|
|
84
84
|
const lifecycleAdapters = bindMetaMaskMobileLifecycleDeadline(
|
|
@@ -97,7 +97,8 @@ async function createMetaMaskRunner(adapter, actionManifest, options = {}) {
|
|
|
97
97
|
].filter(
|
|
98
98
|
(entry) => declaredActions.has(entry.action) && !existing.has(entry.action) && !lifecycle.some((lifecycleEntry) => lifecycleEntry.action === entry.action)
|
|
99
99
|
);
|
|
100
|
-
const
|
|
100
|
+
const hudOffSwitch = process.env.METAMASK_RECIPE_AUTO_HUD === "0" || process.env.METAMASK_RECIPE_AUTO_HUD === "false";
|
|
101
|
+
const autoHudDisabled = options.autoHud === false || options.autoHud === void 0 && hudOffSwitch;
|
|
101
102
|
const externalMutationActions = new Set(
|
|
102
103
|
Object.entries(actionManifest.actions).filter(([, declaration]) => Array.isArray(declaration.execution_capabilities) && declaration.execution_capabilities.includes("external-mutation")).map(([action]) => action)
|
|
103
104
|
);
|
|
@@ -111,7 +112,7 @@ async function createMetaMaskRunner(adapter, actionManifest, options = {}) {
|
|
|
111
112
|
...adapterSecurity(entry.action, actionSources, options.trustTaskActions)
|
|
112
113
|
}));
|
|
113
114
|
const mutationBound = surface.actions.trustedMutation ? withTrustedMutationContext(secured, options.trustedMutation, externalMutationActions) : secured;
|
|
114
|
-
|
|
115
|
+
const runner = createRecipeRunner({
|
|
115
116
|
actionManifest,
|
|
116
117
|
adapters: withActionProgress(
|
|
117
118
|
surface.actions.wrap ? await surface.actions.wrap(mutationBound) : mutationBound,
|
|
@@ -139,6 +140,20 @@ async function createMetaMaskRunner(adapter, actionManifest, options = {}) {
|
|
|
139
140
|
}
|
|
140
141
|
}
|
|
141
142
|
});
|
|
143
|
+
const clearHud = surface.actions.clearHud;
|
|
144
|
+
if (options.autoHud !== false || hudOffSwitch || !clearHud) return runner;
|
|
145
|
+
return {
|
|
146
|
+
preflight: (request) => runner.preflight(request),
|
|
147
|
+
async run(request) {
|
|
148
|
+
if (request.projectRoot) {
|
|
149
|
+
await clearHud(request.projectRoot).catch((error) => {
|
|
150
|
+
process.stderr.write(`WARN: could not clear the HUD a previous run left (${error instanceof Error ? error.message : String(error)}); this run's screenshots may show it.
|
|
151
|
+
`);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
return runner.run(request);
|
|
155
|
+
}
|
|
156
|
+
};
|
|
142
157
|
}
|
|
143
158
|
function withTrustedMutationContext(adapters, base, externalMutationActions) {
|
|
144
159
|
let selectedIdentitySha256;
|
package/docs/CONTRIBUTING.md
CHANGED
|
@@ -176,9 +176,20 @@ or wrapper, verify real callers and retain compatibility only when one exists.
|
|
|
176
176
|
yarn test:unit
|
|
177
177
|
yarn test:coverage
|
|
178
178
|
yarn check
|
|
179
|
+
npm run build
|
|
179
180
|
bash tests/contract/run.sh
|
|
180
181
|
```
|
|
181
182
|
|
|
183
|
+
`run.sh` refuses to start when `dist/` is missing or older than any `src/**/*.ts`: the
|
|
184
|
+
CLI would then run the TypeScript source on every start. It runs half the
|
|
185
|
+
cores' worth of tests at once (`-j N` to change it), each
|
|
186
|
+
in its own process group and `TMPDIR`, and prints each test's duration and the
|
|
187
|
+
15 slowest. A test gets 60 s (`CONTRACT_TEST_TIMEOUT`); one whose product waits
|
|
188
|
+
are longer declares `# contract: timeout=<seconds>`, and one that cannot share
|
|
189
|
+
the machine (it rebuilds `dist/`) declares `# contract: serial` and runs after
|
|
190
|
+
the others. Whatever a test leaves running is killed and named on a `LEAK` line.
|
|
191
|
+
An unfiltered run over 3 minutes (`CONTRACT_BUDGET`) fails.
|
|
192
|
+
|
|
182
193
|
Runtime changes also require real affected-platform QA and a packed installation
|
|
183
194
|
test. Update `CHANGELOG.md` under `Unreleased` for user-visible behavior.
|
|
184
195
|
|
package/docs/QA.md
CHANGED
package/docs/RECIPES.md
CHANGED
|
@@ -19,6 +19,18 @@ mm-harness run <name> --describe
|
|
|
19
19
|
Discovery reports parameters, selected adapter variant, nested recipes, source,
|
|
20
20
|
and shadows. Use `--json` for exact metadata.
|
|
21
21
|
|
|
22
|
+
Before ticket work, run `mm-harness run health.check`. It is read-only and
|
|
23
|
+
picks the variant for the current adapter. Mobile and Extension check that the
|
|
24
|
+
app answers, onboarding is done, the wallet is unlocked on a selected account
|
|
25
|
+
at wallet home, and the app logged no console error since launch outside the
|
|
26
|
+
console allowlists, then take one screenshot. Core reads one HyperLiquid
|
|
27
|
+
testnet account through the Perps controller and requires a numeric balance.
|
|
28
|
+
A failure stops at a node named after the error (`APP_NOT_RUNNING`,
|
|
29
|
+
`ONBOARDING`, `WALLET_LOCKED`, `NO_SELECTED_ACCOUNT`, `NOT_ON_HOME`,
|
|
30
|
+
`CONSOLE_ERRORS`, `CONTROLLER_UNAVAILABLE`, `ACCOUNT_READ_FAILED`), and that
|
|
31
|
+
node's intent gives the recovery command. A reachable bridge does not prove
|
|
32
|
+
the iOS app is in the foreground; check the screenshot.
|
|
33
|
+
|
|
22
34
|
## Optional recipe advisor
|
|
23
35
|
|
|
24
36
|
`recipe-quality plan` assesses acceptance criteria against adapter-supported
|
|
@@ -317,7 +329,10 @@ block, skip, or refuse; a fixture that demands an empty account must converge
|
|
|
317
329
|
rather than refuse. Extension `ensure_*` and `teardown_state` only assert, so
|
|
318
330
|
converge there through an explicitly funded leaf action. Never broaden cleanup
|
|
319
331
|
onto an account or market the task does not own; obtain an owned one instead
|
|
320
|
-
of blocking.
|
|
332
|
+
of blocking. On mainnet, orders follow the Perps order rule above, Core needs
|
|
333
|
+
`CORE_PERPS_ALLOW_MAINNET_WRITES=1` for any write, Mobile cleanup acts on
|
|
334
|
+
whatever network the app is on, and Extension refuses mainnet writes other than
|
|
335
|
+
orders and `close_visible_position` in the fixture-policy lane.
|
|
321
336
|
|
|
322
337
|
### Mobile advanced-order setup
|
|
323
338
|
|
|
@@ -419,7 +434,8 @@ mm-harness run perps.performance \
|
|
|
419
434
|
--heal off --record-video=full-run
|
|
420
435
|
```
|
|
421
436
|
|
|
422
|
-
The
|
|
437
|
+
The recipe HUD is shown by default on every adapter (Mobile, Extension,
|
|
438
|
+
web-dapp/Terminal, headless browsers included), including during performance
|
|
423
439
|
validation. Use `--hud hide` only when a specific opaque/release proof requires
|
|
424
440
|
an unobstructed product surface; do not hide it merely to collect timings.
|
|
425
441
|
|
package/docs/cli-contract.md
CHANGED
|
@@ -101,14 +101,15 @@ Outside this boundary: a successful `launch` and the `temp/recipe/runtime/termin
|
|
|
101
101
|
## Running the goldens
|
|
102
102
|
|
|
103
103
|
```bash
|
|
104
|
-
|
|
104
|
+
npm run build # first: the families run the CLI from dist/, and refuse a stale one
|
|
105
|
+
yarn test:goldens # compare all families in parallel, within 2 minutes (CI runs them in the contract shards)
|
|
105
106
|
yarn test:goldens mobile # families whose name contains "mobile"
|
|
106
107
|
yarn test:goldens --update # rewrite goldens, print a per-field diff summary, drop orphans
|
|
107
108
|
```
|
|
108
109
|
|
|
109
|
-
Every family checks its own golden directory: a golden no case writes fails the family, in CI too (`--update` deletes it). Every family also fails on a golden directory that has no `goldens-<dir>.test.sh`, so deleting a whole family script is caught. Families run
|
|
110
|
+
Every family checks its own golden directory: a golden no case writes fails the family, in CI too (`--update` deletes it). Every family also fails on a golden directory that has no `goldens-<dir>.test.sh`, so deleting a whole family script is caught. Families run through `tests/contract/run.sh`, locally and inside the contract CI shards, as many at once as `-j` allows (half the cores by default).
|
|
110
111
|
|
|
111
|
-
Each case runs under a safety cap, `GOLDENS_CASE_TIMEOUT` (default
|
|
112
|
+
Families run through `tests/contract/run.sh`, so a family that hangs is stopped at its time limit, and the run fails over `GOLDENS_BUDGET` (default 120 s). Each case runs under a safety cap, `GOLDENS_CASE_TIMEOUT` (default 60 s). A case that hits it reports `TIMEOUT <case>` and fails without being compared or written, so `--update` can never accept a truncated capture.
|
|
112
113
|
|
|
113
114
|
Each family is a normal contract test (`tests/contract/goldens-<family>.test.sh`). `gd_init` (`tests/contract/goldens/lib.sh`) builds a sandbox on `ct_init`, and every case runs under `env -i`:
|
|
114
115
|
- `gd_init` first unsets every inherited `MM_HARNESS_*`, `RECIPE_*`, `METAMASK_*` and `FARMSLOT_*` variable, plus device and port pins (`IOS_SIMULATOR`, `ADB_SERIAL`, `WATCHER_PORT`, `METRO_PORT`, `CDP_PORT`, `IDB_PATH`, `ANDROID_HOME`, …). A developer's `MM_HARNESS_BIN` or `RECIPE_RN_IDB_PATH` export therefore cannot redirect a case;
|
|
@@ -116,12 +117,13 @@ Each family is a normal contract test (`tests/contract/goldens-<family>.test.sh`
|
|
|
116
117
|
- every interpreter and tool the goldens launch is resolved to its real binary before `HOME` moves into the sandbox: `node` (`process.execPath`), `git` (`git --exec-path`), `bash` (`$BASH`) and `timeout`. The goldens need no Python. A tool that resolves to a script, the shape of an asdf/mise/volta/pyenv shim, fails the family up front with a message naming it. Each is checked to run with a sandboxed `HOME`;
|
|
117
118
|
- `PATH` is pinned to the case stubs, the sandbox stubs, those real binaries, and `/usr/bin:/bin:/usr/sbin:/sbin`. Host tools (`idb`, `capture-helper`, a developer's Chrome) cannot change a golden;
|
|
118
119
|
- device-tool discovery is pinned: `RECIPE_RN_IDB_PATH` and `RECIPE_RN_ADB_PATH` point at sandbox stubs, which discovery checks before any absolute Homebrew or pipx path;
|
|
119
|
-
- localhost probes cannot reach a real server. `gd_reserve_ports` runs a Node port holder (`tests/contract/goldens/port-holder.mjs`) that holds ephemeral ports on `127.0.0.1` and `::1` and resets every connection it accepts, so Metro `/status` and CDP probes never get a response and nothing real can take the port mid-run. Each family exports the farm's slot port variables (`RECIPE_CDP_PORT`, `WATCHER_PORT`, `METRO_PORT`) set to those ports, so commands without an explicit port use them too. Goldens show them as `<PORT:NAME>`, rewritten only in port positions (`*port` keys, `:<port>`, `port <n>`/`PORT=<n>`, or a string that is exactly the port), so an unrelated equal number such as a 60000 ms timeout is never rewritten;
|
|
120
|
+
- localhost probes cannot reach a real server. `gd_reserve_ports` runs a Node port holder (`tests/contract/goldens/port-holder.mjs`) that holds ephemeral ports on `127.0.0.1` and `::1` and resets every connection it accepts, so Metro `/status` and CDP probes never get a response and nothing real can take the port mid-run. Each family exports the farm's slot port variables (`RECIPE_CDP_PORT`, `WATCHER_PORT`, `METRO_PORT`) set to those ports, so commands without an explicit port use them too. Goldens show them as `<PORT:NAME>`, rewritten only in port positions (`*port` keys, numbers under a `ports` object, `:<port>`, `port <n>`/`PORT=<n>`, or a string that is exactly the port), so an unrelated equal number such as a 60000 ms timeout is never rewritten;
|
|
120
121
|
- `tmux`, `xcrun` and `adb` are stubbed (`ct_init`, plus one booted simulator and one Android device for Mobile);
|
|
121
122
|
- forbidden stubs record any call to `open`, `osascript`, `lsappinfo`, `screencapture`, `yarn`, `npx`, `npm`, `corepack`, `gh` and Chrome/Chromium, and the family fails if one is called. A browser stub answers only `--version`, and `npm` only `root -g` (install-source detection), with a sandbox path;
|
|
122
123
|
- inert stubs on the case PATH only: `lsof`, `pgrep` and `pkill` find nothing, `ps` passes per-pid queries (`-p`, the CLI identifying its own processes) and lists an empty host for table listings, and `watchman` and `caffeinate` are no-ops. `stop` cannot see or signal a real process or touch a host daemon;
|
|
123
124
|
- `RECIPE_HARNESS_BROWSER` points at the sandbox Chrome stub, so browser resolution never probe-launches Chrome for Testing or reads host Chrome policy;
|
|
124
|
-
- `MM_HARNESS_MOBILE_VERIFY_PROBE_TIMEOUT_MS=2000` shortens Mobile verify's live-bridge wait (default 60 s), because no fixture provides a debug target
|
|
125
|
+
- `MM_HARNESS_MOBILE_VERIFY_PROBE_TIMEOUT_MS=2000` shortens Mobile verify's live-bridge wait (default 60 s), because no fixture provides a debug target;
|
|
126
|
+
- `goldens-mobile-launch` points `METAMASK_RECIPE_MOBILE_BRIDGE_SCRIPT` at a bridge that answers "no app is running" at once (`ct_no_mobile_app`): launch clears remote feature-flag overrides through the bridge, and the real one waits out its 10 s CDP timeout when no app runs.
|
|
125
127
|
|
|
126
128
|
Mobile Metro, device, bridge and wallet leaves, the Extension build/browser leaf, and the fixture-state leaf are stubbed through `MM_HARNESS_SCRIPT_BIN_*`. `leaf-calls.log` and `live-calls.log` are frozen per launch case.
|
|
127
129
|
|
|
@@ -140,7 +142,7 @@ The goldens need the harness to be a git checkout: `execution-provenance.json` `
|
|
|
140
142
|
|
|
141
143
|
`tests/contract/goldens/normalize.mjs` is the only normaliser. It rewrites only volatile values:
|
|
142
144
|
|
|
143
|
-
1. Absolute paths: the sandbox becomes `<SANDBOX>`, the repo `<REPO>`, the real `$HOME` `<HOME>`, the node binary and its prefix `<NODE>`/`<NODE_PREFIX>`, and `os.tmpdir()` `<TMP>`. Both `/var` and `/private/var` spellings are covered. Ports from `gd_reserve_ports` become `<PORT:NAME>` in port positions only: numbers under `*port` keys, and in text after `:`, `port `, `port=` or `PORT=`, or a string that is exactly the port (`^<port>$`; `60000 ms` is left alone).
|
|
145
|
+
1. Absolute paths: the sandbox becomes `<SANDBOX>`, the repo `<REPO>`, the real `$HOME` `<HOME>`, the node binary and its prefix `<NODE>`/`<NODE_PREFIX>`, and `os.tmpdir()` `<TMP>`. Both `/var` and `/private/var` spellings are covered. Ports from `gd_reserve_ports` become `<PORT:NAME>` in port positions only: numbers under `*port` keys or a `ports` object (the context's `ports.cdp.value`), and in text after `:`, `port `, `port=` or `PORT=`, or a string that is exactly the port (`^<port>$`; `60000 ms` is left alone).
|
|
144
146
|
2. UUIDs become `<UUID>`. Git object ids (40 hex, or a short hex value under a `*head`/`*gitRef`/`*commit` key) become `<GIT_REF>`. ISO-8601 timestamps become `<TIMESTAMP>`, and compact stamps in names `<STAMP>`. Path-derived slot ids `local-<adapter>-<8 hex>` become `local-<adapter>-<HASH>`.
|
|
145
147
|
3. The package version becomes `<HARNESS_VERSION>`. Any other `x.y.z` under a key ending in `version` becomes `<VERSION>`.
|
|
146
148
|
4. Numbers under volatile keys: `*pid` becomes `<PID>`, `inode` `<INODE>`, `*port` `<PORT>`, `*At`/`*Time`/`*timestamp`/`mtime*` `<TIMESTAMP>`, and `*Ms`/`duration*`/`elapsed*` `<DURATION>`. Values inside `schema` and `examples` subtrees (static manifest content) are never rewritten.
|
|
@@ -229,8 +229,8 @@ let cached = null;
|
|
|
229
229
|
/**
|
|
230
230
|
* Resolve the requested network. Default testnet; mainnet only when a node
|
|
231
231
|
* explicitly sets network: "mainnet". Mainnet reads are safe; mainnet mutations
|
|
232
|
-
* use REAL funds and must be explicitly requested
|
|
233
|
-
*
|
|
232
|
+
* use REAL funds and must be explicitly requested (see the Perps order rule in
|
|
233
|
+
* docs/RECIPES.md).
|
|
234
234
|
*/
|
|
235
235
|
export function resolveNetwork(input) {
|
|
236
236
|
const raw = String(input.node?.network ?? 'testnet').toLowerCase();
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Fail the recipe when MetaMask (its service worker or UI pages) logged an
|
|
3
|
+
// error or uncaught exception that no console allowlist marks as known noise.
|
|
4
|
+
// Reads extension-console.log, written by the slot's console collector
|
|
5
|
+
// (adapters/extension/console-tail.mjs). The same classifier and allowlist
|
|
6
|
+
// feed run diagnostics.
|
|
7
|
+
//
|
|
8
|
+
// Window: since "launch" (the default) judges the events since the launch
|
|
9
|
+
// began, by their own timestamps. The harness records each launch's start
|
|
10
|
+
// before it (re)starts the browser (extension-console.launch.json,
|
|
11
|
+
// written by src/adapters/extension/console-capture.ts): the log's offset
|
|
12
|
+
// and inode, and the time. The collector appends across launches, so
|
|
13
|
+
// the log is read from that offset; and a collector replaced on a launch that
|
|
14
|
+
// keeps the browser replays the browser's console from before it, so a record
|
|
15
|
+
// stamped (with the event's own CDP timestamp) before the launch time is left
|
|
16
|
+
// out too. Without a mark the action fails instead of reading earlier
|
|
17
|
+
// sessions. since "start-of-log" reads the whole file.
|
|
18
|
+
//
|
|
19
|
+
// It never passes on missing evidence: this runtime's collector must be
|
|
20
|
+
// running on the run's CDP port, and a control line logged in MetaMask's
|
|
21
|
+
// service worker or page must reach extension-console.log; otherwise the
|
|
22
|
+
// action fails as unavailable.
|
|
23
|
+
|
|
24
|
+
import { readFileSync } from 'node:fs';
|
|
25
|
+
import { pathToFileURL } from 'node:url';
|
|
26
|
+
|
|
27
|
+
import { recipeRuntimePath } from '@farmslot/recipe-cli/harness';
|
|
28
|
+
|
|
29
|
+
import { captureUnavailable, consoleLevels, inWindow, judgeConsole, nodeAllowlist, readFrom, readLaunchMark, reportVerdict, throwOnFindings } from '../../shared/console-assert.mjs';
|
|
30
|
+
import { collectorOwnerMarker, collectorStatus, verifyCapture } from '../../shared/console-collector.mjs';
|
|
31
|
+
import { runAdapter } from '../platform/cdp.mjs';
|
|
32
|
+
|
|
33
|
+
const LOG = 'extension-console.log';
|
|
34
|
+
const LAUNCH_MARK = 'extension-console.launch.json';
|
|
35
|
+
const unavailable = (detail) => captureUnavailable(detail, 'The MetaMask extension console');
|
|
36
|
+
|
|
37
|
+
// A file's content, or null when it does not exist.
|
|
38
|
+
function readOptional(file) {
|
|
39
|
+
try {
|
|
40
|
+
return readFileSync(file, 'utf8');
|
|
41
|
+
} catch (error) {
|
|
42
|
+
if (error?.code === 'ENOENT') return null;
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function assertNoConsoleErrors(input, env = process.env, deps = {}) {
|
|
48
|
+
const node = input.node ?? {};
|
|
49
|
+
const projectRoot = input.context?.projectRoot;
|
|
50
|
+
const levels = consoleLevels(node);
|
|
51
|
+
const log = recipeRuntimePath(projectRoot, LOG);
|
|
52
|
+
// The CDP port as every Extension live action resolves it.
|
|
53
|
+
const cdpPort = node.cdp_port ?? env.CDP_PORT ?? env.RECIPE_CDP_PORT;
|
|
54
|
+
if (!cdpPort) throw unavailable('no CDP port (node.cdp_port, CDP_PORT or RECIPE_CDP_PORT)');
|
|
55
|
+
const capture = (deps.collectorStatus ?? collectorStatus)({
|
|
56
|
+
pidFile: recipeRuntimePath(projectRoot, 'console-tail.pid'),
|
|
57
|
+
extensionLog: log,
|
|
58
|
+
marker: collectorOwnerMarker(recipeRuntimePath(projectRoot)),
|
|
59
|
+
cdpPort: String(cdpPort),
|
|
60
|
+
});
|
|
61
|
+
if (!capture.ok) throw unavailable(capture.detail);
|
|
62
|
+
const extensionId = readOptional(recipeRuntimePath(projectRoot, 'extension.id'))?.trim() || null;
|
|
63
|
+
const control = await (deps.verifyCapture ?? verifyCapture)({
|
|
64
|
+
cdpPort,
|
|
65
|
+
log,
|
|
66
|
+
target: 'extension',
|
|
67
|
+
extensionId,
|
|
68
|
+
...(node.timeout_ms ? { timeoutMs: Number(node.timeout_ms) } : {}),
|
|
69
|
+
});
|
|
70
|
+
if (!control.ok) throw unavailable(control.detail);
|
|
71
|
+
const mark = node.since === 'start-of-log' ? null : readLaunchMark(recipeRuntimePath(projectRoot, LAUNCH_MARK), unavailable);
|
|
72
|
+
const { text, reset } = readFrom(log, mark ?? { offset: 0 });
|
|
73
|
+
const allowlist = nodeAllowlist(node, env);
|
|
74
|
+
const keep = inWindow({ sinceMs: mark?.launchedAtMs ?? null, now: deps.now ?? Date.now() });
|
|
75
|
+
const { findings, allowlisted } = judgeConsole([text], { levels, allowlist, keep });
|
|
76
|
+
const output = {
|
|
77
|
+
action: input.action,
|
|
78
|
+
since: node.since ?? 'launch',
|
|
79
|
+
levels: [...levels],
|
|
80
|
+
findings,
|
|
81
|
+
allowlisted,
|
|
82
|
+
allowlistProblems: allowlist.problems,
|
|
83
|
+
capture: { collector: capture.detail, control: control.detail },
|
|
84
|
+
// The launch mark was past the end of a replaced (rotated) capture
|
|
85
|
+
// file; that file was read from its start.
|
|
86
|
+
offsetReset: reset,
|
|
87
|
+
proofPath: 'extension-console-capture',
|
|
88
|
+
};
|
|
89
|
+
throwOnFindings(findings, allowlist.problems);
|
|
90
|
+
return output;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
94
|
+
reportVerdict(() => runAdapter((input) => assertNoConsoleErrors(input)));
|
|
95
|
+
}
|