@deeeed/metamask-harness 0.67.0 → 0.68.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 +21 -0
- package/adapters/mobile/metro-config.cjs +4 -0
- package/adapters/mobile/open-device.sh +53 -5
- package/adapters/mobile/start-metro.sh +14 -3
- package/adapters/mobile/wait-for-bridge.cjs +28 -81
- package/dist/adapters/mobile/prepare.js +5 -4
- package/dist/adapters/mobile/runtime-decision.js +3 -1
- package/dist/adapters/mobile/verify.js +57 -104
- package/dist/checkout-lock.js +62 -10
- package/dist/command-contract.js +1 -0
- package/dist/command-journal.js +52 -7
- package/dist/commands/checklist.js +13 -2
- package/dist/commands/fixtures.js +7 -7
- package/dist/commands/launch/index.js +20 -0
- package/dist/commands/prepare.js +25 -4
- package/dist/commands/run-engine.js +8 -2
- package/dist/commands/runtime-launch.js +19 -7
- package/dist/commands/shared.js +48 -21
- package/dist/commands/status-watch.js +23 -0
- package/dist/execution-provenance.js +1 -1
- package/dist/harness.js +7 -11
- package/dist/json-stream.js +3 -0
- package/dist/live-adapter-contract.js +5 -0
- package/dist/mm-harness-cli.js +3 -1
- package/dist/task-view.js +8 -1
- package/docs/CONTRIBUTING.md +37 -0
- package/docs/QA.md +12 -0
- package/library/actions/mobile/ui/native-navigation.mjs +18 -6
- package/package.json +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.68.0 - 2026-09-29
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Verify Mobile readiness with one combined status/bridge recipe, retaining device-discovery retries and RPC checks; recognize Lite screens when the release hides the Pro toggle.
|
|
10
|
+
- Restart Metro when build environment inputs change and select the matching transform-cache generation without clearing caches.
|
|
11
|
+
- Forward child checklist progress through the public CLI while retaining the owning step's diff-validation gate.
|
|
12
|
+
- Keep operation and checkout ownership records readable under restrictive umasks, and exclude observation updates from product provenance.
|
|
13
|
+
- Retain command-specific logs and stage observations in `status --watch`; preserve nested operation and checkout ownership across long builds. Pin protocol 0.33.0, recipe-harness 0.21.1, agent-runtime 0.15.1 and expo-recipe 0.12.2 for a consistent runtime dependency tree.
|
|
14
|
+
|
|
15
|
+
## 0.67.1 - 2026-09-29
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- Prepare the selected Android display on cold and ready-path launches. Locked or unreadable states return `MOBILE_DEVICE_LOCKED` or `MOBILE_DEVICE_UI_UNAVAILABLE` without recovery; the internal `--ui-only` mode supports Android.
|
|
20
|
+
- Honor the full Mobile bridge startup deadline with integer probe timeouts and aligned fixture-reset bounds, instead of restarting slow cold starts prematurely. Unresponsive runtimes still fail at the caller's deadline.
|
|
21
|
+
|
|
22
|
+
### Removed
|
|
23
|
+
|
|
24
|
+
- The undocumented `MOBILE_BRIDGE_AGENTIC_GRACE_MS` and `MOBILE_BRIDGE_UNRESPONSIVE_GRACE_MS` early-abort settings; use `MOBILE_BRIDGE_READY_TIMEOUT_MS` to set the startup budget.
|
|
25
|
+
|
|
5
26
|
## 0.67.0 - 2026-09-28
|
|
6
27
|
|
|
7
28
|
### Fixed
|
|
@@ -84,8 +84,12 @@ module.exports = async function harnessMetroConfig(baseConfig) {
|
|
|
84
84
|
`^${escapeRegExp(tempRoot)}(?:${escapeRegExp(path.sep)}|$)`,
|
|
85
85
|
blockListFlags(existingBlockList),
|
|
86
86
|
);
|
|
87
|
+
// Inline environment values affect transformed JavaScript. Keep separate
|
|
88
|
+
// cache generations for those inputs instead of deleting the shared cache.
|
|
89
|
+
const envFingerprint = process.env.MM_HARNESS_METRO_ENV_FINGERPRINT;
|
|
87
90
|
return {
|
|
88
91
|
...productConfig,
|
|
92
|
+
...(envFingerprint ? { cacheVersion: `${productConfig.cacheVersion ?? ''}:mm-env:${envFingerprint}` } : {}),
|
|
89
93
|
resolver: {
|
|
90
94
|
...productConfig.resolver,
|
|
91
95
|
blockList: appendBlockList(existingBlockList, tempPattern),
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
# --adb-serial <serial> Android device serial (default: ADB_SERIAL env)
|
|
15
15
|
# --preflight-mode fast|auto|rebuild-native|clean (default: fast; env MOBILE_PREFLIGHT_MODE)
|
|
16
16
|
# --restart terminate the selected app before opening it
|
|
17
|
-
# --ui-only
|
|
17
|
+
# --ui-only prepare the selected device UI without launching the app
|
|
18
18
|
# MM_HARNESS_SIMULATOR_APP absolute Simulator.app or DeviceHub.app path; GUI only
|
|
19
19
|
# MM_HARNESS_SHOW_SIMULATOR=0 opt out of automatic GUI opening
|
|
20
20
|
# MM_HARNESS_FOCUS_SIMULATOR=1 explicitly open/activate the GUI, even with SHOW_SIMULATOR=0
|
|
@@ -69,10 +69,6 @@ if [ "$PLATFORM" = "android" ]; then
|
|
|
69
69
|
fi
|
|
70
70
|
export MM_HARNESS_ADB_PATH="$ADB_BIN"
|
|
71
71
|
fi
|
|
72
|
-
if $UI_ONLY && [ "$PLATFORM" != "ios" ]; then
|
|
73
|
-
printf 'open-device: --ui-only is supported only for ios.\n' >&2
|
|
74
|
-
exit 2
|
|
75
|
-
fi
|
|
76
72
|
|
|
77
73
|
TARGET="$(cd "$TARGET" && pwd)"
|
|
78
74
|
|
|
@@ -345,6 +341,52 @@ focus_simulator() {
|
|
|
345
341
|
|
|
346
342
|
# --- Android helpers ----------------------------------------------------------
|
|
347
343
|
|
|
344
|
+
prepare_android_display() {
|
|
345
|
+
local power_state="" wake_state="" trust_state="" lock_state="" _attempt
|
|
346
|
+
printf 'Waking selected Android device before launch\n' >&2
|
|
347
|
+
if ! "$ADB_BIN" "${ADB_ARGS[@]}" shell input keyevent KEYCODE_WAKEUP >&2; then
|
|
348
|
+
printf 'open-device: Android display preparation failed.\nNext: wake and unlock the selected device, then retry.\n' >&2
|
|
349
|
+
return 1
|
|
350
|
+
fi
|
|
351
|
+
for _attempt in {1..12}; do
|
|
352
|
+
if ! power_state="$("$ADB_BIN" "${ADB_ARGS[@]}" shell dumpsys power)"; then
|
|
353
|
+
break
|
|
354
|
+
fi
|
|
355
|
+
wake_state="$(printf '%s\n' "$power_state" | tr -d '\r' | sed -nE 's/^[[:space:]]*mWakefulness=([^[:space:]]+).*/\1/p' | head -1)"
|
|
356
|
+
case "$wake_state" in
|
|
357
|
+
Awake|1) break ;;
|
|
358
|
+
Asleep|Dozing|Dreaming|0|2|3) sleep 0.25 ;;
|
|
359
|
+
*) break ;;
|
|
360
|
+
esac
|
|
361
|
+
done
|
|
362
|
+
if [ "$wake_state" != Awake ] && [ "$wake_state" != 1 ]; then
|
|
363
|
+
printf 'open-device: Android display preparation failed.\nNext: wake the selected device and check adb access, then retry.\n' >&2
|
|
364
|
+
return 1
|
|
365
|
+
fi
|
|
366
|
+
if ! "$ADB_BIN" "${ADB_ARGS[@]}" shell wm dismiss-keyguard >&2; then
|
|
367
|
+
printf 'open-device: Android display preparation failed.\nNext: unlock the selected device, then retry.\n' >&2
|
|
368
|
+
return 1
|
|
369
|
+
fi
|
|
370
|
+
# OS dismissal can finish asynchronously, and cannot unlock a secure keyguard.
|
|
371
|
+
for _attempt in {1..12}; do
|
|
372
|
+
if ! trust_state="$("$ADB_BIN" "${ADB_ARGS[@]}" shell dumpsys trust)"; then
|
|
373
|
+
printf 'open-device: Android lock state unavailable.\nNext: check adb access and the selected device screen, then retry.\n' >&2
|
|
374
|
+
return 1
|
|
375
|
+
fi
|
|
376
|
+
lock_state="$(printf '%s\n' "$trust_state" | tr -d '\r' | sed -nE '/\(current\):/s/.*deviceLocked=([01]|true|false)(,.*)?$/\1/p')"
|
|
377
|
+
case "$lock_state" in
|
|
378
|
+
0|false) return 0 ;;
|
|
379
|
+
1|true) sleep 0.25 ;;
|
|
380
|
+
*)
|
|
381
|
+
printf 'open-device: Android lock state unavailable.\nNext: check adb access and the selected device screen, then retry.\n' >&2
|
|
382
|
+
return 1
|
|
383
|
+
;;
|
|
384
|
+
esac
|
|
385
|
+
done
|
|
386
|
+
printf 'open-device: Android device is locked.\nNext: unlock the selected device, then retry; credentials are never entered automatically.\n' >&2
|
|
387
|
+
return 1
|
|
388
|
+
}
|
|
389
|
+
|
|
348
390
|
android_app_installed() {
|
|
349
391
|
local pkg="$1"
|
|
350
392
|
shift
|
|
@@ -597,6 +639,10 @@ if [ "$PLATFORM" = "ios" ]; then
|
|
|
597
639
|
else
|
|
598
640
|
ADB_ARGS=()
|
|
599
641
|
[ -n "$ADB_SERIAL_ARG" ] && ADB_ARGS+=("-s" "$ADB_SERIAL_ARG")
|
|
642
|
+
if $UI_ONLY; then
|
|
643
|
+
prepare_android_display
|
|
644
|
+
exit $?
|
|
645
|
+
fi
|
|
600
646
|
|
|
601
647
|
LAUNCHED=false
|
|
602
648
|
for PKG in "${ANDROID_PKGS[@]}"; do
|
|
@@ -621,6 +667,8 @@ else
|
|
|
621
667
|
|| printf 'http%%3A%%2F%%2Flocalhost%%3A%s%%3FdisableOnboarding%%3D1' "$PORT")"
|
|
622
668
|
DEEP_LINK="expo-metamask://expo-development-client/?url=${ENCODED_URL}"
|
|
623
669
|
|
|
670
|
+
prepare_android_display || exit 1
|
|
671
|
+
|
|
624
672
|
printf 'Launching Android dev client %s\n' "$PKG" >&2
|
|
625
673
|
if "$ADB_BIN" "${ADB_ARGS[@]}" shell am start \
|
|
626
674
|
-a android.intent.action.VIEW \
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
# Inputs:
|
|
11
11
|
# --target <metamask-mobile dir> (default $PWD)
|
|
12
12
|
# --port <number> (default: WATCHER_PORT env, else METRO_PORT, else 8081)
|
|
13
|
+
# --restart restart Metro while retaining its transform cache
|
|
13
14
|
# --clear reset Metro bundler cache before starting
|
|
14
15
|
# Outputs:
|
|
15
16
|
# Progress on stderr; exit 0 Metro ready; 1 failed to start; 2 bad args.
|
|
@@ -20,16 +21,19 @@ set -uo pipefail
|
|
|
20
21
|
TARGET="$PWD"
|
|
21
22
|
PORT="${WATCHER_PORT:-${METRO_PORT:-8081}}"
|
|
22
23
|
CLEAR=false
|
|
24
|
+
RESTART=false
|
|
23
25
|
|
|
24
26
|
while [ "$#" -gt 0 ]; do
|
|
25
27
|
case "$1" in
|
|
26
28
|
--target) TARGET="$2"; shift 2 ;;
|
|
27
29
|
--port) PORT="$2"; shift 2 ;;
|
|
28
30
|
--clear) CLEAR=true; shift ;;
|
|
31
|
+
--restart) RESTART=true; shift ;;
|
|
29
32
|
-h|--help)
|
|
30
|
-
printf 'Usage: start-metro.sh [--target <dir>] [--port <port>] [--clear]\n'
|
|
33
|
+
printf 'Usage: start-metro.sh [--target <dir>] [--port <port>] [--restart] [--clear]\n'
|
|
31
34
|
printf ' --target MetaMask Mobile checkout directory\n'
|
|
32
35
|
printf ' --port Metro port (default: WATCHER_PORT env or 8081)\n'
|
|
36
|
+
printf ' --restart restart while preserving the transform cache\n'
|
|
33
37
|
printf ' --clear restart with cleared cache\n'
|
|
34
38
|
exit 0
|
|
35
39
|
;;
|
|
@@ -60,6 +64,9 @@ export MM_HARNESS_METRO_PROJECT_ROOT="$TARGET"
|
|
|
60
64
|
export MM_HARNESS_METRO_BASE_CONFIG="$PRODUCT_METRO_CONFIG"
|
|
61
65
|
export EXPO_OVERRIDE_METRO_CONFIG="$HARNESS_METRO_CONFIG"
|
|
62
66
|
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"
|
|
67
|
+
if [ -n "${MM_HARNESS_METRO_ENV_FINGERPRINT:-}" ]; then
|
|
68
|
+
MOBILE_METRO_REQUIRED_ENV="$MOBILE_METRO_REQUIRED_ENV MM_HARNESS_METRO_ENV_FINGERPRINT"
|
|
69
|
+
fi
|
|
63
70
|
|
|
64
71
|
# RECIPE_RUNTIME_DIR (relative, validated) makes runtime state per-run so jobs
|
|
65
72
|
# sharing one checkout do not collide on metro.log / metro.pid / metro.tmux.
|
|
@@ -212,8 +219,12 @@ fi
|
|
|
212
219
|
|
|
213
220
|
# Decide whether to (re)start or skip.
|
|
214
221
|
if metro_ready; then
|
|
215
|
-
if [ "$CLEAR" = true ]; then
|
|
216
|
-
|
|
222
|
+
if [ "$CLEAR" = true ] || [ "$RESTART" = true ]; then
|
|
223
|
+
if [ "$CLEAR" = true ]; then
|
|
224
|
+
printf 'Restarting Metro on port %s with cleared cache\n' "$PORT" >&2
|
|
225
|
+
else
|
|
226
|
+
printf 'Restarting Metro on port %s with its transform cache preserved\n' "$PORT" >&2
|
|
227
|
+
fi
|
|
217
228
|
stop_metro_listener || exit 1
|
|
218
229
|
rm -f "$PID_FILE"
|
|
219
230
|
elif metro_listener_valid; then
|
|
@@ -22,8 +22,6 @@ const {
|
|
|
22
22
|
const DEFAULT_MAX_POLLS = 90;
|
|
23
23
|
const DEFAULT_POLL_INTERVAL_MS = 2_000;
|
|
24
24
|
const DEFAULT_PROBE_TIMEOUT_MS = 1_500;
|
|
25
|
-
const DEFAULT_AGENTIC_GRACE_MS = 15_000;
|
|
26
|
-
const DEFAULT_UNRESPONSIVE_GRACE_MS = 12_000;
|
|
27
25
|
const PROGRESS_INTERVAL_MS = 10_000;
|
|
28
26
|
const MAX_HTTP_BODY_BYTES = 2 * 1024 * 1024;
|
|
29
27
|
|
|
@@ -76,15 +74,6 @@ function parseArgs(argv, env = process.env) {
|
|
|
76
74
|
env.MOBILE_BRIDGE_PROBE_TIMEOUT_MS || String(DEFAULT_PROBE_TIMEOUT_MS),
|
|
77
75
|
'MOBILE_BRIDGE_PROBE_TIMEOUT_MS',
|
|
78
76
|
);
|
|
79
|
-
const agenticGraceMs = parsePositiveInteger(
|
|
80
|
-
env.MOBILE_BRIDGE_AGENTIC_GRACE_MS || String(DEFAULT_AGENTIC_GRACE_MS),
|
|
81
|
-
'MOBILE_BRIDGE_AGENTIC_GRACE_MS',
|
|
82
|
-
);
|
|
83
|
-
const unresponsiveGraceMs = parsePositiveInteger(
|
|
84
|
-
env.MOBILE_BRIDGE_UNRESPONSIVE_GRACE_MS ||
|
|
85
|
-
String(DEFAULT_UNRESPONSIVE_GRACE_MS),
|
|
86
|
-
'MOBILE_BRIDGE_UNRESPONSIVE_GRACE_MS',
|
|
87
|
-
);
|
|
88
77
|
|
|
89
78
|
return {
|
|
90
79
|
...options,
|
|
@@ -93,8 +82,6 @@ function parseArgs(argv, env = process.env) {
|
|
|
93
82
|
timeoutMs,
|
|
94
83
|
pollIntervalMs,
|
|
95
84
|
probeTimeoutMs,
|
|
96
|
-
agenticGraceMs,
|
|
97
|
-
unresponsiveGraceMs,
|
|
98
85
|
};
|
|
99
86
|
}
|
|
100
87
|
|
|
@@ -128,15 +115,17 @@ function fetchJson(port, pathname, timeoutMs) {
|
|
|
128
115
|
}
|
|
129
116
|
|
|
130
117
|
function runBridgeStatus({ bridgePath, target, timeoutMs, env }) {
|
|
118
|
+
// performance.now() leaves fractional milliseconds near the deadline.
|
|
119
|
+
const budgetMs = Math.max(1, Math.floor(timeoutMs));
|
|
131
120
|
const result = spawnSync(process.execPath, [bridgePath, 'status'], {
|
|
132
121
|
cwd: target,
|
|
133
122
|
env: {
|
|
134
123
|
...env,
|
|
135
124
|
APP_ROOT: target,
|
|
136
|
-
CDP_TIMEOUT: String(
|
|
125
|
+
CDP_TIMEOUT: String(budgetMs),
|
|
137
126
|
},
|
|
138
127
|
encoding: 'utf8',
|
|
139
|
-
timeout:
|
|
128
|
+
timeout: budgetMs,
|
|
140
129
|
killSignal: 'SIGTERM',
|
|
141
130
|
maxBuffer: MAX_HTTP_BODY_BYTES,
|
|
142
131
|
});
|
|
@@ -154,7 +143,7 @@ function runBridgeStatus({ bridgePath, target, timeoutMs, env }) {
|
|
|
154
143
|
stdout || stderr
|
|
155
144
|
? `${stdout}${stderr}`.trimEnd()
|
|
156
145
|
: result.error?.code === 'ETIMEDOUT'
|
|
157
|
-
? `bridge status probe exceeded ${
|
|
146
|
+
? `bridge status probe exceeded ${budgetMs}ms`
|
|
158
147
|
: '',
|
|
159
148
|
timedOut: result.error?.code === 'ETIMEDOUT',
|
|
160
149
|
};
|
|
@@ -270,26 +259,15 @@ function waitReason(observation, state, config) {
|
|
|
270
259
|
if (observation.metro === 'no-target') {
|
|
271
260
|
return 'Metro running; no React Native debug target yet';
|
|
272
261
|
}
|
|
273
|
-
if (state.
|
|
262
|
+
if (state.agenticMissing) {
|
|
274
263
|
return 'React Native target answered; in-app bridge not installed yet';
|
|
275
264
|
}
|
|
276
|
-
if (state.
|
|
265
|
+
if (state.unresponsive) {
|
|
277
266
|
return 'React Native target visible; CDP is not answering';
|
|
278
267
|
}
|
|
279
268
|
return 'React Native target answered; waiting for an in-app route';
|
|
280
269
|
}
|
|
281
270
|
|
|
282
|
-
function failure(kind, startedAt, deadline, now, attempts, lastObservation) {
|
|
283
|
-
return {
|
|
284
|
-
ok: false,
|
|
285
|
-
kind,
|
|
286
|
-
attempts,
|
|
287
|
-
elapsedMs: Math.max(0, now - startedAt),
|
|
288
|
-
remainingMs: Math.max(0, deadline - now),
|
|
289
|
-
lastObservation,
|
|
290
|
-
};
|
|
291
|
-
}
|
|
292
|
-
|
|
293
271
|
async function waitForBridge(config, dependencies = {}) {
|
|
294
272
|
const now = dependencies.now || (() => performance.now());
|
|
295
273
|
const sleep =
|
|
@@ -299,11 +277,13 @@ async function waitForBridge(config, dependencies = {}) {
|
|
|
299
277
|
const progress = dependencies.progress || (() => undefined);
|
|
300
278
|
const status = dependencies.status || (() => undefined);
|
|
301
279
|
const startedAt = now();
|
|
280
|
+
// Native inspector discovery can precede JavaScript and bridge installation.
|
|
281
|
+
// Only the caller's startup deadline bounds those intermediate states.
|
|
302
282
|
const deadline = startedAt + config.timeoutMs;
|
|
303
283
|
const state = {
|
|
304
284
|
readyStreak: 0,
|
|
305
|
-
|
|
306
|
-
|
|
285
|
+
unresponsive: false,
|
|
286
|
+
agenticMissing: false,
|
|
307
287
|
};
|
|
308
288
|
let attempts = 0;
|
|
309
289
|
let nextProgressAt = startedAt;
|
|
@@ -328,8 +308,8 @@ async function waitForBridge(config, dependencies = {}) {
|
|
|
328
308
|
|
|
329
309
|
if (hasMatchingRoute(lastObservation.statusValue, config.env)) {
|
|
330
310
|
state.readyStreak += 1;
|
|
331
|
-
state.
|
|
332
|
-
state.
|
|
311
|
+
state.unresponsive = false;
|
|
312
|
+
state.agenticMissing = false;
|
|
333
313
|
if (state.readyStreak >= 2) {
|
|
334
314
|
return {
|
|
335
315
|
ok: true,
|
|
@@ -345,37 +325,13 @@ async function waitForBridge(config, dependencies = {}) {
|
|
|
345
325
|
(snapshot) => snapshot.agenticPresent === false,
|
|
346
326
|
);
|
|
347
327
|
if (agenticMissing) {
|
|
348
|
-
state.
|
|
349
|
-
state.
|
|
350
|
-
if (afterProbe - state.agenticMissingSince >= config.agenticGraceMs) {
|
|
351
|
-
return failure(
|
|
352
|
-
'agentic-missing',
|
|
353
|
-
startedAt,
|
|
354
|
-
deadline,
|
|
355
|
-
afterProbe,
|
|
356
|
-
attempts,
|
|
357
|
-
lastObservation,
|
|
358
|
-
);
|
|
359
|
-
}
|
|
328
|
+
state.agenticMissing = true;
|
|
329
|
+
state.unresponsive = false;
|
|
360
330
|
} else {
|
|
361
|
-
state.
|
|
331
|
+
state.agenticMissing = false;
|
|
362
332
|
const targetUnresponsive =
|
|
363
333
|
lastObservation.metro === 'target-visible' && snapshots.length === 0;
|
|
364
|
-
|
|
365
|
-
else state.unresponsiveSince = null;
|
|
366
|
-
if (
|
|
367
|
-
state.unresponsiveSince !== null &&
|
|
368
|
-
afterProbe - state.unresponsiveSince >= config.unresponsiveGraceMs
|
|
369
|
-
) {
|
|
370
|
-
return failure(
|
|
371
|
-
'target-unresponsive',
|
|
372
|
-
startedAt,
|
|
373
|
-
deadline,
|
|
374
|
-
afterProbe,
|
|
375
|
-
attempts,
|
|
376
|
-
lastObservation,
|
|
377
|
-
);
|
|
378
|
-
}
|
|
334
|
+
state.unresponsive = targetUnresponsive;
|
|
379
335
|
}
|
|
380
336
|
}
|
|
381
337
|
|
|
@@ -394,14 +350,15 @@ async function waitForBridge(config, dependencies = {}) {
|
|
|
394
350
|
}
|
|
395
351
|
|
|
396
352
|
const endedAt = now();
|
|
397
|
-
return
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
deadline,
|
|
401
|
-
endedAt,
|
|
353
|
+
return {
|
|
354
|
+
ok: false,
|
|
355
|
+
kind: 'deadline',
|
|
402
356
|
attempts,
|
|
357
|
+
elapsedMs: Math.max(0, endedAt - startedAt),
|
|
358
|
+
remainingMs: Math.max(0, deadline - endedAt),
|
|
359
|
+
reason: waitReason(lastObservation, state, config),
|
|
403
360
|
lastObservation,
|
|
404
|
-
|
|
361
|
+
};
|
|
405
362
|
}
|
|
406
363
|
|
|
407
364
|
function bundleProgress(logPath) {
|
|
@@ -507,21 +464,11 @@ async function main() {
|
|
|
507
464
|
process.stderr.write(fs.readFileSync(statusLog, 'utf8'));
|
|
508
465
|
} catch {}
|
|
509
466
|
const requested = describeRequested(env);
|
|
510
|
-
if (result.kind === 'target-unresponsive') {
|
|
511
|
-
process.stderr.write(
|
|
512
|
-
`wait-for-bridge: React Native target for ${requested} was visible but CDP did not answer for ${formatSeconds(parsed.unresponsiveGraceMs)} (${formatSeconds(result.elapsedMs)} elapsed, ${formatSeconds(result.remainingMs)} remaining).\n`,
|
|
513
|
-
);
|
|
514
|
-
} else if (result.kind === 'agentic-missing') {
|
|
515
|
-
process.stderr.write(
|
|
516
|
-
`wait-for-bridge: React Native target for ${requested} answered without __AGENTIC__ for ${formatSeconds(parsed.agenticGraceMs)}.\n`,
|
|
517
|
-
);
|
|
518
|
-
} else {
|
|
519
|
-
process.stderr.write(
|
|
520
|
-
`wait-for-bridge: readiness deadline expired after ${formatSeconds(result.elapsedMs)} on port ${parsed.port}.\n`,
|
|
521
|
-
);
|
|
522
|
-
}
|
|
523
467
|
process.stderr.write(
|
|
524
|
-
`wait-for-bridge:
|
|
468
|
+
`wait-for-bridge: readiness deadline expired after ${formatSeconds(result.elapsedMs)} on port ${parsed.port}: ${result.reason}.\n`,
|
|
469
|
+
);
|
|
470
|
+
process.stderr.write(
|
|
471
|
+
`wait-for-bridge: no bridge target matched the requested ${requested} on port ${parsed.port}: ${result.reason}.\n`,
|
|
525
472
|
);
|
|
526
473
|
process.stderr.write(` requested: ${requested}\n`);
|
|
527
474
|
process.stderr.write(
|
|
@@ -242,7 +242,6 @@ function startMobileConsoleForwarder(target, platform, json, watcherPort) {
|
|
|
242
242
|
);
|
|
243
243
|
}
|
|
244
244
|
function ensureMobileHumanSurface(target, platform, json, watcherPort) {
|
|
245
|
-
if (platform !== "ios") return Promise.resolve({ status: 0, output: "" });
|
|
246
245
|
return dispatchAction(
|
|
247
246
|
{ id: "ensure-device-ui", cwd: target },
|
|
248
247
|
target,
|
|
@@ -271,7 +270,8 @@ async function dispatchActionSequence(requestedActions, target, platform, json,
|
|
|
271
270
|
platform,
|
|
272
271
|
json,
|
|
273
272
|
preflightMode,
|
|
274
|
-
watcherPort
|
|
273
|
+
watcherPort,
|
|
274
|
+
pendingMetroEnvFingerprint
|
|
275
275
|
);
|
|
276
276
|
if (result.status !== 0) return result;
|
|
277
277
|
if (action.id === "yarn-setup") recordDepsBaseline(resolved);
|
|
@@ -379,7 +379,7 @@ function enableIosAccessibility(json) {
|
|
|
379
379
|
return false;
|
|
380
380
|
}
|
|
381
381
|
}
|
|
382
|
-
async function dispatchAction(action, target, platform, json, preflightMode = "fast", watcherPort) {
|
|
382
|
+
async function dispatchAction(action, target, platform, json, preflightMode = "fast", watcherPort, metroEnvFingerprint) {
|
|
383
383
|
const cwd = action.cwd ?? target;
|
|
384
384
|
switch (action.id) {
|
|
385
385
|
case "yarn-setup": {
|
|
@@ -397,7 +397,8 @@ async function dispatchAction(action, target, platform, json, preflightMode = "f
|
|
|
397
397
|
return spawnScriptStreaming(
|
|
398
398
|
leaf,
|
|
399
399
|
withWatcherPort(["--target", cwd, ...extra], watcherPort),
|
|
400
|
-
target
|
|
400
|
+
target,
|
|
401
|
+
{ MM_HARNESS_METRO_ENV_FINGERPRINT: metroEnvFingerprint ?? mobileMetroEnvCheck(path.resolve(target)).fingerprint }
|
|
401
402
|
);
|
|
402
403
|
}
|
|
403
404
|
case "start-console-forwarder": {
|
|
@@ -184,7 +184,9 @@ async function decideMobileReadiness(target, options = {}) {
|
|
|
184
184
|
env.status === "missing" ? "Metro environment has no successful bundle baseline; restart Metro to load the current environment without clearing its transform cache." : "Metro environment inputs changed since the successful bundle baseline; restart Metro without clearing its transform cache. Pass --clear-metro only if stale transforms are observed.",
|
|
185
185
|
...report.reasons
|
|
186
186
|
],
|
|
187
|
-
actions: report.decision === "ready" ? launchActions(resolved) : report.actions
|
|
187
|
+
actions: (report.decision === "ready" ? launchActions(resolved) : report.actions).map(
|
|
188
|
+
(action) => action.id === "start-metro" && !action.argv?.includes("--clear") && !action.argv?.includes("--restart") ? { ...action, argv: [...action.argv ?? [], "--restart"] } : action
|
|
189
|
+
)
|
|
188
190
|
};
|
|
189
191
|
}
|
|
190
192
|
function writeDepsNote(resolved) {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { execFileSync
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
2
|
import crypto from "node:crypto";
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
|
+
import { spawnScriptStreaming } from "../../commands/shared.js";
|
|
5
6
|
import { readRuntimeContextField } from "../../harness.js";
|
|
6
7
|
import { recipeHarnessPath, recipeHarnessRoot, recipeRuntimeDir, runnerDir } from "../../paths.js";
|
|
7
8
|
const USAGE = "Usage: mm-harness verify --adapter mobile -- [--target <metamask-mobile>] [--artifacts-dir <dir>] [--static-only] [--platform ios|android] [--preflight-mode fast|auto|default|rebuild-native|clean] [--no-auto-start]";
|
|
@@ -11,19 +12,6 @@ const AUTO_START_TRUE = ["1", "true", "TRUE", "True", "yes", "YES", "Yes", "on",
|
|
|
11
12
|
const AUTO_START_FALSE = ["0", "false", "FALSE", "False", "no", "NO", "No", "off", "OFF", "Off", ""];
|
|
12
13
|
const AUTO_START_REFUSAL = "Mobile auto-start is not allowed from product-local scripts. Start or prepare the app through the runner/slot runtime, then rerun verify with --no-auto-start.";
|
|
13
14
|
const EVM_READINESS_EXPRESSION = "(async function(){try{var engine=globalThis.Engine;var controller=engine&&engine.context&&engine.context.NetworkController;if(!controller||typeof controller.getSelectedNetworkClient!=='function')throw new Error('NetworkController selected client is unavailable');var client=controller.getSelectedNetworkClient();if(!client||!client.provider||typeof client.provider.request!=='function')throw new Error('selected EVM provider is unavailable');var result=await client.provider.request({method:'eth_blockNumber'});if(typeof result!=='string'||!/^0x[0-9a-f]+$/i.test(result))throw new Error('eth_blockNumber returned an invalid result');return {ok:true,result:result};}catch(error){return {ok:false,error:error&&(error.message||String(error))};}})()";
|
|
14
|
-
const STATUS_SMOKE_RECIPE = `{
|
|
15
|
-
"$schema": "https://farmslot.io/schemas/recipe-v1.schema.json",
|
|
16
|
-
"title": "Mobile runner status smoke",
|
|
17
|
-
"description": "Checks that the installed runner can read Mobile app status before live verification.",
|
|
18
|
-
"workflow": {
|
|
19
|
-
"entry": "status",
|
|
20
|
-
"nodes": {
|
|
21
|
-
"status": { "action": "app.status", "intent": "Read Mobile app status through the runner-owned bridge", "next": "done" },
|
|
22
|
-
"done": { "action": "end", "status": "pass" }
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
`;
|
|
27
15
|
const LIVE_SMOKE_RECIPE = `{
|
|
28
16
|
"$schema": "https://farmslot.io/schemas/recipe-v1.schema.json",
|
|
29
17
|
"title": "Mobile v1 runner live bridge smoke",
|
|
@@ -241,30 +229,21 @@ function mobilePortHolder(port) {
|
|
|
241
229
|
note: "HTTP /status probing is skipped during live verify because the React Native bridge is the authoritative readiness path for Mobile."
|
|
242
230
|
};
|
|
243
231
|
}
|
|
244
|
-
function spawnToLog(logPath, command, args, options = {}) {
|
|
232
|
+
async function spawnToLog(logPath, command, args, options = {}) {
|
|
245
233
|
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
fd,
|
|
258
|
-
timedOut ? `Timed out after ${(options.timeoutMs ?? 0) / 1e3}s: ${[command, ...args].join(" ")}
|
|
259
|
-
` : `${result.error.message}
|
|
260
|
-
`
|
|
261
|
-
);
|
|
262
|
-
return timedOut ? 124 : 127;
|
|
263
|
-
}
|
|
264
|
-
return result.status ?? 1;
|
|
265
|
-
} finally {
|
|
266
|
-
fs.closeSync(fd);
|
|
234
|
+
const result = await spawnScriptStreaming(command, args, options.cwd ?? process.cwd(), {
|
|
235
|
+
env: Object.fromEntries(Object.entries(options.env ?? process.env).filter((entry) => entry[1] !== void 0)),
|
|
236
|
+
timeoutMs: options.timeoutMs,
|
|
237
|
+
forward: "none",
|
|
238
|
+
stage: path.basename(logPath, ".log")
|
|
239
|
+
});
|
|
240
|
+
fs.writeFileSync(logPath, result.output);
|
|
241
|
+
if (result.error) {
|
|
242
|
+
fs.appendFileSync(logPath, `${result.error.message}
|
|
243
|
+
`);
|
|
244
|
+
return 127;
|
|
267
245
|
}
|
|
246
|
+
return result.timedOut ? 124 : result.status;
|
|
268
247
|
}
|
|
269
248
|
function readJsonFile(file) {
|
|
270
249
|
try {
|
|
@@ -372,42 +351,6 @@ async function runMobileVerify(argv, io = defaultIo) {
|
|
|
372
351
|
deviceTarget = platform === "android" ? resolveJsEnvValue(target, "ADB_SERIAL") || resolveJsEnvValue(target, "ANDROID_DEVICE") : resolveJsEnvValue(target, "IOS_SIMULATOR");
|
|
373
352
|
}
|
|
374
353
|
const deviceArgs = deviceTarget ? ["--device", deviceTarget] : [];
|
|
375
|
-
const statusSmokeRecipe = path.join(artifacts, "mobile-status-smoke.recipe.json");
|
|
376
|
-
const precheckLog = path.join(artifacts, "logs/app-status-precheck.log");
|
|
377
|
-
let liveRuntimeOk = false;
|
|
378
|
-
for (let attempt = 1; attempt <= 5; attempt += 1) {
|
|
379
|
-
fs.writeFileSync(statusSmokeRecipe, STATUS_SMOKE_RECIPE);
|
|
380
|
-
const code = spawnToLog(precheckLog, runnerBin, [
|
|
381
|
-
"run",
|
|
382
|
-
statusSmokeRecipe,
|
|
383
|
-
"--adapter",
|
|
384
|
-
"mobile",
|
|
385
|
-
"--target",
|
|
386
|
-
target,
|
|
387
|
-
...deviceArgs,
|
|
388
|
-
"--watcher-port",
|
|
389
|
-
port,
|
|
390
|
-
"--artifacts-dir",
|
|
391
|
-
path.join(artifacts, "runner-status-smoke"),
|
|
392
|
-
"--json"
|
|
393
|
-
], { cwd: target });
|
|
394
|
-
if (code === 0) {
|
|
395
|
-
liveRuntimeOk = true;
|
|
396
|
-
break;
|
|
397
|
-
}
|
|
398
|
-
const deviceNotFound = /"code"[ \t]*:[ \t]*"DEVICE_NOT_FOUND"/u.test(
|
|
399
|
-
fs.readFileSync(precheckLog, "utf8")
|
|
400
|
-
);
|
|
401
|
-
if (attempt < 5 && deviceNotFound) {
|
|
402
|
-
io.err(`Waiting for Mobile device discovery (${attempt}/5): ${deviceTarget || "configured target"}`);
|
|
403
|
-
await sleep(1e3);
|
|
404
|
-
continue;
|
|
405
|
-
}
|
|
406
|
-
break;
|
|
407
|
-
}
|
|
408
|
-
if (!liveRuntimeOk && autoStart) io.err(AUTO_START_REFUSAL);
|
|
409
|
-
checks.push(liveRuntimeOk ? { name: "mobile runtime controllable precheck", status: "pass" } : { name: "mobile runtime controllable precheck", status: "fail", detail: "see logs/app-status-precheck.log" });
|
|
410
|
-
if (!liveRuntimeOk) status = "fail";
|
|
411
354
|
fixtureStatus = mobileFixtureStatus(target);
|
|
412
355
|
fs.writeFileSync(path.join(artifacts, "logs/fixture-status.json"), `${JSON.stringify(fixtureStatus)}
|
|
413
356
|
`);
|
|
@@ -423,20 +366,19 @@ async function runMobileVerify(argv, io = defaultIo) {
|
|
|
423
366
|
portHolder = mobilePortHolder(port);
|
|
424
367
|
fs.writeFileSync(path.join(artifacts, "logs/port-holder.json"), `${JSON.stringify(portHolder)}
|
|
425
368
|
`);
|
|
426
|
-
if (status
|
|
369
|
+
if (!checks.some((check) => check.status === "fail") && portHolder.listening) {
|
|
427
370
|
const waiter = path.join(runnerDir, "adapters/mobile/wait-for-bridge.sh");
|
|
428
371
|
if (!fs.existsSync(waiter)) {
|
|
429
372
|
addNote("Mobile bridge settle skipped: wait-for-bridge.sh not found under the runner adapters; reinstall the runner.");
|
|
430
373
|
} else {
|
|
431
|
-
const waiterCode = spawnToLog(
|
|
374
|
+
const waiterCode = await spawnToLog(
|
|
432
375
|
path.join(artifacts, "logs/wait-for-bridge.log"),
|
|
433
376
|
"bash",
|
|
434
377
|
[waiter, "--target", target, "--port", port, "--platform", platform],
|
|
435
378
|
{
|
|
436
379
|
env: {
|
|
437
380
|
...process.env,
|
|
438
|
-
MOBILE_BRIDGE_READY_TIMEOUT_MS: process.env.MOBILE_BRIDGE_READY_TIMEOUT_MS || "90000"
|
|
439
|
-
MOBILE_BRIDGE_UNRESPONSIVE_GRACE_MS: process.env.MOBILE_BRIDGE_UNRESPONSIVE_GRACE_MS || "90000"
|
|
381
|
+
MOBILE_BRIDGE_READY_TIMEOUT_MS: process.env.MOBILE_BRIDGE_READY_TIMEOUT_MS || "90000"
|
|
440
382
|
}
|
|
441
383
|
}
|
|
442
384
|
);
|
|
@@ -447,34 +389,45 @@ async function runMobileVerify(argv, io = defaultIo) {
|
|
|
447
389
|
fs.writeFileSync(liveSmokeRecipe, LIVE_SMOKE_RECIPE);
|
|
448
390
|
const iosSimulatorResolved = resolveJsEnvValue(target, "IOS_SIMULATOR");
|
|
449
391
|
const adbSerialResolved = resolveJsEnvValue(target, "ADB_SERIAL");
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
392
|
+
let liveSmokeCode = 1;
|
|
393
|
+
for (let attempt = 1; attempt <= 5; attempt += 1) {
|
|
394
|
+
liveSmokeCode = await spawnToLog(
|
|
395
|
+
path.join(artifacts, "logs/runner-live-smoke.log"),
|
|
396
|
+
runnerBin,
|
|
397
|
+
[
|
|
398
|
+
"run",
|
|
399
|
+
liveSmokeRecipe,
|
|
400
|
+
"--adapter",
|
|
401
|
+
"mobile",
|
|
402
|
+
"--target",
|
|
403
|
+
target,
|
|
404
|
+
...deviceArgs,
|
|
405
|
+
"--watcher-port",
|
|
406
|
+
port,
|
|
407
|
+
"--artifacts-dir",
|
|
408
|
+
path.join(artifacts, "runner-live-smoke"),
|
|
409
|
+
"--json"
|
|
410
|
+
],
|
|
411
|
+
{
|
|
412
|
+
cwd: target,
|
|
413
|
+
env: {
|
|
414
|
+
...process.env,
|
|
415
|
+
METAMASK_RECIPE_AUTO_HUD: "0",
|
|
416
|
+
IOS_SIMULATOR: process.env.IOS_SIMULATOR || iosSimulatorResolved,
|
|
417
|
+
ADB_SERIAL: process.env.ADB_SERIAL || adbSerialResolved,
|
|
418
|
+
ANDROID_SERIAL: process.env.ANDROID_SERIAL || adbSerialResolved
|
|
419
|
+
}
|
|
475
420
|
}
|
|
476
|
-
|
|
477
|
-
|
|
421
|
+
);
|
|
422
|
+
if (liveSmokeCode === 0) break;
|
|
423
|
+
const deviceNotFound = /"code"[ \t]*:[ \t]*"DEVICE_NOT_FOUND"/u.test(
|
|
424
|
+
fs.readFileSync(path.join(artifacts, "logs/runner-live-smoke.log"), "utf8")
|
|
425
|
+
);
|
|
426
|
+
if (!deviceNotFound || attempt === 5) break;
|
|
427
|
+
io.err(`Waiting for Mobile device discovery (${attempt}/5): ${deviceTarget || "configured target"}`);
|
|
428
|
+
await sleep(1e3);
|
|
429
|
+
}
|
|
430
|
+
if (liveSmokeCode !== 0 && autoStart) io.err(AUTO_START_REFUSAL);
|
|
478
431
|
if (liveSmokeCode === 0) {
|
|
479
432
|
checks.push({ name: "runner v1 live bridge smoke", status: "pass" });
|
|
480
433
|
} else {
|
|
@@ -489,7 +442,7 @@ async function runMobileVerify(argv, io = defaultIo) {
|
|
|
489
442
|
fs.writeFileSync(evmLog, `Mobile bridge is missing at ${bridge}
|
|
490
443
|
`);
|
|
491
444
|
} else {
|
|
492
|
-
const evmCode = spawnToLog(evmLog, process.execPath, [bridge, "eval-async", EVM_READINESS_EXPRESSION], {
|
|
445
|
+
const evmCode = await spawnToLog(evmLog, process.execPath, [bridge, "eval-async", EVM_READINESS_EXPRESSION], {
|
|
493
446
|
timeoutMs: 25e3,
|
|
494
447
|
env: {
|
|
495
448
|
...process.env,
|