@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
package/dist/commands/last.js
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { readCommandJournal } from "../command-journal.js";
|
|
2
|
-
import { EXIT } from "./shared.js";
|
|
3
|
-
import {
|
|
4
|
-
optionFlag,
|
|
5
|
-
optionString,
|
|
6
|
-
targetPath
|
|
7
|
-
} from "./parse-args.js";
|
|
8
|
-
async function handleLast({ options }) {
|
|
9
|
-
const target = targetPath(options);
|
|
10
|
-
const { file, record } = readCommandJournal(target, optionString(options, "runtimeDir"));
|
|
11
|
-
const json = optionFlag(options, "json");
|
|
12
|
-
if (!record) {
|
|
13
|
-
const message = `no resumability journal exists for ${target}`;
|
|
14
|
-
const userAction = "run a proof or runtime command in this checkout, then re-run mm-harness last --json";
|
|
15
|
-
if (json) {
|
|
16
|
-
console.log(JSON.stringify({
|
|
17
|
-
schemaVersion: 1,
|
|
18
|
-
command: "last",
|
|
19
|
-
target,
|
|
20
|
-
journalPath: file,
|
|
21
|
-
status: "fail",
|
|
22
|
-
exitCode: EXIT.runtime,
|
|
23
|
-
error: { code: "LAST_NOT_FOUND", message, userAction }
|
|
24
|
-
}, null, 2));
|
|
25
|
-
} else {
|
|
26
|
-
console.error(`\u2717 mm-harness last: ${message}
|
|
27
|
-
Next: ${userAction}`);
|
|
28
|
-
}
|
|
29
|
-
return EXIT.runtime;
|
|
30
|
-
}
|
|
31
|
-
if (json) {
|
|
32
|
-
console.log(JSON.stringify({
|
|
33
|
-
schemaVersion: 1,
|
|
34
|
-
command: "last",
|
|
35
|
-
target,
|
|
36
|
-
journalPath: file,
|
|
37
|
-
status: "pass",
|
|
38
|
-
exitCode: EXIT.ok,
|
|
39
|
-
last: record
|
|
40
|
-
}, null, 2));
|
|
41
|
-
} else {
|
|
42
|
-
const end = record.finishedAt ?? "still running or interrupted";
|
|
43
|
-
console.log(`${record.verdict.toUpperCase()} ${record.command} (exit ${record.exitCode ?? "pending"})`);
|
|
44
|
-
console.log(`started: ${record.startedAt}`);
|
|
45
|
-
console.log(`finished: ${end}`);
|
|
46
|
-
if (record.evidencePaths.length > 0) console.log(`evidence: ${record.evidencePaths.join(", ")}`);
|
|
47
|
-
}
|
|
48
|
-
return EXIT.ok;
|
|
49
|
-
}
|
|
50
|
-
export {
|
|
51
|
-
handleLast
|
|
52
|
-
};
|
package/dist/json-stream.js
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
class JsonStreamWriter {
|
|
2
|
-
enabled;
|
|
3
|
-
command;
|
|
4
|
-
completed = false;
|
|
5
|
-
writeLine;
|
|
6
|
-
constructor(command, enabled, output = process.stdout) {
|
|
7
|
-
this.command = command;
|
|
8
|
-
this.enabled = enabled;
|
|
9
|
-
const write = output.write.bind(output);
|
|
10
|
-
this.writeLine = (line) => {
|
|
11
|
-
write(`${line}
|
|
12
|
-
`);
|
|
13
|
-
};
|
|
14
|
-
}
|
|
15
|
-
isolateStdout() {
|
|
16
|
-
if (!this.enabled) return () => void 0;
|
|
17
|
-
const original = process.stdout.write;
|
|
18
|
-
const redirect = ((chunk, encoding, callback) => process.stderr.write(chunk, encoding, callback));
|
|
19
|
-
process.stdout.write = redirect;
|
|
20
|
-
return () => {
|
|
21
|
-
if (process.stdout.write === redirect) process.stdout.write = original;
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
emit(event, fields = {}) {
|
|
25
|
-
if (!this.enabled || this.completed) return;
|
|
26
|
-
this.writeLine(JSON.stringify({
|
|
27
|
-
schemaVersion: 1,
|
|
28
|
-
command: this.command,
|
|
29
|
-
event,
|
|
30
|
-
...fields,
|
|
31
|
-
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
32
|
-
}));
|
|
33
|
-
}
|
|
34
|
-
phase(phase, fields = {}) {
|
|
35
|
-
this.emit("phase", { phase, ...fields });
|
|
36
|
-
}
|
|
37
|
-
node(nodeId, action, status) {
|
|
38
|
-
this.emit("node", { nodeId, action, status });
|
|
39
|
-
}
|
|
40
|
-
mutation(mutation) {
|
|
41
|
-
this.emit("mutation", { mutation });
|
|
42
|
-
}
|
|
43
|
-
recovery(code) {
|
|
44
|
-
this.emit("recovery", { code });
|
|
45
|
-
}
|
|
46
|
-
error(error) {
|
|
47
|
-
this.emit("error", { error });
|
|
48
|
-
}
|
|
49
|
-
complete(status, exitCode, fields = {}) {
|
|
50
|
-
if (!this.enabled || this.completed) return;
|
|
51
|
-
this.emit("complete", { status, exitCode, ...fields });
|
|
52
|
-
this.completed = true;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
export {
|
|
56
|
-
JsonStreamWriter
|
|
57
|
-
};
|
package/dist/run-diagnostics.js
DELETED
|
@@ -1,261 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { createHash } from "node:crypto";
|
|
4
|
-
import { spawnSync } from "node:child_process";
|
|
5
|
-
import { getAdapterSurface } from "./adapters/surface.js";
|
|
6
|
-
import { runnerDir } from "./paths.js";
|
|
7
|
-
import { ensureExtensionConsoleCapture } from "./adapters/extension/console-capture.js";
|
|
8
|
-
const MAX_CAPTURE_BYTES = 512 * 1024;
|
|
9
|
-
const MAX_FINDINGS = 20;
|
|
10
|
-
const MAX_PREVIEW_CHARS = 320;
|
|
11
|
-
function readRunDiagnosticsDocument(diagnosticsPath) {
|
|
12
|
-
if (typeof diagnosticsPath !== "string" || diagnosticsPath.length === 0) return null;
|
|
13
|
-
try {
|
|
14
|
-
const document = JSON.parse(fs.readFileSync(diagnosticsPath, "utf8"));
|
|
15
|
-
return isRunDiagnosticsDocument(document) ? document : null;
|
|
16
|
-
} catch {
|
|
17
|
-
return null;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
function formatRunDiagnosticsForHuman(diagnostics, adapter) {
|
|
21
|
-
if (!diagnostics) {
|
|
22
|
-
return [adapter === "core" ? "N/A \u2014 Core is headless and has no application log source." : "UNAVAILABLE \u2014 application diagnostics could not be collected for this run."];
|
|
23
|
-
}
|
|
24
|
-
const status = diagnostics.status === "clean" ? "CLEAN" : diagnostics.status === "review" ? "REVIEW" : "UNAVAILABLE";
|
|
25
|
-
const lines = [`${status} \u2014 ${diagnostics.note}`];
|
|
26
|
-
for (const finding of diagnostics.findings) {
|
|
27
|
-
const count = finding.count > 1 ? ` \xD7${finding.count}` : "";
|
|
28
|
-
lines.push(`${finding.level.toUpperCase()}${count} \u2014 ${finding.preview}`);
|
|
29
|
-
}
|
|
30
|
-
return lines;
|
|
31
|
-
}
|
|
32
|
-
async function beginRunDiagnostics(adapter, projectRoot) {
|
|
33
|
-
const source = getAdapterSurface(adapter).appLogSource(projectRoot);
|
|
34
|
-
if (!source) return null;
|
|
35
|
-
const mobileIssueBuffer = adapter === "mobile" && armMobileIssueBuffer(projectRoot) ? { projectRoot } : void 0;
|
|
36
|
-
if (adapter === "extension") {
|
|
37
|
-
await ensureExtensionConsoleCapture(projectRoot).catch(() => void 0);
|
|
38
|
-
}
|
|
39
|
-
const stat = safeStat(source.path);
|
|
40
|
-
return {
|
|
41
|
-
source,
|
|
42
|
-
offset: stat?.size ?? 0,
|
|
43
|
-
...stat ? { inode: stat.ino } : {},
|
|
44
|
-
...mobileIssueBuffer ? { mobileIssueBuffer } : {}
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
function isRunDiagnosticsDocument(value) {
|
|
48
|
-
if (!isRecord(value) || value.schemaVersion !== 1 || value.scope !== "recipe-run-application") return false;
|
|
49
|
-
if (value.status !== "clean" && value.status !== "review" && value.status !== "unavailable") return false;
|
|
50
|
-
return typeof value.note === "string" && Array.isArray(value.findings);
|
|
51
|
-
}
|
|
52
|
-
function finishRunDiagnostics(baseline, result) {
|
|
53
|
-
if (!baseline) return result;
|
|
54
|
-
try {
|
|
55
|
-
const bufferedIssues = baseline.mobileIssueBuffer ? collectMobileIssueBuffer(baseline.mobileIssueBuffer.projectRoot) : void 0;
|
|
56
|
-
const diagnostics = collectRunDiagnostics(baseline, bufferedIssues);
|
|
57
|
-
const artifactsDir = path.dirname(result.summaryPath);
|
|
58
|
-
const diagnosticsPath = path.join(artifactsDir, "diagnostics.json");
|
|
59
|
-
fs.writeFileSync(diagnosticsPath, `${JSON.stringify(diagnostics, null, 2)}
|
|
60
|
-
`);
|
|
61
|
-
indexDiagnosticArtifact(result.artifactManifestPath);
|
|
62
|
-
indexDiagnosticSummary(result.summaryPath, diagnostics);
|
|
63
|
-
return {
|
|
64
|
-
...result,
|
|
65
|
-
diagnosticsPath,
|
|
66
|
-
sideFindings: {
|
|
67
|
-
status: diagnostics.status,
|
|
68
|
-
note: diagnostics.note,
|
|
69
|
-
counts: diagnostics.counts
|
|
70
|
-
}
|
|
71
|
-
};
|
|
72
|
-
} catch (error) {
|
|
73
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
74
|
-
console.warn(`WARN: run diagnostics were unavailable: ${detail}`);
|
|
75
|
-
return {
|
|
76
|
-
...result,
|
|
77
|
-
sideFindings: {
|
|
78
|
-
status: "unavailable",
|
|
79
|
-
note: "Run diagnostics could not be collected; the recipe result is unchanged.",
|
|
80
|
-
counts: { total: 0, warning: 0, error: 0, exception: 0 }
|
|
81
|
-
}
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
function collectRunDiagnostics(baseline, bufferedIssues) {
|
|
86
|
-
const stat = safeStat(baseline.source.path);
|
|
87
|
-
const source = {
|
|
88
|
-
label: baseline.source.label,
|
|
89
|
-
path: baseline.source.path,
|
|
90
|
-
startOffset: baseline.offset,
|
|
91
|
-
endOffset: stat?.size ?? baseline.offset,
|
|
92
|
-
bytesRead: 0,
|
|
93
|
-
truncated: false,
|
|
94
|
-
inAppBuffer: bufferedIssues === void 0 ? "n/a" : bufferedIssues === null ? "unavailable" : "collected"
|
|
95
|
-
};
|
|
96
|
-
let text = "";
|
|
97
|
-
if (stat) {
|
|
98
|
-
const sameFile = baseline.inode === void 0 || baseline.inode === stat.ino;
|
|
99
|
-
const startOffset = sameFile && stat.size >= baseline.offset ? baseline.offset : 0;
|
|
100
|
-
const available = Math.max(0, stat.size - startOffset);
|
|
101
|
-
const bytesToRead = Math.min(available, MAX_CAPTURE_BYTES);
|
|
102
|
-
source.startOffset = startOffset;
|
|
103
|
-
source.endOffset = stat.size;
|
|
104
|
-
source.bytesRead = bytesToRead;
|
|
105
|
-
source.truncated = available > MAX_CAPTURE_BYTES;
|
|
106
|
-
if (bytesToRead > 0) text = readSlice(baseline.source.path, startOffset, bytesToRead);
|
|
107
|
-
}
|
|
108
|
-
const allFindings = dedupeFindings(
|
|
109
|
-
[
|
|
110
|
-
...text.split(/\r?\n/u).map(classifyLine).filter((finding) => finding !== null),
|
|
111
|
-
...(bufferedIssues ?? []).map(classifyBufferedIssue).filter((finding) => finding !== null)
|
|
112
|
-
]
|
|
113
|
-
);
|
|
114
|
-
const findings = allFindings.slice(0, MAX_FINDINGS);
|
|
115
|
-
const counts = countFindings(allFindings);
|
|
116
|
-
const status = counts.total > 0 ? "review" : stat || bufferedIssues !== void 0 && bufferedIssues !== null ? "clean" : "unavailable";
|
|
117
|
-
const note = counts.total > 0 ? `Observed ${counts.total} distinct application warning/error event(s) during the recipe run; relation to the task is not determined.` : status === "clean" ? "No application warnings or errors were emitted during the recipe run." : "Application diagnostics were unavailable for this run.";
|
|
118
|
-
return {
|
|
119
|
-
schemaVersion: 1,
|
|
120
|
-
scope: "recipe-run-application",
|
|
121
|
-
status,
|
|
122
|
-
nonBlocking: true,
|
|
123
|
-
note,
|
|
124
|
-
source,
|
|
125
|
-
counts,
|
|
126
|
-
findings
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
function classifyLine(line) {
|
|
130
|
-
const trimmed = line.trim();
|
|
131
|
-
if (!trimmed) return null;
|
|
132
|
-
let level = null;
|
|
133
|
-
if (/\bEXCEPTION\s{2,}|\b(?:Uncaught|UnhandledPromiseRejection|FATAL EXCEPTION)\b/iu.test(trimmed)) {
|
|
134
|
-
level = "exception";
|
|
135
|
-
} else if (/^(?:ERROR\s{2,}|\[error\]\s*)/iu.test(trimmed) || /\]\s+ERROR\s{2,}/u.test(trimmed)) {
|
|
136
|
-
level = "error";
|
|
137
|
-
} else if (/^(?:WARN(?:ING)?\s{2,}|\[warn(?:ing)?\]\s*)/iu.test(trimmed) || /\]\s+WARN(?:ING)?\s{2,}/u.test(trimmed)) {
|
|
138
|
-
level = "warning";
|
|
139
|
-
}
|
|
140
|
-
if (!level) return null;
|
|
141
|
-
return makeFinding(level, trimmed);
|
|
142
|
-
}
|
|
143
|
-
function classifyBufferedIssue(value) {
|
|
144
|
-
if (!isRecord(value) || typeof value.text !== "string") return null;
|
|
145
|
-
const rawLevel = String(value.level ?? "").toLowerCase();
|
|
146
|
-
const level = rawLevel === "warn" || rawLevel === "warning" ? "warning" : rawLevel === "error" ? "error" : rawLevel === "exception" ? "exception" : null;
|
|
147
|
-
return level ? makeFinding(level, value.text) : null;
|
|
148
|
-
}
|
|
149
|
-
function makeFinding(level, text) {
|
|
150
|
-
const preview = redactPreview(text.trim());
|
|
151
|
-
const identity = preview.replace(/\b\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?\b/gu, "[TIME]").replace(/\[(?:sw|page:[^\]]+|console:[^\]]+)\]/gu, "[APP]").replace(/^(?:(?:WARN(?:ING)?|ERROR|EXCEPTION|\[TIME\]|\[APP\])\s*)+/u, "");
|
|
152
|
-
return {
|
|
153
|
-
level,
|
|
154
|
-
fingerprint: createHash("sha256").update(`${level}|${identity}`).digest("hex").slice(0, 12),
|
|
155
|
-
preview
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
function armMobileIssueBuffer(projectRoot) {
|
|
159
|
-
const armed = runMobileIssueCommand(projectRoot, "issues-arm");
|
|
160
|
-
if (!armed) return false;
|
|
161
|
-
return runMobileIssueCommand(projectRoot, "issues-collect") !== null;
|
|
162
|
-
}
|
|
163
|
-
function collectMobileIssueBuffer(projectRoot) {
|
|
164
|
-
const result = runMobileIssueCommand(projectRoot, "issues-collect");
|
|
165
|
-
return isRecord(result) && Array.isArray(result.entries) ? result.entries : null;
|
|
166
|
-
}
|
|
167
|
-
function runMobileIssueCommand(projectRoot, command) {
|
|
168
|
-
const bridge = path.join(runnerDir, "adapters", "mobile", "bridge-runtime", "cdp-bridge.cjs");
|
|
169
|
-
const result = spawnSync(process.execPath, [bridge, command], {
|
|
170
|
-
cwd: projectRoot,
|
|
171
|
-
env: { ...process.env, APP_ROOT: projectRoot, CDP_TIMEOUT: "5000" },
|
|
172
|
-
encoding: "utf8",
|
|
173
|
-
timeout: 1e4
|
|
174
|
-
});
|
|
175
|
-
if (result.status !== 0) return null;
|
|
176
|
-
try {
|
|
177
|
-
return JSON.parse(result.stdout.trim());
|
|
178
|
-
} catch {
|
|
179
|
-
return null;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
function redactPreview(value) {
|
|
183
|
-
return value.replace(/\b(Bearer)\s+\S+/giu, "$1 [REDACTED]").replace(/\b(password|passphrase|mnemonic|seed(?:Phrase)?|privateKey|secret|token|authorization)\b\s*[:=]\s*(?:"[^"]*"|'[^']*'|\S+)/giu, "$1=[REDACTED]").replace(/\b(?:0x)?[a-f0-9]{64,}\b/giu, "[REDACTED_HEX]").replace(/(https?:\/\/[^\s?]+)\?\S+/giu, "$1?[REDACTED_QUERY]").slice(0, MAX_PREVIEW_CHARS);
|
|
184
|
-
}
|
|
185
|
-
function dedupeFindings(findings) {
|
|
186
|
-
const byFingerprint = /* @__PURE__ */ new Map();
|
|
187
|
-
for (const finding of findings) {
|
|
188
|
-
const existing = byFingerprint.get(finding.fingerprint);
|
|
189
|
-
if (existing) existing.count += 1;
|
|
190
|
-
else byFingerprint.set(finding.fingerprint, { ...finding, count: 1 });
|
|
191
|
-
}
|
|
192
|
-
return [...byFingerprint.values()];
|
|
193
|
-
}
|
|
194
|
-
function countFindings(findings) {
|
|
195
|
-
const counts = { total: findings.length, warning: 0, error: 0, exception: 0 };
|
|
196
|
-
for (const finding of findings) counts[finding.level] += 1;
|
|
197
|
-
return counts;
|
|
198
|
-
}
|
|
199
|
-
function readSlice(filePath, offset, length) {
|
|
200
|
-
const fd = fs.openSync(filePath, "r");
|
|
201
|
-
try {
|
|
202
|
-
const buffer = Buffer.alloc(length);
|
|
203
|
-
const bytesRead = fs.readSync(fd, buffer, 0, length, offset);
|
|
204
|
-
return buffer.subarray(0, bytesRead).toString("utf8");
|
|
205
|
-
} finally {
|
|
206
|
-
fs.closeSync(fd);
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
function safeStat(filePath) {
|
|
210
|
-
try {
|
|
211
|
-
return fs.statSync(filePath);
|
|
212
|
-
} catch {
|
|
213
|
-
return null;
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
function indexDiagnosticArtifact(manifestPath) {
|
|
217
|
-
const manifest = readJsonRecord(manifestPath);
|
|
218
|
-
if (!manifest) return;
|
|
219
|
-
const artifacts = Array.isArray(manifest.artifacts) ? manifest.artifacts : [];
|
|
220
|
-
manifest.artifacts = [
|
|
221
|
-
...artifacts.filter((artifact) => !isRecord(artifact) || artifact.path !== "diagnostics.json"),
|
|
222
|
-
{
|
|
223
|
-
path: "diagnostics.json",
|
|
224
|
-
type: "json",
|
|
225
|
-
label: "Run-scoped application diagnostics",
|
|
226
|
-
category: "diagnostic"
|
|
227
|
-
}
|
|
228
|
-
];
|
|
229
|
-
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}
|
|
230
|
-
`);
|
|
231
|
-
}
|
|
232
|
-
function indexDiagnosticSummary(summaryPath, diagnostics) {
|
|
233
|
-
const summary = readJsonRecord(summaryPath);
|
|
234
|
-
if (!summary) return;
|
|
235
|
-
summary.sideFindings = {
|
|
236
|
-
status: diagnostics.status,
|
|
237
|
-
nonBlocking: true,
|
|
238
|
-
counts: diagnostics.counts,
|
|
239
|
-
diagnosticsPath: "diagnostics.json"
|
|
240
|
-
};
|
|
241
|
-
fs.writeFileSync(summaryPath, `${JSON.stringify(summary, null, 2)}
|
|
242
|
-
`);
|
|
243
|
-
}
|
|
244
|
-
function readJsonRecord(filePath) {
|
|
245
|
-
try {
|
|
246
|
-
const value = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
247
|
-
return isRecord(value) ? value : null;
|
|
248
|
-
} catch {
|
|
249
|
-
return null;
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
function isRecord(value) {
|
|
253
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
254
|
-
}
|
|
255
|
-
export {
|
|
256
|
-
beginRunDiagnostics,
|
|
257
|
-
collectRunDiagnostics,
|
|
258
|
-
finishRunDiagnostics,
|
|
259
|
-
formatRunDiagnosticsForHuman,
|
|
260
|
-
readRunDiagnosticsDocument
|
|
261
|
-
};
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
<!-- Generated by scripts/generate-cli-ergonomics-audit.mjs. Do not edit by hand. -->
|
|
2
|
-
# CLI ergonomics audit
|
|
3
|
-
|
|
4
|
-
This matrix tracks the public command surface. “Covered” means the shared preflight contract and
|
|
5
|
-
`tests/contract/cli-teaching-errors.test.sh` exercise the behavior without dispatching runtime work.
|
|
6
|
-
Runtime recovery is claimed only where command-specific failures return a stable `userAction`.
|
|
7
|
-
Success hints are intentionally limited to lifecycle transitions with one truthful next command;
|
|
8
|
-
discovery, read-only, and terminal evidence commands do not invent one. ID prefix support is N/A
|
|
9
|
-
unless a command displays a shortened ID that another command accepts.
|
|
10
|
-
|
|
11
|
-
| Command | Bad flag | First feedback | `--json` recovery | Success next step | Displayed ID prefix |
|
|
12
|
-
|---|---|---|---|---|---|
|
|
13
|
-
| `status` | covered: `CLI_UNKNOWN_OPTION` | N/A: no newly introduced slow path | usage + runtime `error.code/message/userAction` | structured `next` | N/A: command displays no shortened ID |
|
|
14
|
-
| `checklist` | covered: `CLI_UNKNOWN_OPTION` | N/A: no newly introduced slow path | usage errors covered; no runtime recovery claim | N/A: discovery, read-only, or terminal result | N/A: command displays no shortened ID |
|
|
15
|
-
| `actions` | covered: `CLI_UNKNOWN_OPTION` | N/A: no newly introduced slow path | usage + runtime `error.code/message/userAction` | N/A: discovery, read-only, or terminal result | N/A: command displays no shortened ID |
|
|
16
|
-
| `stop` | covered: `CLI_UNKNOWN_OPTION` | N/A: no newly introduced slow path | usage + runtime `error.code/message/userAction` | launch `next` when the adapter has one unambiguous launch | N/A: command displays no shortened ID |
|
|
17
|
-
| `call` | covered: `CLI_UNKNOWN_OPTION` | N/A: no newly introduced slow path | usage + runtime `error.code/message/userAction` | N/A: discovery, read-only, or terminal result | N/A: command displays no shortened ID |
|
|
18
|
-
| `flows` | covered: `CLI_UNKNOWN_OPTION` | N/A: no newly introduced slow path | usage errors covered; no runtime recovery claim | N/A: discovery, read-only, or terminal result | N/A: command displays no shortened ID |
|
|
19
|
-
| `run` | covered: `CLI_UNKNOWN_OPTION` | TTY intent before slow work; machine output clean | usage + runtime `error.code/message/userAction` | N/A: discovery, read-only, or terminal result | N/A: command displays no shortened ID |
|
|
20
|
-
| `last` | covered: `CLI_UNKNOWN_OPTION` | N/A: no newly introduced slow path | usage + runtime `error.code/message/userAction` | N/A: discovery, read-only, or terminal result | N/A: command displays no shortened ID |
|
|
21
|
-
| `doctor` | covered: `CLI_UNKNOWN_OPTION` | N/A: no newly introduced slow path | usage + runtime `error.code/message/userAction` | N/A: discovery, read-only, or terminal result | N/A: command displays no shortened ID |
|
|
22
|
-
| `check` | covered: `CLI_UNKNOWN_OPTION` | N/A: no newly introduced slow path | usage + runtime `error.code/message/userAction` | N/A: discovery, read-only, or terminal result | N/A: command displays no shortened ID |
|
|
23
|
-
| `recipe-quality` | covered: `CLI_UNKNOWN_OPTION` | N/A: no newly introduced slow path | usage + runtime `error.code/message/userAction` | N/A: discovery, read-only, or terminal result | N/A: command displays no shortened ID |
|
|
24
|
-
| `provision` | covered: `CLI_UNKNOWN_OPTION` | TTY intent before slow work; machine output clean | usage + runtime `error.code/message/userAction` | launch `next` after install | N/A: command displays no shortened ID |
|
|
25
|
-
| `install` | covered: `CLI_UNKNOWN_OPTION` | N/A: no newly introduced slow path | usage + runtime `error.code/message/userAction` | verify `next` | N/A: command displays no shortened ID |
|
|
26
|
-
| `verify` | covered: `CLI_UNKNOWN_OPTION` | N/A: no newly introduced slow path | usage + runtime `error.code/message/userAction` | status `next` | N/A: command displays no shortened ID |
|
|
27
|
-
| `cleanup` | covered: `CLI_UNKNOWN_OPTION` | N/A: no newly introduced slow path | usage + runtime `error.code/message/userAction` | install `next` | N/A: command displays no shortened ID |
|
|
28
|
-
| `launch` | covered: `CLI_UNKNOWN_OPTION` | TTY intent before slow work; machine output clean | usage + runtime `error.code/message/userAction` | N/A: discovery, read-only, or terminal result | N/A: command displays no shortened ID |
|
|
29
|
-
| `logs` | covered: `CLI_UNKNOWN_OPTION` | N/A: no newly introduced slow path | usage + runtime `error.code/message/userAction` | N/A: discovery, read-only, or terminal result | N/A: command displays no shortened ID |
|
|
30
|
-
| `debug` | covered: `CLI_UNKNOWN_OPTION` | N/A: no newly introduced slow path | usage errors covered; no runtime recovery claim | N/A: discovery, read-only, or terminal result | N/A: command displays no shortened ID |
|
|
31
|
-
| `update` | covered: `CLI_UNKNOWN_OPTION` | N/A: no newly introduced slow path | usage + runtime `error.code/message/userAction` | N/A: discovery, read-only, or terminal result | N/A: command displays no shortened ID |
|
|
32
|
-
| `fixtures` | covered: `CLI_UNKNOWN_OPTION` | N/A: no newly introduced slow path | usage + runtime `error.code/message/userAction` | N/A: discovery, read-only, or terminal result | N/A: command displays no shortened ID |
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
# Human QA
|
|
2
|
-
|
|
3
|
-
Run this from each product checkout before a release. Verify the visible outcome,
|
|
4
|
-
not only the exit code.
|
|
5
|
-
|
|
6
|
-
Run the non-visual gate first:
|
|
7
|
-
|
|
8
|
-
```bash
|
|
9
|
-
yarn qa:human \
|
|
10
|
-
--extension /path/to/metamask-extension \
|
|
11
|
-
--mobile /path/to/metamask-mobile \
|
|
12
|
-
--core /path/to/metamask-core \
|
|
13
|
-
--artifacts-dir temp/human-qa
|
|
14
|
-
```
|
|
15
|
-
|
|
16
|
-
It verifies live readiness, installed runtime, debugger target resolution, smoke
|
|
17
|
-
evidence, and unchanged tracked product source. The checks below cover outcomes
|
|
18
|
-
that still require eyes or an intentional source edit.
|
|
19
|
-
|
|
20
|
-
## Common
|
|
21
|
-
|
|
22
|
-
- [ ] `mm-harness --version` prints the candidate version.
|
|
23
|
-
- [ ] `mm-harness doctor` detects the correct product and reports only current
|
|
24
|
-
configuration.
|
|
25
|
-
- [ ] Without `capture-helper`, `doctor` names the CDP or simulator/ADB screenshot
|
|
26
|
-
fallback and says video is unavailable.
|
|
27
|
-
- [ ] A misspelled flag explains the error and gives a runnable next command.
|
|
28
|
-
- [ ] `mm-harness actions` and `mm-harness flows` return useful, bounded discovery.
|
|
29
|
-
- [ ] `mm-harness fixtures set` either applies the fixture or names the missing
|
|
30
|
-
file and exact recovery action.
|
|
31
|
-
- [ ] `mm-harness run <safe-recipe> --artifacts-dir temp/qa-proof` prints findings,
|
|
32
|
-
diagnostics, and absolute evidence paths.
|
|
33
|
-
- [ ] `mm-harness last --json` identifies that run without rerunning it.
|
|
34
|
-
- [ ] `mm-harness doctor --json` is one parseable document with no human decoration.
|
|
35
|
-
|
|
36
|
-
## Extension
|
|
37
|
-
|
|
38
|
-
- [ ] `mm-harness launch` opens/reuses the checkout's named Chrome profile.
|
|
39
|
-
- [ ] `mm-harness launch --sidepanel` opens the MetaMask sidepanel without extra
|
|
40
|
-
Extension tabs.
|
|
41
|
-
- [ ] `mm-harness debug` opens connected DevTools for the active MetaMask target.
|
|
42
|
-
- [ ] `mm-harness logs --source extension` shows Extension console events.
|
|
43
|
-
- [ ] `mm-harness logs --source dapp` shows dapp console events separately.
|
|
44
|
-
- [ ] Edit a visible source string twice, waiting for successful incremental
|
|
45
|
-
compilation after each edit; refresh the browser and confirm both changes.
|
|
46
|
-
- [ ] Revert the edit, confirm a third incremental compilation, and verify the
|
|
47
|
-
same watcher and Chrome process were reused.
|
|
48
|
-
|
|
49
|
-
## Mobile
|
|
50
|
-
|
|
51
|
-
- [ ] `mm-harness launch ios` boots/reuses the intended simulator and opens the app.
|
|
52
|
-
- [ ] `mm-harness launch android` targets the intended emulator/device.
|
|
53
|
-
- [ ] `mm-harness logs --source metro` and `--source app` show distinct streams.
|
|
54
|
-
- [ ] `mm-harness debug` opens the usable React Native developer surface.
|
|
55
|
-
- [ ] Change a visible JS string, confirm Metro rebuilds, reload the app, and see
|
|
56
|
-
the change without a native rebuild.
|
|
57
|
-
|
|
58
|
-
## Core
|
|
59
|
-
|
|
60
|
-
- [ ] `mm-harness status`, `doctor`, `actions`, `flows`, and a safe recipe run work
|
|
61
|
-
without browser/device assumptions.
|
|
62
|
-
|
|
63
|
-
Record the command, version, checkout SHA, visible mismatch, exit code, and JSON
|
|
64
|
-
error envelope for any failure.
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
# Video demo validation
|
|
2
|
-
|
|
3
|
-
Use this checklist against the installed release before recording. Each checkout
|
|
4
|
-
auto-detects its platform, ports, and slot; the paths below are placeholders only.
|
|
5
|
-
|
|
6
|
-
```bash
|
|
7
|
-
export EXTENSION_CHECKOUT=/path/to/metamask-extension
|
|
8
|
-
export MOBILE_CHECKOUT=/path/to/metamask-mobile
|
|
9
|
-
export CORE_CHECKOUT=/path/to/core
|
|
10
|
-
```
|
|
11
|
-
|
|
12
|
-
## Common
|
|
13
|
-
|
|
14
|
-
- [ ] `mm-harness --version` prints the intended release.
|
|
15
|
-
- [ ] `mm-harness doctor` explains any unavailable capability and gives one next step.
|
|
16
|
-
- [ ] Recipe results print diagnostics and absolute artifact paths inline.
|
|
17
|
-
- [ ] `git status --short` is unchanged in each product checkout after validation.
|
|
18
|
-
|
|
19
|
-
## Extension
|
|
20
|
-
|
|
21
|
-
```bash
|
|
22
|
-
cd "$EXTENSION_CHECKOUT"
|
|
23
|
-
mm-harness launch --build --verify
|
|
24
|
-
mm-harness doctor
|
|
25
|
-
mm-harness actions positions
|
|
26
|
-
mm-harness flows
|
|
27
|
-
mm-harness run runner.smoke --artifacts-dir temp/demo
|
|
28
|
-
mm-harness run wallet.smoke --artifacts-dir temp/wallet-demo
|
|
29
|
-
mm-harness logs --source app
|
|
30
|
-
mm-harness debug
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
- [ ] Launch keeps an incremental webpack watcher alive.
|
|
34
|
-
- [ ] Two small source edits compile incrementally; browser refresh shows both changes.
|
|
35
|
-
- [ ] `debug` connects to the active slot's MetaMask target and opens Console.
|
|
36
|
-
- [ ] `logs --source app` shows extension logs; `--source dapp` and `--source webpack`
|
|
37
|
-
show their separate streams.
|
|
38
|
-
- [ ] Each recipe prints `diagnostics: CLEAN` or expands every warning/error inline.
|
|
39
|
-
- [ ] Each recipe prints absolute paths for its report, trace, diagnostics, screenshots,
|
|
40
|
-
and artifact manifest.
|
|
41
|
-
|
|
42
|
-
## Mobile
|
|
43
|
-
|
|
44
|
-
```bash
|
|
45
|
-
cd "$MOBILE_CHECKOUT"
|
|
46
|
-
mm-harness launch ios --verify
|
|
47
|
-
mm-harness fixtures set
|
|
48
|
-
mm-harness call metamask.wallet.ensure_unlocked
|
|
49
|
-
mm-harness status
|
|
50
|
-
mm-harness logs --source app
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
- [ ] The intended simulator is running; focus changes only when explicitly requested.
|
|
54
|
-
- [ ] Fixture setup reads the canonical checkout fixture and unlocks the wallet.
|
|
55
|
-
- [ ] Status reports the live route, wallet state, account, and fixture readiness.
|
|
56
|
-
- [ ] App logs are separate from Metro logs (`mm-harness logs --source metro`).
|
|
57
|
-
|
|
58
|
-
## Core
|
|
59
|
-
|
|
60
|
-
```bash
|
|
61
|
-
cd "$CORE_CHECKOUT"
|
|
62
|
-
mm-harness doctor
|
|
63
|
-
mm-harness run runner.smoke --artifacts-dir temp/demo
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
- [ ] Doctor reports the headless controller ready.
|
|
67
|
-
- [ ] The canonical wallet fixture supplies the default account.
|
|
68
|
-
- [ ] Evidence paths are absolute; diagnostics is explicitly `N/A` for headless Core.
|
|
69
|
-
|
|
70
|
-
## Recording path
|
|
71
|
-
|
|
72
|
-
Show the Extension discovery and proof commands, then the equivalent Mobile and Core
|
|
73
|
-
commands. The useful story is: one auto-detected CLI, reusable team libraries,
|
|
74
|
-
parameterized recipes, app diagnostics, and proof artifacts that a human can inspect.
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"schema_version": 1,
|
|
3
|
-
"title": "MetaMask Core runner smoke",
|
|
4
|
-
"description": "Proves the headless Core runner resolves the checkout, executes generic actions, and writes a valid artifact package.",
|
|
5
|
-
"validate": {
|
|
6
|
-
"workflow": {
|
|
7
|
-
"entry": "status",
|
|
8
|
-
"nodes": {
|
|
9
|
-
"status": {
|
|
10
|
-
"action": "app.status",
|
|
11
|
-
"next": "package",
|
|
12
|
-
"intent": "Resolve the Core checkout and report headless readiness"
|
|
13
|
-
},
|
|
14
|
-
"package": {
|
|
15
|
-
"action": "assert_file",
|
|
16
|
-
"path": "package.json",
|
|
17
|
-
"next": "done",
|
|
18
|
-
"intent": "Prove generic project-file assertions work in Core"
|
|
19
|
-
},
|
|
20
|
-
"done": {
|
|
21
|
-
"action": "end",
|
|
22
|
-
"status": "pass"
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
}
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"schema_version": 1,
|
|
3
|
-
"title": "MetaMask Extension wallet smoke",
|
|
4
|
-
"description": "Fixture-backed proof that the Extension wallet is reachable, unlocked, readable, and visually capturable without depending on a product feature domain.",
|
|
5
|
-
"validate": {
|
|
6
|
-
"workflow": {
|
|
7
|
-
"entry": "status",
|
|
8
|
-
"nodes": {
|
|
9
|
-
"status": {
|
|
10
|
-
"action": "app.status",
|
|
11
|
-
"next": "fixture",
|
|
12
|
-
"intent": "Confirm the Extension runtime is reachable"
|
|
13
|
-
},
|
|
14
|
-
"fixture": {
|
|
15
|
-
"action": "metamask.wallet.fixture_status",
|
|
16
|
-
"next": "unlock",
|
|
17
|
-
"intent": "Confirm the checkout wallet fixture is ready"
|
|
18
|
-
},
|
|
19
|
-
"unlock": {
|
|
20
|
-
"action": "metamask.wallet.ensure_unlocked",
|
|
21
|
-
"next": "state",
|
|
22
|
-
"intent": "Ensure the fixture-backed wallet is unlocked"
|
|
23
|
-
},
|
|
24
|
-
"state": {
|
|
25
|
-
"action": "metamask.wallet.read_state",
|
|
26
|
-
"next": "screenshot",
|
|
27
|
-
"intent": "Read the current wallet state through the public adapter"
|
|
28
|
-
},
|
|
29
|
-
"screenshot": {
|
|
30
|
-
"action": "ui.screenshot",
|
|
31
|
-
"path": "screenshots/wallet-smoke.png",
|
|
32
|
-
"next": "done",
|
|
33
|
-
"intent": "Capture visual wallet evidence"
|
|
34
|
-
},
|
|
35
|
-
"done": {
|
|
36
|
-
"action": "end",
|
|
37
|
-
"status": "pass"
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
}
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"schema_version": 1,
|
|
3
|
-
"title": "MetaMask Mobile wallet smoke",
|
|
4
|
-
"description": "Fixture-backed proof that the Mobile wallet is reachable, unlocked, readable, and visually capturable without depending on a product feature domain.",
|
|
5
|
-
"validate": {
|
|
6
|
-
"workflow": {
|
|
7
|
-
"entry": "status",
|
|
8
|
-
"nodes": {
|
|
9
|
-
"status": {
|
|
10
|
-
"action": "app.status",
|
|
11
|
-
"next": "fixture",
|
|
12
|
-
"intent": "Confirm the Mobile runtime is reachable"
|
|
13
|
-
},
|
|
14
|
-
"fixture": {
|
|
15
|
-
"action": "metamask.wallet.fixture_status",
|
|
16
|
-
"next": "unlock",
|
|
17
|
-
"intent": "Confirm the checkout wallet fixture is ready"
|
|
18
|
-
},
|
|
19
|
-
"unlock": {
|
|
20
|
-
"action": "metamask.wallet.ensure_unlocked",
|
|
21
|
-
"next": "state",
|
|
22
|
-
"intent": "Ensure the fixture-backed wallet is unlocked"
|
|
23
|
-
},
|
|
24
|
-
"state": {
|
|
25
|
-
"action": "metamask.wallet.read_state",
|
|
26
|
-
"next": "screenshot",
|
|
27
|
-
"intent": "Read the current wallet state through the public adapter"
|
|
28
|
-
},
|
|
29
|
-
"screenshot": {
|
|
30
|
-
"action": "ui.screenshot",
|
|
31
|
-
"path": "screenshots/wallet-smoke.png",
|
|
32
|
-
"next": "done",
|
|
33
|
-
"intent": "Capture visual wallet evidence"
|
|
34
|
-
},
|
|
35
|
-
"done": {
|
|
36
|
-
"action": "end",
|
|
37
|
-
"status": "pass"
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
}
|