@deeeed/metamask-harness 0.73.0 → 0.75.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.
Files changed (62) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +1 -1
  3. package/adapters/shared/ensure-runner-deps.sh +15 -3
  4. package/dist/action-values.js +15 -0
  5. package/dist/adapters/core/actions.js +20 -0
  6. package/dist/adapters/core/live-script-env.js +36 -0
  7. package/dist/adapters/core/run.js +54 -0
  8. package/dist/adapters/core/surface.js +8 -1
  9. package/dist/adapters/extension/action-inputs.js +206 -0
  10. package/dist/adapters/extension/actions.js +51 -0
  11. package/dist/adapters/extension/console-capture.js +13 -1
  12. package/dist/adapters/extension/launch.js +315 -0
  13. package/dist/adapters/extension/launched-browser.js +51 -0
  14. package/dist/adapters/extension/run.js +105 -0
  15. package/dist/adapters/extension/surface.js +17 -1
  16. package/dist/adapters/mobile/actions.js +54 -0
  17. package/dist/adapters/mobile/launch.js +192 -0
  18. package/dist/adapters/mobile/lifecycle.js +300 -0
  19. package/dist/adapters/mobile/run.js +156 -0
  20. package/dist/adapters/mobile/runtime-identity.js +34 -0
  21. package/dist/adapters/mobile/surface.js +19 -1
  22. package/dist/adapters/mobile/ui.js +701 -0
  23. package/dist/adapters/performance/cdp-trace.js +24 -6
  24. package/dist/adapters/performance/device-platform.js +10 -0
  25. package/dist/adapters/registry.js +17 -0
  26. package/dist/adapters/surface.js +6 -11
  27. package/dist/adapters/terminal/actions.js +43 -0
  28. package/dist/adapters/terminal/console-capture.js +14 -1
  29. package/dist/adapters/terminal/launch.js +85 -0
  30. package/dist/adapters/terminal/run.js +30 -0
  31. package/dist/adapters/terminal/surface.js +9 -1
  32. package/dist/adapters.js +32 -824
  33. package/dist/cli-commands.js +1 -0
  34. package/dist/command-contract.js +3 -0
  35. package/dist/commands/call.js +2 -5
  36. package/dist/commands/launch/device.js +342 -0
  37. package/dist/commands/launch/index.js +9 -942
  38. package/dist/commands/launch/quote.js +7 -0
  39. package/dist/commands/pin.js +226 -0
  40. package/dist/commands/review.js +3 -1
  41. package/dist/commands/run-engine.js +38 -138
  42. package/dist/commands/run-report.js +4 -48
  43. package/dist/commands/run.js +6 -48
  44. package/dist/commands/shared.js +2 -2
  45. package/dist/doctor.js +3 -1
  46. package/dist/execution-provenance.js +3 -2
  47. package/dist/harness-pin.js +10 -0
  48. package/dist/heal-bounds.js +3 -2
  49. package/dist/live-adapter-contract.js +8 -34
  50. package/dist/manifest.js +3 -5
  51. package/dist/metamask-action-validation.js +5 -196
  52. package/dist/mm-harness-cli.js +27 -2
  53. package/dist/network-observation.js +10 -3
  54. package/dist/performance-observation.js +15 -22
  55. package/dist/pr-body/render.js +3 -1
  56. package/dist/recording-target.js +4 -39
  57. package/dist/run-diagnostics.js +21 -60
  58. package/dist/run-recording.js +5 -4
  59. package/dist/runner.js +20 -368
  60. package/docs/CONTRIBUTING.md +20 -3
  61. package/package.json +2 -1
  62. package/scripts/completions.sh +2 -1
