@deeeed/metamask-harness 0.36.0 → 0.38.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 +51 -0
- package/adapters/extension/artifact-runtime-state.cjs +138 -0
- package/adapters/extension/inject.mjs +2 -0
- package/adapters/extension/launch-browser.cjs +24 -0
- package/adapters/extension/live.sh +82 -12
- package/adapters/extension/readiness.mjs +77 -36
- package/adapters/extension/snapshot-dist.sh +88 -3
- package/adapters/extension/verify.sh +6 -2
- package/adapters/manifest.json +17 -1
- package/adapters/shared/log-tui.mjs +1 -1
- package/dist/adapters/extension/artifact-integrity.js +38 -0
- package/dist/adapters/extension/extension-id.js +23 -4
- package/dist/adapters/extension/release-artifact.js +386 -0
- package/dist/adapters/extension/runtime-decision.js +164 -20
- package/dist/adapters/extension/runtime.js +169 -3
- package/dist/adapters/mobile/provision.js +38 -3
- package/dist/adapters/mobile/release-artifact-state.js +124 -0
- package/dist/adapters/mobile/release-artifact.js +295 -0
- package/dist/adapters.js +78 -6
- package/dist/app-lifecycle.js +4 -3
- package/dist/command-contract.js +16 -0
- package/dist/commands/call.js +6 -2
- package/dist/commands/launch/extension.js +74 -9
- package/dist/commands/launch/index.js +3 -0
- package/dist/commands/launch/mobile.js +2 -0
- package/dist/commands/provision.js +2 -0
- package/dist/commands/run-engine.js +109 -14
- package/dist/commands/run.js +4 -2
- package/dist/commands/runtime-launch.js +178 -10
- package/dist/heal-bounds.js +1 -1
- package/dist/live-adapter-contract.js +3 -1
- package/dist/metamask-action-validation.js +47 -1
- package/dist/mm-harness-cli.js +31 -1
- package/dist/recipe-security.js +40 -0
- package/dist/run-diagnostics.js +1 -1
- package/dist/runner.js +3 -0
- package/docs/RELEASE-QA-CAPABILITY-MAP.md +125 -0
- package/library/actions/extension/assets/finish_send.mjs +128 -0
- package/library/actions/extension/assets/import_custom_token.mjs +227 -0
- package/library/actions/extension/assets/prepare_send.mjs +53 -0
- package/library/actions/extension/assets/read_details.mjs +57 -0
- package/library/actions/extension/assets/read_visible_state.mjs +50 -0
- package/library/actions/extension/assets/review_send.mjs +23 -0
- package/library/actions/extension/assets/select_network_scope.mjs +30 -0
- package/library/actions/extension/assets/send.mjs +104 -0
- package/library/actions/extension/assets/set_custom_gas.mjs +56 -0
- package/library/actions/extension/assets/set_token_visibility.mjs +129 -0
- package/library/actions/extension/assets/verify_sorting.mjs +89 -0
- package/library/actions/extension/networks/add_chainlist.mjs +94 -0
- package/library/actions/extension/networks/add_custom.mjs +123 -0
- package/library/actions/extension/networks/custom_network.mjs +103 -0
- package/library/actions/extension/networks/read_visible_state.mjs +50 -0
- package/library/actions/extension/networks/remove_custom.mjs +68 -0
- package/library/actions/extension/perps/close_orders.mjs +1 -0
- package/library/actions/extension/perps/close_visible_position.mjs +133 -0
- package/library/actions/extension/perps/compare_provider_market.mjs +109 -0
- package/library/actions/extension/perps/edit_margin.mjs +82 -0
- package/library/actions/extension/perps/open_balance_action.mjs +58 -0
- package/library/actions/extension/perps/open_position_action.mjs +80 -0
- package/library/actions/extension/perps/perps.mjs +196 -2
- package/library/actions/extension/perps/place_order.mjs +5 -2
- package/library/actions/extension/perps/read_eligibility.mjs +34 -0
- package/library/actions/extension/perps/read_funds_confirmation.mjs +69 -0
- package/library/actions/extension/perps/read_snapshot.mjs +470 -0
- package/library/actions/extension/perps/read_visible_state.mjs +98 -0
- package/library/actions/extension/perps/select_activity_filter.mjs +44 -0
- package/library/actions/extension/perps/select_market_filter.mjs +71 -0
- package/library/actions/extension/perps/set_market_favorite.mjs +47 -0
- package/library/actions/extension/perps/update_position_tpsl.mjs +106 -0
- package/library/actions/extension/platform/cdp.mjs +109 -3
- package/library/actions/extension/settings/set_basic_functionality.mjs +58 -0
- package/library/actions/extension/swap_bridge/read_visible_state.mjs +71 -0
- package/library/actions/extension/swap_bridge/select_assets.mjs +103 -0
- package/library/actions/extension/swap_bridge/set_amount.mjs +36 -0
- package/library/actions/extension/swap_bridge/set_max_amount.mjs +58 -0
- package/library/actions/extension/swap_bridge/set_slippage.mjs +121 -0
- package/library/actions/extension/swap_bridge/submit_transaction.mjs +363 -0
- package/library/actions/extension/ui/activate_and_observe.mjs +96 -0
- package/library/actions/extension/ui/navigate.mjs +264 -8
- package/library/actions/extension/wallet/ensure_unlocked.mjs +2 -6
- package/library/actions/extension/wallet/import.mjs +13 -46
- package/library/actions/extension/wallet/lock.mjs +35 -0
- package/library/actions/extension/wallet/secret-input.mjs +98 -0
- package/library/actions/extension/wallet/select_account.mjs +71 -7
- package/library/actions/mobile/assets/read_visible_state.mjs +37 -0
- package/library/actions/mobile/perps/read_visible_state.mjs +31 -0
- package/library/actions/mobile/platform/native-session-name.mjs +5 -0
- package/library/actions/mobile/platform/observe-ui.mjs +86 -2
- package/library/actions/mobile/swap_bridge/native-session.mjs +45 -0
- package/library/actions/mobile/swap_bridge/read_visible_state.mjs +31 -0
- package/library/actions/mobile/swap_bridge/select_assets.mjs +94 -0
- package/library/actions/mobile/swap_bridge/set_amount.mjs +73 -0
- package/library/actions/mobile/ui/native-navigation.mjs +265 -0
- package/library/actions/mobile/ui/navigate.mjs +10 -1
- package/library/actions/mobile/wallet/ensure_unlocked.mjs +17 -0
- package/library/actions/mobile/wallet/import.mjs +71 -2
- package/library/actions/mobile/wallet/native-ui.mjs +512 -0
- package/library/actions/mobile/wallet/read_state.mjs +16 -0
- package/library/actions/mobile/wallet/reset.mjs +17 -4
- package/library/actions/shared/assets/visible-state.mjs +176 -0
- package/library/actions/shared/networks/visible-state.mjs +70 -0
- package/library/actions/shared/perps/visible-state.mjs +354 -0
- package/library/actions/shared/swap-bridge/transaction.mjs +54 -0
- package/library/actions/shared/swap-bridge/visible-state.mjs +97 -0
- package/library/actions/shared/ui/locators.mjs +7 -0
- package/library/manifests/extension.action-manifest.json +1366 -184
- package/library/manifests/mobile.action-manifest.json +235 -1
- package/library/recipes/assets/release-custom-gas-send.recipe.json +88 -0
- package/library/recipes/assets/release-custom-network-token-persistence.recipe.json +106 -0
- package/library/recipes/assets/release-native-max-cancel.recipe.json +70 -0
- package/library/recipes/assets/release-native-max-send.recipe.json +78 -0
- package/library/recipes/assets/release-own-account-send.recipe.json +80 -0
- package/library/recipes/assets/release-token-address-import.recipe.json +47 -0
- package/library/recipes/assets/release-token-details.recipe.json +48 -0
- package/library/recipes/assets/release-token-hide-readd.recipe.json +55 -0
- package/library/recipes/assets/release-token-list.recipe.json +75 -0
- package/library/recipes/assets/release-token-persistence.recipe.json +73 -0
- package/library/recipes/assets/release-token-search-import.recipe.json +40 -0
- package/library/recipes/assets/release-token-sorting.recipe.json +29 -0
- package/library/recipes/extension/networks/release-balance-stability.recipe.json +51 -0
- package/library/recipes/extension/perps/release-live-limit-order.recipe.json +104 -0
- package/library/recipes/extension/perps/release-order-entry.recipe.json +155 -0
- package/library/recipes/extension/perps/release-order-validation.recipe.json +220 -0
- package/library/recipes/extension/runner/action-validation.recipe.json +12 -1
- package/library/recipes/perps/release-activity-filters.recipe.json +51 -0
- package/library/recipes/perps/release-funds-flows.recipe.json +76 -0
- package/library/recipes/perps/release-live-market-order.recipe.json +122 -0
- package/library/recipes/perps/release-market-details.recipe.json +83 -0
- package/library/recipes/perps/release-market-search.recipe.json +79 -0
- package/library/recipes/perps/release-watchlist.recipe.json +99 -0
- package/library/recipes/swap-bridge/release-custom-slippage.recipe.json +90 -0
- package/library/recipes/swap-bridge/release-live-max-swap.recipe.json +99 -0
- package/library/recipes/swap-bridge/release-live-swap.recipe.json +101 -0
- package/library/recipes/swap-bridge/release-max-quote.recipe.json +72 -0
- package/library/recipes/swap-bridge/release-quote.recipe.json +72 -0
- package/library/recipes/swap-bridge/release-surface.recipe.json +43 -0
- package/library/recipes/wallet/import.recipe.json +20 -1
- package/library/recipes/wallet/reset-import.recipe.json +20 -1
- package/package.json +6 -5
- package/scripts/validate-release-report.mjs +319 -0
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
acquireExtensionReleaseArtifact
|
|
6
|
+
} from "../adapters/extension/release-artifact.js";
|
|
7
|
+
import { installAndroidReleaseArtifact } from "../adapters/mobile/release-artifact.js";
|
|
8
|
+
import {
|
|
9
|
+
clearMobileReleaseArtifactState,
|
|
10
|
+
writeMobileReleaseArtifactState
|
|
11
|
+
} from "../adapters/mobile/release-artifact-state.js";
|
|
4
12
|
import { recipeHarnessPath } from "../paths.js";
|
|
5
13
|
import {
|
|
6
14
|
adapterOption,
|
|
@@ -13,9 +21,56 @@ import {
|
|
|
13
21
|
usageError
|
|
14
22
|
} from "./parse-args.js";
|
|
15
23
|
async function handleRuntimeLaunch({ options }) {
|
|
24
|
+
if (optionFlag(options, "help")) {
|
|
25
|
+
console.log(`mm-harness runtime-launch --adapter <extension|mobile> --target <checkout> [flags]
|
|
26
|
+
|
|
27
|
+
--artifact-file <path> Artifact downloaded by the validation skill
|
|
28
|
+
--artifact-version <X.Y.Z> Expected manifest or Android version name
|
|
29
|
+
--artifact-sha256 <digest> Trusted SHA-256 from the acquisition step
|
|
30
|
+
--artifact-build-id <id> Android version code
|
|
31
|
+
--artifact-package-id <id> Android application ID
|
|
32
|
+
--artifact-cache <dir> Artifact cache root
|
|
33
|
+
--platform <android> Mobile artifact platform (iOS is unsupported)
|
|
34
|
+
--device <serial> Explicit Android device serial
|
|
35
|
+
--cdp-port <port> Extension CDP port
|
|
36
|
+
--chrome-user-data-dir Isolated Chrome profile path
|
|
37
|
+
--artifacts-dir <dir> Validation evidence directory
|
|
38
|
+
--json Machine-readable terminal report
|
|
39
|
+
|
|
40
|
+
The harness verifies and runs a local artifact but never downloads one. Extension
|
|
41
|
+
launch resets an isolated profile. Android install requires an explicit serial.`);
|
|
42
|
+
return 0;
|
|
43
|
+
}
|
|
16
44
|
const adapter = adapterOption(options);
|
|
17
45
|
const target = targetPath(options);
|
|
46
|
+
const artifactFile = optionString(options, "artifactFile");
|
|
47
|
+
const artifactVersion = optionString(options, "artifactVersion");
|
|
48
|
+
const artifactSha256 = optionString(options, "artifactSha256");
|
|
49
|
+
const artifactBuildId = optionString(options, "artifactBuildId");
|
|
50
|
+
const artifactPackageId = optionString(options, "artifactPackageId");
|
|
51
|
+
const artifactCache = optionString(options, "artifactCache");
|
|
52
|
+
const artifactRequested = Boolean(
|
|
53
|
+
artifactFile || artifactVersion || artifactSha256 || artifactBuildId || artifactPackageId || artifactCache
|
|
54
|
+
);
|
|
55
|
+
if (artifactRequested && (!artifactFile || !artifactVersion || !artifactSha256)) {
|
|
56
|
+
throw usageError("artifact launch requires --artifact-file, --artifact-version, and --artifact-sha256 together.");
|
|
57
|
+
}
|
|
58
|
+
if (adapter === "mobile") {
|
|
59
|
+
return handleAndroidArtifactLaunch({
|
|
60
|
+
options,
|
|
61
|
+
target,
|
|
62
|
+
artifactFile,
|
|
63
|
+
artifactVersion,
|
|
64
|
+
artifactSha256,
|
|
65
|
+
artifactBuildId,
|
|
66
|
+
artifactPackageId,
|
|
67
|
+
artifactCache
|
|
68
|
+
});
|
|
69
|
+
}
|
|
18
70
|
if (adapter !== "extension") throw new Error("runtime-launch currently applies to the extension adapter.");
|
|
71
|
+
if (artifactBuildId || artifactPackageId || optionString(options, "platform") || optionString(options, "device")) {
|
|
72
|
+
throw usageError("Extension runtime-launch does not accept Android artifact options.");
|
|
73
|
+
}
|
|
19
74
|
const cdpPort = parsePort(
|
|
20
75
|
optionString(options, "cdpPort") ?? process.env.CDP_PORT ?? process.env.RECIPE_CDP_PORT,
|
|
21
76
|
"runtime-launch requires --cdp-port <port>."
|
|
@@ -29,7 +84,19 @@ async function handleRuntimeLaunch({ options }) {
|
|
|
29
84
|
if (startWatch && buildLavaMoat) {
|
|
30
85
|
throw usageError("runtime-launch accepts only one of --start-watch or --build-lavamoat.");
|
|
31
86
|
}
|
|
32
|
-
|
|
87
|
+
if (artifactRequested && (startWatch || buildLavaMoat || optionString(options, "remoteFlag"))) {
|
|
88
|
+
throw usageError("artifact launch cannot be combined with build, watch, or remote-flag options.");
|
|
89
|
+
}
|
|
90
|
+
let artifact;
|
|
91
|
+
if (artifactRequested) {
|
|
92
|
+
artifact = await acquireExtensionReleaseArtifact({
|
|
93
|
+
file: artifactFile,
|
|
94
|
+
expectedVersion: artifactVersion,
|
|
95
|
+
expectedSha256: artifactSha256,
|
|
96
|
+
cacheRoot: artifactCache
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
const mode = artifact ? "release-artifact" : buildLavaMoat ? "build-lavamoat" : startWatch ? "start-watch" : "existing-dist";
|
|
33
100
|
const liveScript = recipeHarnessPath(target, "extension", "scripts", "live.sh");
|
|
34
101
|
const command = [
|
|
35
102
|
"bash",
|
|
@@ -42,6 +109,15 @@ async function handleRuntimeLaunch({ options }) {
|
|
|
42
109
|
"--artifacts-dir",
|
|
43
110
|
artifactsDir
|
|
44
111
|
];
|
|
112
|
+
if (artifact) {
|
|
113
|
+
command.push(
|
|
114
|
+
"--external-artifact",
|
|
115
|
+
"--dist-dir",
|
|
116
|
+
artifact.extensionDir,
|
|
117
|
+
"--artifact-provenance",
|
|
118
|
+
artifact.provenancePath
|
|
119
|
+
);
|
|
120
|
+
}
|
|
45
121
|
if (chromeUserDataDir) command.push("--chrome-user-data-dir", chromeUserDataDir);
|
|
46
122
|
const remoteFlag = optionString(options, "remoteFlag");
|
|
47
123
|
if (remoteFlag) command.push("--remote-flag", remoteFlag);
|
|
@@ -52,7 +128,7 @@ async function handleRuntimeLaunch({ options }) {
|
|
|
52
128
|
cdpPort,
|
|
53
129
|
artifactsDir,
|
|
54
130
|
reason: "harness_live_script_missing",
|
|
55
|
-
fix: `Run mm-harness install --adapter extension --target ${shellQuote(target)}, then rerun: ${runtimeLaunchCommand(target, cdpPort, chromeUserDataDir, mode)}`,
|
|
131
|
+
fix: `Run mm-harness install --adapter extension --target ${shellQuote(target)}, then rerun: ${runtimeLaunchCommand(target, cdpPort, chromeUserDataDir, mode, artifactInvocation(options))}`,
|
|
56
132
|
command
|
|
57
133
|
});
|
|
58
134
|
printRuntimeLaunchReport(report2, optionFlag(options, "json"));
|
|
@@ -61,7 +137,7 @@ async function handleRuntimeLaunch({ options }) {
|
|
|
61
137
|
fs.mkdirSync(artifactsDir, { recursive: true });
|
|
62
138
|
const jsonMode = optionFlag(options, "json");
|
|
63
139
|
if (!jsonMode) {
|
|
64
|
-
const detail = mode === "build-lavamoat" ? "production-like LavaMoat build" : mode === "start-watch" ? "test webpack watcher" : "existing dist";
|
|
140
|
+
const detail = mode === "release-artifact" ? `release artifact ${artifact?.manifestVersion}` : mode === "build-lavamoat" ? "production-like LavaMoat build" : mode === "start-watch" ? "test webpack watcher" : "existing dist";
|
|
65
141
|
console.error(`recipe: runtime-launch starting (cdp=${cdpPort}, ${detail})`);
|
|
66
142
|
} else {
|
|
67
143
|
console.error(JSON.stringify({
|
|
@@ -69,7 +145,7 @@ async function handleRuntimeLaunch({ options }) {
|
|
|
69
145
|
type: "progress",
|
|
70
146
|
command: "rebuild",
|
|
71
147
|
phase: "runtime-launch",
|
|
72
|
-
message: mode === "build-lavamoat" ? "production-like LavaMoat build starting" : mode === "start-watch" ? "test webpack watcher starting" : "launching existing dist",
|
|
148
|
+
message: mode === "release-artifact" ? `release artifact ${artifact?.manifestVersion} launch starting` : mode === "build-lavamoat" ? "production-like LavaMoat build starting" : mode === "start-watch" ? "test webpack watcher starting" : "launching existing dist",
|
|
73
149
|
cdpPort
|
|
74
150
|
}));
|
|
75
151
|
}
|
|
@@ -90,7 +166,8 @@ async function handleRuntimeLaunch({ options }) {
|
|
|
90
166
|
summaryPath,
|
|
91
167
|
reason: "runtime_ready",
|
|
92
168
|
fix: "",
|
|
93
|
-
command
|
|
169
|
+
command,
|
|
170
|
+
artifact
|
|
94
171
|
});
|
|
95
172
|
printRuntimeLaunchReport(report2, optionFlag(options, "json"));
|
|
96
173
|
return 0;
|
|
@@ -109,20 +186,111 @@ async function handleRuntimeLaunch({ options }) {
|
|
|
109
186
|
launchLogPath: fs.existsSync(launchLogPath) ? launchLogPath : void 0,
|
|
110
187
|
verifySummaryPath: fs.existsSync(verifySummaryPath) ? verifySummaryPath : void 0,
|
|
111
188
|
reason,
|
|
112
|
-
fix: `Read ${failurePath}, fix the first error, then rerun: ${runtimeLaunchCommand(target, cdpPort, chromeUserDataDir, mode)}`,
|
|
189
|
+
fix: `Read ${failurePath}, fix the first error, then rerun: ${runtimeLaunchCommand(target, cdpPort, chromeUserDataDir, mode, artifactInvocation(options))}`,
|
|
113
190
|
command,
|
|
191
|
+
artifact,
|
|
114
192
|
exitCode: result.status ?? 1
|
|
115
193
|
});
|
|
116
194
|
printRuntimeLaunchReport(report, optionFlag(options, "json"));
|
|
117
195
|
return 1;
|
|
118
196
|
}
|
|
197
|
+
async function handleAndroidArtifactLaunch(input) {
|
|
198
|
+
const {
|
|
199
|
+
options,
|
|
200
|
+
target,
|
|
201
|
+
artifactFile,
|
|
202
|
+
artifactVersion,
|
|
203
|
+
artifactSha256,
|
|
204
|
+
artifactBuildId,
|
|
205
|
+
artifactPackageId,
|
|
206
|
+
artifactCache
|
|
207
|
+
} = input;
|
|
208
|
+
const platform = optionString(options, "platform") ?? "android";
|
|
209
|
+
if (platform === "ios") {
|
|
210
|
+
throw usageError("Downloaded iOS artifact launch is unsupported. Next: use --platform android with a local APK.");
|
|
211
|
+
}
|
|
212
|
+
if (platform !== "android") throw usageError("--platform must be android for Mobile artifact launch.");
|
|
213
|
+
if (!artifactFile || !artifactVersion || !artifactSha256 || !artifactBuildId || !artifactPackageId) {
|
|
214
|
+
throw usageError(
|
|
215
|
+
"Android artifact launch requires --artifact-file, --artifact-version, --artifact-build-id, --artifact-package-id, and --artifact-sha256 together."
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
const deviceSerial = optionString(options, "device");
|
|
219
|
+
if (!deviceSerial) {
|
|
220
|
+
throw usageError("Android artifact launch requires --device <serial>. Next: run adb devices -l and rerun with one serial.");
|
|
221
|
+
}
|
|
222
|
+
if (artifactCache || optionString(options, "cdpPort") || optionString(options, "chromeUserDataDir") || optionString(options, "remoteFlag") || optionFlag(options, "startWatch") || optionFlag(options, "buildLavamoat")) {
|
|
223
|
+
throw usageError("Android artifact launch cannot be combined with Extension build, profile, cache, or CDP options.");
|
|
224
|
+
}
|
|
225
|
+
const artifactsDir = path.resolve(
|
|
226
|
+
optionString(options, "artifactsDir") ?? recipeHarnessPath(target, "mobile", "runtime-launch", (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/gu, ""))
|
|
227
|
+
);
|
|
228
|
+
fs.mkdirSync(artifactsDir, { recursive: true });
|
|
229
|
+
const summaryPath = path.join(artifactsDir, "summary.json");
|
|
230
|
+
try {
|
|
231
|
+
clearMobileReleaseArtifactState(target);
|
|
232
|
+
const artifact = await installAndroidReleaseArtifact({
|
|
233
|
+
file: artifactFile,
|
|
234
|
+
expectedVersion: artifactVersion,
|
|
235
|
+
expectedBuildId: artifactBuildId,
|
|
236
|
+
expectedSha256: artifactSha256,
|
|
237
|
+
packageId: artifactPackageId,
|
|
238
|
+
deviceSerial
|
|
239
|
+
});
|
|
240
|
+
const runtimeState = writeMobileReleaseArtifactState(target, artifact);
|
|
241
|
+
const report = runtimeLaunchReport("pass", {
|
|
242
|
+
adapter: "mobile",
|
|
243
|
+
platform,
|
|
244
|
+
target,
|
|
245
|
+
artifactsDir,
|
|
246
|
+
summaryPath,
|
|
247
|
+
reason: "runtime_ready",
|
|
248
|
+
fix: "",
|
|
249
|
+
artifact,
|
|
250
|
+
runtimeState
|
|
251
|
+
});
|
|
252
|
+
fs.writeFileSync(summaryPath, `${JSON.stringify(report, null, 2)}
|
|
253
|
+
`, { encoding: "utf8", mode: 384 });
|
|
254
|
+
printRuntimeLaunchReport(report, optionFlag(options, "json"));
|
|
255
|
+
return 0;
|
|
256
|
+
} catch (error) {
|
|
257
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
258
|
+
const report = runtimeLaunchReport("fail", {
|
|
259
|
+
adapter: "mobile",
|
|
260
|
+
platform,
|
|
261
|
+
target,
|
|
262
|
+
artifactsDir,
|
|
263
|
+
summaryPath,
|
|
264
|
+
reason: "android_artifact_launch_failed",
|
|
265
|
+
error: message,
|
|
266
|
+
fix: `Fix the first error, then rerun with --device ${shellQuote(deviceSerial)} and the same verified artifact identity.`
|
|
267
|
+
});
|
|
268
|
+
fs.writeFileSync(summaryPath, `${JSON.stringify(report, null, 2)}
|
|
269
|
+
`, { encoding: "utf8", mode: 384 });
|
|
270
|
+
printRuntimeLaunchReport(report, optionFlag(options, "json"));
|
|
271
|
+
return 1;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
119
274
|
function runtimeLaunchLiveFlag(mode) {
|
|
120
|
-
return mode === "existing-dist" ? "--launch-existing-dist" : `--${mode}`;
|
|
275
|
+
return mode === "existing-dist" || mode === "release-artifact" ? "--launch-existing-dist" : `--${mode}`;
|
|
121
276
|
}
|
|
122
|
-
function runtimeLaunchCommand(target, cdpPort, chromeUserDataDir, mode = "existing-dist") {
|
|
277
|
+
function runtimeLaunchCommand(target, cdpPort, chromeUserDataDir, mode = "existing-dist", artifact) {
|
|
123
278
|
const base = `mm-harness runtime-launch --adapter extension --target ${shellQuote(target)} --cdp-port ${cdpPort}`;
|
|
124
|
-
|
|
125
|
-
|
|
279
|
+
let command = mode === "existing-dist" || mode === "release-artifact" ? base : `${base} --${mode}`;
|
|
280
|
+
if (artifact?.file) command += ` --artifact-file ${shellQuote(artifact.file)}`;
|
|
281
|
+
if (artifact?.version) command += ` --artifact-version ${shellQuote(artifact.version)}`;
|
|
282
|
+
if (artifact?.sha256) command += ` --artifact-sha256 ${shellQuote(artifact.sha256)}`;
|
|
283
|
+
if (artifact?.cache) command += ` --artifact-cache ${shellQuote(artifact.cache)}`;
|
|
284
|
+
return chromeUserDataDir ? `${command} --chrome-user-data-dir ${shellQuote(chromeUserDataDir)}` : command;
|
|
285
|
+
}
|
|
286
|
+
function artifactInvocation(options) {
|
|
287
|
+
const artifact = {
|
|
288
|
+
file: optionString(options, "artifactFile"),
|
|
289
|
+
version: optionString(options, "artifactVersion"),
|
|
290
|
+
sha256: optionString(options, "artifactSha256"),
|
|
291
|
+
cache: optionString(options, "artifactCache")
|
|
292
|
+
};
|
|
293
|
+
return Object.values(artifact).some(Boolean) ? artifact : void 0;
|
|
126
294
|
}
|
|
127
295
|
function runtimeLaunchReport(status, fields) {
|
|
128
296
|
return {
|
package/dist/heal-bounds.js
CHANGED
|
@@ -86,7 +86,7 @@ function checkHealBounds(target, output, state) {
|
|
|
86
86
|
originalError
|
|
87
87
|
};
|
|
88
88
|
}
|
|
89
|
-
const failureClass = classifyFailure(output);
|
|
89
|
+
const failureClass = classifyFailure(conciseFailureForHuman(output));
|
|
90
90
|
if (failureClass === "screenshot-protected") {
|
|
91
91
|
return {
|
|
92
92
|
code: "SCREENSHOT_PROTECTED",
|
|
@@ -158,7 +158,9 @@ async function prepareLiveAdapterScript({
|
|
|
158
158
|
target: `node${process.versions.node.split(".")[0]}`,
|
|
159
159
|
legalComments: "none",
|
|
160
160
|
sourcemap: false,
|
|
161
|
-
banner: {
|
|
161
|
+
banner: {
|
|
162
|
+
js: `import { createRequire as __mmCreateRequire } from 'node:module';const require = __mmCreateRequire(import.meta.url);process.argv[1] = ${JSON.stringify(entryReal)};`
|
|
163
|
+
},
|
|
162
164
|
plugins: [bindImportMetaUrls()],
|
|
163
165
|
logLevel: "silent"
|
|
164
166
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
function isRecord(value) {
|
|
2
2
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
3
3
|
}
|
|
4
|
-
function validateMetaMaskActionInputs(recipe) {
|
|
4
|
+
function validateMetaMaskActionInputs(recipe, adapter) {
|
|
5
5
|
if (!isRecord(recipe) || !isRecord(recipe.workflow)) return [];
|
|
6
6
|
const nodes = isRecord(recipe.workflow.nodes) ? recipe.workflow.nodes : void 0;
|
|
7
7
|
if (!nodes) return [];
|
|
@@ -9,6 +9,52 @@ function validateMetaMaskActionInputs(recipe) {
|
|
|
9
9
|
for (const [nodeId, node] of Object.entries(nodes)) {
|
|
10
10
|
if (!isRecord(node)) continue;
|
|
11
11
|
const action = node.action;
|
|
12
|
+
if (action === "metamask.perps.read_snapshot" && adapter === "extension") {
|
|
13
|
+
if (node.market !== void 0 && node.markets !== void 0) {
|
|
14
|
+
findings.push({
|
|
15
|
+
severity: "error",
|
|
16
|
+
code: "recipe.invalid_param",
|
|
17
|
+
path: `workflow.nodes.${nodeId}.markets`,
|
|
18
|
+
message: `${action} accepts market or markets, not both`
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
if (isRecord(node.history)) {
|
|
22
|
+
const start = node.history.start_time_ms;
|
|
23
|
+
const end = node.history.end_time_ms;
|
|
24
|
+
if (typeof start === "number" && typeof end === "number" && start > end) {
|
|
25
|
+
findings.push({
|
|
26
|
+
severity: "error",
|
|
27
|
+
code: "recipe.invalid_param",
|
|
28
|
+
path: `workflow.nodes.${nodeId}.history.start_time_ms`,
|
|
29
|
+
message: `${action} history.start_time_ms must not exceed history.end_time_ms`
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
if (typeof node.history.limit === "number" && node.history.limit > 500) {
|
|
33
|
+
findings.push({
|
|
34
|
+
severity: "error",
|
|
35
|
+
code: "recipe.invalid_param",
|
|
36
|
+
path: `workflow.nodes.${nodeId}.history.limit`,
|
|
37
|
+
message: `${action} history.limit must not exceed 500`
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (typeof node.max_items_per_section === "number" && node.max_items_per_section > 500) {
|
|
42
|
+
findings.push({
|
|
43
|
+
severity: "error",
|
|
44
|
+
code: "recipe.invalid_param",
|
|
45
|
+
path: `workflow.nodes.${nodeId}.max_items_per_section`,
|
|
46
|
+
message: `${action} max_items_per_section must not exceed 500`
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
if (typeof node.timeout_ms === "number" && node.timeout_ms > 6e4) {
|
|
50
|
+
findings.push({
|
|
51
|
+
severity: "error",
|
|
52
|
+
code: "recipe.invalid_param",
|
|
53
|
+
path: `workflow.nodes.${nodeId}.timeout_ms`,
|
|
54
|
+
message: `${action} timeout_ms must not exceed 60000`
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
12
58
|
const state = typeof node.state === "string" ? node.state.toLowerCase() : void 0;
|
|
13
59
|
const createsOrder = action === "metamask.perps.place_order" || (action === "metamask.perps.ensure_positions" || action === "metamask.perps.ensure_orders") && (state === "open" || state === "present");
|
|
14
60
|
if (!createsOrder) continue;
|
package/dist/mm-harness-cli.js
CHANGED
|
@@ -199,6 +199,7 @@ Example:
|
|
|
199
199
|
--list List actions accepted by call for this adapter; no <action> needed
|
|
200
200
|
--arg k=v Action field value (repeatable; equivalent to key=value shorthand)
|
|
201
201
|
--device <udid|serial|name> Mobile only: target this device (sets IOS_SIMULATOR / ADB_SERIAL and updates agentic-runtime.json). Without it, >1 connected mobile device fails fast and lists them. Do not shut down other devices to force selection.
|
|
202
|
+
--cdp-port <port> Extension only: bind the action to this live browser runtime.
|
|
202
203
|
--adapter <mobile|extension|core> Target adapter (auto-detected inside a checkout)
|
|
203
204
|
--target <path> Checkout path (default: cwd)
|
|
204
205
|
--artifacts-dir <dir> Where to write evidence (default: temp dir)
|
|
@@ -537,6 +538,36 @@ Example:
|
|
|
537
538
|
mm-harness update
|
|
538
539
|
mm-harness update --check --json`
|
|
539
540
|
},
|
|
541
|
+
{
|
|
542
|
+
name: "runtime-launch",
|
|
543
|
+
summary: "Launch and verify an Extension runtime or local Android release artifact.",
|
|
544
|
+
example: "mm-harness runtime-launch --adapter extension --target <checkout> --cdp-port 9222 --artifact-file <zip> --artifact-version <X.Y.Z> --artifact-sha256 <digest>",
|
|
545
|
+
helpText: `mm-harness runtime-launch --adapter <extension|mobile> --target <checkout> [flags]
|
|
546
|
+
|
|
547
|
+
Verify and run an artifact already downloaded by the validation skill. The
|
|
548
|
+
harness performs no network acquisition. Extension ZIPs run through the
|
|
549
|
+
packaged browser/CDP path; Android APKs install to one explicitly named device.
|
|
550
|
+
|
|
551
|
+
--artifact-file <path> Artifact downloaded by the validation skill
|
|
552
|
+
--artifact-version <X.Y.Z> Expected manifest or Android version name
|
|
553
|
+
--artifact-build-id <id> Android version code
|
|
554
|
+
--artifact-package-id <id> Android application ID
|
|
555
|
+
--artifact-sha256 <digest> Trusted SHA-256 from the acquisition step
|
|
556
|
+
--artifact-cache <dir> Extension artifact cache root
|
|
557
|
+
--platform <android> Mobile platform (downloaded iOS is unsupported)
|
|
558
|
+
--device <serial> Explicit Android device serial
|
|
559
|
+
--cdp-port <port> Extension CDP port
|
|
560
|
+
--chrome-user-data-dir <dir> Isolated Chrome profile path
|
|
561
|
+
--artifacts-dir <dir> Validation evidence directory
|
|
562
|
+
--start-watch Build through the checkout watcher
|
|
563
|
+
--build-lavamoat Build through the production-like path
|
|
564
|
+
--remote-flag <KEY=VARIANT> Pin checkout snapshot flags (not allowed for artifacts)
|
|
565
|
+
--json Machine-readable terminal report
|
|
566
|
+
|
|
567
|
+
Example:
|
|
568
|
+
mm-harness runtime-launch --adapter extension --target /path/to/checkout --cdp-port 9222 --artifact-file /path/to/metamask-chrome-X.Y.Z.zip --artifact-version X.Y.Z --artifact-sha256 <digest>
|
|
569
|
+
mm-harness runtime-launch --adapter mobile --platform android --target /path/to/checkout --device <serial> --artifact-file /path/to/app.apk --artifact-version X.Y.Z --artifact-build-id <code> --artifact-package-id io.metamask --artifact-sha256 <digest>`
|
|
570
|
+
},
|
|
540
571
|
{
|
|
541
572
|
name: "fixtures",
|
|
542
573
|
summary: "Manage the canonical wallet fixture (wallet DATA only) \u2014 init / sync / set / reset / generate / finalize.",
|
|
@@ -745,7 +776,6 @@ const HIDDEN = [
|
|
|
745
776
|
"completion-candidates",
|
|
746
777
|
"runtime-health",
|
|
747
778
|
"runtime-decision",
|
|
748
|
-
"runtime-launch",
|
|
749
779
|
"resolve-extension",
|
|
750
780
|
"ensure-ready",
|
|
751
781
|
"self-test"
|
package/dist/recipe-security.js
CHANGED
|
@@ -3,10 +3,19 @@ import { lstat, readFile, readdir, realpath } from "node:fs/promises";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
const READ_ONLY_CUSTOM_ACTIONS = /* @__PURE__ */ new Set([
|
|
5
5
|
"ui.locators",
|
|
6
|
+
"ui.activate_and_observe",
|
|
6
7
|
"metamask.wallet.fixture_status",
|
|
7
8
|
"metamask.wallet.validate_import",
|
|
8
9
|
"metamask.wallet.list_accounts",
|
|
9
10
|
"metamask.wallet.read_state",
|
|
11
|
+
"metamask.assets.read_visible_state",
|
|
12
|
+
"metamask.assets.read_details",
|
|
13
|
+
"metamask.networks.read_visible_state",
|
|
14
|
+
"metamask.perps.read_visible_state",
|
|
15
|
+
"metamask.perps.compare_provider_market",
|
|
16
|
+
"metamask.perps.read_eligibility",
|
|
17
|
+
"metamask.perps.read_funds_confirmation",
|
|
18
|
+
"metamask.swap_bridge.read_visible_state",
|
|
10
19
|
"metamask.perps.read_positions",
|
|
11
20
|
"metamask.perps.read_orders",
|
|
12
21
|
"metamask.perps.read_account",
|
|
@@ -19,15 +28,37 @@ const READ_ONLY_CUSTOM_ACTIONS = /* @__PURE__ */ new Set([
|
|
|
19
28
|
"metamask.analytics.assert_events"
|
|
20
29
|
]);
|
|
21
30
|
const APP_MUTATION_CUSTOM_ACTIONS = /* @__PURE__ */ new Set([
|
|
31
|
+
"ui.activate_and_observe",
|
|
32
|
+
"metamask.assets.verify_sorting",
|
|
33
|
+
"metamask.assets.set_token_visibility",
|
|
34
|
+
"metamask.assets.import_custom_token",
|
|
35
|
+
"metamask.assets.prepare_send",
|
|
36
|
+
"metamask.assets.review_send",
|
|
37
|
+
"metamask.assets.finish_send",
|
|
38
|
+
"metamask.assets.set_custom_gas",
|
|
39
|
+
"metamask.assets.select_network_scope",
|
|
40
|
+
"metamask.wallet.lock",
|
|
41
|
+
"metamask.networks.add_custom",
|
|
42
|
+
"metamask.networks.add_chainlist",
|
|
43
|
+
"metamask.networks.remove_custom",
|
|
22
44
|
"metamask.wallet.setup",
|
|
23
45
|
"metamask.wallet.import",
|
|
24
46
|
"metamask.wallet.reset",
|
|
25
47
|
"metamask.wallet.ensure_unlocked",
|
|
26
48
|
"metamask.wallet.select_account",
|
|
49
|
+
"metamask.swap_bridge.select_assets",
|
|
50
|
+
"metamask.swap_bridge.set_amount",
|
|
51
|
+
"metamask.swap_bridge.set_max_amount",
|
|
52
|
+
"metamask.swap_bridge.set_slippage",
|
|
53
|
+
"metamask.swap_bridge.submit_transaction",
|
|
54
|
+
"metamask.settings.set_basic_functionality",
|
|
27
55
|
"metamask.deeplink.open",
|
|
28
56
|
"metamask.perps.close_positions",
|
|
29
57
|
"metamask.perps.close_orders",
|
|
30
58
|
"metamask.perps.place_order",
|
|
59
|
+
"metamask.perps.close_visible_position",
|
|
60
|
+
"metamask.perps.open_position_action",
|
|
61
|
+
"metamask.perps.edit_margin",
|
|
31
62
|
"metamask.perps.edit_order",
|
|
32
63
|
"metamask.perps.update_position_tpsl",
|
|
33
64
|
"metamask.perps.ensure_positions",
|
|
@@ -35,15 +66,24 @@ const APP_MUTATION_CUSTOM_ACTIONS = /* @__PURE__ */ new Set([
|
|
|
35
66
|
"metamask.perps.clear_performance_caches",
|
|
36
67
|
"metamask.perps.measure_homepage_visible",
|
|
37
68
|
"metamask.perps.prepare_local_snapshot_endpoint",
|
|
69
|
+
"metamask.perps.set_market_favorite",
|
|
70
|
+
"metamask.perps.select_activity_filter",
|
|
71
|
+
"metamask.perps.select_market_filter",
|
|
72
|
+
"metamask.perps.open_balance_action",
|
|
38
73
|
"metamask.perps.start_state",
|
|
39
74
|
"metamask.perps.teardown_state",
|
|
40
75
|
// Consent changes wallet state and is therefore an app mutation.
|
|
41
76
|
"metamask.analytics.set_consent"
|
|
42
77
|
]);
|
|
43
78
|
const EXTERNAL_MUTATION_CUSTOM_ACTIONS = /* @__PURE__ */ new Set([
|
|
79
|
+
"ui.activate_and_observe",
|
|
80
|
+
"metamask.assets.finish_send",
|
|
81
|
+
"metamask.swap_bridge.submit_transaction",
|
|
44
82
|
"metamask.perps.close_positions",
|
|
45
83
|
"metamask.perps.close_orders",
|
|
46
84
|
"metamask.perps.place_order",
|
|
85
|
+
"metamask.perps.close_visible_position",
|
|
86
|
+
"metamask.perps.edit_margin",
|
|
47
87
|
"metamask.perps.edit_order",
|
|
48
88
|
"metamask.perps.update_position_tpsl",
|
|
49
89
|
"metamask.perps.ensure_positions",
|
package/dist/run-diagnostics.js
CHANGED
|
@@ -32,7 +32,7 @@ function formatRunDiagnosticsForHuman(diagnostics, adapter) {
|
|
|
32
32
|
async function beginRunDiagnostics(adapter, projectRoot) {
|
|
33
33
|
const source = getAdapterSurface(adapter).appLogSource(projectRoot);
|
|
34
34
|
if (!source) return null;
|
|
35
|
-
const mobileIssueBuffer = adapter === "mobile" && armMobileIssueBuffer(projectRoot) ? { projectRoot } : void 0;
|
|
35
|
+
const mobileIssueBuffer = adapter === "mobile" && process.env.METAMASK_RECIPE_MOBILE_OPAQUE_RUNTIME !== "1" && armMobileIssueBuffer(projectRoot) ? { projectRoot } : void 0;
|
|
36
36
|
if (adapter === "extension") {
|
|
37
37
|
await ensureExtensionConsoleCapture(projectRoot).catch(() => void 0);
|
|
38
38
|
}
|
package/dist/runner.js
CHANGED
|
@@ -246,6 +246,9 @@ function mobileSourceAwareLifecycleAdapters(adapter, lifecycle, readinessProbe =
|
|
|
246
246
|
return lifecycle.map((entry) => ({
|
|
247
247
|
...entry,
|
|
248
248
|
async execute(node, context) {
|
|
249
|
+
if (context.env?.METAMASK_RECIPE_MOBILE_OPAQUE_RUNTIME === "1") {
|
|
250
|
+
return entry.execute(node, context);
|
|
251
|
+
}
|
|
249
252
|
const command = node.command ?? node.event ?? node.state;
|
|
250
253
|
const reloadsSource = command === "launch" || command === "foreground" || command === "restart";
|
|
251
254
|
const checksIosContinuity = command === "foreground" && !isAndroidLifecycle(node, context.env ?? {});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# Release QA capability map
|
|
2
|
+
|
|
3
|
+
This document maps the current mm-harness release-validation surface to the
|
|
4
|
+
MetaMask Extension QA checklists at commit
|
|
5
|
+
[`77591c5`](https://github.com/MetaMask/experimental-mm-qa-ai-tasks/tree/77591c5998fcf6368417fd5be01871e6c9c3dc5b/tasks/rc-extension).
|
|
6
|
+
It describes reusable capabilities, not a release verdict. A release verdict
|
|
7
|
+
must come from the case inventory, terminal traces, and completion gate for that
|
|
8
|
+
specific artifact.
|
|
9
|
+
|
|
10
|
+
The referenced plans contain 135 cases:
|
|
11
|
+
|
|
12
|
+
- Perps: [64 cases](https://github.com/MetaMask/experimental-mm-qa-ai-tasks/blob/77591c5998fcf6368417fd5be01871e6c9c3dc5b/tasks/rc-extension/perps-rc/perps-rc-regression.md)
|
|
13
|
+
- Assets: [22 cases](https://github.com/MetaMask/experimental-mm-qa-ai-tasks/blob/77591c5998fcf6368417fd5be01871e6c9c3dc5b/tasks/rc-extension/assets-rc/assets-rc-regression.md)
|
|
14
|
+
- Swap and Bridge: [39 cases](https://github.com/MetaMask/experimental-mm-qa-ai-tasks/blob/77591c5998fcf6368417fd5be01871e6c9c3dc5b/tasks/rc-extension/swap-bridge-rc/swap-bridge-rc-regression.md)
|
|
15
|
+
- Networks: [10 cases](https://github.com/MetaMask/experimental-mm-qa-ai-tasks/blob/77591c5998fcf6368417fd5be01871e6c9c3dc5b/tasks/rc-extension/networks-regression-rc/networks-regression-rc.md)
|
|
16
|
+
|
|
17
|
+
## Artifact boundary
|
|
18
|
+
|
|
19
|
+
Artifact discovery and download belong to the release-validation workflow. The
|
|
20
|
+
workflow supplies mm-harness with a local artifact, expected product identity,
|
|
21
|
+
and trusted SHA-256. mm-harness never discovers or downloads a release.
|
|
22
|
+
|
|
23
|
+
For Extension, `runtime-launch` verifies the ZIP digest and manifest version,
|
|
24
|
+
extracts it safely, records archive and tree provenance, copies the exact tree
|
|
25
|
+
to an isolated runtime directory, resets only the selected Chrome profile, and
|
|
26
|
+
launches without building or patching the checkout.
|
|
27
|
+
|
|
28
|
+
For Android, `runtime-launch` requires one physical-device serial, verifies the
|
|
29
|
+
local APK digest, package ID, version name, and version code, installs it, and
|
|
30
|
+
binds subsequent recipes to the installed APK digest and selected device. The
|
|
31
|
+
release path does not use Metro, a development bridge, or the HUD.
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
mm-harness runtime-launch \
|
|
35
|
+
--adapter extension \
|
|
36
|
+
--target /path/to/metamask-extension \
|
|
37
|
+
--cdp-port 9222 \
|
|
38
|
+
--artifact-file /path/to/metamask-chrome-X.Y.Z.zip \
|
|
39
|
+
--artifact-version X.Y.Z \
|
|
40
|
+
--artifact-sha256 <trusted-digest>
|
|
41
|
+
|
|
42
|
+
mm-harness runtime-launch \
|
|
43
|
+
--adapter mobile \
|
|
44
|
+
--platform android \
|
|
45
|
+
--target /path/to/metamask-mobile \
|
|
46
|
+
--device <physical-device-serial> \
|
|
47
|
+
--artifact-file /path/to/app.apk \
|
|
48
|
+
--artifact-version X.Y.Z \
|
|
49
|
+
--artifact-build-id <version-code> \
|
|
50
|
+
--artifact-package-id io.metamask \
|
|
51
|
+
--artifact-sha256 <trusted-digest>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Completion gate
|
|
55
|
+
|
|
56
|
+
[`scripts/validate-release-report.mjs`](../scripts/validate-release-report.mjs)
|
|
57
|
+
compares a canonical case inventory with terminal verdicts. It rejects missing
|
|
58
|
+
or unknown IDs, duplicate IDs, non-terminal statuses, absent evidence, evidence
|
|
59
|
+
outside the declared root, incomplete run directories, and any non-empty
|
|
60
|
+
`remainingCases`. A passing gate writes `metadata.json`,
|
|
61
|
+
`report-quality.json`, and `report.md`.
|
|
62
|
+
|
|
63
|
+
Supported verdicts are `PASS`, `FAIL`, `BLOCKED_FUNDS`, `BLOCKED_COST`, and
|
|
64
|
+
`BLOCKED_ENVIRONMENT`. Product failures must remain failures; infrastructure,
|
|
65
|
+
funding, and cost limits must not be reported as passes.
|
|
66
|
+
|
|
67
|
+
## Capability matrix
|
|
68
|
+
|
|
69
|
+
`AVAILABLE` means a typed action or bundled recipe covers the reusable
|
|
70
|
+
operation and produces machine-readable evidence. `PARTIAL` means the generic
|
|
71
|
+
operation exists but the complete checklist family still depends on task-local
|
|
72
|
+
composition, live funds, or deterministic external state. `MISSING` means the
|
|
73
|
+
current manifests do not expose the required durable capability.
|
|
74
|
+
|
|
75
|
+
### Perps
|
|
76
|
+
|
|
77
|
+
| Case family | Required state and behavior | Current capability | Status |
|
|
78
|
+
| --- | --- | --- | --- |
|
|
79
|
+
| Navigation, home, markets, and details | Eligible account, live catalog, market summaries, chart, statistics, and route recovery | Visible-state reads, market navigation/search/filter, provider comparison, eligibility reads, and market-detail recipes | **AVAILABLE** for visible live flows |
|
|
80
|
+
| Watchlist and activity | Existing favorites and typed activity rows | Favorite mutation, activity-filter selection, and release recipes | **AVAILABLE** |
|
|
81
|
+
| Order entry | Funded Perps balance, market/limit inputs, validation, preview, fill, and duplicate protection | Parameterized order placement plus visible order-entry and validation recipes | **PARTIAL**: live state and provider outcomes remain release-dependent |
|
|
82
|
+
| Position management | Open position, close, margin, and TP/SL changes | Visible position actions, close, edit-margin, and TP/SL actions | **PARTIAL**: exact outcome depends on live account state |
|
|
83
|
+
| Deposit and withdrawal | Wallet funds, Perps balance, confirmations, and resulting activity | Balance actions and confirmation reads with a funds-flow recipe | **PARTIAL**: funds and provider availability are external |
|
|
84
|
+
| Snapshot assertions | Positions, orders, markets, history, and watchlist | Bounded redacted snapshot when the background hook exists; visible-state reads on untouched releases | **AVAILABLE** with runtime-specific observation paths |
|
|
85
|
+
| Deterministic seeded provider state | Balance, positions, limit orders, TP/SL, history, and controlled REST/stream behavior | Existing setup fixtures do not reproduce the complete QA provider model inside an untouched release artifact | **MISSING** |
|
|
86
|
+
|
|
87
|
+
### Assets
|
|
88
|
+
|
|
89
|
+
| Case family | Required state and behavior | Current capability | Status |
|
|
90
|
+
| --- | --- | --- | --- |
|
|
91
|
+
| Token list, prices, details, and sorting | Funded account with known holdings and price data | Visible token-list/details reads, network scope, sorting assertions, and bundled recipes | **AVAILABLE** for EVM token UI |
|
|
92
|
+
| Token search/import/hide/persistence | Known contracts and a persistent profile | Custom-token import, visibility changes, restart-safe recipes, and custom-network composition | **AVAILABLE** for Extension EVM tokens |
|
|
93
|
+
| Native/token send and gas | Funded owned accounts and live gas | Prepare, review, custom-gas, finish, and Max/cancel/send recipes | **PARTIAL**: terminal submission is gated by funds and cost policy |
|
|
94
|
+
| Aggregate Android holdings | Imported wallet with multi-chain assets | Native-accessibility token-list read on an untouched physical-device release | **AVAILABLE** |
|
|
95
|
+
| Balance streaming and aggregation arithmetic | Triggered inbound update and expected cross-network totals | Visible snapshots exist; deterministic inbound trigger and tolerance assertions are not bundled | **PARTIAL** |
|
|
96
|
+
| NFTs, DeFi, Solana send, and Bitcoin details | Indexed owned positions/assets and chain-specific transaction flows | Generic visible UI only | **MISSING** |
|
|
97
|
+
|
|
98
|
+
### Swap and Bridge
|
|
99
|
+
|
|
100
|
+
| Case family | Required state and behavior | Current capability | Status |
|
|
101
|
+
| --- | --- | --- | --- |
|
|
102
|
+
| Surface, asset, network, and amount selection | Supported source/destination pair | Exact asset/network selection, amount/Max input, and visible-state reads on Extension; exact native selection and amount input on Android | **AVAILABLE** |
|
|
103
|
+
| Quote and slippage | Live quote services and supported route | Surface, quote, Max-quote, and custom-slippage recipes | **AVAILABLE** |
|
|
104
|
+
| Submit and confirmation | Sufficient source balance and gas | Typed submit action with explicit confirmation policy | **PARTIAL**: live funds and cost policy decide whether submission is allowed |
|
|
105
|
+
| Sequential multi-network journey | Balances delivered by preceding swaps and bridges | Individual steps are composable, but the complete live-funds journey remains task-local and resumable by its case inventory | **PARTIAL** |
|
|
106
|
+
| Activity, balance deltas, and completed bridge tracking | Confirmed transactions and indexed activity | Visible-state evidence exists; reusable terminal activity and arithmetic assertions are incomplete | **PARTIAL** |
|
|
107
|
+
|
|
108
|
+
### Networks
|
|
109
|
+
|
|
110
|
+
| Case family | Required state and behavior | Current capability | Status |
|
|
111
|
+
| --- | --- | --- | --- |
|
|
112
|
+
| Add, select, and remove custom networks | Known RPC configuration and chain identity | Typed custom-network and Chainlist add, visible-state read, select, and remove actions | **AVAILABLE** on Extension |
|
|
113
|
+
| Custom-network tokens and persistence | Known token contract and persistent profile | Network actions compose with Assets import/visibility/persistence recipes | **AVAILABLE** on Extension |
|
|
114
|
+
| Balance stability and fiat visibility | Funded custom network and live price APIs | Visible-state reads and a bounded balance-stability recipe | **PARTIAL**: external price availability remains uncontrolled |
|
|
115
|
+
| Send Max and activity | Funded custom network and recipient | Assets send actions cover the transaction path | **PARTIAL**: exact activity and balance-delta assertions remain task-local |
|
|
116
|
+
| Bridge tracking and details | Completed supported bridge route | Swap/Bridge composition exists; reusable terminal tracking assertions are incomplete | **PARTIAL** |
|
|
117
|
+
| Android multi-network visibility | Imported wallet with holdings on multiple chains | Native Assets read reports aggregate visible holdings without Metro or a development bridge | **AVAILABLE** |
|
|
118
|
+
|
|
119
|
+
## Platform scope
|
|
120
|
+
|
|
121
|
+
The 135-case source inventory targets Extension. It must not be relabeled as an
|
|
122
|
+
Android checklist. Android release validation uses a separate case inventory
|
|
123
|
+
for the product behaviors exercised on a physical device, while reusing the
|
|
124
|
+
same action vocabulary and report gate where the clients expose equivalent
|
|
125
|
+
flows. Simulator-only evidence is not accepted for Android release claims.
|