@deeeed/metamask-harness 0.67.0 → 0.67.1
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 +11 -0
- package/adapters/mobile/open-device.sh +53 -5
- package/adapters/mobile/wait-for-bridge.cjs +28 -81
- package/dist/adapters/mobile/prepare.js +0 -1
- package/dist/adapters/mobile/verify.js +1 -2
- package/dist/commands/fixtures.js +3 -2
- package/dist/commands/launch/index.js +20 -0
- package/docs/QA.md +12 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.67.1 - 2026-09-29
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- 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.
|
|
10
|
+
- 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.
|
|
11
|
+
|
|
12
|
+
### Removed
|
|
13
|
+
|
|
14
|
+
- 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.
|
|
15
|
+
|
|
5
16
|
## 0.67.0 - 2026-09-28
|
|
6
17
|
|
|
7
18
|
### Fixed
|
|
@@ -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 \
|
|
@@ -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,
|
|
@@ -435,8 +435,7 @@ async function runMobileVerify(argv, io = defaultIo) {
|
|
|
435
435
|
{
|
|
436
436
|
env: {
|
|
437
437
|
...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"
|
|
438
|
+
MOBILE_BRIDGE_READY_TIMEOUT_MS: process.env.MOBILE_BRIDGE_READY_TIMEOUT_MS || "90000"
|
|
440
439
|
}
|
|
441
440
|
}
|
|
442
441
|
);
|
|
@@ -12,6 +12,7 @@ import { FeatureFlagSpecError, fixtureFeatureFlags, parseRemoteFlagSpec, sameFea
|
|
|
12
12
|
import { readExtensionPinnedFlags } from "../adapters/extension/feature-flags.js";
|
|
13
13
|
const FIXTURES_BOOLEANS = /* @__PURE__ */ new Set(["dev", "force", "json"]);
|
|
14
14
|
const FIXTURE_STREAM_TIMEOUT_MS = 12e4;
|
|
15
|
+
const FIXTURE_BRIDGE_READY_TIMEOUT_MS = 18e4;
|
|
15
16
|
const MOBILE_SETUP_WALLET_TIMEOUT_MS = 36e4;
|
|
16
17
|
async function handleFixtures(argv) {
|
|
17
18
|
const { positional, options } = parseFlags(argv, FIXTURES_BOOLEANS);
|
|
@@ -251,9 +252,9 @@ async function handleFixturesLocked(argv) {
|
|
|
251
252
|
const waitForBridge = path.join(runnerDir, "adapters/mobile/wait-for-bridge.sh");
|
|
252
253
|
const waitResult = await spawnScriptStreaming(
|
|
253
254
|
waitForBridge,
|
|
254
|
-
["--target", target, "--port", port, "--platform", platform],
|
|
255
|
+
["--target", target, "--port", port, "--platform", platform, "--timeout-ms", String(FIXTURE_BRIDGE_READY_TIMEOUT_MS)],
|
|
255
256
|
target,
|
|
256
|
-
{ timeoutMs:
|
|
257
|
+
{ timeoutMs: FIXTURE_BRIDGE_READY_TIMEOUT_MS + 1e4 }
|
|
257
258
|
);
|
|
258
259
|
if (waitResult.status !== 0) {
|
|
259
260
|
resetError = new Error("Mobile bridge did not return after app data reset.");
|
|
@@ -381,6 +381,26 @@ async function handleLaunchLocked(argv, stream) {
|
|
|
381
381
|
originalError: attempt.output.trim() || void 0
|
|
382
382
|
});
|
|
383
383
|
}
|
|
384
|
+
if (adapter === "mobile" && attempt.output.includes("open-device: Android device is locked.")) {
|
|
385
|
+
return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
|
|
386
|
+
code: "MOBILE_DEVICE_LOCKED",
|
|
387
|
+
message: "the selected Android device is locked.",
|
|
388
|
+
recoverable: false,
|
|
389
|
+
userAction: "Unlock the selected Android device, then retry the launch.",
|
|
390
|
+
exitCode: EXIT.runtime,
|
|
391
|
+
originalError: attempt.output.trim()
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
if (adapter === "mobile" && /open-device: Android (?:lock state unavailable|display preparation failed)\./u.test(attempt.output)) {
|
|
395
|
+
return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
|
|
396
|
+
code: "MOBILE_DEVICE_UI_UNAVAILABLE",
|
|
397
|
+
message: "the selected Android display could not be prepared.",
|
|
398
|
+
recoverable: false,
|
|
399
|
+
userAction: "Check adb access and the selected device screen, then retry the launch.",
|
|
400
|
+
exitCode: EXIT.runtime,
|
|
401
|
+
originalError: attempt.output.trim()
|
|
402
|
+
});
|
|
403
|
+
}
|
|
384
404
|
if (heal === "off") {
|
|
385
405
|
return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
|
|
386
406
|
code: "LAUNCH_FAILED",
|
package/docs/QA.md
CHANGED
|
@@ -105,6 +105,18 @@ mm-harness debug
|
|
|
105
105
|
|
|
106
106
|
## Mobile
|
|
107
107
|
|
|
108
|
+
Physical Android devices must be awake and unlocked. Launch wakes the selected
|
|
109
|
+
device, requests keyguard dismissal and checks the current user's lock state.
|
|
110
|
+
`MOBILE_DEVICE_LOCKED` requires a human unlock; `MOBILE_DEVICE_UI_UNAVAILABLE`
|
|
111
|
+
means the display or lock-state check failed. These prerequisites stop launch
|
|
112
|
+
without an automatic recovery restart.
|
|
113
|
+
|
|
114
|
+
For dedicated powered QA devices, configure stay-awake for the power sources
|
|
115
|
+
actually reported by Android. An ADB-connected phone can report AC power, so a
|
|
116
|
+
USB-only setting may still allow sleep. `adb -s <serial> shell svc power stayon true`
|
|
117
|
+
enables stay-awake on external power. Record the previous setting before changing
|
|
118
|
+
it, and restore it when returning a device to ordinary use.
|
|
119
|
+
|
|
108
120
|
```bash
|
|
109
121
|
cd /path/to/metamask-mobile
|
|
110
122
|
mm-harness provision runway ios
|