@deeeed/metamask-harness 0.35.0 → 0.37.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 +39 -0
- package/README.md +9 -1
- package/adapters/extension/artifact-runtime-state.cjs +128 -0
- package/adapters/extension/check-infura-readiness.cjs +102 -0
- package/adapters/extension/inject.mjs +2 -0
- package/adapters/extension/launch-browser.cjs +22 -0
- package/adapters/extension/live.sh +103 -18
- package/adapters/extension/readiness.mjs +77 -36
- package/adapters/extension/snapshot-dist.sh +88 -3
- package/adapters/extension/start-watch.sh +15 -0
- package/adapters/extension/verify.sh +6 -2
- package/adapters/extension/wallet-fixture-state.cjs +3 -1
- package/adapters/manifest.json +25 -1
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +73 -42
- package/adapters/mobile/reset-app-data.sh +154 -0
- 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/product-config.js +29 -1
- package/dist/adapters/extension/release-artifact.js +386 -0
- package/dist/adapters/extension/runtime-decision.js +161 -20
- package/dist/adapters/extension/runtime.js +127 -0
- package/dist/adapters/mobile/release-artifact-state.js +124 -0
- package/dist/adapters/mobile/release-artifact.js +295 -0
- package/dist/adapters.js +29 -4
- package/dist/cli-commands.js +1 -1
- package/dist/cli.js +2 -2
- package/dist/command-contract.js +17 -1
- package/dist/commands/call.js +2 -1
- package/dist/commands/device-target.js +5 -0
- package/dist/commands/fixtures.js +106 -31
- package/dist/commands/launch/extension.js +92 -7
- package/dist/commands/launch/index.js +13 -0
- package/dist/commands/launch/mobile.js +2 -0
- package/dist/commands/provision.js +2 -0
- package/dist/commands/run-engine.js +89 -5
- package/dist/commands/run.js +3 -1
- 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 +37 -4
- package/dist/recipe-security.js +3 -0
- package/dist/run-diagnostics.js +1 -1
- package/docs/RECIPES.md +29 -0
- package/docs/RELEASE-QA-CAPABILITY-MAP.md +150 -0
- package/library/actions/extension/perps/perps.mjs +2 -0
- package/library/actions/extension/perps/read_snapshot.mjs +470 -0
- package/library/actions/extension/platform/cdp.mjs +6 -3
- package/library/actions/extension/wallet/import.mjs +201 -0
- package/library/actions/extension/wallet/reset.mjs +98 -0
- package/library/actions/extension/wallet/secret-input.mjs +98 -0
- package/library/actions/extension/wallet/state.mjs +1 -0
- package/library/actions/mobile/analytics/consent-settings.mjs +112 -0
- package/library/actions/mobile/analytics/set_consent.mjs +4 -112
- package/library/actions/mobile/platform/bridge.mjs +8 -0
- package/library/actions/mobile/platform/observe-ui.mjs +84 -2
- package/library/actions/mobile/ui/native-navigation.mjs +225 -0
- package/library/actions/mobile/ui/navigate.mjs +7 -0
- package/library/actions/mobile/wallet/import.mjs +328 -0
- package/library/actions/mobile/wallet/native-ui.mjs +493 -0
- package/library/actions/mobile/wallet/read_state.mjs +16 -0
- package/library/actions/mobile/wallet/reset-helper.mjs +99 -0
- package/library/actions/mobile/wallet/reset.mjs +20 -0
- package/library/actions/shared/ui/locators.mjs +7 -0
- package/library/actions/shared/wallet/import-source.mjs +101 -0
- package/library/manifests/extension.action-manifest.json +228 -0
- package/library/manifests/mobile.action-manifest.json +124 -0
- package/library/recipes/extension/runner/action-validation.recipe.json +12 -1
- package/library/recipes/wallet/import.recipe.json +102 -0
- package/library/recipes/wallet/reset-import.recipe.json +107 -0
- package/package.json +1 -1
- package/scripts/completions.sh +2 -2
package/dist/command-contract.js
CHANGED
|
@@ -239,6 +239,22 @@ const PUBLIC_COMMAND_CONTRACTS = {
|
|
|
239
239
|
}),
|
|
240
240
|
positionals: [{ label: "platform", choices: ["ios", "android"] }]
|
|
241
241
|
},
|
|
242
|
+
"runtime-launch": {
|
|
243
|
+
options: options(HELP, JSON, TARGET, ADAPTER, DEVICE, MOBILE_PLATFORM, {
|
|
244
|
+
"--cdp-port": value(),
|
|
245
|
+
"--chrome-user-data-dir": value(),
|
|
246
|
+
"--artifacts-dir": value(),
|
|
247
|
+
"--start-watch": bool(),
|
|
248
|
+
"--build-lavamoat": bool(),
|
|
249
|
+
"--remote-flag": value(),
|
|
250
|
+
"--artifact-file": value(),
|
|
251
|
+
"--artifact-version": value(),
|
|
252
|
+
"--artifact-build-id": value(),
|
|
253
|
+
"--artifact-package-id": value(),
|
|
254
|
+
"--artifact-sha256": value(),
|
|
255
|
+
"--artifact-cache": value()
|
|
256
|
+
})
|
|
257
|
+
},
|
|
242
258
|
logs: {
|
|
243
259
|
options: options(HELP, JSON, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM, {
|
|
244
260
|
"--full": bool(),
|
|
@@ -270,7 +286,7 @@ const PUBLIC_COMMAND_CONTRACTS = {
|
|
|
270
286
|
"--extension-id-file": value(),
|
|
271
287
|
"--action-manifest": value()
|
|
272
288
|
}),
|
|
273
|
-
positionals: [{ label: "action", choices: ["init", "sync", "set", "generate", "finalize"] }],
|
|
289
|
+
positionals: [{ label: "action", choices: ["init", "sync", "set", "reset", "generate", "finalize"] }],
|
|
274
290
|
minimumPositionals: 0
|
|
275
291
|
}
|
|
276
292
|
};
|
package/dist/commands/call.js
CHANGED
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
synthesizeOneNodeRecipe,
|
|
41
41
|
validateRecipeAdapterAware
|
|
42
42
|
} from "./run-engine.js";
|
|
43
|
+
import { readMobileReleaseArtifactState } from "../adapters/mobile/release-artifact-state.js";
|
|
43
44
|
import { acquireCheckoutLock } from "../checkout-lock.js";
|
|
44
45
|
import { formatRunDiagnosticsForHuman, readRunDiagnosticsDocument } from "../run-diagnostics.js";
|
|
45
46
|
import { recipeTrustFailure } from "../recipe-security.js";
|
|
@@ -267,7 +268,7 @@ async function handleCall(argv) {
|
|
|
267
268
|
}
|
|
268
269
|
try {
|
|
269
270
|
const prepared = await prepareHeal(adapter, target, options, json, {
|
|
270
|
-
skipMobileSourceFreshness: adapter === "mobile" && resolvedAction === "app.lifecycle"
|
|
271
|
+
skipMobileSourceFreshness: adapter === "mobile" && (resolvedAction === "app.lifecycle" || readMobileReleaseArtifactState(target) !== null)
|
|
271
272
|
});
|
|
272
273
|
if (typeof prepared === "number") return prepared;
|
|
273
274
|
const { state, heal } = prepared;
|
|
@@ -256,6 +256,11 @@ ${formatConnectedDevices(connected)}
|
|
|
256
256
|
userAction: deviceRecovery(options, opts.rerun)
|
|
257
257
|
};
|
|
258
258
|
}
|
|
259
|
+
if (targetable.length === 1) {
|
|
260
|
+
const selected = targetable[0];
|
|
261
|
+
if (selected.platform === "android") setAndroidDeviceEnv(selected.id, selected.name);
|
|
262
|
+
else setIosDeviceEnv(selected.id, selected.name);
|
|
263
|
+
}
|
|
259
264
|
return { ok: true };
|
|
260
265
|
}
|
|
261
266
|
function scopedDevices(devices, allDevices = false) {
|
|
@@ -48,7 +48,7 @@ function fixturesStatus(options) {
|
|
|
48
48
|
const fixture = { ...summary, path: canonical };
|
|
49
49
|
const ready = summary.status === "ready";
|
|
50
50
|
const nextCommand = ready ? "mm-harness fixtures set" : "mm-harness fixtures init --from <path> # or: mm-harness fixtures init --dev";
|
|
51
|
-
const operations = adapter === "extension" ? ["init", "sync", "set", "generate", "finalize"] : ["init", "sync", "set"];
|
|
51
|
+
const operations = adapter === "extension" ? ["init", "sync", "set", "reset", "generate", "finalize"] : adapter === "mobile" ? ["init", "sync", "set", "reset"] : ["init", "sync", "set"];
|
|
52
52
|
if (json) {
|
|
53
53
|
console.log(JSON.stringify({
|
|
54
54
|
schemaVersion: 1,
|
|
@@ -103,8 +103,8 @@ async function handleFixturesLocked(argv) {
|
|
|
103
103
|
const { positional, options } = parseFlags(argv, FIXTURES_BOOLEANS);
|
|
104
104
|
const json = flag(options, "json");
|
|
105
105
|
const sub = positional[0];
|
|
106
|
-
if (sub !== "init" && sub !== "sync" && sub !== "set" && sub !== "generate" && sub !== "finalize") {
|
|
107
|
-
return usageOut(json, "fixtures", "fixtures requires a subcommand: mm-harness fixtures <init|sync|set|generate|finalize>", "mm-harness fixtures init --from <path> or mm-harness fixtures init --dev");
|
|
106
|
+
if (sub !== "init" && sub !== "sync" && sub !== "set" && sub !== "reset" && sub !== "generate" && sub !== "finalize") {
|
|
107
|
+
return usageOut(json, "fixtures", "fixtures requires a subcommand: mm-harness fixtures <init|sync|set|reset|generate|finalize>", "mm-harness fixtures init --from <path> or mm-harness fixtures init --dev");
|
|
108
108
|
}
|
|
109
109
|
if (sub !== "init" && (flag(options, "dev") || flag(options, "force") || str(options, "from"))) {
|
|
110
110
|
return usageOut(
|
|
@@ -121,7 +121,7 @@ async function handleFixturesLocked(argv) {
|
|
|
121
121
|
}
|
|
122
122
|
const surface = getAdapterSurface(adapter);
|
|
123
123
|
surface.resolveSlotPorts(target);
|
|
124
|
-
const dtResult = applyDeviceTargeting("fixtures", adapter, options, { gate:
|
|
124
|
+
const dtResult = applyDeviceTargeting("fixtures", adapter, options, { gate: sub === "reset", rerun: "" });
|
|
125
125
|
if ("code" in dtResult) {
|
|
126
126
|
return usageOut(json, "fixtures", dtResult.message, dtResult.userAction);
|
|
127
127
|
}
|
|
@@ -129,6 +129,9 @@ async function handleFixturesLocked(argv) {
|
|
|
129
129
|
if (sub === "finalize") return fixturesFinalize(adapter, target, options, json);
|
|
130
130
|
const canonicalFixture = walletFixturePath(target);
|
|
131
131
|
if (sub === "init") return fixturesInit(adapter, target, options, json);
|
|
132
|
+
if (sub === "reset" && surface.headless) {
|
|
133
|
+
return usageOut(json, "fixtures", "fixtures reset requires a Mobile or Extension client.", "use fixtures set for the core adapter");
|
|
134
|
+
}
|
|
132
135
|
if (surface.headless && sub === "set") {
|
|
133
136
|
const fixture = fixtureSummary(target, adapter);
|
|
134
137
|
if (fixture.status !== "ready") {
|
|
@@ -139,7 +142,7 @@ async function handleFixturesLocked(argv) {
|
|
|
139
142
|
else console.error(message);
|
|
140
143
|
return EXIT.ok;
|
|
141
144
|
}
|
|
142
|
-
const retryHint = `${surface.hints.relaunch} # relaunch, then retry: mm-harness fixtures
|
|
145
|
+
const retryHint = `${surface.hints.relaunch} # relaunch, then retry: mm-harness fixtures ${sub}`;
|
|
143
146
|
if (sub === "sync") {
|
|
144
147
|
const exitCode = fixturesSync(adapter, target, json);
|
|
145
148
|
if (json) {
|
|
@@ -162,34 +165,105 @@ async function handleFixturesLocked(argv) {
|
|
|
162
165
|
"mm-harness fixtures init --dev && mm-harness fixtures set"
|
|
163
166
|
);
|
|
164
167
|
}
|
|
168
|
+
if (sub === "reset") {
|
|
169
|
+
const selectedFixture = fixtureFileSummary(fixturePath, adapter, fixturePath);
|
|
170
|
+
if (selectedFixture.status !== "ready") {
|
|
171
|
+
return usageOut(
|
|
172
|
+
json,
|
|
173
|
+
"fixtures",
|
|
174
|
+
`wallet fixture is ${String(selectedFixture.status)} at ${fixturePath}; reset was not started.`,
|
|
175
|
+
`mm-harness fixtures init --from <path> --target ${target}`
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
165
179
|
if (adapter === "extension") {
|
|
166
|
-
return applyExtensionFixture(target, fixturePath, canonicalFixture, json);
|
|
180
|
+
return applyExtensionFixture(target, fixturePath, canonicalFixture, json, sub);
|
|
167
181
|
}
|
|
168
|
-
|
|
182
|
+
const reset = sub === "reset";
|
|
183
|
+
process.stderr.write(`\u2192 fixtures ${sub} ${adapter} \u2014 ${reset ? "resetting selected app data, relaunching, then applying" : "connecting bridge + applying"} wallet fixture (can take ~30s)\u2026
|
|
169
184
|
`);
|
|
170
185
|
let status;
|
|
171
186
|
let mobileSetupResult;
|
|
187
|
+
let resetError;
|
|
172
188
|
if (adapter === "mobile") {
|
|
173
189
|
const setupWalletSh = path.join(runnerDir, "adapters/mobile/bridge-runtime/setup-wallet.sh");
|
|
174
190
|
const previousAppRoot = process.env.APP_ROOT;
|
|
175
191
|
process.env.APP_ROOT = target;
|
|
176
192
|
try {
|
|
193
|
+
if (reset) {
|
|
194
|
+
const platform = process.env.PLATFORM === "android" || process.env.ADB_SERIAL || process.env.ANDROID_SERIAL ? "android" : "ios";
|
|
195
|
+
const port = process.env.WATCHER_PORT ?? process.env.METRO_PORT ?? "8081";
|
|
196
|
+
const resetAppData = path.join(runnerDir, "adapters/mobile/reset-app-data.sh");
|
|
197
|
+
const resetArgs = ["--platform", platform, "--target", target];
|
|
198
|
+
if (platform === "ios") {
|
|
199
|
+
resetArgs.push("--simulator", process.env.SIM_UDID || process.env.IOS_SIMULATOR || "booted");
|
|
200
|
+
} else if (process.env.ADB_SERIAL || process.env.ANDROID_SERIAL) {
|
|
201
|
+
resetArgs.push("--adb-serial", process.env.ADB_SERIAL || process.env.ANDROID_SERIAL || "");
|
|
202
|
+
}
|
|
203
|
+
const resetResult = await spawnScriptStreaming(resetAppData, resetArgs, target, {
|
|
204
|
+
timeoutMs: FIXTURE_STREAM_TIMEOUT_MS
|
|
205
|
+
});
|
|
206
|
+
if (resetResult.status !== 0) {
|
|
207
|
+
resetError = new Error("Mobile app data reset failed.");
|
|
208
|
+
} else {
|
|
209
|
+
const prewarmBundle = path.join(runnerDir, "adapters/mobile/prewarm-bundle.sh");
|
|
210
|
+
const prewarmResult = await spawnScriptStreaming(
|
|
211
|
+
prewarmBundle,
|
|
212
|
+
["--platform", platform, "--target", target, "--port", port],
|
|
213
|
+
target,
|
|
214
|
+
{
|
|
215
|
+
timeoutMs: FIXTURE_STREAM_TIMEOUT_MS
|
|
216
|
+
}
|
|
217
|
+
);
|
|
218
|
+
if (prewarmResult.status !== 0) {
|
|
219
|
+
resetError = new Error("Mobile bundle prewarm failed after app data reset.");
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (!resetError) {
|
|
223
|
+
const openDevice = path.join(runnerDir, "adapters/mobile/open-device.sh");
|
|
224
|
+
const openArgs = ["--platform", platform, "--target", target, "--port", port, "--preflight-mode", "fast", "--restart"];
|
|
225
|
+
if (platform === "ios") {
|
|
226
|
+
openArgs.push("--simulator", process.env.SIM_UDID || process.env.IOS_SIMULATOR || "booted");
|
|
227
|
+
} else if (process.env.ADB_SERIAL || process.env.ANDROID_SERIAL) {
|
|
228
|
+
openArgs.push("--adb-serial", process.env.ADB_SERIAL || process.env.ANDROID_SERIAL || "");
|
|
229
|
+
}
|
|
230
|
+
const openResult = await spawnScriptStreaming(openDevice, openArgs, target, {
|
|
231
|
+
timeoutMs: FIXTURE_STREAM_TIMEOUT_MS
|
|
232
|
+
});
|
|
233
|
+
if (openResult.status !== 0) {
|
|
234
|
+
resetError = new Error("Mobile app relaunch after data reset failed.");
|
|
235
|
+
} else {
|
|
236
|
+
const waitForBridge = path.join(runnerDir, "adapters/mobile/wait-for-bridge.sh");
|
|
237
|
+
const waitResult = await spawnScriptStreaming(
|
|
238
|
+
waitForBridge,
|
|
239
|
+
["--target", target, "--port", port, "--platform", platform],
|
|
240
|
+
target,
|
|
241
|
+
{ timeoutMs: FIXTURE_STREAM_TIMEOUT_MS }
|
|
242
|
+
);
|
|
243
|
+
if (waitResult.status !== 0) {
|
|
244
|
+
resetError = new Error("Mobile bridge did not return after app data reset.");
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
177
249
|
const configuredTimeout = Number(process.env.MM_HARNESS_FIXTURES_SET_TIMEOUT_MS ?? FIXTURE_STREAM_TIMEOUT_MS);
|
|
178
250
|
const timeoutMs = Number.isFinite(configuredTimeout) && configuredTimeout > 0 ? configuredTimeout : FIXTURE_STREAM_TIMEOUT_MS;
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
251
|
+
if (!resetError) {
|
|
252
|
+
mobileSetupResult = await spawnScriptStreaming(
|
|
253
|
+
setupWalletSh,
|
|
254
|
+
["--fixture", fixturePath],
|
|
255
|
+
target,
|
|
256
|
+
{
|
|
257
|
+
env: {
|
|
258
|
+
APP_ROOT: target,
|
|
259
|
+
WATCHER_PORT: process.env.WATCHER_PORT ?? "8081",
|
|
260
|
+
METRO_PORT: process.env.METRO_PORT ?? process.env.WATCHER_PORT ?? "8081"
|
|
261
|
+
},
|
|
262
|
+
timeoutMs
|
|
263
|
+
}
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
status = !resetError && mobileSetupResult?.status === 0 ? "pass" : "fail";
|
|
193
267
|
} finally {
|
|
194
268
|
if (previousAppRoot === void 0) delete process.env.APP_ROOT;
|
|
195
269
|
else process.env.APP_ROOT = previousAppRoot;
|
|
@@ -205,16 +279,17 @@ async function handleFixturesLocked(argv) {
|
|
|
205
279
|
{
|
|
206
280
|
schemaVersion: 1,
|
|
207
281
|
command: "fixtures",
|
|
208
|
-
action:
|
|
282
|
+
action: sub,
|
|
209
283
|
adapter,
|
|
210
284
|
fixture: fixturePath,
|
|
211
285
|
canonicalFixture,
|
|
212
286
|
status,
|
|
213
287
|
exitCode: status === "pass" ? EXIT.ok : EXIT.runtime,
|
|
288
|
+
walletReset: reset && status === "pass",
|
|
214
289
|
message: teaching,
|
|
215
290
|
error: status === "fail" ? {
|
|
216
|
-
code: mobileSetupResult?.timedOut ? "SETUP_WALLET_TIMEOUT" : "SETUP_WALLET_FAILED",
|
|
217
|
-
message: mobileSetupResult?.timedOut ? `wallet fixture setup exceeded its ${String(mobileSetupResult.timeoutMs)}ms bound on Metro port ${process.env.WATCHER_PORT ?? "8081"}` : `wallet fixture setup failed on ${process.env.WATCHER_PORT ?? "8081"}`,
|
|
291
|
+
code: resetError ? "MOBILE_WALLET_RESET_FAILED" : mobileSetupResult?.timedOut ? "SETUP_WALLET_TIMEOUT" : "SETUP_WALLET_FAILED",
|
|
292
|
+
message: resetError ? `wallet reset failed: ${resetError instanceof Error ? resetError.message : String(resetError)}` : mobileSetupResult?.timedOut ? `wallet fixture setup exceeded its ${String(mobileSetupResult.timeoutMs)}ms bound on Metro port ${process.env.WATCHER_PORT ?? "8081"}` : `wallet fixture setup failed on ${process.env.WATCHER_PORT ?? "8081"}`,
|
|
218
293
|
userAction: retryHint
|
|
219
294
|
} : null
|
|
220
295
|
},
|
|
@@ -227,7 +302,7 @@ async function handleFixturesLocked(argv) {
|
|
|
227
302
|
}
|
|
228
303
|
return status === "pass" ? EXIT.ok : EXIT.runtime;
|
|
229
304
|
}
|
|
230
|
-
async function applyExtensionFixture(target, fixturePath, canonicalFixture, json) {
|
|
305
|
+
async function applyExtensionFixture(target, fixturePath, canonicalFixture, json, action) {
|
|
231
306
|
const distManifest = path.join(target, "dist/chrome/manifest.json");
|
|
232
307
|
const userAction = `mm-harness launch --build --verify --target ${JSON.stringify(target)}`;
|
|
233
308
|
if (!fs.existsSync(distManifest)) {
|
|
@@ -236,7 +311,7 @@ async function applyExtensionFixture(target, fixturePath, canonicalFixture, json
|
|
|
236
311
|
console.log(JSON.stringify({
|
|
237
312
|
schemaVersion: 1,
|
|
238
313
|
command: "fixtures",
|
|
239
|
-
action
|
|
314
|
+
action,
|
|
240
315
|
adapter: "extension",
|
|
241
316
|
fixture: fixturePath,
|
|
242
317
|
canonicalFixture,
|
|
@@ -245,15 +320,15 @@ async function applyExtensionFixture(target, fixturePath, canonicalFixture, json
|
|
|
245
320
|
error: { code: "EXTENSION_FIXTURE_BUILD_REQUIRED", message: message2, userAction }
|
|
246
321
|
}, null, 2));
|
|
247
322
|
} else {
|
|
248
|
-
console.error(`\u2717 mm-harness fixtures
|
|
323
|
+
console.error(`\u2717 mm-harness fixtures ${action}: ${message2}
|
|
249
324
|
Next: ${userAction}`);
|
|
250
325
|
}
|
|
251
326
|
return EXIT.runtime;
|
|
252
327
|
}
|
|
253
|
-
process.stderr.write(`${colorHumanMessage(
|
|
328
|
+
process.stderr.write(`${colorHumanMessage(`\u2192 fixtures ${action} extension \u2014 resetting and seeding the slot-owned profile from the existing build\u2026`)}
|
|
254
329
|
`);
|
|
255
330
|
const liveScript = path.join(runnerDir, "adapters/extension/live.sh");
|
|
256
|
-
const args = ["--target", target, "--launch-existing-dist"];
|
|
331
|
+
const args = ["--target", target, "--launch-existing-dist", "--launch-only"];
|
|
257
332
|
if (process.env.CDP_PORT) args.push("--cdp-port", process.env.CDP_PORT);
|
|
258
333
|
const result = await spawnScriptStreaming(liveScript, args, target, {
|
|
259
334
|
env: { RECIPE_WALLET_FIXTURE: fixturePath },
|
|
@@ -266,7 +341,7 @@ async function applyExtensionFixture(target, fixturePath, canonicalFixture, json
|
|
|
266
341
|
console.log(JSON.stringify({
|
|
267
342
|
schemaVersion: 1,
|
|
268
343
|
command: "fixtures",
|
|
269
|
-
action
|
|
344
|
+
action,
|
|
270
345
|
adapter: "extension",
|
|
271
346
|
fixture: fixturePath,
|
|
272
347
|
canonicalFixture,
|
|
@@ -279,7 +354,7 @@ async function applyExtensionFixture(target, fixturePath, canonicalFixture, json
|
|
|
279
354
|
} else if (status === "pass") {
|
|
280
355
|
console.error(message);
|
|
281
356
|
} else {
|
|
282
|
-
console.error(`\u2717 mm-harness fixtures
|
|
357
|
+
console.error(`\u2717 mm-harness fixtures ${action}: ${message}
|
|
283
358
|
Next: ${userAction}`);
|
|
284
359
|
}
|
|
285
360
|
return exitCode;
|
|
@@ -15,11 +15,20 @@ import {
|
|
|
15
15
|
runnerDir
|
|
16
16
|
} from "../../paths.js";
|
|
17
17
|
import { extensionIdFromKey } from "../../adapters/extension/extension-id.js";
|
|
18
|
-
import {
|
|
19
|
-
|
|
18
|
+
import {
|
|
19
|
+
EXTENSION_PRODUCT_CONFIG_FINGERPRINT_FILENAME,
|
|
20
|
+
extensionCompiledScriptsMatchProductConfig,
|
|
21
|
+
extensionProductConfigBlock,
|
|
22
|
+
extensionProductConfigFingerprint
|
|
23
|
+
} from "../../adapters/extension/product-config.js";
|
|
24
|
+
import {
|
|
25
|
+
isExtensionDistStale,
|
|
26
|
+
releaseArtifactState,
|
|
27
|
+
runtimeDistCheck
|
|
28
|
+
} from "../../adapters/extension/runtime-decision.js";
|
|
20
29
|
import { checkExtensionRuntimeHealth } from "../../adapters/extension/runtime.js";
|
|
21
30
|
import { ensureHarnessFresh } from "../../adapters/harness-freshness.js";
|
|
22
|
-
import { stopExtensionWatcher } from "../../adapters/slot-ports.js";
|
|
31
|
+
import { isExtensionWatcherLive, stopExtensionWatcher } from "../../adapters/slot-ports.js";
|
|
23
32
|
import { EXIT, spawnScriptStreaming } from "../shared.js";
|
|
24
33
|
const { CdpSession } = await importRecipeHarnessRuntimeCdp();
|
|
25
34
|
const { RUNTIME_IDENTITY_FILENAME, RUNTIME_NONCE_PREFIX } = createRequire(import.meta.url)(
|
|
@@ -50,7 +59,18 @@ async function launchExtension(target, tier, wantWatch, displayMode = "fullscree
|
|
|
50
59
|
if (process.env.CHROME_USER_DATA_DIR) {
|
|
51
60
|
process.env.CHROME_USER_DATA_DIR = path.resolve(process.env.CHROME_USER_DATA_DIR);
|
|
52
61
|
}
|
|
53
|
-
const
|
|
62
|
+
const artifact = releaseArtifactState(target);
|
|
63
|
+
const artifactSnapshot = runtimeDistCheck(target, artifact);
|
|
64
|
+
const artifactLaunchable = !wantWatch && tier !== "build" && artifact.status === "valid" && artifactSnapshot.status === "fresh";
|
|
65
|
+
const reusable = !wantWatch && tier !== "build" && await extensionRuntimeReusable(target, artifact);
|
|
66
|
+
if (artifact.status !== "none" && !reusable && !artifactLaunchable) {
|
|
67
|
+
const detail = artifact.status === "invalid" ? `identity is invalid: ${artifact.reason ?? "unknown reason"}` : "runtime is not healthy and reusable";
|
|
68
|
+
return {
|
|
69
|
+
status: EXIT.infra,
|
|
70
|
+
output: `RELEASE_ARTIFACT_NOT_REUSABLE: The loaded release artifact ${detail}.
|
|
71
|
+
Next: rerun runtime-launch with the same artifact source.`
|
|
72
|
+
};
|
|
73
|
+
}
|
|
54
74
|
if (!reusable) {
|
|
55
75
|
const block = extensionProductConfigBlock(target);
|
|
56
76
|
if (block) {
|
|
@@ -75,12 +95,23 @@ Next: ${block.userAction}`
|
|
|
75
95
|
if (reusable) {
|
|
76
96
|
return extensionReattach(target, displayMode);
|
|
77
97
|
}
|
|
78
|
-
|
|
98
|
+
if (artifactLaunchable) {
|
|
99
|
+
return extensionLaunchReleaseArtifact(target, artifact);
|
|
100
|
+
}
|
|
101
|
+
return extensionLaunchDevelopment(target);
|
|
79
102
|
}
|
|
80
|
-
async function extensionRuntimeReusable(target) {
|
|
103
|
+
async function extensionRuntimeReusable(target, artifact = releaseArtifactState(target)) {
|
|
81
104
|
const cdpPort = process.env.CDP_PORT;
|
|
82
105
|
if (!cdpPort) return false;
|
|
83
|
-
|
|
106
|
+
const runtimeSnapshot = runtimeDistCheck(target, artifact);
|
|
107
|
+
const releaseArtifact = runtimeSnapshot.source === "release-artifact";
|
|
108
|
+
if (releaseArtifact) {
|
|
109
|
+
if (runtimeSnapshot.status !== "fresh") return false;
|
|
110
|
+
} else {
|
|
111
|
+
if (isExtensionDistStale(target)) return false;
|
|
112
|
+
if (isExtensionWatcherLive(target) && !harnessWatcherMatchesProductConfig(target)) return false;
|
|
113
|
+
if (!extensionCompiledScriptsMatchProductConfig(target, expectedRuntimeDist(target))) return false;
|
|
114
|
+
}
|
|
84
115
|
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
85
116
|
const reachable = await cdpVersionReachable(cdpPort);
|
|
86
117
|
const nonceOwned = reachable && await cdpOwnedByRuntimeNonce(cdpPort, target);
|
|
@@ -325,6 +356,59 @@ async function extensionReattach(target, displayMode = "fullscreen") {
|
|
|
325
356
|
console.error(colorHumanMessage(`\u2192 extension quick reattach \u2014 reload in place \xB7 CDP :${process.env.CDP_PORT ?? "default"} (no rebuild, no relaunch)`));
|
|
326
357
|
return spawnScriptStreaming(reattachScript, reattachArgs, target);
|
|
327
358
|
}
|
|
359
|
+
async function extensionLaunchDevelopment(target) {
|
|
360
|
+
const liveScript = recipeHarnessPath(target, "extension", "scripts", "live.sh");
|
|
361
|
+
const liveArgs = ["--target", target];
|
|
362
|
+
const existingBuild = isExtensionWatcherLive(target) && harnessWatcherMatchesProductConfig(target) && extensionCompiledScriptsMatchProductConfig(target, path.join(target, "dist/chrome")) && fs.existsSync(path.join(target, "dist/chrome/manifest.json")) && !isExtensionDistStale(target);
|
|
363
|
+
liveArgs.push(existingBuild ? "--launch-existing-dist" : "--start-watch");
|
|
364
|
+
if (process.env.CDP_PORT) liveArgs.push("--cdp-port", process.env.CDP_PORT);
|
|
365
|
+
if (process.env.EXTENSION_START_URL) liveArgs.push("--start-url", process.env.EXTENSION_START_URL);
|
|
366
|
+
console.error(colorHumanMessage(
|
|
367
|
+
existingBuild ? `\u2192 extension quick launch \u2014 existing yarn start build \xB7 CDP :${process.env.CDP_PORT ?? "default"} (output streams below)` : `\u2192 extension quick launch \u2014 yarn start development build \xB7 CDP :${process.env.CDP_PORT ?? "default"} (output streams below)`
|
|
368
|
+
));
|
|
369
|
+
return spawnScriptStreaming(liveScript, liveArgs, target);
|
|
370
|
+
}
|
|
371
|
+
async function extensionLaunchReleaseArtifact(target, artifact) {
|
|
372
|
+
if (!artifact.sourceDir || !artifact.provenancePath) {
|
|
373
|
+
return {
|
|
374
|
+
status: EXIT.infra,
|
|
375
|
+
output: "RELEASE_ARTIFACT_NOT_REUSABLE: The loaded release artifact identity is incomplete.\nNext: rerun runtime-launch with the same artifact source."
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
const liveScript = recipeHarnessPath(target, "extension", "scripts", "live.sh");
|
|
379
|
+
const liveArgs = [
|
|
380
|
+
"--target",
|
|
381
|
+
target,
|
|
382
|
+
"--external-artifact",
|
|
383
|
+
"--dist-dir",
|
|
384
|
+
artifact.sourceDir,
|
|
385
|
+
"--artifact-provenance",
|
|
386
|
+
artifact.provenancePath,
|
|
387
|
+
"--preserve-profile"
|
|
388
|
+
];
|
|
389
|
+
if (process.env.CDP_PORT) liveArgs.push("--cdp-port", process.env.CDP_PORT);
|
|
390
|
+
if (process.env.EXTENSION_START_URL) liveArgs.push("--start-url", process.env.EXTENSION_START_URL);
|
|
391
|
+
console.error(colorHumanMessage(
|
|
392
|
+
`\u2192 extension release relaunch \u2014 verified artifact ${artifact.manifestVersion ?? artifact.expectedVersion ?? ""} \xB7 CDP :${process.env.CDP_PORT ?? "default"}`
|
|
393
|
+
));
|
|
394
|
+
return spawnScriptStreaming(liveScript, liveArgs, target);
|
|
395
|
+
}
|
|
396
|
+
function harnessWatcherMatchesProductConfig(target) {
|
|
397
|
+
const runtimeDir = path.join(target, recipeRuntimeDir());
|
|
398
|
+
const harnessPidFile = path.join(runtimeDir, "recipe-harness-webpack.pid");
|
|
399
|
+
if (!fs.existsSync(harnessPidFile)) return true;
|
|
400
|
+
const expected = extensionProductConfigFingerprint(target);
|
|
401
|
+
if (!expected) return true;
|
|
402
|
+
try {
|
|
403
|
+
const actual = fs.readFileSync(
|
|
404
|
+
path.join(runtimeDir, EXTENSION_PRODUCT_CONFIG_FINGERPRINT_FILENAME),
|
|
405
|
+
"utf8"
|
|
406
|
+
).trim();
|
|
407
|
+
return actual === expected;
|
|
408
|
+
} catch {
|
|
409
|
+
return false;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
328
412
|
async function extensionRebuild(target) {
|
|
329
413
|
const runtimeDirRel = recipeRuntimeDir();
|
|
330
414
|
const runtimeAbs = path.join(target, runtimeDirRel);
|
|
@@ -348,6 +432,7 @@ async function extensionRebuild(target) {
|
|
|
348
432
|
}
|
|
349
433
|
export {
|
|
350
434
|
extensionDepsBlock,
|
|
435
|
+
extensionLaunchDevelopment,
|
|
351
436
|
extensionReattach,
|
|
352
437
|
extensionRebuild,
|
|
353
438
|
extensionRuntimeReusable,
|
|
@@ -259,6 +259,16 @@ async function handleLaunchLocked(argv, stream) {
|
|
|
259
259
|
exitCode: EXIT.infra
|
|
260
260
|
});
|
|
261
261
|
}
|
|
262
|
+
if (adapter === "extension" && extensionEvmRpcUnreachable(attempt.output)) {
|
|
263
|
+
return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
|
|
264
|
+
code: "EVM_RPC_UNREACHABLE",
|
|
265
|
+
message: "EVM RPC readiness failed; Infura configured does not mean RPC reachable.",
|
|
266
|
+
recoverable: false,
|
|
267
|
+
userAction: `Check the effective INFURA_PROJECT_ID from the environment, .metamaskprodrc, or ${shellQuote(path.join(target, ".metamaskrc"))}, then rerun: mm-harness launch --verify --target ${shellQuote(target)}`,
|
|
268
|
+
exitCode: EXIT.runtime,
|
|
269
|
+
originalError: attempt.output.trim() || void 0
|
|
270
|
+
});
|
|
271
|
+
}
|
|
262
272
|
if (heal === "off") {
|
|
263
273
|
return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
|
|
264
274
|
code: "LAUNCH_FAILED",
|
|
@@ -374,6 +384,9 @@ function extensionBrowserMissing(output) {
|
|
|
374
384
|
function extensionProductConfigMissing(output) {
|
|
375
385
|
return /EXTENSION_PRODUCT_CONFIG_REQUIRED:/u.test(output);
|
|
376
386
|
}
|
|
387
|
+
function extensionEvmRpcUnreachable(output) {
|
|
388
|
+
return /EVM_RPC_UNREACHABLE|EVM RPC readiness probe failed|Infura configured ≠ RPC reachable/u.test(output);
|
|
389
|
+
}
|
|
377
390
|
function mobileProvisioningBlocked(output) {
|
|
378
391
|
return /open-device: configured iOS simulator '.+' does not exist|open-device: no MetaMask bundle found|fast mode requires an installed (?:iOS dev client|Android dev client)/u.test(output);
|
|
379
392
|
}
|
|
@@ -4,8 +4,10 @@ import {
|
|
|
4
4
|
recordMobileSourceBaseline
|
|
5
5
|
} from "../../adapters/mobile/source-freshness.js";
|
|
6
6
|
import { ensureHarnessFresh } from "../../adapters/harness-freshness.js";
|
|
7
|
+
import { clearMobileReleaseArtifactState } from "../../adapters/mobile/release-artifact-state.js";
|
|
7
8
|
import { EXIT } from "../shared.js";
|
|
8
9
|
async function launchMobile(target, mobileTarget, tier, json, restartApp = false, clearMetro = false) {
|
|
10
|
+
clearMobileReleaseArtifactState(target);
|
|
9
11
|
await ensureHarnessFresh(target, "mobile");
|
|
10
12
|
const platform = mobileTarget ?? "ios";
|
|
11
13
|
const watcherPort = process.env.WATCHER_PORT ? parseInt(process.env.WATCHER_PORT, 10) : void 0;
|
|
@@ -1,6 +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 { clearMobileReleaseArtifactState } from "../adapters/mobile/release-artifact-state.js";
|
|
4
5
|
import { ADAPTER_DETECT_NEXT, usageOut, writeInteractiveProgress } from "./shared.js";
|
|
5
6
|
import {
|
|
6
7
|
applyWatcherPortOption,
|
|
@@ -28,6 +29,7 @@ async function handleProvision({ positional, options, rawArgv }) {
|
|
|
28
29
|
const platform = optionString(options, "platform") ?? optionString(options, "devicePlatform") ?? (positional[0] === "runway" ? positional[1] : positional[0]) ?? "ios";
|
|
29
30
|
if (adapter === "mobile") {
|
|
30
31
|
writeInteractiveProgress(json, `\u2192 provision mobile ${platform} \u2014 resolving simulator and Runway artifact`);
|
|
32
|
+
if (!optionFlag(options, "resolveOnly")) clearMobileReleaseArtifactState(target);
|
|
31
33
|
}
|
|
32
34
|
const result = await surface.runwayProvision.run(target, {
|
|
33
35
|
json,
|