@deeeed/metamask-harness 0.74.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 (60) hide show
  1. package/CHANGELOG.md +17 -1
  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 +5 -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 +9 -2
  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 +11 -2
  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 +1 -1
  26. package/dist/adapters/terminal/actions.js +43 -0
  27. package/dist/adapters/terminal/console-capture.js +14 -1
  28. package/dist/adapters/terminal/run.js +30 -0
  29. package/dist/adapters/terminal/surface.js +6 -1
  30. package/dist/adapters.js +32 -824
  31. package/dist/cli-commands.js +1 -0
  32. package/dist/command-contract.js +3 -0
  33. package/dist/commands/call.js +2 -5
  34. package/dist/commands/launch/device.js +2 -6
  35. package/dist/commands/launch/extension.js +3 -303
  36. package/dist/commands/launch/index.js +1 -1
  37. package/dist/commands/launch/mobile.js +2 -190
  38. package/dist/commands/launch/quote.js +7 -0
  39. package/dist/commands/pin.js +226 -0
  40. package/dist/commands/run-engine.js +38 -138
  41. package/dist/commands/run-report.js +4 -48
  42. package/dist/commands/run.js +6 -48
  43. package/dist/doctor.js +3 -1
  44. package/dist/execution-provenance.js +3 -2
  45. package/dist/harness-pin.js +10 -0
  46. package/dist/heal-bounds.js +3 -2
  47. package/dist/live-adapter-contract.js +8 -34
  48. package/dist/manifest.js +3 -5
  49. package/dist/metamask-action-validation.js +5 -196
  50. package/dist/mm-harness-cli.js +27 -2
  51. package/dist/network-observation.js +10 -3
  52. package/dist/performance-observation.js +15 -22
  53. package/dist/pr-body/render.js +3 -1
  54. package/dist/recording-target.js +4 -39
  55. package/dist/run-diagnostics.js +21 -60
  56. package/dist/run-recording.js +5 -4
  57. package/dist/runner.js +20 -368
  58. package/docs/CONTRIBUTING.md +19 -7
  59. package/package.json +2 -1
  60. package/scripts/completions.sh +2 -1
@@ -6,45 +6,10 @@ const IOS_SIMULATOR_RECORDING_TARGET_PREFIX = "ios-simulator:";
6
6
  function createMetaMaskRecordingTargetProvider(adapter) {
7
7
  return {
8
8
  async resolveRecordingTarget(context) {
9
- if (adapter === "extension") {
10
- const pid = resolveExtensionBrowserPid(
11
- context.projectRoot,
12
- context.artifactsDir,
13
- context.env.CDP_PORT ?? context.env.RECIPE_CDP_PORT ?? process.env.CDP_PORT ?? process.env.RECIPE_CDP_PORT
14
- );
15
- if (!pid) {
16
- throw new Error(
17
- "Could not resolve the extension browser PID for --record-video. Ensure the slot browser is running and CDP_PORT is set."
18
- );
19
- }
20
- return { kind: "pid", pid };
21
- }
22
- if (adapter === "mobile") {
23
- const platform = (context.env.PLATFORM ?? process.env.PLATFORM ?? "ios").trim().toLowerCase();
24
- const androidSerial = resolveAndroidDeviceSerial(context.env);
25
- if (platform === "android" || androidSerial) {
26
- if (!androidSerial) {
27
- throw new Error(
28
- "Could not resolve an Android device for --record-video. Set ADB_SERIAL or pass --device to mm-harness run."
29
- );
30
- }
31
- return {
32
- kind: "android-device",
33
- serial: androidSerial
34
- };
35
- }
36
- const simulator = resolveMobileIosSimulatorName(context.env);
37
- if (!simulator) {
38
- throw new Error(
39
- "Could not resolve an iOS simulator name for --record-video. Set IOS_SIMULATOR or pass --simulator to the recipe hook."
40
- );
41
- }
42
- return {
43
- kind: "window-id",
44
- windowId: `${IOS_SIMULATOR_RECORDING_TARGET_PREFIX}${simulator}`
45
- };
46
- }
47
- throw new Error(`--record-video is not implemented for the ${adapter} adapter.`);
9
+ const { getAdapterSurface } = await import("./adapters/surface.js");
10
+ const recording = getAdapterSurface(adapter).recording;
11
+ if (!recording) throw new Error(`--record-video is not implemented for the ${adapter} adapter.`);
12
+ return recording.target(context);
48
13
  }
49
14
  };
50
15
  }
@@ -1,20 +1,10 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { createHash } from "node:crypto";
4
- import { spawnSync } from "node:child_process";
5
4
  import { getAdapterSurface } from "./adapters/surface.js";
