@deeeed/metamask-harness 0.41.1 → 0.43.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 (52) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +12 -0
  3. package/adapters/manifest.json +8 -0
  4. package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +108 -10
  5. package/adapters/mobile/bridge-runtime/console-forwarder.cjs +117 -16
  6. package/adapters/mobile/bridge-runtime/lib/bridge-errors.cjs +2 -0
  7. package/adapters/mobile/bridge-runtime/lib/cdp-broker.cjs +921 -0
  8. package/adapters/mobile/bridge-runtime/lib/config.cjs +14 -4
  9. package/adapters/mobile/bridge-runtime/lib/devtools-proxy.cjs +177 -0
  10. package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +13 -3
  11. package/adapters/mobile/reload-app.mjs +67 -0
  12. package/adapters/mobile/start-console-forwarder.sh +22 -3
  13. package/adapters/mobile/start-metro.sh +6 -11
  14. package/adapters/mobile/stop-metro.sh +10 -1
  15. package/adapters/shared/open-debug.mjs +172 -2
  16. package/dist/adapters/extension/browser-cdp.js +174 -0
  17. package/dist/adapters/extension/network-observer.js +209 -0
  18. package/dist/adapters/extension/performance-observer.js +75 -0
  19. package/dist/adapters/mobile/frame-metrics.js +45 -0
  20. package/dist/adapters/mobile/metro-env.js +0 -5
  21. package/dist/adapters/mobile/performance-observer.js +43 -0
  22. package/dist/adapters/mobile/prepare.js +1 -3
  23. package/dist/adapters/mobile/runtime-decision.js +6 -9
  24. package/dist/adapters/performance/cdp-trace.js +342 -0
  25. package/dist/adapters/performance/js-task-metrics.js +35 -0
  26. package/dist/adapters.js +29 -1
  27. package/dist/artifact-files.js +92 -0
  28. package/dist/async.js +19 -0
  29. package/dist/cli-commands.js +6 -3
  30. package/dist/cli.js +4 -0
  31. package/dist/command-contract.js +3 -0
  32. package/dist/commands/call.js +66 -20
  33. package/dist/commands/reload.js +80 -0
  34. package/dist/commands/run-engine.js +18 -0
  35. package/dist/commands/run.js +68 -22
  36. package/dist/mm-harness-cli.js +17 -1
  37. package/dist/network-observation.js +283 -0
  38. package/dist/performance-observation.js +465 -0
  39. package/docs/NETWORK-CAPTURE.md +98 -0
  40. package/docs/PERFORMANCE-CAPTURE.md +33 -0
  41. package/docs/RECIPES.md +17 -0
  42. package/library/actions/mobile/app/network_assert.mjs +14 -0
  43. package/library/actions/mobile/app/network_capture.mjs +72 -0
  44. package/library/actions/mobile/platform/bridge.mjs +10 -2
  45. package/library/actions/shared/app/network-artifact.mjs +10 -0
  46. package/library/actions/shared/app/network-assert.mjs +154 -0
  47. package/library/manifests/extension.action-manifest.json +173 -0
  48. package/library/manifests/mobile.action-manifest.json +204 -0
  49. package/library/recipes/mobile/perps/performance.recipe.json +11 -11
  50. package/package.json +1 -1
  51. package/scripts/completions.sh +2 -1
  52. package/scripts/site-contrast.mjs +43 -27
