@deeeed/metamask-harness 0.74.0 → 0.76.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 (96) hide show
  1. package/CHANGELOG.md +39 -1
  2. package/README.md +1 -1
  3. package/adapters/shared/ensure-runner-deps.sh +15 -3
  4. package/adapters/shared/reap-checkout-metros.sh +9 -0
  5. package/dist/action-values.js +15 -0
  6. package/dist/adapters/core/actions.js +20 -0
  7. package/dist/adapters/core/live-script-env.js +36 -0
  8. package/dist/adapters/core/overlay.js +34 -0
  9. package/dist/adapters/core/readiness.js +8 -0
  10. package/dist/adapters/core/run.js +54 -0
  11. package/dist/adapters/core/surface.js +12 -1
  12. package/dist/adapters/extension/action-inputs.js +206 -0
  13. package/dist/adapters/extension/actions.js +51 -0
  14. package/dist/adapters/extension/console-capture.js +13 -1
  15. package/dist/adapters/extension/doctor-checks.js +135 -0
  16. package/dist/adapters/extension/fixtures.js +103 -0
  17. package/dist/adapters/extension/launch.js +315 -0
  18. package/dist/adapters/extension/launched-browser.js +51 -0
  19. package/dist/adapters/extension/overlay.js +55 -0
  20. package/dist/adapters/extension/readiness.js +32 -0
  21. package/dist/adapters/extension/reload.js +40 -0
  22. package/dist/adapters/extension/run.js +105 -0
  23. package/dist/adapters/extension/surface.js +31 -3
  24. package/dist/adapters/harness-freshness.js +4 -2
  25. package/dist/adapters/mobile/actions.js +54 -0
  26. package/dist/adapters/mobile/devices.js +21 -0
  27. package/dist/adapters/mobile/doctor-checks.js +40 -0
  28. package/dist/adapters/mobile/fixtures.js +141 -0
  29. package/dist/adapters/mobile/launch.js +192 -0
  30. package/dist/adapters/mobile/lifecycle.js +300 -0
  31. package/dist/adapters/mobile/overlay.js +27 -0
  32. package/dist/adapters/mobile/prepare-target.js +38 -0
  33. package/dist/adapters/mobile/readiness.js +39 -0
  34. package/dist/adapters/mobile/reload.js +15 -0
  35. package/dist/adapters/mobile/run.js +156 -0
  36. package/dist/adapters/mobile/runtime-identity.js +34 -0
  37. package/dist/adapters/mobile/surface.js +39 -3
  38. package/dist/adapters/mobile/ui.js +701 -0
  39. package/dist/adapters/performance/cdp-trace.js +24 -6
  40. package/dist/adapters/performance/device-platform.js +10 -0
  41. package/dist/adapters/registry.js +1 -1
  42. package/dist/adapters/terminal/actions.js +43 -0
  43. package/dist/adapters/terminal/console-capture.js +14 -1
  44. package/dist/adapters/terminal/doctor-checks.js +22 -0
  45. package/dist/adapters/terminal/overlay.js +18 -0
  46. package/dist/adapters/terminal/readiness.js +11 -0
  47. package/dist/adapters/terminal/run.js +30 -0
  48. package/dist/adapters/terminal/surface.js +21 -3
  49. package/dist/adapters.js +32 -824
  50. package/dist/cli-commands.js +1 -0
  51. package/dist/cli.js +3 -1
  52. package/dist/command-contract.js +3 -0
  53. package/dist/commands/call.js +2 -5
  54. package/dist/commands/debug.js +6 -25
  55. package/dist/commands/device-target.js +2 -1
  56. package/dist/commands/doctor.js +26 -137
  57. package/dist/commands/fixtures.js +13 -217
  58. package/dist/commands/launch/device.js +2 -6
  59. package/dist/commands/launch/extension.js +3 -303
  60. package/dist/commands/launch/index.js +1 -1
  61. package/dist/commands/launch/mobile.js +2 -190
  62. package/dist/commands/launch/quote.js +7 -0
  63. package/dist/commands/logs.js +1 -1
  64. package/dist/commands/parse-args.js +9 -4
  65. package/dist/commands/pin.js +226 -0
  66. package/dist/commands/prepare.js +23 -53
  67. package/dist/commands/provision.js +3 -7
  68. package/dist/commands/reload.js +8 -47
  69. package/dist/commands/run-engine.js +38 -138
  70. package/dist/commands/run-report.js +4 -48
  71. package/dist/commands/run.js +6 -48
  72. package/dist/commands/runtime-decision.js +5 -27
  73. package/dist/commands/self-test.js +1 -3
  74. package/dist/commands/status.js +20 -28
  75. package/dist/commands/stop.js +1 -1
  76. package/dist/doctor.js +12 -120
  77. package/dist/execution-provenance.js +3 -2
  78. package/dist/harness-pin.js +10 -0
  79. package/dist/harness.js +31 -100
  80. package/dist/heal-bounds.js +3 -2
  81. package/dist/live-adapter-contract.js +8 -34
  82. package/dist/manifest.js +3 -5
  83. package/dist/metamask-action-validation.js +5 -196
  84. package/dist/mm-harness-cli.js +30 -4
  85. package/dist/network-observation.js +10 -3
  86. package/dist/paths.js +8 -2
  87. package/dist/performance-observation.js +15 -22
  88. package/dist/pr-body/render.js +3 -1
  89. package/dist/recording-target.js +4 -39
  90. package/dist/run-diagnostics.js +21 -60
  91. package/dist/run-recording.js +5 -4
  92. package/dist/runner.js +20 -368
  93. package/dist/runtime-context.js +12 -17
  94. package/docs/CONTRIBUTING.md +29 -7
  95. package/package.json +2 -1
  96. package/scripts/completions.sh +2 -1
