@deeeed/metamask-harness 0.17.4 → 0.17.5
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 +6 -103
- package/README.md +136 -96
- package/adapters/extension/console-tail.mjs +24 -55
- package/adapters/extension/ensure-browser.sh +7 -1
- package/adapters/extension/inject.mjs +15 -5
- package/adapters/extension/launch-browser.cjs +4 -3
- package/adapters/extension/launch.sh +11 -8
- package/adapters/extension/lib/chrome-args.cjs +24 -0
- package/adapters/extension/live.sh +1 -2
- package/adapters/extension/reattach.sh +0 -1
- package/adapters/extension/seed-fixture.sh +12 -4
- package/adapters/extension/sidepanel-toggle.sh +1 -4
- package/adapters/extension/start-watch.sh +4 -17
- package/adapters/extension/stop-viewers.sh +1 -1
- package/adapters/extension/verify.sh +1 -1
- package/adapters/manifest.json +45 -53
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +0 -16
- package/adapters/mobile/bridge-runtime/console-forwarder.cjs +1 -1
- package/adapters/mobile/bridge-runtime/lib/match-bridge-target.cjs +1 -3
- package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +3 -2
- package/adapters/mobile/cleanup.sh +176 -16
- package/adapters/mobile/inject.sh +671 -90
- package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +185 -0
- package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgenticService.ts.patch +1662 -0
- package/adapters/mobile/overlay/compat/README.md +28 -0
- package/adapters/mobile/overlay/compat/rn81-message-event-source.patch +42 -0
- package/adapters/mobile/start-metro.sh +41 -0
- package/adapters/mobile/verify.sh +61 -4
- package/adapters/mobile/wait-for-bridge.sh +8 -22
- package/adapters/shared/harness-source-fingerprint.mjs +49 -0
- package/adapters/shared/install-repo-deps.sh +1 -5
- package/adapters/shared/open-debug.mjs +184 -103
- package/adapters/shared/resolve-farmslot-ports-core.mjs +3 -0
- package/adapters/shared/resolve-farmslot-ports.mjs +5 -0
- package/adapters/shared/resolve-farmslot-ports.sh +7 -0
- package/adapters/shared/resolve-slot-ports-core.mjs +6 -0
- package/adapters/shared/resolve-slot-ports.sh +7 -2
- package/bin/mm-harness +0 -7
- package/dist/adapters/extension/harness-freshness.js +39 -0
- package/dist/adapters/extension/runtime-decision.js +4 -73
- package/dist/adapters/extension/surface.js +2 -3
- package/dist/adapters/mobile/perps-env.js +3 -43
- package/dist/adapters/mobile/prepare.js +7 -22
- package/dist/adapters/mobile/source-freshness.js +26 -41
- package/dist/adapters/mobile/surface.js +1 -1
- package/dist/adapters/resolve-slot-ports.js +2 -0
- package/dist/adapters/slot-ports.js +0 -16
- package/dist/adapters.js +5 -12
- package/dist/cli-commands.js +1 -1
- package/dist/cli.js +12 -6
- package/dist/commands/call.js +22 -56
- package/dist/commands/check.js +3 -9
- package/dist/commands/checklist.js +4 -1
- package/dist/commands/debug.js +31 -38
- package/dist/commands/device-target.js +12 -27
- package/dist/commands/doctor.js +23 -77
- package/dist/commands/fixtures.js +17 -27
- package/dist/commands/launch/extension.js +2 -0
- package/dist/commands/launch/index.js +59 -167
- package/dist/commands/logs.js +6 -8
- package/dist/commands/manifest.js +25 -151
- package/dist/commands/parse-args.js +0 -2
- package/dist/commands/provision.js +3 -10
- package/dist/commands/run-engine.js +69 -71
- package/dist/commands/run-report.js +3 -12
- package/dist/commands/run.js +35 -245
- package/dist/commands/shared.js +3 -55
- package/dist/commands/status-probe.js +1 -1
- package/dist/commands/status.js +1 -1
- package/dist/commands/stop.js +2 -7
- package/dist/commands/update.js +28 -16
- package/dist/doctor.js +4 -1
- package/dist/harness.js +134 -43
- package/dist/heal-bounds.js +1 -1
- package/dist/live-adapter-contract.js +20 -6
- package/dist/mm-harness-cli.js +48 -123
- package/dist/paths.js +5 -2
- package/dist/runner.js +2 -37
- package/dist/runtime-context.js +8 -26
- package/docs/ADAPTER-SURFACE.md +112 -19
- package/docs/CHEATSHEET.md +52 -39
- package/docs/CLI-SPEC.md +1098 -0
- package/docs/CODE-MAP.md +1 -1
- package/docs/DEBUG-HANDOVER.md +36 -0
- package/docs/MENTAL-MODEL.md +295 -0
- package/docs/UX-PRINCIPLES.md +1 -3
- package/docs/architecture.md +375 -65
- package/docs/live-adapter-contract.md +2 -4
- package/docs/recipe-libraries.md +275 -45
- package/library/README.md +0 -4
- package/library/actions/core/perps/_controller.mjs +55 -10
- package/library/actions/mobile/platform/bridge.mjs +1 -5
- package/library/manifests/core.action-manifest.json +7 -7
- package/library/manifests/mobile.action-manifest.json +3 -28
- package/package.json +1 -4
- package/adapters/extension/launch-webpack.cjs +0 -83
- package/adapters/extension/stamp-runtime-title.cjs +0 -58
- package/adapters/extension/sync-webpack-dist.cjs +0 -107
- package/adapters/mobile/launch-console-forwarder.cjs +0 -39
- package/adapters/mobile/start-console-forwarder.sh +0 -70
- package/adapters/shared/update-check-worker.mjs +0 -43
- package/dist/adapters/extension/console-capture.js +0 -93
- package/dist/command-contract.js +0 -441
- package/dist/command-journal.js +0 -225
- package/dist/commands/last.js +0 -52
- package/dist/json-stream.js +0 -57
- package/dist/run-diagnostics.js +0 -261
- package/docs/CLI-ERGONOMICS-AUDIT.md +0 -32
- package/docs/CLI-ERGONOMICS-HUMAN-QA.md +0 -64
- package/docs/VIDEO-DEMO-VALIDATION.md +0 -74
- package/library/recipes/runner/smoke.core.recipe.json +0 -27
- package/library/recipes/wallet/smoke.extension.recipe.json +0 -42
- package/library/recipes/wallet/smoke.mobile.recipe.json +0 -42
- package/scripts/validate-human-outcomes.mjs +0 -169
|
@@ -1,129 +1,57 @@
|
|
|
1
1
|
import { loadActionManifest, validateManifest } from "../manifest.js";
|
|
2
2
|
import { EXIT } from "./shared.js";
|
|
3
|
-
import {
|
|
4
|
-
|
|
3
|
+
import {
|
|
4
|
+
actionManifestPathOption,
|
|
5
|
+
isRecord,
|
|
6
|
+
optionFlag,
|
|
7
|
+
optionString,
|
|
8
|
+
resolveAdapter
|
|
9
|
+
} from "./parse-args.js";
|
|
10
|
+
async function handleManifest({ options }) {
|
|
11
|
+
const { adapter } = resolveAdapter(options);
|
|
12
|
+
const actionManifestPath = actionManifestPathOption(options, adapter);
|
|
13
|
+
const manifest = loadActionManifest(adapter, optionString(options, "actionManifest"));
|
|
14
|
+
await validateManifest(manifest);
|
|
15
|
+
if (optionFlag(options, "json")) console.log(JSON.stringify(manifest, null, 2));
|
|
16
|
+
else console.log(actionManifestPath);
|
|
17
|
+
return 0;
|
|
18
|
+
}
|
|
19
|
+
async function handleActions({ options }) {
|
|
5
20
|
const { adapter } = resolveAdapter(options);
|
|
6
21
|
const manifest = loadActionManifest(adapter, optionString(options, "actionManifest"));
|
|
7
22
|
await validateManifest(manifest);
|
|
8
|
-
if (optionFlag(options, "raw")) {
|
|
9
|
-
console.log(JSON.stringify(manifest, null, 2));
|
|
10
|
-
return EXIT.ok;
|
|
11
|
-
}
|
|
12
23
|
const json = optionFlag(options, "json");
|
|
13
24
|
const action = optionString(options, "action");
|
|
14
|
-
const query = positional[0]?.trim();
|
|
15
|
-
const category = optionString(options, "category")?.toLowerCase();
|
|
16
|
-
const categoriesOnly = optionFlag(options, "categories");
|
|
17
25
|
const all = describeManifestActions(manifest);
|
|
18
|
-
const
|
|
19
|
-
if (categoriesOnly && (action || category)) {
|
|
20
|
-
const message = "--categories cannot be combined with --action or --category.";
|
|
21
|
-
const userAction = `mm-harness actions --adapter ${adapter} --categories`;
|
|
22
|
-
if (json) {
|
|
23
|
-
console.log(JSON.stringify({ schemaVersion: 1, command: "actions", adapter, error: { code: "ACTION_FILTER_CONFLICT", message, userAction } }, null, 2));
|
|
24
|
-
} else {
|
|
25
|
-
console.error(`\u2717 mm-harness actions: ${message}
|
|
26
|
-
Next: ${userAction}`);
|
|
27
|
-
}
|
|
28
|
-
return EXIT.usage;
|
|
29
|
-
}
|
|
30
|
-
if (categoriesOnly) {
|
|
31
|
-
if (json) console.log(JSON.stringify({ schemaVersion: 1, command: "actions", adapter, categories }, null, 2));
|
|
32
|
-
else for (const entry of categories) console.log(`${entry.name} (${entry.count})`);
|
|
33
|
-
return EXIT.ok;
|
|
34
|
-
}
|
|
35
|
-
const categoryActions = category ? all.filter((entry) => entry.category === category) : all;
|
|
36
|
-
if (category && categoryActions.length === 0) {
|
|
37
|
-
const message = `no action category matches "${category}" for the ${adapter} adapter.`;
|
|
38
|
-
const userAction = `mm-harness actions --adapter ${adapter} --categories`;
|
|
39
|
-
if (json) {
|
|
40
|
-
console.log(JSON.stringify({ schemaVersion: 1, command: "actions", adapter, category, availableCategories: categories, error: { code: "ACTION_CATEGORY_UNKNOWN", message, userAction } }, null, 2));
|
|
41
|
-
} else {
|
|
42
|
-
console.error(`\u2717 mm-harness actions: ${message}
|
|
43
|
-
Next: ${userAction}`);
|
|
44
|
-
}
|
|
45
|
-
return EXIT.usage;
|
|
46
|
-
}
|
|
47
|
-
const actions = action ? fuzzyResolveActions(categoryActions, action) : query ? searchActions(categoryActions, query) : categoryActions;
|
|
26
|
+
const actions = action ? fuzzyResolveActions(all, action) : all;
|
|
48
27
|
if (action && actions.length === 0) {
|
|
49
28
|
const message = `no action matches "${action}" for the ${adapter} adapter.`;
|
|
50
|
-
const userAction = `mm-harness actions --adapter ${adapter}`;
|
|
51
|
-
if (json) {
|
|
52
|
-
console.log(JSON.stringify({ schemaVersion: 1, command: "actions", adapter, action, category, error: { code: "ACTION_UNKNOWN", message, userAction } }, null, 2));
|
|
53
|
-
} else {
|
|
54
|
-
console.error(`\u2717 mm-harness actions: ${message}
|
|
55
|
-
Next: ${userAction}`);
|
|
56
|
-
}
|
|
57
|
-
return EXIT.usage;
|
|
58
|
-
}
|
|
59
|
-
if (query && actions.length === 0) {
|
|
60
|
-
const message = `no action matches search "${query}" for the ${adapter} adapter.`;
|
|
61
|
-
const userAction = `mm-harness actions --adapter ${adapter} --categories`;
|
|
29
|
+
const userAction = `mm-harness actions --adapter ${adapter} # list the vocabulary`;
|
|
62
30
|
if (json) {
|
|
63
|
-
console.log(JSON.stringify({
|
|
64
|
-
schemaVersion: 1,
|
|
65
|
-
command: "actions",
|
|
66
|
-
adapter,
|
|
67
|
-
query,
|
|
68
|
-
category,
|
|
69
|
-
error: { code: "ACTION_SEARCH_EMPTY", message, userAction }
|
|
70
|
-
}, null, 2));
|
|
31
|
+
console.log(JSON.stringify({ schemaVersion: 1, command: "actions", adapter, action, error: { code: "ACTION_UNKNOWN", message, userAction } }, null, 2));
|
|
71
32
|
} else {
|
|
72
33
|
console.error(`\u2717 mm-harness actions: ${message}
|
|
73
34
|
Next: ${userAction}`);
|
|
74
35
|
}
|
|
75
36
|
return EXIT.usage;
|
|
76
37
|
}
|
|
77
|
-
const relatedActions = action && actions.length === 1 ? findRelatedActions(all, actions[0]) : void 0;
|
|
78
38
|
if (json) {
|
|
79
|
-
console.log(JSON.stringify({
|
|
80
|
-
schemaVersion: 1,
|
|
81
|
-
command: "actions",
|
|
82
|
-
adapter,
|
|
83
|
-
...query ? { query } : {},
|
|
84
|
-
category,
|
|
85
|
-
actions,
|
|
86
|
-
...relatedActions ? { relatedActions } : {}
|
|
87
|
-
}, null, 2));
|
|
39
|
+
console.log(JSON.stringify({ adapter, actions }, null, 2));
|
|
88
40
|
} else {
|
|
89
41
|
for (const entry of actions) {
|
|
90
42
|
const fields = entry.fields.length ? ` fields=${entry.fields.join(",")}` : "";
|
|
91
43
|
console.log(`${entry.name} (${entry.kind})${fields}${entry.description ? ` \u2014 ${entry.description}` : ""}`);
|
|
92
44
|
}
|
|
93
|
-
if (relatedActions) console.log(`Related: ${relatedActions.join(", ")}`);
|
|
94
45
|
}
|
|
95
46
|
return 0;
|
|
96
47
|
}
|
|
97
48
|
function fuzzyResolveActions(entries, query) {
|
|
98
49
|
const exactFull = entries.filter((e) => e.name === query);
|
|
99
50
|
if (exactFull.length > 0) return exactFull;
|
|
100
|
-
const
|
|
101
|
-
const exactSegment = entries.filter((e) =>
|
|
51
|
+
const finalSegment = (name) => name.split(".").pop() ?? name;
|
|
52
|
+
const exactSegment = entries.filter((e) => finalSegment(e.name) === query);
|
|
102
53
|
if (exactSegment.length > 0) return exactSegment;
|
|
103
|
-
return entries.filter((e) =>
|
|
104
|
-
}
|
|
105
|
-
function searchActions(entries, query) {
|
|
106
|
-
const terms = searchTerms(query);
|
|
107
|
-
if (terms.length === 0) return [];
|
|
108
|
-
return entries.map((entry) => {
|
|
109
|
-
const scores = terms.map((term) => actionSearchScore(entry, term));
|
|
110
|
-
return { entry, scores, score: scores.reduce((total, value) => total + value, 0) };
|
|
111
|
-
}).filter(({ score, scores }) => score > 0 && scores.every((value) => value > 0)).sort((left, right) => right.score - left.score || left.entry.name.localeCompare(right.entry.name)).map(({ entry }) => entry);
|
|
112
|
-
}
|
|
113
|
-
function findRelatedActions(entries, selected, limit = 5) {
|
|
114
|
-
const selectedNameTerms = searchTerms(finalSegment(selected.name).replaceAll("_", " "));
|
|
115
|
-
return entries.filter((entry) => entry.name !== selected.name).map((entry) => {
|
|
116
|
-
const nameTerms = new Set(searchTerms(finalSegment(entry.name).replaceAll("_", " ")));
|
|
117
|
-
const sharedScore = selectedNameTerms.filter((term) => nameTerms.has(term)).reduce((score2, term) => score2 + (GENERIC_OPERATION_TERMS.has(term) ? 5 : 30), 0);
|
|
118
|
-
const score = (entry.category === selected.category ? 100 : 0) + sharedScore;
|
|
119
|
-
return { name: entry.name, score };
|
|
120
|
-
}).filter(({ score }) => score > 0).sort((left, right) => right.score - left.score || left.name.localeCompare(right.name)).slice(0, limit).map(({ name }) => name);
|
|
121
|
-
}
|
|
122
|
-
const GENERIC_OPERATION_TERMS = /* @__PURE__ */ new Set(["assert", "call", "close", "ensure", "place", "read", "start", "teardown"]);
|
|
123
|
-
function summarizeActionCategories(actions) {
|
|
124
|
-
const counts = /* @__PURE__ */ new Map();
|
|
125
|
-
for (const action of actions) counts.set(action.category, (counts.get(action.category) ?? 0) + 1);
|
|
126
|
-
return [...counts.entries()].map(([name, count]) => ({ name, count })).sort((left, right) => left.name.localeCompare(right.name));
|
|
54
|
+
return entries.filter((e) => finalSegment(e.name).includes(query));
|
|
127
55
|
}
|
|
128
56
|
function describeManifestActions(manifest) {
|
|
129
57
|
const manifestRecord = isRecord(manifest) ? manifest : {};
|
|
@@ -152,69 +80,15 @@ function describeManifestAction(name, kind, metadata) {
|
|
|
152
80
|
return {
|
|
153
81
|
name,
|
|
154
82
|
kind,
|
|
155
|
-
category: actionCategory(name, record.category),
|
|
156
83
|
description: typeof record.description === "string" ? record.description : "",
|
|
157
84
|
fields: properties,
|
|
158
85
|
schema,
|
|
159
86
|
examples: record.examples
|
|
160
87
|
};
|
|
161
88
|
}
|
|
162
|
-
function actionCategory(name, configured) {
|
|
163
|
-
if (typeof configured === "string" && configured.trim()) return configured.trim().toLowerCase();
|
|
164
|
-
const segments = name.split(".");
|
|
165
|
-
if (segments[0] === "metamask" && segments.length > 2) return segments[1] ?? "metamask";
|
|
166
|
-
if (segments[0] === "app" || segments[0] === "cdp") return "runtime";
|
|
167
|
-
if (segments[0] === "ui") return "ui";
|
|
168
|
-
if (name.startsWith("assert_")) return "assertion";
|
|
169
|
-
if (name === "watch_logs" || name === "index_artifacts") return "evidence";
|
|
170
|
-
if (name === "command" || name === "wait" || name === "call" || name === "end") return "control";
|
|
171
|
-
return segments.length > 1 ? segments[0] || "utility" : "utility";
|
|
172
|
-
}
|
|
173
|
-
function actionSearchScore(entry, term) {
|
|
174
|
-
const name = entry.name.toLowerCase();
|
|
175
|
-
const segment = finalSegment(name);
|
|
176
|
-
const nameTerms = searchTerms(name.replaceAll(".", " ").replaceAll("_", " "));
|
|
177
|
-
const fields = entry.fields.map((field) => field.toLowerCase());
|
|
178
|
-
const description = entry.description.toLowerCase();
|
|
179
|
-
if (name === term) return 120;
|
|
180
|
-
if (segment === term) return 110;
|
|
181
|
-
if (entry.category === term) return 100;
|
|
182
|
-
if (fields.includes(term)) return 90;
|
|
183
|
-
if (nameTerms.includes(term)) return 80;
|
|
184
|
-
if (name.includes(term)) return 70;
|
|
185
|
-
if (fields.some((field) => field.includes(term))) return 60;
|
|
186
|
-
if (description.includes(term)) return 50;
|
|
187
|
-
if ([...nameTerms, ...fields].some((candidate) => fuzzyTermMatch(term, candidate))) return 40;
|
|
188
|
-
return 0;
|
|
189
|
-
}
|
|
190
|
-
function fuzzyTermMatch(term, candidate) {
|
|
191
|
-
if (term.length < 4 || candidate.length < 4) return false;
|
|
192
|
-
const threshold = Math.max(1, Math.floor(Math.max(term.length, candidate.length) / 4));
|
|
193
|
-
return levenshtein(term, candidate) <= threshold;
|
|
194
|
-
}
|
|
195
|
-
function searchTerms(value) {
|
|
196
|
-
return value.toLowerCase().match(/[a-z0-9]+/gu) ?? [];
|
|
197
|
-
}
|
|
198
|
-
function finalSegment(name) {
|
|
199
|
-
return name.split(".").pop() ?? name;
|
|
200
|
-
}
|
|
201
|
-
function levenshtein(left, right) {
|
|
202
|
-
const prior = Array.from({ length: right.length + 1 }, (_, index) => index);
|
|
203
|
-
for (let leftIndex = 1; leftIndex <= left.length; leftIndex += 1) {
|
|
204
|
-
const current = [leftIndex];
|
|
205
|
-
for (let rightIndex = 1; rightIndex <= right.length; rightIndex += 1) {
|
|
206
|
-
const substitution = (prior[rightIndex - 1] ?? 0) + (left[leftIndex - 1] === right[rightIndex - 1] ? 0 : 1);
|
|
207
|
-
current[rightIndex] = Math.min((current[rightIndex - 1] ?? 0) + 1, (prior[rightIndex] ?? 0) + 1, substitution);
|
|
208
|
-
}
|
|
209
|
-
for (let index = 0; index < current.length; index += 1) prior[index] = current[index] ?? 0;
|
|
210
|
-
}
|
|
211
|
-
return prior[right.length] ?? right.length;
|
|
212
|
-
}
|
|
213
89
|
export {
|
|
214
90
|
describeManifestActions,
|
|
215
|
-
findRelatedActions,
|
|
216
91
|
fuzzyResolveActions,
|
|
217
92
|
handleActions,
|
|
218
|
-
|
|
219
|
-
summarizeActionCategories
|
|
93
|
+
handleManifest
|
|
220
94
|
};
|
|
@@ -19,14 +19,12 @@ function parseArgs(argv, command) {
|
|
|
19
19
|
const options = {};
|
|
20
20
|
const booleanOptions = /* @__PURE__ */ new Set([
|
|
21
21
|
"json",
|
|
22
|
-
"jsonStream",
|
|
23
22
|
"launchExistingDist",
|
|
24
23
|
"startWatch",
|
|
25
24
|
"record",
|
|
26
25
|
"plan",
|
|
27
26
|
"list",
|
|
28
27
|
"raw",
|
|
29
|
-
"categories",
|
|
30
28
|
"fix",
|
|
31
29
|
"force",
|
|
32
30
|
"resolveOnly",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { detectAdapter } from "../harness.js";
|
|
2
2
|
import { assertAdapter } from "../paths.js";
|
|
3
3
|
import { getAdapterSurface } from "../adapters/surface.js";
|
|
4
|
-
import { ADAPTER_DETECT_NEXT, usageOut
|
|
4
|
+
import { ADAPTER_DETECT_NEXT, usageOut } from "./shared.js";
|
|
5
5
|
import {
|
|
6
6
|
applyWatcherPortOption,
|
|
7
7
|
optionFlag,
|
|
@@ -25,13 +25,9 @@ async function handleProvision({ positional, options, rawArgv }) {
|
|
|
25
25
|
}
|
|
26
26
|
const surface = getAdapterSurface(adapter);
|
|
27
27
|
const rerunCommand = provisionRerunCommand(rawArgv, options, adapter, target);
|
|
28
|
-
const platform = optionString(options, "platform") ?? optionString(options, "devicePlatform") ?? (positional[0] === "runway" ? positional[1] : positional[0]) ?? "ios";
|
|
29
|
-
if (adapter === "mobile") {
|
|
30
|
-
writeInteractiveProgress(json, `\u2192 provision mobile ${platform} \u2014 resolving simulator and Runway artifact`);
|
|
31
|
-
}
|
|
32
28
|
const result = await surface.runwayProvision.run(target, {
|
|
33
29
|
json,
|
|
34
|
-
platform,
|
|
30
|
+
platform: optionString(options, "platform") ?? optionString(options, "devicePlatform") ?? (positional[0] === "runway" ? positional[1] : positional[0]) ?? "ios",
|
|
35
31
|
branch: optionString(options, "branch"),
|
|
36
32
|
defaultBranch: optionString(options, "defaultBranch"),
|
|
37
33
|
run: optionString(options, "run"),
|
|
@@ -46,10 +42,8 @@ async function handleProvision({ positional, options, rawArgv }) {
|
|
|
46
42
|
resolveOnly: optionFlag(options, "resolveOnly"),
|
|
47
43
|
rerunCommand
|
|
48
44
|
});
|
|
49
|
-
const next = result.status === "pass" && adapter === "mobile" && result.resolveOnly !== true ? `mm-harness launch ${String(result.platform ?? platform)} --adapter mobile --target ${shellQuote(target)}` : void 0;
|
|
50
|
-
const output = next ? { ...result, next } : result;
|
|
51
45
|
if (json) {
|
|
52
|
-
console.log(JSON.stringify(
|
|
46
|
+
console.log(JSON.stringify(result, null, 2));
|
|
53
47
|
} else if (result.status === "pass") {
|
|
54
48
|
const cache = typeof result.cache === "object" && result.cache ? result.cache : void 0;
|
|
55
49
|
const simulator = typeof result.simulator === "object" && result.simulator ? result.simulator : void 0;
|
|
@@ -60,7 +54,6 @@ async function handleProvision({ positional, options, rawArgv }) {
|
|
|
60
54
|
const action = result.skipped ? "already provisioned" : "provisioned";
|
|
61
55
|
console.error(`\u2713 ${action} ${adapter} ${result.platform ?? ""} simulator=${simulator?.name ?? "unknown"} cache=${cache?.status ?? "unknown"}`);
|
|
62
56
|
}
|
|
63
|
-
if (next) console.error(` Next: ${next}`);
|
|
64
57
|
} else {
|
|
65
58
|
console.error(`\u2717 mm-harness provision: ${result.error?.message ?? "provision failed"}
|
|
66
59
|
Next: ${result.error?.userAction ?? rerunCommand}`);
|
|
@@ -20,17 +20,12 @@ import {
|
|
|
20
20
|
import { captureHelperSupportsRecordSessionSnapshots } from "../recording-target.js";
|
|
21
21
|
import { listRecipeFiles } from "../recipe-files.js";
|
|
22
22
|
import { startRecipeRecording, stopRecipeRecording } from "../run-recording.js";
|
|
23
|
-
import {
|
|
24
|
-
beginRunDiagnostics,
|
|
25
|
-
finishRunDiagnostics
|
|
26
|
-
} from "../run-diagnostics.js";
|
|
27
|
-
import { EXIT } from "./shared.js";
|
|
23
|
+
import { EXIT, spawnScriptStreaming } from "./shared.js";
|
|
28
24
|
import {
|
|
29
25
|
actionManifestPathOption,
|
|
30
26
|
optionString,
|
|
31
27
|
isRecord,
|
|
32
28
|
shellQuoteArg,
|
|
33
|
-
targetPath,
|
|
34
29
|
usageError
|
|
35
30
|
} from "./parse-args.js";
|
|
36
31
|
async function runRecipe(adapter, recipe, artifactsDir, projectRoot, actionManifestPath, runtimeOptions = {}) {
|
|
@@ -54,7 +49,6 @@ async function runRecipe(adapter, recipe, artifactsDir, projectRoot, actionManif
|
|
|
54
49
|
try {
|
|
55
50
|
await prepareRuntimeIfNeeded(adapter, projectRoot, runtimeOptions);
|
|
56
51
|
const absoluteArtifactsDir = path.resolve(artifactsDir);
|
|
57
|
-
const diagnosticBaseline = await beginRunDiagnostics(adapter, projectRoot);
|
|
58
52
|
const recordVideo = runtimeOptions.recordVideo ?? false;
|
|
59
53
|
const useFramedExtensionRecording = adapter === "extension" && recordVideo === "full-run" && captureHelperSupportsRecordSessionSnapshots(projectRoot);
|
|
60
54
|
const recording = useFramedExtensionRecording ? await startRecipeRecording(adapter, projectRoot, absoluteArtifactsDir, {
|
|
@@ -66,8 +60,7 @@ async function runRecipe(adapter, recipe, artifactsDir, projectRoot, actionManif
|
|
|
66
60
|
await validateManifest(manifest);
|
|
67
61
|
const { createMetaMaskRunner } = await import("../runner.js");
|
|
68
62
|
const runner = await createMetaMaskRunner(adapter, manifest, {
|
|
69
|
-
quietStdout: runtimeOptions.stdoutIsMachineContract === true
|
|
70
|
-
onActionEvent: runtimeOptions.onActionEvent
|
|
63
|
+
quietStdout: runtimeOptions.stdoutIsMachineContract === true
|
|
71
64
|
});
|
|
72
65
|
const runRequest = {
|
|
73
66
|
recipePath: path.resolve(recipe),
|
|
@@ -87,7 +80,7 @@ async function runRecipe(adapter, recipe, artifactsDir, projectRoot, actionManif
|
|
|
87
80
|
}
|
|
88
81
|
}
|
|
89
82
|
await stopRecipeRecording(recording, result);
|
|
90
|
-
return
|
|
83
|
+
return result;
|
|
91
84
|
} finally {
|
|
92
85
|
await stopRecipeRecording(recording);
|
|
93
86
|
}
|
|
@@ -198,7 +191,12 @@ function libraryRecipeNames(adapter) {
|
|
|
198
191
|
return [...new Set(names)].sort();
|
|
199
192
|
}
|
|
200
193
|
function recipeNameCandidates(recipeArg, adapter) {
|
|
201
|
-
|
|
194
|
+
const names = [recipeArg, ...LEGACY_RECIPE_ALIASES[recipeArg] ?? []];
|
|
195
|
+
const candidates = [];
|
|
196
|
+
for (const name of names) {
|
|
197
|
+
candidates.push(...singleRecipeNameCandidates(name, adapter));
|
|
198
|
+
}
|
|
199
|
+
return [...new Set(candidates)];
|
|
202
200
|
}
|
|
203
201
|
function singleRecipeNameCandidates(name, adapter) {
|
|
204
202
|
if (name.endsWith(".recipe.json")) {
|
|
@@ -224,6 +222,32 @@ function domainPathForRecipeName(name) {
|
|
|
224
222
|
function uniqueStrings(values) {
|
|
225
223
|
return [...new Set(values)];
|
|
226
224
|
}
|
|
225
|
+
const LEGACY_RECIPE_ALIASES = {
|
|
226
|
+
"action-validation": ["runner.action-validation"],
|
|
227
|
+
"action-validation.extension.recipe.json": ["runner.action-validation.extension.recipe.json"],
|
|
228
|
+
"action-validation.mobile.recipe.json": ["runner.action-validation.mobile.recipe.json"],
|
|
229
|
+
"app-lifecycle-android-smoke": ["app.lifecycle.android-smoke"],
|
|
230
|
+
"app-lifecycle-android-smoke.mobile.recipe.json": ["app.lifecycle.android-smoke.mobile.recipe.json"],
|
|
231
|
+
"order-lifecycle": ["perps.order-lifecycle"],
|
|
232
|
+
"order-lifecycle.core.recipe.json": ["perps.order-lifecycle.core.recipe.json"],
|
|
233
|
+
"perps-lifecycle": ["perps.lifecycle"],
|
|
234
|
+
"perps-lifecycle.recipe.json": ["perps.lifecycle.recipe.json"],
|
|
235
|
+
"perps-performance": ["perps.performance"],
|
|
236
|
+
"perps-performance-background-resume": ["perps.performance.background-resume"],
|
|
237
|
+
"perps-performance-background-resume.mobile.recipe.json": ["perps.performance.background-resume.mobile.recipe.json"],
|
|
238
|
+
"perps-performance-cold-start": ["perps.performance.cold-start"],
|
|
239
|
+
"perps-performance-cold-start.mobile.recipe.json": ["perps.performance.cold-start.mobile.recipe.json"],
|
|
240
|
+
"perps-performance-warm-start": ["perps.performance.warm-start"],
|
|
241
|
+
"perps-performance-warm-start.mobile.recipe.json": ["perps.performance.warm-start.mobile.recipe.json"],
|
|
242
|
+
"perps-performance.mobile.recipe.json": ["perps.performance.mobile.recipe.json"],
|
|
243
|
+
"read-markets": ["perps.read-markets"],
|
|
244
|
+
"read-markets.core.recipe.json": ["perps.read-markets.core.recipe.json"],
|
|
245
|
+
"smoke": ["runner.smoke"],
|
|
246
|
+
"smoke.extension.recipe.json": ["runner.smoke.extension.recipe.json"],
|
|
247
|
+
"smoke.mobile.recipe.json": ["runner.smoke.mobile.recipe.json"],
|
|
248
|
+
"trading-lifecycle": ["perps.trading-lifecycle"],
|
|
249
|
+
"trading-lifecycle.core.recipe.json": ["perps.trading-lifecycle.core.recipe.json"]
|
|
250
|
+
};
|
|
227
251
|
async function validateRunRecipeStatic(recipeArg, adapter, options) {
|
|
228
252
|
const librarySources = await resolveMetaMaskLibrarySources(optionString(options, "library"));
|
|
229
253
|
const resolved = resolveRunRecipeArg(recipeArg, adapter, librarySources);
|
|
@@ -317,7 +341,6 @@ async function actionInsteadOfRecipeHint(recipeArg, adapter, options) {
|
|
|
317
341
|
const device = optionString(options, "device");
|
|
318
342
|
const actionManifest = optionString(options, "actionManifest");
|
|
319
343
|
const parts = ["mm-harness", "call", matches[0]];
|
|
320
|
-
parts.push("--adapter", adapter, "--target", shellQuoteArg(targetPath(options)));
|
|
321
344
|
if (device) parts.push("--device", shellQuoteArg(device));
|
|
322
345
|
if (actionManifest) parts.push("--action-manifest", shellQuoteArg(actionManifest));
|
|
323
346
|
return ` This is an action, not a recipe. Use: ${parts.join(" ")}.`;
|
|
@@ -375,12 +398,10 @@ async function runOneNode(adapter, action, args, target, actionManifest) {
|
|
|
375
398
|
async function prepareHeal(adapter, target, options, json, prepareOptions = {}) {
|
|
376
399
|
if (recipeRunning(target)) {
|
|
377
400
|
const msg = "a recipe is currently running \u2014 refusing to start while another recipe executes.";
|
|
378
|
-
const userAction = `inspect the checkout state with: mm-harness status --target ${shellQuoteArg(target)} --json; retry after the active recipe finishes`;
|
|
379
401
|
if (json) {
|
|
380
|
-
console.log(JSON.stringify({ schemaVersion: 1, status: "fail", recoverable: false, error: { code: "RECIPE_RUNNING", message: msg
|
|
402
|
+
console.log(JSON.stringify({ schemaVersion: 1, status: "fail", recoverable: false, error: { code: "RECIPE_RUNNING", message: msg } }, null, 2));
|
|
381
403
|
} else {
|
|
382
|
-
console.error(`\u2717 mm-harness: ${msg}
|
|
383
|
-
Next: ${userAction}`);
|
|
404
|
+
console.error(`\u2717 mm-harness: ${msg}`);
|
|
384
405
|
}
|
|
385
406
|
return EXIT.bounded;
|
|
386
407
|
}
|
|
@@ -393,25 +414,21 @@ async function prepareHeal(adapter, target, options, json, prepareOptions = {})
|
|
|
393
414
|
return EXIT.usage;
|
|
394
415
|
}
|
|
395
416
|
const state = newHealState();
|
|
396
|
-
prepareOptions.onPhase?.("install");
|
|
397
417
|
const ensured = await ensureOverlay(adapter, target, heal, state, json);
|
|
398
418
|
if (!ensured.ok) {
|
|
399
|
-
const userAction = `mm-harness install --adapter ${adapter} --target ${shellQuoteArg(target)}`;
|
|
400
419
|
if (json) {
|
|
401
420
|
console.log(
|
|
402
421
|
JSON.stringify(
|
|
403
|
-
{ schemaVersion: 1, status: "fail", recoverable: false, mutations: state.mutations, error: { code: "OVERLAY_INSTALL_FAILED", message: ensured.error
|
|
422
|
+
{ schemaVersion: 1, status: "fail", recoverable: false, mutations: state.mutations, error: { code: "OVERLAY_INSTALL_FAILED", message: ensured.error } },
|
|
404
423
|
null,
|
|
405
424
|
2
|
|
406
425
|
)
|
|
407
426
|
);
|
|
408
427
|
} else {
|
|
409
|
-
console.error(`\u2717 overlay auto-ensure failed: ${ensured.error}
|
|
410
|
-
Next: ${userAction}`);
|
|
428
|
+
console.error(`\u2717 overlay auto-ensure failed: ${ensured.error}`);
|
|
411
429
|
}
|
|
412
430
|
return EXIT.infra;
|
|
413
431
|
}
|
|
414
|
-
prepareOptions.onPhase?.("healthcheck");
|
|
415
432
|
if (adapter === "extension") {
|
|
416
433
|
const previousCdpPort = process.env.CDP_PORT;
|
|
417
434
|
const previousRecipeCdpPort = process.env.RECIPE_CDP_PORT;
|
|
@@ -424,9 +441,7 @@ async function prepareHeal(adapter, target, options, json, prepareOptions = {})
|
|
|
424
441
|
}
|
|
425
442
|
if (explicitWatcherPort) process.env.WATCHER_PORT = explicitWatcherPort;
|
|
426
443
|
try {
|
|
427
|
-
const current = await ensureExtensionProofRuntime(target, heal, state, json
|
|
428
|
-
onRecovery: (code) => prepareOptions.onPhase?.("recover", { code })
|
|
429
|
-
});
|
|
444
|
+
const current = await ensureExtensionProofRuntime(target, heal, state, json);
|
|
430
445
|
if (current !== null) return current;
|
|
431
446
|
} finally {
|
|
432
447
|
restoreEnv("CDP_PORT", previousCdpPort);
|
|
@@ -435,9 +450,7 @@ async function prepareHeal(adapter, target, options, json, prepareOptions = {})
|
|
|
435
450
|
}
|
|
436
451
|
}
|
|
437
452
|
if (adapter === "mobile" && prepareOptions.skipMobileSourceFreshness !== true) {
|
|
438
|
-
const current = await ensureMobileProofRuntime(target, heal, state, json
|
|
439
|
-
onRecovery: (code) => prepareOptions.onPhase?.("recover", { code })
|
|
440
|
-
});
|
|
453
|
+
const current = await ensureMobileProofRuntime(target, heal, state, json);
|
|
441
454
|
if (current !== null) return current;
|
|
442
455
|
}
|
|
443
456
|
return { state, heal };
|
|
@@ -447,20 +460,6 @@ async function ensureMobileProofRuntime(target, heal, state, json, deps = {}) {
|
|
|
447
460
|
const check = deps.check ?? (() => source.mobileSourceCheck(target));
|
|
448
461
|
const record = deps.record ?? ((fingerprint) => source.recordMobileSourceBaseline(target, fingerprint));
|
|
449
462
|
const initial = check();
|
|
450
|
-
if (initial.status === "unavailable") {
|
|
451
|
-
if (json) {
|
|
452
|
-
console.log(JSON.stringify({
|
|
453
|
-
schemaVersion: 1,
|
|
454
|
-
status: "fail",
|
|
455
|
-
recoverable: false,
|
|
456
|
-
error: initial.error
|
|
457
|
-
}, null, 2));
|
|
458
|
-
} else {
|
|
459
|
-
console.error(`\u2717 ${initial.error.message}`);
|
|
460
|
-
console.error(` Next: ${initial.error.userAction}`);
|
|
461
|
-
}
|
|
462
|
-
return EXIT.usage;
|
|
463
|
-
}
|
|
464
463
|
if (initial.status === "current") return null;
|
|
465
464
|
const targetArg = JSON.stringify(path.resolve(target));
|
|
466
465
|
const userAction = `mm-harness call app.lifecycle --arg command=restart --adapter mobile --target ${targetArg}`;
|
|
@@ -482,7 +481,6 @@ async function ensureMobileProofRuntime(target, heal, state, json, deps = {}) {
|
|
|
482
481
|
}
|
|
483
482
|
const recoveryCode = "mobile.source_reloaded";
|
|
484
483
|
state.attemptedRecoveries.push(recoveryCode);
|
|
485
|
-
deps.onRecovery?.(recoveryCode);
|
|
486
484
|
if (!json) console.error(`\u2192 Mobile proof preflight: source ${initial.status}; restarting the app before execution`);
|
|
487
485
|
const restart = deps.restart ?? (async () => {
|
|
488
486
|
const result2 = await runOneNode(
|
|
@@ -495,21 +493,27 @@ async function ensureMobileProofRuntime(target, heal, state, json, deps = {}) {
|
|
|
495
493
|
return { status: result2.status === "pass" ? 0 : EXIT.runtime, output: result2.status };
|
|
496
494
|
});
|
|
497
495
|
const result = await restart();
|
|
498
|
-
const
|
|
496
|
+
const platform = resolveRunMobilePlatform();
|
|
497
|
+
const bridge = result.status === 0 ? await (deps.waitForBridge ? deps.waitForBridge() : spawnScriptStreaming(
|
|
498
|
+
path.join(runnerDir, "adapters/mobile/wait-for-bridge.sh"),
|
|
499
|
+
["--target", target, "--platform", platform],
|
|
500
|
+
target
|
|
501
|
+
)) : { status: result.status, output: result.output };
|
|
502
|
+
const recorded = result.status === 0 && bridge.status === 0 && record(initial.fingerprint);
|
|
499
503
|
const verified = recorded ? check() : initial;
|
|
500
|
-
if (result.status !== 0 || !recorded || verified.status !== "current") {
|
|
501
|
-
const message = "Mobile source restart did not produce a current loaded-source baseline.";
|
|
504
|
+
if (result.status !== 0 || bridge.status !== 0 || !recorded || verified.status !== "current") {
|
|
505
|
+
const message = bridge.status !== 0 ? "Mobile source restart completed but the platform-matched bridge did not become ready." : "Mobile source restart did not produce a current loaded-source baseline.";
|
|
502
506
|
if (json) {
|
|
503
507
|
console.log(JSON.stringify({
|
|
504
508
|
schemaVersion: 1,
|
|
505
509
|
status: "fail",
|
|
506
510
|
recoverable: false,
|
|
507
511
|
attemptedRecoveries: state.attemptedRecoveries,
|
|
508
|
-
error: { code: "MOBILE_SOURCE_RELOAD_FAILED", message, userAction, originalError: result.output }
|
|
512
|
+
error: { code: bridge.status !== 0 ? "MOBILE_BRIDGE_NOT_READY" : "MOBILE_SOURCE_RELOAD_FAILED", message, userAction, originalError: bridge.output || result.output }
|
|
509
513
|
}, null, 2));
|
|
510
514
|
} else {
|
|
511
515
|
console.error(`\u2717 ${message}`);
|
|
512
|
-
console.error(` Next: ${userAction}`);
|
|
516
|
+
console.error(` Next: ${bridge.status !== 0 ? "inspect the bridge-status log and rerun mm-harness launch <platform>" : userAction}`);
|
|
513
517
|
}
|
|
514
518
|
return EXIT.infra;
|
|
515
519
|
}
|
|
@@ -528,22 +532,6 @@ async function ensureExtensionProofRuntime(target, heal, state, json, deps = {})
|
|
|
528
532
|
});
|
|
529
533
|
const initial = await decide();
|
|
530
534
|
if (initial.decision === "ready") return null;
|
|
531
|
-
if (initial.reasonCode === "runtime-dist-check-failed") {
|
|
532
|
-
const message = `Extension proof runtime freshness could not be checked: ${initial.reasons[0] ?? initial.decision}`;
|
|
533
|
-
const userAction2 = `mm-harness doctor --adapter extension --target ${JSON.stringify(path.resolve(target))}`;
|
|
534
|
-
if (json) {
|
|
535
|
-
console.log(JSON.stringify({
|
|
536
|
-
schemaVersion: 1,
|
|
537
|
-
status: "fail",
|
|
538
|
-
recoverable: false,
|
|
539
|
-
error: { code: "EXTENSION_RUNTIME_CHECK_FAILED", message, userAction: userAction2 }
|
|
540
|
-
}, null, 2));
|
|
541
|
-
} else {
|
|
542
|
-
console.error(`\u2717 ${message}`);
|
|
543
|
-
console.error(` Next: ${userAction2}`);
|
|
544
|
-
}
|
|
545
|
-
return EXIT.infra;
|
|
546
|
-
}
|
|
547
535
|
if (!["dist-stale", "dist-missing", "runtime-dist-stale", "runtime-dist-missing"].includes(initial.reasonCode)) {
|
|
548
536
|
return null;
|
|
549
537
|
}
|
|
@@ -569,14 +557,19 @@ async function ensureExtensionProofRuntime(target, heal, state, json, deps = {})
|
|
|
569
557
|
}
|
|
570
558
|
const recoveryCode = tier === "build" ? "extension.runtime_rebuilt" : "extension.runtime_reloaded";
|
|
571
559
|
state.attemptedRecoveries.push(recoveryCode);
|
|
572
|
-
deps.onRecovery?.(recoveryCode);
|
|
573
560
|
if (!json) console.error(`\u2192 Extension proof preflight: ${initial.reasonCode}; ${tier === "build" ? "rebuilding" : "reloading"} before execution`);
|
|
574
561
|
const launch = deps.launch ?? (async (requestedTier) => {
|
|
575
562
|
const { launchExtension } = await import("./launch/extension.js");
|
|
576
563
|
return launchExtension(target, requestedTier, false);
|
|
577
564
|
});
|
|
578
565
|
const result = await launch(tier);
|
|
579
|
-
|
|
566
|
+
let verified = result.status === 0 ? await decide() : initial;
|
|
567
|
+
if (result.status === 0 && verified.decision !== "ready") {
|
|
568
|
+
for (let attempt = 0; attempt < 8 && verified.decision !== "ready"; attempt += 1) {
|
|
569
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
570
|
+
verified = await decide();
|
|
571
|
+
}
|
|
572
|
+
}
|
|
580
573
|
if (result.status !== 0 || verified.decision !== "ready") {
|
|
581
574
|
const message = `Extension runtime recovery did not produce a current healthy runtime (${verified.reasonCode}).`;
|
|
582
575
|
if (json) {
|
|
@@ -610,11 +603,17 @@ async function recoverRunInfra(adapter, target, json) {
|
|
|
610
603
|
}
|
|
611
604
|
if (adapter === "mobile") {
|
|
612
605
|
const { launchMobile } = await import("./launch/mobile.js");
|
|
613
|
-
const platform =
|
|
606
|
+
const platform = resolveRunMobilePlatform();
|
|
614
607
|
return launchMobile(target, platform, "quick", json);
|
|
615
608
|
}
|
|
616
609
|
return { status: 0, output: "headless run retry" };
|
|
617
610
|
}
|
|
611
|
+
function resolveRunMobilePlatform() {
|
|
612
|
+
if (process.env.PLATFORM === "android" || process.env.PLATFORM === "ios") {
|
|
613
|
+
return process.env.PLATFORM;
|
|
614
|
+
}
|
|
615
|
+
return process.env.ADB_SERIAL || process.env.ANDROID_SERIAL ? "android" : "ios";
|
|
616
|
+
}
|
|
618
617
|
function readRunFailureText(result) {
|
|
619
618
|
try {
|
|
620
619
|
const trace = JSON.parse(fs.readFileSync(result.tracePath, "utf8"));
|
|
@@ -624,7 +623,7 @@ function readRunFailureText(result) {
|
|
|
624
623
|
return "";
|
|
625
624
|
}
|
|
626
625
|
}
|
|
627
|
-
async function executeWithHealBounds(exec, adapter, target, heal, state, recover = () => recoverRunInfra(adapter, target, false)
|
|
626
|
+
async function executeWithHealBounds(exec, adapter, target, heal, state, recover = () => recoverRunInfra(adapter, target, false)) {
|
|
628
627
|
let result = await exec();
|
|
629
628
|
for (; ; ) {
|
|
630
629
|
if (result.status === "pass") return { result, violation: null };
|
|
@@ -633,7 +632,6 @@ async function executeWithHealBounds(exec, adapter, target, heal, state, recover
|
|
|
633
632
|
if (heal === "off") return { result, violation: null };
|
|
634
633
|
const recoveryCode = RUN_RECOVERY_CODE[adapter];
|
|
635
634
|
state.attemptedRecoveries.push(recoveryCode);
|
|
636
|
-
onRecovery(recoveryCode);
|
|
637
635
|
await recover();
|
|
638
636
|
result = await exec();
|
|
639
637
|
if (result.status === "pass") {
|
|
@@ -643,7 +641,7 @@ async function executeWithHealBounds(exec, adapter, target, heal, state, recover
|
|
|
643
641
|
}
|
|
644
642
|
}
|
|
645
643
|
function emitHealViolation(json, command, result, violation, state, adapter) {
|
|
646
|
-
const userAction = adapter === "core" && violation.code === "WALLET_STATE_REQUIRED" ? '
|
|
644
|
+
const userAction = adapter === "core" && violation.code === "WALLET_STATE_REQUIRED" ? 'set MM_TEST_ACCOUNT_ADDRESS=<0x\u2026> in env, or add "account": "<0x\u2026>" to the node block in the recipe' : violation.userAction;
|
|
647
645
|
if (json) {
|
|
648
646
|
console.log(
|
|
649
647
|
JSON.stringify(
|
|
@@ -663,7 +661,7 @@ function emitHealViolation(json, command, result, violation, state, adapter) {
|
|
|
663
661
|
code: violation.code,
|
|
664
662
|
message: violation.message,
|
|
665
663
|
retryable: false,
|
|
666
|
-
userAction,
|
|
664
|
+
userAction: userAction ?? null,
|
|
667
665
|
originalError: violation.originalError ?? null
|
|
668
666
|
}
|
|
669
667
|
},
|
|
@@ -25,19 +25,10 @@ function renderRunReport(summary, entries) {
|
|
|
25
25
|
"",
|
|
26
26
|
`Status: ${String(summary.status ?? "unknown")}`,
|
|
27
27
|
`Duration: ${formatDuration(Number(summary.durationMs ?? 0))}`,
|
|
28
|
-
`Nodes: ${Number(summary.passed ?? 0)}/${Number(summary.total ?? entries.length)} passed
|
|
28
|
+
`Nodes: ${Number(summary.passed ?? 0)}/${Number(summary.total ?? entries.length)} passed`,
|
|
29
|
+
"",
|
|
30
|
+
"## Steps"
|
|
29
31
|
];
|
|
30
|
-
const sideFindings = isRecord(summary.sideFindings) ? summary.sideFindings : void 0;
|
|
31
|
-
const sideFindingCounts = sideFindings && isRecord(sideFindings.counts) ? sideFindings.counts : void 0;
|
|
32
|
-
const sideFindingTotal = Number(sideFindingCounts?.total ?? 0);
|
|
33
|
-
if (sideFindingTotal > 0) {
|
|
34
|
-
lines.push(
|
|
35
|
-
"",
|
|
36
|
-
"## Side findings",
|
|
37
|
-
`- REVIEW ${sideFindingTotal} distinct application warning/error event(s) (non-blocking; expanded below and stored in diagnostics.json)`
|
|
38
|
-
);
|
|
39
|
-
}
|
|
40
|
-
lines.push("", "## Steps");
|
|
41
32
|
for (const entry of entries) {
|
|
42
33
|
if (!isRecord(entry)) continue;
|
|
43
34
|
const mark = entry.ok === false ? "FAIL" : "PASS";
|