6
- import { recipeRuntimePath, runnerDir } from "./paths.js";
7
- import { ensureExtensionConsoleCapture, extensionCaptureFiles, extensionCdpPort } from "./adapters/extension/console-capture.js";
8
- import {
9
- ensureTerminalConsoleCapture,
10
- terminalCaptureFiles,
11
- terminalCdpPort,
12
- terminalRuntimeFile
13
- } from "./adapters/terminal/console-capture.js";
14
5
  import {
15
6
  collectorOwnerMarker,
16
- collectorStatus,
17
- verifyCapture
7
+ collectorStatus
18
8
  } from "../library/actions/shared/console-collector.mjs";
19
9
  import {
20
10
  allowlistMatch,
@@ -39,7 +29,7 @@ function readRunDiagnosticsDocument(diagnosticsPath) {
39
29
  }
40
30
  function formatRunDiagnosticsForHuman(diagnostics, adapter) {
41
31
  if (!diagnostics) {
42
- return [adapter === "core" ? "N/A \u2014 Core is headless and has no application log source." : "UNAVAILABLE \u2014 application diagnostics could not be collected for this run."];
32
+ return [getAdapterSurface(adapter).headless ? `N/A \u2014 ${adapter.charAt(0).toUpperCase()}${adapter.slice(1)} is headless and has no application log source.` : "UNAVAILABLE \u2014 application diagnostics could not be collected for this run."];
43
33
  }
44
34
  const status = diagnostics.status === "clean" ? "CLEAN" : diagnostics.status === "review" ? "REVIEW" : "UNAVAILABLE";
45
35
  const lines = [`${status} \u2014 ${diagnostics.note}`];
@@ -52,25 +42,27 @@ function formatRunDiagnosticsForHuman(diagnostics, adapter) {
52
42
  return lines;
53
43
  }
54
44
  async function beginRunDiagnostics(adapter, projectRoot) {
55
- const source = getAdapterSurface(adapter).appLogSource(projectRoot);
45
+ const surface = getAdapterSurface(adapter);
46
+ const source = surface.appLogSource(projectRoot);
56
47
  if (!source) return null;
57
- const mobileIssueBuffer = adapter === "mobile" && process.env.METAMASK_RECIPE_MOBILE_OPAQUE_RUNTIME !== "1" && armMobileIssueBuffer(projectRoot) ? { projectRoot } : void 0;
48
+ const diagnostics = surface.diagnostics;
49
+ const issueBuffer = diagnostics?.issueBuffer?.arm(projectRoot) ? { adapter, projectRoot } : void 0;
58
50
  let capture;
59
- if (adapter === "extension" || adapter === "terminal") {
60
- const start = adapter === "extension" ? ensureExtensionConsoleCapture : ensureTerminalConsoleCapture;
61
- const startError = await start(projectRoot).then(() => void 0, (error) => error instanceof Error ? error.message : String(error));
62
- const { extensionLog, pageLog, pidFile } = adapter === "extension" ? extensionCaptureFiles(projectRoot) : terminalCaptureFiles(projectRoot);
63
- const cdpPort = adapter === "extension" ? extensionCdpPort() : terminalCdpPort();
51
+ if (diagnostics?.console) {
52
+ const collector = diagnostics.console;
53
+ const startError = await collector.start(projectRoot).then(() => void 0, (error) => error instanceof Error ? error.message : String(error));
54
+ const { extensionLog, pageLog, pidFile } = collector.files(projectRoot);
55
+ const cdpPort = collector.cdpPort();
64
56
  capture = { adapter, projectRoot, extensionLog, pageLog, pidFile, ...cdpPort ? { cdpPort } : {}, ...startError ? { startError } : {} };
65
57
  }
66
- const walletLogPath = adapter === "terminal" ? terminalRuntimeFile(projectRoot, "wallet-requests.jsonl") : null;
58
+ const walletLogPath = diagnostics?.walletLog?.(projectRoot) ?? null;
67
59
  const walletStat = walletLogPath ? safeStat(walletLogPath) : null;
68
60
  const stat = safeStat(source.path);
69
61
  return {
70
62
  source,
71
63
  offset: stat?.size ?? 0,
72
64
  ...stat ? { inode: stat.ino } : {},
73
- ...mobileIssueBuffer ? { mobileIssueBuffer } : {},
65
+ ...issueBuffer ? { issueBuffer } : {},
74
66
  ...capture ? { capture } : {},
75
67
  ...walletLogPath ? { walletLog: { path: walletLogPath, offset: walletStat?.size ?? 0, ...walletStat ? { inode: walletStat.ino } : {} } } : {}
76
68
  };
@@ -85,20 +77,13 @@ async function verifyConsoleCapture(capture) {
85
77
  cdpPort: capture.cdpPort
86
78
  });
87
79
  if (!status.ok) return { verified: false, detail: status.detail };
88
- let control;
89
- if (capture.adapter === "extension") {
90
- let extensionId = null;
91
- try {
92
- extensionId = fs.readFileSync(recipeRuntimePath(capture.projectRoot, "extension.id"), "utf8").trim();
93
- } catch {
94
- extensionId = null;
95
- }
96
- control = await verifyCapture({ cdpPort: capture.cdpPort, log: capture.extensionLog, target: "extension", extensionId });
97
- } else {
98
- const browser = readJsonRecord(terminalRuntimeFile(capture.projectRoot, "browser.json"));
99
- if (typeof browser?.appOrigin !== "string") return { verified: false, detail: "no slot browser state (browser.json) to verify the page console capture against" };
100
- control = await verifyCapture({ cdpPort: capture.cdpPort, log: capture.pageLog, target: "page", appOrigin: browser.appOrigin });
101
- }
80
+ const control = await getAdapterSurface(capture.adapter).diagnostics.console.verifyControl({
81
+ projectRoot: capture.projectRoot,
82
+ cdpPort: capture.cdpPort,
83
+ extensionLog: capture.extensionLog,
84
+ pageLog: capture.pageLog,
85
+ pidFile: capture.pidFile
86
+ });
102
87
  return { verified: control.ok, detail: control.ok ? `${status.detail}; ${control.detail}` : control.detail };
103
88
  }
104
89
  function isRunDiagnosticsDocument(value) {
@@ -109,7 +94,7 @@ function isRunDiagnosticsDocument(value) {
109
94
  async function finishRunDiagnostics(baseline, result) {
110
95
  if (!baseline) return result;
111
96
  try {
112
- const bufferedIssues = baseline.mobileIssueBuffer ? collectMobileIssueBuffer(baseline.mobileIssueBuffer.projectRoot) : void 0;
97
+ const bufferedIssues = baseline.issueBuffer ? getAdapterSurface(baseline.issueBuffer.adapter).diagnostics?.issueBuffer?.collect(baseline.issueBuffer.projectRoot) : void 0;
113
98
  const allowlist = loadConsoleAllowlist(recipeLibraryRoots(result.summaryPath));
114
99
  const capture = baseline.capture ? await verifyConsoleCapture(baseline.capture) : void 0;
115
100
  const { diagnostics, rawAppLog } = collectRunDiagnosticsCapture(baseline, bufferedIssues, allowlist, capture);
@@ -271,30 +256,6 @@ function applyConsoleAllowlist(findings, allowlist) {
271
256
  finding.fingerprint = createHash("sha256").update(`${finding.fingerprint}|info|${match.file}#${match.index}`).digest("hex").slice(0, 12);
272
257
  }
273
258
  }
274
- function armMobileIssueBuffer(projectRoot) {
275
- const armed = runMobileIssueCommand(projectRoot, "issues-arm");
276
- if (!armed) return false;
277
- return runMobileIssueCommand(projectRoot, "issues-collect") !== null;
278
- }
279
- function collectMobileIssueBuffer(projectRoot) {
280
- const result = runMobileIssueCommand(projectRoot, "issues-collect");
281
- return isRecord(result) && Array.isArray(result.entries) ? result.entries : null;
282
- }
283
- function runMobileIssueCommand(projectRoot, command) {
284
- const bridge = path.join(runnerDir, "adapters", "mobile", "bridge-runtime", "cdp-bridge.cjs");
285
- const result = spawnSync(process.execPath, [bridge, command], {
286
- cwd: projectRoot,
287
- env: { ...process.env, APP_ROOT: projectRoot, CDP_TIMEOUT: "5000" },
288
- encoding: "utf8",
289
- timeout: 1e4
290
- });
291
- if (result.status !== 0) return null;
292
- try {
293
- return JSON.parse(result.stdout.trim());
294
- } catch {
295
- return null;
296
- }
297
- }
298
259
  function redactPreview(value) {
299
260
  return redactDiagnosticText(value).slice(0, MAX_PREVIEW_CHARS);
300
261
  }
@@ -2,10 +2,10 @@ import { spawn, spawnSync } from "node:child_process";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { optionalVideoTiming, readCaptureHelperTiming, writeRecordingTimeline } from "@farmslot/recipe-harness";
5
+ import { getAdapterSurface } from "./adapters/surface.js";
5
6
  import {
6
7
  captureHelperSupportsRecordSessionSnapshots,
7
- captureHelperSupportsCapability,
8
- resolveExtensionBrowserPid
8
+ captureHelperSupportsCapability
9
9
  } from "./recording-target.js";
10
10
  const activeRecordingsByPid = /* @__PURE__ */ new Map();
11
11
  const ACTIVE_EXTENSION_RECORDING_PID = "METAMASK_RECIPE_EXTENSION_ACTIVE_RECORDING_PID";
@@ -17,7 +17,8 @@ async function startRecipeRecording(adapter, projectRoot, artifactsDir, options)
17
17
  );
18
18
  return void 0;
19
19
  }
20
- if (adapter !== "extension") return void 0;
20
+ const framed = getAdapterSurface(adapter).recording?.framed;
21
+ if (!framed) return void 0;
21
22
  const cdpPort = options.cdpPort ?? process.env.CDP_PORT ?? process.env.RECIPE_CDP_PORT;
22
23
  if (!captureHelperSupportsRecordSessionSnapshots(projectRoot)) {
23
24
  console.error(
@@ -25,7 +26,7 @@ async function startRecipeRecording(adapter, projectRoot, artifactsDir, options)
25
26
  );
26
27
  return void 0;
27
28
  }
28
- const pid = resolveExtensionBrowserPid(projectRoot, artifactsDir, cdpPort);
29
+ const pid = framed.browserPid(projectRoot, artifactsDir, cdpPort);
29
30
  if (!pid) {
30
31
  console.error(
31
32
  `WARN: framed extension recording could not resolve browser PID from CDP port ${cdpPort ?? "<unset>"}; falling back to harness --record-video.`
package/dist/runner.js CHANGED
@@ -1,27 +1,10 @@
1
- import { execFileSync, execSync } from "node:child_process";
1
+ import { execSync } from "node:child_process";
2
2
  import { OFFICIAL_RECIPE_ACTIONS } from "@farmslot/protocol";
3
- import { createAndroidMirrorVideoRecorder } from "@farmslot/recipe-harness";
4
3
  import { createMetaMaskAdapters, createMetaMaskUiTransport } from "./adapters.js";
5
- import {
6
- bridgeCommand,
7
- MOBILE_BRIDGE_ERROR_CODES
8
- } from "../library/actions/mobile/platform/bridge.mjs";
9
- import { resolveMobileToolPath } from "../library/actions/mobile/platform/tool-paths.mjs";
10
- import {
11
- mobileSourceFingerprint,
12
- recordMobileSourceBaseline
13
- } from "./adapters/mobile/source-freshness.js";
14
- import { readAndroidNativeRuntimeIdentity } from "./adapters/mobile/release-artifact.js";
15
- import { readMobileReleaseArtifactState } from "./adapters/mobile/release-artifact-state.js";
16
- import {
17
- createAndroidVideoRecorder,
18
- createIosSimulatorVideoRecorder
19
- } from "./adapters/mobile/video-recorder.js";
4
+ import { getAdapterSurface } from "./adapters/surface.js";
20
5
  import {
21
6
  bindMetaMaskMobileLifecycleDeadline,
22
7
  createMetaMaskMobileLifecycleCommandRunner,
23
- MOBILE_LIFECYCLE_DEADLINE_FIELD,
24
- MOBILE_LIFECYCLE_PREPARED,
25
8
  resolveMetaMaskMobileLifecycleTarget
26
9
  } from "./app-lifecycle.js";
27
10
  import { loadMetaMaskExtensionActionManifest, loadMetaMaskMobileActionManifest } from "./manifest.js";
@@ -29,11 +12,7 @@ import { actionSourceIntegrity, metaMaskActionExecutionCapabilities } from "./re
29
12
  import {
30
13
  prepareLiveAdapterScript
31
14
  } from "./live-adapter-contract.js";
32
- import {
33
- createMetaMaskRecordingTargetProvider,
34
- resolveAndroidDeviceSerial,
35
- resolveMobileIosSimulatorName
36
- } from "./recording-target.js";
15
+ import { createMetaMaskRecordingTargetProvider } from "./recording-target.js";
37
16
  import {
38
17
  importMetaMaskNativeUiProvider,
39
18
  importRecipeHarness,
@@ -92,16 +71,15 @@ async function createMetaMaskRunner(adapter, actionManifest, options = {}) {
92
71
  }, preparedLiveAdapters, { hudPolicy: options.autoHud === true ? "show" : "auto" })
93
72
  });
94
73
  const existing = new Set([...core, ...ui].map((entry) => entry.action));
95
- const lifecycle = mobileSourceAwareLifecycleAdapters(
96
- adapter,
97
- bindMetaMaskMobileLifecycleDeadline(
98
- createAppLifecycleAdapters({
99
- actions,
100
- commandRunner: createMetaMaskMobileLifecycleCommandRunner(),
101
- targetProvider: { resolveTarget: resolveMetaMaskMobileLifecycleTarget }
102
- })
103
- )
74
+ const surface = getAdapterSurface(adapter);
75
+ const lifecycleAdapters = bindMetaMaskMobileLifecycleDeadline(
76
+ createAppLifecycleAdapters({
77
+ actions,
78
+ commandRunner: createMetaMaskMobileLifecycleCommandRunner(),
79
+ targetProvider: { resolveTarget: resolveMetaMaskMobileLifecycleTarget }
80
+ })
104
81
  );
82
+ const lifecycle = surface.actions.lifecycle ? await surface.actions.lifecycle(lifecycleAdapters) : lifecycleAdapters;
105
83
  const custom = createMetaMaskAdapters(
106
84
  adapter,
107
85
  customActions,
@@ -117,31 +95,22 @@ async function createMetaMaskRunner(adapter, actionManifest, options = {}) {
117
95
  options.quietStdout ? new console.Console(process.stderr) : console,
118
96
  options.suppressLibraryResolutionLogs
119
97
  );
120
- const androidRecordingSerial = adapter === "mobile" ? resolveAndroidDeviceSerial() : void 0;
121
- const iosRecordingSimulator = adapter === "mobile" && !androidRecordingSerial ? resolveMobileIosSimulatorName() : void 0;
98
+ const videoRecorder = await surface.recording?.videoRecorder?.();
99
+ const secured = [...core, ...ui, ...lifecycle, ...custom].map((entry) => ({
100
+ ...entry,
101
+ ...adapterSecurity(entry.action, actionSources, options.trustTaskActions)
102
+ }));
103
+ const mutationBound = surface.actions.trustedMutation ? withTrustedMutationContext(secured, options.trustedMutation, externalMutationActions) : secured;
122
104
  return createRecipeRunner({
123
105
  actionManifest,
124
106
  adapters: withActionProgress(
125
- withMobileNativeRuntimeIdentity(
126
- adapter,
127
- withTrustedMutationContext(
128
- [...core, ...ui, ...lifecycle, ...custom].map((entry) => ({
129
- ...entry,
130
- ...adapterSecurity(entry.action, actionSources, options.trustTaskActions)
131
- })),
132
- options.trustedMutation,
133
- externalMutationActions,
134
- adapter
135
- )
136
- ),
107
+ surface.actions.wrap ? await surface.actions.wrap(mutationBound) : mutationBound,
137
108
  options.onActionEvent
138
109
  ),
139
110
  logger,
140
111
  recording: {
141
112
  targetProvider: createMetaMaskRecordingTargetProvider(adapter),
142
- ...androidRecordingSerial ? { videoRecorder: createAndroidMirrorVideoRecorder({ serial: androidRecordingSerial, fallback: createAndroidVideoRecorder(androidRecordingSerial) }) } : iosRecordingSimulator ? {
143
- videoRecorder: createIosSimulatorVideoRecorder(iosRecordingSimulator)
144
- } : {}
113
+ ...videoRecorder ? { videoRecorder } : {}
145
114
  },
146
115
  runner: runnerProvenance(),
147
116
  defaultSource: {
@@ -161,38 +130,7 @@ async function createMetaMaskRunner(adapter, actionManifest, options = {}) {
161
130
  }
162
131
  });
163
132
  }
164
- function withMobileNativeRuntimeIdentity(adapter, adapters, identity = {
165
- state: readMobileReleaseArtifactState,
166
- read: readAndroidNativeRuntimeIdentity
167
- }) {
168
- if (adapter !== "mobile") return adapters;
169
- return adapters.map((entry) => ({
170
- ...entry,
171
- async execute(node, context) {
172
- const state = identity.state(context.projectRoot);
173
- if (!state) {
174
- if (context.env.METAMASK_RECIPE_MOBILE_OPAQUE_RUNTIME === "1") {
175
- throw new Error("Mobile release-artifact actions require current trusted runtime state. Next: rerun runtime-launch with the exact artifact.");
176
- }
177
- return entry.execute(node, context);
178
- }
179
- identity.read(state);
180
- const result = await entry.execute(node, context);
181
- const nativeRuntimeIdentity = identity.read(state);
182
- const output = result.output && typeof result.output === "object" && !Array.isArray(result.output) ? result.output : { result: result.output };
183
- return {
184
- ...result,
185
- output: {
186
- ...output,
187
- nativeRuntimeIdentity,
188
- runtimeSessionId: nativeRuntimeIdentity.runtimeSessionId
189
- }
190
- };
191
- }
192
- }));
193
- }
194
- function withTrustedMutationContext(adapters, base, externalMutationActions, adapter = "extension") {
195
- if (adapter !== "extension") return adapters;
133
+ function withTrustedMutationContext(adapters, base, externalMutationActions) {
196
134
  let selectedIdentitySha256;
197
135
  const identityProofAction = "metamask.wallet.import";
198
136
  const identityInvalidatingActions = /* @__PURE__ */ new Set([
@@ -353,289 +291,6 @@ function isAutomaticHudProgress(action, node, nodeId) {
353
291
  const progress = payload.progress;
354
292
  return nodeId === "recipe-complete" && progress !== null && typeof progress === "object" && !Array.isArray(progress) && progress.complete === true;
355
293
  }
356
- function mobileSourceAwareLifecycleAdapters(adapter, lifecycle, readinessProbe = probeMobileLifecycleRuntime, sourceFreshness = {
357
- fingerprint: mobileSourceFingerprint,
358
- record: recordMobileSourceBaseline
359
- }, processIdentity = {
360
- readIosPid: readIosAppPid,
361
- readAndroidPid: readAndroidAppPid
362
- }, runtimeMarker = readMobileLifecycleRuntimeIdentity) {
363
- if (adapter !== "mobile") return lifecycle;
364
- return lifecycle.map((entry) => ({
365
- ...entry,
366
- async execute(node, context) {
367
- const command = node.command ?? node.event ?? node.state;
368
- const opaqueRuntime = context.env?.METAMASK_RECIPE_MOBILE_OPAQUE_RUNTIME === "1";
369
- const requiresProcessContinuity = command !== "restart" && node.require_process_continuity === true;
370
- if (opaqueRuntime && !requiresProcessContinuity) {
371
- return entry.execute(node, context);
372
- }
373
- const reloadsSource = !opaqueRuntime && (command === "launch" || command === "foreground" || command === "restart");
374
- const recordsSource = !opaqueRuntime && command === "restart";
375
- const androidLifecycle = isAndroidLifecycle(node, context.env ?? {});
376
- const checksIosContinuity = command === "foreground" && !androidLifecycle;
377
- const checksRequiredContinuity = requiresProcessContinuity;
378
- const recordsProcessIdentity = checksIosContinuity || checksRequiredContinuity;
379
- const readProcessId = androidLifecycle ? processIdentity.readAndroidPid ?? readAndroidAppPid : processIdentity.readIosPid;
380
- const processBefore = recordsProcessIdentity ? readProcessId(node, context) : void 0;
381
- const fingerprint = recordsSource ? sourceFreshness.fingerprint(context.projectRoot) : void 0;
382
- const previousRuntimeIdentity = recordsSource ? await runtimeMarker(node, context) : void 0;
383
- const androidRestart = command === "restart" && androidLifecycle;
384
- const initialResult = await entry.execute(node, context);
385
- const readinessDeadline = reloadsSource ? mobileLifecycleReadinessDeadline(command, node) : void 0;
386
- let result = initialResult;
387
- let runtimeReady = false;
388
- if (androidRestart) {
389
- try {
390
- const remainingMs = Math.max(
391
- 1,
392
- (readinessDeadline ?? Date.now()) - Date.now()
393
- );
394
- const handoffDeadline = Math.min(
395
- readinessDeadline ?? Number.POSITIVE_INFINITY,
396
- Date.now() + Math.min(1e4, Math.max(1, remainingMs / 2))
397
- );
398
- await readinessProbe(
399
- node,
400
- context,
401
- previousRuntimeIdentity,
402
- handoffDeadline
403
- );
404
- runtimeReady = true;
405
- } catch (error) {
406
- if (readinessDeadline !== void 0 && Date.now() >= readinessDeadline) {
407
- throw error;
408
- }
409
- const foregroundTimeoutMs = Math.max(
410
- 1,
411
- (readinessDeadline ?? Date.now() + 1) - Date.now()
412
- );
413
- const foregroundResult = await entry.execute(
414
- {
415
- ...node,
416
- command: "foreground",
417
- settle_ms: 0,
418
- timeout_ms: foregroundTimeoutMs,
419
- [MOBILE_LIFECYCLE_DEADLINE_FIELD]: readinessDeadline,
420
- [MOBILE_LIFECYCLE_PREPARED]: true
421
- },
422
- context
423
- );
424
- result = mergeAndroidRestartResults(initialResult, foregroundResult);
425
- }
426
- }
427
- if (reloadsSource && !runtimeReady) {
428
- if (readinessDeadline !== void 0 && Date.now() >= readinessDeadline) {
429
- throw new Error(
430
- `Mobile lifecycle ${String(command)} exhausted its runtime readiness deadline before recovery completed.`
431
- );
432
- }
433
- await readinessProbe(
434
- node,
435
- context,
436
- previousRuntimeIdentity,
437
- readinessDeadline
438
- );
439
- }
440
- if (recordsProcessIdentity) {
441
- const processAfter = readProcessId(node, context);
442
- const processPreserved = processBefore !== null && processBefore === processAfter;
443
- const actualTransition = processPreserved ? command === "background" ? "background_same_process" : "foreground_resume" : processAfter !== null ? "cold_relaunch" : "unknown";
444
- const processContinuity = processPreserved ? "preserved" : processAfter === null ? "unknown" : "changed";
445
- const resultRecord = asRecord(result);
446
- result = {
447
- ...resultRecord,
448
- output: {
449
- ...asRecord(resultRecord.output),
450
- actualTransition,
451
- processContinuity,
452
- processBefore,
453
- processAfter
454
- }
455
- };
456
- if (checksRequiredContinuity && !processPreserved) {
457
- const platform = androidLifecycle ? "Android" : "iOS";
458
- throw new Error(
459
- `${platform} ${String(command)} did not prove app-process continuity (actualTransition=${actualTransition}, before=${String(processBefore)}, after=${String(processAfter)}); this lifecycle sample cannot be classified as a same-process reconnect.`
460
- );
461
- }
462
- }
463
- if (fingerprint !== void 0 && !sourceFreshness.record(context.projectRoot, fingerprint)) {
464
- throw new Error(
465
- "Mobile source changed while the app was reloading; the loaded-source baseline was not recorded. Next: retry app.lifecycle restart."
466
- );
467
- }
468
- return result;
469
- }
470
- }));
471
- }
472
- function readAndroidAppPid(node, context) {
473
- const env = context.env ?? {};
474
- const device = node.adb_serial ?? node.android_device ?? node.device ?? env.ADB_SERIAL ?? env.ANDROID_SERIAL ?? process.env.ADB_SERIAL ?? process.env.ANDROID_SERIAL;
475
- const packageId = node.package_id ?? node.packageName ?? node.app_id ?? env.ANDROID_PACKAGE_ID ?? process.env.ANDROID_PACKAGE_ID ?? "io.metamask";
476
- if (typeof device !== "string" || typeof packageId !== "string") return null;
477
- try {
478
- const adb = resolveMobileToolPath("adb", { required: true });
479
- const output = execFileSync(
480
- adb,
481
- ["-s", device, "shell", "pidof", packageId],
482
- { encoding: "utf8", timeout: 5e3, stdio: ["ignore", "pipe", "ignore"] }
483
- );
484
- const pid = Number.parseInt(output.trim().split(/\s+/u)[0] ?? "", 10);
485
- return Number.isInteger(pid) && pid > 0 ? pid : null;
486
- } catch {
487
- return null;
488
- }
489
- }
490
- function readIosAppPid(node, context) {
491
- const env = context.env ?? {};
492
- const device = node.simulator ?? node.ios_simulator ?? env.SIM_UDID ?? env.IOS_SIMULATOR ?? process.env.SIM_UDID ?? process.env.IOS_SIMULATOR;
493
- const bundleId = node.bundle_id ?? node.bundleId ?? env.IOS_BUNDLE_ID ?? process.env.IOS_BUNDLE_ID ?? "io.metamask.MetaMask";
494
- if (typeof device !== "string" || typeof bundleId !== "string") return null;
495
- try {
496
- const output = execFileSync(
497
- "xcrun",
498
- ["simctl", "spawn", device, "launchctl", "list"],
499
- { encoding: "utf8", timeout: 5e3 }
500
- );
501
- const labelPrefix = `UIKitApplication:${bundleId}[`;
502
- const line = output.split("\n").find((candidate) => candidate.includes(labelPrefix));
503
- if (!line) return null;
504
- const pid = Number.parseInt(line.trim().split(/\s+/u)[0] ?? "", 10);
505
- return Number.isInteger(pid) && pid > 0 ? pid : null;
506
- } catch {
507
- return null;
508
- }
509
- }
510
- function isAndroidLifecycle(node, env) {
511
- const platform = node.platform ?? env.PLATFORM ?? process.env.PLATFORM;
512
- if (platform === "android") return true;
513
- if (platform === "ios") return false;
514
- return Boolean(
515
- node.adb_serial ?? node.android_device ?? env.ADB_SERIAL ?? env.ANDROID_SERIAL ?? process.env.ADB_SERIAL ?? process.env.ANDROID_SERIAL
516
- );
517
- }
518
- function mergeAndroidRestartResults(initialResult, foregroundResult) {
519
- const initial = asRecord(initialResult);
520
- const foreground = asRecord(foregroundResult);
521
- const initialOutput = asRecord(initial.output);
522
- const foregroundOutput = asRecord(foreground.output);
523
- return {
524
- ...initial,
525
- output: {
526
- ...initialOutput,
527
- command: "restart",
528
- calls: [
529
- ...Array.isArray(initialOutput.calls) ? initialOutput.calls : [],
530
- ...Array.isArray(foregroundOutput.calls) ? foregroundOutput.calls : []
531
- ],
532
- settleMs: foregroundOutput.settleMs ?? initialOutput.settleMs ?? 0,
533
- foregroundEnsured: true
534
- }
535
- };
536
- }
537
- async function probeMobileLifecycleRuntime(node, context, previousRuntimeIdentity, deadlineMs) {
538
- const command = node.command ?? node.event ?? node.state;
539
- const timeoutMs = resolveMobileLifecycleReadinessTimeout(
540
- command,
541
- node.runtime_ready_timeout_ms
542
- );
543
- const startedAt = Date.now();
544
- const deadline = deadlineMs ?? startedAt + timeoutMs;
545
- let lastError;
546
- while (Date.now() < deadline) {
547
- try {
548
- const status = await bridgeCommand(
549
- {
550
- action: "app.lifecycle",
551
- node: {
552
- ...node,
553
- cdp_timeout_ms: Math.min(8e3, Math.max(1, deadline - Date.now()))
554
- },
555
- context
556
- },
557
- ["status-selected"]
558
- );
559
- if (status?.agenticPresent === true && status.route && (!previousRuntimeIdentity || status.runtimeIdentity !== previousRuntimeIdentity)) {
560
- return status;
561
- }
562
- lastError = new Error(
563
- previousRuntimeIdentity && status?.runtimeIdentity === previousRuntimeIdentity ? "retiring Mobile runtime is still selected" : "selected Mobile runtime is not ready"
564
- );
565
- } catch (error) {
566
- lastError = error;
567
- }
568
- const remainingMs = deadline - Date.now();
569
- if (remainingMs > 0) {
570
- await new Promise(
571
- (resolve) => setTimeout(resolve, Math.min(1e3, remainingMs))
572
- );
573
- }
574
- }
575
- throw new Error(
576
- `Mobile lifecycle ${String(command)} did not expose a usable pinned __AGENTIC__ runtime within ${Math.max(0, deadline - startedAt)}ms: ${lastError instanceof Error ? lastError.message : String(lastError)}`
577
- );
578
- }
579
- async function readMobileLifecycleRuntimeIdentity(node, context) {
580
- const deadline = Date.now() + 5e3;
581
- let lastError;
582
- do {
583
- try {
584
- const status = await bridgeCommand(
585
- {
586
- action: "app.lifecycle",
587
- node: {
588
- ...node,
589
- cdp_timeout_ms: Math.min(1e3, Math.max(1, deadline - Date.now()))
590
- },
591
- context
592
- },
593
- ["target-identity-selected"]
594
- );
595
- if (typeof status?.runtimeIdentity === "string") {
596
- return status.runtimeIdentity;
597
- }
598
- lastError = new Error("selected Mobile runtime has no stable identity");
599
- } catch (error) {
600
- if (error instanceof Error && "code" in error && error.code === MOBILE_BRIDGE_ERROR_CODES.NO_TARGET) {
601
- return void 0;
602
- }
603
- lastError = error;
604
- }
605
- const remainingMs = deadline - Date.now();
606
- if (remainingMs <= 0) {
607
- break;
608
- }
609
- await new Promise(
610
- (resolve) => setTimeout(resolve, Math.min(250, remainingMs))
611
- );
612
- } while (Date.now() < deadline);
613
- throw new Error(
614
- `Mobile lifecycle restart could not identify the retiring runtime: ${lastError instanceof Error ? lastError.message : String(lastError)}`
615
- );
616
- }
617
- function mobileLifecycleReadinessDeadline(command, node) {
618
- const timeoutMs = resolveMobileLifecycleReadinessTimeout(
619
- command,
620
- node.runtime_ready_timeout_ms
621
- );
622
- return Date.now() + timeoutMs;
623
- }
624
- function resolveMobileLifecycleReadinessTimeout(command, value) {
625
- if (value === void 0) {
626
- return command === "launch" || command === "restart" || command === "foreground" ? 6e4 : 3e4;
627
- }
628
- const timeout = Number(value);
629
- if (!Number.isInteger(timeout) || timeout < 1e3 || timeout > 12e4) {
630
- throw new Error(
631
- "app.lifecycle.runtime_ready_timeout_ms must be an integer from 1000 through 120000."
632
- );
633
- }
634
- return timeout;
635
- }
636
- function asRecord(value) {
637
- return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
638
- }
639
294
  function runnerProvenance() {
640
295
  return {
641
296
  source: runnerDir,
@@ -659,8 +314,5 @@ export {
659
314
  createMetaMaskMobileRunner,
660
315
  createMetaMaskRunner,
661
316
  isAutomaticHudProgress,
662
- mobileSourceAwareLifecycleAdapters,
663
- resolveMobileLifecycleReadinessTimeout,
664
- withMobileNativeRuntimeIdentity,
665
317
  withTrustedMutationContext
666
318
  };