@deeeed/metamask-harness 0.92.0 → 0.92.2
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 +13 -0
- package/dist/adapters/console-capture.js +48 -0
- package/dist/runner.js +10 -5
- package/docs/RECIPES.md +3 -2
- package/library/actions/extension/app/assert_no_console_errors.mjs +4 -3
- package/library/actions/shared/console-assert.mjs +15 -1
- package/library/actions/web-dapp/app/assert_no_console_errors.mjs +13 -5
- package/library/manifests/extension.action-manifest.json +1 -1
- package/library/manifests/web-dapp.action-manifest.json +1 -1
- package/package.json +8 -8
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.92.2 - 2026-10-14
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Check console errors within the current recipe run when reusing a browser, preserving prior logs and still rejecting new errors.
|
|
10
|
+
|
|
11
|
+
## 0.92.1 - 2026-10-14
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- Use the coordinated recipe-runner 0.34.1 dependency release so recorded UI runs bind the browser after setup, with one engine version across adapters and recipe hosts.
|
|
16
|
+
|
|
17
|
+
|
|
5
18
|
## 0.92.0 - 2026-10-14
|
|
6
19
|
|
|
7
20
|
### Changed
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
3
|
import fs from "node:fs";
|
|
3
4
|
import path from "node:path";
|
|
5
|
+
import { RUN_CONSOLE_WINDOW_ENV } from "../../library/actions/shared/console-assert.mjs";
|
|
4
6
|
import {
|
|
5
7
|
collectorMatches,
|
|
6
8
|
collectorOwnerMarker,
|
|
@@ -13,6 +15,51 @@ import {
|
|
|
13
15
|
readPid
|
|
14
16
|
} from "../../library/actions/shared/console-collector.mjs";
|
|
15
17
|
import { runnerDir } from "../paths.js";
|
|
18
|
+
import { isRecord } from "../action-values.js";
|
|
19
|
+
function createRunConsoleWindow(captureFiles) {
|
|
20
|
+
const current = new AsyncLocalStorage();
|
|
21
|
+
const wrapAdapters = (adapters) => adapters.map((entry) => ({
|
|
22
|
+
...entry,
|
|
23
|
+
execute(node, context) {
|
|
24
|
+
const env = { ...context.env };
|
|
25
|
+
delete env[RUN_CONSOLE_WINDOW_ENV];
|
|
26
|
+
const boundary = current.getStore();
|
|
27
|
+
if (boundary) env[RUN_CONSOLE_WINDOW_ENV] = boundary;
|
|
28
|
+
return entry.execute(node, { ...context, env });
|
|
29
|
+
}
|
|
30
|
+
}));
|
|
31
|
+
const wrapRunner = (runner) => ({
|
|
32
|
+
preflight: (request) => runner.preflight(request),
|
|
33
|
+
async run(request) {
|
|
34
|
+
const { DEFAULT_RECIPE_RUNTIME_DIR, harnessHost, recipeRuntimePath, validateRelativeRecipePath } = await import("@farmslot/recipe-cli/harness");
|
|
35
|
+
if (isStandaloneCall(request, harnessHost().name)) return current.run(void 0, () => runner.run(request));
|
|
36
|
+
const projectRoot = fs.realpathSync(path.resolve(request.projectRoot ?? process.cwd()));
|
|
37
|
+
const startedAtMs = Date.now();
|
|
38
|
+
const { pageLog, extensionLog } = captureFiles(projectRoot);
|
|
39
|
+
const env = { ...process.env, ...request.env };
|
|
40
|
+
const runtime = path.join(projectRoot, validateRelativeRecipePath("RECIPE_RUNTIME_DIR", env.RECIPE_RUNTIME_DIR || DEFAULT_RECIPE_RUNTIME_DIR));
|
|
41
|
+
const parentRuntime = recipeRuntimePath(projectRoot);
|
|
42
|
+
const files = Object.fromEntries([pageLog, extensionLog].map((original) => {
|
|
43
|
+
const file = path.join(runtime, path.relative(parentRuntime, original));
|
|
44
|
+
try {
|
|
45
|
+
const stat = fs.statSync(file);
|
|
46
|
+
return [file, { offset: stat.size, inode: stat.ino }];
|
|
47
|
+
} catch (error) {
|
|
48
|
+
if (error.code !== "ENOENT") throw error;
|
|
49
|
+
return [file, { offset: 0 }];
|
|
50
|
+
}
|
|
51
|
+
}));
|
|
52
|
+
return current.run(JSON.stringify({ projectRoot, startedAtMs, files }), () => runner.run(request));
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
return { wrapAdapters, wrapRunner };
|
|
56
|
+
}
|
|
57
|
+
function isStandaloneCall(request, host) {
|
|
58
|
+
const document = request.recipeDocument;
|
|
59
|
+
if (request.recipePath || !isRecord(document) || !isRecord(document.workflow)) return false;
|
|
60
|
+
const nodes = document.workflow.nodes;
|
|
61
|
+
return isRecord(nodes) && isRecord(nodes.call) && isRecord(nodes.done) && Object.keys(nodes).length === 2 && document.workflow.entry === "call" && document.title === `${host} call ${nodes.call.action}` && nodes.call.next === "done" && nodes.done.action === "end";
|
|
62
|
+
}
|
|
16
63
|
const COLLECTOR_SCRIPT = path.join(runnerDir, "adapters", "extension", "console-tail.mjs");
|
|
17
64
|
async function ensureConsoleCollector(options) {
|
|
18
65
|
const { cdpPort, extensionLog, pageLog, collectorLog, pidFile } = options;
|
|
@@ -132,6 +179,7 @@ function containsAttachment(from) {
|
|
|
132
179
|
export {
|
|
133
180
|
COLLECTOR_SCRIPT,
|
|
134
181
|
ROTATE_LOG_BYTES,
|
|
182
|
+
createRunConsoleWindow,
|
|
135
183
|
ensureConsoleCollector,
|
|
136
184
|
verifyOwnedCollectorControl
|
|
137
185
|
};
|
package/dist/runner.js
CHANGED
|
@@ -3,6 +3,7 @@ import { createHash } from "node:crypto";
|
|
|
3
3
|
import { OFFICIAL_RECIPE_ACTIONS } from "@farmslot/protocol";
|
|
4
4
|
import { createMetaMaskAdapters, createMetaMaskUiTransport } from "./adapters.js";
|
|
5
5
|
import { getAdapterSurface } from "./adapters/surface.js";
|
|
6
|
+
import { createRunConsoleWindow } from "./adapters/console-capture.js";
|
|
6
7
|
import {
|
|
7
8
|
bindMetaMaskMobileLifecycleDeadline,
|
|
8
9
|
createMetaMaskMobileLifecycleCommandRunner,
|
|
@@ -113,12 +114,15 @@ async function createMetaMaskRunner(adapter, actionManifest, options = {}) {
|
|
|
113
114
|
...adapterSecurity(entry.action, actionSources, options.trustTaskActions)
|
|
114
115
|
}));
|
|
115
116
|
const mutationBound = surface.actions.trustedMutation ? withTrustedMutationContext(secured, options.trustedMutation, externalMutationActions) : secured;
|
|
116
|
-
const
|
|
117
|
+
const consoleCapture = surface.diagnostics?.console;
|
|
118
|
+
const consoleWindow = consoleCapture ? createRunConsoleWindow((projectRoot) => consoleCapture.files(projectRoot)) : void 0;
|
|
119
|
+
const adapters = withActionProgress(
|
|
120
|
+
surface.actions.wrap ? await surface.actions.wrap(mutationBound) : mutationBound,
|
|
121
|
+
options.onActionEvent
|
|
122
|
+
);
|
|
123
|
+
const engineRunner = createRecipeRunner({
|
|
117
124
|
actionManifest,
|
|
118
|
-
adapters:
|
|
119
|
-
surface.actions.wrap ? await surface.actions.wrap(mutationBound) : mutationBound,
|
|
120
|
-
options.onActionEvent
|
|
121
|
-
),
|
|
125
|
+
adapters: consoleWindow ? consoleWindow.wrapAdapters(adapters) : adapters,
|
|
122
126
|
logger,
|
|
123
127
|
recording: {
|
|
124
128
|
targetProvider: createRecordingTargetProvider(adapter),
|
|
@@ -141,6 +145,7 @@ async function createMetaMaskRunner(adapter, actionManifest, options = {}) {
|
|
|
141
145
|
}
|
|
142
146
|
}
|
|
143
147
|
});
|
|
148
|
+
const runner = consoleWindow ? consoleWindow.wrapRunner(engineRunner) : engineRunner;
|
|
144
149
|
const clearHud = surface.actions.clearHud;
|
|
145
150
|
if (options.autoHud !== false || hudOffSwitch || !clearHud) return runner;
|
|
146
151
|
return {
|
package/docs/RECIPES.md
CHANGED
|
@@ -22,8 +22,9 @@ and shadows. Use `--json` for exact metadata.
|
|
|
22
22
|
Before ticket work, run `mm-harness run health.check`. It is read-only and
|
|
23
23
|
picks the variant for the current adapter. Mobile and Extension check that the
|
|
24
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
|
|
26
|
-
|
|
25
|
+
at wallet home, and the app logged no console error outside the console
|
|
26
|
+
allowlists, then take one screenshot. Extension checks this recipe run;
|
|
27
|
+
Mobile retains its launch window. Core reads one HyperLiquid
|
|
27
28
|
testnet account through the Perps controller and requires a numeric balance.
|
|
28
29
|
A failure stops at a node named after the error (`APP_NOT_RUNNING`,
|
|
29
30
|
`ONBOARDING`, `WALLET_LOCKED`, `NO_SELECTED_ACCOUNT`, `NOT_ON_HOME`,
|
|
@@ -26,7 +26,7 @@ import { pathToFileURL } from 'node:url';
|
|
|
26
26
|
|
|
27
27
|
import { recipeRuntimePath } from '@farmslot/recipe-cli/harness';
|
|
28
28
|
|
|
29
|
-
import { captureUnavailable, consoleLevels, inWindow, judgeConsole, nodeAllowlist, readFrom, readLaunchMark, reportVerdict, throwOnFindings } from '../../shared/console-assert.mjs';
|
|
29
|
+
import { captureUnavailable, consoleLevels, inWindow, judgeConsole, nodeAllowlist, readFrom, readLaunchMark, reportVerdict, runConsoleMark, throwOnFindings } from '../../shared/console-assert.mjs';
|
|
30
30
|
import { collectorOwnerMarker, collectorStatus, verifyCapture } from '../../shared/console-collector.mjs';
|
|
31
31
|
import { runAdapter } from '../platform/cdp.mjs';
|
|
32
32
|
|
|
@@ -68,14 +68,15 @@ export async function assertNoConsoleErrors(input, env = process.env, deps = {})
|
|
|
68
68
|
...(node.timeout_ms ? { timeoutMs: Number(node.timeout_ms) } : {}),
|
|
69
69
|
});
|
|
70
70
|
if (!control.ok) throw unavailable(control.detail);
|
|
71
|
-
const
|
|
71
|
+
const runMark = runConsoleMark(input, env, log);
|
|
72
|
+
const mark = runMark ?? (node.since === 'start-of-log' ? null : readLaunchMark(recipeRuntimePath(projectRoot, LAUNCH_MARK), unavailable));
|
|
72
73
|
const { text, reset } = readFrom(log, mark ?? { offset: 0 });
|
|
73
74
|
const allowlist = nodeAllowlist(node, env);
|
|
74
75
|
const keep = inWindow({ sinceMs: mark?.launchedAtMs ?? null, now: deps.now ?? Date.now() });
|
|
75
76
|
const { findings, allowlisted } = judgeConsole([text], { levels, allowlist, keep });
|
|
76
77
|
const output = {
|
|
77
78
|
action: input.action,
|
|
78
|
-
since: node.since ?? 'launch',
|
|
79
|
+
since: runMark ? 'run' : node.since ?? 'launch',
|
|
79
80
|
levels: [...levels],
|
|
80
81
|
findings,
|
|
81
82
|
allowlisted,
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// (unallowlisted errors, capture not proven). Each adapter's action proves its
|
|
5
5
|
// own capture and window, then calls these.
|
|
6
6
|
|
|
7
|
-
import { closeSync, fstatSync, openSync, readFileSync } from 'node:fs';
|
|
7
|
+
import { closeSync, fstatSync, openSync, readFileSync, realpathSync } from 'node:fs';
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
|
|
10
10
|
import {
|
|
@@ -17,6 +17,20 @@ import {
|
|
|
17
17
|
withBundledLibraryRoot,
|
|
18
18
|
} from './console-findings.mjs';
|
|
19
19
|
|
|
20
|
+
// Private run-local state passed to action subprocesses, never a slot-wide mark.
|
|
21
|
+
export const RUN_CONSOLE_WINDOW_ENV = 'METAMASK_RECIPE_RUN_CONSOLE_WINDOW';
|
|
22
|
+
|
|
23
|
+
export function runConsoleMark(input, env, file) {
|
|
24
|
+
if (input.node?.since === 'start-of-log' || !env[RUN_CONSOLE_WINDOW_ENV]) return null;
|
|
25
|
+
const window = JSON.parse(env[RUN_CONSOLE_WINDOW_ENV]);
|
|
26
|
+
const captured = window.files?.[realpathSync(file)];
|
|
27
|
+
if (window.projectRoot !== realpathSync(path.resolve(input.context.projectRoot)) ||
|
|
28
|
+
!Number.isFinite(window.startedAtMs) || !captured) {
|
|
29
|
+
throw captureUnavailable('the run console boundary does not match this capture');
|
|
30
|
+
}
|
|
31
|
+
return { ...captured, launchedAtMs: window.startedAtMs };
|
|
32
|
+
}
|
|
33
|
+
|
|
20
34
|
// The capture from a recorded offset. A file now shorter than the offset, or
|
|
21
35
|
// a different file, was replaced since: read it whole and say so, rather than
|
|
22
36
|
// reading nothing.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// Fail the recipe when the app page logged an error or uncaught exception
|
|
2
2
|
// that the recipe libraries' console allowlist does not mark as known
|
|
3
3
|
// third-party noise. Reads the slot console capture (app-console.log, and
|
|
4
|
-
// extension-console.log when asked) from
|
|
5
|
-
// launch
|
|
4
|
+
// extension-console.log when asked) from this recipe run's boundary, or the
|
|
5
|
+
// browser launch for standalone actions. The classifier also feeds diagnostics.
|
|
6
6
|
// It never passes on missing evidence: the slot's own console collector must
|
|
7
7
|
// be running on the browser's CDP port, and for every capture file a
|
|
8
8
|
// requested source reads, a control line logged in its target (the app page,
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import path from 'node:path';
|
|
13
13
|
|
|
14
|
-
import { captureUnavailable, consoleLevels, judgeConsole, nodeAllowlist, readFrom, throwOnFindings } from '../../shared/console-assert.mjs';
|
|
14
|
+
import { captureUnavailable, consoleLevels, inWindow, judgeConsole, nodeAllowlist, readFrom, runConsoleMark, throwOnFindings } from '../../shared/console-assert.mjs';
|
|
15
15
|
import { collectorOwnerMarker, collectorStatus, verifyCapture } from '../../shared/console-collector.mjs';
|
|
16
16
|
import { isDirectRun, readBrowserState, runAdapter, webDappPolicy, webDappRuntimeDir } from '../platform/runtime.mjs';
|
|
17
17
|
|
|
@@ -49,14 +49,22 @@ export async function assertNoConsoleErrors(input, env = process.env, deps = {})
|
|
|
49
49
|
const allowlist = nodeAllowlist(node, env);
|
|
50
50
|
const texts = [];
|
|
51
51
|
const offsetResets = [];
|
|
52
|
+
let runMark = null;
|
|
52
53
|
for (const file of files) {
|
|
53
|
-
const
|
|
54
|
+
const capturePath = path.join(runtime, LOGS[file]);
|
|
55
|
+
const mark = runConsoleMark(input, env, capturePath);
|
|
56
|
+
runMark ??= mark;
|
|
57
|
+
const { text, reset } = readFrom(capturePath, mark ?? { offset: offsets[file] ?? 0, inode: offsets.inodes?.[file] });
|
|
54
58
|
if (reset) offsetResets.push(file);
|
|
55
59
|
texts.push(text);
|
|
56
60
|
}
|
|
57
|
-
const { findings, allowlisted } = judgeConsole(texts, {
|
|
61
|
+
const { findings, allowlisted } = judgeConsole(texts, {
|
|
62
|
+
levels, allowlist, accept: (finding) => sources.includes(finding.source),
|
|
63
|
+
keep: inWindow({ sinceMs: runMark?.launchedAtMs ?? null, now: deps.now ?? Date.now() }),
|
|
64
|
+
});
|
|
58
65
|
const output = {
|
|
59
66
|
action: input.action,
|
|
67
|
+
since: runMark ? 'run' : node.since ?? 'launch',
|
|
60
68
|
sources,
|
|
61
69
|
levels: [...levels],
|
|
62
70
|
findings,
|
|
@@ -1244,7 +1244,7 @@
|
|
|
1244
1244
|
"execution_capabilities": []
|
|
1245
1245
|
},
|
|
1246
1246
|
"metamask.app.assert_no_console_errors": {
|
|
1247
|
-
"description": "Fail when MetaMask (service worker or UI pages) logged an error or uncaught exception that no console allowlist (the bundled library's, then each recipe library's diagnostics/console-allowlist.json) marks as known noise, judging
|
|
1247
|
+
"description": "Fail when MetaMask (service worker or UI pages) logged an error or uncaught exception that no console allowlist (the bundled library's, then each recipe library's diagnostics/console-allowlist.json) marks as known noise, judging events during the current recipe run by their own timestamps, or since launch for a standalone action. Reads extension-console.log from the run boundary, or the offset mm-harness launch records when it starts, and leaves out records the collector stamps with an event time before the launch (a replaced collector's replay of a browser the launch kept); a standalone action with no recorded launch fails unless since is start-of-log. The same classification feeds run diagnostics. Fails as unavailable unless this runtime's console collector runs on the run's CDP port and a control line logged in MetaMask's service worker or page reaches extension-console.log. Allowlist entries are exact signatures (schemaVersion 3).",
|
|
1248
1248
|
"execution_capabilities": ["host-read-export"],
|
|
1249
1249
|
"schema": {
|
|
1250
1250
|
"type": "object",
|
|
@@ -502,7 +502,7 @@
|
|
|
502
502
|
]
|
|
503
503
|
},
|
|
504
504
|
"metamask.app.assert_no_console_errors": {
|
|
505
|
-
"description": "Fail when the app page logged an error or uncaught exception since the slot browser's launch that no recipe-library console allowlist (diagnostics/console-allowlist.json) marks as known third-party noise. Reads the slot console capture; the same classification feeds run diagnostics. Fails as unavailable unless the slot's own console collector runs on the browser's CDP port and, for each capture file a requested source reads, a control line logged in its target (the app page; MetaMask for the extension source) reaches that file. Allowlist entries are exact signatures (schemaVersion 3).",
|
|
505
|
+
"description": "Fail when the app page logged an error or uncaught exception during the current recipe run, or since the slot browser's launch for a standalone action that no recipe-library console allowlist (diagnostics/console-allowlist.json) marks as known third-party noise. Reads the slot console capture; the same classification feeds run diagnostics. Fails as unavailable unless the slot's own console collector runs on the browser's CDP port and, for each capture file a requested source reads, a control line logged in its target (the app page; MetaMask for the extension source) reaches that file. Allowlist entries are exact signatures (schemaVersion 3).",
|
|
506
506
|
"execution_capabilities": [
|
|
507
507
|
"host-read-export"
|
|
508
508
|
],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deeeed/metamask-harness",
|
|
3
|
-
"version": "0.92.
|
|
3
|
+
"version": "0.92.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"mm-harness": "bin/mm-harness"
|
|
@@ -21,15 +21,15 @@
|
|
|
21
21
|
"check:syntax": "find . -name '*.mjs' -print0 | xargs -0 -n1 node --check"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@farmslot/adapter-node": "0.1.
|
|
25
|
-
"@farmslot/adapter-rn": "0.22.
|
|
26
|
-
"@farmslot/adapter-sdk": "0.12.
|
|
27
|
-
"@farmslot/adapter-web": "0.8.
|
|
28
|
-
"@farmslot/agent-runtime": "0.
|
|
24
|
+
"@farmslot/adapter-node": "0.1.18",
|
|
25
|
+
"@farmslot/adapter-rn": "0.22.2",
|
|
26
|
+
"@farmslot/adapter-sdk": "0.12.1",
|
|
27
|
+
"@farmslot/adapter-web": "0.8.3",
|
|
28
|
+
"@farmslot/agent-runtime": "0.21.1",
|
|
29
29
|
"@farmslot/handoff": "^0.3.1",
|
|
30
30
|
"@farmslot/protocol": "0.43.0",
|
|
31
|
-
"@farmslot/recipe-cli": "0.16.
|
|
32
|
-
"@farmslot/recipe-runner": "0.34.
|
|
31
|
+
"@farmslot/recipe-cli": "0.16.3",
|
|
32
|
+
"@farmslot/recipe-runner": "0.34.1",
|
|
33
33
|
"agent-device": "0.19.3",
|
|
34
34
|
"commander": "^12.0.0",
|
|
35
35
|
"esbuild": "0.28.1",
|