@deeeed/metamask-harness 0.13.0 → 0.14.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 +17 -4
- package/adapters/core/cleanup.sh +0 -0
- package/adapters/core/inject.sh +0 -0
- package/adapters/extension/cleanup.mjs +0 -0
- package/adapters/extension/ensure-browser.sh +0 -0
- package/adapters/extension/inject.mjs +0 -0
- package/adapters/extension/launch-browser.cjs +0 -0
- package/adapters/extension/launch.sh +0 -0
- package/adapters/extension/live.sh +0 -0
- package/adapters/extension/readiness.mjs +0 -0
- package/adapters/extension/reattach.sh +0 -0
- package/adapters/extension/refresh-build.sh +0 -0
- package/adapters/extension/seed-fixture.sh +0 -0
- package/adapters/extension/sidepanel-toggle.sh +0 -0
- package/adapters/extension/snapshot-dist.sh +0 -0
- package/adapters/extension/start-watch.sh +0 -0
- package/adapters/extension/verify.sh +4 -4
- package/adapters/extension/wallet-fixture-state.cjs +0 -0
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +0 -0
- package/adapters/mobile/bridge-runtime/setup-wallet.sh +0 -0
- package/adapters/mobile/cleanup.sh +0 -0
- package/adapters/mobile/inject.sh +0 -0
- package/adapters/mobile/lib/metro-listener.sh +0 -0
- package/adapters/mobile/lib/tmux-viewer.sh +0 -0
- package/adapters/mobile/open-device.sh +0 -0
- package/adapters/mobile/prewarm-bundle.sh +0 -0
- package/adapters/mobile/start-metro.sh +15 -1
- package/adapters/mobile/stop-metro.sh +0 -0
- package/adapters/mobile/verify.sh +50 -13
- package/adapters/mobile/wait-for-bridge.sh +0 -0
- package/adapters/mobile/yarn-setup.sh +0 -0
- package/adapters/shared/activate-repo-node.sh +0 -0
- package/adapters/shared/activate-repo-ruby.sh +0 -0
- package/adapters/shared/cli-ux.sh +0 -0
- package/adapters/shared/ensure-runner-deps.sh +0 -0
- package/adapters/shared/harness-path.sh +0 -0
- package/adapters/shared/hash-helpers.sh +0 -0
- package/adapters/shared/json-field.sh +0 -0
- package/adapters/shared/log-tui.mjs +7 -3
- package/adapters/shared/open-log-window.sh +0 -0
- package/adapters/shared/reap-checkout-metros.sh +0 -0
- package/adapters/shared/resolve-farmslot-ports.mjs +0 -0
- package/adapters/shared/resolve-farmslot-ports.sh +0 -0
- package/adapters/shared/resolve-slot-ports.mjs +0 -0
- package/adapters/shared/resolve-slot-ports.sh +0 -0
- package/adapters/shared/sync-wallet-fixture.sh +0 -0
- package/adapters/shared/tmux-session.sh +0 -0
- package/dist/cli-commands.js +5 -4
- package/dist/commands/call.js +61 -5
- package/dist/commands/device-target.js +10 -3
- package/dist/commands/fixtures.js +2 -1
- package/dist/commands/run-engine.js +62 -3
- package/dist/commands/run.js +2 -0
- package/dist/commands/self-test.js +1 -1
- package/dist/commands/status.js +19 -4
- package/dist/mm-harness-cli.js +7 -5
- package/dist/recipe-files.js +14 -0
- package/docs/architecture.md +2 -2
- package/docs/live-adapter-contract.md +1 -1
- package/docs/recipe-libraries.md +21 -18
- package/library/recipes/{app-lifecycle-android-smoke.mobile.recipe.json → app/lifecycle.android-smoke.mobile.recipe.json} +1 -1
- package/library/recipes/{perps-performance.mobile.recipe.json → perps/performance.mobile.recipe.json} +1 -1
- package/library/recipes/perps/smoke.core.recipe.json +39 -0
- package/library/recipes/perps/smoke.extension.recipe.json +51 -0
- package/library/recipes/perps/smoke.mobile.recipe.json +51 -0
- package/library/recipes/{action-validation.extension.recipe.json → runner/action-validation.extension.recipe.json} +7 -7
- package/library/recipes/{action-validation.mobile.recipe.json → runner/action-validation.mobile.recipe.json} +7 -7
- package/package.json +2 -2
- package/scripts/completions.sh +0 -0
- package/scripts/install-completions.sh +0 -0
- /package/library/recipes/{perps-lifecycle.recipe.json → perps/lifecycle.recipe.json} +0 -0
- /package/library/recipes/{order-lifecycle.core.recipe.json → perps/order-lifecycle.core.recipe.json} +0 -0
- /package/library/recipes/{perps-performance-background-resume.mobile.recipe.json → perps/performance.background-resume.mobile.recipe.json} +0 -0
- /package/library/recipes/{perps-performance-cold-start.mobile.recipe.json → perps/performance.cold-start.mobile.recipe.json} +0 -0
- /package/library/recipes/{perps-performance-warm-start.mobile.recipe.json → perps/performance.warm-start.mobile.recipe.json} +0 -0
- /package/library/recipes/{read-markets.core.recipe.json → perps/read-markets.core.recipe.json} +0 -0
- /package/library/recipes/{trading-lifecycle.core.recipe.json → perps/trading-lifecycle.core.recipe.json} +0 -0
- /package/library/recipes/{smoke.extension.recipe.json → runner/smoke.extension.recipe.json} +0 -0
- /package/library/recipes/{smoke.mobile.recipe.json → runner/smoke.mobile.recipe.json} +0 -0
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
runnerDir
|
|
19
19
|
} from "../paths.js";
|
|
20
20
|
import { captureHelperSupportsRecordSessionSnapshots } from "../recording-target.js";
|
|
21
|
+
import { listRecipeFiles } from "../recipe-files.js";
|
|
21
22
|
import { startRecipeRecording, stopRecipeRecording } from "../run-recording.js";
|
|
22
23
|
import { EXIT } from "./shared.js";
|
|
23
24
|
import {
|
|
@@ -152,7 +153,7 @@ function resolveRunRecipeArg(recipeArg, adapter, librarySources) {
|
|
|
152
153
|
const direct = path.resolve(recipeArg);
|
|
153
154
|
if (isRecipeFile(direct)) return { recipeFile: direct };
|
|
154
155
|
if (!recipeArg.includes("/") && !recipeArg.includes(path.sep)) {
|
|
155
|
-
const candidates =
|
|
156
|
+
const candidates = recipeNameCandidates(recipeArg, adapter);
|
|
156
157
|
if (librarySources && librarySources.length > 0) {
|
|
157
158
|
for (const source of librarySources) {
|
|
158
159
|
for (const candidate of candidates) {
|
|
@@ -176,17 +177,75 @@ function resolveRunRecipeArg(recipeArg, adapter, librarySources) {
|
|
|
176
177
|
function libraryRecipeNames(adapter) {
|
|
177
178
|
let entries;
|
|
178
179
|
try {
|
|
179
|
-
entries =
|
|
180
|
+
entries = listRecipeFiles(recipePath(""));
|
|
180
181
|
} catch {
|
|
181
182
|
return [];
|
|
182
183
|
}
|
|
183
184
|
const scopes = ["mobile", "extension", "core"];
|
|
184
|
-
const names = entries.filter((f) => f.endsWith(".recipe.json")).map((f) => f.slice(0, -".recipe.json".length)).filter((n) => {
|
|
185
|
+
const names = entries.filter((f) => f.endsWith(".recipe.json")).map((f) => f.slice(0, -".recipe.json".length).replace(/[\\/]/gu, ".")).filter((n) => {
|
|
185
186
|
const scope = n.split(".").pop() ?? "";
|
|
186
187
|
return !scopes.includes(scope) || scope === adapter;
|
|
187
188
|
}).map((n) => n.endsWith(`.${adapter}`) ? n.slice(0, -(adapter.length + 1)) : n);
|
|
188
189
|
return [...new Set(names)].sort();
|
|
189
190
|
}
|
|
191
|
+
function recipeNameCandidates(recipeArg, adapter) {
|
|
192
|
+
const names = [recipeArg, ...LEGACY_RECIPE_ALIASES[recipeArg] ?? []];
|
|
193
|
+
const candidates = [];
|
|
194
|
+
for (const name of names) {
|
|
195
|
+
candidates.push(...singleRecipeNameCandidates(name, adapter));
|
|
196
|
+
}
|
|
197
|
+
return [...new Set(candidates)];
|
|
198
|
+
}
|
|
199
|
+
function singleRecipeNameCandidates(name, adapter) {
|
|
200
|
+
if (name.endsWith(".recipe.json")) {
|
|
201
|
+
return uniqueStrings([domainPathForRecipeFile(name), name]);
|
|
202
|
+
}
|
|
203
|
+
const bases = uniqueStrings([domainPathForRecipeName(name), name]);
|
|
204
|
+
const candidates = [];
|
|
205
|
+
for (const base of bases) {
|
|
206
|
+
candidates.push(`${base}.${adapter}.recipe.json`, `${base}.recipe.json`, base);
|
|
207
|
+
}
|
|
208
|
+
return candidates;
|
|
209
|
+
}
|
|
210
|
+
function domainPathForRecipeFile(name) {
|
|
211
|
+
const suffix = ".recipe.json";
|
|
212
|
+
const base = name.slice(0, -suffix.length);
|
|
213
|
+
return `${domainPathForRecipeName(base)}${suffix}`;
|
|
214
|
+
}
|
|
215
|
+
function domainPathForRecipeName(name) {
|
|
216
|
+
if (name.includes("/") || name.includes(path.sep)) return name;
|
|
217
|
+
const dot = name.indexOf(".");
|
|
218
|
+
return dot > 0 ? `${name.slice(0, dot)}/${name.slice(dot + 1)}` : name;
|
|
219
|
+
}
|
|
220
|
+
function uniqueStrings(values) {
|
|
221
|
+
return [...new Set(values)];
|
|
222
|
+
}
|
|
223
|
+
const LEGACY_RECIPE_ALIASES = {
|
|
224
|
+
"action-validation": ["runner.action-validation"],
|
|
225
|
+
"action-validation.extension.recipe.json": ["runner.action-validation.extension.recipe.json"],
|
|
226
|
+
"action-validation.mobile.recipe.json": ["runner.action-validation.mobile.recipe.json"],
|
|
227
|
+
"app-lifecycle-android-smoke": ["app.lifecycle.android-smoke"],
|
|
228
|
+
"app-lifecycle-android-smoke.mobile.recipe.json": ["app.lifecycle.android-smoke.mobile.recipe.json"],
|
|
229
|
+
"order-lifecycle": ["perps.order-lifecycle"],
|
|
230
|
+
"order-lifecycle.core.recipe.json": ["perps.order-lifecycle.core.recipe.json"],
|
|
231
|
+
"perps-lifecycle": ["perps.lifecycle"],
|
|
232
|
+
"perps-lifecycle.recipe.json": ["perps.lifecycle.recipe.json"],
|
|
233
|
+
"perps-performance": ["perps.performance"],
|
|
234
|
+
"perps-performance-background-resume": ["perps.performance.background-resume"],
|
|
235
|
+
"perps-performance-background-resume.mobile.recipe.json": ["perps.performance.background-resume.mobile.recipe.json"],
|
|
236
|
+
"perps-performance-cold-start": ["perps.performance.cold-start"],
|
|
237
|
+
"perps-performance-cold-start.mobile.recipe.json": ["perps.performance.cold-start.mobile.recipe.json"],
|
|
238
|
+
"perps-performance-warm-start": ["perps.performance.warm-start"],
|
|
239
|
+
"perps-performance-warm-start.mobile.recipe.json": ["perps.performance.warm-start.mobile.recipe.json"],
|
|
240
|
+
"perps-performance.mobile.recipe.json": ["perps.performance.mobile.recipe.json"],
|
|
241
|
+
"read-markets": ["perps.read-markets"],
|
|
242
|
+
"read-markets.core.recipe.json": ["perps.read-markets.core.recipe.json"],
|
|
243
|
+
"smoke": ["runner.smoke"],
|
|
244
|
+
"smoke.extension.recipe.json": ["runner.smoke.extension.recipe.json"],
|
|
245
|
+
"smoke.mobile.recipe.json": ["runner.smoke.mobile.recipe.json"],
|
|
246
|
+
"trading-lifecycle": ["perps.trading-lifecycle"],
|
|
247
|
+
"trading-lifecycle.core.recipe.json": ["perps.trading-lifecycle.core.recipe.json"]
|
|
248
|
+
};
|
|
190
249
|
async function validateRunRecipeStatic(recipeArg, adapter, options) {
|
|
191
250
|
const librarySources = await resolveMetaMaskLibrarySources(optionString(options, "library"));
|
|
192
251
|
const resolved = resolveRunRecipeArg(recipeArg, adapter, librarySources);
|
package/dist/commands/run.js
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
} from "./run-engine.js";
|
|
21
21
|
import { handleListExecutables } from "./list-executables.js";
|
|
22
22
|
import { applyDeviceTargeting } from "./device-target.js";
|
|
23
|
+
import { getAdapterSurface } from "../adapters/surface.js";
|
|
23
24
|
async function handleRun({ positional, options }) {
|
|
24
25
|
if (optionFlag(options, "list")) return handleListExecutables("run", options);
|
|
25
26
|
const targetRecipe = positional[0];
|
|
@@ -27,6 +28,7 @@ async function handleRun({ positional, options }) {
|
|
|
27
28
|
if (optionFlag(options, "plan")) return handleRunPlan(targetRecipe, options);
|
|
28
29
|
const { adapter, target } = resolveAdapter(options);
|
|
29
30
|
const json = optionFlag(options, "json");
|
|
31
|
+
getAdapterSurface(adapter).resolveSlotPorts(target);
|
|
30
32
|
const dtResult = applyDeviceTargeting("run", adapter, options, { gate: true, rerun: "" });
|
|
31
33
|
if ("code" in dtResult) {
|
|
32
34
|
return emitRunUsageError(json, adapter, targetRecipe, dtResult.code, dtResult.message);
|
|
@@ -26,7 +26,7 @@ async function runSelfTest(options) {
|
|
|
26
26
|
const manifest = loadActionManifest(adapter);
|
|
27
27
|
const manifestValidation = await validateManifest(manifest);
|
|
28
28
|
const smokeRecipe = recipePath(
|
|
29
|
-
adapter === "mobile" ? "smoke.mobile.recipe.json" : "smoke.extension.recipe.json"
|
|
29
|
+
adapter === "mobile" ? "runner/smoke.mobile.recipe.json" : "runner/smoke.extension.recipe.json"
|
|
30
30
|
);
|
|
31
31
|
const artifactsDir = path.join(root, adapter);
|
|
32
32
|
const result = await runRecipe(adapter, smokeRecipe, artifactsDir, runnerDir, void 0, {
|
package/dist/commands/status.js
CHANGED
|
@@ -16,8 +16,10 @@ async function handleStatus({ options }) {
|
|
|
16
16
|
return usageOut(json, "status", `could not detect the MetaMask repo type for ${target}`, ADAPTER_DETECT_NEXT);
|
|
17
17
|
}
|
|
18
18
|
assertAdapter(adapter);
|
|
19
|
-
const
|
|
20
|
-
|
|
19
|
+
const surface = getAdapterSurface(adapter);
|
|
20
|
+
surface.resolveSlotPorts(target);
|
|
21
|
+
const next = surface.hints.relaunch;
|
|
22
|
+
const devices = adapter === "mobile" ? slotScopedDevices(listConnectedDevices()) : [];
|
|
21
23
|
if (json) {
|
|
22
24
|
const doProbe = !fast && adapter === "mobile" && devices.length > 0;
|
|
23
25
|
if (doProbe) {
|
|
@@ -25,7 +27,7 @@ async function handleStatus({ options }) {
|
|
|
25
27
|
const devicesWithLive = devices.map((d) => mergeDeviceLive(d, liveMap.get(d.id)));
|
|
26
28
|
console.log(
|
|
27
29
|
JSON.stringify(
|
|
28
|
-
{ schemaVersion: 1, command: "status", adapter, target, devices: devicesWithLive, next },
|
|
30
|
+
{ schemaVersion: 1, command: "status", adapter, target, devices: devicesWithLive, next: nextForLive(next, liveMap) },
|
|
29
31
|
null,
|
|
30
32
|
2
|
|
31
33
|
)
|
|
@@ -40,13 +42,26 @@ async function handleStatus({ options }) {
|
|
|
40
42
|
const out = (style, text) => color(style, text, { stream: process.stdout });
|
|
41
43
|
console.log(`${out("label", "status")} ${out("bold", adapter)} ${out("dim", target)}`);
|
|
42
44
|
if (adapter === "mobile") renderDeviceList(devices, out);
|
|
43
|
-
console.log(`${out("label", "Next:")} ${out("cmd", next)}`);
|
|
44
45
|
if (!fast && adapter === "mobile" && devices.length > 0) {
|
|
45
46
|
const liveMap = await probeMobileLiveState(target, devices);
|
|
46
47
|
renderLiveBlock(devices, liveMap, out);
|
|
48
|
+
console.log(`${out("label", "Next:")} ${out("cmd", nextForLive(next, liveMap))}`);
|
|
49
|
+
} else {
|
|
50
|
+
console.log(`${out("label", "Next:")} ${out("cmd", next)}`);
|
|
47
51
|
}
|
|
48
52
|
return EXIT.ok;
|
|
49
53
|
}
|
|
54
|
+
function nextForLive(fallback, liveMap) {
|
|
55
|
+
for (const live of liveMap.values()) {
|
|
56
|
+
if (live.liveState === void 0) return "mm-harness logs";
|
|
57
|
+
}
|
|
58
|
+
return fallback;
|
|
59
|
+
}
|
|
60
|
+
function slotScopedDevices(devices) {
|
|
61
|
+
const withSelection = devices.map((device) => ({ ...device, selected: deviceSelected(device) }));
|
|
62
|
+
const selected = withSelection.filter((device) => device.selected);
|
|
63
|
+
return selected.length > 0 ? selected : withSelection;
|
|
64
|
+
}
|
|
50
65
|
function mergeDeviceLive(device, live) {
|
|
51
66
|
if (!live) return device;
|
|
52
67
|
return {
|
package/dist/mm-harness-cli.js
CHANGED
|
@@ -90,15 +90,16 @@ Example:
|
|
|
90
90
|
name: "call",
|
|
91
91
|
summary: "Run one action in isolation as a one-node recipe through the real engine path (fuzzy short names; --arg k=v; same trace/evidence as run).",
|
|
92
92
|
example: "mm-harness call ensure_unlocked",
|
|
93
|
-
helpText: `mm-harness call <action> [--arg k=v ...] [flags]
|
|
93
|
+
helpText: `mm-harness call <action> [key=value ...] [--arg k=v ...] [flags]
|
|
94
94
|
|
|
95
95
|
Run one action in isolation as a one-node recipe through the real engine path.
|
|
96
96
|
Fuzzy short-name: 'ensure_unlocked' resolves to 'metamask.wallet.ensure_unlocked'
|
|
97
|
-
if unique; ambiguous = exit 2.
|
|
98
|
-
with
|
|
97
|
+
if unique; ambiguous = exit 2. Pass action fields as key=value shorthand or
|
|
98
|
+
with --arg k=v. Actions differ per adapter \u2014 list this checkout's with:
|
|
99
|
+
mm-harness actions.
|
|
99
100
|
|
|
100
101
|
--list List everything invocable for the adapter (actions + flows); no <action> needed
|
|
101
|
-
--arg k=v Action field value (repeatable)
|
|
102
|
+
--arg k=v Action field value (repeatable; equivalent to key=value shorthand)
|
|
102
103
|
--device <udid|serial|name> Mobile only: target this device (env: IOS_SIMULATOR / ADB_SERIAL). Without it, >1 connected mobile device fails fast and lists them.
|
|
103
104
|
--adapter <mobile|extension|core> Target adapter (auto-detected inside a checkout)
|
|
104
105
|
--target <path> Checkout path (default: cwd)
|
|
@@ -110,7 +111,8 @@ Example:
|
|
|
110
111
|
|
|
111
112
|
Example (real actions; run mm-harness actions for this checkout's full set):
|
|
112
113
|
mm-harness call ensure_unlocked --adapter extension # a wallet action (extension/mobile)
|
|
113
|
-
mm-harness call
|
|
114
|
+
mm-harness call navigate page=perps --adapter mobile
|
|
115
|
+
mm-harness call command cmd="echo hi" --adapter core # the universal action (all adapters)`
|
|
114
116
|
},
|
|
115
117
|
{
|
|
116
118
|
name: "flows",
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
function listRecipeFiles(root, base = "") {
|
|
4
|
+
const out = [];
|
|
5
|
+
for (const entry of fs.readdirSync(path.join(root, base), { withFileTypes: true })) {
|
|
6
|
+
const rel = path.join(base, entry.name);
|
|
7
|
+
if (entry.isDirectory()) out.push(...listRecipeFiles(root, rel));
|
|
8
|
+
else if (entry.isFile() && entry.name.endsWith(".recipe.json")) out.push(rel);
|
|
9
|
+
}
|
|
10
|
+
return out;
|
|
11
|
+
}
|
|
12
|
+
export {
|
|
13
|
+
listRecipeFiles
|
|
14
|
+
};
|
package/docs/architecture.md
CHANGED
|
@@ -216,7 +216,7 @@ describes **how an agent should work**, it belongs in skills.
|
|
|
216
216
|
| `adapters/extension/` | Runner-owned Extension launch/live/watch/windows/wallet-state/readiness helpers (orchestration) and verify (recipe) copied into installed harnesses. |
|
|
217
217
|
| `adapters/shared/path-defaults.json` | Single source for default `recipeHarnessRoot` and `recipeRuntimeDir`. |
|
|
218
218
|
| `adapters/shared/harness-path.sh`, `src/paths.ts` | Shell and TypeScript accessors for those defaults plus validation. |
|
|
219
|
-
| `library/recipes/` | Reusable smoke/action-validation recipes only. Task-specific proof recipes stay task-local. |
|
|
219
|
+
| `library/recipes/` | Reusable smoke/runner.action-validation recipes only. Task-specific proof recipes stay task-local. |
|
|
220
220
|
| `library/library.json`, `library/flows/` | Canonical recipe library: minimal, stable flows recipes can `call`. Personal/team libraries shadow it by default — see [Recipe Libraries](recipe-libraries.md). |
|
|
221
221
|
| `docs/` | Runner architecture, contracts, and operational conventions. |
|
|
222
222
|
|
|
@@ -377,7 +377,7 @@ TypeScript or a focused `.mjs` adapter and keep shell as the launcher.
|
|
|
377
377
|
5. Use official `ui.*` actions and screenshot claims for visible acceptance
|
|
378
378
|
criteria; do not add task-specific `metamask.*` actions for exact copy,
|
|
379
379
|
styling, ticket IDs, or one-off selectors.
|
|
380
|
-
6. Update smoke/action-validation recipes only when the capability is reusable.
|
|
380
|
+
6. Update smoke/runner.action-validation recipes only when the capability is reusable.
|
|
381
381
|
|
|
382
382
|
## Standalone use first; farm later
|
|
383
383
|
|
|
@@ -85,7 +85,7 @@ domain capability useful across many tasks, such as `metamask.perps.start_state`
|
|
|
85
85
|
`metamask.perps.place_order`, `metamask.perps.close_positions`, or
|
|
86
86
|
`metamask.perps.assert_positions`.
|
|
87
87
|
|
|
88
|
-
`ui.scroll` is part of the current executable contract and action-validation must
|
|
88
|
+
`ui.scroll` is part of the current executable contract and runner.action-validation must
|
|
89
89
|
prove both normal scrolling and `scroll_into_view` before screenshot capture.
|
|
90
90
|
`ui.gesture` is intentionally not advertised yet; drag/swipe proof must wait
|
|
91
91
|
until the shared runtime and this runner expose and validate that action on both
|
package/docs/recipe-libraries.md
CHANGED
|
@@ -101,19 +101,20 @@ each user-visible step takes. The runner has no benchmark verb: timings are just
|
|
|
101
101
|
the per-node `duration`s in a passing run's `trace.json`, so a flow is comparable
|
|
102
102
|
across runs only when you pin the run (same device, healing off) and keep the node
|
|
103
103
|
graph stable. The repo ships one canonical example,
|
|
104
|
-
`library/recipes/perps
|
|
104
|
+
`library/recipes/perps/performance.mobile.recipe.json` — unlock → open the Perps
|
|
105
105
|
market list → read live state → open a market detail. This walkthrough copies it
|
|
106
106
|
into a library of your own and retargets it to your journey. A peer engineer can
|
|
107
|
-
follow it verbatim; the same steps run as the `perps
|
|
107
|
+
follow it verbatim; the same steps run as the `perps.performance-recipe` contract
|
|
108
108
|
test.
|
|
109
109
|
|
|
110
110
|
### 1. Scaffold a personal library
|
|
111
111
|
|
|
112
112
|
A library is a directory with a `library.json` marker. Keep reusable `flows/`
|
|
113
|
-
(referenced via `call`) beside a `recipes/` folder for the full
|
|
113
|
+
(referenced via `call`) beside a domain-grouped `recipes/` folder for the full
|
|
114
|
+
flows you run:
|
|
114
115
|
|
|
115
116
|
```bash
|
|
116
|
-
mkdir -p ~/my-recipes/flows ~/my-recipes/recipes
|
|
117
|
+
mkdir -p ~/my-recipes/flows ~/my-recipes/recipes/my-perps
|
|
117
118
|
cat > ~/my-recipes/library.json <<'JSON'
|
|
118
119
|
{ "kind": "recipe-library", "schema_version": 1, "name": "mydev", "owner": "mydev" }
|
|
119
120
|
JSON
|
|
@@ -130,8 +131,8 @@ it works headlessly (CI, a scratch checkout) and so the path is unambiguous.
|
|
|
130
131
|
Copy it out of your runner checkout's `library/recipes/`:
|
|
131
132
|
|
|
132
133
|
```bash
|
|
133
|
-
cp library/recipes/perps
|
|
134
|
-
~/my-recipes/recipes/my-perps
|
|
134
|
+
cp library/recipes/perps/performance.mobile.recipe.json \
|
|
135
|
+
~/my-recipes/recipes/my-perps/performance.mobile.recipe.json
|
|
135
136
|
```
|
|
136
137
|
|
|
137
138
|
### 3. Edit the nodes to your journey
|
|
@@ -211,33 +212,35 @@ field on flow catalog entries — not yet in the protocol.
|
|
|
211
212
|
|
|
212
213
|
### 4. Run it and read the timings
|
|
213
214
|
|
|
214
|
-
Validate statically **by name** first — `run` probes each library source's
|
|
215
|
-
directory in precedence order (personal → team → canonical), so
|
|
216
|
-
|
|
215
|
+
Validate statically **by name** first — `run` probes each library source's
|
|
216
|
+
`recipes/` directory in precedence order (personal → team → canonical), so
|
|
217
|
+
`my-perps.performance` resolves from
|
|
218
|
+
`~/my-recipes/recipes/my-perps/performance.mobile.recipe.json` without you
|
|
219
|
+
spelling out the path:
|
|
217
220
|
|
|
218
221
|
```bash
|
|
219
222
|
# Static validation by NAME — resolves from the personal library via --library.
|
|
220
|
-
mm-harness run my-perps
|
|
223
|
+
mm-harness run my-perps.performance \
|
|
221
224
|
--library mydev=~/my-recipes --plan --adapter mobile
|
|
222
225
|
|
|
223
226
|
# Zero-flag personal-library: when ~/my-recipes is placed at
|
|
224
227
|
# $FARMSLOT_HOME/recipe-library (default ~/.farmslot/recipe-library), the runner
|
|
225
228
|
# discovers it automatically and run-by-name works without --library:
|
|
226
|
-
mm-harness run my-perps
|
|
229
|
+
mm-harness run my-perps.performance --plan --adapter mobile
|
|
227
230
|
|
|
228
231
|
# Pinned live run — same device, healing OFF, so durations are comparable.
|
|
229
|
-
mm-harness run my-perps
|
|
232
|
+
mm-harness run my-perps.performance \
|
|
230
233
|
--library mydev=~/my-recipes \
|
|
231
234
|
--adapter mobile --device <serial> --heal off --artifacts-dir artifacts
|
|
232
235
|
|
|
233
236
|
# Canonical start-state variants — same installed app, no rebuild.
|
|
234
|
-
mm-harness run app
|
|
237
|
+
mm-harness run app.lifecycle.android-smoke \
|
|
235
238
|
--adapter mobile --device <serial> --heal off --artifacts-dir artifacts/lifecycle-smoke
|
|
236
|
-
mm-harness run perps
|
|
239
|
+
mm-harness run perps.performance.warm-start \
|
|
237
240
|
--adapter mobile --device <serial> --heal off --artifacts-dir artifacts/warm
|
|
238
|
-
mm-harness run perps
|
|
241
|
+
mm-harness run perps.performance.background-resume \
|
|
239
242
|
--adapter mobile --device <serial> --heal off --artifacts-dir artifacts/background
|
|
240
|
-
mm-harness run perps
|
|
243
|
+
mm-harness run perps.performance.cold-start \
|
|
241
244
|
--adapter mobile --device <serial> --heal off --artifacts-dir artifacts/cold
|
|
242
245
|
|
|
243
246
|
# Per-node durations to diff across runs (trace.json is an array of entries, or
|
|
@@ -249,7 +252,7 @@ node -e 'const t=require("./artifacts/trace.json"); \
|
|
|
249
252
|
A miss with `--library` names the sources that were searched, so you can tell at a
|
|
250
253
|
glance whether a typo or a missing library entry caused the failure.
|
|
251
254
|
|
|
252
|
-
Run `app
|
|
255
|
+
Run `app.lifecycle.android-smoke` first when validating a new Android slot; it
|
|
253
256
|
isolates lifecycle control from wallet setup and Perps navigation. The start-state
|
|
254
257
|
variants use the standard outer `app.lifecycle` action. Every
|
|
255
258
|
variant begins with `app.status` so `trace.json` records an idempotent start
|
|
@@ -294,5 +297,5 @@ mm-harness run my-perps-with-segment --plan --adapter mobile \
|
|
|
294
297
|
```
|
|
295
298
|
|
|
296
299
|
That resolution — unresolved without the source, `pass` with it — is what the
|
|
297
|
-
`perps
|
|
300
|
+
`perps.performance-recipe` contract test asserts, alongside the canonical recipe
|
|
298
301
|
resolving by name and the personal mobile copy planning by path and basename.
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
},
|
|
69
69
|
"screenshot-after": {
|
|
70
70
|
"action": "ui.screenshot",
|
|
71
|
-
"path": "screenshots/app
|
|
71
|
+
"path": "screenshots/app.lifecycle.android-smoke.png",
|
|
72
72
|
"intent": "Capture the app after Android lifecycle foreground",
|
|
73
73
|
"detail": "Visual proof that the selected device is displaying the relaunched app.",
|
|
74
74
|
"flow": "evidence",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema_version": 1,
|
|
3
3
|
"title": "MetaMask Mobile Perps performance flow",
|
|
4
|
-
"description": "Canonical measured flow for MetaMask Mobile Perps: setup unlocks the wallet, startState records the stable status, then the measured workflow opens the Perps market list, reads live state, and opens a market detail. Timings live in trace.json as each node's duration; there is no built-in benchmark verb. Run pinned so numbers are comparable: `mm-harness run perps
|
|
4
|
+
"description": "Canonical measured flow for MetaMask Mobile Perps: setup unlocks the wallet, startState records the stable status, then the measured workflow opens the Perps market list, reads live state, and opens a market detail. Timings live in trace.json as each node's duration; there is no built-in benchmark verb. Run pinned so numbers are comparable: `mm-harness run perps.performance --device <serial> --heal off`. Optional testnet order placement is intentionally omitted because a live order would make the flow destructive and non-repeatable; add metamask.perps.place_order + close in a personal copy when you want to measure the trade path.",
|
|
5
5
|
"startState": {
|
|
6
6
|
"action": "app.status",
|
|
7
7
|
"intent": "Record the stable run start status after wallet setup",
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": 1,
|
|
3
|
+
"title": "MetaMask core Perps smoke",
|
|
4
|
+
"description": "Non-mutating Perps domain smoke for the headless core adapter: instantiates the Perps controller and reads live positions, orders, and account state.",
|
|
5
|
+
"validate": {
|
|
6
|
+
"workflow": {
|
|
7
|
+
"entry": "status",
|
|
8
|
+
"nodes": {
|
|
9
|
+
"status": {
|
|
10
|
+
"action": "app.status",
|
|
11
|
+
"next": "read-positions",
|
|
12
|
+
"intent": "Resolve the core checkout and report headless compatibility mode"
|
|
13
|
+
},
|
|
14
|
+
"read-positions": {
|
|
15
|
+
"action": "metamask.perps.read_positions",
|
|
16
|
+
"mode": "all",
|
|
17
|
+
"next": "read-orders",
|
|
18
|
+
"intent": "Read live Perps positions from the headless controller"
|
|
19
|
+
},
|
|
20
|
+
"read-orders": {
|
|
21
|
+
"action": "metamask.perps.read_orders",
|
|
22
|
+
"mode": "all",
|
|
23
|
+
"next": "read-account",
|
|
24
|
+
"intent": "Read live Perps open orders from the headless controller"
|
|
25
|
+
},
|
|
26
|
+
"read-account": {
|
|
27
|
+
"action": "metamask.perps.read_account",
|
|
28
|
+
"next": "done",
|
|
29
|
+
"intent": "Read live Perps account state from the headless controller"
|
|
30
|
+
},
|
|
31
|
+
"done": {
|
|
32
|
+
"action": "end",
|
|
33
|
+
"status": "pass",
|
|
34
|
+
"intent": "Finish the core Perps smoke"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": 1,
|
|
3
|
+
"title": "MetaMask Extension Perps smoke",
|
|
4
|
+
"description": "Non-mutating Perps domain smoke for MetaMask Extension: verifies CDP, unlocks if needed, opens the Perps page, and reads live Perps positions and orders.",
|
|
5
|
+
"validate": {
|
|
6
|
+
"workflow": {
|
|
7
|
+
"entry": "status",
|
|
8
|
+
"nodes": {
|
|
9
|
+
"status": {
|
|
10
|
+
"action": "app.status",
|
|
11
|
+
"next": "cdp",
|
|
12
|
+
"intent": "Resolve the Extension checkout and adapter status before Perps proof"
|
|
13
|
+
},
|
|
14
|
+
"cdp": {
|
|
15
|
+
"action": "cdp.target",
|
|
16
|
+
"required": true,
|
|
17
|
+
"timeout_ms": 15000,
|
|
18
|
+
"next": "ensure-unlocked",
|
|
19
|
+
"intent": "Confirm the Extension CDP runtime is reachable"
|
|
20
|
+
},
|
|
21
|
+
"ensure-unlocked": {
|
|
22
|
+
"action": "metamask.wallet.ensure_unlocked",
|
|
23
|
+
"timeout_ms": 45000,
|
|
24
|
+
"next": "open-perps",
|
|
25
|
+
"intent": "Ensure the wallet is unlocked before opening Perps"
|
|
26
|
+
},
|
|
27
|
+
"open-perps": {
|
|
28
|
+
"action": "ui.navigate",
|
|
29
|
+
"page": "perps",
|
|
30
|
+
"next": "read-positions",
|
|
31
|
+
"intent": "Open the Perps page through the stable page alias"
|
|
32
|
+
},
|
|
33
|
+
"read-positions": {
|
|
34
|
+
"action": "metamask.perps.read_positions",
|
|
35
|
+
"next": "read-orders",
|
|
36
|
+
"intent": "Read live Perps positions without mutating account state"
|
|
37
|
+
},
|
|
38
|
+
"read-orders": {
|
|
39
|
+
"action": "metamask.perps.read_orders",
|
|
40
|
+
"next": "done",
|
|
41
|
+
"intent": "Read live Perps open orders without mutating account state"
|
|
42
|
+
},
|
|
43
|
+
"done": {
|
|
44
|
+
"action": "end",
|
|
45
|
+
"status": "pass",
|
|
46
|
+
"intent": "Finish the Extension Perps smoke"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": 1,
|
|
3
|
+
"title": "MetaMask Mobile Perps smoke",
|
|
4
|
+
"description": "Non-mutating Perps domain smoke for MetaMask Mobile: verifies the live bridge, unlocks if needed, opens the Perps market list, and reads live Perps positions and orders.",
|
|
5
|
+
"validate": {
|
|
6
|
+
"workflow": {
|
|
7
|
+
"entry": "status",
|
|
8
|
+
"nodes": {
|
|
9
|
+
"status": {
|
|
10
|
+
"action": "app.status",
|
|
11
|
+
"next": "cdp",
|
|
12
|
+
"intent": "Resolve the Mobile checkout and adapter status before Perps proof"
|
|
13
|
+
},
|
|
14
|
+
"cdp": {
|
|
15
|
+
"action": "cdp.target",
|
|
16
|
+
"required": true,
|
|
17
|
+
"timeout_ms": 15000,
|
|
18
|
+
"next": "ensure-unlocked",
|
|
19
|
+
"intent": "Confirm the React Native debug bridge is reachable"
|
|
20
|
+
},
|
|
21
|
+
"ensure-unlocked": {
|
|
22
|
+
"action": "metamask.wallet.ensure_unlocked",
|
|
23
|
+
"timeout_ms": 45000,
|
|
24
|
+
"next": "open-perps",
|
|
25
|
+
"intent": "Ensure the wallet is unlocked before opening Perps"
|
|
26
|
+
},
|
|
27
|
+
"open-perps": {
|
|
28
|
+
"action": "ui.navigate",
|
|
29
|
+
"page": "perps",
|
|
30
|
+
"next": "read-positions",
|
|
31
|
+
"intent": "Open the Perps market list through the stable page alias"
|
|
32
|
+
},
|
|
33
|
+
"read-positions": {
|
|
34
|
+
"action": "metamask.perps.read_positions",
|
|
35
|
+
"next": "read-orders",
|
|
36
|
+
"intent": "Read live Perps positions without mutating account state"
|
|
37
|
+
},
|
|
38
|
+
"read-orders": {
|
|
39
|
+
"action": "metamask.perps.read_orders",
|
|
40
|
+
"next": "done",
|
|
41
|
+
"intent": "Read live Perps open orders without mutating account state"
|
|
42
|
+
},
|
|
43
|
+
"done": {
|
|
44
|
+
"action": "end",
|
|
45
|
+
"status": "pass",
|
|
46
|
+
"intent": "Finish the Mobile Perps smoke"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
"nodes": {
|
|
9
9
|
"prepare-files": {
|
|
10
10
|
"action": "command",
|
|
11
|
-
"cmd": "mkdir -p temp/recipe-action-validation && printf 'E2E_ACTION_VALIDATION extension\\n' > temp/recipe-action-validation/probe.txt && printf '{\"platform\":\"extension\",\"ok\":true}\n' > temp/recipe-action-validation/probe.json && printf 'extension log E2E_ACTION_VALIDATION\\n' > temp/recipe-action-validation/validation.log && echo E2E_ACTION_VALIDATION extension",
|
|
11
|
+
"cmd": "mkdir -p temp/recipe-runner.action-validation && printf 'E2E_ACTION_VALIDATION extension\\n' > temp/recipe-runner.action-validation/probe.txt && printf '{\"platform\":\"extension\",\"ok\":true}\n' > temp/recipe-runner.action-validation/probe.json && printf 'extension log E2E_ACTION_VALIDATION\\n' > temp/recipe-runner.action-validation/validation.log && echo E2E_ACTION_VALIDATION extension",
|
|
12
12
|
"next": "assert-command-exit",
|
|
13
|
-
"intent": "Prepare action-validation probe files",
|
|
13
|
+
"intent": "Prepare runner.action-validation probe files",
|
|
14
14
|
"detail": "Create local files used to prove command/assert/log/artifact actions.",
|
|
15
15
|
"flow": "setup"
|
|
16
16
|
},
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
},
|
|
36
36
|
"assert-file": {
|
|
37
37
|
"action": "assert_file",
|
|
38
|
-
"path": "temp/recipe-action-validation/probe.txt",
|
|
38
|
+
"path": "temp/recipe-runner.action-validation/probe.txt",
|
|
39
39
|
"contains": "E2E_ACTION_VALIDATION extension",
|
|
40
40
|
"next": "assert-json",
|
|
41
41
|
"intent": "Verify file assertions work",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
},
|
|
45
45
|
"assert-json": {
|
|
46
46
|
"action": "assert_json",
|
|
47
|
-
"path": "temp/recipe-action-validation/probe.json",
|
|
47
|
+
"path": "temp/recipe-runner.action-validation/probe.json",
|
|
48
48
|
"assert": {
|
|
49
49
|
"path": "$.platform",
|
|
50
50
|
"operator": "eq",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
},
|
|
58
58
|
"watch-logs": {
|
|
59
59
|
"action": "watch_logs",
|
|
60
|
-
"path": "temp/recipe-action-validation/validation.log",
|
|
60
|
+
"path": "temp/recipe-runner.action-validation/validation.log",
|
|
61
61
|
"contains": "E2E_ACTION_VALIDATION",
|
|
62
62
|
"next": "index-artifacts",
|
|
63
63
|
"intent": "Verify log watching works",
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"action": "index_artifacts",
|
|
69
69
|
"artifacts": [
|
|
70
70
|
{
|
|
71
|
-
"path": "temp/recipe-action-validation/probe.txt",
|
|
71
|
+
"path": "temp/recipe-runner.action-validation/probe.txt",
|
|
72
72
|
"type": "log",
|
|
73
73
|
"category": "debug",
|
|
74
74
|
"label": "Extension action validation probe"
|
|
@@ -173,7 +173,7 @@
|
|
|
173
173
|
},
|
|
174
174
|
"ui-screenshot-perps": {
|
|
175
175
|
"action": "ui.screenshot",
|
|
176
|
-
"path": "screenshots/extension-perps-action-validation.png",
|
|
176
|
+
"path": "screenshots/extension-perps-runner.action-validation.png",
|
|
177
177
|
"timeout_ms": 5000,
|
|
178
178
|
"next": "wallet-navigate-perps",
|
|
179
179
|
"intent": "Capture Perps visual proof",
|
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
"nodes": {
|
|
9
9
|
"prepare-files": {
|
|
10
10
|
"action": "command",
|
|
11
|
-
"cmd": "mkdir -p temp/recipe-action-validation && printf 'E2E_ACTION_VALIDATION mobile\\n' > temp/recipe-action-validation/probe.txt && printf '{\"platform\":\"mobile\",\"ok\":true}\n' > temp/recipe-action-validation/probe.json && printf 'mobile log E2E_ACTION_VALIDATION\\n' > temp/recipe-action-validation/validation.log && echo E2E_ACTION_VALIDATION mobile",
|
|
11
|
+
"cmd": "mkdir -p temp/recipe-runner.action-validation && printf 'E2E_ACTION_VALIDATION mobile\\n' > temp/recipe-runner.action-validation/probe.txt && printf '{\"platform\":\"mobile\",\"ok\":true}\n' > temp/recipe-runner.action-validation/probe.json && printf 'mobile log E2E_ACTION_VALIDATION\\n' > temp/recipe-runner.action-validation/validation.log && echo E2E_ACTION_VALIDATION mobile",
|
|
12
12
|
"next": "assert-command-exit",
|
|
13
|
-
"intent": "Prepare action-validation probe files",
|
|
13
|
+
"intent": "Prepare runner.action-validation probe files",
|
|
14
14
|
"detail": "Create local files used to prove command/assert/log/artifact actions.",
|
|
15
15
|
"flow": "setup"
|
|
16
16
|
},
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
},
|
|
36
36
|
"assert-file": {
|
|
37
37
|
"action": "assert_file",
|
|
38
|
-
"path": "temp/recipe-action-validation/probe.txt",
|
|
38
|
+
"path": "temp/recipe-runner.action-validation/probe.txt",
|
|
39
39
|
"contains": "E2E_ACTION_VALIDATION mobile",
|
|
40
40
|
"next": "assert-json",
|
|
41
41
|
"intent": "Verify file assertions work",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
},
|
|
45
45
|
"assert-json": {
|
|
46
46
|
"action": "assert_json",
|
|
47
|
-
"path": "temp/recipe-action-validation/probe.json",
|
|
47
|
+
"path": "temp/recipe-runner.action-validation/probe.json",
|
|
48
48
|
"assert": {
|
|
49
49
|
"path": "$.platform",
|
|
50
50
|
"operator": "eq",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
},
|
|
58
58
|
"watch-logs": {
|
|
59
59
|
"action": "watch_logs",
|
|
60
|
-
"path": "temp/recipe-action-validation/validation.log",
|
|
60
|
+
"path": "temp/recipe-runner.action-validation/validation.log",
|
|
61
61
|
"contains": "E2E_ACTION_VALIDATION",
|
|
62
62
|
"next": "index-artifacts",
|
|
63
63
|
"intent": "Verify log watching works",
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"action": "index_artifacts",
|
|
69
69
|
"artifacts": [
|
|
70
70
|
{
|
|
71
|
-
"path": "temp/recipe-action-validation/probe.txt",
|
|
71
|
+
"path": "temp/recipe-runner.action-validation/probe.txt",
|
|
72
72
|
"type": "log",
|
|
73
73
|
"category": "debug",
|
|
74
74
|
"label": "Mobile action validation probe"
|
|
@@ -174,7 +174,7 @@
|
|
|
174
174
|
},
|
|
175
175
|
"ui-screenshot-wallet": {
|
|
176
176
|
"action": "ui.screenshot",
|
|
177
|
-
"path": "screenshots/mobile-wallet-action-validation.png",
|
|
177
|
+
"path": "screenshots/mobile-wallet-runner.action-validation.png",
|
|
178
178
|
"next": "wallet-navigate-perps",
|
|
179
179
|
"intent": "Capture wallet visual proof",
|
|
180
180
|
"detail": "Save a screenshot artifact after generic UI actions.",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deeeed/metamask-harness",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"mm-harness": "bin/mm-harness"
|
|
@@ -69,4 +69,4 @@
|
|
|
69
69
|
"url": "https://github.com/MetaMask/experimental-metamask-harness/issues"
|
|
70
70
|
},
|
|
71
71
|
"homepage": "https://github.com/MetaMask/experimental-metamask-harness#readme"
|
|
72
|
-
}
|
|
72
|
+
}
|
package/scripts/completions.sh
CHANGED
|
File without changes
|