@deeeed/metamask-harness 0.35.0 → 0.37.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 +39 -0
- package/README.md +9 -1
- package/adapters/extension/artifact-runtime-state.cjs +128 -0
- package/adapters/extension/check-infura-readiness.cjs +102 -0
- package/adapters/extension/inject.mjs +2 -0
- package/adapters/extension/launch-browser.cjs +22 -0
- package/adapters/extension/live.sh +103 -18
- package/adapters/extension/readiness.mjs +77 -36
- package/adapters/extension/snapshot-dist.sh +88 -3
- package/adapters/extension/start-watch.sh +15 -0
- package/adapters/extension/verify.sh +6 -2
- package/adapters/extension/wallet-fixture-state.cjs +3 -1
- package/adapters/manifest.json +25 -1
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +73 -42
- package/adapters/mobile/reset-app-data.sh +154 -0
- package/adapters/shared/log-tui.mjs +1 -1
- package/dist/adapters/extension/artifact-integrity.js +38 -0
- package/dist/adapters/extension/extension-id.js +23 -4
- package/dist/adapters/extension/product-config.js +29 -1
- package/dist/adapters/extension/release-artifact.js +386 -0
- package/dist/adapters/extension/runtime-decision.js +161 -20
- package/dist/adapters/extension/runtime.js +127 -0
- package/dist/adapters/mobile/release-artifact-state.js +124 -0
- package/dist/adapters/mobile/release-artifact.js +295 -0
- package/dist/adapters.js +29 -4
- package/dist/cli-commands.js +1 -1
- package/dist/cli.js +2 -2
- package/dist/command-contract.js +17 -1
- package/dist/commands/call.js +2 -1
- package/dist/commands/device-target.js +5 -0
- package/dist/commands/fixtures.js +106 -31
- package/dist/commands/launch/extension.js +92 -7
- package/dist/commands/launch/index.js +13 -0
- package/dist/commands/launch/mobile.js +2 -0
- package/dist/commands/provision.js +2 -0
- package/dist/commands/run-engine.js +89 -5
- package/dist/commands/run.js +3 -1
- package/dist/commands/runtime-launch.js +178 -10
- package/dist/heal-bounds.js +1 -1
- package/dist/live-adapter-contract.js +3 -1
- package/dist/metamask-action-validation.js +47 -1
- package/dist/mm-harness-cli.js +37 -4
- package/dist/recipe-security.js +3 -0
- package/dist/run-diagnostics.js +1 -1
- package/docs/RECIPES.md +29 -0
- package/docs/RELEASE-QA-CAPABILITY-MAP.md +150 -0
- package/library/actions/extension/perps/perps.mjs +2 -0
- package/library/actions/extension/perps/read_snapshot.mjs +470 -0
- package/library/actions/extension/platform/cdp.mjs +6 -3
- package/library/actions/extension/wallet/import.mjs +201 -0
- package/library/actions/extension/wallet/reset.mjs +98 -0
- package/library/actions/extension/wallet/secret-input.mjs +98 -0
- package/library/actions/extension/wallet/state.mjs +1 -0
- package/library/actions/mobile/analytics/consent-settings.mjs +112 -0
- package/library/actions/mobile/analytics/set_consent.mjs +4 -112
- package/library/actions/mobile/platform/bridge.mjs +8 -0
- package/library/actions/mobile/platform/observe-ui.mjs +84 -2
- package/library/actions/mobile/ui/native-navigation.mjs +225 -0
- package/library/actions/mobile/ui/navigate.mjs +7 -0
- package/library/actions/mobile/wallet/import.mjs +328 -0
- package/library/actions/mobile/wallet/native-ui.mjs +493 -0
- package/library/actions/mobile/wallet/read_state.mjs +16 -0
- package/library/actions/mobile/wallet/reset-helper.mjs +99 -0
- package/library/actions/mobile/wallet/reset.mjs +20 -0
- package/library/actions/shared/ui/locators.mjs +7 -0
- package/library/actions/shared/wallet/import-source.mjs +101 -0
- package/library/manifests/extension.action-manifest.json +228 -0
- package/library/manifests/mobile.action-manifest.json +124 -0
- package/library/recipes/extension/runner/action-validation.recipe.json +12 -1
- package/library/recipes/wallet/import.recipe.json +102 -0
- package/library/recipes/wallet/reset-import.recipe.json +107 -0
- package/package.json +1 -1
- package/scripts/completions.sh +2 -2
|
@@ -64,10 +64,29 @@ function parseArgs(argv) {
|
|
|
64
64
|
return out;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
function extensionBuildRoot(target) {
|
|
68
|
+
const runtimeRoot = path.join(target, recipeRuntimeDir());
|
|
69
|
+
const statePath = path.join(runtimeRoot, 'extension-release-artifact.json');
|
|
70
|
+
if (!fs.existsSync(statePath)) return path.join(target, 'dist/chrome');
|
|
71
|
+
try {
|
|
72
|
+
const stat = fs.lstatSync(statePath);
|
|
73
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size <= 0 || stat.size > 64 * 1024) {
|
|
74
|
+
throw new Error('Loaded release artifact identity is not a regular bounded file.');
|
|
75
|
+
}
|
|
76
|
+
const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
|
|
77
|
+
const expected = path.join(runtimeRoot, process.env.RECIPE_RUNTIME_DIST_DIR || 'runtime-dist');
|
|
78
|
+
if (state.schemaVersion === 1 && path.resolve(state.runtimeDist || '') === expected) return expected;
|
|
79
|
+
} catch (error) {
|
|
80
|
+
throw new Error(`Loaded release artifact identity is invalid: ${error instanceof Error ? error.message : String(error)}`);
|
|
81
|
+
}
|
|
82
|
+
throw new Error('Loaded release artifact identity is invalid.');
|
|
83
|
+
}
|
|
84
|
+
|
|
67
85
|
function readManifest(target) {
|
|
68
|
-
const
|
|
86
|
+
const buildRoot = extensionBuildRoot(target);
|
|
87
|
+
const manifestPath = path.join(buildRoot, 'manifest.json');
|
|
69
88
|
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
70
|
-
return { manifestPath, manifest };
|
|
89
|
+
return { buildRoot, manifestPath, manifest };
|
|
71
90
|
}
|
|
72
91
|
|
|
73
92
|
function manifestExpectedFiles(manifest) {
|
|
@@ -190,6 +209,17 @@ function resolveWebSocket(target) {
|
|
|
190
209
|
}
|
|
191
210
|
|
|
192
211
|
async function cdpEvaluate(target, webSocketDebuggerUrl, expression, timeoutMs = 5000) {
|
|
212
|
+
const result = await cdpCall(
|
|
213
|
+
target,
|
|
214
|
+
webSocketDebuggerUrl,
|
|
215
|
+
'Runtime.evaluate',
|
|
216
|
+
{ expression, awaitPromise: true, returnByValue: true },
|
|
217
|
+
timeoutMs,
|
|
218
|
+
);
|
|
219
|
+
return result?.result?.value ?? null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function cdpCall(target, webSocketDebuggerUrl, method, params, timeoutMs = 5000) {
|
|
193
223
|
assertLocalWebSocketUrl(webSocketDebuggerUrl);
|
|
194
224
|
const WebSocketImpl = resolveWebSocket(target);
|
|
195
225
|
if (!WebSocketImpl) return { skipped: true, reason: 'WebSocket unavailable in this Node runtime' };
|
|
@@ -206,8 +236,8 @@ async function cdpEvaluate(target, webSocketDebuggerUrl, expression, timeoutMs =
|
|
|
206
236
|
const onOpen = () => {
|
|
207
237
|
ws.send(JSON.stringify({
|
|
208
238
|
id: 1,
|
|
209
|
-
method
|
|
210
|
-
params
|
|
239
|
+
method,
|
|
240
|
+
params,
|
|
211
241
|
}));
|
|
212
242
|
};
|
|
213
243
|
const onMessage = (event) => {
|
|
@@ -220,11 +250,11 @@ async function cdpEvaluate(target, webSocketDebuggerUrl, expression, timeoutMs =
|
|
|
220
250
|
reject(new Error(msg.error.message || JSON.stringify(msg.error)));
|
|
221
251
|
return;
|
|
222
252
|
}
|
|
223
|
-
resolve(msg.result
|
|
253
|
+
resolve(msg.result ?? null);
|
|
224
254
|
};
|
|
225
255
|
const onError = (err) => {
|
|
226
256
|
clearTimeout(timer);
|
|
227
|
-
reject(new Error(`CDP websocket error
|
|
257
|
+
reject(new Error(`CDP websocket error during ${method}: ${err?.message || err || 'unknown'}`));
|
|
228
258
|
};
|
|
229
259
|
if (typeof ws.on === 'function') {
|
|
230
260
|
ws.on('open', onOpen);
|
|
@@ -286,6 +316,23 @@ async function openExtensionPage(cdpPort, extensionId, pagePath) {
|
|
|
286
316
|
}
|
|
287
317
|
}
|
|
288
318
|
|
|
319
|
+
const PAGE_UI_EXPRESSION = `(() => {
|
|
320
|
+
const text = document.body?.innerText || '';
|
|
321
|
+
const cell = (sel) => document.querySelector(sel)?.innerText?.trim() || '';
|
|
322
|
+
const errorBoundaryMessage = cell('[data-testid="error-page-error-message"]');
|
|
323
|
+
const errorBoundaryName = cell('[data-testid="error-page-error-name"]');
|
|
324
|
+
const hasErrorBoundary = Boolean(errorBoundaryMessage) || Boolean(errorBoundaryName);
|
|
325
|
+
return {
|
|
326
|
+
title: document.title,
|
|
327
|
+
url: location.href,
|
|
328
|
+
textSample: text.slice(0, 500),
|
|
329
|
+
hasStartupError: /MetaMask had trouble starting|Background connection unresponsive|Unknown Infura network/i.test(text),
|
|
330
|
+
hasErrorBoundary,
|
|
331
|
+
errorBoundaryName,
|
|
332
|
+
errorBoundaryMessage: errorBoundaryMessage || (hasErrorBoundary ? text.slice(0, 300) : ''),
|
|
333
|
+
};
|
|
334
|
+
})()`;
|
|
335
|
+
|
|
289
336
|
// The page path only, with any '#fragment' or '?query' stripped. The app router
|
|
290
337
|
// rewrites home.html to home.html#/ once loaded, so an exact '/home.html' suffix
|
|
291
338
|
// match must compare against this, not the raw url.
|
|
@@ -349,29 +396,7 @@ async function inspectCdp(target, cdpPort, expectedExtensionId, expectedServiceW
|
|
|
349
396
|
// tab has none; skip inspection and report it rather than opening a duplicate.
|
|
350
397
|
if (pageTarget && typeof pageTarget.webSocketDebuggerUrl === 'string') {
|
|
351
398
|
const slotId = readSlotId(target);
|
|
352
|
-
ui = await cdpEvaluate(
|
|
353
|
-
target,
|
|
354
|
-
pageTarget.webSocketDebuggerUrl,
|
|
355
|
-
`(() => {
|
|
356
|
-
const text = document.body?.innerText || '';
|
|
357
|
-
const cell = (sel) => document.querySelector(sel)?.innerText?.trim() || '';
|
|
358
|
-
// React error-boundary screen (ui/pages/error-page); deterministic testids.
|
|
359
|
-
const errorBoundaryMessage = cell('[data-testid="error-page-error-message"]');
|
|
360
|
-
const errorBoundaryName = cell('[data-testid="error-page-error-name"]');
|
|
361
|
-
// Testid-only trigger: ui/pages/error-page reliably renders these testids.
|
|
362
|
-
// Avoid matching the visible phrase in body text (false positives).
|
|
363
|
-
const hasErrorBoundary = Boolean(errorBoundaryMessage) || Boolean(errorBoundaryName);
|
|
364
|
-
return {
|
|
365
|
-
title: document.title,
|
|
366
|
-
url: location.href,
|
|
367
|
-
textSample: text.slice(0, 500),
|
|
368
|
-
hasStartupError: /MetaMask had trouble starting|Background connection unresponsive|Unknown Infura network/i.test(text),
|
|
369
|
-
hasErrorBoundary,
|
|
370
|
-
errorBoundaryName,
|
|
371
|
-
errorBoundaryMessage: errorBoundaryMessage || (hasErrorBoundary ? text.slice(0, 300) : ''),
|
|
372
|
-
};
|
|
373
|
-
})()`,
|
|
374
|
-
);
|
|
399
|
+
ui = await cdpEvaluate(target, pageTarget.webSocketDebuggerUrl, PAGE_UI_EXPRESSION);
|
|
375
400
|
pageInspected = Boolean(ui) && !ui.skipped;
|
|
376
401
|
if (pageInspected && ui.hasStartupError) {
|
|
377
402
|
throw Object.assign(new Error('MetaMask extension page loaded startup error UI'), {
|
|
@@ -385,9 +410,25 @@ async function inspectCdp(target, cdpPort, expectedExtensionId, expectedServiceW
|
|
|
385
410
|
});
|
|
386
411
|
}
|
|
387
412
|
if (pageInspected && String(ui.url || '').startsWith('chrome-error://')) {
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
413
|
+
const destination = `chrome-extension://${selectedExtensionId}/${String(extensionPagePath || 'home.html').replace(/^\/+/u, '')}`;
|
|
414
|
+
await cdpCall(
|
|
415
|
+
target,
|
|
416
|
+
pageTarget.webSocketDebuggerUrl,
|
|
417
|
+
'Page.navigate',
|
|
418
|
+
{ url: destination },
|
|
419
|
+
);
|
|
420
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
421
|
+
ui = await cdpEvaluate(target, pageTarget.webSocketDebuggerUrl, PAGE_UI_EXPRESSION);
|
|
422
|
+
if (
|
|
423
|
+
!ui ||
|
|
424
|
+
String(ui.url || '').startsWith('chrome-error://') ||
|
|
425
|
+
ui.hasStartupError ||
|
|
426
|
+
ui.hasErrorBoundary
|
|
427
|
+
) {
|
|
428
|
+
throw Object.assign(new Error('MetaMask extension page loaded Chrome error UI'), {
|
|
429
|
+
report: { cdp: { browser: version.Browser || 'unknown', selectedExtensionId, ui } },
|
|
430
|
+
});
|
|
431
|
+
}
|
|
391
432
|
}
|
|
392
433
|
if (pageInspected && slotId) {
|
|
393
434
|
const stampedTitle = await cdpEvaluate(
|
|
@@ -419,12 +460,12 @@ async function main() {
|
|
|
419
460
|
const args = parseArgs(process.argv.slice(2));
|
|
420
461
|
const target = path.resolve(args.target);
|
|
421
462
|
const checks = [];
|
|
422
|
-
const { manifestPath, manifest } = readManifest(target);
|
|
463
|
+
const { buildRoot, manifestPath, manifest } = readManifest(target);
|
|
423
464
|
const expectedFiles = manifestExpectedFiles(manifest);
|
|
424
465
|
const missingFiles = [];
|
|
425
466
|
for (const rel of expectedFiles) {
|
|
426
|
-
const exists = fs.existsSync(path.join(
|
|
427
|
-
checks.push({ name:
|
|
467
|
+
const exists = fs.existsSync(path.join(buildRoot, rel));
|
|
468
|
+
checks.push({ name: `${path.relative(target, buildRoot)}/${rel}`, status: exists ? 'pass' : 'fail' });
|
|
428
469
|
if (!exists) missingFiles.push(rel);
|
|
429
470
|
}
|
|
430
471
|
if (missingFiles.length > 0) {
|
|
@@ -434,7 +475,7 @@ async function main() {
|
|
|
434
475
|
);
|
|
435
476
|
}
|
|
436
477
|
|
|
437
|
-
const defaultPage = fs.existsSync(path.join(
|
|
478
|
+
const defaultPage = fs.existsSync(path.join(buildRoot, 'home.html'))
|
|
438
479
|
? 'home.html'
|
|
439
480
|
: manifestDefaultPage(manifest);
|
|
440
481
|
const report = {
|
|
@@ -9,9 +9,12 @@
|
|
|
9
9
|
#
|
|
10
10
|
# Inputs (flags):
|
|
11
11
|
# --dist <dir> source dist (required, e.g. <repo>/dist/chrome)
|
|
12
|
+
# --target <dir> absolute checkout containment root
|
|
13
|
+
# --runtime-root <dir> absolute directory that owns the snapshot
|
|
12
14
|
# --runtime-dist <dir> snapshot destination (required, recreated)
|
|
13
15
|
# --wait-iterations <n> manifest wait loop length, 2s each (default 180)
|
|
14
16
|
# --summary <file> optional standard summary.json
|
|
17
|
+
# --exact preserve every source entry (release artifacts)
|
|
15
18
|
#
|
|
16
19
|
# Outputs:
|
|
17
20
|
# <runtime-dist>/ snapshot (excludes _metadata); optional --summary file
|
|
@@ -24,24 +27,32 @@
|
|
|
24
27
|
set -euo pipefail
|
|
25
28
|
|
|
26
29
|
DIST=""
|
|
30
|
+
TARGET_ROOT=""
|
|
31
|
+
RUNTIME_ROOT=""
|
|
27
32
|
RUNTIME_DIST=""
|
|
28
33
|
WAIT_ITERATIONS=180
|
|
29
34
|
SUMMARY=""
|
|
35
|
+
EXACT=false
|
|
30
36
|
require_value() { [ "$#" -ge 2 ] || { echo "Missing value for $1" >&2; exit 2; }; }
|
|
31
37
|
while [ "$#" -gt 0 ]; do
|
|
32
38
|
case "$1" in
|
|
33
39
|
--dist) require_value "$@"; DIST="$2"; shift 2 ;;
|
|
40
|
+
--target) require_value "$@"; TARGET_ROOT="$2"; shift 2 ;;
|
|
41
|
+
--runtime-root) require_value "$@"; RUNTIME_ROOT="$2"; shift 2 ;;
|
|
34
42
|
--runtime-dist) require_value "$@"; RUNTIME_DIST="$2"; shift 2 ;;
|
|
35
43
|
--wait-iterations) require_value "$@"; WAIT_ITERATIONS="$2"; shift 2 ;;
|
|
36
44
|
--summary) require_value "$@"; SUMMARY="$2"; shift 2 ;;
|
|
45
|
+
--exact) EXACT=true; shift ;;
|
|
37
46
|
-h|--help)
|
|
38
|
-
echo "Usage: snapshot-dist.sh --dist <dir> --runtime-dist <dir> [--wait-iterations <n>] [--summary <file>]"
|
|
47
|
+
echo "Usage: snapshot-dist.sh --dist <dir> --target <dir> --runtime-root <dir> --runtime-dist <dir> [--wait-iterations <n>] [--summary <file>]"
|
|
39
48
|
exit 0
|
|
40
49
|
;;
|
|
41
50
|
*) echo "Unknown arg: $1" >&2; exit 2 ;;
|
|
42
51
|
esac
|
|
43
52
|
done
|
|
44
53
|
[ -n "$DIST" ] || { echo "Missing --dist" >&2; exit 2; }
|
|
54
|
+
[ -n "$TARGET_ROOT" ] || { echo "Missing --target" >&2; exit 2; }
|
|
55
|
+
[ -n "$RUNTIME_ROOT" ] || { echo "Missing --runtime-root" >&2; exit 2; }
|
|
45
56
|
[ -n "$RUNTIME_DIST" ] || { echo "Missing --runtime-dist" >&2; exit 2; }
|
|
46
57
|
case "$WAIT_ITERATIONS" in ''|*[!0-9]*) echo "Invalid --wait-iterations (must be numeric): $WAIT_ITERATIONS" >&2; exit 2 ;; esac
|
|
47
58
|
|
|
@@ -73,8 +84,82 @@ done
|
|
|
73
84
|
test -f "$DIST/manifest.json" || { echo "snapshot-dist: no manifest at $DIST/manifest.json" >&2; exit 1; }
|
|
74
85
|
|
|
75
86
|
echo "[recipe-harness] snapshotting dist -> runtime-dist: $RUNTIME_DIST" >&2
|
|
76
|
-
|
|
77
|
-
|
|
87
|
+
node - "$DIST" "$TARGET_ROOT" "$RUNTIME_ROOT" "$RUNTIME_DIST" <<'NODE'
|
|
88
|
+
const fs = require('node:fs');
|
|
89
|
+
const path = require('node:path');
|
|
90
|
+
|
|
91
|
+
const [sourceInput, targetInput, rootInput, destinationInput] = process.argv.slice(2);
|
|
92
|
+
const source = path.resolve(sourceInput);
|
|
93
|
+
const target = path.resolve(targetInput);
|
|
94
|
+
const root = path.resolve(rootInput);
|
|
95
|
+
const destination = path.resolve(destinationInput);
|
|
96
|
+
|
|
97
|
+
function refuse(message) {
|
|
98
|
+
throw new Error(`snapshot-dist: ${message}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (![targetInput, rootInput, destinationInput].every(path.isAbsolute)) {
|
|
102
|
+
refuse('--target, --runtime-root, and --runtime-dist must be absolute paths.');
|
|
103
|
+
}
|
|
104
|
+
const targetStat = fs.lstatSync(target, { throwIfNoEntry: false });
|
|
105
|
+
if (!targetStat?.isDirectory() || targetStat.isSymbolicLink()) {
|
|
106
|
+
refuse(`target is not a regular directory: ${target}.`);
|
|
107
|
+
}
|
|
108
|
+
const rootRelative = path.relative(target, root);
|
|
109
|
+
if (!rootRelative || rootRelative === '..' || rootRelative.startsWith(`..${path.sep}`) || path.isAbsolute(rootRelative)) {
|
|
110
|
+
refuse(`runtime root must be a child of ${target}.`);
|
|
111
|
+
}
|
|
112
|
+
const relative = path.relative(root, destination);
|
|
113
|
+
if (!relative || relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
114
|
+
refuse(`runtime destination must be a child of ${root}.`);
|
|
115
|
+
}
|
|
116
|
+
if (
|
|
117
|
+
source === destination ||
|
|
118
|
+
source.startsWith(`${destination}${path.sep}`) ||
|
|
119
|
+
destination.startsWith(`${source}${path.sep}`)
|
|
120
|
+
) {
|
|
121
|
+
refuse('source and runtime destination must be separate directory trees.');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function ensureDirectoryTree(base, relativePath) {
|
|
125
|
+
let current = base;
|
|
126
|
+
const components = relativePath.split(path.sep).filter(Boolean);
|
|
127
|
+
for (const component of components) {
|
|
128
|
+
current = path.join(current, component);
|
|
129
|
+
const stat = fs.lstatSync(current, { throwIfNoEntry: false });
|
|
130
|
+
if (!stat) {
|
|
131
|
+
fs.mkdirSync(current, { mode: 0o700 });
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
135
|
+
refuse(`runtime path contains an unsafe entry: ${current}.`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
ensureDirectoryTree(target, rootRelative);
|
|
141
|
+
ensureDirectoryTree(root, path.relative(root, path.dirname(destination)));
|
|
142
|
+
const realTarget = fs.realpathSync(target);
|
|
143
|
+
const realRoot = fs.realpathSync(root);
|
|
144
|
+
const realParent = fs.realpathSync(path.dirname(destination));
|
|
145
|
+
if (
|
|
146
|
+
!realRoot.startsWith(`${realTarget}${path.sep}`) ||
|
|
147
|
+
(realParent !== realRoot && !realParent.startsWith(`${realRoot}${path.sep}`))
|
|
148
|
+
) {
|
|
149
|
+
refuse(`runtime destination escapes ${root}.`);
|
|
150
|
+
}
|
|
151
|
+
const destinationStat = fs.lstatSync(destination, { throwIfNoEntry: false });
|
|
152
|
+
if (destinationStat && (!destinationStat.isDirectory() || destinationStat.isSymbolicLink())) {
|
|
153
|
+
refuse(`runtime destination is not a regular directory: ${destination}.`);
|
|
154
|
+
}
|
|
155
|
+
if (destinationStat) fs.rmSync(destination, { recursive: true });
|
|
156
|
+
fs.mkdirSync(destination, { mode: 0o700 });
|
|
157
|
+
NODE
|
|
158
|
+
if $EXACT; then
|
|
159
|
+
rsync -a --delete "$DIST/" "$RUNTIME_DIST/" || exit 1
|
|
160
|
+
else
|
|
161
|
+
rsync -a --delete --exclude _metadata "$DIST/" "$RUNTIME_DIST/" || exit 1
|
|
162
|
+
fi
|
|
78
163
|
echo "[recipe-harness] runtime-dist snapshot complete" >&2
|
|
79
164
|
|
|
80
165
|
# Freshness guard: the loaded runtime-dist must match dist/chrome's git id. A
|
|
@@ -142,6 +142,21 @@ trap finish EXIT
|
|
|
142
142
|
|
|
143
143
|
mkdir -p "$RUNTIME_DIR"
|
|
144
144
|
|
|
145
|
+
PRODUCT_CONFIG_MODULE="$SCRIPT_DIR/lib/product-config.cjs" \
|
|
146
|
+
node - "$PWD" "$RUNTIME_DIR/extension-product-config.sha256" <<'NODE'
|
|
147
|
+
const crypto = require('node:crypto');
|
|
148
|
+
const fs = require('node:fs');
|
|
149
|
+
const config = require(process.env.PRODUCT_CONFIG_MODULE);
|
|
150
|
+
const [target, destination] = process.argv.slice(2);
|
|
151
|
+
const resolution = config.resolveExtensionInfuraProjectId(target, process.env);
|
|
152
|
+
if (resolution.kind !== 'configured') {
|
|
153
|
+
fs.rmSync(destination, { force: true });
|
|
154
|
+
process.exit(0);
|
|
155
|
+
}
|
|
156
|
+
const fingerprint = crypto.createHash('sha256').update(resolution.value).digest('hex');
|
|
157
|
+
fs.writeFileSync(destination, `${fingerprint}\n`);
|
|
158
|
+
NODE
|
|
159
|
+
|
|
145
160
|
watcher_pids() {
|
|
146
161
|
ps -axo pid=,command= | while read -r current_pid command; do
|
|
147
162
|
[ -n "${current_pid:-}" ] || continue
|
|
@@ -344,14 +344,18 @@ let r = {};
|
|
|
344
344
|
try { r = JSON.parse(fs.readFileSync(dir + "/runtime-decision.json", "utf8")); } catch {}
|
|
345
345
|
const c = r.checks || {};
|
|
346
346
|
const dist = c.dist || { status: "unknown" };
|
|
347
|
-
const distMsg = dist.
|
|
347
|
+
const distMsg = dist.source === "release-artifact"
|
|
348
|
+
? ("release artifact " + (dist.manifestVersion || "?") + " sha256=" + (dist.artifactSha256 || "?") + " matches the loaded runtime snapshot.")
|
|
349
|
+
: dist.status === "fresh" ? "dist id matches HEAD; no uncommitted source."
|
|
348
350
|
: dist.status === "stale" ? (dist.reason === "uncommitted-source"
|
|
349
351
|
? ((dist.modified ? dist.modified.length : "some") + " uncommitted source file(s); rebuild or commit.")
|
|
350
352
|
: ("dist id " + (dist.distGitId || "?") + " != HEAD " + (dist.head || "?") + "; rebuild."))
|
|
351
353
|
: dist.status === "no-build" ? "no dist/chrome build."
|
|
352
354
|
: "no git id in dist or not a git checkout; cannot prove parity.";
|
|
353
355
|
fs.writeFileSync(dir + "/dist-freshness.json", JSON.stringify({ ...dist, message: distMsg }));
|
|
354
|
-
const bl = c.
|
|
356
|
+
const bl = c.releaseArtifact?.status === "valid"
|
|
357
|
+
? { status: "no-watch", source: "release-artifact" }
|
|
358
|
+
: (c.buildLog || { status: "unknown" });
|
|
355
359
|
const blMsg = bl.status === "ok" ? "webpack compiled."
|
|
356
360
|
: bl.status === "no-watch" ? "no webpack watch log; build-health n/a (e.g. one-shot build)."
|
|
357
361
|
: bl.status === "building" ? "webpack has not reported a successful compile yet."
|
|
@@ -658,7 +658,9 @@ async function detectExtension(context, extensionDir, extensionIdFile) {
|
|
|
658
658
|
}
|
|
659
659
|
|
|
660
660
|
for (const candidate of candidates) {
|
|
661
|
-
const page =
|
|
661
|
+
const page = context.pages().find(
|
|
662
|
+
(candidatePage) => extensionIdFromUrl(candidatePage.url()) === candidate.id,
|
|
663
|
+
) || await context.newPage();
|
|
662
664
|
try {
|
|
663
665
|
await page.goto(`chrome-extension://${candidate.id}/home.html`, {
|
|
664
666
|
waitUntil: 'domcontentloaded',
|
package/adapters/manifest.json
CHANGED
|
@@ -58,6 +58,14 @@
|
|
|
58
58
|
"inputs": "--platform ios|android --target --port; env MOBILE_BUNDLE_PREWARM, MOBILE_BUNDLE_PREWARM_TIMEOUT",
|
|
59
59
|
"outputs": "progress on stderr; exit 0 bundle ready / 1 timeout / 2 bad args"
|
|
60
60
|
},
|
|
61
|
+
{
|
|
62
|
+
"id": "mobile/reset-app-data",
|
|
63
|
+
"entry": "adapters/mobile/reset-app-data.sh",
|
|
64
|
+
"kind": "bash",
|
|
65
|
+
"purpose": "Reset exactly one installed MetaMask Mobile app while preserving its install for fixture reapplication.",
|
|
66
|
+
"inputs": "--platform ios|android --target --simulator --adb-serial; env IOS_BUNDLE_ID, ANDROID_PACKAGE_ID",
|
|
67
|
+
"outputs": "selected app data cleared and app restored on iOS / cleared on Android; exit 0/1/2"
|
|
68
|
+
},
|
|
61
69
|
{
|
|
62
70
|
"id": "mobile/open-device",
|
|
63
71
|
"entry": "adapters/mobile/open-device.sh",
|
|
@@ -162,6 +170,14 @@
|
|
|
162
170
|
"inputs": "--target --manifest",
|
|
163
171
|
"outputs": "runtime manifest _flags.testing.infuraProjectId; exit 0/1"
|
|
164
172
|
},
|
|
173
|
+
{
|
|
174
|
+
"id": "extension/check-infura-readiness",
|
|
175
|
+
"entry": "adapters/extension/check-infura-readiness.cjs",
|
|
176
|
+
"kind": "node",
|
|
177
|
+
"purpose": "Reject compiled Infura credentials that are stale or unreachable on Ethereum or Linea before Chromium starts.",
|
|
178
|
+
"inputs": "--target --runtime-dist",
|
|
179
|
+
"outputs": "readiness result without credential values; exit 0/1/2"
|
|
180
|
+
},
|
|
165
181
|
{
|
|
166
182
|
"id": "extension/stamp-runtime-title",
|
|
167
183
|
"entry": "adapters/extension/stamp-runtime-title.cjs",
|
|
@@ -170,6 +186,14 @@
|
|
|
170
186
|
"inputs": "--target --runtime-dist; optional --runtime-dir",
|
|
171
187
|
"outputs": "home.html and sidepanel.html titles updated in the isolated runtime only; exit 0/1"
|
|
172
188
|
},
|
|
189
|
+
{
|
|
190
|
+
"id": "extension/artifact-runtime-state",
|
|
191
|
+
"entry": "adapters/extension/artifact-runtime-state.cjs",
|
|
192
|
+
"kind": "node",
|
|
193
|
+
"purpose": "Record or clear the acquired release artifact identity bound to the loaded runtime snapshot.",
|
|
194
|
+
"inputs": "set|clear --target --runtime-dir; set also requires --source-dir --runtime-dist --provenance",
|
|
195
|
+
"outputs": "<runtime-dir>/extension-release-artifact.json or removal; exit 0/1/2"
|
|
196
|
+
},
|
|
173
197
|
{
|
|
174
198
|
"id": "extension/stop-viewers",
|
|
175
199
|
"entry": "adapters/extension/stop-viewers.sh",
|
|
@@ -191,7 +215,7 @@
|
|
|
191
215
|
"entry": "adapters/extension/snapshot-dist.sh",
|
|
192
216
|
"kind": "bash",
|
|
193
217
|
"purpose": "Runtime-dist snapshot (rsync, excludes _metadata) with from-git-id freshness guard.",
|
|
194
|
-
"inputs": "--dist --runtime-dist --wait-iterations --summary",
|
|
218
|
+
"inputs": "--dist --target --runtime-root --runtime-dist --wait-iterations --summary",
|
|
195
219
|
"outputs": "<runtime-dist>/ snapshot; optional summary; exit 0/1/2"
|
|
196
220
|
},
|
|
197
221
|
{
|
|
@@ -84,6 +84,65 @@ const NESTED_ROUTE_PARENTS = {
|
|
|
84
84
|
DeveloperOptions: 'Settings', NotificationsSettings: 'Settings',
|
|
85
85
|
};
|
|
86
86
|
|
|
87
|
+
async function setInput(client, testId, value, { deviceName } = {}, redact = false) {
|
|
88
|
+
if (!testId) {
|
|
89
|
+
throw new Error('Usage: set-input <testId> <value>');
|
|
90
|
+
}
|
|
91
|
+
const expr = `(function() {
|
|
92
|
+
if (globalThis.__AGENTIC__?.setInput) return globalThis.__AGENTIC__.setInput(${JSON.stringify(testId)}, ${JSON.stringify(value)});
|
|
93
|
+
var hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
94
|
+
if (!hook) return { ok: false, error: 'No React DevTools hook' };
|
|
95
|
+
var renderers = hook.renderers;
|
|
96
|
+
if (!renderers) return { ok: false, error: 'No renderers' };
|
|
97
|
+
var getFiberRoots = hook.getFiberRoots;
|
|
98
|
+
function findByTestId(fiber) {
|
|
99
|
+
if (!fiber) return null;
|
|
100
|
+
var props = fiber.memoizedProps;
|
|
101
|
+
if (props && props.testID === ${JSON.stringify(testId)}) return fiber;
|
|
102
|
+
return findByTestId(fiber.child) || findByTestId(fiber.sibling);
|
|
103
|
+
}
|
|
104
|
+
for (var [id] of renderers) {
|
|
105
|
+
var roots = getFiberRoots ? getFiberRoots(id) : undefined;
|
|
106
|
+
if (!roots) continue;
|
|
107
|
+
var result = null;
|
|
108
|
+
roots.forEach(function(r) {
|
|
109
|
+
if (result) return;
|
|
110
|
+
var fiber = findByTestId(r.current);
|
|
111
|
+
if (!fiber) return;
|
|
112
|
+
var cur = fiber;
|
|
113
|
+
while (cur) {
|
|
114
|
+
if (cur.memoizedProps && typeof cur.memoizedProps.onChangeText === 'function') {
|
|
115
|
+
cur.memoizedProps.onChangeText(${JSON.stringify(value)});
|
|
116
|
+
result = { ok: true, testId: ${JSON.stringify(testId)} };
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
cur = cur.return || null;
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
if (result) return result;
|
|
123
|
+
}
|
|
124
|
+
return { ok: false, error: 'No component with testID=' + ${JSON.stringify(testId)} + ' found or no onChangeText' };
|
|
125
|
+
})()`;
|
|
126
|
+
let result;
|
|
127
|
+
try {
|
|
128
|
+
result = await cdpEval(client, expr);
|
|
129
|
+
} catch (error) {
|
|
130
|
+
if (redact) {
|
|
131
|
+
throw new Error(`Secret input could not be applied for testID ${testId}.`);
|
|
132
|
+
}
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
if (redact && result?.ok === false) {
|
|
136
|
+
result = { ok: false, error: 'Secret input could not be applied.' };
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
...result,
|
|
140
|
+
testId,
|
|
141
|
+
value: redact ? '<redacted>' : value,
|
|
142
|
+
deviceName,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
87
146
|
const COMMANDS = {
|
|
88
147
|
async navigate(client, args, { deviceName, platform } = {}) {
|
|
89
148
|
const routeName = ROUTE_ALIASES[args[0]] || args[0];
|
|
@@ -608,50 +667,21 @@ const COMMANDS = {
|
|
|
608
667
|
};
|
|
609
668
|
},
|
|
610
669
|
|
|
611
|
-
async 'set-input'(client, args,
|
|
670
|
+
async 'set-input'(client, args, context = {}) {
|
|
671
|
+
return setInput(client, args[0], args.slice(1).join(' '), context);
|
|
672
|
+
},
|
|
673
|
+
|
|
674
|
+
async 'set-input-file'(client, args, context = {}) {
|
|
612
675
|
const testId = args[0];
|
|
613
|
-
const
|
|
614
|
-
if (!testId) {
|
|
615
|
-
throw new Error('Usage: set-input <testId> <value>');
|
|
676
|
+
const file = args[1];
|
|
677
|
+
if (!testId || !file) {
|
|
678
|
+
throw new Error('Usage: set-input-file <testId> <value-file>');
|
|
616
679
|
}
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
var renderers = hook.renderers;
|
|
623
|
-
if (!renderers) return { ok: false, error: 'No renderers' };
|
|
624
|
-
var getFiberRoots = hook.getFiberRoots;
|
|
625
|
-
function findByTestId(fiber) {
|
|
626
|
-
if (!fiber) return null;
|
|
627
|
-
var props = fiber.memoizedProps;
|
|
628
|
-
if (props && props.testID === ${JSON.stringify(testId)}) return fiber;
|
|
629
|
-
return findByTestId(fiber.child) || findByTestId(fiber.sibling);
|
|
630
|
-
}
|
|
631
|
-
for (var [id] of renderers) {
|
|
632
|
-
var roots = getFiberRoots ? getFiberRoots(id) : undefined;
|
|
633
|
-
if (!roots) continue;
|
|
634
|
-
var result = null;
|
|
635
|
-
roots.forEach(function(r) {
|
|
636
|
-
if (result) return;
|
|
637
|
-
var fiber = findByTestId(r.current);
|
|
638
|
-
if (!fiber) return;
|
|
639
|
-
var cur = fiber;
|
|
640
|
-
while (cur) {
|
|
641
|
-
if (cur.memoizedProps && typeof cur.memoizedProps.onChangeText === 'function') {
|
|
642
|
-
cur.memoizedProps.onChangeText(${JSON.stringify(value)});
|
|
643
|
-
result = { ok: true, testId: ${JSON.stringify(testId)}, value: ${JSON.stringify(value)} };
|
|
644
|
-
return;
|
|
645
|
-
}
|
|
646
|
-
cur = cur.return || null;
|
|
647
|
-
}
|
|
648
|
-
});
|
|
649
|
-
if (result) return result;
|
|
650
|
-
}
|
|
651
|
-
return { ok: false, error: 'No component with testID=' + ${JSON.stringify(testId)} + ' found or no onChangeText' };
|
|
652
|
-
})()`;
|
|
653
|
-
const result = await cdpEval(client, expr);
|
|
654
|
-
return { ...result, testId, value, deviceName };
|
|
680
|
+
const stat = fs.lstatSync(file);
|
|
681
|
+
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
682
|
+
throw new Error('set-input-file requires a regular value file.');
|
|
683
|
+
}
|
|
684
|
+
return setInput(client, testId, fs.readFileSync(file, 'utf8'), context, true);
|
|
655
685
|
},
|
|
656
686
|
|
|
657
687
|
async 'sentry-debug'(client, args, { deviceName } = {}) {
|
|
@@ -977,6 +1007,7 @@ Commands:
|
|
|
977
1007
|
Scroll a ScrollView/FlatList
|
|
978
1008
|
measure-scroll-transition <json> Scroll and measure a visible target using one CDP session
|
|
979
1009
|
set-input <testId> <value> Set text input value by testID (calls onChangeText)
|
|
1010
|
+
set-input-file <testId> <value-file> Set text input from a regular file and redact the value
|
|
980
1011
|
sentry-debug [enable|disable] Patch Sentry to log errors to console with [SENTRY-DEBUG] prefix
|
|
981
1012
|
unlock <password> Unlock wallet (inject password + press login button via fiber tree)
|
|
982
1013
|
profiler-start Start Hermes sampling profiler
|