@deeeed/metamask-harness 0.40.1 → 0.41.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 +16 -0
- package/adapters/manifest.json +8 -0
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +5 -4
- package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +24 -5
- package/adapters/mobile/metro-config.cjs +94 -0
- package/adapters/mobile/start-metro.sh +13 -1
- package/dist/commands/device-target.js +3 -0
- package/dist/commands/run-engine.js +4 -14
- package/dist/heal-bounds.js +1 -0
- package/dist/live-adapter-contract.js +2 -1
- package/dist/mm-harness-cli.js +1 -1
- package/docs/RECIPES.md +25 -19
- package/library/actions/mobile/perps/capture_performance.mjs +2 -2
- package/library/actions/mobile/perps/performance-capture.mjs +375 -17
- package/library/actions/mobile/perps/perps.mjs +56 -10
- package/library/actions/mobile/platform/bridge.mjs +59 -1
- package/library/actions/mobile/wallet/ensure_unlocked.mjs +60 -26
- package/library/actions/mobile/wallet/import.mjs +2 -0
- package/library/actions/mobile/wallet/select_account.mjs +21 -2
- package/library/manifests/mobile.action-manifest.json +47 -6
- package/library/recipes/mobile/perps/performance.recipe.json +680 -26
- package/package.json +1 -1
- package/library/recipes/mobile/perps/performance.background-resume.recipe.json +0 -56
- package/library/recipes/mobile/perps/performance.cold-start.recipe.json +0 -56
- package/library/recipes/mobile/perps/performance.homepage.android-background-reconnect.recipe.json +0 -159
- package/library/recipes/mobile/perps/performance.homepage.android-background-short.recipe.json +0 -157
- package/library/recipes/mobile/perps/performance.homepage.android-cold-disk-cache.recipe.json +0 -165
- package/library/recipes/mobile/perps/performance.homepage.android-cold-no-cache.recipe.json +0 -139
- package/library/recipes/mobile/perps/performance.homepage.android-network-recovery.recipe.json +0 -149
- package/library/recipes/mobile/perps/performance.homepage.cold-position-sample.recipe.json +0 -120
- package/library/recipes/mobile/perps/performance.homepage.ios-background-reconnect.recipe.json +0 -111
- package/library/recipes/mobile/perps/performance.homepage.ios-background-short.recipe.json +0 -108
- package/library/recipes/mobile/perps/performance.homepage.ios-cold-disk-cache.recipe.json +0 -122
- package/library/recipes/mobile/perps/performance.homepage.ios-cold-no-cache.recipe.json +0 -95
- package/library/recipes/mobile/perps/performance.warm-start.recipe.json +0 -49
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.41.0 - 2026-08-19
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Add one parameterized, platform-neutral Mobile Perps performance recipe covering cold, cached, resume, reconnect, account-switch, and network-switch lifecycles with native visible-content proof and production loading/WebSocket evidence.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Replace the duplicated Android, iOS, cold, warm, and background Perps performance recipes with the single lifecycle recipe.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- Keep explicitly pinned iOS and Android bridge targets isolated when both platforms are active, and classify bounded CDP target failures as infrastructure failures.
|
|
18
|
+
- Make Mobile wallet unlock and account selection deterministic across app restarts, propagate recipe timeouts through Perps state setup, and preserve missing performance counts and offsets as unknown instead of zero.
|
|
19
|
+
- Keep the entire checkout-local `temp` tree outside Metro watching during canonical `yarn watch` launches, and leave the Mobile recipe HUD visible by default for performance lifecycles.
|
|
20
|
+
|
|
5
21
|
## 0.40.1 - 2026-08-16
|
|
6
22
|
|
|
7
23
|
### Fixed
|
package/adapters/manifest.json
CHANGED
|
@@ -42,6 +42,14 @@
|
|
|
42
42
|
"inputs": "--target --port --log --pid-file [--workers] [--clear]",
|
|
43
43
|
"outputs": "Metro PID/launch metadata and redirected log; exit 0/1/2"
|
|
44
44
|
},
|
|
45
|
+
{
|
|
46
|
+
"id": "mobile/metro-config",
|
|
47
|
+
"entry": "adapters/mobile/metro-config.cjs",
|
|
48
|
+
"kind": "module",
|
|
49
|
+
"purpose": "Preserve the product Metro config while excluding the checkout-local temp tree owned by harness evidence and runtime state.",
|
|
50
|
+
"inputs": "base Metro config; env MM_HARNESS_METRO_PROJECT_ROOT, MM_HARNESS_METRO_BASE_CONFIG",
|
|
51
|
+
"outputs": "merged Metro config with resolver.blockList for <projectRoot>/temp"
|
|
52
|
+
},
|
|
45
53
|
{
|
|
46
54
|
"id": "mobile/stop-metro",
|
|
47
55
|
"entry": "adapters/mobile/stop-metro.sh",
|
|
@@ -30,12 +30,13 @@ const {
|
|
|
30
30
|
const { cdpEval, cdpEvalAsync } = require('./lib/cdp-eval.cjs');
|
|
31
31
|
const { buildArmSnippet, buildCollectSnippet } = require('./lib/issue-capture.cjs');
|
|
32
32
|
|
|
33
|
-
function
|
|
34
|
-
const
|
|
33
|
+
function parsePerformanceConsoleEvent(params) {
|
|
34
|
+
const markers = ['[PerpsPerf] ', '[HomepagePerf] '];
|
|
35
35
|
for (const arg of params?.args || []) {
|
|
36
36
|
const text = typeof arg?.value === 'string' ? arg.value : '';
|
|
37
|
+
const marker = markers.find((candidate) => text.includes(candidate));
|
|
38
|
+
if (!marker) continue;
|
|
37
39
|
const markerIndex = text.indexOf(marker);
|
|
38
|
-
if (markerIndex < 0) continue;
|
|
39
40
|
try {
|
|
40
41
|
return JSON.parse(text.slice(markerIndex + marker.length));
|
|
41
42
|
} catch {}
|
|
@@ -490,7 +491,7 @@ const COMMANDS = {
|
|
|
490
491
|
let removeConsoleListener = null;
|
|
491
492
|
if (visibleEventStage) {
|
|
492
493
|
removeConsoleListener = client.on('Runtime.consoleAPICalled', (params) => {
|
|
493
|
-
const payload =
|
|
494
|
+
const payload = parsePerformanceConsoleEvent(params);
|
|
494
495
|
if (payload?.stage === visibleEventStage) visibleEvents.push(payload);
|
|
495
496
|
});
|
|
496
497
|
await client.send('Runtime.enable');
|
|
@@ -145,6 +145,9 @@ async function discoverTarget(port) {
|
|
|
145
145
|
const adbSerial = process.env.ADB_SERIAL || process.env.ANDROID_SERIAL || '';
|
|
146
146
|
const androidPinned = Boolean(androidTargetName || androidDevice);
|
|
147
147
|
const simName = loadSimulatorName();
|
|
148
|
+
const iosPinned =
|
|
149
|
+
process.env.MM_HARNESS_EXPLICIT_PLATFORM === 'ios' ||
|
|
150
|
+
Boolean(process.env.SIM_UDID);
|
|
148
151
|
const matchesAndroidPin = (target) => androidTargetName
|
|
149
152
|
? target.deviceName !== simName &&
|
|
150
153
|
(target.deviceName === androidTargetName ||
|
|
@@ -166,6 +169,18 @@ async function discoverTarget(port) {
|
|
|
166
169
|
}
|
|
167
170
|
return false;
|
|
168
171
|
}
|
|
172
|
+
if (iosPinned && simName) {
|
|
173
|
+
const pinnedCandidates = runtimeCandidates.filter(
|
|
174
|
+
(candidate) => candidate.deviceName === simName,
|
|
175
|
+
);
|
|
176
|
+
for (const candidate of pinnedCandidates) {
|
|
177
|
+
if (await probeTarget(candidate.webSocketDebuggerUrl)) {
|
|
178
|
+
acceptedPinnedCandidate = candidate;
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
169
184
|
return true;
|
|
170
185
|
});
|
|
171
186
|
} catch (e) {
|
|
@@ -193,12 +208,16 @@ async function discoverTarget(port) {
|
|
|
193
208
|
// context, and letting it win here sends a pinned android action to the iOS
|
|
194
209
|
// target. No-match keeps the full candidate set (ambient sim configs tolerate a
|
|
195
210
|
// sim that is not currently attached).
|
|
196
|
-
if (simName && !androidPinned
|
|
211
|
+
if (simName && !androidPinned) {
|
|
197
212
|
const deviceFiltered = candidates.filter(
|
|
198
213
|
(t) => t.deviceName === simName,
|
|
199
214
|
);
|
|
200
215
|
if (deviceFiltered.length > 0) {
|
|
201
216
|
candidates = deviceFiltered;
|
|
217
|
+
} else if (iosPinned) {
|
|
218
|
+
throw coded(new Error(
|
|
219
|
+
`Pinned iOS simulator '${simName}' did not match any Metro target.`,
|
|
220
|
+
), BRIDGE_ERROR_CODES.NO_TARGET);
|
|
202
221
|
}
|
|
203
222
|
}
|
|
204
223
|
|
|
@@ -306,15 +325,15 @@ async function discoverTarget(port) {
|
|
|
306
325
|
}
|
|
307
326
|
}
|
|
308
327
|
|
|
309
|
-
if (androidPinned) {
|
|
328
|
+
if (androidPinned || iosPinned) {
|
|
310
329
|
const candidateList = candidates
|
|
311
330
|
.map((target) => ` deviceName=${JSON.stringify(target.deviceName || '')} ws=${target.webSocketDebuggerUrl || ''}`)
|
|
312
331
|
.join('\n');
|
|
313
332
|
throw coded(new Error(
|
|
314
|
-
`Pinned Android device has no responding __AGENTIC__ JS runtime after ${FETCH_RETRIES} discovery attempt(s).\n` +
|
|
315
|
-
` Requested --device (ADB_SERIAL): ${adbSerial || '(not set)'}\n` +
|
|
333
|
+
`Pinned ${androidPinned ? 'Android device' : `iOS simulator '${simName || process.env.SIM_UDID || '(unknown)'}'`} has no responding __AGENTIC__ JS runtime after ${FETCH_RETRIES} discovery attempt(s).\n` +
|
|
334
|
+
(androidPinned ? ` Requested --device (ADB_SERIAL): ${adbSerial || '(not set)'}\n` : '') +
|
|
316
335
|
` Matching non-agentic targets:\n${candidateList}\n` +
|
|
317
|
-
` The native C++ page is not a valid recipe target; wait for
|
|
336
|
+
` The native C++ page is not a valid recipe target; wait for Hermes to re-register or relaunch the pinned app.`,
|
|
318
337
|
), BRIDGE_ERROR_CODES.NO_TARGET);
|
|
319
338
|
}
|
|
320
339
|
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const { createRequire } = require('node:module');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const { pathToFileURL } = require('node:url');
|
|
7
|
+
|
|
8
|
+
function escapeRegExp(value) {
|
|
9
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function appendBlockList(blockList, pattern) {
|
|
13
|
+
if (Array.isArray(blockList)) return [...blockList, pattern];
|
|
14
|
+
return blockList instanceof RegExp ? [blockList, pattern] : [pattern];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function blockListFlags(blockList) {
|
|
18
|
+
const first = Array.isArray(blockList) ? blockList[0] : blockList;
|
|
19
|
+
return first instanceof RegExp && first.ignoreCase ? 'i' : '';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function resolveExpoConfigLoader(projectRoot) {
|
|
23
|
+
const requireFromProject = createRequire(path.join(projectRoot, 'package.json'));
|
|
24
|
+
let requireUtilsPath;
|
|
25
|
+
try {
|
|
26
|
+
requireUtilsPath = requireFromProject.resolve('@expo/require-utils');
|
|
27
|
+
} catch (error) {
|
|
28
|
+
if (error?.code !== 'MODULE_NOT_FOUND') throw error;
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
const { loadModuleSync } = requireFromProject(requireUtilsPath);
|
|
32
|
+
return typeof loadModuleSync === 'function' ? loadModuleSync : null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function loadProductConfig(configPath, baseConfig, projectRoot) {
|
|
36
|
+
let loaded;
|
|
37
|
+
const expoConfigLoader = resolveExpoConfigLoader(projectRoot);
|
|
38
|
+
if (expoConfigLoader) {
|
|
39
|
+
loaded = expoConfigLoader(configPath);
|
|
40
|
+
} else {
|
|
41
|
+
try {
|
|
42
|
+
loaded = require(configPath);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
if (error?.code !== 'ERR_REQUIRE_ESM') throw error;
|
|
45
|
+
loaded = await import(pathToFileURL(configPath).href);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const moduleValue = await loaded;
|
|
50
|
+
const exported =
|
|
51
|
+
moduleValue?.__esModule || moduleValue?.[Symbol.toStringTag] === 'Module'
|
|
52
|
+
? moduleValue.default
|
|
53
|
+
: moduleValue;
|
|
54
|
+
const config = await exported;
|
|
55
|
+
return typeof config === 'function' ? config(baseConfig) : config;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = async function harnessMetroConfig(baseConfig) {
|
|
59
|
+
const projectRoot = path.resolve(
|
|
60
|
+
process.env.MM_HARNESS_METRO_PROJECT_ROOT || process.cwd(),
|
|
61
|
+
);
|
|
62
|
+
const productConfigPath = path.resolve(
|
|
63
|
+
process.env.MM_HARNESS_METRO_BASE_CONFIG ||
|
|
64
|
+
path.join(projectRoot, 'metro.config.js'),
|
|
65
|
+
);
|
|
66
|
+
if (productConfigPath === __filename) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
'MM_HARNESS_METRO_BASE_CONFIG must reference the product Metro config, not the harness wrapper.',
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
let productConfig = baseConfig;
|
|
72
|
+
if (fs.existsSync(productConfigPath)) {
|
|
73
|
+
productConfig = await loadProductConfig(
|
|
74
|
+
productConfigPath,
|
|
75
|
+
baseConfig,
|
|
76
|
+
projectRoot,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const tempRoot = path.join(projectRoot, 'temp');
|
|
81
|
+
const existingBlockList =
|
|
82
|
+
productConfig.resolver?.blockList ?? baseConfig.resolver?.blockList;
|
|
83
|
+
const tempPattern = new RegExp(
|
|
84
|
+
`^${escapeRegExp(tempRoot)}(?:${escapeRegExp(path.sep)}|$)`,
|
|
85
|
+
blockListFlags(existingBlockList),
|
|
86
|
+
);
|
|
87
|
+
return {
|
|
88
|
+
...productConfig,
|
|
89
|
+
resolver: {
|
|
90
|
+
...productConfig.resolver,
|
|
91
|
+
blockList: appendBlockList(existingBlockList, tempPattern),
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
};
|
|
@@ -50,7 +50,16 @@ fi
|
|
|
50
50
|
# only ever inspect or signal a Metro listening on $PORT.
|
|
51
51
|
# shellcheck disable=SC1091
|
|
52
52
|
. "$SCRIPT_DIR/lib/metro-listener.sh"
|
|
53
|
-
|
|
53
|
+
HARNESS_METRO_CONFIG="$SCRIPT_DIR/metro-config.cjs"
|
|
54
|
+
if [ "${EXPO_OVERRIDE_METRO_CONFIG:-}" = "$HARNESS_METRO_CONFIG" ]; then
|
|
55
|
+
PRODUCT_METRO_CONFIG="${MM_HARNESS_METRO_BASE_CONFIG:-$TARGET/metro.config.js}"
|
|
56
|
+
else
|
|
57
|
+
PRODUCT_METRO_CONFIG="${EXPO_OVERRIDE_METRO_CONFIG:-$TARGET/metro.config.js}"
|
|
58
|
+
fi
|
|
59
|
+
export MM_HARNESS_METRO_PROJECT_ROOT="$TARGET"
|
|
60
|
+
export MM_HARNESS_METRO_BASE_CONFIG="$PRODUCT_METRO_CONFIG"
|
|
61
|
+
export EXPO_OVERRIDE_METRO_CONFIG="$HARNESS_METRO_CONFIG"
|
|
62
|
+
MOBILE_METRO_REQUIRED_ENV="${MOBILE_METRO_REQUIRED_ENV:-MM_INFURA_PROJECT_ID METAMASK_BUILD_TYPE METAMASK_ENVIRONMENT} MM_HARNESS_METRO_PROJECT_ROOT MM_HARNESS_METRO_BASE_CONFIG EXPO_OVERRIDE_METRO_CONFIG"
|
|
54
63
|
|
|
55
64
|
# RECIPE_RUNTIME_DIR (relative, validated) makes runtime state per-run so jobs
|
|
56
65
|
# sharing one checkout do not collide on metro.log / metro.pid / metro.tmux.
|
|
@@ -88,6 +97,9 @@ write_metro_build_env() {
|
|
|
88
97
|
done
|
|
89
98
|
)" || { rm -f "$temporary"; return 1; }
|
|
90
99
|
eval "$product_values"
|
|
100
|
+
required_MM_HARNESS_METRO_PROJECT_ROOT="$MM_HARNESS_METRO_PROJECT_ROOT"
|
|
101
|
+
required_MM_HARNESS_METRO_BASE_CONFIG="$MM_HARNESS_METRO_BASE_CONFIG"
|
|
102
|
+
required_EXPO_OVERRIDE_METRO_CONFIG="$EXPO_OVERRIDE_METRO_CONFIG"
|
|
91
103
|
[ "$CLEAR" = true ] && clear_value=1
|
|
92
104
|
{
|
|
93
105
|
printf 'export MM_HARNESS_METRO_WATCHER_PORT=%q\n' "$PORT"
|
|
@@ -26,6 +26,7 @@ function resolveAndroidModel(serial) {
|
|
|
26
26
|
}
|
|
27
27
|
function setAndroidDeviceEnv(id, fallbackName) {
|
|
28
28
|
process.env.PLATFORM = "android";
|
|
29
|
+
process.env.MM_HARNESS_EXPLICIT_PLATFORM = "android";
|
|
29
30
|
process.env.ADB_SERIAL = id;
|
|
30
31
|
process.env.ANDROID_SERIAL = id;
|
|
31
32
|
process.env.ANDROID_DEVICE = id;
|
|
@@ -33,9 +34,11 @@ function setAndroidDeviceEnv(id, fallbackName) {
|
|
|
33
34
|
if (model) process.env.ANDROID_TARGET_DEVICE_NAME = model;
|
|
34
35
|
else delete process.env.ANDROID_TARGET_DEVICE_NAME;
|
|
35
36
|
process.env.IOS_SIMULATOR = "";
|
|
37
|
+
process.env.SIM_UDID = "";
|
|
36
38
|
}
|
|
37
39
|
function setIosDeviceEnv(id, fallbackName) {
|
|
38
40
|
process.env.PLATFORM = "ios";
|
|
41
|
+
process.env.MM_HARNESS_EXPLICIT_PLATFORM = "ios";
|
|
39
42
|
process.env.IOS_SIMULATOR = fallbackName?.trim() || id;
|
|
40
43
|
process.env.SIM_UDID = id;
|
|
41
44
|
process.env.ADB_SERIAL = "";
|
|
@@ -155,12 +155,11 @@ async function resolveRecipeExecution(adapter, recipe, artifactsDir, projectRoot
|
|
|
155
155
|
projectRoot,
|
|
156
156
|
rootRecipe: recipeDocument
|
|
157
157
|
});
|
|
158
|
-
const suppressHudForLifecycleTiming = adapter === "mobile" && mobileRecipeRequiresUninterruptedLifecycleTiming(recipeDocument);
|
|
159
158
|
const suppressHudForOpaqueMobile = adapter === "mobile" && process.env.METAMASK_RECIPE_MOBILE_OPAQUE_RUNTIME === "1";
|
|
160
159
|
const runnerOptions = {
|
|
161
160
|
quietStdout: runtimeOptions.stdoutIsMachineContract === true,
|
|
162
161
|
suppressLibraryResolutionLogs: runtimeOptions.suppressLibraryResolutionLogs,
|
|
163
|
-
autoHud:
|
|
162
|
+
autoHud: suppressHudForOpaqueMobile ? false : trust.source && trust.source.trust !== "trusted" ? false : runtimeOptions.autoHud,
|
|
164
163
|
onActionEvent: runtimeOptions.onActionEvent,
|
|
165
164
|
actionSources,
|
|
166
165
|
// A direct engineer invocation is the explicit trust boundary for a
|
|
@@ -213,16 +212,6 @@ function runtimeRecipeDocument(recipe, absoluteRecipePath) {
|
|
|
213
212
|
return void 0;
|
|
214
213
|
}
|
|
215
214
|
}
|
|
216
|
-
function mobileRecipeRequiresUninterruptedLifecycleTiming(recipe) {
|
|
217
|
-
if (!isRecord(recipe)) return false;
|
|
218
|
-
const workflow = isRecord(recipe.workflow) ? recipe.workflow : void 0;
|
|
219
|
-
const nodes = workflow && isRecord(workflow.nodes) ? workflow.nodes : void 0;
|
|
220
|
-
if (!nodes) return false;
|
|
221
|
-
const actions = new Set(
|
|
222
|
-
Object.values(nodes).filter(isRecord).map((node) => node.action).filter((action) => typeof action === "string")
|
|
223
|
-
);
|
|
224
|
-
return actions.has("app.lifecycle") && actions.has("metamask.perps.capture_performance");
|
|
225
|
-
}
|
|
226
215
|
function recipeProcessEnvironment() {
|
|
227
216
|
const env = { ...process.env };
|
|
228
217
|
delete env.MM_HARNESS_CHECKOUT_LOCK_TOKEN;
|
|
@@ -314,7 +303,8 @@ function recipeRunEnv(adapter, runtimeOptions = {}) {
|
|
|
314
303
|
RECIPE_CDP_PORT: runtimeOptions.cdpPort ?? process.env.RECIPE_CDP_PORT,
|
|
315
304
|
FARMSLOT_SLOT_ID: runtimeOptions.slot ?? process.env.FARMSLOT_SLOT_ID,
|
|
316
305
|
SLOT_ID: runtimeOptions.slot ?? process.env.SLOT_ID,
|
|
317
|
-
PLATFORM: process.env.PLATFORM
|
|
306
|
+
PLATFORM: process.env.PLATFORM,
|
|
307
|
+
MM_HARNESS_EXPLICIT_PLATFORM: process.env.MM_HARNESS_EXPLICIT_PLATFORM
|
|
318
308
|
};
|
|
319
309
|
if (adapter !== "mobile") return base;
|
|
320
310
|
return {
|
|
@@ -322,6 +312,7 @@ function recipeRunEnv(adapter, runtimeOptions = {}) {
|
|
|
322
312
|
WATCHER_PORT: process.env.WATCHER_PORT ?? base.CDP_PORT ?? base.RECIPE_CDP_PORT,
|
|
323
313
|
METRO_PORT: process.env.METRO_PORT ?? process.env.WATCHER_PORT ?? base.CDP_PORT ?? base.RECIPE_CDP_PORT,
|
|
324
314
|
IOS_SIMULATOR: process.env.IOS_SIMULATOR,
|
|
315
|
+
SIM_UDID: process.env.SIM_UDID,
|
|
325
316
|
ANDROID_DEVICE: process.env.ANDROID_DEVICE,
|
|
326
317
|
ANDROID_TARGET_DEVICE_NAME: process.env.ANDROID_TARGET_DEVICE_NAME,
|
|
327
318
|
ADB_SERIAL: process.env.ADB_SERIAL,
|
|
@@ -1378,7 +1369,6 @@ export {
|
|
|
1378
1369
|
ensureMobileProofRuntime,
|
|
1379
1370
|
executeWithHealBounds,
|
|
1380
1371
|
listRunnableRecipes,
|
|
1381
|
-
mobileRecipeRequiresUninterruptedLifecycleTiming,
|
|
1382
1372
|
preflightRecipe,
|
|
1383
1373
|
prepareHeal,
|
|
1384
1374
|
prepareRuntimeIfNeeded,
|
package/dist/heal-bounds.js
CHANGED
|
@@ -62,6 +62,7 @@ async function ensureOverlay(adapter, target, heal, state, json) {
|
|
|
62
62
|
}
|
|
63
63
|
function classifyFailure(output) {
|
|
64
64
|
if (/SCREENSHOT_PROTECTED|screenshot is protected by FLAG_SECURE/iu.test(output)) return "screenshot-protected";
|
|
65
|
+
if (/CDP_TIMEOUT|Mobile CDP bridge command timed out|NO_TARGET|WS_CLOSED/iu.test(output)) return "infra";
|
|
65
66
|
if (/wallet|fixture|keyring|not seeded|\bsrp\b|password|onboard/iu.test(output)) return "wallet";
|
|
66
67
|
if (/metro|cdp|chrome|bridge|bundle|packager|port\b|econnrefused|not reachable|watcher|dev client|websocket/iu.test(output)) {
|
|
67
68
|
return "infra";
|
|
@@ -359,7 +359,8 @@ function liveAdapterProcessTimeoutMs(node) {
|
|
|
359
359
|
const actionTimeouts = [
|
|
360
360
|
node.target_timeout_ms,
|
|
361
361
|
node.unlock_timeout_ms,
|
|
362
|
-
node.timeout_ms
|
|
362
|
+
node.timeout_ms,
|
|
363
|
+
node.wait_timeout_ms
|
|
363
364
|
].map(Number).filter((value) => Number.isFinite(value) && value > 0);
|
|
364
365
|
if (actionTimeouts.length > 0) {
|
|
365
366
|
return Math.max(
|
package/dist/mm-harness-cli.js
CHANGED
|
@@ -243,7 +243,7 @@ Common overrides:
|
|
|
243
243
|
--json Machine-readable output
|
|
244
244
|
--json-stream Line-flushed JSONL progress + terminal event
|
|
245
245
|
--record-video=full-run Record a video of the run
|
|
246
|
-
--hud <auto|show|hide> Recipe HUD policy (
|
|
246
|
+
--hud <auto|show|hide> Recipe HUD policy (auto shows normal Mobile recipes; hide is explicit)
|
|
247
247
|
|
|
248
248
|
Advanced integrations:
|
|
249
249
|
runtime: --cdp-port, --watcher-port/--metro-port, --runtime-dir, --slot,
|
package/docs/RECIPES.md
CHANGED
|
@@ -176,30 +176,36 @@ For visual PR proof, prefer a short simulator recording or GIF over still
|
|
|
176
176
|
screenshots when the claim is a UI flow (timing / transitions). Use screenshots
|
|
177
177
|
as supporting stills.
|
|
178
178
|
|
|
179
|
-
### Perps
|
|
179
|
+
### Perps loading lifecycle validation
|
|
180
180
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
181
|
+
One platform-neutral recipe owns the reusable Perps lifecycle proof. The
|
|
182
|
+
harness adapter supplies Android or iOS behavior; the graph and lifecycle
|
|
183
|
+
parameters remain identical. Run it with an explicit device pin:
|
|
184
184
|
|
|
185
185
|
```bash
|
|
186
|
-
mm-harness run perps.performance
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
mm-harness run perps.performance.homepage.ios-cold-no-cache --device <udid> --heal off --record-video=full-run
|
|
194
|
-
mm-harness run perps.performance.homepage.ios-cold-disk-cache --device <udid> --heal off --record-video=full-run
|
|
186
|
+
mm-harness run perps.performance \
|
|
187
|
+
--device <android-serial-or-ios-udid> \
|
|
188
|
+
account=<fixture-account-name> \
|
|
189
|
+
content_variant=trending \
|
|
190
|
+
market=BTC \
|
|
191
|
+
lifecycle=cold_no_cache \
|
|
192
|
+
--heal off --record-video=full-run
|
|
195
193
|
```
|
|
196
194
|
|
|
197
|
-
The
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
195
|
+
The Mobile recipe HUD is shown by default, including during performance
|
|
196
|
+
validation. Use `--hud hide` only when a specific opaque/release proof requires
|
|
197
|
+
an unobstructed product surface; do not hide it merely to collect timings.
|
|
198
|
+
|
|
199
|
+
Supported lifecycle values are `navigate_return`, `cold_disk_cache`,
|
|
200
|
+
`cold_no_cache`, `background_short`, `background_reconnect`, `account_switch`,
|
|
201
|
+
and `network_switch`. The shared graph uses an existing fixture position and
|
|
202
|
+
never fabricates exchange state inside the measurement. It proves native
|
|
203
|
+
visible content with `ui.wait_for`, captures production `[PerpsLoadProof]` and
|
|
204
|
+
WebSocket milestones, and leaves detailed TTC/DFD to the app's Sentry traces.
|
|
205
|
+
It never relabels Metro, fixture, or process-start duration as TTC.
|
|
206
|
+
The recipe does not label a checkout as main or candidate; pin and record the
|
|
207
|
+
exact checkout commit externally, then use captured source milestones to verify
|
|
208
|
+
which data path actually ran.
|
|
203
209
|
|
|
204
210
|
Rules:
|
|
205
211
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { runAdapter } from '../platform/bridge.mjs';
|
|
2
|
-
import {
|
|
2
|
+
import { capturePerpsPerformance } from './performance-capture.mjs';
|
|
3
3
|
|
|
4
|
-
runAdapter(
|
|
4
|
+
runAdapter(capturePerpsPerformance);
|