@deeeed/metamask-harness 0.43.0 → 0.44.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 +32 -0
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +312 -35
- package/adapters/mobile/bridge-runtime/console-forwarder.cjs +20 -8
- package/adapters/mobile/bridge-runtime/lib/cdp-broker.cjs +102 -0
- package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +4 -2
- package/adapters/mobile/reload-app.mjs +99 -1
- package/dist/adapters/mobile/prepare.js +12 -0
- package/dist/adapters.js +22 -3
- package/dist/commands/call.js +40 -2
- package/dist/commands/run-engine.js +247 -139
- package/dist/commands/run-report.js +68 -0
- package/dist/commands/run.js +47 -2
- package/dist/execution-provenance.js +342 -0
- package/dist/run-diagnostics.js +36 -11
- package/dist/runner.js +44 -13
- package/library/actions/mobile/perps/performance-capture.mjs +577 -189
- package/library/actions/mobile/perps/perps.mjs +122 -0
- package/library/actions/mobile/platform/bridge.mjs +40 -8
- package/library/actions/mobile/platform/native-session.mjs +1 -1
- package/library/actions/mobile/wallet/ensure_unlocked.mjs +22 -4
- package/library/actions/mobile/wallet/lock.mjs +1 -4
- package/library/actions/mobile/wallet/select_account.mjs +129 -17
- package/library/manifests/mobile.action-manifest.json +28 -3
- package/library/recipes/mobile/perps/performance.recipe.json +73 -47
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,38 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.44.1 - 2026-08-27
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Accept retained market context as lifecycle-valid Homepage evidence during account-only switches.
|
|
10
|
+
- Require controller-backed Mobile unlock proof before continuing a recipe after startup or restart.
|
|
11
|
+
|
|
12
|
+
## 0.44.0 - 2026-08-25
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- Remove `cold_disk_cache` from the `perps.performance` lifecycle enum because disk hydration is a cache source within a lifecycle, not a standalone lifecycle.
|
|
17
|
+
- Await promise-returning Mobile `press-test-id` and `set-input` bridge handlers so recipe nodes finish only after the in-app operation settles.
|
|
18
|
+
- Keep Mobile broker commands available while Hermes installs the in-app bridge after a target handoff, instead of reconnecting the inspector until actions time out.
|
|
19
|
+
- Honor `launch --build` when a healthy Mobile runtime is already connected; the explicit native build can no longer return a false pass without invoking the build leaf.
|
|
20
|
+
- Let resident Homepage and short-resume performance recipes finish from the ordered visible-content sequence without requiring a new live takeover.
|
|
21
|
+
- Make Mobile reload stay on the pinned device, fail before an active Android Hermes sampler can abort the app, and fail if the process exits or a replacement target never appears; reload never relaunches the app.
|
|
22
|
+
- Bind run evidence to the executable Harness source, build, bin, package, library, and adapter inputs plus task-local command/config helpers, and reject input drift or symlink swaps.
|
|
23
|
+
- Persist recovery and mutation lists in recipe packages, include a redacted run-scoped application log, reject failed recovery before retrying, and prove lifecycle continuity with native process IDs, including opaque runtimes when requested.
|
|
24
|
+
- Refuse stale Mobile source under every heal policy unless an authored `app.lifecycle restart` reloads it, and invalidate run evidence if recipe, library, or product provenance drifts after preparation.
|
|
25
|
+
- Require Homepage demand, committed UI, resolved content, and live-current frame records before the canonical Mobile Perps performance recipe can pass.
|
|
26
|
+
- Fail a Mobile recipe or action without implicitly restarting Metro or the app; process restarts remain explicit lifecycle actions.
|
|
27
|
+
- Keep one Mobile inspector connection open while React Native replays its console buffer, avoiding repeated `Runtime.enable` requests that can stall Metro after unlock.
|
|
28
|
+
- Allow `metamask.wallet.lock` to use the same visible Android controls on development and opaque clients.
|
|
29
|
+
- Keep account-selection proof attached across the temporary Hermes/CDP stall caused by the account switch itself.
|
|
30
|
+
- Add `metamask.perps.ensure_mode` so recipes can reach Lite or Pro through the visible mode control without inheriting prior device state.
|
|
31
|
+
- Preserve Homepage boundary source and content-variant metadata in performance summaries, and bind recipe manifests to the exact product checkout commit.
|
|
32
|
+
- Accept only structured, generation-coherent Perps live-stream records as performance proof, and keep Mobile wallet and mode probes inside one absolute action deadline.
|
|
33
|
+
- Keep the latest automatic Mobile HUD update in the CDP broker and retry it until the in-app bridge applies it when the pinned Hermes target returns, without restarting the app.
|
|
34
|
+
- Keep the Perps performance recipe's empty-account setup read-only and allow its setup-only prime capture to contain no performance records.
|
|
35
|
+
- Redact quoted JSON credential fields before persisting run-scoped application logs.
|
|
36
|
+
|
|
5
37
|
## 0.43.0 - 2026-08-21
|
|
6
38
|
|
|
7
39
|
### Added
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
const fs = require('node:fs');
|
|
20
20
|
const path = require('node:path');
|
|
21
|
+
const { execFileSync } = require('node:child_process');
|
|
21
22
|
const { loadPort } = require('./lib/config.cjs');
|
|
22
23
|
const {
|
|
23
24
|
discoverTarget,
|
|
@@ -36,6 +37,126 @@ const {
|
|
|
36
37
|
} = require('./lib/bridge-errors.cjs');
|
|
37
38
|
const { cdpEval, cdpEvalAsync } = require('./lib/cdp-eval.cjs');
|
|
38
39
|
const { buildArmSnippet, buildCollectSnippet } = require('./lib/issue-capture.cjs');
|
|
40
|
+
const APPLY_HUD_UPDATE_FUNCTION = `function(step) {
|
|
41
|
+
const bridge = this.__AGENTIC__;
|
|
42
|
+
if (step === null) {
|
|
43
|
+
if (typeof bridge?.hideStep !== 'function') return false;
|
|
44
|
+
setTimeout(() => bridge.hideStep(), 0);
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
if (typeof bridge?.showStep !== 'function') return false;
|
|
48
|
+
setTimeout(() => bridge.showStep(step), 0);
|
|
49
|
+
return true;
|
|
50
|
+
}`;
|
|
51
|
+
|
|
52
|
+
function mobileToolPath(tool) {
|
|
53
|
+
return process.env[`MM_HARNESS_${tool.toUpperCase()}_PATH`] || tool;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function bootedIosDevice(deviceName) {
|
|
57
|
+
if (!deviceName) return null;
|
|
58
|
+
try {
|
|
59
|
+
const devices = JSON.parse(
|
|
60
|
+
execFileSync('xcrun', ['simctl', 'list', 'devices', 'available', '-j'], {
|
|
61
|
+
encoding: 'utf8',
|
|
62
|
+
}),
|
|
63
|
+
);
|
|
64
|
+
const device = Object.values(devices.devices || {})
|
|
65
|
+
.flat()
|
|
66
|
+
.find(
|
|
67
|
+
(candidate) =>
|
|
68
|
+
(candidate?.name === deviceName || candidate?.udid === deviceName) &&
|
|
69
|
+
candidate?.state === 'Booted',
|
|
70
|
+
);
|
|
71
|
+
return device?.udid ? device : null;
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function tapVisibleIosAccessibilityTarget(testId, deviceName, platform) {
|
|
78
|
+
if (platform !== 'ios') return null;
|
|
79
|
+
const device = bootedIosDevice(deviceName);
|
|
80
|
+
if (!device) return null;
|
|
81
|
+
try {
|
|
82
|
+
const idb = mobileToolPath('idb');
|
|
83
|
+
const elements = JSON.parse(
|
|
84
|
+
execFileSync(
|
|
85
|
+
idb,
|
|
86
|
+
['ui', 'describe-all', '--udid', device.udid, '--json'],
|
|
87
|
+
{ encoding: 'utf8' },
|
|
88
|
+
),
|
|
89
|
+
);
|
|
90
|
+
const windowFrame = elements
|
|
91
|
+
.map((element) => element?.frame)
|
|
92
|
+
.filter(
|
|
93
|
+
(frame) =>
|
|
94
|
+
frame &&
|
|
95
|
+
frame.x === 0 &&
|
|
96
|
+
frame.y === 0 &&
|
|
97
|
+
frame.width > 0 &&
|
|
98
|
+
frame.height > 0,
|
|
99
|
+
)
|
|
100
|
+
.sort(
|
|
101
|
+
(first, second) =>
|
|
102
|
+
second.width * second.height - first.width * first.height,
|
|
103
|
+
)[0];
|
|
104
|
+
if (!windowFrame) return null;
|
|
105
|
+
const target = elements.find((element) => {
|
|
106
|
+
const frame = element?.frame;
|
|
107
|
+
return (
|
|
108
|
+
element?.AXUniqueId === testId &&
|
|
109
|
+
element?.enabled !== false &&
|
|
110
|
+
element?.hittable !== false &&
|
|
111
|
+
frame &&
|
|
112
|
+
frame.width > 0 &&
|
|
113
|
+
frame.height > 0 &&
|
|
114
|
+
frame.x >= 0 &&
|
|
115
|
+
frame.y >= 0 &&
|
|
116
|
+
frame.x + frame.width <= windowFrame.width &&
|
|
117
|
+
frame.y + frame.height <= windowFrame.height
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
if (!target) return null;
|
|
121
|
+
const x = Math.round(target.frame.x + target.frame.width / 2);
|
|
122
|
+
const y = Math.round(target.frame.y + target.frame.height / 2);
|
|
123
|
+
execFileSync(
|
|
124
|
+
idb,
|
|
125
|
+
['ui', 'tap', String(x), String(y), '--udid', device.udid],
|
|
126
|
+
{ encoding: 'utf8' },
|
|
127
|
+
);
|
|
128
|
+
return { ok: true, testId, deviceName, provider: 'idb-accessibility' };
|
|
129
|
+
} catch {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function applyHudUpdate(client, step) {
|
|
135
|
+
const globalObject = await client.send('Runtime.evaluate', {
|
|
136
|
+
expression: 'globalThis',
|
|
137
|
+
returnByValue: false,
|
|
138
|
+
awaitPromise: false,
|
|
139
|
+
});
|
|
140
|
+
const objectId = globalObject?.result?.objectId;
|
|
141
|
+
if (!objectId) {
|
|
142
|
+
throw new Error('Mobile HUD runtime did not expose globalThis');
|
|
143
|
+
}
|
|
144
|
+
const result = await client.send('Runtime.callFunctionOn', {
|
|
145
|
+
objectId,
|
|
146
|
+
functionDeclaration: APPLY_HUD_UPDATE_FUNCTION,
|
|
147
|
+
arguments: [{ value: step }],
|
|
148
|
+
returnByValue: true,
|
|
149
|
+
awaitPromise: false,
|
|
150
|
+
});
|
|
151
|
+
if (result?.exceptionDetails) {
|
|
152
|
+
throw new Error(
|
|
153
|
+
result.exceptionDetails.exception?.description ||
|
|
154
|
+
result.exceptionDetails.text ||
|
|
155
|
+
'Mobile HUD update failed',
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
return result?.result?.value === true;
|
|
159
|
+
}
|
|
39
160
|
|
|
40
161
|
function parsePerformanceConsoleEvent(params) {
|
|
41
162
|
const markers = ['[PerpsPerf] ', '[HomepagePerf] '];
|
|
@@ -97,7 +218,7 @@ async function setInput(client, testId, value, { deviceName } = {}, redact = fal
|
|
|
97
218
|
throw new Error('Usage: set-input <testId> <value>');
|
|
98
219
|
}
|
|
99
220
|
const expr = `(function() {
|
|
100
|
-
if (globalThis.__AGENTIC__?.setInput) return globalThis.__AGENTIC__.setInput(${JSON.stringify(testId)}, ${JSON.stringify(value)});
|
|
221
|
+
if (globalThis.__AGENTIC__?.setInput) return Promise.resolve(globalThis.__AGENTIC__.setInput(${JSON.stringify(testId)}, ${JSON.stringify(value)}));
|
|
101
222
|
var hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
102
223
|
if (!hook) return { ok: false, error: 'No React DevTools hook' };
|
|
103
224
|
var renderers = hook.renderers;
|
|
@@ -133,7 +254,7 @@ async function setInput(client, testId, value, { deviceName } = {}, redact = fal
|
|
|
133
254
|
})()`;
|
|
134
255
|
let result;
|
|
135
256
|
try {
|
|
136
|
-
result = await
|
|
257
|
+
result = await cdpEvalAsync(client, expr);
|
|
137
258
|
} catch (error) {
|
|
138
259
|
if (redact) {
|
|
139
260
|
throw new Error(`Secret input could not be applied for testID ${testId}.`);
|
|
@@ -151,6 +272,26 @@ async function setInput(client, testId, value, { deviceName } = {}, redact = fal
|
|
|
151
272
|
};
|
|
152
273
|
}
|
|
153
274
|
|
|
275
|
+
function parseHudStep(args) {
|
|
276
|
+
const raw = args.join(' ');
|
|
277
|
+
let step;
|
|
278
|
+
try {
|
|
279
|
+
step = JSON.parse(raw);
|
|
280
|
+
} catch (error) {
|
|
281
|
+
throw new Error(`show-step-json requires a JSON step payload: ${error.message}`);
|
|
282
|
+
}
|
|
283
|
+
if (!step || typeof step !== 'object' || Array.isArray(step)) {
|
|
284
|
+
throw new Error('show-step-json requires a JSON object step payload');
|
|
285
|
+
}
|
|
286
|
+
if (typeof step.intent !== 'string' || !step.intent.trim()) {
|
|
287
|
+
throw new Error('show-step-json requires step.intent');
|
|
288
|
+
}
|
|
289
|
+
if (step.id !== undefined && typeof step.id !== 'string') {
|
|
290
|
+
throw new Error('show-step-json step.id must be a string when provided');
|
|
291
|
+
}
|
|
292
|
+
return step;
|
|
293
|
+
}
|
|
294
|
+
|
|
154
295
|
const COMMANDS = {
|
|
155
296
|
async navigate(client, args, { deviceName, platform } = {}) {
|
|
156
297
|
const routeName = ROUTE_ALIASES[args[0]] || args[0];
|
|
@@ -277,8 +418,15 @@ const COMMANDS = {
|
|
|
277
418
|
var agenticPresent = typeof globalThis.__AGENTIC__ !== 'undefined';
|
|
278
419
|
var route = globalThis.__AGENTIC__?.getRoute() || null;
|
|
279
420
|
var account = null;
|
|
421
|
+
var keyringUnlocked = false;
|
|
280
422
|
try { account = globalThis.__AGENTIC__?.getSelectedAccount() || null; } catch(e) {}
|
|
281
|
-
|
|
423
|
+
try { keyringUnlocked = globalThis.Engine?.context?.KeyringController?.isUnlocked?.() === true; } catch(e) {}
|
|
424
|
+
return {
|
|
425
|
+
route: route,
|
|
426
|
+
account: account,
|
|
427
|
+
agenticPresent: agenticPresent,
|
|
428
|
+
keyringUnlocked: keyringUnlocked,
|
|
429
|
+
};
|
|
282
430
|
})()`;
|
|
283
431
|
const snapshot = await cdpEval(client, expr);
|
|
284
432
|
return { ...snapshot, deviceName: deviceName || '', platform: platform || '' };
|
|
@@ -300,14 +448,15 @@ const COMMANDS = {
|
|
|
300
448
|
return await cdpEval(client, `globalThis.__AGENTIC__?.switchAccount(${JSON.stringify(address)})`);
|
|
301
449
|
},
|
|
302
450
|
|
|
303
|
-
async 'press-test-id'(client, args, { deviceName } = {}) {
|
|
451
|
+
async 'press-test-id'(client, args, { deviceName, platform } = {}) {
|
|
304
452
|
const testId = args[0];
|
|
305
453
|
if (!testId) {
|
|
306
454
|
throw new Error('Usage: press-test-id <testId>');
|
|
307
455
|
}
|
|
308
|
-
// Try
|
|
456
|
+
// Try the app bridge first. Native accessibility remains an iOS fallback
|
|
457
|
+
// for controls whose React handler cannot be invoked directly.
|
|
309
458
|
const expr = `(function() {
|
|
310
|
-
if (globalThis.__AGENTIC__?.pressTestId) return globalThis.__AGENTIC__.pressTestId(${JSON.stringify(testId)});
|
|
459
|
+
if (globalThis.__AGENTIC__?.pressTestId) return Promise.resolve(globalThis.__AGENTIC__.pressTestId(${JSON.stringify(testId)}));
|
|
311
460
|
var hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
312
461
|
if (!hook) return { ok: false, error: 'No React DevTools hook' };
|
|
313
462
|
var renderers = hook.renderers;
|
|
@@ -330,10 +479,72 @@ const COMMANDS = {
|
|
|
330
479
|
}
|
|
331
480
|
return { ok: false, error: 'No component with testID=' + ${JSON.stringify(testId)} + ' found or no onPress' };
|
|
332
481
|
})()`;
|
|
333
|
-
|
|
482
|
+
let result;
|
|
483
|
+
let bridgeError;
|
|
484
|
+
try {
|
|
485
|
+
result = await cdpEvalAsync(client, expr);
|
|
486
|
+
if (result?.ok !== false) {
|
|
487
|
+
return { ...result, testId, deviceName };
|
|
488
|
+
}
|
|
489
|
+
} catch (error) {
|
|
490
|
+
bridgeError = error;
|
|
491
|
+
}
|
|
492
|
+
const nativeResult = tapVisibleIosAccessibilityTarget(
|
|
493
|
+
testId,
|
|
494
|
+
deviceName,
|
|
495
|
+
platform,
|
|
496
|
+
);
|
|
497
|
+
if (nativeResult) return nativeResult;
|
|
498
|
+
if (bridgeError) throw bridgeError;
|
|
334
499
|
return { ...result, testId, deviceName };
|
|
335
500
|
},
|
|
336
501
|
|
|
502
|
+
async 'key-press'(_client, args, { deviceName, platform } = {}) {
|
|
503
|
+
const requestedKey = String(args[0] || 'Enter').toLowerCase();
|
|
504
|
+
const isEnter = requestedKey === 'enter' || requestedKey === 'return';
|
|
505
|
+
const isBack = requestedKey === 'escape' || requestedKey === 'back';
|
|
506
|
+
if (!isEnter && !isBack) {
|
|
507
|
+
throw new Error(`Unsupported Mobile key ${JSON.stringify(args[0])}`);
|
|
508
|
+
}
|
|
509
|
+
if (platform === 'android') {
|
|
510
|
+
const serial = process.env.ADB_SERIAL || process.env.ANDROID_SERIAL;
|
|
511
|
+
if (!serial) throw new Error('key-press requires a selected Android device');
|
|
512
|
+
const keyCode = isEnter ? '66' : '4';
|
|
513
|
+
execFileSync(
|
|
514
|
+
mobileToolPath('adb'),
|
|
515
|
+
['-s', serial, 'shell', 'input', 'keyevent', keyCode],
|
|
516
|
+
{ encoding: 'utf8' },
|
|
517
|
+
);
|
|
518
|
+
return {
|
|
519
|
+
ok: true,
|
|
520
|
+
key: args[0] || 'Enter',
|
|
521
|
+
keyCode,
|
|
522
|
+
deviceName,
|
|
523
|
+
provider: 'adb-key',
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
if (platform !== 'ios') {
|
|
527
|
+
throw new Error(`key-press requires a selected Mobile platform`);
|
|
528
|
+
}
|
|
529
|
+
const device = bootedIosDevice(deviceName);
|
|
530
|
+
if (!device) {
|
|
531
|
+
throw new Error(`No booted simulator named ${JSON.stringify(deviceName)}`);
|
|
532
|
+
}
|
|
533
|
+
const keyCode = isEnter ? '40' : '41';
|
|
534
|
+
execFileSync(
|
|
535
|
+
mobileToolPath('idb'),
|
|
536
|
+
['ui', 'key', keyCode, '--udid', device.udid],
|
|
537
|
+
{ encoding: 'utf8' },
|
|
538
|
+
);
|
|
539
|
+
return {
|
|
540
|
+
ok: true,
|
|
541
|
+
key: args[0] || 'Enter',
|
|
542
|
+
keyCode,
|
|
543
|
+
deviceName,
|
|
544
|
+
provider: 'idb-key',
|
|
545
|
+
};
|
|
546
|
+
},
|
|
547
|
+
|
|
337
548
|
async 'long-press-test-id'(client, args, { deviceName } = {}) {
|
|
338
549
|
const testId = args[0];
|
|
339
550
|
if (!testId) {
|
|
@@ -406,6 +617,9 @@ const COMMANDS = {
|
|
|
406
617
|
// selector fiber. For into-view requests the scroll container is commonly
|
|
407
618
|
// an ancestor (for example, a Wallet Home section row), so use the
|
|
408
619
|
// ancestor-aware bridge fallback below instead.
|
|
620
|
+
if (${intoView} && globalThis.__AGENTIC__?.scrollIntoView) {
|
|
621
|
+
return globalThis.__AGENTIC__.scrollIntoView(${JSON.stringify(testId)}, ${animated});
|
|
622
|
+
}
|
|
409
623
|
if (!${intoView} && globalThis.__AGENTIC__?.scrollView) return globalThis.__AGENTIC__.scrollView(${optsJson});
|
|
410
624
|
var hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
411
625
|
if (!hook) return { ok: false, error: 'No React DevTools hook' };
|
|
@@ -427,14 +641,56 @@ const COMMANDS = {
|
|
|
427
641
|
}
|
|
428
642
|
return false;
|
|
429
643
|
}
|
|
644
|
+
function findMeasurable(fiber) {
|
|
645
|
+
if (!fiber) return null;
|
|
646
|
+
var sn = fiber.stateNode;
|
|
647
|
+
if (sn && typeof sn.measureLayout === 'function') return sn;
|
|
648
|
+
return findMeasurable(fiber.child);
|
|
649
|
+
}
|
|
430
650
|
function tryScrollNear(anchor) {
|
|
431
651
|
if (!opts.intoView && tryScroll(anchor, false)) return true;
|
|
652
|
+
var target = opts.intoView ? findMeasurable(anchor) : null;
|
|
432
653
|
var current = anchor ? anchor.return : null;
|
|
433
654
|
while (current) {
|
|
434
655
|
var sn = current.stateNode;
|
|
656
|
+
var currentProps = current.memoizedProps;
|
|
435
657
|
if (sn) {
|
|
436
|
-
if (typeof sn.scrollTo === 'function'
|
|
437
|
-
|
|
658
|
+
if (typeof sn.scrollTo === 'function' && !(currentProps && currentProps.horizontal === true)) {
|
|
659
|
+
if (target) {
|
|
660
|
+
return new Promise(function(resolve) {
|
|
661
|
+
target.measureLayout(
|
|
662
|
+
sn,
|
|
663
|
+
function(x, y) {
|
|
664
|
+
sn.scrollTo({ y: y, animated: opts.animated });
|
|
665
|
+
resolve({ ok: true, testId: opts.testId, measuredOffset: y, animated: opts.animated });
|
|
666
|
+
},
|
|
667
|
+
function() {
|
|
668
|
+
resolve({ ok: false, error: 'Unable to measure testID=' + opts.testId + ' relative to its scroll container' });
|
|
669
|
+
}
|
|
670
|
+
);
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
sn.scrollTo({ y: opts.offset, animated: opts.animated });
|
|
674
|
+
return true;
|
|
675
|
+
}
|
|
676
|
+
if (typeof sn.scrollToOffset === 'function' && !(currentProps && currentProps.horizontal === true)) {
|
|
677
|
+
if (target) {
|
|
678
|
+
return new Promise(function(resolve) {
|
|
679
|
+
target.measureLayout(
|
|
680
|
+
sn,
|
|
681
|
+
function(x, y) {
|
|
682
|
+
sn.scrollToOffset({ offset: y, animated: opts.animated });
|
|
683
|
+
resolve({ ok: true, testId: opts.testId, measuredOffset: y, animated: opts.animated });
|
|
684
|
+
},
|
|
685
|
+
function() {
|
|
686
|
+
resolve({ ok: false, error: 'Unable to measure testID=' + opts.testId + ' relative to its scroll container' });
|
|
687
|
+
}
|
|
688
|
+
);
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
sn.scrollToOffset({ offset: opts.offset, animated: opts.animated });
|
|
692
|
+
return true;
|
|
693
|
+
}
|
|
438
694
|
}
|
|
439
695
|
current = current.return;
|
|
440
696
|
}
|
|
@@ -443,7 +699,23 @@ const COMMANDS = {
|
|
|
443
699
|
function findTestId(fiber) {
|
|
444
700
|
if (!fiber) return null;
|
|
445
701
|
var props = fiber.memoizedProps;
|
|
446
|
-
if (props && props.testID === opts.testId)
|
|
702
|
+
if (props && props.testID === opts.testId) {
|
|
703
|
+
var ancestor = fiber;
|
|
704
|
+
var inactive = false;
|
|
705
|
+
while (ancestor) {
|
|
706
|
+
var ancestorProps = ancestor.memoizedProps;
|
|
707
|
+
var ancestorStyle = ancestorProps && ancestorProps.style;
|
|
708
|
+
if (
|
|
709
|
+
(ancestorProps && ancestorProps.activityState === 0) ||
|
|
710
|
+
(ancestorStyle && ancestorStyle.display === 'none')
|
|
711
|
+
) {
|
|
712
|
+
inactive = true;
|
|
713
|
+
break;
|
|
714
|
+
}
|
|
715
|
+
ancestor = ancestor.return;
|
|
716
|
+
}
|
|
717
|
+
if (!inactive) return fiber;
|
|
718
|
+
}
|
|
447
719
|
return findTestId(fiber.child) || findTestId(fiber.sibling);
|
|
448
720
|
}
|
|
449
721
|
for (var [id] of renderers) {
|
|
@@ -459,11 +731,16 @@ const COMMANDS = {
|
|
|
459
731
|
scrolled = tryScroll(r.current);
|
|
460
732
|
}
|
|
461
733
|
});
|
|
462
|
-
if (scrolled)
|
|
734
|
+
if (scrolled) {
|
|
735
|
+
if (typeof scrolled.then === 'function') return scrolled;
|
|
736
|
+
return { ok: true, testId: opts.testId, offset: opts.offset, animated: opts.animated };
|
|
737
|
+
}
|
|
463
738
|
}
|
|
464
739
|
return { ok: false, error: opts.testId ? 'No scrollable near testID=' + opts.testId : 'No scrollable found' };
|
|
465
740
|
})()`;
|
|
466
|
-
const result =
|
|
741
|
+
const result = intoView
|
|
742
|
+
? await cdpEvalAsync(client, expr)
|
|
743
|
+
: await cdpEval(client, expr);
|
|
467
744
|
return { ...result, deviceName };
|
|
468
745
|
},
|
|
469
746
|
|
|
@@ -841,34 +1118,33 @@ const COMMANDS = {
|
|
|
841
1118
|
},
|
|
842
1119
|
|
|
843
1120
|
async 'show-step-json'(client, args) {
|
|
844
|
-
const
|
|
845
|
-
|
|
846
|
-
try {
|
|
847
|
-
step = JSON.parse(raw);
|
|
848
|
-
} catch (error) {
|
|
849
|
-
throw new Error(`show-step-json requires a JSON step payload: ${error.message}`);
|
|
850
|
-
}
|
|
851
|
-
if (!step || typeof step !== 'object' || Array.isArray(step)) {
|
|
852
|
-
throw new Error('show-step-json requires a JSON object step payload');
|
|
853
|
-
}
|
|
854
|
-
if (typeof step.intent !== 'string' || !step.intent.trim()) {
|
|
855
|
-
throw new Error('show-step-json requires step.intent');
|
|
856
|
-
}
|
|
857
|
-
// HUD derives status/progress from step.id when status is omitted; a
|
|
858
|
-
// non-string id makes statusForStep throw during render. Reject at the boundary.
|
|
859
|
-
if (step.id !== undefined && typeof step.id !== 'string') {
|
|
860
|
-
throw new Error('show-step-json step.id must be a string when provided');
|
|
861
|
-
}
|
|
862
|
-
const payload = JSON.stringify(step);
|
|
863
|
-
await cdpEval(client, `globalThis.__AGENTIC__?.showStep && globalThis.__AGENTIC__.showStep(${payload})`);
|
|
1121
|
+
const step = parseHudStep(args);
|
|
1122
|
+
await applyHudUpdate(client, step);
|
|
864
1123
|
return { ok: true };
|
|
865
1124
|
},
|
|
866
1125
|
|
|
1126
|
+
async 'show-step-json-deferred'(client, args) {
|
|
1127
|
+
const step = parseHudStep(args);
|
|
1128
|
+
if (typeof client.control === 'function') {
|
|
1129
|
+
return client.control('hud-update', { step });
|
|
1130
|
+
}
|
|
1131
|
+
await applyHudUpdate(client, step);
|
|
1132
|
+
return { ok: true, status: 'applied' };
|
|
1133
|
+
},
|
|
1134
|
+
|
|
867
1135
|
async 'hide-step'(client) {
|
|
868
|
-
await
|
|
1136
|
+
await applyHudUpdate(client, null);
|
|
869
1137
|
return { ok: true };
|
|
870
1138
|
},
|
|
871
1139
|
|
|
1140
|
+
async 'hide-step-deferred'(client) {
|
|
1141
|
+
if (typeof client.control === 'function') {
|
|
1142
|
+
return client.control('hud-update', { step: null });
|
|
1143
|
+
}
|
|
1144
|
+
await applyHudUpdate(client, null);
|
|
1145
|
+
return { ok: true, status: 'applied' };
|
|
1146
|
+
},
|
|
1147
|
+
|
|
872
1148
|
async 'profiler-start'(client) {
|
|
873
1149
|
// Hermes CDP exposes the sampling profiler via the Profiler domain.
|
|
874
1150
|
// Output of Profiler.stop is a Chrome-compatible .cpuprofile object.
|
|
@@ -1170,8 +1446,9 @@ Environment:
|
|
|
1170
1446
|
const client = await clientFor(wsUrl);
|
|
1171
1447
|
|
|
1172
1448
|
try {
|
|
1173
|
-
|
|
1174
|
-
|
|
1449
|
+
const platform = command.endsWith('-deferred')
|
|
1450
|
+
? ''
|
|
1451
|
+
: await cdpEval(client, 'globalThis.__AGENTIC__?.platform') || '';
|
|
1175
1452
|
const result = await handler(client, args.slice(1), { deviceName, platform });
|
|
1176
1453
|
console.log(JSON.stringify(result, null, 2));
|
|
1177
1454
|
} finally {
|
|
@@ -40,6 +40,7 @@ const { resolvePort } = require('./lib/config.cjs');
|
|
|
40
40
|
// Hermes uses Node's built-in WebSocket client. The local DevTools proxy uses
|
|
41
41
|
// the package's pinned `ws` server implementation.
|
|
42
42
|
const HANDSHAKE_TIMEOUT_MS = 3000;
|
|
43
|
+
const RUNTIME_ENABLE_TIMEOUT_MS = 60_000;
|
|
43
44
|
|
|
44
45
|
const DISCOVER_ACTIVE_MS = 1000; // a device is unattached — look for it quickly
|
|
45
46
|
const DISCOVER_STEADY_MS = 10000; // all known targets attached — cheap liveness tick
|
|
@@ -311,25 +312,36 @@ function connect(target) {
|
|
|
311
312
|
ws.addEventListener('open', async () => {
|
|
312
313
|
clearTimeout(handshakeTimer);
|
|
313
314
|
session.opened = true;
|
|
315
|
+
// A live inspector socket is enough for broker commands. Do not keep the
|
|
316
|
+
// broker unavailable while Runtime.enable replays a large console buffer or
|
|
317
|
+
// while __AGENTIC__ is being installed after a Hermes context handoff.
|
|
318
|
+
// Commands such as status already report agenticPresent=false and their
|
|
319
|
+
// callers retry against the same bounded action deadline.
|
|
320
|
+
session.brokerReady = true;
|
|
321
|
+
broker.onSessionOpen(deviceId);
|
|
322
|
+
devtoolsProxy.onSessionOpen(deviceId, session);
|
|
323
|
+
process.stderr.write(`console-forwarder: attached ${name}\n`);
|
|
314
324
|
try {
|
|
315
|
-
await sendCommand(
|
|
325
|
+
await sendCommand(
|
|
326
|
+
session,
|
|
327
|
+
'Runtime.enable',
|
|
328
|
+
{},
|
|
329
|
+
RUNTIME_ENABLE_TIMEOUT_MS,
|
|
330
|
+
);
|
|
316
331
|
const evaluation = await sendCommand(session, 'Runtime.evaluate', {
|
|
317
332
|
expression: "typeof globalThis.__AGENTIC__ === 'object'",
|
|
318
333
|
returnByValue: true,
|
|
319
334
|
awaitPromise: false,
|
|
320
335
|
});
|
|
321
336
|
if (evaluation?.result?.value !== true) {
|
|
322
|
-
|
|
337
|
+
process.stderr.write(
|
|
338
|
+
`console-forwarder: ${name} is waiting for __AGENTIC__\n`,
|
|
339
|
+
);
|
|
323
340
|
}
|
|
324
|
-
session.brokerReady = true;
|
|
325
|
-
broker.onSessionOpen(deviceId);
|
|
326
|
-
devtoolsProxy.onSessionOpen(deviceId, session);
|
|
327
|
-
process.stderr.write(`console-forwarder: attached ${name}\n`);
|
|
328
341
|
} catch (error) {
|
|
329
342
|
process.stderr.write(
|
|
330
|
-
`console-forwarder:
|
|
343
|
+
`console-forwarder: console bootstrap pending for ${name}: ${String(error?.message || error).slice(0, 256)}\n`,
|
|
331
344
|
);
|
|
332
|
-
ws.close();
|
|
333
345
|
}
|
|
334
346
|
});
|
|
335
347
|
ws.addEventListener('message', (event) => {
|