@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.
- package/CHANGELOG.md +35 -0
- package/README.md +12 -0
- package/adapters/manifest.json +8 -0
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +108 -10
- package/adapters/mobile/bridge-runtime/console-forwarder.cjs +117 -16
- package/adapters/mobile/bridge-runtime/lib/bridge-errors.cjs +2 -0
- package/adapters/mobile/bridge-runtime/lib/cdp-broker.cjs +921 -0
- package/adapters/mobile/bridge-runtime/lib/config.cjs +14 -4
- package/adapters/mobile/bridge-runtime/lib/devtools-proxy.cjs +177 -0
- package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +13 -3
- package/adapters/mobile/reload-app.mjs +67 -0
- package/adapters/mobile/start-console-forwarder.sh +22 -3
- package/adapters/mobile/start-metro.sh +6 -11
- package/adapters/mobile/stop-metro.sh +10 -1
- package/adapters/shared/open-debug.mjs +172 -2
- package/dist/adapters/extension/browser-cdp.js +174 -0
- package/dist/adapters/extension/network-observer.js +209 -0
- package/dist/adapters/extension/performance-observer.js +75 -0
- package/dist/adapters/mobile/frame-metrics.js +45 -0
- package/dist/adapters/mobile/metro-env.js +0 -5
- package/dist/adapters/mobile/performance-observer.js +43 -0
- package/dist/adapters/mobile/prepare.js +1 -3
- package/dist/adapters/mobile/runtime-decision.js +6 -9
- package/dist/adapters/performance/cdp-trace.js +342 -0
- package/dist/adapters/performance/js-task-metrics.js +35 -0
- package/dist/adapters.js +29 -1
- package/dist/artifact-files.js +92 -0
- package/dist/async.js +19 -0
- package/dist/cli-commands.js +6 -3
- package/dist/cli.js +4 -0
- package/dist/command-contract.js +3 -0
- package/dist/commands/call.js +66 -20
- package/dist/commands/reload.js +80 -0
- package/dist/commands/run-engine.js +18 -0
- package/dist/commands/run.js +68 -22
- package/dist/mm-harness-cli.js +17 -1
- package/dist/network-observation.js +283 -0
- package/dist/performance-observation.js +465 -0
- package/docs/NETWORK-CAPTURE.md +98 -0
- package/docs/PERFORMANCE-CAPTURE.md +33 -0
- package/docs/RECIPES.md +17 -0
- package/library/actions/mobile/app/network_assert.mjs +14 -0
- package/library/actions/mobile/app/network_capture.mjs +72 -0
- package/library/actions/mobile/platform/bridge.mjs +10 -2
- package/library/actions/shared/app/network-artifact.mjs +10 -0
- package/library/actions/shared/app/network-assert.mjs +154 -0
- package/library/manifests/extension.action-manifest.json +173 -0
- package/library/manifests/mobile.action-manifest.json +204 -0
- package/library/recipes/mobile/perps/performance.recipe.json +11 -11
- package/package.json +1 -1
- package/scripts/completions.sh +2 -1
- package/scripts/site-contrast.mjs +43 -27
package/dist/commands/call.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
cdpPort:
|
|
298
|
-
watcherPort:
|
|
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,
|
package/dist/commands/run.js
CHANGED
|
@@ -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 }) =>
|
|
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
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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) {
|
package/dist/mm-harness-cli.js
CHANGED
|
@@ -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
|
+
};
|