@deeeed/metamask-harness 0.50.2 → 0.50.4
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 +12 -0
- package/README.md +4 -0
- package/dist/adapters/extension/ensure-ready.js +1 -1
- package/dist/adapters/extension/runtime-decision.js +17 -0
- package/dist/adapters/extension/runtime.js +38 -14
- package/dist/commands/help.js +10 -8
- package/dist/commands/launch/index.js +10 -0
- package/docs/QA.md +4 -4
- package/docs/RECIPES.md +21 -0
- package/library/actions/mobile/perps/read-visible-state-loop.mjs +1 -1
- package/library/actions/mobile/platform/observe-ui.mjs +17 -0
- package/library/actions/shared/perps/visible-state.mjs +11 -3
- package/package.json +2 -2
- package/site/assets/help-recipes.json +5 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.50.4 - 2026-09-10
|
|
6
|
+
|
|
7
|
+
- Distinguish Extension startup/provider absence from attempted RPC failures and preserve diagnosed failed tabs during verification.
|
|
8
|
+
- Bind Mobile native Perps observations to a stable development navigation route when available, and include route/screen context in surface errors.
|
|
9
|
+
- Add an interactive workflow map and sourced template previews to the human tutorial, connecting tasks, skills, checklists, recipes, evidence, and optional Farmslot coordination.
|
|
10
|
+
|
|
11
|
+
## 0.50.3 - 2026-09-09
|
|
12
|
+
|
|
13
|
+
- Recognize enabled form controls through recipe-harness 0.16.1; report Extension startup crashes without reload-based RPC probes or automatic relaunch recovery.
|
|
14
|
+
- Color human recipe help while keeping JSON plain; clarify recipe composition, Extension timing capture, acceptance criteria, and team-library updates.
|
|
15
|
+
- Make architecture topics expandable and correct cheatsheet platform, recording, and recipe examples.
|
|
16
|
+
|
|
5
17
|
## 0.50.2 - 2026-09-08
|
|
6
18
|
|
|
7
19
|
- Remove Mobile receipt actions that require unpublished app/SDK APIs; reject stale receipt-based assertion and cleanup inputs before execution.
|
package/README.md
CHANGED
|
@@ -158,6 +158,10 @@ mm-harness run --list
|
|
|
158
158
|
Shared libraries hold durable actions and composable recipes. Task acceptance
|
|
159
159
|
criteria remain task-local. See [Recipes](docs/RECIPES.md).
|
|
160
160
|
|
|
161
|
+
Teams maintain their library checkouts. Update them before starting a task;
|
|
162
|
+
the harness records local revisions and recipe digests but does not fetch or
|
|
163
|
+
check for upstream updates. Keep the selected library unchanged during a run.
|
|
164
|
+
|
|
161
165
|
## Recover
|
|
162
166
|
|
|
163
167
|
```bash
|
|
@@ -148,7 +148,7 @@ async function ensureExtensionReady(target, options) {
|
|
|
148
148
|
}
|
|
149
149
|
};
|
|
150
150
|
let health = await checkHealth();
|
|
151
|
-
if (action === "pruned" && (health.status !== "PASS" || after !== 1)) {
|
|
151
|
+
if (action === "pruned" && !health.errorCode && (health.status !== "PASS" || after !== 1)) {
|
|
152
152
|
const listing = await jsonList(cdpPort);
|
|
153
153
|
if (listing.ok) {
|
|
154
154
|
const existingHomeIds = new Set(
|
|
@@ -441,6 +441,16 @@ async function decideExtensionReadiness(target, options = {}) {
|
|
|
441
441
|
reasons: ["Build is fresh and the extension runtime is healthy over CDP."],
|
|
442
442
|
actions: []
|
|
443
443
|
},
|
|
444
|
+
{
|
|
445
|
+
when: cdp.errorCode === "EXTENSION_BACKGROUND_UNRESPONSIVE" || cdp.errorCode === "EXTENSION_STARTUP_UNVERIFIED",
|
|
446
|
+
decision: "blocked",
|
|
447
|
+
reasonCode: "background-unresponsive",
|
|
448
|
+
reasons: [
|
|
449
|
+
"Extension startup failed; inspect the background error before relaunching or resetting wallet state.",
|
|
450
|
+
...cdp.findings?.slice(0, 3) ?? []
|
|
451
|
+
],
|
|
452
|
+
actions: []
|
|
453
|
+
},
|
|
444
454
|
{
|
|
445
455
|
when: cdp.errorCode === "EVM_RPC_EXECUTION_FAILED",
|
|
446
456
|
decision: "blocked",
|
|
@@ -492,6 +502,13 @@ async function decideExtensionReadiness(target, options = {}) {
|
|
|
492
502
|
reasons: [`Release artifact ${releaseArtifact.expectedVersion} is intact and healthy over CDP.`],
|
|
493
503
|
actions: []
|
|
494
504
|
},
|
|
505
|
+
{
|
|
506
|
+
when: cdp.errorCode === "EXTENSION_BACKGROUND_UNRESPONSIVE" || cdp.errorCode === "EXTENSION_STARTUP_UNVERIFIED",
|
|
507
|
+
decision: "blocked",
|
|
508
|
+
reasonCode: "background-unresponsive",
|
|
509
|
+
reasons: ["Extension startup failed; inspect the background error before relaunching or resetting wallet state.", ...cdp.findings?.slice(0, 3) ?? []],
|
|
510
|
+
actions: []
|
|
511
|
+
},
|
|
495
512
|
{
|
|
496
513
|
when: cdp.errorCode === "EVM_RPC_EXECUTION_FAILED",
|
|
497
514
|
decision: "blocked",
|
|
@@ -51,7 +51,7 @@ async function assertHealthyExtensionRuntime(options) {
|
|
|
51
51
|
pageMode: options.pageMode
|
|
52
52
|
});
|
|
53
53
|
if (lastReport.status === "PASS") return lastReport;
|
|
54
|
-
if (lastReport.errorCode === "UI_COMPOSITOR_SUSPENDED") {
|
|
54
|
+
if (lastReport.errorCode === "UI_COMPOSITOR_SUSPENDED" || lastReport.errorCode === "EXTENSION_BACKGROUND_UNRESPONSIVE") {
|
|
55
55
|
throw new Error(formatHealthFailure(lastReport, options.projectRoot));
|
|
56
56
|
}
|
|
57
57
|
await sleep(500);
|
|
@@ -151,26 +151,43 @@ async function checkExtensionRuntimeHealth(projectRoot, cdpPort, options = {}) {
|
|
|
151
151
|
};
|
|
152
152
|
}
|
|
153
153
|
const runtime = await evaluateHealth(session, cdpCallTimeoutMs);
|
|
154
|
+
if (runtime.backgroundUnresponsive === true || runtime.hasPageContent === false) {
|
|
155
|
+
const code = runtime.backgroundUnresponsive === true ? "EXTENSION_BACKGROUND_UNRESPONSIVE" : "EXTENSION_STARTUP_UNVERIFIED";
|
|
156
|
+
return {
|
|
157
|
+
status: "FAIL",
|
|
158
|
+
errorCode: code,
|
|
159
|
+
userAction: `mm-harness logs --target ${shellQuote(projectRoot)} --full # inspect the startup error before restarting or resetting an authorized dev fixture`,
|
|
160
|
+
cdpPort,
|
|
161
|
+
targetUrl: target.url,
|
|
162
|
+
extensionId: safeExtensionId(target),
|
|
163
|
+
extensionPageTargets: extensionTargets.length,
|
|
164
|
+
findings: [`${code}: ${runtime.backgroundUnresponsive === true ? "Extension UI reports a startup failure or unresponsive background" : "Extension page has not rendered its UI"}; RPC readiness cannot be established.`],
|
|
165
|
+
details: {
|
|
166
|
+
projectRoot,
|
|
167
|
+
targetUrls: targets.map((entry) => entry.url).filter(Boolean),
|
|
168
|
+
runtime
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
}
|
|
154
172
|
if (runtime.hasSubmitRequest !== true) {
|
|
155
173
|
const providerProbe = await probeUiEthereumProvider(session, cdpCallTimeoutMs);
|
|
156
174
|
runtime.evmRpcProbeOk = providerProbe.ok;
|
|
157
175
|
runtime.evmRpcProbeError = providerProbe.error;
|
|
158
176
|
runtime.evmRpcProbeSource = providerProbe.source;
|
|
177
|
+
runtime.evmRpcProbeAttempted = providerProbe.requestAttempted;
|
|
159
178
|
}
|
|
160
179
|
if (runtime.href && !String(runtime.href).startsWith("chrome-extension://")) {
|
|
161
180
|
findings.push(`Extension page href is not an extension URL: ${runtime.href}`);
|
|
162
181
|
}
|
|
163
|
-
if (runtime.backgroundUnresponsive === true) {
|
|
164
|
-
findings.push("Extension UI reports background connection unresponsive.");
|
|
165
|
-
}
|
|
166
182
|
if (runtime.ethereumConnectionUnavailable === true) {
|
|
167
183
|
findings.push('Extension UI reports "Unable to connect to Ethereum".');
|
|
168
184
|
}
|
|
169
185
|
const rpcExecutionFailed = runtime.evmRpcProbeOk !== true && /Failed to execute 'fetch' on 'WorkerGlobalScope': Illegal invocation/u.test(String(runtime.evmRpcProbeError));
|
|
186
|
+
const startupUnverified = runtime.evmRpcProbeOk !== true && runtime.evmRpcProbeAttempted === false;
|
|
170
187
|
if (runtime.evmRpcProbeOk !== true) {
|
|
171
188
|
const probeError = String(runtime.evmRpcProbeError ?? "unknown error").replace(/[.\s]+$/u, "");
|
|
172
189
|
findings.push(
|
|
173
|
-
rpcExecutionFailed ? `EVM_RPC_EXECUTION_FAILED: background RPC fetch threw before an HTTP response: ${probeError}.` : `EVM RPC readiness probe failed: ${probeError}. Infura configured \u2260 RPC reachable.`
|
|
190
|
+
startupUnverified ? `EXTENSION_STARTUP_UNVERIFIED: no completed provider probe established RPC readiness: ${probeError}.` : rpcExecutionFailed ? `EVM_RPC_EXECUTION_FAILED: background RPC fetch threw before an HTTP response: ${probeError}.` : `EVM RPC readiness probe failed: ${probeError}. Infura configured \u2260 RPC reachable.`
|
|
174
191
|
);
|
|
175
192
|
}
|
|
176
193
|
if (runtime.hasSubmitRequest === true && runtime.backgroundProbeOk !== true) {
|
|
@@ -181,8 +198,8 @@ async function checkExtensionRuntimeHealth(projectRoot, cdpPort, options = {}) {
|
|
|
181
198
|
return {
|
|
182
199
|
status: findings.length === 0 ? "PASS" : "FAIL",
|
|
183
200
|
...evmRpcUnreachable ? {
|
|
184
|
-
errorCode: rpcExecutionFailed ? "EVM_RPC_EXECUTION_FAILED" : "EVM_RPC_UNREACHABLE",
|
|
185
|
-
userAction: rpcExecutionFailed ? `mm-harness logs --target ${shellQuote(projectRoot)} --full # inspect the background fetch stack; credential resync is not indicated by this error` : evmRpcRecoveryAction(projectRoot)
|
|
201
|
+
errorCode: startupUnverified ? "EXTENSION_STARTUP_UNVERIFIED" : rpcExecutionFailed ? "EVM_RPC_EXECUTION_FAILED" : "EVM_RPC_UNREACHABLE",
|
|
202
|
+
userAction: startupUnverified ? `mm-harness logs --target ${shellQuote(projectRoot)} --full # inspect Extension startup; upstream RPC failure has not been established` : rpcExecutionFailed ? `mm-harness logs --target ${shellQuote(projectRoot)} --full # inspect the background fetch stack; credential resync is not indicated by this error` : evmRpcRecoveryAction(projectRoot)
|
|
186
203
|
} : {},
|
|
187
204
|
warnings,
|
|
188
205
|
cdpPort,
|
|
@@ -318,6 +335,7 @@ async function evaluateHealth(session, timeoutMs) {
|
|
|
318
335
|
return {
|
|
319
336
|
href: location.href,
|
|
320
337
|
title: document.title,
|
|
338
|
+
hasPageContent: bodyText.trim().length > 0,
|
|
321
339
|
hookKeys: Object.keys(hooks),
|
|
322
340
|
hasSubmitRequest: typeof hooks.submitRequestToBackground === 'function',
|
|
323
341
|
hasStore: Boolean(hooks.store),
|
|
@@ -339,6 +357,7 @@ async function evaluateHealth(session, timeoutMs) {
|
|
|
339
357
|
new Promise((resolve) => setTimeout(() => resolve({
|
|
340
358
|
href: location.href,
|
|
341
359
|
title: document.title,
|
|
360
|
+
hasPageContent: bodyText.trim().length > 0,
|
|
342
361
|
hookKeys: Object.keys(hooks),
|
|
343
362
|
hasSubmitRequest: typeof hooks.submitRequestToBackground === 'function',
|
|
344
363
|
hasStore: Boolean(hooks.store),
|
|
@@ -381,7 +400,7 @@ async function probeUiEthereumProvider(session, timeoutMs) {
|
|
|
381
400
|
value: {
|
|
382
401
|
async probe() {
|
|
383
402
|
if (!provider || typeof provider.request !== 'function') {
|
|
384
|
-
return { ok: false, error: 'Ethereum provider is unavailable' };
|
|
403
|
+
return { ok: false, error: 'Ethereum provider is unavailable', requestAttempted: false };
|
|
385
404
|
}
|
|
386
405
|
try {
|
|
387
406
|
const code = await provider.request({
|
|
@@ -391,10 +410,11 @@ async function probeUiEthereumProvider(session, timeoutMs) {
|
|
|
391
410
|
const ok = typeof code === 'string' && /^0x[0-9a-f]*$/iu.test(code);
|
|
392
411
|
return {
|
|
393
412
|
ok,
|
|
413
|
+
requestAttempted: true,
|
|
394
414
|
error: ok ? null : 'Ethereum provider returned invalid contract code',
|
|
395
415
|
};
|
|
396
416
|
} catch (error) {
|
|
397
|
-
return { ok: false, error: String(error?.message || error) };
|
|
417
|
+
return { ok: false, error: String(error?.message || error), requestAttempted: true };
|
|
398
418
|
}
|
|
399
419
|
},
|
|
400
420
|
cleanup() {
|
|
@@ -418,13 +438,15 @@ async function probeUiEthereumProvider(session, timeoutMs) {
|
|
|
418
438
|
return {
|
|
419
439
|
ok: false,
|
|
420
440
|
error: "Ethereum provider preload was not installed",
|
|
421
|
-
source: "ui-ethereum-provider"
|
|
441
|
+
source: "ui-ethereum-provider",
|
|
442
|
+
requestAttempted: false
|
|
422
443
|
};
|
|
423
444
|
}
|
|
424
445
|
let outcome = {
|
|
425
446
|
ok: false,
|
|
426
447
|
error: "Ethereum provider is unavailable",
|
|
427
|
-
source: "ui-ethereum-provider"
|
|
448
|
+
source: "ui-ethereum-provider",
|
|
449
|
+
requestAttempted: false
|
|
428
450
|
};
|
|
429
451
|
const cleanupErrors = [];
|
|
430
452
|
try {
|
|
@@ -439,10 +461,11 @@ async function probeUiEthereumProvider(session, timeoutMs) {
|
|
|
439
461
|
}, Math.max(1, deadline - Date.now()));
|
|
440
462
|
const value = result.result?.value;
|
|
441
463
|
if (value?.ok === true) {
|
|
442
|
-
outcome = { ok: true, error: null, source: "ui-ethereum-provider" };
|
|
464
|
+
outcome = { ok: true, error: null, source: "ui-ethereum-provider", requestAttempted: true };
|
|
443
465
|
break;
|
|
444
466
|
}
|
|
445
467
|
if (typeof value?.error === "string") outcome.error = value.error;
|
|
468
|
+
outcome.requestAttempted ||= value?.requestAttempted === true;
|
|
446
469
|
} catch (error) {
|
|
447
470
|
outcome.error = messageOf(error);
|
|
448
471
|
}
|
|
@@ -450,7 +473,7 @@ async function probeUiEthereumProvider(session, timeoutMs) {
|
|
|
450
473
|
await sleep(100);
|
|
451
474
|
}
|
|
452
475
|
} catch (error) {
|
|
453
|
-
outcome = { ok: false, error: messageOf(error)
|
|
476
|
+
outcome = { ...outcome, ok: false, error: messageOf(error) };
|
|
454
477
|
} finally {
|
|
455
478
|
try {
|
|
456
479
|
const cleanup = await boundedCdpCall(session, "Runtime.evaluate", {
|
|
@@ -480,7 +503,8 @@ async function probeUiEthereumProvider(session, timeoutMs) {
|
|
|
480
503
|
return cleanupErrors.length > 0 ? {
|
|
481
504
|
ok: false,
|
|
482
505
|
error: `Ethereum provider probe cleanup failed: ${cleanupErrors.join("; ")}`,
|
|
483
|
-
source: "ui-ethereum-provider"
|
|
506
|
+
source: "ui-ethereum-provider",
|
|
507
|
+
requestAttempted: outcome.requestAttempted
|
|
484
508
|
} : outcome;
|
|
485
509
|
}
|
|
486
510
|
function extensionBackgroundProbeTimeoutMs(cdpCallTimeoutMs) {
|
package/dist/commands/help.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { color } from "../cli-color.js";
|
|
3
4
|
import { detectAdapter } from "../harness.js";
|
|
4
5
|
import { optionString, parseArgs, targetPath } from "./parse-args.js";
|
|
5
6
|
const UNSCOPED_NEXT = "cd into a MetaMask checkout or pass --adapter <mobile|extension|core>";
|
|
@@ -48,21 +49,22 @@ function recipeHelpPayload(topic, harnessVersion, adapter, target) {
|
|
|
48
49
|
};
|
|
49
50
|
}
|
|
50
51
|
function renderRecipeHelp(topic, harnessVersion, adapter) {
|
|
52
|
+
const paint = (style, text) => color(style, text, { stream: process.stdout });
|
|
51
53
|
const lines = [
|
|
52
|
-
`mm-harness ${harnessVersion} recipe guide
|
|
53
|
-
`Context: Recipe Protocol v${topic.recipeProtocolVersion}, adapter ${adapter}
|
|
54
|
+
paint("bold", `mm-harness ${harnessVersion} recipe guide`),
|
|
55
|
+
paint("dim", `Context: Recipe Protocol v${topic.recipeProtocolVersion}, adapter ${adapter}`),
|
|
54
56
|
topic.summary,
|
|
55
57
|
topic.capabilities,
|
|
56
|
-
topic.scopeNotice,
|
|
57
|
-
|
|
58
|
+
paint("warn", topic.scopeNotice),
|
|
59
|
+
`Visual tutorial: ${paint("cmd", "mm-harness tutorial")}`
|
|
58
60
|
];
|
|
59
|
-
if (adapter === "unscoped") lines.push(`No MetaMask checkout detected. Next: ${UNSCOPED_NEXT}.`);
|
|
61
|
+
if (adapter === "unscoped") lines.push(paint("warn", `No MetaMask checkout detected. Next: ${UNSCOPED_NEXT}.`));
|
|
60
62
|
for (const [index, section] of scopedSections(topic, adapter).entries()) {
|
|
61
|
-
lines.push("", `${index + 1}. ${section.title}
|
|
63
|
+
lines.push("", paint("label", `${index + 1}. ${section.title}`), ` ${section.instruction}`);
|
|
62
64
|
for (const detail of section.details) lines.push(` - ${detail}`);
|
|
63
|
-
for (const command of section.commands) lines.push(` $ ${command}`);
|
|
65
|
+
for (const command of section.commands) lines.push(` $ ${paint("cmd", command)}`);
|
|
64
66
|
}
|
|
65
|
-
lines.push("",
|
|
67
|
+
lines.push("", `${paint("warn", "Safety:")} ${topic.safety}`);
|
|
66
68
|
return `${lines.join("\n")}
|
|
67
69
|
`;
|
|
68
70
|
}
|
|
@@ -310,6 +310,16 @@ async function handleLaunchLocked(argv, stream) {
|
|
|
310
310
|
exitCode: EXIT.infra
|
|
311
311
|
});
|
|
312
312
|
}
|
|
313
|
+
if (adapter === "extension" && (attempt.output.includes("EXTENSION_BACKGROUND_UNRESPONSIVE:") || attempt.output.includes("EXTENSION_STARTUP_UNVERIFIED:"))) {
|
|
314
|
+
return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
|
|
315
|
+
code: attempt.output.includes("EXTENSION_BACKGROUND_UNRESPONSIVE:") ? "EXTENSION_BACKGROUND_UNRESPONSIVE" : "EXTENSION_STARTUP_UNVERIFIED",
|
|
316
|
+
message: "Extension startup is not healthy; inspect the background error before restarting or resetting wallet state.",
|
|
317
|
+
recoverable: false,
|
|
318
|
+
userAction: `mm-harness logs --target ${shellQuote(target)} --full`,
|
|
319
|
+
exitCode: EXIT.runtime,
|
|
320
|
+
originalError: attempt.output.trim() || void 0
|
|
321
|
+
});
|
|
322
|
+
}
|
|
313
323
|
if (adapter === "extension" && attempt.output.includes("EVM_RPC_EXECUTION_FAILED:")) {
|
|
314
324
|
return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
|
|
315
325
|
code: "EVM_RPC_EXECUTION_FAILED",
|
package/docs/QA.md
CHANGED
|
@@ -56,8 +56,8 @@ mm-harness verify
|
|
|
56
56
|
- [ ] Bad input fails before runtime work with a stable code and one exact next
|
|
57
57
|
action.
|
|
58
58
|
- [ ] `doctor --fix` is idempotent and does not choose a fixture or launch.
|
|
59
|
-
- [ ] With `capture-helper` absent, Extension reports CDP screenshots and
|
|
60
|
-
reports
|
|
59
|
+
- [ ] With `capture-helper` absent, Extension reports CDP screenshots and no
|
|
60
|
+
Extension video; Mobile reports its independent simctl/ADB capture readiness.
|
|
61
61
|
|
|
62
62
|
Run the bounded multi-platform preflight when all three checkouts are available:
|
|
63
63
|
|
|
@@ -177,8 +177,8 @@ Before release:
|
|
|
177
177
|
Core scenarios.
|
|
178
178
|
- [ ] Independent review approves the exact final diff.
|
|
179
179
|
- [ ] Known limits are explicit: Extension requires product Infura setup;
|
|
180
|
-
Mobile requires its normal dev-client/device setup; video requires
|
|
181
|
-
`capture-helper
|
|
180
|
+
Mobile requires its normal dev-client/device setup; Extension video requires
|
|
181
|
+
`capture-helper`, while Mobile uses platform recorders; the harness never invents funded fixtures.
|
|
182
182
|
- [ ] Existing [Farmslot](https://farmslot.io) slots remain compatible.
|
|
183
183
|
|
|
184
184
|
When a slot manager is available, repeat `runner.smoke` in one existing managed
|
package/docs/RECIPES.md
CHANGED
|
@@ -96,6 +96,23 @@ detached-DOM check, and idle-control comparison. A one-screen slope does not
|
|
|
96
96
|
prove those broader claims. Check the suite's reference markers against the
|
|
97
97
|
tested build before running it.
|
|
98
98
|
|
|
99
|
+
### Extension timing observation
|
|
100
|
+
|
|
101
|
+
First inspect `app.performance_capture` and `app.network_capture`. When the
|
|
102
|
+
`recipe-performance` skill is installed, its
|
|
103
|
+
`scripts/capture-extension-window.mjs --help` describes capture around a single
|
|
104
|
+
command. Run that wrapper outside the recipe; a command node cannot start a
|
|
105
|
+
second `mm-harness run/call` while its parent owns the checkout sandbox.
|
|
106
|
+
|
|
107
|
+
For a custom observer blocked by LavaMoat, register a pre-boot script with
|
|
108
|
+
`Page.addScriptToEvaluateOnNewDocument` to retain only the required native API
|
|
109
|
+
reference, such as a bound `requestAnimationFrame`. Initialize it during explicit
|
|
110
|
+
setup, remove the preload after use, and clean up the observer afterward. Do not
|
|
111
|
+
disable scuttling, pause the debugger, or reload inside the measured window.
|
|
112
|
+
Record lifecycle identity and use the application's clock for UI timing; CLI
|
|
113
|
+
duration is a different measurement. Keep endpoint and readiness predicates in
|
|
114
|
+
the team recipe, not a new harness action.
|
|
115
|
+
|
|
99
116
|
### Extension Perps release recipes
|
|
100
117
|
|
|
101
118
|
Discover these identifiers with `run --list --adapter extension --json` and
|
|
@@ -189,6 +206,10 @@ export RECIPE_LIBRARY_PATH="perps=/path/to/experimental-metamask-recipe-perps"
|
|
|
189
206
|
|
|
190
207
|
`perps.pro-order-setup` preserves existing orders, positions, and strategies by
|
|
191
208
|
default. Its Scale, Chase, and TWAP placement callers inherit that policy.
|
|
209
|
+
All three placement helpers require explicit `account` and `notional` inputs.
|
|
210
|
+
Scale also requires `start_price` and `end_price`; Chase requires `max_distance`.
|
|
211
|
+
Choose values for the current market and approved budget. Preserve JSON quotes
|
|
212
|
+
for string parameters, for example `'notional="15"'`; inspect `--describe` first.
|
|
192
213
|
When a proof requires a clean testnet market, reserve the account/market and
|
|
193
214
|
compose explicit setup with `trading_state=clean_selected_testnet_market`.
|
|
194
215
|
This parameter never authorizes cleanup of another task's trades.
|
|
@@ -43,7 +43,7 @@ export async function readVisiblePerpsState(
|
|
|
43
43
|
action: input.action,
|
|
44
44
|
...normalizeVisiblePerpsState({
|
|
45
45
|
route: screen.route,
|
|
46
|
-
title: screen.
|
|
46
|
+
title: screen.name,
|
|
47
47
|
items: visible.items,
|
|
48
48
|
offscreenItems: visible.hidden_or_offscreen,
|
|
49
49
|
truncated: visible.truncated,
|
|
@@ -27,7 +27,24 @@ export async function observeNativeUi(payload, context) {
|
|
|
27
27
|
if (supported.length === 0) return warnings.length ? { warnings } : {};
|
|
28
28
|
|
|
29
29
|
try {
|
|
30
|
+
const env = { ...process.env, ...record(context?.env) };
|
|
31
|
+
const routeInput = { action: 'ui.observe', node: { ...record(payload?.node), bridge_timeout_ms: 2_000, cdp_timeout_ms: 2_000 }, context };
|
|
32
|
+
let routeBefore;
|
|
33
|
+
if (supported.includes('ui.screen') && !isOpaqueRuntime(env) && (env.METRO_PORT || env.WATCHER_PORT)) {
|
|
34
|
+
try {
|
|
35
|
+
routeBefore = await bridgeCommand(routeInput, ['get-route']);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
warnings.push({ ref: 'ui.screen', message: `Navigation route unavailable: ${error.message}` });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
30
40
|
const hierarchy = await readNativeHierarchy(payload, context);
|
|
41
|
+
if (routeBefore?.name) {
|
|
42
|
+
const routeAfter = await bridgeCommand(routeInput, ['get-route']);
|
|
43
|
+
if (routeAfter?.name !== routeBefore.name || routeAfter?.key !== routeBefore.key) {
|
|
44
|
+
throw new Error('Navigation changed during native UI observation; the snapshot cannot be bound to one route.');
|
|
45
|
+
}
|
|
46
|
+
hierarchy.screen.route = routeAfter.name;
|
|
47
|
+
}
|
|
31
48
|
const observations = {};
|
|
32
49
|
for (const ref of supported) {
|
|
33
50
|
observations[ref] = ref === 'ui.screen' ? hierarchy.screen : hierarchy.visible;
|
|
@@ -144,7 +144,15 @@ function marketFromTestId(testId) {
|
|
|
144
144
|
return undefined;
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
-
function classifySurface(route, testIds) {
|
|
147
|
+
function classifySurface(route, testIds, platform) {
|
|
148
|
+
if (platform === 'mobile') {
|
|
149
|
+
const routes = {
|
|
150
|
+
PerpsMarketListView: 'home',
|
|
151
|
+
PerpsTrendingView: 'market-list',
|
|
152
|
+
PerpsMarketDetails: 'market-details',
|
|
153
|
+
};
|
|
154
|
+
if (Object.hasOwn(routes, route)) return routes[route];
|
|
155
|
+
}
|
|
148
156
|
const joined = [...testIds].join('\n');
|
|
149
157
|
if (/confirm-transaction\/.*[?&]goBackTo=%2Fperps-home(?:&|$)/u.test(route)) {
|
|
150
158
|
return 'funds';
|
|
@@ -213,7 +221,7 @@ export function normalizeVisiblePerpsState(raw, options, platform) {
|
|
|
213
221
|
? /^remove from /iu.test(favoriteLabel) : null;
|
|
214
222
|
const testIds = new Set(unique.map((item) => item.testId));
|
|
215
223
|
const route = text(raw.route) ?? '';
|
|
216
|
-
const surface = classifySurface(route, testIds);
|
|
224
|
+
const surface = classifySurface(route, testIds, platform);
|
|
217
225
|
const marketItems = selected(
|
|
218
226
|
unique,
|
|
219
227
|
(item) => Boolean(marketFromTestId(item.testId)),
|
|
@@ -316,7 +324,7 @@ function assertVisibleState(state, options) {
|
|
|
316
324
|
}
|
|
317
325
|
if (options.surface !== 'auto' && state.surface !== options.surface) {
|
|
318
326
|
throw new Error(
|
|
319
|
-
`Expected visible Perps surface ${options.surface}, but observed ${state.surface}.`,
|
|
327
|
+
`Expected visible Perps surface ${options.surface}, but observed ${state.surface} (route: ${state.route ?? 'unavailable'}; title: ${state.title ?? 'unavailable'}).`,
|
|
320
328
|
);
|
|
321
329
|
}
|
|
322
330
|
if (state.markets.length < options.minimumMarketCount) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deeeed/metamask-harness",
|
|
3
|
-
"version": "0.50.
|
|
3
|
+
"version": "0.50.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"mm-harness": "bin/mm-harness"
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"@farmslot/expo-recipe": "0.9.0",
|
|
25
25
|
"@farmslot/handoff": "^0.3.1",
|
|
26
26
|
"@farmslot/protocol": "0.21.0",
|
|
27
|
-
"@farmslot/recipe-harness": "0.16.
|
|
27
|
+
"@farmslot/recipe-harness": "0.16.1",
|
|
28
28
|
"agent-device": "0.19.3",
|
|
29
29
|
"commander": "^12.0.0",
|
|
30
30
|
"es-module-lexer": "2.3.1",
|
|
@@ -94,6 +94,7 @@
|
|
|
94
94
|
"instruction": "Use static call references for unchanged sub-journeys. Separate preparation, action, assertions, and evidence; add workflow.teardown when cleanup must run after success or failure.",
|
|
95
95
|
"details": [
|
|
96
96
|
"Starting conditions are ordinary nodes at the beginning of workflow, not a preconditions field. Use idempotent ensure actions and independently assert their postconditions.",
|
|
97
|
+
"Do not invoke mm-harness run/call from a command node on the same checkout: the outer run owns its sandbox. Compose actions or call child recipes instead. External measurement wrappers may invoke one root run, with preparation outside it; never bypass the lock.",
|
|
97
98
|
"Inspect existing state before creating or cleaning fixtures. Establish only prerequisites required by the claim; a history or display check may already have usable records and need no trade or cleanup. Record the selected records and verify they still satisfy the proof boundary.",
|
|
98
99
|
"Match history-query ranges to the dates of existing visible records before declaring fixtures missing. Sample only sections needed for change detection; fetch one-time corroborating history once, and count background calls as well as graph nodes.",
|
|
99
100
|
"Actions translate UI, CDP, or controller operations. Recipes compose them. Product code owns business rules. Teams own specialized journeys in their libraries; the harness retains shared capabilities and a small set of composable examples.",
|
|
@@ -188,6 +189,10 @@
|
|
|
188
189
|
]
|
|
189
190
|
},
|
|
190
191
|
"Performance: define a measured window, exclude setup, and compare equivalent builds, devices, and lifecycle conditions; action duration alone is not app latency.",
|
|
192
|
+
{
|
|
193
|
+
"text": "For Extension timing, inspect app.performance_capture/app.network_capture and the installed recipe-performance capture-extension-window.mjs helper before writing a collector. If LavaMoat blocks an observation API, capture its reference before initialization via Page.addScriptToEvaluateOnNewDocument during explicit setup, not inside the measured window. Keep domain predicates in the team recipe; see docs/RECIPES.md#extension-timing-observation.",
|
|
194
|
+
"adapters": ["extension"]
|
|
195
|
+
},
|
|
191
196
|
"Retain samples and declared limits when a measurement fails. For memory growth, compare an equal-duration idle control before attributing it to navigation; do not raise the budget to make the run pass.",
|
|
192
197
|
"Analytics: verify the collection destination and existing consent before capture. Changing participation or marketing consent requires operator approval and restoration of the prior settings; a recipe's hardcoded opt-in is not permission. Missing collection prerequisites are not missing product events.",
|
|
193
198
|
"Flake risk: wait on observable state instead of sleeping, keep device and runtime identity explicit, and never overwrite a prior run's artifacts.",
|