@@ -43,6 +43,12 @@ import {
43
43
  import { readMobileReleaseArtifactState } from "../adapters/mobile/release-artifact-state.js";
44
44
  import { acquireCheckoutLock } from "../checkout-lock.js";
45
45
  import { formatRunDiagnosticsForHuman, readRunDiagnosticsDocument } from "../run-diagnostics.js";
46
+ import {
47
+ startRunNetworkObservation
48
+ } from "../network-observation.js";
49
+ import {
50
+ startRunPerformanceObservation
51
+ } from "../performance-observation.js";
46
52
  import { recipeTrustFailure } from "../recipe-security.js";
47
53
  import { isSensitiveKey, recordCommandEvidence, redactStructuredValue } from "../command-journal.js";
48
54
  import { closest } from "../command-contract.js";
@@ -209,12 +215,18 @@ async function handleCall(argv) {
209
215
  recordCommandEvidence(artifactsDir);
210
216
  const requestedRuntimeOptions = runtimeOptionsFromCli(options);
211
217
  const inheritedSource = process.env.FARMSLOT_RECIPE_SOURCE_TRUST || process.env.FARMSLOT_RECIPE_SOURCE_KIND || process.env.FARMSLOT_RECIPE_SOURCE_NAME || process.env.FARMSLOT_RECIPE_SOURCE_DIGEST;
218
+ let networkObservation;
219
+ let performanceObservation;
212
220
  const callRuntimeOptions = {
213
221
  ...requestedRuntimeOptions,
214
222
  ...librarySources ? { librarySources } : {},
215
223
  autoHud: false,
216
224
  suppressLibraryResolutionLogs: true,
217
225
  stdoutIsMachineContract: json,
226
+ onActionEvent: ({ nodeId, action, status }) => {
227
+ networkObservation?.onActionEvent({ nodeId, action, status });
228
+ performanceObservation?.onActionEvent({ nodeId, action, status });
229
+ },
218
230
  ...requestedRuntimeOptions.source ? { source: requestedRuntimeOptions.source } : inheritedSource ? {} : {
219
231
  source: {
220
232
  kind: "operator",
@@ -275,29 +287,63 @@ async function handleCall(argv) {
275
287
  });
276
288
  if (typeof prepared === "number") return prepared;
277
289
  const { state, heal } = prepared;
278
- const { result, violation } = await executeWithHealBounds(
279
- () => {
280
- const execution = preflightedExecution;
281
- preflightedExecution = void 0;
282
- return runRecipe(
283
- adapter,
284
- recipe,
285
- artifactsDir,
286
- target,
287
- actionManifestOverride,
288
- callRuntimeOptions,
289
- execution
290
- );
291
- },
290
+ networkObservation = await startRunNetworkObservation(
292
291
  adapter,
293
292
  target,
294
- heal,
295
- state,
296
- () => recoverRunInfra(adapter, target, json, {
297
- cdpPort: optionString(options, "cdpPort"),
298
- watcherPort: optionString(options, "watcherPort") ?? optionString(options, "metroPort")
299
- })
293
+ artifactsDir,
294
+ process.env,
295
+ {
296
+ cdpPort: callRuntimeOptions.cdpPort,
297
+ watcherPort: callRuntimeOptions.watcherPort
298
+ }
300
299
  );
300
+ performanceObservation = startRunPerformanceObservation(
301
+ adapter,
302
+ target,
303
+ artifactsDir,
304
+ process.env,
305
+ {
306
+ cdpPort: callRuntimeOptions.cdpPort,
307
+ watcherPort: callRuntimeOptions.watcherPort
308
+ }
309
+ );
310
+ let executionResult;
311
+ try {
312
+ executionResult = await executeWithHealBounds(
313
+ () => {
314
+ const execution = preflightedExecution;
315
+ preflightedExecution = void 0;
316
+ return runRecipe(
317
+ adapter,
318
+ recipe,
319
+ artifactsDir,
320
+ target,
321
+ actionManifestOverride,
322
+ callRuntimeOptions,
323
+ execution
324
+ );
325
+ },
326
+ adapter,
327
+ target,
328
+ heal,
329
+ state,
330
+ () => recoverRunInfra(adapter, target, json, {
331
+ cdpPort: optionString(options, "cdpPort"),
332
+ watcherPort: optionString(options, "watcherPort") ?? optionString(options, "metroPort")
333
+ })
334
+ );
335
+ } catch (error) {
336
+ await networkObservation?.finalize().catch(() => void 0);
337
+ networkObservation = void 0;
338
+ await performanceObservation?.finalize().catch(() => void 0);
339
+ performanceObservation = void 0;
340
+ throw error;
341
+ }
342
+ const { result, violation } = executionResult;
343
+ await networkObservation?.finalize(result.artifactManifestPath);
344
+ networkObservation = void 0;
345
+ await performanceObservation?.finalize(result.artifactManifestPath);
346
+ performanceObservation = void 0;
301
347
  if (violation !== null) {
302
348
  const conciseFailure = violation.originalError ? conciseFailureForHuman(violation.originalError) : "";
303
349
  const example = describedAction ? actionExampleCommand(
@@ -0,0 +1,80 @@
1
+ import path from "node:path";
2
+ import { runnerDir } from "../paths.js";
3
+ import { getAdapterSurface } from "../adapters/surface.js";
4
+ import {
5
+ ADAPTER_DETECT_NEXT,
6
+ EXIT,
7
+ flag,
8
+ parseFlags,
9
+ resolveAdapter,
10
+ spawnScript,
11
+ targetOf,
12
+ usageOut
13
+ } from "./shared.js";
14
+ const RELOAD_BOOLEANS = /* @__PURE__ */ new Set(["json"]);
15
+ async function handleReload(argv) {
16
+ const { options } = parseFlags(argv, RELOAD_BOOLEANS);
17
+ const json = flag(options, "json");
18
+ const target = targetOf(options);
19
+ const adapter = resolveAdapter(options, target);
20
+ if (!adapter) {
21
+ return usageOut(
22
+ json,
23
+ "reload",
24
+ `could not detect the MetaMask repo type for ${target}`,
25
+ ADAPTER_DETECT_NEXT
26
+ );
27
+ }
28
+ if (adapter === "core") {
29
+ return usageOut(
30
+ json,
31
+ "reload",
32
+ "core is headless; there is no app runtime to reload.",
33
+ "mm-harness run <core-recipe>"
34
+ );
35
+ }
36
+ getAdapterSurface(adapter).resolveSlotPorts(target);
37
+ if (adapter === "extension") {
38
+ if (!process.env.CDP_PORT) {
39
+ return usageOut(
40
+ json,
41
+ "reload",
42
+ "the Extension CDP port could not be resolved.",
43
+ "mm-harness launch"
44
+ );
45
+ }
46
+ const script2 = path.join(runnerDir, "adapters/extension/reattach.sh");
47
+ const args2 = ["--target", target, "--cdp-port", process.env.CDP_PORT];
48
+ if (process.env.WATCHER_PORT) {
49
+ args2.push("--watcher-port", process.env.WATCHER_PORT);
50
+ }
51
+ const result2 = spawnScript(script2, args2, target, json);
52
+ if (json) {
53
+ console.log(
54
+ JSON.stringify(
55
+ {
56
+ ok: result2.status === 0,
57
+ adapter,
58
+ method: "cdp-reattach",
59
+ command: "reload",
60
+ cdpPort: Number(process.env.CDP_PORT),
61
+ ...result2.status === 0 ? {} : { error: result2.output.slice(-4e3) || "Extension reload failed." }
62
+ },
63
+ null,
64
+ 2
65
+ )
66
+ );
67
+ }
68
+ return result2.status === 0 ? EXIT.ok : EXIT.runtime;
69
+ }
70
+ const script = path.join(runnerDir, "adapters/mobile/reload-app.mjs");
71
+ const args = [];
72
+ if (process.env.WATCHER_PORT) args.push("--port", process.env.WATCHER_PORT);
73
+ if (json) args.push("--json");
74
+ const result = spawnScript(process.execPath, [script, ...args], target, json);
75
+ if (json) process.stdout.write(result.output);
76
+ return result.status === 0 ? EXIT.ok : EXIT.runtime;
77
+ }
78
+ export {
79
+ handleReload
80
+ };
@@ -241,7 +241,24 @@ function activateRecipeRuntimeEnvironment(adapter, projectRoot, runtimeOptions)
241
241
  const previousAndroidPackageId = process.env.ANDROID_PACKAGE_ID;
242
242
  const previousAdbSerial = process.env.ADB_SERIAL;
243
243
  const previousAndroidSerial = process.env.ANDROID_SERIAL;
244
+ const explicitMobileDeviceEnv = adapter === "mobile" && process.env.MM_HARNESS_EXPLICIT_PLATFORM ? Object.fromEntries(
245
+ [
246
+ "PLATFORM",
247
+ "MM_HARNESS_EXPLICIT_PLATFORM",
248
+ "IOS_SIMULATOR",
249
+ "SIM_UDID",
250
+ "ADB_SERIAL",
251
+ "ANDROID_SERIAL",
252
+ "ANDROID_DEVICE",
253
+ "ANDROID_TARGET_DEVICE_NAME"
254
+ ].map((key) => [key, process.env[key]])
255
+ ) : void 0;
244
256
  getAdapterSurface(adapter).resolveSlotPorts(projectRoot);
257
+ if (explicitMobileDeviceEnv) {
258
+ for (const [key, value] of Object.entries(explicitMobileDeviceEnv)) {
259
+ restoreEnv(key, value);
260
+ }
261
+ }
245
262
  if (runtimeOptions.cdpPort) {
246
263
  process.env.CDP_PORT = runtimeOptions.cdpPort;
247
264
  process.env.RECIPE_CDP_PORT = runtimeOptions.cdpPort;
@@ -1362,6 +1379,7 @@ function countRecipeNodes(recipe) {
1362
1379
  return nodes ? Object.keys(nodes).length : void 0;
1363
1380
  }
1364
1381
  export {
1382
+ activateRecipeRuntimeEnvironment,
1365
1383
  countRecipeNodes,
1366
1384
  describeRunnableRecipe,
1367
1385
  emitHealViolation,
@@ -41,6 +41,12 @@ import {
41
41
  missingActionCapabilities,
42
42
  resolveActionCapabilityMatrix
43
43
  } from "./manifest.js";
44
+ import {
45
+ startRunNetworkObservation
46
+ } from "../network-observation.js";
47
+ import {
48
+ startRunPerformanceObservation
49
+ } from "../performance-observation.js";
44
50
  async function validationCapabilityRefusals(adapter, findings, librarySources) {
45
51
  const actionNames = findings.flatMap((finding) => {
46
52
  if (finding.code !== "recipe.action_not_declared_by_manifest") return [];
@@ -227,12 +233,18 @@ async function handleRunInner({ positional, options }, stream) {
227
233
  }
228
234
  recordCommandEvidence(artifactsDir);
229
235
  const librarySources = validated.librarySources;
236
+ let networkObservation;
237
+ let performanceObservation;
230
238
  const runtimeOptions = {
231
239
  ...runtimeOptionsFromCli(options),
232
240
  params,
233
241
  ...librarySources ? { librarySources } : {},
234
242
  stdoutIsMachineContract: machine,
235
- onActionEvent: ({ nodeId, action, status }) => stream.node(nodeId, action, status)
243
+ onActionEvent: ({ nodeId, action, status }) => {
244
+ stream.node(nodeId, action, status);
245
+ networkObservation?.onActionEvent({ nodeId, action, status });
246
+ performanceObservation?.onActionEvent({ nodeId, action, status });
247
+ }
236
248
  };
237
249
  stream.phase("authorize");
238
250
  let preflightedExecution = await preflightRecipe(
@@ -263,30 +275,64 @@ async function handleRunInner({ positional, options }, stream) {
263
275
  return prepared;
264
276
  }
265
277
  const { state, heal } = prepared;
266
- stream.phase("execute");
267
- const { result, violation } = await executeWithHealBounds(
268
- // validated.recipeFile, not the raw arg: the arg may be a library recipe NAME
269
- // that only the resolver knows how to turn into a file.
270
- () => {
271
- const execution = preflightedExecution;
272
- preflightedExecution = void 0;
273
- return runRecipe(
274
- adapter,
275
- validated.recipeFile,
276
- artifactsDir,
277
- target,
278
- optionString(options, "actionManifest"),
279
- runtimeOptions,
280
- execution
281
- );
282
- },
278
+ networkObservation = await startRunNetworkObservation(
279
+ adapter,
280
+ target,
281
+ artifactsDir,
282
+ process.env,
283
+ {
284
+ cdpPort: runtimeOptions.cdpPort,
285
+ watcherPort: runtimeOptions.watcherPort
286
+ }
287
+ );
288
+ performanceObservation = startRunPerformanceObservation(
283
289
  adapter,
284
290
  target,
285
- heal,
286
- state,
287
- () => recoverRunInfra(adapter, target, machine, runtimeOptions),
288
- (code) => stream.phase("recover", { code })
291
+ artifactsDir,
292
+ process.env,
293
+ {
294
+ cdpPort: runtimeOptions.cdpPort,
295
+ watcherPort: runtimeOptions.watcherPort
296
+ }
289
297
  );
298
+ stream.phase("execute");
299
+ let executionResult;
300
+ try {
301
+ executionResult = await executeWithHealBounds(
302
+ // validated.recipeFile, not the raw arg: the arg may be a library recipe NAME
303
+ // that only the resolver knows how to turn into a file.
304
+ () => {
305
+ const execution = preflightedExecution;
306
+ preflightedExecution = void 0;
307
+ return runRecipe(
308
+ adapter,
309
+ validated.recipeFile,
310
+ artifactsDir,
311
+ target,
312
+ optionString(options, "actionManifest"),
313
+ runtimeOptions,
314
+ execution
315
+ );
316
+ },
317
+ adapter,
318
+ target,
319
+ heal,
320
+ state,
321
+ () => recoverRunInfra(adapter, target, machine, runtimeOptions),
322
+ (code) => stream.phase("recover", { code })
323
+ );
324
+ } catch (error) {
325
+ await networkObservation?.finalize().catch(() => void 0);
326
+ networkObservation = void 0;
327
+ await performanceObservation?.finalize().catch(() => void 0);
328
+ performanceObservation = void 0;
329
+ throw error;
330
+ }
331
+ const { result, violation } = executionResult;
332
+ await networkObservation?.finalize(result.artifactManifestPath);
333
+ networkObservation = void 0;
334
+ await performanceObservation?.finalize(result.artifactManifestPath);
335
+ performanceObservation = void 0;
290
336
  for (const mutation of state.mutations) stream.mutation(mutation);
291
337
  for (const recovery of state.recovered) stream.recovery(recovery);
292
338
  if (violation !== null) {
@@ -524,6 +524,22 @@ Example:
524
524
  mm-harness debug
525
525
  mm-harness debug --worker`
526
526
  },
527
+ {
528
+ name: "reload",
529
+ summary: "Reload the connected Mobile or Extension runtime without restarting its dev server.",
530
+ example: "mm-harness reload",
531
+ helpText: `mm-harness reload [flags]
532
+
533
+ Mobile broadcasts the same reload command as Metro's interactive r key.
534
+ Extension refreshes its loaded UI in place over CDP. Core is headless.
535
+
536
+ --adapter <mobile|extension> Target adapter (auto-detected inside a checkout)
537
+ --target <path> Checkout path (default: cwd)
538
+ --json Machine-readable output
539
+
540
+ Example:
541
+ mm-harness reload`
542
+ },
527
543
  {
528
544
  name: "update",
529
545
  summary: "Update the installed mm-harness to the published latest (--check reports only; --json = {current, latest, updateAvailable}).",
@@ -648,7 +664,7 @@ const HELP_GROUPS = [
648
664
  {
649
665
  title: "DAILY LOOP",
650
666
  blurb: "what a teammate runs many times a day (auto-ensures the overlay; --heal owns recovery)",
651
- commands: ["status", "launch", "stop", "logs", "debug", "fixtures"]
667
+ commands: ["status", "launch", "stop", "logs", "debug", "reload", "fixtures"]
652
668
  },
653
669
  {
654
670
  title: "DISCOVER",
@@ -0,0 +1,283 @@
1
+ import path from "node:path";
2
+ import brokerModule from "../adapters/mobile/bridge-runtime/lib/cdp-broker.cjs";
3
+ import configModule from "../adapters/mobile/bridge-runtime/lib/config.cjs";
4
+ import {
5
+ createExtensionNetworkObserver
6
+ } from "./adapters/extension/network-observer.js";
7
+ import {
8
+ indexArtifactManifest,
9
+ writeContainedArtifact
10
+ } from "./artifact-files.js";
11
+ const AUTO_CAPTURE_ID = "run-network";
12
+ const AUTO_ARTIFACT_PATH = "network/run-summary.json";
13
+ const { brokerSocketPath, createBrokerClient } = brokerModule;
14
+ const { resolvePort } = configModule;
15
+ const sessions = /* @__PURE__ */ new Map();
16
+ async function startRunNetworkObservation(adapter, target, artifactsDir, env, ports = {}) {
17
+ if (adapter === "core") return void 0;
18
+ const runtimeEnv = {
19
+ ...env,
20
+ ...ports.cdpPort ? { CDP_PORT: ports.cdpPort, RECIPE_CDP_PORT: ports.cdpPort } : {},
21
+ ...ports.watcherPort ? { WATCHER_PORT: ports.watcherPort } : {}
22
+ };
23
+ const key = path.resolve(artifactsDir);
24
+ const session = {
25
+ artifactsDir: key,
26
+ autoStarted: false,
27
+ autoStartedAt: Date.now(),
28
+ nodeEvents: []
29
+ };
30
+ sessions.set(key, session);
31
+ try {
32
+ session.backend = adapter === "mobile" ? await createMobileNetworkBackend(target, runtimeEnv) : await createExtensionNetworkObserver(extensionCdpPort(runtimeEnv), key);
33
+ } catch (error) {
34
+ session.setupError = boundedError(error);
35
+ }
36
+ if (runtimeEnv.MM_HARNESS_AUTO_NETWORK_CAPTURE !== "0") {
37
+ session.autoStartedAt = Date.now();
38
+ if (session.backend) {
39
+ try {
40
+ await session.backend.start({
41
+ id: AUTO_CAPTURE_ID,
42
+ bodyJsonFields: ["type"],
43
+ maxDurationMs: 60 * 60 * 1e3,
44
+ maxRequests: 1e4,
45
+ methods: [],
46
+ urlIncludes: []
47
+ });
48
+ session.autoStarted = true;
49
+ } catch (error) {
50
+ session.setupError = boundedError(error);
51
+ }
52
+ }
53
+ }
54
+ return {
55
+ onActionEvent(event) {
56
+ session.nodeEvents.push({ ...event, epochMs: Date.now() });
57
+ },
58
+ async finalize(artifactManifestPath) {
59
+ try {
60
+ if (runtimeEnv.MM_HARNESS_AUTO_NETWORK_CAPTURE !== "0") {
61
+ const summary = await automaticSummary(session);
62
+ await writeSummary(key, AUTO_ARTIFACT_PATH, summary);
63
+ if (artifactManifestPath) {
64
+ await indexArtifact(
65
+ artifactManifestPath,
66
+ AUTO_ARTIFACT_PATH,
67
+ "Automatic network observation"
68
+ );
69
+ }
70
+ }
71
+ } finally {
72
+ sessions.delete(key);
73
+ await session.backend?.close();
74
+ }
75
+ }
76
+ };
77
+ }
78
+ async function handleRunNetworkAction(platform, action, node, context) {
79
+ if (platform !== "extension") return null;
80
+ if (action === "app.network_assert") {
81
+ const assertion = await import("../library/actions/shared/app/network-assert.mjs");
82
+ return {
83
+ output: await assertion.assertNetwork({ action, node, context })
84
+ };
85
+ }
86
+ if (action !== "app.network_capture") return null;
87
+ const phase = String(node.phase ?? "").toLowerCase();
88
+ const id = String(node.id ?? "").trim();
89
+ if (!["start", "end"].includes(phase) || !id) {
90
+ throw new Error(
91
+ "app.network_capture requires phase=start|end and a non-empty id."
92
+ );
93
+ }
94
+ const session = sessions.get(path.resolve(context.artifactsDir));
95
+ if (!session?.backend) {
96
+ throw new Error(
97
+ `Extension network observation is unavailable: ${session?.setupError ?? "run observer was not started"}.`
98
+ );
99
+ }
100
+ if (phase === "start") {
101
+ const result = await session.backend.start(captureParams(id, node));
102
+ return {
103
+ output: {
104
+ action,
105
+ phase,
106
+ ...asRecord(result)
107
+ }
108
+ };
109
+ }
110
+ const artifactPath = String(
111
+ node.artifact_path ?? `network/${id}-summary.json`
112
+ );
113
+ const summary = await session.backend.end(id);
114
+ await writeSummary(context.artifactsDir, artifactPath, summary);
115
+ return {
116
+ output: {
117
+ action,
118
+ phase,
119
+ ...summary
120
+ },
121
+ artifacts: [
122
+ {
123
+ path: artifactPath,
124
+ type: "report",
125
+ nodeId: context.nodeId
126
+ }
127
+ ]
128
+ };
129
+ }
130
+ async function createMobileNetworkBackend(target, env) {
131
+ const { client } = await connectMobileBroker(target, env);
132
+ return {
133
+ start(params) {
134
+ return client.control("capture-start", params, 1e4);
135
+ },
136
+ async end(id) {
137
+ return asRecord(await client.control("capture-end", { id }, 1e4));
138
+ },
139
+ async close() {
140
+ client.close();
141
+ }
142
+ };
143
+ }
144
+ async function connectMobileBroker(target, env) {
145
+ const runtimeDir = env.RECIPE_RUNTIME_DIR ? path.resolve(target, env.RECIPE_RUNTIME_DIR) : path.join(target, "temp", "recipe", "runtime");
146
+ const socketPath = brokerSocketPath(
147
+ runtimeDir,
148
+ resolvePort(env, target)
149
+ );
150
+ const discoveryClient = await createBrokerClient(socketPath, "", 1e4);
151
+ let targets;
152
+ try {
153
+ targets = await discoveryClient.control(
154
+ "resolve-targets",
155
+ { nameIncludes: mobileTargetPin(env) },
156
+ 1e4
157
+ );
158
+ } finally {
159
+ discoveryClient.close();
160
+ }
161
+ const deviceId = selectMobileBrokerTarget(targets, env);
162
+ const client = await createBrokerClient(socketPath, deviceId, 1e4);
163
+ return { client, deviceId };
164
+ }
165
+ function selectMobileBrokerTarget(value, env) {
166
+ const targets = Array.isArray(value) ? value.map(asRecord).filter((target) => target.deviceId) : [];
167
+ const pin = mobileTargetPin(env);
168
+ const candidates = pin ? targets.filter(
169
+ (target) => String(target.name ?? "").toLowerCase().includes(pin.toLowerCase())
170
+ ) : targets;
171
+ if (candidates.length !== 1) {
172
+ throw new Error(
173
+ `Mobile network observation requires one broker target; found ${candidates.length}.`
174
+ );
175
+ }
176
+ return String(candidates[0].deviceId);
177
+ }
178
+ function mobileTargetPin(env) {
179
+ const platform = String(
180
+ env.MM_HARNESS_EXPLICIT_PLATFORM ?? env.RECIPE_HARNESS_PLATFORM ?? ""
181
+ ).toLowerCase();
182
+ return String(
183
+ platform === "android" ? env.ANDROID_TARGET_DEVICE_NAME ?? env.ANDROID_DEVICE ?? "" : platform === "ios" ? env.IOS_SIMULATOR ?? "" : env.IOS_SIMULATOR ?? env.ANDROID_TARGET_DEVICE_NAME ?? env.ANDROID_DEVICE ?? ""
184
+ ).trim();
185
+ }
186
+ async function automaticSummary(session) {
187
+ let summary;
188
+ if (session.autoStarted && session.backend) {
189
+ try {
190
+ summary = await session.backend.end(AUTO_CAPTURE_ID);
191
+ } catch (error) {
192
+ summary = unavailableSummary(session.autoStartedAt, boundedError(error));
193
+ }
194
+ } else {
195
+ summary = unavailableSummary(
196
+ session.autoStartedAt,
197
+ session.setupError ?? "Network observer did not start."
198
+ );
199
+ }
200
+ const startedAt = Number(summary.startedAtEpochMs ?? session.autoStartedAt);
201
+ return {
202
+ ...summary,
203
+ nodeEvents: session.nodeEvents.map((event) => ({
204
+ action: event.action,
205
+ elapsedMs: Math.max(0, event.epochMs - startedAt),
206
+ nodeId: event.nodeId,
207
+ status: event.status
208
+ }))
209
+ };
210
+ }
211
+ function unavailableSummary(startedAtEpochMs, reason) {
212
+ return {
213
+ schemaVersion: 1,
214
+ id: AUTO_CAPTURE_ID,
215
+ status: "unavailable",
216
+ startedAtEpochMs,
217
+ endedAtEpochMs: Date.now(),
218
+ maxDurationMs: 60 * 60 * 1e3,
219
+ reconnects: 0,
220
+ droppedRequests: 0,
221
+ unavailableReasons: [reason],
222
+ coverageGapReasons: [],
223
+ uninspectableBodyRequests: 0,
224
+ projectedBodyFields: ["type"],
225
+ totalRequests: 0,
226
+ requestsByMethod: {},
227
+ requestsByHost: {},
228
+ requestsByType: {},
229
+ requests: []
230
+ };
231
+ }
232
+ function captureParams(id, node) {
233
+ return {
234
+ id,
235
+ urlIncludes: node.url_includes ?? [],
236
+ methods: node.methods ?? [],
237
+ bodyJsonFields: node.body_json_fields ?? [],
238
+ maxRequests: node.max_requests,
239
+ maxDurationMs: node.max_duration_ms
240
+ };
241
+ }
242
+ async function writeSummary(artifactsDir, relativePath, summary) {
243
+ await writeContainedArtifact(
244
+ artifactsDir,
245
+ relativePath,
246
+ `${JSON.stringify(summary, null, 2)}
247
+ `,
248
+ "Network artifact"
249
+ );
250
+ }
251
+ async function indexArtifact(artifactManifestPath, relativePath, label) {
252
+ await indexArtifactManifest(
253
+ artifactManifestPath,
254
+ [
255
+ {
256
+ path: relativePath,
257
+ type: "report",
258
+ label,
259
+ category: "diagnostic"
260
+ }
261
+ ]
262
+ );
263
+ }
264
+ function extensionCdpPort(env) {
265
+ const port = Number(
266
+ env.CDP_PORT ?? env.RECIPE_CDP_PORT ?? process.env.CDP_PORT
267
+ );
268
+ if (!Number.isInteger(port) || port <= 0) {
269
+ throw new Error("Extension network observation requires CDP_PORT.");
270
+ }
271
+ return port;
272
+ }
273
+ function boundedError(error) {
274
+ return String(error instanceof Error ? error.message : error).slice(0, 256);
275
+ }
276
+ function asRecord(value) {
277
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
278
+ }
279
+ export {
280
+ connectMobileBroker,
281
+ handleRunNetworkAction,
282
+ startRunNetworkObservation
283
+ };