@@ -0,0 +1,32 @@
1
+ import { optionFlag, optionString, parsePort } from "../../commands/parse-args.js";
2
+ import { ensureExtensionPerpsBuildFlag, extensionBrowserDoctorCheck, extensionBrowserResolutionDoctorCheck } from "./doctor-checks.js";
3
+ import { readExtensionPinnedFlags } from "./feature-flags.js";
4
+ import { extensionProductConfigBlock } from "./product-config.js";
5
+ const extensionReadiness = {
6
+ compatibilityMode(_target, shape) {
7
+ if (!shape.extensionProject) return "unsupported/no bridge";
8
+ if (shape.extensionRuntime || shape.injectedHarness) return "bridge present";
9
+ return "bridge injectable";
10
+ },
11
+ checks: (target) => [extensionBrowserDoctorCheck(target), extensionBrowserResolutionDoctorCheck(target)],
12
+ captureProviders: ["cdp"],
13
+ fixes: [{ id: "extension-perps-build", apply: ensureExtensionPerpsBuildFlag }],
14
+ runtimeBlock: (target, runtime) => runtime?.reasonCode === "product-config-required" ? { id: "extension-product-config", userAction: extensionProductConfigBlock(target)?.userAction } : void 0,
15
+ pinnedFlags: readExtensionPinnedFlags,
16
+ statusRuntime: true
17
+ };
18
+ async function extensionRuntimeDecision(target, options) {
19
+ const cdpPortRaw = optionString(options, "cdpPort") ?? process.env.CDP_PORT ?? process.env.RECIPE_CDP_PORT;
20
+ const cdpPort = cdpPortRaw === void 0 ? void 0 : parsePort(cdpPortRaw, "runtime-decision --cdp-port must be a port.");
21
+ const { decideExtensionReadiness } = await import("./runtime-decision.js");
22
+ const report = await decideExtensionReadiness(target, {
23
+ cdpPort,
24
+ watchLog: optionString(options, "watchLog"),
25
+ record: optionFlag(options, "record")
26
+ });
27
+ return { report, line: `${report.decision}${report.clean ? " (clean)" : ""} ${report.reasonCode} \u2014 ${report.reasons[0] ?? ""}` };
28
+ }
29
+ export {
30
+ extensionReadiness,
31
+ extensionRuntimeDecision
32
+ };
@@ -0,0 +1,40 @@
1
+ import path from "node:path";
2
+ import { EXIT, spawnScript, usageOut } from "../../commands/shared.js";
3
+ import { runnerDir } from "../../paths.js";
4
+ function reloadExtension(target, json) {
5
+ const adapter = "extension";
6
+ if (!process.env.CDP_PORT) {
7
+ return usageOut(
8
+ json,
9
+ "reload",
10
+ "the Extension CDP port could not be resolved.",
11
+ "mm-harness launch"
12
+ );
13
+ }
14
+ const script = path.join(runnerDir, "adapters/extension/reattach.sh");
15
+ const args = ["--target", target, "--cdp-port", process.env.CDP_PORT];
16
+ if (process.env.WATCHER_PORT) {
17
+ args.push("--watcher-port", process.env.WATCHER_PORT);
18
+ }
19
+ const result = spawnScript(script, args, target, json);
20
+ if (json) {
21
+ console.log(
22
+ JSON.stringify(
23
+ {
24
+ ok: result.status === 0,
25
+ adapter,
26
+ method: "cdp-reattach",
27
+ command: "reload",
28
+ cdpPort: Number(process.env.CDP_PORT),
29
+ ...result.status === 0 ? {} : { error: result.output.slice(-4e3) || "Extension reload failed." }
30
+ },
31
+ null,
32
+ 2
33
+ )
34
+ );
35
+ }
36
+ return result.status === 0 ? EXIT.ok : EXIT.runtime;
37
+ }
38
+ export {
39
+ reloadExtension
40
+ };
@@ -0,0 +1,105 @@
1
+ import { optionString } from "../../commands/parse-args.js";
2
+ import { resolveExtensionBrowserPid } from "../../recording-target.js";
3
+ import {
4
+ ensureExtensionConsoleCapture,
5
+ extensionCaptureFiles,
6
+ extensionCdpPort,
7
+ verifyExtensionConsoleControl
8
+ } from "./console-capture.js";
9
+ import { launchedBrowser } from "./launched-browser.js";
10
+ const extensionRun = {
11
+ async prepareRuntime(projectRoot, runtimeOptions) {
12
+ if (runtimeOptions.skipExtensionRuntimePrepare === true) return;
13
+ const { releaseArtifactState, runtimeDistCheck } = await import("./runtime-decision.js");
14
+ const artifact = releaseArtifactState(projectRoot);
15
+ const runtimeSnapshot = runtimeDistCheck(projectRoot, artifact);
16
+ if (artifact.status !== "none") {
17
+ if (artifact.status === "valid" && runtimeSnapshot.status === "fresh") return;
18
+ throw new Error(
19
+ "The loaded release artifact identity is invalid or stale. Next: rerun runtime-launch with the same artifact source."
20
+ );
21
+ }
22
+ const { prepareExtensionRuntime } = await import("./runtime.js");
23
+ await prepareExtensionRuntime({
24
+ projectRoot,
25
+ cdpPort: runtimeOptions.cdpPort,
26
+ slot: runtimeOptions.slot,
27
+ launchExistingDist: runtimeOptions.launchExistingDist === true,
28
+ validationRuntimeDir: runtimeOptions.validationRuntimeDir,
29
+ pageMode: "home-or-sidepanel"
30
+ });
31
+ },
32
+ // The proof runtime is checked on the run's own ports: explicit flags win over
33
+ // the slot's for the check, and the slot's come back afterwards.
34
+ runtimeCheck({ target, options, heal, state, json, onRecovery }) {
35
+ return async () => {
36
+ const cliOptions = options;
37
+ const previousCdpPort = process.env.CDP_PORT;
38
+ const previousRecipeCdpPort = process.env.RECIPE_CDP_PORT;
39
+ const previousWatcherPort = process.env.WATCHER_PORT;
40
+ const explicitCdpPort = optionString(cliOptions, "cdpPort");
41
+ const explicitWatcherPort = optionString(cliOptions, "watcherPort") ?? optionString(cliOptions, "metroPort");
42
+ if (explicitCdpPort) {
43
+ process.env.CDP_PORT = explicitCdpPort;
44
+ process.env.RECIPE_CDP_PORT = explicitCdpPort;
45
+ }
46
+ if (explicitWatcherPort) process.env.WATCHER_PORT = explicitWatcherPort;
47
+ try {
48
+ const { ensureExtensionProofRuntime } = await import("../../commands/run-engine.js");
49
+ return await ensureExtensionProofRuntime(target, heal, state, json, { onRecovery });
50
+ } finally {
51
+ restoreEnv("CDP_PORT", previousCdpPort);
52
+ restoreEnv("RECIPE_CDP_PORT", previousRecipeCdpPort);
53
+ restoreEnv("WATCHER_PORT", previousWatcherPort);
54
+ }
55
+ };
56
+ },
57
+ launchedBrowser: (target, artifactsDir, cdpPort) => launchedBrowser(target, artifactsDir, cdpPort)
58
+ };
59
+ const extensionRecording = {
60
+ target: (context) => resolveExtensionRecordingTarget(context),
61
+ framed: {
62
+ browserPid: (projectRoot, artifactsDir, cdpPort) => resolveExtensionBrowserPid(projectRoot, artifactsDir, cdpPort)
63
+ }
64
+ };
65
+ async function resolveExtensionRecordingTarget(context) {
66
+ const pid = resolveExtensionBrowserPid(
67
+ context.projectRoot,
68
+ context.artifactsDir,
69
+ context.env.CDP_PORT ?? context.env.RECIPE_CDP_PORT ?? process.env.CDP_PORT ?? process.env.RECIPE_CDP_PORT
70
+ );
71
+ if (!pid) {
72
+ throw new Error(
73
+ "Could not resolve the extension browser PID for --record-video. Ensure the slot browser is running and CDP_PORT is set."
74
+ );
75
+ }
76
+ return { kind: "pid", pid };
77
+ }
78
+ const extensionDiagnostics = {
79
+ console: {
80
+ start: (projectRoot) => ensureExtensionConsoleCapture(projectRoot),
81
+ files: (projectRoot) => extensionCaptureFiles(projectRoot),
82
+ cdpPort: () => extensionCdpPort(),
83
+ verifyControl: (capture) => verifyExtensionConsoleControl(capture)
84
+ }
85
+ };
86
+ const extensionObservation = {
87
+ network: {
88
+ backend: async (_target, env, artifactsDir) => (await import("../../network-observation.js")).createExtensionNetworkBackend(env, artifactsDir),
89
+ actions: true
90
+ },
91
+ performance: {
92
+ backend: async (target, env, ports) => (await import("../../performance-observation.js")).createExtensionPerformanceObserver(target, env, ports),
93
+ platform: () => "extension"
94
+ }
95
+ };
96
+ function restoreEnv(key, value) {
97
+ if (value === void 0) delete process.env[key];
98
+ else process.env[key] = value;
99
+ }
100
+ export {
101
+ extensionDiagnostics,
102
+ extensionObservation,
103
+ extensionRecording,
104
+ extensionRun
105
+ };
@@ -10,6 +10,10 @@ import {
10
10
  import { decideExtensionReadiness } from "./runtime-decision.js";
11
11
  import { extensionProductConfigBlock } from "./product-config.js";
12
12
  import { adapterRegistry } from "../registry.js";
13
+ import { extensionActions } from "./actions.js";
14
+ import { extensionHarness, extensionPrepare, extensionRuntimeContext } from "./overlay.js";
15
+ import { extensionReadiness, extensionRuntimeDecision } from "./readiness.js";
16
+ import { extensionDiagnostics, extensionObservation, extensionRecording, extensionRun } from "./run.js";
13
17
  function watcherStatus(buildLog) {
14
18
  if (buildLog === "ok") return "up";
15
19
  if (buildLog === "no-watch") return "down";
@@ -82,7 +86,8 @@ const extensionSurface = {
82
86
  spawnSync("bash", [stopViewers, "--target", target], { stdio: "ignore", timeout: 5e3 });
83
87
  const summary = stopped.signalled > 0 || stopped.removedMarkers > 0 ? `stopped Extension runtime (${stopped.signalled} process${stopped.signalled === 1 ? "" : "es"}, ${stopped.removedMarkers} stale marker${stopped.removedMarkers === 1 ? "" : "s"}) for ${target}` : `Extension runtime not running for ${target} \u2014 nothing to stop`;
84
88
  return { kind: "stopped", status: 0, summary, signalled: stopped.signalled };
85
- }
89
+ },
90
+ afterStop: (target) => `mm-harness launch --target ${shellQuote(target)}`
86
91
  },
