@deeeed/metamask-harness 0.33.2 → 0.34.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 +31 -0
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +18 -2
- package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +83 -23
- package/dist/adapters/mobile/prepare.js +14 -0
- package/dist/adapters/mobile/video-recorder.js +282 -0
- package/dist/adapters.js +225 -35
- package/dist/command-contract.js +1 -0
- package/dist/commands/device-target.js +17 -6
- package/dist/commands/launch/index.js +27 -5
- package/dist/commands/launch/mobile.js +9 -2
- package/dist/commands/run-engine.js +34 -2
- package/dist/devices.js +9 -2
- package/dist/heal-bounds.js +10 -0
- package/dist/mm-harness-cli.js +4 -2
- package/dist/recipe-security.js +1 -0
- package/dist/recording-target.js +23 -5
- package/dist/runner.js +107 -6
- package/dist/runtime-context.js +33 -1
- package/docs/RECIPES.md +41 -0
- package/library/actions/mobile/app/network-control.mjs +135 -0
- package/library/actions/mobile/app/network.mjs +4 -0
- package/library/actions/mobile/perps/capture_performance.mjs +4 -0
- package/library/actions/mobile/perps/performance-capture.mjs +383 -0
- package/library/actions/mobile/perps/perps.mjs +16 -0
- package/library/actions/mobile/platform/bridge.mjs +229 -13
- package/library/actions/mobile/wallet/ensure_unlocked.mjs +27 -6
- package/library/actions/mobile/wallet/select_account.mjs +21 -17
- package/library/manifests/mobile.action-manifest.json +130 -1
- package/library/recipes/mobile/perps/performance.homepage.android-background-reconnect.recipe.json +159 -0
- package/library/recipes/mobile/perps/performance.homepage.android-background-short.recipe.json +157 -0
- package/library/recipes/mobile/perps/performance.homepage.android-cold-disk-cache.recipe.json +147 -0
- package/library/recipes/mobile/perps/performance.homepage.android-cold-no-cache.recipe.json +137 -0
- package/library/recipes/mobile/perps/performance.homepage.android-network-recovery.recipe.json +149 -0
- package/library/recipes/mobile/perps/performance.homepage.ios-background-reconnect.recipe.json +110 -0
- package/library/recipes/mobile/perps/performance.homepage.ios-background-short.recipe.json +108 -0
- package/library/recipes/mobile/perps/performance.homepage.ios-cold-disk-cache.recipe.json +122 -0
- package/library/recipes/mobile/perps/performance.homepage.ios-cold-no-cache.recipe.json +95 -0
- package/package.json +1 -1
- package/scripts/site-contrast.mjs +34 -1
- package/site/architecture.html +12 -2
- package/site/assets/style.css +20 -1
- package/site/cheatsheet.html +7 -6
- package/site/index.html +119 -77
- package/site/perps.html +195 -0
- package/site/recipes.html +23 -1
- package/site/reviewers.html +2 -1
- package/site/tutorials/index.html +2 -1
- package/site/tutorials/v1.html +3 -2
- package/site/tutorials/v2.html +6 -5
- package/site/tutorials/v3.html +43 -2
- package/site/tutorials/v4.html +2 -1
- package/site/tutorials/v5.html +2 -1
- package/site/tutorials/v6.html +2 -1
- package/site/tutorials/v7.html +2 -1
package/dist/mm-harness-cli.js
CHANGED
|
@@ -198,7 +198,7 @@ Example:
|
|
|
198
198
|
|
|
199
199
|
--list List actions accepted by call for this adapter; no <action> needed
|
|
200
200
|
--arg k=v Action field value (repeatable; equivalent to key=value shorthand)
|
|
201
|
-
--device <udid|serial|name> Mobile only: target this device (
|
|
201
|
+
--device <udid|serial|name> Mobile only: target this device (sets IOS_SIMULATOR / ADB_SERIAL and updates agentic-runtime.json). Without it, >1 connected mobile device fails fast and lists them. Do not shut down other devices to force selection.
|
|
202
202
|
--adapter <mobile|extension|core> Target adapter (auto-detected inside a checkout)
|
|
203
203
|
--target <path> Checkout path (default: cwd)
|
|
204
204
|
--artifacts-dir <dir> Where to write evidence (default: temp dir)
|
|
@@ -228,7 +228,7 @@ Discovery:
|
|
|
228
228
|
--plan Validate + print execution plan, touching nothing. Exit 5 if invalid.
|
|
229
229
|
|
|
230
230
|
Common overrides:
|
|
231
|
-
--device <udid|serial|name> Mobile only: target this device (
|
|
231
|
+
--device <udid|serial|name> Mobile only: target this device (sets IOS_SIMULATOR / ADB_SERIAL and updates agentic-runtime.json). Without it, >1 connected mobile device fails fast and lists them. Do not shut down other devices to force selection.
|
|
232
232
|
--adapter <mobile|extension|core> Target adapter (auto-detected inside a checkout)
|
|
233
233
|
--platform <ios|android> Mobile platform preference; use --adapter for the product adapter
|
|
234
234
|
--target <path> Checkout path (default: cwd)
|
|
@@ -453,6 +453,7 @@ Example:
|
|
|
453
453
|
--build is the escape hatch that always clean-builds + relaunches a fresh runtime.
|
|
454
454
|
|
|
455
455
|
--build Full clean build + relaunch (escape hatch; skips extension reuse)
|
|
456
|
+
--clear-metro Mobile only: clear Metro's transform cache before launch
|
|
456
457
|
--verify Launch then poll CDP/bridge until ready (absorbs the old \`live\`)
|
|
457
458
|
--runway Post-launch runway check (mobile only; teaching error elsewhere)
|
|
458
459
|
--watch Persistent webpack watcher then relaunch (extension only)
|
|
@@ -469,6 +470,7 @@ Example:
|
|
|
469
470
|
|
|
470
471
|
Example:
|
|
471
472
|
mm-harness launch ios
|
|
473
|
+
mm-harness launch android --clear-metro --verify
|
|
472
474
|
mm-harness launch ios --build
|
|
473
475
|
mm-harness launch --verify --json`
|
|
474
476
|
},
|
package/dist/recipe-security.js
CHANGED
|
@@ -28,6 +28,7 @@ const APP_MUTATION_CUSTOM_ACTIONS = /* @__PURE__ */ new Set([
|
|
|
28
28
|
"metamask.perps.update_position_tpsl",
|
|
29
29
|
"metamask.perps.ensure_positions",
|
|
30
30
|
"metamask.perps.ensure_orders",
|
|
31
|
+
"metamask.perps.clear_performance_caches",
|
|
31
32
|
"metamask.perps.start_state",
|
|
32
33
|
"metamask.perps.teardown_state",
|
|
33
34
|
// Consent changes wallet state and is therefore an app mutation.
|
package/dist/recording-target.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
const ANDROID_RECORDING_TARGET_PREFIX = "android-device:";
|
|
5
|
+
const IOS_SIMULATOR_RECORDING_TARGET_PREFIX = "ios-simulator:";
|
|
4
6
|
function createMetaMaskRecordingTargetProvider(adapter) {
|
|
5
7
|
return {
|
|
6
8
|
async resolveRecordingTarget(context) {
|
|
@@ -19,8 +21,17 @@ function createMetaMaskRecordingTargetProvider(adapter) {
|
|
|
19
21
|
}
|
|
20
22
|
if (adapter === "mobile") {
|
|
21
23
|
const platform = (context.env.PLATFORM ?? process.env.PLATFORM ?? "ios").trim().toLowerCase();
|
|
22
|
-
|
|
23
|
-
|
|
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: "window-id",
|
|
33
|
+
windowId: `${ANDROID_RECORDING_TARGET_PREFIX}${androidSerial}`
|
|
34
|
+
};
|
|
24
35
|
}
|
|
25
36
|
const simulator = resolveMobileIosSimulatorName(context.env);
|
|
26
37
|
if (!simulator) {
|
|
@@ -28,14 +39,18 @@ function createMetaMaskRecordingTargetProvider(adapter) {
|
|
|
28
39
|
"Could not resolve an iOS simulator name for --record-video. Set IOS_SIMULATOR or pass --simulator to the recipe hook."
|
|
29
40
|
);
|
|
30
41
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
42
|
+
return {
|
|
43
|
+
kind: "window-id",
|
|
44
|
+
windowId: `${IOS_SIMULATOR_RECORDING_TARGET_PREFIX}${simulator}`
|
|
45
|
+
};
|
|
34
46
|
}
|
|
35
47
|
throw new Error(`--record-video is not implemented for the ${adapter} adapter.`);
|
|
36
48
|
}
|
|
37
49
|
};
|
|
38
50
|
}
|
|
51
|
+
function resolveAndroidDeviceSerial(env = process.env) {
|
|
52
|
+
return env.ADB_SERIAL?.trim() || process.env.ADB_SERIAL?.trim() || void 0;
|
|
53
|
+
}
|
|
39
54
|
function captureHelperSupportsRecordSessionSnapshots(projectRoot) {
|
|
40
55
|
const result = spawnSync(captureHelperPath(), ["version", "--json"], {
|
|
41
56
|
cwd: projectRoot,
|
|
@@ -125,8 +140,11 @@ function readTextFile(file) {
|
|
|
125
140
|
}
|
|
126
141
|
}
|
|
127
142
|
export {
|
|
143
|
+
ANDROID_RECORDING_TARGET_PREFIX,
|
|
144
|
+
IOS_SIMULATOR_RECORDING_TARGET_PREFIX,
|
|
128
145
|
captureHelperSupportsRecordSessionSnapshots,
|
|
129
146
|
createMetaMaskRecordingTargetProvider,
|
|
147
|
+
resolveAndroidDeviceSerial,
|
|
130
148
|
resolveExtensionBrowserPid,
|
|
131
149
|
resolveMobileIosSimulatorName,
|
|
132
150
|
resolveMobileIosSimulatorPid
|
package/dist/runner.js
CHANGED
|
@@ -1,17 +1,26 @@
|
|
|
1
1
|
import { execSync } from "node:child_process";
|
|
2
2
|
import { OFFICIAL_RECIPE_ACTIONS } from "@farmslot/protocol";
|
|
3
3
|
import { createMetaMaskAdapters, createMetaMaskUiTransport } from "./adapters.js";
|
|
4
|
+
import { bridgeCommand } from "../library/actions/mobile/platform/bridge.mjs";
|
|
4
5
|
import {
|
|
5
6
|
mobileSourceFingerprint,
|
|
6
7
|
recordMobileSourceBaseline
|
|
7
8
|
} from "./adapters/mobile/source-freshness.js";
|
|
9
|
+
import {
|
|
10
|
+
createAndroidVideoRecorder,
|
|
11
|
+
createIosSimulatorVideoRecorder
|
|
12
|
+
} from "./adapters/mobile/video-recorder.js";
|
|
8
13
|
import { resolveMetaMaskMobileLifecycleTarget } from "./app-lifecycle.js";
|
|
9
14
|
import { loadMetaMaskExtensionActionManifest, loadMetaMaskMobileActionManifest } from "./manifest.js";
|
|
10
15
|
import { metaMaskActionExecutionCapabilities } from "./recipe-security.js";
|
|
11
16
|
import {
|
|
12
17
|
prepareLiveAdapterScript
|
|
13
18
|
} from "./live-adapter-contract.js";
|
|
14
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
createMetaMaskRecordingTargetProvider,
|
|
21
|
+
resolveAndroidDeviceSerial,
|
|
22
|
+
resolveMobileIosSimulatorName
|
|
23
|
+
} from "./recording-target.js";
|
|
15
24
|
import {
|
|
16
25
|
importMetaMaskNativeUiProvider,
|
|
17
26
|
importRecipeHarness,
|
|
@@ -85,6 +94,8 @@ async function createMetaMaskRunner(adapter, actionManifest, options = {}) {
|
|
|
85
94
|
options.quietStdout ? new console.Console(process.stderr) : console,
|
|
86
95
|
options.suppressLibraryResolutionLogs
|
|
87
96
|
);
|
|
97
|
+
const androidRecordingSerial = adapter === "mobile" ? resolveAndroidDeviceSerial() : void 0;
|
|
98
|
+
const iosRecordingSimulator = adapter === "mobile" && !androidRecordingSerial ? resolveMobileIosSimulatorName() : void 0;
|
|
88
99
|
return createRecipeRunner({
|
|
89
100
|
actionManifest,
|
|
90
101
|
adapters: withActionProgress(
|
|
@@ -96,7 +107,10 @@ async function createMetaMaskRunner(adapter, actionManifest, options = {}) {
|
|
|
96
107
|
),
|
|
97
108
|
logger,
|
|
98
109
|
recording: {
|
|
99
|
-
targetProvider: createMetaMaskRecordingTargetProvider(adapter)
|
|
110
|
+
targetProvider: createMetaMaskRecordingTargetProvider(adapter),
|
|
111
|
+
...androidRecordingSerial ? { videoRecorder: createAndroidVideoRecorder(androidRecordingSerial) } : iosRecordingSimulator ? {
|
|
112
|
+
videoRecorder: createIosSimulatorVideoRecorder(iosRecordingSimulator)
|
|
113
|
+
} : {}
|
|
100
114
|
},
|
|
101
115
|
runner: runnerProvenance(),
|
|
102
116
|
defaultSource: {
|
|
@@ -222,16 +236,32 @@ function isAutomaticHudProgress(action, node, nodeId) {
|
|
|
222
236
|
const progress = payload.progress;
|
|
223
237
|
return nodeId === "recipe-complete" && progress !== null && typeof progress === "object" && !Array.isArray(progress) && progress.complete === true;
|
|
224
238
|
}
|
|
225
|
-
function mobileSourceAwareLifecycleAdapters(adapter, lifecycle
|
|
239
|
+
function mobileSourceAwareLifecycleAdapters(adapter, lifecycle, readinessProbe = probeMobileLifecycleRuntime, sourceFreshness = {
|
|
240
|
+
fingerprint: mobileSourceFingerprint,
|
|
241
|
+
record: recordMobileSourceBaseline
|
|
242
|
+
}) {
|
|
226
243
|
if (adapter !== "mobile") return lifecycle;
|
|
227
244
|
return lifecycle.map((entry) => ({
|
|
228
245
|
...entry,
|
|
229
246
|
async execute(node, context) {
|
|
230
247
|
const command = node.command ?? node.event ?? node.state;
|
|
231
248
|
const reloadsSource = command === "launch" || command === "foreground" || command === "restart";
|
|
232
|
-
const fingerprint = reloadsSource ?
|
|
233
|
-
const
|
|
234
|
-
|
|
249
|
+
const fingerprint = reloadsSource ? sourceFreshness.fingerprint(context.projectRoot) : void 0;
|
|
250
|
+
const androidRestart = command === "restart" && isAndroidLifecycle(node, context.env ?? {});
|
|
251
|
+
const initialNode = androidRestart ? { ...node, settle_ms: 0 } : node;
|
|
252
|
+
const initialResult = await entry.execute(initialNode, context);
|
|
253
|
+
let result = initialResult;
|
|
254
|
+
if (androidRestart) {
|
|
255
|
+
const foregroundResult = await entry.execute(
|
|
256
|
+
{ ...node, command: "foreground" },
|
|
257
|
+
context
|
|
258
|
+
);
|
|
259
|
+
result = mergeAndroidRestartResults(initialResult, foregroundResult);
|
|
260
|
+
}
|
|
261
|
+
if (reloadsSource) {
|
|
262
|
+
await readinessProbe(node, context);
|
|
263
|
+
}
|
|
264
|
+
if (fingerprint !== void 0 && !sourceFreshness.record(context.projectRoot, fingerprint)) {
|
|
235
265
|
throw new Error(
|
|
236
266
|
"Mobile source changed while the app was reloading; the loaded-source baseline was not recorded. Next: retry app.lifecycle restart."
|
|
237
267
|
);
|
|
@@ -240,6 +270,77 @@ function mobileSourceAwareLifecycleAdapters(adapter, lifecycle) {
|
|
|
240
270
|
}
|
|
241
271
|
}));
|
|
242
272
|
}
|
|
273
|
+
function isAndroidLifecycle(node, env) {
|
|
274
|
+
const platform = node.platform ?? env.PLATFORM ?? process.env.PLATFORM;
|
|
275
|
+
if (platform === "android") return true;
|
|
276
|
+
if (platform === "ios") return false;
|
|
277
|
+
return Boolean(
|
|
278
|
+
node.adb_serial ?? node.android_device ?? env.ADB_SERIAL ?? env.ANDROID_SERIAL ?? process.env.ADB_SERIAL ?? process.env.ANDROID_SERIAL
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
function mergeAndroidRestartResults(initialResult, foregroundResult) {
|
|
282
|
+
const initial = asRecord(initialResult);
|
|
283
|
+
const foreground = asRecord(foregroundResult);
|
|
284
|
+
const initialOutput = asRecord(initial.output);
|
|
285
|
+
const foregroundOutput = asRecord(foreground.output);
|
|
286
|
+
return {
|
|
287
|
+
...initial,
|
|
288
|
+
output: {
|
|
289
|
+
...initialOutput,
|
|
290
|
+
command: "restart",
|
|
291
|
+
calls: [
|
|
292
|
+
...Array.isArray(initialOutput.calls) ? initialOutput.calls : [],
|
|
293
|
+
...Array.isArray(foregroundOutput.calls) ? foregroundOutput.calls : []
|
|
294
|
+
],
|
|
295
|
+
settleMs: foregroundOutput.settleMs ?? initialOutput.settleMs ?? 0,
|
|
296
|
+
foregroundEnsured: true
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
async function probeMobileLifecycleRuntime(node, context) {
|
|
301
|
+
const command = node.command ?? node.event ?? node.state;
|
|
302
|
+
const defaultTimeoutMs = command === "restart" || command === "launch" ? 6e4 : 3e4;
|
|
303
|
+
const timeoutMs = lifecycleReadinessTimeout(
|
|
304
|
+
node.runtime_ready_timeout_ms,
|
|
305
|
+
defaultTimeoutMs
|
|
306
|
+
);
|
|
307
|
+
const deadline = Date.now() + timeoutMs;
|
|
308
|
+
let lastError;
|
|
309
|
+
while (Date.now() < deadline) {
|
|
310
|
+
try {
|
|
311
|
+
return await bridgeCommand(
|
|
312
|
+
{
|
|
313
|
+
action: "app.lifecycle",
|
|
314
|
+
node: {
|
|
315
|
+
...node,
|
|
316
|
+
cdp_timeout_ms: Math.min(8e3, Math.max(1e3, deadline - Date.now()))
|
|
317
|
+
},
|
|
318
|
+
context
|
|
319
|
+
},
|
|
320
|
+
["get-route"]
|
|
321
|
+
);
|
|
322
|
+
} catch (error) {
|
|
323
|
+
lastError = error;
|
|
324
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
throw new Error(
|
|
328
|
+
`Mobile lifecycle ${String(command)} did not expose a usable pinned __AGENTIC__ runtime within ${timeoutMs}ms: ${lastError instanceof Error ? lastError.message : String(lastError)}`
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
function lifecycleReadinessTimeout(value, fallback) {
|
|
332
|
+
if (value === void 0) return fallback;
|
|
333
|
+
const timeout = Number(value);
|
|
334
|
+
if (!Number.isInteger(timeout) || timeout < 1e3 || timeout > 12e4) {
|
|
335
|
+
throw new Error(
|
|
336
|
+
"app.lifecycle.runtime_ready_timeout_ms must be an integer from 1000 through 120000."
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
return timeout;
|
|
340
|
+
}
|
|
341
|
+
function asRecord(value) {
|
|
342
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
343
|
+
}
|
|
243
344
|
function runnerProvenance() {
|
|
244
345
|
return {
|
|
245
346
|
source: runnerDir,
|
package/dist/runtime-context.js
CHANGED
|
@@ -226,6 +226,38 @@ function hydrateRuntimeEnv(contextPath, context) {
|
|
|
226
226
|
function setEnv(name, value) {
|
|
227
227
|
if (value !== void 0 && value !== null && value !== "") process.env[name] = String(value);
|
|
228
228
|
}
|
|
229
|
+
function persistExplicitDevicePin(target, device) {
|
|
230
|
+
let repoRoot;
|
|
231
|
+
try {
|
|
232
|
+
repoRoot = fs.realpathSync(target);
|
|
233
|
+
} catch {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
const contextPath = path.join(repoRoot, recipeRuntimeDir(), "agentic-runtime.json");
|
|
237
|
+
if (!fs.existsSync(contextPath)) return;
|
|
238
|
+
let existing;
|
|
239
|
+
try {
|
|
240
|
+
const parsed = JSON.parse(fs.readFileSync(contextPath, "utf8"));
|
|
241
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return;
|
|
242
|
+
existing = parsed;
|
|
243
|
+
} catch {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
const next = { ...existing };
|
|
247
|
+
if (device.platform === "ios") {
|
|
248
|
+
const simulatorName = device.name ? device.name.replace(/_/gu, " ").trim() : device.id;
|
|
249
|
+
next.simulator = simulatorName;
|
|
250
|
+
next.simulatorUdid = device.id;
|
|
251
|
+
delete next.adbSerial;
|
|
252
|
+
} else {
|
|
253
|
+
next.adbSerial = device.id;
|
|
254
|
+
delete next.simulator;
|
|
255
|
+
delete next.simulatorUdid;
|
|
256
|
+
}
|
|
257
|
+
removeEmptyValues(next);
|
|
258
|
+
writeJsonAtomic(contextPath, next);
|
|
259
|
+
}
|
|
229
260
|
export {
|
|
230
|
-
ensureRuntimeContext
|
|
261
|
+
ensureRuntimeContext,
|
|
262
|
+
persistExplicitDevicePin
|
|
231
263
|
};
|
package/docs/RECIPES.md
CHANGED
|
@@ -58,6 +58,47 @@ mm-harness run recipe.json account="Account 2" --plan
|
|
|
58
58
|
mm-harness run recipe.json account="Account 2"
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
+
### Mobile device targeting
|
|
62
|
+
|
|
63
|
+
Always pass `--device <name|udid|serial>` when more than one simulator/device may
|
|
64
|
+
be connected. That pin:
|
|
65
|
+
|
|
66
|
+
1. Sets `IOS_SIMULATOR` / `ADB_SERIAL` for the run.
|
|
67
|
+
2. Rewrites the checkout `agentic-runtime.json` simulator/serial so the next
|
|
68
|
+
`resolveSlotPorts` does not revive a stale pin from another PR or slot.
|
|
69
|
+
|
|
70
|
+
Do **not** shut down other connected devices to "force" selection — selection is
|
|
71
|
+
by `--device`, not by exclusive boot.
|
|
72
|
+
|
|
73
|
+
For visual PR proof, prefer a short simulator recording or GIF over still
|
|
74
|
+
screenshots when the claim is a UI flow (timing / transitions). Use screenshots
|
|
75
|
+
as supporting stills.
|
|
76
|
+
|
|
77
|
+
### Perps Homepage visible performance
|
|
78
|
+
|
|
79
|
+
The bundled `perps.performance.homepage.*` family preserves the device-proven
|
|
80
|
+
Homepage TTC/DFD journeys. Run every recipe with an explicit device pin,
|
|
81
|
+
healing disabled, and full-run video:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
mm-harness run perps.performance.homepage.android-background-short --device <serial> --heal off --record-video=full-run
|
|
85
|
+
mm-harness run perps.performance.homepage.android-background-reconnect --device <serial> --heal off --record-video=full-run
|
|
86
|
+
mm-harness run perps.performance.homepage.android-cold-no-cache --device <serial> --heal off --record-video=full-run
|
|
87
|
+
mm-harness run perps.performance.homepage.android-cold-disk-cache --device <serial> --heal off --record-video=full-run
|
|
88
|
+
mm-harness run perps.performance.homepage.android-network-recovery --device <serial> --heal off --record-video=full-run
|
|
89
|
+
mm-harness run perps.performance.homepage.ios-background-short --device <udid> --heal off --record-video=full-run
|
|
90
|
+
mm-harness run perps.performance.homepage.ios-background-reconnect --device <udid> --heal off --record-video=full-run
|
|
91
|
+
mm-harness run perps.performance.homepage.ios-cold-no-cache --device <udid> --heal off --record-video=full-run
|
|
92
|
+
mm-harness run perps.performance.homepage.ios-cold-disk-cache --device <udid> --heal off --record-video=full-run
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The Android variants use a deterministic Hyperliquid testnet order and cancel
|
|
96
|
+
it after capture. The iOS variants use the existing fixture wallet and validate
|
|
97
|
+
trending content without fabricating position/order state. `app.network` is
|
|
98
|
+
Android-only because host-wide iOS network mutation cannot isolate one
|
|
99
|
+
simulator safely. These recipes validate viewport-demand-to-visible TTC/DFD;
|
|
100
|
+
they do not relabel process-start duration as TTC.
|
|
101
|
+
|
|
61
102
|
Rules:
|
|
62
103
|
|
|
63
104
|
- Validate with `--plan` before side effects.
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
|
|
4
|
+
import { resolveMobileToolPath } from '../platform/tool-paths.mjs';
|
|
5
|
+
|
|
6
|
+
const execFileAsync = promisify(execFile);
|
|
7
|
+
|
|
8
|
+
function sleep(ms) {
|
|
9
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function selectedPlatform(input) {
|
|
13
|
+
const value = String(
|
|
14
|
+
input.node?.platform ?? input.context?.env?.PLATFORM ?? process.env.PLATFORM ?? '',
|
|
15
|
+
).toLowerCase();
|
|
16
|
+
if (value === 'android' || value === 'ios') return value;
|
|
17
|
+
return selectedAndroidSerial(input) ? 'android' : 'ios';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function selectedAndroidSerial(input) {
|
|
21
|
+
const value =
|
|
22
|
+
input.node?.adb_serial ??
|
|
23
|
+
input.node?.device ??
|
|
24
|
+
input.context?.env?.ADB_SERIAL ??
|
|
25
|
+
input.context?.env?.ANDROID_SERIAL ??
|
|
26
|
+
process.env.ADB_SERIAL ??
|
|
27
|
+
process.env.ANDROID_SERIAL;
|
|
28
|
+
return value == null ? '' : String(value).trim();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function selectedAndroidPackage(input) {
|
|
32
|
+
return String(
|
|
33
|
+
input.node?.package_id ??
|
|
34
|
+
input.node?.app_id ??
|
|
35
|
+
input.context?.env?.ANDROID_PACKAGE_ID ??
|
|
36
|
+
process.env.ANDROID_PACKAGE_ID ??
|
|
37
|
+
'io.metamask',
|
|
38
|
+
).trim();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function defaultRun(file, args) {
|
|
42
|
+
return execFileAsync(file, args, { timeout: 15_000, encoding: 'utf8' });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function setMobileNetworkState(input, deps = {}) {
|
|
46
|
+
const state = String(input.node?.state ?? '').toLowerCase();
|
|
47
|
+
if (state !== 'offline' && state !== 'online') {
|
|
48
|
+
throw new Error('app.network.state must be offline or online.');
|
|
49
|
+
}
|
|
50
|
+
const platform = selectedPlatform(input);
|
|
51
|
+
if (platform !== 'android') {
|
|
52
|
+
throw new Error(
|
|
53
|
+
'app.network does not mutate iOS simulator host networking. A safe device-scoped iOS network capability is required before this lifecycle can be validated.',
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
const serial = selectedAndroidSerial(input);
|
|
57
|
+
if (!serial) {
|
|
58
|
+
throw new Error('Android app.network requires the selected ADB serial. Pass --device <serial>.');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const adb = deps.adb ?? resolveMobileToolPath('adb', { required: true });
|
|
62
|
+
const run = deps.run ?? defaultRun;
|
|
63
|
+
const appId = selectedAndroidPackage(input);
|
|
64
|
+
const airplaneMode = state === 'offline' ? 'enable' : 'disable';
|
|
65
|
+
await run(adb, [
|
|
66
|
+
'-s',
|
|
67
|
+
serial,
|
|
68
|
+
'shell',
|
|
69
|
+
'cmd',
|
|
70
|
+
'connectivity',
|
|
71
|
+
'airplane-mode',
|
|
72
|
+
airplaneMode,
|
|
73
|
+
]);
|
|
74
|
+
const status = await run(adb, [
|
|
75
|
+
'-s',
|
|
76
|
+
serial,
|
|
77
|
+
'shell',
|
|
78
|
+
'settings',
|
|
79
|
+
'get',
|
|
80
|
+
'global',
|
|
81
|
+
'airplane_mode_on',
|
|
82
|
+
]);
|
|
83
|
+
const expected = state === 'offline' ? '1' : '0';
|
|
84
|
+
const observed = String(status.stdout ?? '').trim();
|
|
85
|
+
if (observed !== expected) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
`app.network requested ${state}, but Android airplane_mode_on was ${JSON.stringify(observed)} instead of ${expected}.`,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
await run(adb, [
|
|
93
|
+
'-s',
|
|
94
|
+
serial,
|
|
95
|
+
'shell',
|
|
96
|
+
'monkey',
|
|
97
|
+
'-p',
|
|
98
|
+
appId,
|
|
99
|
+
'-c',
|
|
100
|
+
'android.intent.category.LAUNCHER',
|
|
101
|
+
'1',
|
|
102
|
+
]);
|
|
103
|
+
|
|
104
|
+
const fallbackSettleMs = state === 'offline' ? 3000 : 10000;
|
|
105
|
+
const settleMs = Number(input.node?.settle_ms ?? fallbackSettleMs);
|
|
106
|
+
if (!Number.isInteger(settleMs) || settleMs < 0 || settleMs > 60_000) {
|
|
107
|
+
throw new Error('app.network.settle_ms must be an integer from 0 through 60000.');
|
|
108
|
+
}
|
|
109
|
+
await (deps.sleep ?? sleep)(settleMs);
|
|
110
|
+
|
|
111
|
+
const foreground = await run(adb, [
|
|
112
|
+
'-s',
|
|
113
|
+
serial,
|
|
114
|
+
'shell',
|
|
115
|
+
'dumpsys',
|
|
116
|
+
'activity',
|
|
117
|
+
'activities',
|
|
118
|
+
]);
|
|
119
|
+
if (!String(foreground.stdout ?? '').includes(appId)) {
|
|
120
|
+
throw new Error(
|
|
121
|
+
`app.network changed Android networking but ${appId} was not foregrounded afterward.`,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
action: 'app.network',
|
|
127
|
+
platform,
|
|
128
|
+
deviceId: serial,
|
|
129
|
+
state,
|
|
130
|
+
appId,
|
|
131
|
+
airplaneMode: Number(expected),
|
|
132
|
+
settleMs,
|
|
133
|
+
foregroundEnsured: true,
|
|
134
|
+
};
|
|
135
|
+
}
|