@deeeed/metamask-harness 0.34.0 → 0.34.2
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 +29 -0
- package/adapters/manifest.json +0 -8
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +236 -3
- package/adapters/mobile/bridge-runtime/console-forwarder.cjs +22 -1
- package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +22 -15
- package/adapters/mobile/bridge-runtime/lib/ws-client.cjs +18 -0
- package/adapters/mobile/launch-metro.cjs +6 -4
- package/adapters/mobile/open-device.sh +170 -19
- package/adapters/mobile/start-metro.sh +165 -93
- package/adapters/mobile/stop-metro.sh +1 -0
- package/dist/adapters/mobile/prepare.js +1 -4
- package/dist/adapters/mobile/video-recorder.js +12 -10
- package/dist/adapters.js +134 -6
- package/dist/commands/launch/index.js +9 -0
- package/dist/commands/launch/mobile.js +17 -3
- package/dist/live-adapter-contract.js +10 -3
- package/dist/recipe-security.js +2 -0
- package/dist/runner.js +53 -8
- package/dist/runtime-context.js +1 -0
- package/library/actions/mobile/perps/measure_homepage_visible.mjs +4 -0
- package/library/actions/mobile/perps/performance-capture.mjs +579 -111
- package/library/actions/mobile/perps/perps.mjs +221 -38
- package/library/actions/mobile/perps/prepare_local_snapshot_endpoint.mjs +58 -0
- package/library/actions/mobile/platform/bridge.mjs +102 -8
- package/library/actions/mobile/wallet/ensure_unlocked.mjs +35 -8
- package/library/manifests/mobile.action-manifest.json +243 -511
- package/library/recipes/mobile/perps/performance.homepage.android-cold-disk-cache.recipe.json +20 -2
- package/library/recipes/mobile/perps/performance.homepage.android-cold-no-cache.recipe.json +2 -0
- package/library/recipes/mobile/perps/performance.homepage.cold-position-sample.recipe.json +120 -0
- package/library/recipes/mobile/perps/performance.homepage.ios-background-reconnect.recipe.json +7 -6
- package/library/recipes/mobile/perps/performance.homepage.ios-cold-no-cache.recipe.json +5 -5
- package/package.json +1 -1
- package/adapters/mobile/metro-config.cjs +0 -93
|
@@ -7,7 +7,16 @@ import {
|
|
|
7
7
|
} from "../../recording-target.js";
|
|
8
8
|
const RECORDING_START_TIMEOUT_MS = 5e3;
|
|
9
9
|
const RECORDING_STOP_TIMEOUT_MS = 1e4;
|
|
10
|
-
const RECORDING_TIME_LIMIT_SECONDS =
|
|
10
|
+
const RECORDING_TIME_LIMIT_SECONDS = 0;
|
|
11
|
+
function androidScreenrecordArgs(remotePath) {
|
|
12
|
+
return [
|
|
13
|
+
"shell",
|
|
14
|
+
"screenrecord",
|
|
15
|
+
"--time-limit",
|
|
16
|
+
String(RECORDING_TIME_LIMIT_SECONDS),
|
|
17
|
+
remotePath
|
|
18
|
+
];
|
|
19
|
+
}
|
|
11
20
|
function createAndroidVideoRecorder(serial) {
|
|
12
21
|
return {
|
|
13
22
|
name: "adb-screenrecord",
|
|
@@ -139,15 +148,7 @@ async function startAndroidRecording(serial, request) {
|
|
|
139
148
|
const remotePath = `/sdcard/Download/mm-harness-recipe-${Date.now()}-${process.pid}.mp4`;
|
|
140
149
|
const child = spawn(
|
|
141
150
|
"adb",
|
|
142
|
-
[
|
|
143
|
-
"-s",
|
|
144
|
-
serial,
|
|
145
|
-
"shell",
|
|
146
|
-
"screenrecord",
|
|
147
|
-
"--time-limit",
|
|
148
|
-
String(RECORDING_TIME_LIMIT_SECONDS),
|
|
149
|
-
remotePath
|
|
150
|
-
],
|
|
151
|
+
["-s", serial, ...androidScreenrecordArgs(remotePath)],
|
|
151
152
|
{ stdio: ["ignore", "pipe", "pipe"] }
|
|
152
153
|
);
|
|
153
154
|
const stderr = [];
|
|
@@ -277,6 +278,7 @@ function sleep(durationMs) {
|
|
|
277
278
|
return new Promise((resolve) => setTimeout(resolve, durationMs));
|
|
278
279
|
}
|
|
279
280
|
export {
|
|
281
|
+
androidScreenrecordArgs,
|
|
280
282
|
createAndroidVideoRecorder,
|
|
281
283
|
createIosSimulatorVideoRecorder
|
|
282
284
|
};
|
package/dist/adapters.js
CHANGED
|
@@ -39,6 +39,8 @@ const LIVE_ONLY_PERPS_ACTIONS = /* @__PURE__ */ new Set([
|
|
|
39
39
|
"metamask.perps.ensure_orders",
|
|
40
40
|
"metamask.perps.assert_orders",
|
|
41
41
|
"metamask.perps.clear_performance_caches",
|
|
42
|
+
"metamask.perps.measure_homepage_visible",
|
|
43
|
+
"metamask.perps.prepare_local_snapshot_endpoint",
|
|
42
44
|
"metamask.perps.start_state",
|
|
43
45
|
"metamask.perps.teardown_state",
|
|
44
46
|
"metamask.perps.capture_performance"
|
|
@@ -198,7 +200,11 @@ function createMetaMaskSemanticAdapters(platform, declaredCustomActions = [], pr
|
|
|
198
200
|
"metamask.perps.start_state",
|
|
199
201
|
"metamask.perps.teardown_state",
|
|
200
202
|
"metamask.perps.capture_performance",
|
|
201
|
-
...platform === "mobile" ? [
|
|
203
|
+
...platform === "mobile" ? [
|
|
204
|
+
"metamask.perps.clear_performance_caches",
|
|
205
|
+
"metamask.perps.measure_homepage_visible",
|
|
206
|
+
"metamask.perps.prepare_local_snapshot_endpoint"
|
|
207
|
+
] : [],
|
|
202
208
|
// read_account is core-only: only the headless core adapter implements it.
|
|
203
209
|
...platform === "core" ? ["metamask.perps.read_account"] : []
|
|
204
210
|
];
|
|
@@ -283,6 +289,17 @@ function resolveMobileWaitTarget(payload) {
|
|
|
283
289
|
visibility: expected === "visible" || expected === "hidden" ? "viewport" : "tree"
|
|
284
290
|
};
|
|
285
291
|
}
|
|
292
|
+
const RETRYABLE_MOBILE_WAIT_CODES = /* @__PURE__ */ new Set([
|
|
293
|
+
MOBILE_BRIDGE_ERROR_CODES.NO_TARGET,
|
|
294
|
+
MOBILE_BRIDGE_ERROR_CODES.CDP_TIMEOUT,
|
|
295
|
+
MOBILE_BRIDGE_ERROR_CODES.WS_CLOSED,
|
|
296
|
+
MOBILE_BRIDGE_ERROR_CODES.METRO_UNREACHABLE
|
|
297
|
+
]);
|
|
298
|
+
function isRetryableMobileWaitError(error) {
|
|
299
|
+
return Boolean(
|
|
300
|
+
isRecord(error) && typeof error.code === "string" && RETRYABLE_MOBILE_WAIT_CODES.has(error.code)
|
|
301
|
+
);
|
|
302
|
+
}
|
|
286
303
|
async function waitForMobileTarget(input, payload) {
|
|
287
304
|
const timeoutMs = Number(payload.timeout_ms ?? payload.timeoutMs ?? 1e4);
|
|
288
305
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -308,9 +325,28 @@ async function waitForMobileTarget(input, payload) {
|
|
|
308
325
|
})()` : null;
|
|
309
326
|
let lastResult = null;
|
|
310
327
|
let lastText = null;
|
|
328
|
+
let lastBridgeError = null;
|
|
311
329
|
const expectsAbsent = expected === "absent" || expected === "hidden" || expected === "not_present";
|
|
312
330
|
while (Date.now() <= deadline) {
|
|
313
|
-
const
|
|
331
|
+
const remainingMs = Math.max(1, deadline - Date.now());
|
|
332
|
+
const probeTimeoutMs = Math.min(5e3, remainingMs);
|
|
333
|
+
const probeInput = {
|
|
334
|
+
...input,
|
|
335
|
+
node: {
|
|
336
|
+
...input.node,
|
|
337
|
+
bridge_timeout_ms: probeTimeoutMs,
|
|
338
|
+
cdp_timeout_ms: probeTimeoutMs
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
let result;
|
|
342
|
+
try {
|
|
343
|
+
result = await evalAsync(probeInput, queryExpression);
|
|
344
|
+
} catch (error) {
|
|
345
|
+
if (!isRetryableMobileWaitError(error)) throw error;
|
|
346
|
+
lastBridgeError = error;
|
|
347
|
+
await sleep(250);
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
314
350
|
if (!isRecord(result) || result.unsupported === true) {
|
|
315
351
|
throw new Error(
|
|
316
352
|
"Mobile ui.wait_for requires the current __AGENTIC__.queryUiTarget bridge for text-only or viewport-visible assertions."
|
|
@@ -321,7 +357,14 @@ async function waitForMobileTarget(input, payload) {
|
|
|
321
357
|
const visible = result.visible === true;
|
|
322
358
|
let textMatched = !expectedText || textMatch === "contains";
|
|
323
359
|
if (expectedText && textMatch === "exact" && textExpression) {
|
|
324
|
-
|
|
360
|
+
try {
|
|
361
|
+
lastText = await evalSync(probeInput, textExpression);
|
|
362
|
+
} catch (error) {
|
|
363
|
+
if (!isRetryableMobileWaitError(error)) throw error;
|
|
364
|
+
lastBridgeError = error;
|
|
365
|
+
await sleep(250);
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
325
368
|
textMatched = traceText(lastText) === expectedText;
|
|
326
369
|
}
|
|
327
370
|
const matched = expected === "visible" ? visible && textMatched : expected === "hidden" ? !visible : expectsAbsent ? !present : present && textMatched;
|
|
@@ -344,8 +387,9 @@ async function waitForMobileTarget(input, payload) {
|
|
|
344
387
|
expectedText ? `text ${textMatch} ${JSON.stringify(expectedText)}` : ""
|
|
345
388
|
].filter(Boolean).join(" with ");
|
|
346
389
|
const exactReason = textMatch === "exact" ? `; last text=${JSON.stringify(lastText)}` : "";
|
|
390
|
+
const bridgeReason = lastBridgeError ? `; last bridge error=${traceText(lastBridgeError)}` : "";
|
|
347
391
|
throw new Error(
|
|
348
|
-
`Timed out waiting for mobile ${target} to be ${expected}; last result=${JSON.stringify(lastResult)}${exactReason}.`
|
|
392
|
+
`Timed out waiting for mobile ${target} to be ${expected}; last result=${JSON.stringify(lastResult)}${exactReason}${bridgeReason}.`
|
|
349
393
|
);
|
|
350
394
|
}
|
|
351
395
|
const MOBILE_BRIDGE_HANDLERS = {
|
|
@@ -427,6 +471,48 @@ async function handleMobileScroll(payload, context) {
|
|
|
427
471
|
const testId = optionalScalarText(payload.test_id ?? payload.testID, "ui.scroll.test_id");
|
|
428
472
|
const offset = scalarText(payload.offset ?? payload.delta_y ?? payload.deltaY, "ui.scroll.offset", "600");
|
|
429
473
|
const intoView = payload.scroll_into_view === true || payload.into_view === true;
|
|
474
|
+
const measureFromTestId = optionalScalarText(
|
|
475
|
+
payload.measure_from_test_id ?? payload.measureFromTestId,
|
|
476
|
+
"ui.scroll.measure_from_test_id"
|
|
477
|
+
);
|
|
478
|
+
const measureTargetTestId = optionalScalarText(
|
|
479
|
+
payload.measure_target_test_id ?? payload.measureTargetTestId,
|
|
480
|
+
"ui.scroll.measure_target_test_id"
|
|
481
|
+
);
|
|
482
|
+
const measureTargetText = optionalScalarText(
|
|
483
|
+
payload.measure_target_text ?? payload.measureTargetText,
|
|
484
|
+
"ui.scroll.measure_target_text"
|
|
485
|
+
);
|
|
486
|
+
const measureTargetCount = Number(Boolean(measureTargetTestId)) + Number(Boolean(measureTargetText));
|
|
487
|
+
const measureRequiredPresentTestId = optionalScalarText(
|
|
488
|
+
payload.measure_required_present_test_id ?? payload.measureRequiredPresentTestId,
|
|
489
|
+
"ui.scroll.measure_required_present_test_id"
|
|
490
|
+
);
|
|
491
|
+
if (Boolean(measureFromTestId) !== (measureTargetCount === 1)) {
|
|
492
|
+
throw new Error(
|
|
493
|
+
"ui.scroll visible-transition measurement requires measure_from_test_id and exactly one of measure_target_test_id or measure_target_text."
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
if (measureFromTestId && measureTargetCount === 1) {
|
|
497
|
+
if (intoView) {
|
|
498
|
+
throw new Error(
|
|
499
|
+
"ui.scroll visible-transition measurement uses an explicit offset and does not support scroll_into_view."
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
return measureMobileScrollTransition(input, {
|
|
503
|
+
animated: payload.animated === true,
|
|
504
|
+
fromTestId: measureFromTestId,
|
|
505
|
+
offset: Number(offset),
|
|
506
|
+
...measureTargetTestId ? { targetTestId: measureTargetTestId } : {},
|
|
507
|
+
...measureTargetText ? { targetText: measureTargetText } : {},
|
|
508
|
+
...measureRequiredPresentTestId ? { requiredPresentTestId: measureRequiredPresentTestId } : {},
|
|
509
|
+
timeoutMs: Math.max(
|
|
510
|
+
1,
|
|
511
|
+
Number(payload.timeout_ms ?? payload.timeoutMs ?? 3e4)
|
|
512
|
+
),
|
|
513
|
+
...testId ? { scrollTestId: testId } : {}
|
|
514
|
+
});
|
|
515
|
+
}
|
|
430
516
|
if (testId && intoView) {
|
|
431
517
|
const before = await queryMobileViewportTarget(input, testId);
|
|
432
518
|
if (before.visible === true) {
|
|
@@ -448,12 +534,23 @@ async function handleMobileScroll(payload, context) {
|
|
|
448
534
|
...intoView ? ["--into-view"] : [],
|
|
449
535
|
animatedFlag(payload)
|
|
450
536
|
] : ["scroll-view", "--offset", offset, animatedFlag(payload)];
|
|
451
|
-
const
|
|
537
|
+
const timeoutMs = Math.max(0, Number(payload.timeout_ms ?? payload.timeoutMs ?? 5e3));
|
|
538
|
+
const deadline = Date.now() + timeoutMs;
|
|
539
|
+
let result;
|
|
540
|
+
while (true) {
|
|
541
|
+
try {
|
|
542
|
+
result = await bridgeCommand(input, args);
|
|
543
|
+
break;
|
|
544
|
+
} catch (error) {
|
|
545
|
+
if (!testId || Date.now() >= deadline) throw error;
|
|
546
|
+
await sleep(100);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
452
549
|
if (testId && intoView) {
|
|
453
550
|
const after = await waitForMobileViewportTarget(
|
|
454
551
|
input,
|
|
455
552
|
testId,
|
|
456
|
-
|
|
553
|
+
timeoutMs
|
|
457
554
|
);
|
|
458
555
|
if (after.visible !== true) {
|
|
459
556
|
throw new Error(
|
|
@@ -466,6 +563,36 @@ async function handleMobileScroll(payload, context) {
|
|
|
466
563
|
intoView
|
|
467
564
|
};
|
|
468
565
|
}
|
|
566
|
+
async function measureMobileScrollTransition(input, options) {
|
|
567
|
+
const result = await bridgeCommand(
|
|
568
|
+
{
|
|
569
|
+
...input,
|
|
570
|
+
node: {
|
|
571
|
+
...input.node,
|
|
572
|
+
bridge_timeout_ms: options.timeoutMs + 5e3,
|
|
573
|
+
cdp_timeout_ms: options.timeoutMs + 5e3
|
|
574
|
+
}
|
|
575
|
+
},
|
|
576
|
+
["measure-scroll-transition", JSON.stringify(options)]
|
|
577
|
+
);
|
|
578
|
+
if (!isRecord(result) || result.unsupported === true) {
|
|
579
|
+
throw new Error(
|
|
580
|
+
"ui.scroll visible-transition measurement requires queryUiTarget and scrollView on the current mobile bridge."
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
if (result.ok !== true) {
|
|
584
|
+
throw new Error(
|
|
585
|
+
`ui.scroll visible-transition measurement failed: ${traceText(result.error ?? result)}.`
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
return {
|
|
589
|
+
...result,
|
|
590
|
+
measureFromTestId: options.fromTestId,
|
|
591
|
+
...options.targetTestId ? { measureTargetTestId: options.targetTestId } : {},
|
|
592
|
+
...options.targetText ? { measureTargetText: options.targetText } : {},
|
|
593
|
+
...options.requiredPresentTestId ? { measureRequiredPresentTestId: options.requiredPresentTestId } : {}
|
|
594
|
+
};
|
|
595
|
+
}
|
|
469
596
|
async function queryMobileViewportTarget(input, testId) {
|
|
470
597
|
const expression = `(function(){
|
|
471
598
|
const api = globalThis.__AGENTIC__;
|
|
@@ -851,6 +978,7 @@ export {
|
|
|
851
978
|
createMetaMaskUiTransport,
|
|
852
979
|
hideMobileHudOnTeardown,
|
|
853
980
|
isMobileHudLifecycleSkip,
|
|
981
|
+
isRetryableMobileWaitError,
|
|
854
982
|
normalizeUiWaitNode,
|
|
855
983
|
resolveMobileWaitTarget
|
|
856
984
|
};
|
|
@@ -316,6 +316,15 @@ async function handleLaunchLocked(argv, stream) {
|
|
|
316
316
|
originalError: attempt.output.trim() || void 0
|
|
317
317
|
});
|
|
318
318
|
}
|
|
319
|
+
if (adapter === "mobile" && tier === "build" && /open-device: (?:Android|iOS) native build failed/u.test(attempt.output)) {
|
|
320
|
+
return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
|
|
321
|
+
code: "MOBILE_NATIVE_BUILD_FAILED",
|
|
322
|
+
message: "the selected Mobile native build failed; refusing to repeat the expensive build as a Metro recovery.",
|
|
323
|
+
recoverable: false,
|
|
324
|
+
exitCode: EXIT.runtime,
|
|
325
|
+
originalError: attempt.output.trim() || void 0
|
|
326
|
+
});
|
|
327
|
+
}
|
|
319
328
|
const bound = checkHealBounds(target, attempt.output, state);
|
|
320
329
|
if (bound !== null) {
|
|
321
330
|
return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
|
|
@@ -1,18 +1,32 @@
|
|
|
1
1
|
import { prepareMobile } from "../../adapters/mobile/prepare.js";
|
|
2
|
+
import {
|
|
3
|
+
mobileSourceCheck,
|
|
4
|
+
recordMobileSourceBaseline
|
|
5
|
+
} from "../../adapters/mobile/source-freshness.js";
|
|
2
6
|
import { ensureHarnessFresh } from "../../adapters/harness-freshness.js";
|
|
7
|
+
import { EXIT } from "../shared.js";
|
|
3
8
|
async function launchMobile(target, mobileTarget, tier, json, restartApp = false, clearMetro = false) {
|
|
4
9
|
await ensureHarnessFresh(target, "mobile");
|
|
5
10
|
const platform = mobileTarget ?? "ios";
|
|
6
11
|
const watcherPort = process.env.WATCHER_PORT ? parseInt(process.env.WATCHER_PORT, 10) : void 0;
|
|
7
|
-
const preflightMode = tier === "build" ? "
|
|
8
|
-
|
|
12
|
+
const preflightMode = tier === "build" ? "rebuild-native" : "fast";
|
|
13
|
+
const source = mobileSourceCheck(target);
|
|
14
|
+
const result = await prepareMobile(target, {
|
|
9
15
|
platform,
|
|
10
16
|
json,
|
|
11
17
|
watcherPort,
|
|
12
18
|
preflightMode,
|
|
13
|
-
restartApp,
|
|
19
|
+
restartApp: restartApp || source.status !== "current",
|
|
14
20
|
clearMetro
|
|
15
21
|
});
|
|
22
|
+
if (result.status !== 0) return result;
|
|
23
|
+
if (!recordMobileSourceBaseline(target, source.fingerprint)) {
|
|
24
|
+
const message = "Mobile source changed while the app was launching; the loaded-source baseline was not recorded.\n Next: re-run mm-harness launch.";
|
|
25
|
+
if (!json) process.stderr.write(`${message}
|
|
26
|
+
`);
|
|
27
|
+
return { status: EXIT.runtime, output: message };
|
|
28
|
+
}
|
|
29
|
+
return result;
|
|
16
30
|
}
|
|
17
31
|
export {
|
|
18
32
|
launchMobile
|
|
@@ -352,9 +352,16 @@ function liveAdapterProcessTimeoutMs(node) {
|
|
|
352
352
|
}
|
|
353
353
|
const settleMs = Number(node.settle_ms);
|
|
354
354
|
const settleAllowance = Number.isFinite(settleMs) && settleMs > 0 ? settleMs : 0;
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
355
|
+
const actionTimeouts = [
|
|
356
|
+
node.target_timeout_ms,
|
|
357
|
+
node.unlock_timeout_ms,
|
|
358
|
+
node.timeout_ms
|
|
359
|
+
].map(Number).filter((value) => Number.isFinite(value) && value > 0);
|
|
360
|
+
if (actionTimeouts.length > 0) {
|
|
361
|
+
return Math.max(
|
|
362
|
+
6e4,
|
|
363
|
+
actionTimeouts.reduce((total, value) => total + value, 0) + settleAllowance + 5e3
|
|
364
|
+
);
|
|
358
365
|
}
|
|
359
366
|
return 6e4 + settleAllowance;
|
|
360
367
|
}
|
package/dist/recipe-security.js
CHANGED
|
@@ -29,6 +29,8 @@ const APP_MUTATION_CUSTOM_ACTIONS = /* @__PURE__ */ new Set([
|
|
|
29
29
|
"metamask.perps.ensure_positions",
|
|
30
30
|
"metamask.perps.ensure_orders",
|
|
31
31
|
"metamask.perps.clear_performance_caches",
|
|
32
|
+
"metamask.perps.measure_homepage_visible",
|
|
33
|
+
"metamask.perps.prepare_local_snapshot_endpoint",
|
|
32
34
|
"metamask.perps.start_state",
|
|
33
35
|
"metamask.perps.teardown_state",
|
|
34
36
|
// Consent changes wallet state and is therefore an app mutation.
|
package/dist/runner.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { execSync } from "node:child_process";
|
|
1
|
+
import { execFileSync, execSync } from "node:child_process";
|
|
2
2
|
import { OFFICIAL_RECIPE_ACTIONS } from "@farmslot/protocol";
|
|
3
3
|
import { createMetaMaskAdapters, createMetaMaskUiTransport } from "./adapters.js";
|
|
4
4
|
import { bridgeCommand } from "../library/actions/mobile/platform/bridge.mjs";
|
|
@@ -239,6 +239,8 @@ function isAutomaticHudProgress(action, node, nodeId) {
|
|
|
239
239
|
function mobileSourceAwareLifecycleAdapters(adapter, lifecycle, readinessProbe = probeMobileLifecycleRuntime, sourceFreshness = {
|
|
240
240
|
fingerprint: mobileSourceFingerprint,
|
|
241
241
|
record: recordMobileSourceBaseline
|
|
242
|
+
}, processIdentity = {
|
|
243
|
+
readIosPid: readIosAppPid
|
|
242
244
|
}) {
|
|
243
245
|
if (adapter !== "mobile") return lifecycle;
|
|
244
246
|
return lifecycle.map((entry) => ({
|
|
@@ -246,6 +248,8 @@ function mobileSourceAwareLifecycleAdapters(adapter, lifecycle, readinessProbe =
|
|
|
246
248
|
async execute(node, context) {
|
|
247
249
|
const command = node.command ?? node.event ?? node.state;
|
|
248
250
|
const reloadsSource = command === "launch" || command === "foreground" || command === "restart";
|
|
251
|
+
const checksIosContinuity = command === "foreground" && !isAndroidLifecycle(node, context.env ?? {});
|
|
252
|
+
const processBefore = checksIosContinuity ? processIdentity.readIosPid(node, context) : void 0;
|
|
249
253
|
const fingerprint = reloadsSource ? sourceFreshness.fingerprint(context.projectRoot) : void 0;
|
|
250
254
|
const androidRestart = command === "restart" && isAndroidLifecycle(node, context.env ?? {});
|
|
251
255
|
const initialNode = androidRestart ? { ...node, settle_ms: 0 } : node;
|
|
@@ -261,6 +265,25 @@ function mobileSourceAwareLifecycleAdapters(adapter, lifecycle, readinessProbe =
|
|
|
261
265
|
if (reloadsSource) {
|
|
262
266
|
await readinessProbe(node, context);
|
|
263
267
|
}
|
|
268
|
+
if (checksIosContinuity) {
|
|
269
|
+
const processAfter = processIdentity.readIosPid(node, context);
|
|
270
|
+
const actualTransition = processBefore !== null && processBefore === processAfter ? "foreground_resume" : processAfter !== null ? "cold_relaunch" : "unknown";
|
|
271
|
+
const resultRecord = asRecord(result);
|
|
272
|
+
result = {
|
|
273
|
+
...resultRecord,
|
|
274
|
+
output: {
|
|
275
|
+
...asRecord(resultRecord.output),
|
|
276
|
+
actualTransition,
|
|
277
|
+
processBefore,
|
|
278
|
+
processAfter
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
if (node.require_process_continuity === true && actualTransition !== "foreground_resume") {
|
|
282
|
+
throw new Error(
|
|
283
|
+
`iOS foreground did not preserve the app process (actualTransition=${actualTransition}, before=${String(processBefore)}, after=${String(processAfter)}); this lifecycle sample is a cold relaunch, not a reconnect.`
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
264
287
|
if (fingerprint !== void 0 && !sourceFreshness.record(context.projectRoot, fingerprint)) {
|
|
265
288
|
throw new Error(
|
|
266
289
|
"Mobile source changed while the app was reloading; the loaded-source baseline was not recorded. Next: retry app.lifecycle restart."
|
|
@@ -270,6 +293,26 @@ function mobileSourceAwareLifecycleAdapters(adapter, lifecycle, readinessProbe =
|
|
|
270
293
|
}
|
|
271
294
|
}));
|
|
272
295
|
}
|
|
296
|
+
function readIosAppPid(node, context) {
|
|
297
|
+
const env = context.env ?? {};
|
|
298
|
+
const device = node.simulator ?? node.ios_simulator ?? env.SIM_UDID ?? env.IOS_SIMULATOR ?? process.env.SIM_UDID ?? process.env.IOS_SIMULATOR;
|
|
299
|
+
const bundleId = node.bundle_id ?? node.bundleId ?? env.IOS_BUNDLE_ID ?? process.env.IOS_BUNDLE_ID ?? "io.metamask.MetaMask";
|
|
300
|
+
if (typeof device !== "string" || typeof bundleId !== "string") return null;
|
|
301
|
+
try {
|
|
302
|
+
const output = execFileSync(
|
|
303
|
+
"xcrun",
|
|
304
|
+
["simctl", "spawn", device, "launchctl", "list"],
|
|
305
|
+
{ encoding: "utf8", timeout: 5e3 }
|
|
306
|
+
);
|
|
307
|
+
const labelPrefix = `UIKitApplication:${bundleId}[`;
|
|
308
|
+
const line = output.split("\n").find((candidate) => candidate.includes(labelPrefix));
|
|
309
|
+
if (!line) return null;
|
|
310
|
+
const pid = Number.parseInt(line.trim().split(/\s+/u)[0] ?? "", 10);
|
|
311
|
+
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
312
|
+
} catch {
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
273
316
|
function isAndroidLifecycle(node, env) {
|
|
274
317
|
const platform = node.platform ?? env.PLATFORM ?? process.env.PLATFORM;
|
|
275
318
|
if (platform === "android") return true;
|
|
@@ -299,10 +342,9 @@ function mergeAndroidRestartResults(initialResult, foregroundResult) {
|
|
|
299
342
|
}
|
|
300
343
|
async function probeMobileLifecycleRuntime(node, context) {
|
|
301
344
|
const command = node.command ?? node.event ?? node.state;
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
node.runtime_ready_timeout_ms
|
|
305
|
-
defaultTimeoutMs
|
|
345
|
+
const timeoutMs = resolveMobileLifecycleReadinessTimeout(
|
|
346
|
+
command,
|
|
347
|
+
node.runtime_ready_timeout_ms
|
|
306
348
|
);
|
|
307
349
|
const deadline = Date.now() + timeoutMs;
|
|
308
350
|
let lastError;
|
|
@@ -328,8 +370,10 @@ async function probeMobileLifecycleRuntime(node, context) {
|
|
|
328
370
|
`Mobile lifecycle ${String(command)} did not expose a usable pinned __AGENTIC__ runtime within ${timeoutMs}ms: ${lastError instanceof Error ? lastError.message : String(lastError)}`
|
|
329
371
|
);
|
|
330
372
|
}
|
|
331
|
-
function
|
|
332
|
-
if (value === void 0)
|
|
373
|
+
function resolveMobileLifecycleReadinessTimeout(command, value) {
|
|
374
|
+
if (value === void 0) {
|
|
375
|
+
return command === "launch" || command === "restart" || command === "foreground" ? 6e4 : 3e4;
|
|
376
|
+
}
|
|
333
377
|
const timeout = Number(value);
|
|
334
378
|
if (!Number.isInteger(timeout) || timeout < 1e3 || timeout > 12e4) {
|
|
335
379
|
throw new Error(
|
|
@@ -364,5 +408,6 @@ export {
|
|
|
364
408
|
createMetaMaskMobileRunner,
|
|
365
409
|
createMetaMaskRunner,
|
|
366
410
|
isAutomaticHudProgress,
|
|
367
|
-
mobileSourceAwareLifecycleAdapters
|
|
411
|
+
mobileSourceAwareLifecycleAdapters,
|
|
412
|
+
resolveMobileLifecycleReadinessTimeout
|
|
368
413
|
};
|
package/dist/runtime-context.js
CHANGED
|
@@ -244,6 +244,7 @@ function persistExplicitDevicePin(target, device) {
|
|
|
244
244
|
return;
|
|
245
245
|
}
|
|
246
246
|
const next = { ...existing };
|
|
247
|
+
next.platform = device.platform;
|
|
247
248
|
if (device.platform === "ios") {
|
|
248
249
|
const simulatorName = device.name ? device.name.replace(/_/gu, " ").trim() : device.id;
|
|
249
250
|
next.simulator = simulatorName;
|