87
92
  logSources(target) {
88
93
  return [
@@ -103,10 +108,33 @@ const extensionSurface = {
103
108
  launch: async (context) => {
104
109
  const [{ runDeviceLaunch }, { extensionLaunchProfile }] = await Promise.all([
105
110
  import("../../commands/launch/device.js"),
106
- import("../../commands/launch/extension.js")
111
+ import("./launch.js")
107
112
  ]);
108
113
  return runDeviceLaunch(context, extensionLaunchProfile);
109
- }
114
+ },
115
+ actions: extensionActions,
116
+ harness: extensionHarness,
117
+ runtimeContext: extensionRuntimeContext,
118
+ readiness: extensionReadiness,
119
+ fixtures: {
120
+ operations: ["init", "sync", "set", "reset", "generate", "finalize"],
121
+ apply: async (context) => (await import("./fixtures.js")).applyExtensionFixture(context)
122
+ },
123
+ prepare: extensionPrepare,
124
+ debug: {
125
+ refuse: ({ devMenu }) => devMenu ? { message: "--dev-menu is mobile-only (RN developer menu). Use --worker on the extension.", userAction: "mm-harness debug --worker" } : void 0,
126
+ args: ({ worker }) => [
127
+ ...process.env.CDP_PORT ? ["--cdp-port", process.env.CDP_PORT] : [],
128
+ "--target",
129
+ worker ? "worker" : "page"
130
+ ]
131
+ },
132
+ reload: async (target, json) => (await import("./reload.js")).reloadExtension(target, json),
133
+ runtimeDecision: extensionRuntimeDecision,
134
+ run: extensionRun,
135
+ recording: extensionRecording,
136
+ diagnostics: extensionDiagnostics,
137
+ observation: extensionObservation
110
138
  };
111
139
  adapterRegistry.register(extensionSurface);
112
140
  export {
@@ -3,6 +3,7 @@ import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { pathToFileURL } from "node:url";
5
5
  import { recipeHarnessPath, runnerDir } from "../paths.js";
6
+ import { getAdapterSurface } from "./surface.js";
6
7
  async function ensureHarnessFresh(target, adapter) {
7
8
  if (!fs.existsSync(path.join(target, "package.json")) || !hasCheckoutHead(target)) return;
8
9
  let current;
@@ -17,9 +18,10 @@ async function ensureHarnessFresh(target, adapter) {
17
18
  const installed = readInstalledFingerprint(manifestPath);
18
19
  if (installed === current) return;
19
20
  const reason = installed === null ? "harness not installed" : "runner changed";
20
- const injector = path.join(runnerDir, `adapters/${adapter}/inject.${adapter === "extension" ? "mjs" : "sh"}`);
21
+ const install = getAdapterSurface(adapter).harness.install;
22
+ const injector = path.resolve(runnerDir, install.entry);
21
23
  try {
22
- execFileSync(adapter === "extension" ? process.execPath : "bash", [injector, "--target", target], {
24
+ execFileSync(install.node ? process.execPath : "bash", [injector, "--target", target], {
23
25
  stdio: ["ignore", "ignore", "inherit"]
24
26
  });
25
27
  console.error(`\u2192 refreshed ${adapter} harness overlay (${reason})`);
@@ -0,0 +1,54 @@
1
+ import { manifestPath } from "../../paths.js";
2
+ const NATIVE_PROVIDER_UI_ACTIONS = /* @__PURE__ */ new Set([
3
+ "ui.press",
4
+ "ui.key_press",
5
+ "ui.set_input",
6
+ "ui.scroll",
7
+ "ui.swipe",
8
+ "ui.pan",
9
+ "ui.drag",
10
+ "ui.long_press",
11
+ "ui.wait_for",
12
+ "ui.screenshot",
13
+ "ui.capture_surface"
14
+ ]);
15
+ const mobileActions = {
16
+ manifestPath: () => manifestPath("mobile"),
17
+ semantic: [
18
+ "app.network_capture",
19
+ "app.network_assert",
20
+ "app.performance_capture",
21
+ "app.performance_assert",
22
+ "metamask.swap_bridge.submit_transaction",
23
+ "metamask.perps.close_positions",
24
+ "metamask.perps.start_state",
25
+ "metamask.perps.clear_performance_caches",
26
+ "metamask.perps.measure_homepage_visible",
27
+ "metamask.perps.prepare_local_snapshot_endpoint"
28
+ ],
29
+ cdpTarget: {
30
+ transport: "react-native-debug-bridge",
31
+ probePath: "json/list",
32
+ watcherPortFirst: true,
33
+ requiredProbe: async (node, context) => (await import("./ui.js")).probeMobileBridgeTarget(node, context)
34
+ },
35
+ ui: (harness) => ({
36
+ base: harness.createReactNativeBridgeUiTransport({
37
+ bridge: {
38
+ send: async (command, context) => (await import("./ui.js")).sendMobileBridgeCommand(command, context)
39
+ }
40
+ }),
41
+ async execute(action, node, context, standard) {
42
+ if (NATIVE_PROVIDER_UI_ACTIONS.has(action) && (context.env?.METAMASK_RECIPE_MOBILE_OPAQUE_RUNTIME === "1" || process.env.METAMASK_RECIPE_MOBILE_OPAQUE_RUNTIME === "1" || ["ui.swipe", "ui.pan", "ui.drag", "ui.long_press", "ui.capture_surface"].includes(action))) {
43
+ const { executeNativeProviderAction } = await import("./ui.js");
44
+ return executeNativeProviderAction(action, node, context, harness.createNativeUiTransport);
45
+ }
46
+ return standard();
47
+ }
48
+ }),
49
+ wrap: async (adapters) => (await import("./runtime-identity.js")).withMobileNativeRuntimeIdentity(adapters),
50
+ lifecycle: async (adapters) => (await import("./lifecycle.js")).mobileSourceAwareLifecycleAdapters(adapters)
51
+ };
52
+ export {
53
+ mobileActions
54
+ };
@@ -0,0 +1,21 @@
1
+ import {
2
+ mobileDeviceLiveView,
3
+ mobileDeviceView,
4
+ nextForLive,
5
+ renderAdditionalReachableDevices,
6
+ renderAndroidPortHints,
7
+ renderMobileDeviceList,
8
+ renderMobileLiveBlock
9
+ } from "../../commands/mobile-device-view.js";
10
+ const mobileDevices = {
11
+ view: mobileDeviceView,
12
+ live: mobileDeviceLiveView,
13
+ renderList: renderMobileDeviceList,
14
+ renderAdditional: renderAdditionalReachableDevices,
15
+ renderLive: renderMobileLiveBlock,
16
+ renderPortHints: renderAndroidPortHints,
17
+ nextForLive
18
+ };
19
+ export {
20
+ mobileDevices
21
+ };
@@ -0,0 +1,40 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import path from "node:path";
3
+ import {
4
+ mobileToolRecovery,
5
+ resolveMobileToolPath
6
+ } from "../../../library/actions/mobile/platform/tool-paths.mjs";
7
+ import { runnerDir } from "../../paths.js";
8
+ function mobileToolDoctorChecks(platform) {
9
+ const specs = platform === "android" ? [{ tool: "adb", platform: "Android" }] : platform === "ios" ? [{ tool: "idb", platform: "iOS" }] : [
10
+ { tool: "adb", platform: "Android" },
11
+ { tool: "idb", platform: "iOS" }
12
+ ];
13
+ return specs.map(({ tool, platform: platformName }) => {
14
+ const resolved = resolveMobileToolPath(tool);
15
+ return {
16
+ id: `device-tool-${tool}`,
17
+ status: resolved ? "pass" : "fail",
18
+ required: platform !== void 0,
19
+ message: resolved ? `${platformName} device tool ${tool} is ready.` : `${platformName} device tool ${tool} is unavailable.`,
20
+ ...resolved ? { detail: resolved } : {},
21
+ ...!resolved ? { userAction: mobileToolRecovery(tool) } : {}
22
+ };
23
+ });
24
+ }
25
+ function detectOrphanMetros(target, livePort) {
26
+ try {
27
+ const script = path.join(runnerDir, "adapters/shared/reap-checkout-metros.sh");
28
+ const out = execFileSync("bash", [script, "detect", target, livePort ?? ""], {
29
+ encoding: "utf8",
30
+ timeout: 5e3
31
+ });
32
+ return out.split("\n").map((line) => line.trim()).filter((line) => /^\d+$/u.test(line));
33
+ } catch {
34
+ return [];
35
+ }
36
+ }
37
+ export {
38
+ detectOrphanMetros,
39
+ mobileToolDoctorChecks
40
+ };
@@ -0,0 +1,141 @@
1
+ import path from "node:path";
2
+ import { EXIT, spawnScriptStreaming } from "../../commands/shared.js";
3
+ import { runnerDir } from "../../paths.js";
4
+ import { FIXTURE_STREAM_TIMEOUT_MS } from "../../commands/fixtures.js";
5
+ const FIXTURE_BRIDGE_READY_TIMEOUT_MS = 18e4;
6
+ const MOBILE_SETUP_WALLET_TIMEOUT_MS = 36e4;
7
+ async function applyMobileFixture({
8
+ target,
9
+ sub,
10
+ fixturePath,
11
+ canonicalFixture,
12
+ fixtureFlags,
13
+ json,
14
+ retryHint
15
+ }) {
16
+ const adapter = "mobile";
17
+ const fixtureFlagsJson = JSON.stringify(fixtureFlags);
18
+ const reset = sub === "reset";
19
+ 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
20
+ `);
21
+ let status;
22
+ let mobileSetupResult;
23
+ let resetError;
24
+ const setupWalletSh = path.join(runnerDir, "adapters/mobile/bridge-runtime/setup-wallet.sh");
25
+ const previousAppRoot = process.env.APP_ROOT;
26
+ process.env.APP_ROOT = target;
27
+ try {
28
+ if (reset) {
29
+ const platform = process.env.PLATFORM === "android" || process.env.ADB_SERIAL || process.env.ANDROID_SERIAL ? "android" : "ios";
30
+ const port = process.env.WATCHER_PORT ?? process.env.METRO_PORT ?? "8081";
31
+ const resetAppData = path.join(runnerDir, "adapters/mobile/reset-app-data.sh");
32
+ const resetArgs = ["--platform", platform, "--target", target];
33
+ if (platform === "ios") {
34
+ resetArgs.push("--simulator", process.env.SIM_UDID || process.env.IOS_SIMULATOR || "booted");
35
+ } else if (process.env.ADB_SERIAL || process.env.ANDROID_SERIAL) {
36
+ resetArgs.push("--adb-serial", process.env.ADB_SERIAL || process.env.ANDROID_SERIAL || "");
37
+ }
38
+ const resetResult = await spawnScriptStreaming(resetAppData, resetArgs, target, {
39
+ timeoutMs: FIXTURE_STREAM_TIMEOUT_MS
40
+ });
41
+ if (resetResult.status !== 0) {
42
+ resetError = new Error("Mobile app data reset failed.");
43
+ } else {
44
+ const prewarmBundle = path.join(runnerDir, "adapters/mobile/prewarm-bundle.sh");
45
+ const prewarmResult = await spawnScriptStreaming(
46
+ prewarmBundle,
47
+ ["--platform", platform, "--target", target, "--port", port],
48
+ target,
49
+ {
50
+ timeoutMs: FIXTURE_STREAM_TIMEOUT_MS
51
+ }
52
+ );
53
+ if (prewarmResult.status !== 0) {
54
+ resetError = new Error("Mobile bundle prewarm failed after app data reset.");
55
+ }
56
+ }
57
+ if (!resetError) {
58
+ const openDevice = path.join(runnerDir, "adapters/mobile/open-device.sh");
59
+ const openArgs = ["--platform", platform, "--target", target, "--port", port, "--preflight-mode", "fast", "--restart"];
60
+ if (platform === "ios") {
61
+ openArgs.push("--simulator", process.env.SIM_UDID || process.env.IOS_SIMULATOR || "booted");
62
+ } else if (process.env.ADB_SERIAL || process.env.ANDROID_SERIAL) {
63
+ openArgs.push("--adb-serial", process.env.ADB_SERIAL || process.env.ANDROID_SERIAL || "");
64
+ }
65
+ const openResult = await spawnScriptStreaming(openDevice, openArgs, target, {
66
+ timeoutMs: FIXTURE_STREAM_TIMEOUT_MS
67
+ });
68
+ if (openResult.status !== 0) {
69
+ resetError = new Error("Mobile app relaunch after data reset failed.");
70
+ } else {
71
+ const waitForBridge = path.join(runnerDir, "adapters/mobile/wait-for-bridge.sh");
72
+ const waitResult = await spawnScriptStreaming(
73
+ waitForBridge,
74
+ ["--target", target, "--port", port, "--platform", platform, "--timeout-ms", String(FIXTURE_BRIDGE_READY_TIMEOUT_MS)],
75
+ target,
76
+ { timeoutMs: FIXTURE_BRIDGE_READY_TIMEOUT_MS + 1e4 }
77
+ );
78
+ if (waitResult.status !== 0) {
79
+ resetError = new Error("Mobile bridge did not return after app data reset.");
80
+ }
81
+ }
82
+ }
83
+ }
84
+ const configuredTimeout = Number(process.env.MM_HARNESS_FIXTURES_SET_TIMEOUT_MS ?? MOBILE_SETUP_WALLET_TIMEOUT_MS);
85
+ const timeoutMs = Number.isFinite(configuredTimeout) && configuredTimeout > 0 ? configuredTimeout : MOBILE_SETUP_WALLET_TIMEOUT_MS;
86
+ if (!resetError) {
87
+ mobileSetupResult = await spawnScriptStreaming(
88
+ setupWalletSh,
89
+ ["--fixture", fixturePath, "--remote-flags-json", fixtureFlagsJson],
90
+ target,
91
+ {
92
+ env: {
93
+ APP_ROOT: target,
94
+ WATCHER_PORT: process.env.WATCHER_PORT ?? "8081",
95
+ METRO_PORT: process.env.METRO_PORT ?? process.env.WATCHER_PORT ?? "8081"
96
+ },
97
+ timeoutMs
98
+ }
99
+ );
100
+ }
101
+ status = !resetError && mobileSetupResult?.status === 0 ? "pass" : "fail";
102
+ } finally {
103
+ if (previousAppRoot === void 0) delete process.env.APP_ROOT;
104
+ else process.env.APP_ROOT = previousAppRoot;
105
+ }
106
+ if (!json && status === "fail") {
107
+ console.error(` Next: ${retryHint}`);
108
+ }
109
+ const teaching = status === "pass" ? `Wallet fixture applied. Want different accounts? Edit: ${canonicalFixture}` : "Wallet fixture was not applied.";
110
+ if (json) {
111
+ console.log(
112
+ JSON.stringify(
113
+ {
114
+ schemaVersion: 1,
115
+ command: "fixtures",
116
+ action: sub,
117
+ adapter,
118
+ fixture: fixturePath,
119
+ canonicalFixture,
120
+ status,
121
+ exitCode: status === "pass" ? EXIT.ok : EXIT.runtime,
122
+ walletReset: reset && status === "pass",
123
+ message: teaching,
124
+ error: status === "fail" ? {
125
+ code: resetError ? "MOBILE_WALLET_RESET_FAILED" : mobileSetupResult?.timedOut ? "SETUP_WALLET_TIMEOUT" : "SETUP_WALLET_FAILED",
126
+ 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"}`,
127
+ userAction: retryHint
128
+ } : null
129
+ },
130
+ null,
131
+ 2
132
+ )
133
+ );
134
+ } else if (status === "pass") {
135
+ console.error(teaching);
136
+ }
137
+ return status === "pass" ? EXIT.ok : EXIT.runtime;
138
+ }
139
+ export {
140
+ applyMobileFixture
141
+ };