@@ -19,6 +19,7 @@ const SPEC = {
19
19
  { name: "ports", desc: "Slot ports and runtime paths", flags: ["--json"] },
20
20
  { name: "up", desc: "Decide + run minimum work to reach ready", flags: ["--json", "--dry-run"] },
21
21
  { name: "sync", desc: "Refresh harness + canonicalize wallet fixture", flags: ["--json"] },
22
+ { name: "pin", desc: "Copy this install into an immutable store for a task to run", flags: ["--json"] },
22
23
  { name: "logs", aliases: ["tail"], desc: "Compact build events or full log", flags: ["--full", "-f", "--window", "--events", "--source", "--json"] },
23
24
  { name: "debug", aliases: ["devtools", "inspect"], desc: "Open DevTools UI", flags: ["--json", "--no-open"] },
24
25
  { name: "reload", desc: "Reload the connected app runtime", flags: ["--json"] },
@@ -398,6 +398,9 @@ const PUBLIC_COMMAND_CONTRACTS = {
398
398
  update: {
399
399
  options: options(HELP, JSON, { "--check": bool() })
400
400
  },
401
+ pin: {
402
+ options: options(HELP, JSON)
403
+ },
401
404
  fixtures: {
402
405
  options: options(HELP, JSON, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM, DEVICE, {
403
406
  "--from": value(),
@@ -28,7 +28,6 @@ import {
28
28
  import { getAdapterSurface } from "../adapters/surface.js";
29
29
  import { handleListExecutables } from "./list-executables.js";
30
30
  import { applyDeviceTargeting } from "./device-target.js";
31
- import { coreDependencyBlock } from "./core-readiness.js";
32
31
  import {
33
32
  emitHealViolation,
34
33
  executeWithHealBounds,
@@ -40,7 +39,6 @@ import {
40
39
  synthesizeOneNodeRecipe,
41
40
  validateRecipeAdapterAware
42
41
  } from "./run-engine.js";
43
- import { readMobileReleaseArtifactState } from "../adapters/mobile/release-artifact-state.js";
44
42
  import { acquireCheckoutLock } from "../checkout-lock.js";
45
43
  import { formatRunDiagnosticsForHuman, readRunDiagnosticsDocument } from "../run-diagnostics.js";
46
44
  import {
@@ -178,7 +176,7 @@ async function handleCall(argv) {
178
176
  const resolvedAction = resolution.resolved;
179
177
  const describedAction = describeManifestActions(manifest, actionSources).find((entry) => entry.name === resolvedAction);
180
178
  const defaultsUsed = actionDefaultsUsed(describedAction, args);
181
- const depsBlock = adapter === "core" && resolvedAction.startsWith("metamask.perps.") ? coreDependencyBlock(target) : null;
179
+ const depsBlock = await getAdapterSurface(adapter).run?.dependencyBlock?.(target, { action: resolvedAction }) ?? null;
182
180
  if (depsBlock) {
183
181
  if (json) {
184
182
  console.log(JSON.stringify({ schemaVersion: 1, command: "call", adapter, status: "fail", exitCode: EXIT.usage, error: depsBlock }, null, 2));
@@ -286,9 +284,8 @@ async function handleCall(argv) {
286
284
  return checkoutBusyOut(json, "call", lock.message, lock.path);
287
285
  }
288
286
  try {
289
- const authoredMobileRestart = adapter === "mobile" && resolvedAction === "app.lifecycle" && args.command === "restart";
290
287
  const prepared = await prepareHeal(adapter, target, options, json, {
291
- skipMobileSourceFreshness: authoredMobileRestart || adapter === "mobile" && readMobileReleaseArtifactState(target) !== null
288
+ appRestartAuthored: resolvedAction === "app.lifecycle" && args.command === "restart"
292
289
  });
293
290
  if (typeof prepared === "number") return prepared;
294
291
  const { state } = prepared;
@@ -0,0 +1,342 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import { color, colorHumanMessage } from "../../cli-color.js";
4
+ import { handleHarness } from "../../harness.js";
5
+ import { recipeRuntimePath, walletFixturePath } from "../../paths.js";
6
+ import { getAdapterSurface } from "../../adapters/surface.js";
7
+ import { EXIT, flag, str } from "../shared.js";
8
+ import {
9
+ checkHealBounds,
10
+ ensureOverlay,
11
+ newHealState
12
+ } from "../../heal-bounds.js";
13
+ import {
14
+ FeatureFlagSpecError,
15
+ fixtureFeatureFlags,
16
+ parseRemoteFlagSpec,
17
+ remoteFlagRequestFor
18
+ } from "../../feature-flags.js";
19
+ import { terminalOnlyLaunchFlags } from "../../adapters/terminal/launch.js";
20
+ import { shellQuote } from "./quote.js";
21
+ async function runDeviceLaunch(context, profile) {
22
+ const { adapter, options, target, heal, jsonOutput, machine, stream, usage } = context;
23
+ const terminalOnly = terminalOnlyLaunchFlags(options);
24
+ if (terminalOnly.length > 0) {
25
+ return usage(
26
+ `${terminalOnly.join(", ")} apply only to --adapter terminal; ${adapter} does not read them.`,
27
+ "drop the terminal flags, or pass --adapter terminal"
28
+ );
29
+ }
30
+ const wantBuild = flag(options, "build");
31
+ const wantVerify = flag(options, "verify");
32
+ const wantWatch = flag(options, "watch");
33
+ const wantRunway = flag(options, "runway");
34
+ const wantClearMetro = flag(options, "clearMetro");
35
+ const requestedSurface = str(options, "surface");
36
+ const legacySidepanel = flag(options, "sidepanel");
37
+ const legacyFullscreen = flag(options, "fullscreen");
38
+ const remoteFlagSpec = str(options, "remoteFlag");
39
+ const refused = profile.refuseFlags(context, { watch: wantWatch, runway: wantRunway, clearMetro: wantClearMetro, remoteFlagSpec });
40
+ if (refused) return usage(refused.message, refused.userAction);
41
+ if (requestedSurface !== void 0 && requestedSurface !== "fullscreen" && requestedSurface !== "sidepanel") {
42
+ return usage(
43
+ `--surface must be fullscreen or sidepanel (got: ${requestedSurface}).`,
44
+ "pass --surface fullscreen or --surface sidepanel"
45
+ );
46
+ }
47
+ if (legacySidepanel && legacyFullscreen) {
48
+ return usage("--sidepanel and --fullscreen conflict.", "pass exactly one Extension surface");
49
+ }
50
+ const legacySurface = legacySidepanel ? "sidepanel" : legacyFullscreen ? "fullscreen" : void 0;
51
+ if (requestedSurface && legacySurface && requestedSurface !== legacySurface) {
52
+ return usage(
53
+ `--surface ${requestedSurface} conflicts with --${legacySurface}.`,
54
+ "pass one consistent Extension surface"
55
+ );
56
+ }
57
+ const displayMode = requestedSurface ?? legacySurface ?? "fullscreen";
58
+ let remoteFlagRequest;
59
+ try {
60
+ remoteFlagRequest = remoteFlagRequestFor({
61
+ ...fixtureFeatureFlags(walletFixturePath(target)),
62
+ ...remoteFlagSpec === void 0 ? {} : parseRemoteFlagSpec(remoteFlagSpec)
63
+ });
64
+ } catch (error) {
65
+ if (!(error instanceof FeatureFlagSpecError)) throw error;
66
+ return usage(error.message, error.userAction);
67
+ }
68
+ for (const portFlag of ["cdpPort", "watcherPort"]) {
69
+ const value = str(options, portFlag);
70
+ if (value !== void 0 && !/^\d+$/u.test(value)) {
71
+ return usage(
72
+ `--${portFlag === "cdpPort" ? "cdp-port" : "watcher-port"} must be numeric (got: ${value}).`,
73
+ `pass a numeric port, e.g. --${portFlag === "cdpPort" ? "cdp-port 6663" : "watcher-port 8081"}`
74
+ );
75
+ }
76
+ }
77
+ getAdapterSurface(adapter).resolveSlotPorts(target);
78
+ const envError = profile.applyEnv?.(context);
79
+ if (envError) return usage(envError.message, envError.userAction);
80
+ const cdpPort = str(options, "cdpPort");
81
+ if (cdpPort) {
82
+ process.env.CDP_PORT = cdpPort;
83
+ process.env.RECIPE_CDP_PORT = cdpPort;
84
+ }
85
+ const watcherPort = str(options, "watcherPort");
86
+ if (watcherPort) {
87
+ process.env.WATCHER_PORT = watcherPort;
88
+ process.env.RECIPE_WATCHER_PORT = watcherPort;
89
+ process.env.METRO_PORT = watcherPort;
90
+ }
91
+ const launch = {
92
+ ...context,
93
+ tier: wantBuild ? "build" : "quick",
94
+ wantWatch,
95
+ wantVerify,
96
+ wantClearMetro,
97
+ displayMode,
98
+ remoteFlagRequest,
99
+ compose: (tier2, restartApp = false, clearMetro = false) => profile.compose(launch, tier2, restartApp, clearMetro),
100
+ settle: (state2, provenFlags, sidepanelRecoveryAllowed = true) => settleLaunch(launch, profile, state2, sidepanelRecoveryAllowed, provenFlags),
101
+ fail: (state2, failure) => launchFail(launch, profile, state2, failure)
102
+ };
103
+ const { tier } = launch;
104
+ profile.start(launch);
105
+ const state = newHealState();
106
+ if (tier === "quick" && nativeInputsChanged(target, profile.nativeInputs)) {
107
+ return usage("native build inputs changed since last build.", profile.rebuildCommand(launch));
108
+ }
109
+ stream.phase("install");
110
+ const ensured = await ensureOverlay(adapter, target, heal, state, machine);
111
+ if (!ensured.ok) {
112
+ return launch.fail(state, {
113
+ code: "OVERLAY_INSTALL_FAILED",
114
+ message: ensured.error ?? "overlay install failed",
115
+ recoverable: false,
116
+ exitCode: EXIT.infra
117
+ });
118
+ }
119
+ stream.phase("healthcheck");
120
+ const blocked = await profile.healthcheck?.(launch, state);
121
+ if (blocked !== void 0) return blocked;
122
+ stream.phase("launch");
123
+ let attempt = await launch.compose(tier, false, wantClearMetro);
124
+ if (attempt.status === 0) {
125
+ return await launch.settle(state, attempt.remoteFeatureFlags);
126
+ }
127
+ const readinessFailure = readinessBlockFailure(attempt);
128
+ if (readinessFailure) return launch.fail(state, readinessFailure);
129
+ const classified = profile.classify(launch, attempt.output);
130
+ if (classified) return launch.fail(state, classified);
131
+ if (heal === "off") {
132
+ return launch.fail(state, {
133
+ code: "LAUNCH_FAILED",
134
+ message: `launch ${adapter}${launch.mobileTarget ? ` ${launch.mobileTarget}` : ""} failed (healing off)`,
135
+ recoverable: false,
136
+ exitCode: EXIT.infra
137
+ });
138
+ }
139
+ const handled = await profile.beforeHeal?.(launch, attempt, state);
140
+ if (handled !== void 0) return handled;
141
+ const bound = checkHealBounds(target, attempt.output, state);
142
+ if (bound !== null) {
143
+ return launch.fail(state, {
144
+ code: bound.code,
145
+ message: bound.message,
146
+ recoverable: false,
147
+ userAction: bound.userAction,
148
+ exitCode: bound.exitCode,
149
+ originalError: bound.originalError
150
+ });
151
+ }
152
+ const recovery = profile.recovery(launch, attempt.output);
153
+ state.attemptedRecoveries.push(recovery.code);
154
+ stream.phase("recover");
155
+ const recoveryTier = recovery.restartApp && tier === "build" ? "quick" : tier;
156
+ if (!jsonOutput) {
157
+ process.stderr.write(`launch: recovering (${recovery.code}) with the ${recoveryTier} tier
158
+ `);
159
+ }
160
+ attempt = await launch.compose(recoveryTier, recovery.restartApp, false);
161
+ if (attempt.status === 0) {
162
+ state.recovered.push(recovery.code);
163
+ return await launch.settle(state, attempt.remoteFeatureFlags);
164
+ }
165
+ const retryReadinessFailure = readinessBlockFailure(attempt);
166
+ if (retryReadinessFailure) return launch.fail(state, retryReadinessFailure);
167
+ return launch.fail(state, {
168
+ code: "SAME_RECOVERY_TWICE",
169
+ message: `launch ${adapter}${launch.mobileTarget ? ` ${launch.mobileTarget}` : ""} failed again after ${recovery.code} recovery \u2014 refusing to loop.`,
170
+ recoverable: false,
171
+ exitCode: EXIT.bounded,
172
+ originalError: attempt.output.trim() || void 0
173
+ });
174
+ }
175
+ function readinessBlockFailure(result) {
176
+ const block = result.readinessBlock;
177
+ if (!block) return void 0;
178
+ return {
179
+ code: block.reasonCode === "deps-not-ready" ? "MOBILE_DEPENDENCIES_NOT_READY" : "MOBILE_READINESS_BLOCKED",
180
+ message: block.message,
181
+ userAction: block.userAction,
182
+ recoverable: false,
183
+ exitCode: result.status
184
+ };
185
+ }
186
+ function nativeInputsChanged(target, paths) {
187
+ const baselineFile = recipeRuntimePath(target, ".last-build-ref");
188
+ let baseline;
189
+ try {
190
+ baseline = fs.readFileSync(baselineFile, "utf8").trim();
191
+ } catch {
192
+ return false;
193
+ }
194
+ if (!baseline) return false;
195
+ try {
196
+ const diff = execFileSync("git", ["-C", target, "diff", "--name-only", baseline, "--", ...paths], {
197
+ encoding: "utf8",
198
+ stdio: ["ignore", "pipe", "ignore"]
199
+ }).trim();
200
+ return diff.length > 0;
201
+ } catch {
202
+ return false;
203
+ }
204
+ }
205
+ async function settleLaunch(launch, profile, state, sidepanelRecoveryAllowed, provenFlags) {
206
+ const proven = profile.provenFlags(launch, launch.remoteFlagRequest?.flags ?? {}, provenFlags);
207
+ if (!("flags" in proven)) return launch.fail(state, proven);
208
+ const stopped = await profile.finish?.(launch, state, sidepanelRecoveryAllowed);
209
+ if (stopped !== void 0) return stopped;
210
+ const { adapter, target, stream, tier } = launch;
211
+ if (launch.wantVerify) {
212
+ stream.phase("verify");
213
+ const verifyArgs = ["verify", "--adapter", adapter, "--target", target, ...profile.verifyArgs(launch)];
214
+ if (launch.machine) verifyArgs.push("--json");
215
+ const exitCode = await handleHarness(verifyArgs);
216
+ if (stream.enabled) {
217
+ for (const mutation of state.mutations) stream.mutation(mutation);
218
+ for (const recovery of state.recovered) stream.recovery(recovery);
219
+ stream.complete(exitCode === EXIT.ok ? "pass" : "fail", exitCode, {
220
+ adapter,
221
+ platform: profile.platformLabel(launch),
222
+ tier,
223
+ recovered: state.recovered,
224
+ mutations: state.mutations,
225
+ metro: profile.evidence?.(target),
226
+ remoteFeatureFlags: proven.flags
227
+ });
228
+ }
229
+ return exitCode;
230
+ }
231
+ return launchPass(launch, profile, state, proven.flags);
232
+ }
233
+ function launchPass(launch, profile, state, remoteFlags) {
234
+ const { adapter, mobileTarget, target, stream, tier } = launch;
235
+ if (stream.enabled) {
236
+ for (const mutation of state.mutations) stream.mutation(mutation);
237
+ for (const recovery of state.recovered) stream.recovery(recovery);
238
+ stream.complete("pass", EXIT.ok, {
239
+ adapter,
240
+ platform: profile.platformLabel(launch),
241
+ tier,
242
+ recovered: state.recovered,
243
+ mutations: state.mutations,
244
+ metro: profile.evidence?.(target),
245
+ remoteFeatureFlags: remoteFlags
246
+ });
247
+ } else if (launch.jsonOutput) {
248
+ console.log(
249
+ JSON.stringify(
250
+ {
251
+ schemaVersion: 1,
252
+ command: "launch",
253
+ adapter,
254
+ platform: profile.platformLabel(launch),
255
+ tier,
256
+ status: "pass",
257
+ phase: "launch",
258
+ recovered: state.recovered,
259
+ mutations: state.mutations,
260
+ metro: profile.evidence?.(target),
261
+ remoteFeatureFlags: remoteFlags,
262
+ exitCode: EXIT.ok
263
+ },
264
+ null,
265
+ 2
266
+ )
267
+ );
268
+ } else {
269
+ const tierNote = tier === "quick" ? "quick relaunch, no native build" : tier;
270
+ const devNote = process.env.MM_HARNESS_BIN ? ` ${color("dim", "[dev: MM_HARNESS_BIN]")}` : "";
271
+ console.error(
272
+ `${color("ok", "\u2713")} launch ${adapter}${mobileTarget ? ` ${mobileTarget}` : ""} \u2014 ${color("ok", profile.deviceLabel(launch))} \xB7 ${tierNote} \xB7 app + bridge ready${devNote}`
273
+ );
274
+ }
275
+ return EXIT.ok;
276
+ }
277
+ function launchFail(launch, profile, state, failure) {
278
+ const { adapter, mobileTarget, target, stream, tier } = launch;
279
+ const userAction = failure.userAction ?? `mm-harness doctor --fix --adapter ${adapter} --target ${shellQuote(target)} --json`;
280
+ if (stream.enabled) {
281
+ for (const mutation of state.mutations) stream.mutation(mutation);
282
+ for (const recovery of state.recovered) stream.recovery(recovery);
283
+ stream.error({
284
+ code: failure.code,
285
+ message: failure.message,
286
+ retryable: failure.recoverable,
287
+ userAction,
288
+ originalError: failure.originalError ?? null
289
+ });
290
+ stream.complete("fail", failure.exitCode, {
291
+ adapter,
292
+ platform: mobileTarget ?? null,
293
+ tier,
294
+ recovered: state.recovered,
295
+ mutations: state.mutations,
296
+ recoverable: failure.recoverable,
297
+ attemptedRecoveries: state.attemptedRecoveries,
298
+ metro: profile.evidence?.(target)
299
+ });
300
+ } else if (launch.jsonOutput) {
301
+ console.log(
302
+ JSON.stringify(
303
+ {
304
+ schemaVersion: 1,
305
+ command: "launch",
306
+ adapter,
307
+ platform: mobileTarget ?? null,
308
+ tier,
309
+ status: "fail",
310
+ phase: state.attemptedRecoveries.length ? "recover" : "launch",
311
+ recovered: state.recovered,
312
+ mutations: state.mutations,
313
+ recoverable: failure.recoverable,
314
+ attemptedRecoveries: state.attemptedRecoveries,
315
+ metro: profile.evidence?.(target),
316
+ exitCode: failure.exitCode,
317
+ error: {
318
+ code: failure.code,
319
+ message: failure.message,
320
+ retryable: failure.recoverable,
321
+ userAction,
322
+ originalError: failure.originalError ?? null
323
+ }
324
+ },
325
+ null,
326
+ 2
327
+ )
328
+ );
329
+ } else {
330
+ console.error(colorHumanMessage(
331
+ `\u2717 launch ${adapter}${mobileTarget ? ` ${mobileTarget}` : ""} failed
332
+ ${failure.message}` + (failure.originalError ? `
333
+ --- original failure ---
334
+ ${failure.originalError}` : "") + `
335
+ Next: ${userAction}`
336
+ ));
337
+ }
338
+ return failure.exitCode;
339
+ }
340
+ export {
341
+ runDeviceLaunch
342
+ };