@deeeed/metamask-harness 0.43.0 → 0.44.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +304 -34
- 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 +570 -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/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
|
@@ -300,6 +300,7 @@ async function discoverTarget(port, { probe = true } = {}) {
|
|
|
300
300
|
for (const candidate of candidates) {
|
|
301
301
|
if (candidate.webSocketDebuggerUrl === acceptedPinnedCandidate.webSocketDebuggerUrl) {
|
|
302
302
|
return {
|
|
303
|
+
id: acceptedPinnedCandidate.id || '',
|
|
303
304
|
wsUrl: acceptedPinnedCandidate.webSocketDebuggerUrl,
|
|
304
305
|
deviceName: acceptedPinnedCandidate.deviceName || '',
|
|
305
306
|
};
|
|
@@ -312,6 +313,7 @@ async function discoverTarget(port, { probe = true } = {}) {
|
|
|
312
313
|
// platform/device filtering and select the highest-ranked runtime instead.
|
|
313
314
|
if (!probe) {
|
|
314
315
|
return {
|
|
316
|
+
id: candidates[0].id || '',
|
|
315
317
|
wsUrl: candidates[0].webSocketDebuggerUrl,
|
|
316
318
|
deviceName: candidates[0].deviceName || '',
|
|
317
319
|
};
|
|
@@ -331,7 +333,7 @@ async function discoverTarget(port, { probe = true } = {}) {
|
|
|
331
333
|
for (const candidate of candidates) {
|
|
332
334
|
const hasAgentic = await probeTarget(candidate.webSocketDebuggerUrl);
|
|
333
335
|
if (hasAgentic) {
|
|
334
|
-
return { wsUrl: candidate.webSocketDebuggerUrl, deviceName: candidate.deviceName || '' };
|
|
336
|
+
return { id: candidate.id || '', wsUrl: candidate.webSocketDebuggerUrl, deviceName: candidate.deviceName || '' };
|
|
335
337
|
}
|
|
336
338
|
}
|
|
337
339
|
|
|
@@ -348,7 +350,7 @@ async function discoverTarget(port, { probe = true } = {}) {
|
|
|
348
350
|
}
|
|
349
351
|
|
|
350
352
|
// Fallback: return highest page number (most likely the JS runtime)
|
|
351
|
-
return { wsUrl: candidates[0].webSocketDebuggerUrl, deviceName: candidates[0].deviceName || '' };
|
|
353
|
+
return { id: candidates[0].id || '', wsUrl: candidates[0].webSocketDebuggerUrl, deviceName: candidates[0].deviceName || '' };
|
|
352
354
|
}
|
|
353
355
|
|
|
354
356
|
/**
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
3
4
|
import { createRequire } from 'node:module';
|
|
5
|
+
import { resolveMobileToolPath } from '../../library/actions/mobile/platform/tool-paths.mjs';
|
|
4
6
|
|
|
5
7
|
const require = createRequire(import.meta.url);
|
|
6
8
|
const WebSocket = require('ws');
|
|
9
|
+
const { discoverTarget } = require('./bridge-runtime/lib/target-discovery.cjs');
|
|
7
10
|
|
|
8
11
|
function parseArgs(argv) {
|
|
9
|
-
const args = {
|
|
12
|
+
const args = {
|
|
13
|
+
port: process.env.WATCHER_PORT || process.env.METRO_PORT || '8081',
|
|
14
|
+
json: false,
|
|
15
|
+
};
|
|
10
16
|
for (let index = 0; index < argv.length; index += 1) {
|
|
11
17
|
if (argv[index] === '--port') args.port = argv[++index];
|
|
12
18
|
else if (argv[index] === '--json') args.json = true;
|
|
@@ -25,6 +31,21 @@ async function reload(port) {
|
|
|
25
31
|
if (!Number.isInteger(numericPort) || numericPort <= 0) {
|
|
26
32
|
throw new Error(`Metro port is invalid: ${port}`);
|
|
27
33
|
}
|
|
34
|
+
const targetBefore = await readHermesTarget(numericPort);
|
|
35
|
+
if (!targetBefore?.id) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
`No React Native debug target with a stable ID is connected to Metro on port ${numericPort}`,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
const processBefore = readAndroidPid();
|
|
41
|
+
if (androidSerial() && !processBefore) {
|
|
42
|
+
throw new Error(`The Android app is not running on ${androidSerial()}`);
|
|
43
|
+
}
|
|
44
|
+
if (processBefore && hasAndroidThread(processBefore, 'hermes-sampling')) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
'The Android Hermes sampling profiler is active, so a runtime reload can abort the app. Use an explicit app.lifecycle restart.',
|
|
47
|
+
);
|
|
48
|
+
}
|
|
28
49
|
await new Promise((resolve, reject) => {
|
|
29
50
|
const socket = new WebSocket(`ws://127.0.0.1:${numericPort}/message`);
|
|
30
51
|
const timeout = setTimeout(() => {
|
|
@@ -44,15 +65,92 @@ async function reload(port) {
|
|
|
44
65
|
reject(new Error(`Metro is not reachable on port ${numericPort}`));
|
|
45
66
|
});
|
|
46
67
|
});
|
|
68
|
+
const targetAfter = await waitForReload(
|
|
69
|
+
numericPort,
|
|
70
|
+
targetBefore.id,
|
|
71
|
+
processBefore,
|
|
72
|
+
);
|
|
73
|
+
const processAfter = processBefore ? readAndroidPid() : null;
|
|
74
|
+
if (processBefore && processAfter !== processBefore) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
`Android app process ${processBefore} changed to ${processAfter ?? 'none'} during reload.`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
47
79
|
return {
|
|
48
80
|
ok: true,
|
|
49
81
|
adapter: 'mobile',
|
|
50
82
|
method: 'metro-message',
|
|
51
83
|
command: 'reload',
|
|
52
84
|
port: numericPort,
|
|
85
|
+
targetBefore: targetBefore.id,
|
|
86
|
+
targetAfter: targetAfter.id,
|
|
87
|
+
...(processBefore
|
|
88
|
+
? {
|
|
89
|
+
processBefore,
|
|
90
|
+
processAfter,
|
|
91
|
+
processContinuity: 'preserved',
|
|
92
|
+
}
|
|
93
|
+
: {}),
|
|
53
94
|
};
|
|
54
95
|
}
|
|
55
96
|
|
|
97
|
+
async function waitForReload(port, targetBefore, processBefore) {
|
|
98
|
+
const deadline = Date.now() + 30_000;
|
|
99
|
+
while (Date.now() < deadline) {
|
|
100
|
+
if (processBefore && readAndroidPid() !== processBefore) {
|
|
101
|
+
throw new Error(
|
|
102
|
+
`Android app process ${processBefore} exited during reload. Inspect the native crash before using an explicit app.lifecycle restart.`,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
const target = await readHermesTarget(port).catch(() => null);
|
|
106
|
+
if (target && target.id !== targetBefore) return target;
|
|
107
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
108
|
+
}
|
|
109
|
+
throw new Error(
|
|
110
|
+
`React Native did not expose a new Hermes target after reload on port ${port}`,
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function readHermesTarget(port) {
|
|
115
|
+
return discoverTarget(port, { probe: false });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function androidSerial() {
|
|
119
|
+
return process.env.ADB_SERIAL || process.env.ANDROID_SERIAL || '';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function readAndroidPid() {
|
|
123
|
+
const serial = androidSerial();
|
|
124
|
+
if (!serial) return null;
|
|
125
|
+
const adb = resolveMobileToolPath('adb', { required: true });
|
|
126
|
+
const result = spawnSync(
|
|
127
|
+
adb,
|
|
128
|
+
['-s', serial, 'shell', 'pidof', process.env.ANDROID_PACKAGE_ID || 'io.metamask'],
|
|
129
|
+
{ encoding: 'utf8', timeout: 5_000 },
|
|
130
|
+
);
|
|
131
|
+
if (result.status !== 0) return null;
|
|
132
|
+
const pid = Number.parseInt(
|
|
133
|
+
String(result.stdout).trim().split(/\s+/u)[0] ?? '',
|
|
134
|
+
10,
|
|
135
|
+
);
|
|
136
|
+
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function hasAndroidThread(pid, name) {
|
|
140
|
+
const adb = resolveMobileToolPath('adb', { required: true });
|
|
141
|
+
const result = spawnSync(
|
|
142
|
+
adb,
|
|
143
|
+
['-s', androidSerial(), 'shell', 'ps', '-T', '-p', String(pid)],
|
|
144
|
+
{ encoding: 'utf8', timeout: 5_000 },
|
|
145
|
+
);
|
|
146
|
+
return (
|
|
147
|
+
result.status === 0 &&
|
|
148
|
+
String(result.stdout)
|
|
149
|
+
.split(/\r?\n/u)
|
|
150
|
+
.some((line) => line.trim().endsWith(name))
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
56
154
|
const args = parseArgs(process.argv.slice(2));
|
|
57
155
|
reload(args.port)
|
|
58
156
|
.then((result) => {
|
|
@@ -59,6 +59,18 @@ async function prepareMobile(target, opts = {}) {
|
|
|
59
59
|
return { status: EXIT.runtime, output: msg };
|
|
60
60
|
}
|
|
61
61
|
const iosAccessibilityChanged = platform === "ios" && report.decision !== "unknown" && enableIosAccessibility(json);
|
|
62
|
+
const rebuildNative = preflightMode === "rebuild-native" || preflightMode === "clean";
|
|
63
|
+
if (report.decision === "ready" && rebuildNative) {
|
|
64
|
+
const launch = await dispatchActionSequence(
|
|
65
|
+
launchActions(path.resolve(target), clearMetro),
|
|
66
|
+
target,
|
|
67
|
+
platform,
|
|
68
|
+
json,
|
|
69
|
+
preflightMode,
|
|
70
|
+
opts.watcherPort
|
|
71
|
+
);
|
|
72
|
+
return launch.status === 0 ? startMobileConsoleForwarder(target, platform, json, opts.watcherPort) : launch;
|
|
73
|
+
}
|
|
62
74
|
if (report.decision === "ready" && clearMetro) {
|
|
63
75
|
const launch = await dispatchActionSequence(
|
|
64
76
|
withAppRestart(
|
package/dist/adapters.js
CHANGED
|
@@ -526,6 +526,7 @@ const MOBILE_BRIDGE_HANDLERS = {
|
|
|
526
526
|
status: handleMobileStatus,
|
|
527
527
|
navigate: handleMobileNavigate,
|
|
528
528
|
press: handleMobilePress,
|
|
529
|
+
keyPress: handleMobileKeyPress,
|
|
529
530
|
setInput: handleMobileSetInput,
|
|
530
531
|
scroll: handleMobileScroll,
|
|
531
532
|
waitFor: handleMobileWaitFor,
|
|
@@ -585,6 +586,11 @@ async function handleMobilePress(payload, context) {
|
|
|
585
586
|
if (text !== void 0) return bridgeCommand(input, ["press-text", target]);
|
|
586
587
|
return bridgeCommand(input, [longPress ? "long-press-test-id" : "press-test-id", target]);
|
|
587
588
|
}
|
|
589
|
+
async function handleMobileKeyPress(payload, context) {
|
|
590
|
+
const key = scalarText(payload.key, "ui.key_press.key", "Enter");
|
|
591
|
+
const input = mobileUiInput(context, "key_press", payload);
|
|
592
|
+
return bridgeCommand(input, ["key-press", key]);
|
|
593
|
+
}
|
|
588
594
|
async function handleMobileSetInput(payload, context) {
|
|
589
595
|
const input = mobileUiInput(context, "set_input", payload);
|
|
590
596
|
const testId = firstScalarText(payload, ["test_id", "testID"], "ui.set_input");
|
|
@@ -753,7 +759,17 @@ async function handleMobileWaitFor(payload, context) {
|
|
|
753
759
|
return waitForMobileTarget(mobileUiInput(context, "waitFor", payload), payload);
|
|
754
760
|
}
|
|
755
761
|
async function handleMobileHud(payload, context) {
|
|
756
|
-
const
|
|
762
|
+
const automaticProgress = typeof payload.action_name === "string" || context.nodeId === "recipe-complete" && isRecord(payload.progress) && payload.progress.complete === true;
|
|
763
|
+
const captureProgress = payload.action_name === "ui.screenshot" || payload.action_name === "ui.capture_surface";
|
|
764
|
+
const input = mobileUiInput(
|
|
765
|
+
context,
|
|
766
|
+
"hud",
|
|
767
|
+
automaticProgress ? {
|
|
768
|
+
...payload,
|
|
769
|
+
bridge_timeout_ms: captureProgress ? 1e4 : 2e3,
|
|
770
|
+
cdp_timeout_ms: captureProgress ? 1e4 : 2e3
|
|
771
|
+
} : payload
|
|
772
|
+
);
|
|
757
773
|
if (payload.clear === true) {
|
|
758
774
|
try {
|
|
759
775
|
return await bridgeCommand(input, ["hide-step"]);
|
|
@@ -763,7 +779,10 @@ async function handleMobileHud(payload, context) {
|
|
|
763
779
|
}
|
|
764
780
|
const hud = mobileHudPayload(payload, context);
|
|
765
781
|
try {
|
|
766
|
-
const result = await bridgeCommand(input, [
|
|
782
|
+
const result = await bridgeCommand(input, [
|
|
783
|
+
automaticProgress && !captureProgress ? "show-step-json-deferred" : "show-step-json",
|
|
784
|
+
JSON.stringify(hud.step)
|
|
785
|
+
]);
|
|
767
786
|
return { hud: true, nodeId: hud.nodeId, status: hud.status, result };
|
|
768
787
|
} catch (error) {
|
|
769
788
|
return mobileHudSkippedOrThrow(error, { nodeId: hud.nodeId, status: hud.status });
|
|
@@ -777,7 +796,7 @@ async function hideMobileHudOnTeardown(projectRoot, env = {}) {
|
|
|
777
796
|
context: { nodeId: "teardown", projectRoot, artifactsDir: projectRoot, env }
|
|
778
797
|
};
|
|
779
798
|
try {
|
|
780
|
-
await bridgeCommand(input, ["hide-step"]);
|
|
799
|
+
await bridgeCommand(input, ["hide-step-deferred"]);
|
|
781
800
|
} catch {
|
|
782
801
|
}
|
|
783
802
|
}
|
package/dist/commands/call.js
CHANGED
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
emitHealViolation,
|
|
34
34
|
executeWithHealBounds,
|
|
35
35
|
prepareHeal,
|
|
36
|
+
persistRunEffects,
|
|
36
37
|
recoverRunInfra,
|
|
37
38
|
resolveMetaMaskLibrarySources,
|
|
38
39
|
preflightRecipe,
|
|
@@ -52,6 +53,7 @@ import {
|
|
|
52
53
|
import { recipeTrustFailure } from "../recipe-security.js";
|
|
53
54
|
import { isSensitiveKey, recordCommandEvidence, redactStructuredValue } from "../command-journal.js";
|
|
54
55
|
import { closest } from "../command-contract.js";
|
|
56
|
+
import { ProvenanceDriftError } from "../execution-provenance.js";
|
|
55
57
|
async function handleCall(argv) {
|
|
56
58
|
if (argv.includes("--list")) {
|
|
57
59
|
const { options: options2 } = parseArgs(argv, "call");
|
|
@@ -282,11 +284,20 @@ async function handleCall(argv) {
|
|
|
282
284
|
return checkoutBusyOut(json, "call", lock.message, lock.path);
|
|
283
285
|
}
|
|
284
286
|
try {
|
|
287
|
+
const authoredMobileRestart = adapter === "mobile" && resolvedAction === "app.lifecycle" && args.command === "restart";
|
|
285
288
|
const prepared = await prepareHeal(adapter, target, options, json, {
|
|
286
|
-
skipMobileSourceFreshness: adapter === "mobile" &&
|
|
289
|
+
skipMobileSourceFreshness: authoredMobileRestart || adapter === "mobile" && readMobileReleaseArtifactState(target) !== null
|
|
287
290
|
});
|
|
288
291
|
if (typeof prepared === "number") return prepared;
|
|
289
292
|
const { state, heal } = prepared;
|
|
293
|
+
preflightedExecution = await preflightRecipe(
|
|
294
|
+
adapter,
|
|
295
|
+
recipe,
|
|
296
|
+
artifactsDir,
|
|
297
|
+
target,
|
|
298
|
+
actionManifestOverride,
|
|
299
|
+
callRuntimeOptions
|
|
300
|
+
);
|
|
290
301
|
networkObservation = await startRunNetworkObservation(
|
|
291
302
|
adapter,
|
|
292
303
|
target,
|
|
@@ -320,7 +331,8 @@ async function handleCall(argv) {
|
|
|
320
331
|
target,
|
|
321
332
|
actionManifestOverride,
|
|
322
333
|
callRuntimeOptions,
|
|
323
|
-
execution
|
|
334
|
+
execution,
|
|
335
|
+
state
|
|
324
336
|
);
|
|
325
337
|
},
|
|
326
338
|
adapter,
|
|
@@ -337,6 +349,31 @@ async function handleCall(argv) {
|
|
|
337
349
|
networkObservation = void 0;
|
|
338
350
|
await performanceObservation?.finalize().catch(() => void 0);
|
|
339
351
|
performanceObservation = void 0;
|
|
352
|
+
if (error instanceof ProvenanceDriftError) {
|
|
353
|
+
const failure = {
|
|
354
|
+
code: error.code,
|
|
355
|
+
message: error.message,
|
|
356
|
+
userAction: error.userAction,
|
|
357
|
+
provenancePath: error.provenancePath,
|
|
358
|
+
drift: error.drift
|
|
359
|
+
};
|
|
360
|
+
if (json) {
|
|
361
|
+
console.log(JSON.stringify({
|
|
362
|
+
schemaVersion: 1,
|
|
363
|
+
command: "call",
|
|
364
|
+
adapter,
|
|
365
|
+
action: shortName,
|
|
366
|
+
resolvedAction,
|
|
367
|
+
status: "fail",
|
|
368
|
+
error: failure,
|
|
369
|
+
exitCode: error.exitCode
|
|
370
|
+
}, null, 2));
|
|
371
|
+
} else {
|
|
372
|
+
console.error(`\u2717 mm-harness call: ${error.message}`);
|
|
373
|
+
console.error(` Next: ${error.userAction}`);
|
|
374
|
+
}
|
|
375
|
+
return error.exitCode;
|
|
376
|
+
}
|
|
340
377
|
throw error;
|
|
341
378
|
}
|
|
342
379
|
const { result, violation } = executionResult;
|
|
@@ -344,6 +381,7 @@ async function handleCall(argv) {
|
|
|
344
381
|
networkObservation = void 0;
|
|
345
382
|
await performanceObservation?.finalize(result.artifactManifestPath);
|
|
346
383
|
performanceObservation = void 0;
|
|
384
|
+
persistRunEffects(result.summaryPath, result.artifactManifestPath, state);
|
|
347
385
|
if (violation !== null) {
|
|
348
386
|
const conciseFailure = violation.originalError ? conciseFailureForHuman(violation.originalError) : "";
|
|
349
387
|
const example = describedAction ? actionExampleCommand(
|