@deeeed/metamask-harness 0.12.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 +26 -5
- 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 +16 -0
- package/adapters/mobile/bridge-runtime/console-forwarder.cjs +2 -4
- package/adapters/mobile/bridge-runtime/lib/bridge-errors.cjs +106 -0
- package/adapters/mobile/bridge-runtime/lib/console-format.cjs +53 -0
- package/adapters/mobile/bridge-runtime/lib/match-bridge-target.cjs +82 -0
- package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +17 -13
- package/adapters/mobile/bridge-runtime/lib/ws-client.cjs +10 -5
- 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 +22 -1
- package/adapters/mobile/stop-metro.sh +0 -0
- package/adapters/mobile/verify.sh +50 -13
- package/adapters/mobile/wait-for-bridge.sh +41 -23
- 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/adapters/mobile/prepare.js +23 -2
- package/dist/adapters/mobile/runtime-decision.js +2 -1
- package/dist/adapters.js +33 -10
- package/dist/cli-commands.js +5 -4
- package/dist/commands/call.js +138 -3
- package/dist/commands/device-target.js +10 -3
- package/dist/commands/fixtures.js +2 -1
- package/dist/commands/manifest.js +2 -0
- package/dist/commands/run-engine.js +71 -4
- 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 +19 -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/actions/mobile/platform/bridge.mjs +35 -4
- 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
package/dist/commands/call.js
CHANGED
|
@@ -6,14 +6,17 @@ import { loadActionManifest } from "../manifest.js";
|
|
|
6
6
|
import { importRecipeProtocol } from "../paths.js";
|
|
7
7
|
import { recipeRunning } from "../heal-bounds.js";
|
|
8
8
|
import { EXIT } from "./shared.js";
|
|
9
|
+
import { describeManifestActions, fuzzyResolveActions } from "./manifest.js";
|
|
9
10
|
import {
|
|
10
11
|
parseArgs,
|
|
12
|
+
isRecord,
|
|
11
13
|
optionFlag,
|
|
12
14
|
optionString,
|
|
13
15
|
resolveAdapter,
|
|
14
16
|
runtimeOptionsFromCli,
|
|
15
17
|
usageError
|
|
16
18
|
} from "./parse-args.js";
|
|
19
|
+
import { getAdapterSurface } from "../adapters/surface.js";
|
|
17
20
|
import { handleListExecutables } from "./list-executables.js";
|
|
18
21
|
import { applyDeviceTargeting } from "./device-target.js";
|
|
19
22
|
import {
|
|
@@ -31,7 +34,7 @@ async function handleCall(argv) {
|
|
|
31
34
|
return handleListExecutables("call", options2);
|
|
32
35
|
}
|
|
33
36
|
if (argv.length > 0 && argv[0].startsWith("--")) {
|
|
34
|
-
const message = "call requires <action> first: mm-harness call <action> [--arg k=v ...] [flags]";
|
|
37
|
+
const message = "call requires <action> first: mm-harness call <action> [key=value ...] [--arg k=v ...] [flags]";
|
|
35
38
|
console.error(message);
|
|
36
39
|
return EXIT.usage;
|
|
37
40
|
}
|
|
@@ -58,6 +61,7 @@ async function handleCall(argv) {
|
|
|
58
61
|
return EXIT.usage;
|
|
59
62
|
}
|
|
60
63
|
const { adapter, target } = resolveAdapter(options);
|
|
64
|
+
getAdapterSurface(adapter).resolveSlotPorts(target);
|
|
61
65
|
const dtResult = applyDeviceTargeting("call", adapter, options, { gate: true, rerun: "" });
|
|
62
66
|
if ("code" in dtResult) {
|
|
63
67
|
if (json) console.log(JSON.stringify({ schemaVersion: 1, command: "call", adapter, error: { code: dtResult.code, message: dtResult.message } }, null, 2));
|
|
@@ -125,6 +129,7 @@ async function handleCall(argv) {
|
|
|
125
129
|
state
|
|
126
130
|
);
|
|
127
131
|
if (violation !== null) return emitHealViolation(json, "call", result, violation, state, adapter);
|
|
132
|
+
const callOutput = readCallOutput(result.tracePath);
|
|
128
133
|
if (json) {
|
|
129
134
|
console.log(
|
|
130
135
|
JSON.stringify(
|
|
@@ -139,6 +144,7 @@ async function handleCall(argv) {
|
|
|
139
144
|
summaryPath: result.summaryPath,
|
|
140
145
|
tracePath: result.tracePath,
|
|
141
146
|
artifactManifestPath: result.artifactManifestPath,
|
|
147
|
+
...callOutput !== void 0 ? { output: callOutput } : {},
|
|
142
148
|
recovered: state.recovered,
|
|
143
149
|
mutations: state.mutations,
|
|
144
150
|
exitCode: result.status === "pass" ? EXIT.ok : EXIT.runtime
|
|
@@ -148,11 +154,105 @@ async function handleCall(argv) {
|
|
|
148
154
|
)
|
|
149
155
|
);
|
|
150
156
|
} else {
|
|
151
|
-
|
|
157
|
+
const rendered = callOutput !== void 0 ? `
|
|
158
|
+
Result:
|
|
159
|
+
${formatCallOutput(callOutput)}` : "";
|
|
160
|
+
console.log(`call ${resolvedAction}: ${result.status}${rendered}
|
|
152
161
|
Artifacts: ${result.artifactManifestPath}`);
|
|
153
162
|
}
|
|
154
163
|
return result.status === "pass" ? EXIT.ok : EXIT.runtime;
|
|
155
164
|
}
|
|
165
|
+
function readCallOutput(tracePath) {
|
|
166
|
+
try {
|
|
167
|
+
const trace = JSON.parse(fs.readFileSync(tracePath, "utf8"));
|
|
168
|
+
const entries = Array.isArray(trace) ? trace : isRecord(trace) && Array.isArray(trace.entries) ? trace.entries : [];
|
|
169
|
+
const entry = entries.find((item) => isRecord(item) && item.nodeId === "call");
|
|
170
|
+
return isRecord(entry) && Object.prototype.hasOwnProperty.call(entry, "output") ? entry.output : void 0;
|
|
171
|
+
} catch {
|
|
172
|
+
return void 0;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
function formatCallOutput(value) {
|
|
176
|
+
if (typeof value === "string") return value;
|
|
177
|
+
if (typeof value === "number" || typeof value === "boolean" || value === null) return String(value);
|
|
178
|
+
return JSON.stringify(value, null, 2);
|
|
179
|
+
}
|
|
180
|
+
async function handleCallHelp(argv, genericHelp) {
|
|
181
|
+
const { action: shortName, rest } = parseCallArgs(argv.filter((arg) => arg !== "--help" && arg !== "-h"));
|
|
182
|
+
const { options } = parseArgs(rest, "call");
|
|
183
|
+
let adapter;
|
|
184
|
+
let manifest;
|
|
185
|
+
try {
|
|
186
|
+
({ adapter } = resolveAdapter(options));
|
|
187
|
+
manifest = loadActionManifest(adapter, optionString(options, "actionManifest"));
|
|
188
|
+
} catch {
|
|
189
|
+
process.stdout.write(`${genericHelp}
|
|
190
|
+
`);
|
|
191
|
+
return EXIT.ok;
|
|
192
|
+
}
|
|
193
|
+
const matches = shortName ? fuzzyResolveActions(describeManifestActions(manifest), shortName) : [];
|
|
194
|
+
if (matches.length === 0) {
|
|
195
|
+
process.stdout.write(`${genericHelp}
|
|
196
|
+
`);
|
|
197
|
+
if (shortName) {
|
|
198
|
+
process.stdout.write(
|
|
199
|
+
`
|
|
200
|
+
No action matches "${shortName}" for the ${adapter} adapter.
|
|
201
|
+
Next: mm-harness actions --adapter ${adapter} # list the action vocabulary
|
|
202
|
+
`
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
return EXIT.ok;
|
|
206
|
+
}
|
|
207
|
+
for (const entry of matches) process.stdout.write(renderCallActionHelp(entry));
|
|
208
|
+
process.stdout.write(`${genericHelp}
|
|
209
|
+
`);
|
|
210
|
+
return EXIT.ok;
|
|
211
|
+
}
|
|
212
|
+
function renderCallActionHelp(entry) {
|
|
213
|
+
const short = entry.name.split(".").pop() ?? entry.name;
|
|
214
|
+
const schema = isRecord(entry.schema) ? entry.schema : {};
|
|
215
|
+
const properties = isRecord(schema.properties) ? schema.properties : {};
|
|
216
|
+
const required = new Set(
|
|
217
|
+
Array.isArray(schema.required) ? schema.required.filter((r) => typeof r === "string") : []
|
|
218
|
+
);
|
|
219
|
+
const lines = [`mm-harness call ${entry.name} [key=value ...] [--arg k=v ...] [flags]`, ""];
|
|
220
|
+
if (entry.description) lines.push(` ${entry.description}`, "");
|
|
221
|
+
const names = Object.keys(properties).sort();
|
|
222
|
+
if (names.length === 0) {
|
|
223
|
+
lines.push(" Fields: (none)");
|
|
224
|
+
} else {
|
|
225
|
+
lines.push(" Fields (pass as <name>=<value> or --arg <name>=<value>):");
|
|
226
|
+
const width = Math.max(...names.map((name) => name.length));
|
|
227
|
+
for (const name of names) {
|
|
228
|
+
const prop = isRecord(properties[name]) ? properties[name] : {};
|
|
229
|
+
const type = typeof prop.type === "string" ? prop.type : "any";
|
|
230
|
+
const req = required.has(name) ? " (required)" : "";
|
|
231
|
+
const desc = typeof prop.description === "string" ? ` \u2014 ${prop.description}` : "";
|
|
232
|
+
const enumVals = Array.isArray(prop.enum) ? ` [one of: ${prop.enum.join(", ")}]` : "";
|
|
233
|
+
lines.push(` ${name.padEnd(width)} ${type}${req}${desc}${enumVals}`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
const examples = renderCallExamples(short, entry.examples);
|
|
237
|
+
if (examples.length > 0) {
|
|
238
|
+
lines.push("", " Examples:");
|
|
239
|
+
for (const example of examples) lines.push(` ${example}`);
|
|
240
|
+
}
|
|
241
|
+
return `${lines.join("\n")}
|
|
242
|
+
|
|
243
|
+
`;
|
|
244
|
+
}
|
|
245
|
+
function renderCallExamples(short, examples) {
|
|
246
|
+
if (!Array.isArray(examples)) return [];
|
|
247
|
+
const out = [];
|
|
248
|
+
for (const example of examples.slice(0, 2)) {
|
|
249
|
+
const node = isRecord(example) && isRecord(example.node) ? example.node : void 0;
|
|
250
|
+
if (!node) continue;
|
|
251
|
+
const tokens = Object.entries(node).filter(([key]) => key !== "action" && key !== "intent").map(([key, value]) => `${key}=${typeof value === "string" ? value : JSON.stringify(value)}`);
|
|
252
|
+
out.push(`mm-harness call ${short} ${tokens.join(" ")}`.trim());
|
|
253
|
+
}
|
|
254
|
+
return out;
|
|
255
|
+
}
|
|
156
256
|
function parseCallArgs(argv) {
|
|
157
257
|
const args = {};
|
|
158
258
|
const rest = [];
|
|
@@ -171,10 +271,44 @@ function parseCallArgs(argv) {
|
|
|
171
271
|
action = arg;
|
|
172
272
|
continue;
|
|
173
273
|
}
|
|
274
|
+
if (action !== void 0 && !isForwardedOptionValue(argv, i) && isArgPair(arg)) {
|
|
275
|
+
const eq = arg.indexOf("=");
|
|
276
|
+
args[arg.slice(0, eq)] = arg.slice(eq + 1);
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
174
279
|
rest.push(arg);
|
|
175
280
|
}
|
|
176
281
|
return { action, args, rest };
|
|
177
282
|
}
|
|
283
|
+
function isArgPair(value) {
|
|
284
|
+
const eq = value.indexOf("=");
|
|
285
|
+
return eq > 0;
|
|
286
|
+
}
|
|
287
|
+
function isForwardedOptionValue(argv, index) {
|
|
288
|
+
if (index === 0) return false;
|
|
289
|
+
const previous = argv[index - 1];
|
|
290
|
+
if (!previous.startsWith("--") || previous.includes("=")) return false;
|
|
291
|
+
return VALUE_TAKING_CALL_FLAGS.has(normalizeFlagName(previous));
|
|
292
|
+
}
|
|
293
|
+
const VALUE_TAKING_CALL_FLAGS = /* @__PURE__ */ new Set([
|
|
294
|
+
"action-manifest",
|
|
295
|
+
"adapter",
|
|
296
|
+
"artifacts-dir",
|
|
297
|
+
"cdp-port",
|
|
298
|
+
"device",
|
|
299
|
+
"heal",
|
|
300
|
+
"library",
|
|
301
|
+
"metro-port",
|
|
302
|
+
"platform",
|
|
303
|
+
"project-root",
|
|
304
|
+
"slot",
|
|
305
|
+
"target",
|
|
306
|
+
"validation-runtime-dir",
|
|
307
|
+
"watcher-port"
|
|
308
|
+
]);
|
|
309
|
+
function normalizeFlagName(flag) {
|
|
310
|
+
return flag.replace(/^--/u, "");
|
|
311
|
+
}
|
|
178
312
|
function resolveActionName(shortName, names) {
|
|
179
313
|
if (names.includes(shortName)) return { status: "ok", resolved: shortName, candidates: [shortName] };
|
|
180
314
|
const finalSegment = (name) => name.split(".").pop() ?? name;
|
|
@@ -191,5 +325,6 @@ function pickCallExampleAction(names) {
|
|
|
191
325
|
return names[0] ?? "command";
|
|
192
326
|
}
|
|
193
327
|
export {
|
|
194
|
-
handleCall
|
|
328
|
+
handleCall,
|
|
329
|
+
handleCallHelp
|
|
195
330
|
};
|
|
@@ -111,9 +111,16 @@ function applyDeviceTargeting(command, adapter, options, opts) {
|
|
|
111
111
|
${formatConnectedDevices(connected2)}` : ` No devices are currently connected (adb devices / booted iOS simulators).`)
|
|
112
112
|
};
|
|
113
113
|
}
|
|
114
|
-
if (!opts.gate) return { ok: true };
|
|
115
114
|
const connected = listConnectedDevices();
|
|
116
115
|
const targetable = connected.filter(isTargetable);
|
|
116
|
+
const selectedTargetable = targetable.filter(deviceSelected);
|
|
117
|
+
if (selectedTargetable.length === 1) {
|
|
118
|
+
const selected = selectedTargetable[0];
|
|
119
|
+
if (selected.platform === "android") setAndroidDeviceEnv(selected.id, selected.name);
|
|
120
|
+
else setIosDeviceEnv(selected.id, selected.name);
|
|
121
|
+
return { ok: true };
|
|
122
|
+
}
|
|
123
|
+
if (!opts.gate) return { ok: true };
|
|
117
124
|
if (targetable.length > 1) {
|
|
118
125
|
return {
|
|
119
126
|
ok: false,
|
|
@@ -121,8 +128,8 @@ ${formatConnectedDevices(connected2)}` : ` No devices are currently connected (
|
|
|
121
128
|
message: `${targetable.length} mobile devices available \u2014 ${command} needs exactly one target.
|
|
122
129
|
Connected devices:
|
|
123
130
|
${formatConnectedDevices(connected)}
|
|
124
|
-
Add --device <id> to disambiguate, e.g.:${targetable.map((d) => `
|
|
125
|
-
--device ${d.id}`).join("")}`
|
|
131
|
+
` + (selectedTargetable.length > 1 ? ` The current slot context selects more than one target; fix the slot device pins or add --device <id>.` : ` Add --device <id> to disambiguate, e.g.:${targetable.map((d) => `
|
|
132
|
+
--device ${d.id}`).join("")}`)
|
|
126
133
|
};
|
|
127
134
|
}
|
|
128
135
|
return { ok: true };
|
|
@@ -39,13 +39,14 @@ async function handleFixtures(argv, deps) {
|
|
|
39
39
|
if (!adapter) {
|
|
40
40
|
return usageOut(json, "fixtures", `could not detect the MetaMask repo type for ${target}`, ADAPTER_DETECT_NEXT);
|
|
41
41
|
}
|
|
42
|
+
const surface = getAdapterSurface(adapter);
|
|
43
|
+
surface.resolveSlotPorts(target);
|
|
42
44
|
const dtResult = applyDeviceTargeting("fixtures", adapter, options, { gate: false, rerun: "" });
|
|
43
45
|
if ("code" in dtResult) {
|
|
44
46
|
return usageOut(json, "fixtures", dtResult.message, "mm-harness fixtures set --adapter mobile --device <adb-serial|simulator-udid>");
|
|
45
47
|
}
|
|
46
48
|
if (sub === "generate") return fixturesGenerate(adapter, target, options, json);
|
|
47
49
|
if (sub === "finalize") return fixturesFinalize(adapter, target, options, json);
|
|
48
|
-
const surface = getAdapterSurface(adapter);
|
|
49
50
|
if (surface.headless) return usageOut(json, "fixtures", "core is headless; it has no wallet fixture.", surface.hints.launch);
|
|
50
51
|
const canonicalFixture = walletFixturePath(target);
|
|
51
52
|
const retryHint = `${surface.hints.relaunch} # relaunch, then retry: mm-harness fixtures set`;
|
|
@@ -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 {
|
|
@@ -68,7 +69,15 @@ async function runRecipe(adapter, recipe, artifactsDir, projectRoot, actionManif
|
|
|
68
69
|
recordVideo: useFramedExtensionRecording ? false : recordVideo,
|
|
69
70
|
...runtimeOptions.librarySources ? { librarySources: runtimeOptions.librarySources } : {}
|
|
70
71
|
};
|
|
71
|
-
|
|
72
|
+
let result;
|
|
73
|
+
try {
|
|
74
|
+
result = await runner.run(runRequest);
|
|
75
|
+
} finally {
|
|
76
|
+
if (adapter === "mobile") {
|
|
77
|
+
const { hideMobileHudOnTeardown } = await import("../adapters.js");
|
|
78
|
+
await hideMobileHudOnTeardown(projectRoot, recipeRunEnv(adapter, runtimeOptions));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
72
81
|
await stopRecipeRecording(recording, result);
|
|
73
82
|
return result;
|
|
74
83
|
} finally {
|
|
@@ -144,7 +153,7 @@ function resolveRunRecipeArg(recipeArg, adapter, librarySources) {
|
|
|
144
153
|
const direct = path.resolve(recipeArg);
|
|
145
154
|
if (isRecipeFile(direct)) return { recipeFile: direct };
|
|
146
155
|
if (!recipeArg.includes("/") && !recipeArg.includes(path.sep)) {
|
|
147
|
-
const candidates =
|
|
156
|
+
const candidates = recipeNameCandidates(recipeArg, adapter);
|
|
148
157
|
if (librarySources && librarySources.length > 0) {
|
|
149
158
|
for (const source of librarySources) {
|
|
150
159
|
for (const candidate of candidates) {
|
|
@@ -168,17 +177,75 @@ function resolveRunRecipeArg(recipeArg, adapter, librarySources) {
|
|
|
168
177
|
function libraryRecipeNames(adapter) {
|
|
169
178
|
let entries;
|
|
170
179
|
try {
|
|
171
|
-
entries =
|
|
180
|
+
entries = listRecipeFiles(recipePath(""));
|
|
172
181
|
} catch {
|
|
173
182
|
return [];
|
|
174
183
|
}
|
|
175
184
|
const scopes = ["mobile", "extension", "core"];
|
|
176
|
-
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) => {
|
|
177
186
|
const scope = n.split(".").pop() ?? "";
|
|
178
187
|
return !scopes.includes(scope) || scope === adapter;
|
|
179
188
|
}).map((n) => n.endsWith(`.${adapter}`) ? n.slice(0, -(adapter.length + 1)) : n);
|
|
180
189
|
return [...new Set(names)].sort();
|
|
181
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
|
+
};
|
|
182
249
|
async function validateRunRecipeStatic(recipeArg, adapter, options) {
|
|
183
250
|
const librarySources = await resolveMetaMaskLibrarySources(optionString(options, "library"));
|
|
184
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
|
@@ -5,6 +5,7 @@ import { fileURLToPath } from "node:url";
|
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
import { color } from "./cli-color.js";
|
|
7
7
|
import { handleUpdate, maybeNudge } from "./commands/update.js";
|
|
8
|
+
import { handleCallHelp } from "./commands/call.js";
|
|
8
9
|
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
9
10
|
globalThis.__MM_HARNESS_WRAPPER__ = true;
|
|
10
11
|
const { main: recipeMain } = await import("./cli.js");
|
|
@@ -89,15 +90,16 @@ Example:
|
|
|
89
90
|
name: "call",
|
|
90
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).",
|
|
91
92
|
example: "mm-harness call ensure_unlocked",
|
|
92
|
-
helpText: `mm-harness call <action> [--arg k=v ...] [flags]
|
|
93
|
+
helpText: `mm-harness call <action> [key=value ...] [--arg k=v ...] [flags]
|
|
93
94
|
|
|
94
95
|
Run one action in isolation as a one-node recipe through the real engine path.
|
|
95
96
|
Fuzzy short-name: 'ensure_unlocked' resolves to 'metamask.wallet.ensure_unlocked'
|
|
96
|
-
if unique; ambiguous = exit 2.
|
|
97
|
-
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.
|
|
98
100
|
|
|
99
101
|
--list List everything invocable for the adapter (actions + flows); no <action> needed
|
|
100
|
-
--arg k=v Action field value (repeatable)
|
|
102
|
+
--arg k=v Action field value (repeatable; equivalent to key=value shorthand)
|
|
101
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.
|
|
102
104
|
--adapter <mobile|extension|core> Target adapter (auto-detected inside a checkout)
|
|
103
105
|
--target <path> Checkout path (default: cwd)
|
|
@@ -109,7 +111,8 @@ Example:
|
|
|
109
111
|
|
|
110
112
|
Example (real actions; run mm-harness actions for this checkout's full set):
|
|
111
113
|
mm-harness call ensure_unlocked --adapter extension # a wallet action (extension/mobile)
|
|
112
|
-
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)`
|
|
113
116
|
},
|
|
114
117
|
{
|
|
115
118
|
name: "flows",
|
|
@@ -614,6 +617,13 @@ function hasPassthroughHelp(argv) {
|
|
|
614
617
|
if (divider === -1) return false;
|
|
615
618
|
return argv.slice(divider + 1).some((arg) => arg === "-h" || arg === "--help");
|
|
616
619
|
}
|
|
620
|
+
function isCallActionHelp(argv) {
|
|
621
|
+
if (argv[0] !== "call") return false;
|
|
622
|
+
if (!argv[1] || argv[1].startsWith("-")) return false;
|
|
623
|
+
const divider = argv.indexOf("--");
|
|
624
|
+
const scope = divider === -1 ? argv : argv.slice(0, divider);
|
|
625
|
+
return scope.includes("--help") || scope.includes("-h");
|
|
626
|
+
}
|
|
617
627
|
if (rawArgv.length === 0) {
|
|
618
628
|
process.stdout.write(groupedHelp());
|
|
619
629
|
process.exit(0);
|
|
@@ -621,4 +631,8 @@ if (rawArgv.length === 0) {
|
|
|
621
631
|
if (hasPassthroughHelp(rawArgv)) {
|
|
622
632
|
process.exit(await delegate(rawArgv));
|
|
623
633
|
}
|
|
634
|
+
if (isCallActionHelp(rawArgv)) {
|
|
635
|
+
const callHelp = REAL.find((command) => command.name === "call")?.helpText ?? "";
|
|
636
|
+
process.exit(await handleCallHelp(rawArgv.slice(1), callHelp));
|
|
637
|
+
}
|
|
624
638
|
await program.parseAsync(process.argv);
|
|
@@ -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.
|