@deeeed/metamask-harness 0.50.3 → 0.50.5
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 +13 -0
- package/adapters/extension/ensure-browser.sh +5 -2
- package/adapters/extension/launch-browser.cjs +10 -8
- package/adapters/extension/lib/macos-focus.cjs +10 -1
- package/adapters/extension/live.sh +0 -2
- package/dist/adapters/extension/ensure-ready.js +1 -1
- package/dist/adapters/extension/runtime-decision.js +2 -2
- package/dist/adapters/extension/runtime.js +23 -13
- package/dist/commands/launch/index.js +3 -3
- package/library/actions/mobile/perps/read-visible-state-loop.mjs +1 -1
- package/library/actions/mobile/platform/observe-ui.mjs +17 -0
- package/library/actions/shared/perps/visible-state.mjs +11 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.50.5 - 2026-09-10
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Avoid disabling and re-enabling the Extension after a fresh browser launch, which could cause `No SW` storage errors in reused wallet profiles. Keep build handoff for updates in an already-running browser.
|
|
10
|
+
- Keep the previously focused macOS app in front while Chrome starts, instead of restoring only after CDP is up. Do not let caffeinate declare user activity during that launch.
|
|
11
|
+
|
|
12
|
+
## 0.50.4 - 2026-09-10
|
|
13
|
+
|
|
14
|
+
- Distinguish Extension startup/provider absence from attempted RPC failures and preserve diagnosed failed tabs during verification.
|
|
15
|
+
- Bind Mobile native Perps observations to a stable development navigation route when available, and include route/screen context in surface errors.
|
|
16
|
+
- Add an interactive workflow map and sourced template previews to the human tutorial, connecting tasks, skills, checklists, recipes, evidence, and optional Farmslot coordination.
|
|
17
|
+
|
|
5
18
|
## 0.50.3 - 2026-09-09
|
|
6
19
|
|
|
7
20
|
- Recognize enabled form controls through recipe-harness 0.16.1; report Extension startup crashes without reload-based RPC probes or automatic relaunch recovery.
|
|
@@ -301,7 +301,7 @@ const {
|
|
|
301
301
|
runtimeIdentityArgs,
|
|
302
302
|
writeRuntimeIdentity,
|
|
303
303
|
} = require(path.join('${SCRIPT_DIR}', 'lib/chrome-args.cjs'));
|
|
304
|
-
const { captureMacFrontmostProcess,
|
|
304
|
+
const { captureMacFrontmostProcess, preserveMacFrontmostProcess } = require(path.join('${SCRIPT_DIR}', 'lib/macos-focus.cjs'));
|
|
305
305
|
|
|
306
306
|
const SLOT_ID = '${SLOT_ID}';
|
|
307
307
|
const AGENT_DIR = '${AGENT_DIR}';
|
|
@@ -345,7 +345,9 @@ const resumeWebpack = () => {
|
|
|
345
345
|
|
|
346
346
|
const chromiumApp = path.dirname(path.dirname(path.dirname(chromium.executablePath())));
|
|
347
347
|
const previousFrontmostPid = captureMacFrontmostProcess();
|
|
348
|
+
const holdOperatorFocus = () => preserveMacFrontmostProcess(previousFrontmostPid);
|
|
348
349
|
execFileSync('open', ['-g', '-n', '-a', chromiumApp, '--args', ...args], { stdio: 'ignore' });
|
|
350
|
+
holdOperatorFocus();
|
|
349
351
|
for (let i = 0; i < 60; i++) {
|
|
350
352
|
try {
|
|
351
353
|
const version = await new Promise((resolve, reject) => {
|
|
@@ -358,6 +360,7 @@ const resumeWebpack = () => {
|
|
|
358
360
|
});
|
|
359
361
|
if (version) break;
|
|
360
362
|
} catch {}
|
|
363
|
+
holdOperatorFocus();
|
|
361
364
|
await new Promise(r => setTimeout(r, 1000));
|
|
362
365
|
}
|
|
363
366
|
const browser = await chromium.connectOverCDP('http://127.0.0.1:' + CDP_PORT);
|
|
@@ -498,7 +501,7 @@ const resumeWebpack = () => {
|
|
|
498
501
|
if (process.env.MM_HARNESS_FOCUS_BROWSER === '1') {
|
|
499
502
|
await page.bringToFront().catch(() => {});
|
|
500
503
|
} else {
|
|
501
|
-
|
|
504
|
+
holdOperatorFocus();
|
|
502
505
|
}
|
|
503
506
|
|
|
504
507
|
fs.writeFileSync(path.join(AGENT_DIR, 'extension.id'), extId);
|
|
@@ -42,7 +42,7 @@ const {
|
|
|
42
42
|
validationLaunchQuarantineError,
|
|
43
43
|
writeRuntimeIdentity,
|
|
44
44
|
} = require('./lib/chrome-args.cjs');
|
|
45
|
-
const { captureMacFrontmostProcess,
|
|
45
|
+
const { captureMacFrontmostProcess, preserveMacFrontmostProcess } = require('./lib/macos-focus.cjs');
|
|
46
46
|
|
|
47
47
|
if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
|
48
48
|
console.log(
|
|
@@ -165,6 +165,7 @@ const chromeArgs = [
|
|
|
165
165
|
];
|
|
166
166
|
const logFd = fs.openSync(args['chrome-log'], 'a');
|
|
167
167
|
const previousFrontmostPid = captureMacFrontmostProcess();
|
|
168
|
+
const holdOperatorFocus = () => preserveMacFrontmostProcess(previousFrontmostPid);
|
|
168
169
|
let browserPid;
|
|
169
170
|
try {
|
|
170
171
|
const application = process.platform === 'darwin' ? macApplicationForExecutable(args['chrome-bin']) : null;
|
|
@@ -174,7 +175,8 @@ try {
|
|
|
174
175
|
env: sanitizedChildEnv(),
|
|
175
176
|
stdio: ['ignore', logFd, logFd],
|
|
176
177
|
});
|
|
177
|
-
|
|
178
|
+
holdOperatorFocus();
|
|
179
|
+
browserPid = waitForOwnedCdpPid(cdpPort, args.profile, args['extension-dir'], holdOperatorFocus);
|
|
178
180
|
if (browserPid === null) {
|
|
179
181
|
throw new Error(`Chrome launched but did not expose CDP on 127.0.0.1:${cdpPort}`);
|
|
180
182
|
}
|
|
@@ -186,7 +188,8 @@ try {
|
|
|
186
188
|
stdio: ['ignore', logFd, logFd],
|
|
187
189
|
});
|
|
188
190
|
child.unref();
|
|
189
|
-
|
|
191
|
+
holdOperatorFocus();
|
|
192
|
+
browserPid = waitForOwnedCdpPid(cdpPort, args.profile, args['extension-dir'], holdOperatorFocus);
|
|
190
193
|
if (browserPid === null) {
|
|
191
194
|
if (child.pid) terminatePids([child.pid]);
|
|
192
195
|
throw new Error(
|
|
@@ -220,15 +223,13 @@ try {
|
|
|
220
223
|
);
|
|
221
224
|
}
|
|
222
225
|
if (process.platform === 'darwin') {
|
|
223
|
-
const wake = spawn('caffeinate', ['-
|
|
226
|
+
const wake = spawn('caffeinate', ['-dims', '-w', String(browserPid)], {
|
|
224
227
|
detached: true,
|
|
225
228
|
stdio: 'ignore',
|
|
226
229
|
});
|
|
227
230
|
wake.unref();
|
|
228
231
|
}
|
|
229
|
-
|
|
230
|
-
restoreMacFrontmostProcess(previousFrontmostPid);
|
|
231
|
-
}
|
|
232
|
+
holdOperatorFocus();
|
|
232
233
|
|
|
233
234
|
function sanitizedChildEnv() {
|
|
234
235
|
return {
|
|
@@ -264,12 +265,13 @@ function macApplicationForExecutable(executable) {
|
|
|
264
265
|
return null;
|
|
265
266
|
}
|
|
266
267
|
|
|
267
|
-
function waitForOwnedCdpPid(port, profile, extensionDir) {
|
|
268
|
+
function waitForOwnedCdpPid(port, profile, extensionDir, onWait) {
|
|
268
269
|
for (let i = 0; i < 300; i += 1) {
|
|
269
270
|
const pid = cdpListenerPids(port).find((candidate) =>
|
|
270
271
|
processIsOwnedHarnessBrowser(candidate, profile, extensionDir),
|
|
271
272
|
);
|
|
272
273
|
if (pid !== undefined) return pid;
|
|
274
|
+
if (typeof onWait === 'function' && i % 10 === 0) onWait();
|
|
273
275
|
spawnSync('sleep', ['0.1']);
|
|
274
276
|
}
|
|
275
277
|
return null;
|
|
@@ -29,4 +29,13 @@ function restoreMacFrontmostProcess(pid) {
|
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
function preserveMacFrontmostProcess(pid) {
|
|
33
|
+
if (process.env.MM_HARNESS_FOCUS_BROWSER === '1') return false;
|
|
34
|
+
return restoreMacFrontmostProcess(pid);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = {
|
|
38
|
+
captureMacFrontmostProcess,
|
|
39
|
+
preserveMacFrontmostProcess,
|
|
40
|
+
restoreMacFrontmostProcess,
|
|
41
|
+
};
|
|
@@ -228,7 +228,6 @@ NODE
|
|
|
228
228
|
quoted_check_infura="$(printf '%q' "$SCRIPT_DIR/check-infura-readiness.cjs")"
|
|
229
229
|
quoted_stamp_title="$(printf '%q' "$SCRIPT_DIR/stamp-runtime-title.cjs")"
|
|
230
230
|
quoted_chrome_launcher="$(printf '%q' "$SCRIPT_DIR/launch-browser.cjs")"
|
|
231
|
-
quoted_build_handoff="$(printf '%q' "$SCRIPT_DIR/build-handoff.cjs")"
|
|
232
231
|
quoted_artifact_state="$(printf '%q' "$SCRIPT_DIR/artifact-runtime-state.cjs")"
|
|
233
232
|
quoted_fixture_state="$(printf '%q' "$FIXTURE_STATE_ABS")"
|
|
234
233
|
quoted_fixture_validation="$(printf '%q' "$FIXTURE_VALIDATION_ABS")"
|
|
@@ -353,7 +352,6 @@ NODE
|
|
|
353
352
|
fi
|
|
354
353
|
prepare_parts+=("$chrome_launch_cmd")
|
|
355
354
|
prepare_parts+=("for i in {1..60}; do curl -fsS --max-time 1 http://127.0.0.1:${CDP_PORT}/json/version >/dev/null 2>&1 && break; sleep 1; done; curl -fsS --max-time 1 http://127.0.0.1:${CDP_PORT}/json/version >/dev/null")
|
|
356
|
-
prepare_parts+=("node ${quoted_build_handoff} --target ${quoted_target} --runtime-dist ${quoted_runtime_dist} --cdp-port ${CDP_PORT}")
|
|
357
355
|
if [ -n "$WALLET_FIXTURE_ABS" ]; then
|
|
358
356
|
prepare_parts+=("bash ${quoted_seed_fixture} seed-cdp --target ${quoted_target} --fixture ${quoted_wallet_fixture} --state ${quoted_fixture_state} --cdp-port ${CDP_PORT} --extension-dir ${quoted_runtime_dist} --extension-id-file ${quoted_extension_id_file} --out ${quoted_fixture_validation}")
|
|
359
357
|
fi
|
|
@@ -148,7 +148,7 @@ async function ensureExtensionReady(target, options) {
|
|
|
148
148
|
}
|
|
149
149
|
};
|
|
150
150
|
let health = await checkHealth();
|
|
151
|
-
if (action === "pruned" && (health.status !== "PASS" || after !== 1)) {
|
|
151
|
+
if (action === "pruned" && !health.errorCode && (health.status !== "PASS" || after !== 1)) {
|
|
152
152
|
const listing = await jsonList(cdpPort);
|
|
153
153
|
if (listing.ok) {
|
|
154
154
|
const existingHomeIds = new Set(
|
|
@@ -442,7 +442,7 @@ async function decideExtensionReadiness(target, options = {}) {
|
|
|
442
442
|
actions: []
|
|
443
443
|
},
|
|
444
444
|
{
|
|
445
|
-
when: cdp.errorCode === "EXTENSION_BACKGROUND_UNRESPONSIVE",
|
|
445
|
+
when: cdp.errorCode === "EXTENSION_BACKGROUND_UNRESPONSIVE" || cdp.errorCode === "EXTENSION_STARTUP_UNVERIFIED",
|
|
446
446
|
decision: "blocked",
|
|
447
447
|
reasonCode: "background-unresponsive",
|
|
448
448
|
reasons: [
|
|
@@ -503,7 +503,7 @@ async function decideExtensionReadiness(target, options = {}) {
|
|
|
503
503
|
actions: []
|
|
504
504
|
},
|
|
505
505
|
{
|
|
506
|
-
when: cdp.errorCode === "EXTENSION_BACKGROUND_UNRESPONSIVE",
|
|
506
|
+
when: cdp.errorCode === "EXTENSION_BACKGROUND_UNRESPONSIVE" || cdp.errorCode === "EXTENSION_STARTUP_UNVERIFIED",
|
|
507
507
|
decision: "blocked",
|
|
508
508
|
reasonCode: "background-unresponsive",
|
|
509
509
|
reasons: ["Extension startup failed; inspect the background error before relaunching or resetting wallet state.", ...cdp.findings?.slice(0, 3) ?? []],
|
|
@@ -151,16 +151,17 @@ async function checkExtensionRuntimeHealth(projectRoot, cdpPort, options = {}) {
|
|
|
151
151
|
};
|
|
152
152
|
}
|
|
153
153
|
const runtime = await evaluateHealth(session, cdpCallTimeoutMs);
|
|
154
|
-
if (runtime.backgroundUnresponsive === true) {
|
|
154
|
+
if (runtime.backgroundUnresponsive === true || runtime.hasPageContent === false) {
|
|
155
|
+
const code = runtime.backgroundUnresponsive === true ? "EXTENSION_BACKGROUND_UNRESPONSIVE" : "EXTENSION_STARTUP_UNVERIFIED";
|
|
155
156
|
return {
|
|
156
157
|
status: "FAIL",
|
|
157
|
-
errorCode:
|
|
158
|
+
errorCode: code,
|
|
158
159
|
userAction: `mm-harness logs --target ${shellQuote(projectRoot)} --full # inspect the startup error before restarting or resetting an authorized dev fixture`,
|
|
159
160
|
cdpPort,
|
|
160
161
|
targetUrl: target.url,
|
|
161
162
|
extensionId: safeExtensionId(target),
|
|
162
163
|
extensionPageTargets: extensionTargets.length,
|
|
163
|
-
findings: [
|
|
164
|
+
findings: [`${code}: ${runtime.backgroundUnresponsive === true ? "Extension UI reports a startup failure or unresponsive background" : "Extension page has not rendered its UI"}; RPC readiness cannot be established.`],
|
|
164
165
|
details: {
|
|
165
166
|
projectRoot,
|
|
166
167
|
targetUrls: targets.map((entry) => entry.url).filter(Boolean),
|
|
@@ -173,6 +174,7 @@ async function checkExtensionRuntimeHealth(projectRoot, cdpPort, options = {}) {
|
|
|
173
174
|
runtime.evmRpcProbeOk = providerProbe.ok;
|
|
174
175
|
runtime.evmRpcProbeError = providerProbe.error;
|
|
175
176
|
runtime.evmRpcProbeSource = providerProbe.source;
|
|
177
|
+
runtime.evmRpcProbeAttempted = providerProbe.requestAttempted;
|
|
176
178
|
}
|
|
177
179
|
if (runtime.href && !String(runtime.href).startsWith("chrome-extension://")) {
|
|
178
180
|
findings.push(`Extension page href is not an extension URL: ${runtime.href}`);
|
|
@@ -181,10 +183,11 @@ async function checkExtensionRuntimeHealth(projectRoot, cdpPort, options = {}) {
|
|
|
181
183
|
findings.push('Extension UI reports "Unable to connect to Ethereum".');
|
|
182
184
|
}
|
|
183
185
|
const rpcExecutionFailed = runtime.evmRpcProbeOk !== true && /Failed to execute 'fetch' on 'WorkerGlobalScope': Illegal invocation/u.test(String(runtime.evmRpcProbeError));
|
|
186
|
+
const startupUnverified = runtime.evmRpcProbeOk !== true && runtime.evmRpcProbeAttempted === false;
|
|
184
187
|
if (runtime.evmRpcProbeOk !== true) {
|
|
185
188
|
const probeError = String(runtime.evmRpcProbeError ?? "unknown error").replace(/[.\s]+$/u, "");
|
|
186
189
|
findings.push(
|
|
187
|
-
rpcExecutionFailed ? `EVM_RPC_EXECUTION_FAILED: background RPC fetch threw before an HTTP response: ${probeError}.` : `EVM RPC readiness probe failed: ${probeError}. Infura configured \u2260 RPC reachable.`
|
|
190
|
+
startupUnverified ? `EXTENSION_STARTUP_UNVERIFIED: no completed provider probe established RPC readiness: ${probeError}.` : rpcExecutionFailed ? `EVM_RPC_EXECUTION_FAILED: background RPC fetch threw before an HTTP response: ${probeError}.` : `EVM RPC readiness probe failed: ${probeError}. Infura configured \u2260 RPC reachable.`
|
|
188
191
|
);
|
|
189
192
|
}
|
|
190
193
|
if (runtime.hasSubmitRequest === true && runtime.backgroundProbeOk !== true) {
|
|
@@ -195,8 +198,8 @@ async function checkExtensionRuntimeHealth(projectRoot, cdpPort, options = {}) {
|
|
|
195
198
|
return {
|
|
196
199
|
status: findings.length === 0 ? "PASS" : "FAIL",
|
|
197
200
|
...evmRpcUnreachable ? {
|
|
198
|
-
errorCode: rpcExecutionFailed ? "EVM_RPC_EXECUTION_FAILED" : "EVM_RPC_UNREACHABLE",
|
|
199
|
-
userAction: rpcExecutionFailed ? `mm-harness logs --target ${shellQuote(projectRoot)} --full # inspect the background fetch stack; credential resync is not indicated by this error` : evmRpcRecoveryAction(projectRoot)
|
|
201
|
+
errorCode: startupUnverified ? "EXTENSION_STARTUP_UNVERIFIED" : rpcExecutionFailed ? "EVM_RPC_EXECUTION_FAILED" : "EVM_RPC_UNREACHABLE",
|
|
202
|
+
userAction: startupUnverified ? `mm-harness logs --target ${shellQuote(projectRoot)} --full # inspect Extension startup; upstream RPC failure has not been established` : rpcExecutionFailed ? `mm-harness logs --target ${shellQuote(projectRoot)} --full # inspect the background fetch stack; credential resync is not indicated by this error` : evmRpcRecoveryAction(projectRoot)
|
|
200
203
|
} : {},
|
|
201
204
|
warnings,
|
|
202
205
|
cdpPort,
|
|
@@ -332,6 +335,7 @@ async function evaluateHealth(session, timeoutMs) {
|
|
|
332
335
|
return {
|
|
333
336
|
href: location.href,
|
|
334
337
|
title: document.title,
|
|
338
|
+
hasPageContent: bodyText.trim().length > 0,
|
|
335
339
|
hookKeys: Object.keys(hooks),
|
|
336
340
|
hasSubmitRequest: typeof hooks.submitRequestToBackground === 'function',
|
|
337
341
|
hasStore: Boolean(hooks.store),
|
|
@@ -353,6 +357,7 @@ async function evaluateHealth(session, timeoutMs) {
|
|
|
353
357
|
new Promise((resolve) => setTimeout(() => resolve({
|
|
354
358
|
href: location.href,
|
|
355
359
|
title: document.title,
|
|
360
|
+
hasPageContent: bodyText.trim().length > 0,
|
|
356
361
|
hookKeys: Object.keys(hooks),
|
|
357
362
|
hasSubmitRequest: typeof hooks.submitRequestToBackground === 'function',
|
|
358
363
|
hasStore: Boolean(hooks.store),
|
|
@@ -395,7 +400,7 @@ async function probeUiEthereumProvider(session, timeoutMs) {
|
|
|
395
400
|
value: {
|
|
396
401
|
async probe() {
|
|
397
402
|
if (!provider || typeof provider.request !== 'function') {
|
|
398
|
-
return { ok: false, error: 'Ethereum provider is unavailable' };
|
|
403
|
+
return { ok: false, error: 'Ethereum provider is unavailable', requestAttempted: false };
|
|
399
404
|
}
|
|
400
405
|
try {
|
|
401
406
|
const code = await provider.request({
|
|
@@ -405,10 +410,11 @@ async function probeUiEthereumProvider(session, timeoutMs) {
|
|
|
405
410
|
const ok = typeof code === 'string' && /^0x[0-9a-f]*$/iu.test(code);
|
|
406
411
|
return {
|
|
407
412
|
ok,
|
|
413
|
+
requestAttempted: true,
|
|
408
414
|
error: ok ? null : 'Ethereum provider returned invalid contract code',
|
|
409
415
|
};
|
|
410
416
|
} catch (error) {
|
|
411
|
-
return { ok: false, error: String(error?.message || error) };
|
|
417
|
+
return { ok: false, error: String(error?.message || error), requestAttempted: true };
|
|
412
418
|
}
|
|
413
419
|
},
|
|
414
420
|
cleanup() {
|
|
@@ -432,13 +438,15 @@ async function probeUiEthereumProvider(session, timeoutMs) {
|
|
|
432
438
|
return {
|
|
433
439
|
ok: false,
|
|
434
440
|
error: "Ethereum provider preload was not installed",
|
|
435
|
-
source: "ui-ethereum-provider"
|
|
441
|
+
source: "ui-ethereum-provider",
|
|
442
|
+
requestAttempted: false
|
|
436
443
|
};
|
|
437
444
|
}
|
|
438
445
|
let outcome = {
|
|
439
446
|
ok: false,
|
|
440
447
|
error: "Ethereum provider is unavailable",
|
|
441
|
-
source: "ui-ethereum-provider"
|
|
448
|
+
source: "ui-ethereum-provider",
|
|
449
|
+
requestAttempted: false
|
|
442
450
|
};
|
|
443
451
|
const cleanupErrors = [];
|
|
444
452
|
try {
|
|
@@ -453,10 +461,11 @@ async function probeUiEthereumProvider(session, timeoutMs) {
|
|
|
453
461
|
}, Math.max(1, deadline - Date.now()));
|
|
454
462
|
const value = result.result?.value;
|
|
455
463
|
if (value?.ok === true) {
|
|
456
|
-
outcome = { ok: true, error: null, source: "ui-ethereum-provider" };
|
|
464
|
+
outcome = { ok: true, error: null, source: "ui-ethereum-provider", requestAttempted: true };
|
|
457
465
|
break;
|
|
458
466
|
}
|
|
459
467
|
if (typeof value?.error === "string") outcome.error = value.error;
|
|
468
|
+
outcome.requestAttempted ||= value?.requestAttempted === true;
|
|
460
469
|
} catch (error) {
|
|
461
470
|
outcome.error = messageOf(error);
|
|
462
471
|
}
|
|
@@ -464,7 +473,7 @@ async function probeUiEthereumProvider(session, timeoutMs) {
|
|
|
464
473
|
await sleep(100);
|
|
465
474
|
}
|
|
466
475
|
} catch (error) {
|
|
467
|
-
outcome = { ok: false, error: messageOf(error)
|
|
476
|
+
outcome = { ...outcome, ok: false, error: messageOf(error) };
|
|
468
477
|
} finally {
|
|
469
478
|
try {
|
|
470
479
|
const cleanup = await boundedCdpCall(session, "Runtime.evaluate", {
|
|
@@ -494,7 +503,8 @@ async function probeUiEthereumProvider(session, timeoutMs) {
|
|
|
494
503
|
return cleanupErrors.length > 0 ? {
|
|
495
504
|
ok: false,
|
|
496
505
|
error: `Ethereum provider probe cleanup failed: ${cleanupErrors.join("; ")}`,
|
|
497
|
-
source: "ui-ethereum-provider"
|
|
506
|
+
source: "ui-ethereum-provider",
|
|
507
|
+
requestAttempted: outcome.requestAttempted
|
|
498
508
|
} : outcome;
|
|
499
509
|
}
|
|
500
510
|
function extensionBackgroundProbeTimeoutMs(cdpCallTimeoutMs) {
|
|
@@ -310,10 +310,10 @@ async function handleLaunchLocked(argv, stream) {
|
|
|
310
310
|
exitCode: EXIT.infra
|
|
311
311
|
});
|
|
312
312
|
}
|
|
313
|
-
if (adapter === "extension" && attempt.output.includes("EXTENSION_BACKGROUND_UNRESPONSIVE:")) {
|
|
313
|
+
if (adapter === "extension" && (attempt.output.includes("EXTENSION_BACKGROUND_UNRESPONSIVE:") || attempt.output.includes("EXTENSION_STARTUP_UNVERIFIED:"))) {
|
|
314
314
|
return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
|
|
315
|
-
code: "EXTENSION_BACKGROUND_UNRESPONSIVE",
|
|
316
|
-
message: "Extension startup
|
|
315
|
+
code: attempt.output.includes("EXTENSION_BACKGROUND_UNRESPONSIVE:") ? "EXTENSION_BACKGROUND_UNRESPONSIVE" : "EXTENSION_STARTUP_UNVERIFIED",
|
|
316
|
+
message: "Extension startup is not healthy; inspect the background error before restarting or resetting wallet state.",
|
|
317
317
|
recoverable: false,
|
|
318
318
|
userAction: `mm-harness logs --target ${shellQuote(target)} --full`,
|
|
319
319
|
exitCode: EXIT.runtime,
|
|
@@ -43,7 +43,7 @@ export async function readVisiblePerpsState(
|
|
|
43
43
|
action: input.action,
|
|
44
44
|
...normalizeVisiblePerpsState({
|
|
45
45
|
route: screen.route,
|
|
46
|
-
title: screen.
|
|
46
|
+
title: screen.name,
|
|
47
47
|
items: visible.items,
|
|
48
48
|
offscreenItems: visible.hidden_or_offscreen,
|
|
49
49
|
truncated: visible.truncated,
|
|
@@ -27,7 +27,24 @@ export async function observeNativeUi(payload, context) {
|
|
|
27
27
|
if (supported.length === 0) return warnings.length ? { warnings } : {};
|
|
28
28
|
|
|
29
29
|
try {
|
|
30
|
+
const env = { ...process.env, ...record(context?.env) };
|
|
31
|
+
const routeInput = { action: 'ui.observe', node: { ...record(payload?.node), bridge_timeout_ms: 2_000, cdp_timeout_ms: 2_000 }, context };
|
|
32
|
+
let routeBefore;
|
|
33
|
+
if (supported.includes('ui.screen') && !isOpaqueRuntime(env) && (env.METRO_PORT || env.WATCHER_PORT)) {
|
|
34
|
+
try {
|
|
35
|
+
routeBefore = await bridgeCommand(routeInput, ['get-route']);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
warnings.push({ ref: 'ui.screen', message: `Navigation route unavailable: ${error.message}` });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
30
40
|
const hierarchy = await readNativeHierarchy(payload, context);
|
|
41
|
+
if (routeBefore?.name) {
|
|
42
|
+
const routeAfter = await bridgeCommand(routeInput, ['get-route']);
|
|
43
|
+
if (routeAfter?.name !== routeBefore.name || routeAfter?.key !== routeBefore.key) {
|
|
44
|
+
throw new Error('Navigation changed during native UI observation; the snapshot cannot be bound to one route.');
|
|
45
|
+
}
|
|
46
|
+
hierarchy.screen.route = routeAfter.name;
|
|
47
|
+
}
|
|
31
48
|
const observations = {};
|
|
32
49
|
for (const ref of supported) {
|
|
33
50
|
observations[ref] = ref === 'ui.screen' ? hierarchy.screen : hierarchy.visible;
|
|
@@ -144,7 +144,15 @@ function marketFromTestId(testId) {
|
|
|
144
144
|
return undefined;
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
-
function classifySurface(route, testIds) {
|
|
147
|
+
function classifySurface(route, testIds, platform) {
|
|
148
|
+
if (platform === 'mobile') {
|
|
149
|
+
const routes = {
|
|
150
|
+
PerpsMarketListView: 'home',
|
|
151
|
+
PerpsTrendingView: 'market-list',
|
|
152
|
+
PerpsMarketDetails: 'market-details',
|
|
153
|
+
};
|
|
154
|
+
if (Object.hasOwn(routes, route)) return routes[route];
|
|
155
|
+
}
|
|
148
156
|
const joined = [...testIds].join('\n');
|
|
149
157
|
if (/confirm-transaction\/.*[?&]goBackTo=%2Fperps-home(?:&|$)/u.test(route)) {
|
|
150
158
|
return 'funds';
|
|
@@ -213,7 +221,7 @@ export function normalizeVisiblePerpsState(raw, options, platform) {
|
|
|
213
221
|
? /^remove from /iu.test(favoriteLabel) : null;
|
|
214
222
|
const testIds = new Set(unique.map((item) => item.testId));
|
|
215
223
|
const route = text(raw.route) ?? '';
|
|
216
|
-
const surface = classifySurface(route, testIds);
|
|
224
|
+
const surface = classifySurface(route, testIds, platform);
|
|
217
225
|
const marketItems = selected(
|
|
218
226
|
unique,
|
|
219
227
|
(item) => Boolean(marketFromTestId(item.testId)),
|
|
@@ -316,7 +324,7 @@ function assertVisibleState(state, options) {
|
|
|
316
324
|
}
|
|
317
325
|
if (options.surface !== 'auto' && state.surface !== options.surface) {
|
|
318
326
|
throw new Error(
|
|
319
|
-
`Expected visible Perps surface ${options.surface}, but observed ${state.surface}.`,
|
|
327
|
+
`Expected visible Perps surface ${options.surface}, but observed ${state.surface} (route: ${state.route ?? 'unavailable'}; title: ${state.title ?? 'unavailable'}).`,
|
|
320
328
|
);
|
|
321
329
|
}
|
|
322
330
|
if (state.markets.length < options.minimumMarketCount) {
|