@deeeed/metamask-harness 0.13.0 → 0.14.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 +43 -4
- package/adapters/extension/inject.mjs +1 -0
- package/adapters/extension/launch-browser.cjs +15 -3
- package/adapters/extension/lib/extension-id.cjs +36 -0
- package/adapters/extension/live.sh +5 -3
- package/adapters/extension/readiness.mjs +48 -0
- package/adapters/extension/reattach.sh +163 -55
- package/adapters/extension/sidepanel-toggle.sh +96 -24
- package/adapters/extension/verify.sh +15 -4
- package/adapters/extension/wallet-fixture-state.cjs +6 -11
- package/adapters/mobile/open-device.sh +32 -5
- package/adapters/mobile/start-metro.sh +15 -1
- package/adapters/mobile/verify.sh +50 -13
- package/adapters/shared/log-tui.mjs +7 -3
- package/adapters/shared/resolve-slot-ports-core.mjs +14 -4
- package/dist/adapters/core/surface.js +1 -0
- package/dist/adapters/extension/runtime.js +14 -5
- package/dist/adapters/extension/surface.js +1 -0
- package/dist/adapters/mobile/provision.js +49 -4
- package/dist/adapters/mobile/surface.js +1 -0
- package/dist/adapters/slot-ports.js +11 -4
- package/dist/cli-commands.js +5 -4
- package/dist/cli.js +4 -0
- package/dist/commands/call.js +79 -6
- package/dist/commands/check.js +326 -0
- package/dist/commands/core-readiness.js +75 -0
- package/dist/commands/device-target.js +123 -13
- package/dist/commands/doctor.js +30 -18
- package/dist/commands/fixtures.js +2 -1
- package/dist/commands/launch/extension.js +105 -9
- package/dist/commands/launch/index.js +116 -15
- package/dist/commands/mobile-device-view.js +140 -0
- package/dist/commands/parse-args.js +4 -1
- package/dist/commands/recipe-quality.js +6 -2
- package/dist/commands/run-engine.js +90 -5
- package/dist/commands/run-report.js +115 -0
- package/dist/commands/run.js +115 -8
- package/dist/commands/self-test.js +1 -1
- package/dist/commands/shared.js +2 -1
- package/dist/commands/status-probe.js +9 -3
- package/dist/commands/status.js +45 -50
- package/dist/live-adapter-contract.js +5 -1
- package/dist/mm-harness-cli.js +68 -27
- package/dist/recipe-files.js +14 -0
- package/docs/CLI-SPEC.md +25 -0
- package/docs/architecture.md +2 -2
- package/docs/live-adapter-contract.md +1 -1
- package/docs/recipe-libraries.md +21 -18
- package/library/actions/extension/platform/cdp.mjs +7 -4
- package/library/recipes/{app-lifecycle-android-smoke.mobile.recipe.json → app/lifecycle.android-smoke.mobile.recipe.json} +1 -1
- package/library/recipes/{perps-performance.mobile.recipe.json → perps/performance.mobile.recipe.json} +1 -1
- package/library/recipes/perps/smoke.core.recipe.json +39 -0
- package/library/recipes/perps/smoke.extension.recipe.json +51 -0
- package/library/recipes/perps/smoke.mobile.recipe.json +51 -0
- package/library/recipes/{action-validation.extension.recipe.json → runner/action-validation.extension.recipe.json} +7 -7
- package/library/recipes/{action-validation.mobile.recipe.json → runner/action-validation.mobile.recipe.json} +7 -7
- package/package.json +1 -1
- /package/library/recipes/{perps-lifecycle.recipe.json → perps/lifecycle.recipe.json} +0 -0
- /package/library/recipes/{order-lifecycle.core.recipe.json → perps/order-lifecycle.core.recipe.json} +0 -0
- /package/library/recipes/{perps-performance-background-resume.mobile.recipe.json → perps/performance.background-resume.mobile.recipe.json} +0 -0
- /package/library/recipes/{perps-performance-cold-start.mobile.recipe.json → perps/performance.cold-start.mobile.recipe.json} +0 -0
- /package/library/recipes/{perps-performance-warm-start.mobile.recipe.json → perps/performance.warm-start.mobile.recipe.json} +0 -0
- /package/library/recipes/{read-markets.core.recipe.json → perps/read-markets.core.recipe.json} +0 -0
- /package/library/recipes/{trading-lifecycle.core.recipe.json → perps/trading-lifecycle.core.recipe.json} +0 -0
- /package/library/recipes/{smoke.extension.recipe.json → runner/smoke.extension.recipe.json} +0 -0
- /package/library/recipes/{smoke.mobile.recipe.json → runner/smoke.mobile.recipe.json} +0 -0
|
@@ -36,6 +36,11 @@ fi
|
|
|
36
36
|
shift || true
|
|
37
37
|
|
|
38
38
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
39
|
+
# shellcheck disable=SC1091
|
|
40
|
+
for _hp in "$SCRIPT_DIR/lib/harness-path.sh" "$SCRIPT_DIR/../../adapters/shared/harness-path.sh" "$SCRIPT_DIR/../shared/harness-path.sh"; do
|
|
41
|
+
[ -f "$_hp" ] && { . "$_hp"; break; }
|
|
42
|
+
done
|
|
43
|
+
unset _hp
|
|
39
44
|
REPO="${REPO:-}"
|
|
40
45
|
if [ -z "$REPO" ]; then
|
|
41
46
|
d="$SCRIPT_DIR"
|
|
@@ -52,6 +57,12 @@ if [ -z "$REPO" ]; then
|
|
|
52
57
|
exit 1
|
|
53
58
|
fi
|
|
54
59
|
cd "$REPO"
|
|
60
|
+
if command -v recipe_runtime_dir >/dev/null 2>&1; then
|
|
61
|
+
RUNTIME_DIR="$(recipe_runtime_dir)"
|
|
62
|
+
else
|
|
63
|
+
RUNTIME_DIR="${RECIPE_RUNTIME_DIR:-temp/recipe/runtime}"
|
|
64
|
+
fi
|
|
65
|
+
RUNTIME_DIST_DIR="${RECIPE_RUNTIME_DIST_DIR:-runtime-dist}"
|
|
55
66
|
|
|
56
67
|
CDP_PORT="${CDP_PORT:-}"
|
|
57
68
|
EXT_ID="${EXT_ID:-}"
|
|
@@ -94,14 +105,33 @@ find_sidepanel_id() {
|
|
|
94
105
|
}
|
|
95
106
|
|
|
96
107
|
resolve_ext_id() {
|
|
97
|
-
if [
|
|
108
|
+
if [[ "$EXT_ID" =~ ^[a-p]{32}$ ]]; then
|
|
98
109
|
printf '%s\n' "$EXT_ID"
|
|
99
110
|
return
|
|
100
111
|
fi
|
|
101
|
-
|
|
112
|
+
|
|
113
|
+
local manifest_id
|
|
114
|
+
manifest_id="$(
|
|
115
|
+
SCRIPT_DIR="$SCRIPT_DIR" REPO="$REPO" RUNTIME_DIR="$RUNTIME_DIR" RUNTIME_DIST_DIR="$RUNTIME_DIST_DIR" node <<'NODE' 2>/dev/null || true
|
|
116
|
+
const path = require('node:path');
|
|
117
|
+
const { extensionIdFromManifestFile } = require(path.join(process.env.SCRIPT_DIR, 'lib/extension-id.cjs'));
|
|
118
|
+
const id = extensionIdFromManifestFile(path.join(process.env.REPO, process.env.RUNTIME_DIR, process.env.RUNTIME_DIST_DIR, 'manifest.json'));
|
|
119
|
+
if (id) process.stdout.write(id);
|
|
120
|
+
NODE
|
|
121
|
+
)"
|
|
122
|
+
if [[ "$manifest_id" =~ ^[a-p]{32}$ ]]; then
|
|
123
|
+
printf '%s\n' "$manifest_id"
|
|
124
|
+
return
|
|
125
|
+
fi
|
|
126
|
+
|
|
127
|
+
for idf in "$REPO/$RUNTIME_DIR/extension.id" "$AGENT_DIR/extension.id"; do
|
|
102
128
|
if [ -f "$idf" ]; then
|
|
103
|
-
|
|
104
|
-
|
|
129
|
+
local marker_id
|
|
130
|
+
marker_id="$(tr -d '[:space:]' < "$idf")"
|
|
131
|
+
if [[ "$marker_id" =~ ^[a-p]{32}$ ]]; then
|
|
132
|
+
printf '%s\n' "$marker_id"
|
|
133
|
+
return
|
|
134
|
+
fi
|
|
105
135
|
fi
|
|
106
136
|
done
|
|
107
137
|
json_list | python3 -c "import json,re,sys; d=json.load(sys.stdin); ids=[]; [ids.extend(re.findall(r'^chrome-extension://([^/]+)/', t.get('url',''))) for t in d]; print(ids[0] if ids else '')"
|
|
@@ -142,6 +172,32 @@ print_targets() {
|
|
|
142
172
|
json_list | python3 -c "import json,sys; d=json.load(sys.stdin); [print(f' {t.get(\"type\",\"?\")[:18]:18s} {t.get(\"url\",\"\")[:100]}') for t in d]"
|
|
143
173
|
}
|
|
144
174
|
|
|
175
|
+
activate_dapp_tab() {
|
|
176
|
+
local target_id
|
|
177
|
+
target_id="$(
|
|
178
|
+
json_list | python3 -c "import json,sys; d=json.load(sys.stdin); pages=[t for t in d if t.get('type')=='page']; target=next((t for t in pages if t.get('url','').startswith('http://') or t.get('url','').startswith('https://')), None); print(target.get('id','') if target else '')"
|
|
179
|
+
)"
|
|
180
|
+
if [ -n "$target_id" ]; then
|
|
181
|
+
curl -s "http://127.0.0.1:${CDP_PORT}/json/activate/${target_id}" >/dev/null || true
|
|
182
|
+
fi
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
cleanup_sidepanel_tabs() {
|
|
186
|
+
local ext_id close_ids
|
|
187
|
+
ext_id="$(resolve_ext_id)"
|
|
188
|
+
[ -n "$ext_id" ] || return 0
|
|
189
|
+
close_ids="$(
|
|
190
|
+
json_list | EXT_ID="$ext_id" python3 -c "import json,os,sys; d=json.load(sys.stdin); ext=os.environ.get('EXT_ID',''); has_panel=any(t.get('type')=='page' and t.get('url','').startswith('chrome-extension://'+ext+'/') and 'sidepanel.html' in t.get('url','') for t in d); has_dapp=any(t.get('type')=='page' and (t.get('url','').startswith('http://') or t.get('url','').startswith('https://')) for t in d); print('\\n'.join(t.get('id','') for t in d if has_panel and has_dapp and t.get('type')=='page' and t.get('url','').startswith('chrome-extension://'+ext+'/') and 'home.html' in t.get('url','')))"
|
|
191
|
+
)"
|
|
192
|
+
if [ -n "$close_ids" ]; then
|
|
193
|
+
while IFS= read -r target_id; do
|
|
194
|
+
[ -n "$target_id" ] || continue
|
|
195
|
+
curl -s "http://127.0.0.1:${CDP_PORT}/json/close/${target_id}" >/dev/null || true
|
|
196
|
+
done <<< "$close_ids"
|
|
197
|
+
fi
|
|
198
|
+
activate_dapp_tab
|
|
199
|
+
}
|
|
200
|
+
|
|
145
201
|
status_sidepanel() {
|
|
146
202
|
local sp
|
|
147
203
|
sp="$(find_sidepanel_id)"
|
|
@@ -165,6 +221,7 @@ close_sidepanel() {
|
|
|
165
221
|
|
|
166
222
|
open_sidepanel() {
|
|
167
223
|
if [ -n "$(find_sidepanel_id)" ]; then
|
|
224
|
+
cleanup_sidepanel_tabs
|
|
168
225
|
echo "[sidepanel] already open"
|
|
169
226
|
return 0
|
|
170
227
|
fi
|
|
@@ -186,7 +243,6 @@ const { chromium } = require('playwright');
|
|
|
186
243
|
const extId = process.env.EXT_ID;
|
|
187
244
|
const browser = await chromium.connectOverCDP(`http://127.0.0.1:${port}`);
|
|
188
245
|
const context = browser.contexts()[0];
|
|
189
|
-
let createdPage = false;
|
|
190
246
|
let page = context
|
|
191
247
|
.pages()
|
|
192
248
|
.find(
|
|
@@ -197,7 +253,6 @@ const { chromium } = require('playwright');
|
|
|
197
253
|
|
|
198
254
|
if (!page) {
|
|
199
255
|
page = await context.newPage();
|
|
200
|
-
createdPage = true;
|
|
201
256
|
await page.goto(`chrome-extension://${extId}/home.html`, {
|
|
202
257
|
waitUntil: 'domcontentloaded',
|
|
203
258
|
timeout: 15000,
|
|
@@ -241,23 +296,35 @@ const { chromium } = require('playwright');
|
|
|
241
296
|
document.getElementById('__recipe_open_sidepanel__')?.remove();
|
|
242
297
|
});
|
|
243
298
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
299
|
+
const deadline = Date.now() + 5000;
|
|
300
|
+
let sidepanelPage;
|
|
301
|
+
while (Date.now() < deadline) {
|
|
302
|
+
sidepanelPage = context
|
|
303
|
+
.pages()
|
|
304
|
+
.find((candidate) => candidate.url().startsWith(`chrome-extension://${extId}/`) && candidate.url().includes('/sidepanel.html'));
|
|
305
|
+
if (sidepanelPage) break;
|
|
306
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Only collapse fullscreen extension tabs after Chrome exposes the sidepanel
|
|
310
|
+
// target. chrome.sidePanel.open can return before the panel page appears; if we
|
|
311
|
+
// close home first, a slow/failed panel leaves the operator with no visible
|
|
312
|
+
// wallet page and the next readiness probe has to reopen from scratch.
|
|
313
|
+
if (sidepanelPage) {
|
|
314
|
+
for (const candidate of context.pages()) {
|
|
315
|
+
if (
|
|
316
|
+
candidate.url().startsWith(`chrome-extension://${extId}/`) &&
|
|
317
|
+
!candidate.url().includes('/sidepanel.html')
|
|
318
|
+
) {
|
|
319
|
+
try {
|
|
320
|
+
await candidate.close();
|
|
321
|
+
} catch (error) {
|
|
322
|
+
console.warn(
|
|
323
|
+
`[sidepanel] extension page close failed after successful open: ${
|
|
324
|
+
error && error.message ? error.message : error
|
|
325
|
+
}`,
|
|
326
|
+
);
|
|
327
|
+
}
|
|
261
328
|
}
|
|
262
329
|
}
|
|
263
330
|
}
|
|
@@ -271,7 +338,11 @@ const { chromium } = require('playwright');
|
|
|
271
338
|
// focus is cosmetic; the panel is already open
|
|
272
339
|
}
|
|
273
340
|
}
|
|
274
|
-
|
|
341
|
+
if (typeof browser.disconnect === 'function') {
|
|
342
|
+
await browser.disconnect();
|
|
343
|
+
} else {
|
|
344
|
+
await browser.close();
|
|
345
|
+
}
|
|
275
346
|
})().catch((error) => {
|
|
276
347
|
console.error(`FAIL: ${error.message || error}`);
|
|
277
348
|
process.exit(4);
|
|
@@ -279,6 +350,7 @@ const { chromium } = require('playwright');
|
|
|
279
350
|
NODE
|
|
280
351
|
|
|
281
352
|
if wait_for_sidepanel; then
|
|
353
|
+
cleanup_sidepanel_tabs
|
|
282
354
|
echo "[sidepanel] opened"
|
|
283
355
|
return 0
|
|
284
356
|
fi
|
|
@@ -88,7 +88,7 @@ RUNNER_BIN="$HARNESS_DIR/runner/bin/mm-harness"
|
|
|
88
88
|
# --out (optional): a task-local recipes dir. Resolve it safely within the target
|
|
89
89
|
# (resolve_harness_out rejects absolute/.. escapes) and prefer its smoke recipe so
|
|
90
90
|
# `live --out <dir>` does not silently fall back to the installed default.
|
|
91
|
-
SMOKE_RECIPE="$HARNESS_DIR/runner/recipes/smoke.extension.recipe.json"
|
|
91
|
+
SMOKE_RECIPE="$HARNESS_DIR/runner/recipes/runner/smoke.extension.recipe.json"
|
|
92
92
|
if [ -n "$OUT" ]; then
|
|
93
93
|
# Fail fast on a task-local --out that does not contain the requested recipe.
|
|
94
94
|
# Silently falling back to the installed default would validate a different
|
|
@@ -98,11 +98,11 @@ if [ -n "$OUT" ]; then
|
|
|
98
98
|
echo "extension verify: --out '$OUT' did not resolve to a safe path under the target." >&2
|
|
99
99
|
exit 2
|
|
100
100
|
fi
|
|
101
|
-
if [ ! -f "$OUT_ABS/smoke.extension.recipe.json" ]; then
|
|
102
|
-
echo "extension verify: --out '$OUT' (resolved: $OUT_ABS) has no smoke.extension.recipe.json. Refusing to fall back to the installed default recipe; place the recipe under --out or omit --out." >&2
|
|
101
|
+
if [ ! -f "$OUT_ABS/runner/smoke.extension.recipe.json" ]; then
|
|
102
|
+
echo "extension verify: --out '$OUT' (resolved: $OUT_ABS) has no runner/smoke.extension.recipe.json. Refusing to fall back to the installed default recipe; place the recipe under --out or omit --out." >&2
|
|
103
103
|
exit 2
|
|
104
104
|
fi
|
|
105
|
-
SMOKE_RECIPE="$OUT_ABS/smoke.extension.recipe.json"
|
|
105
|
+
SMOKE_RECIPE="$OUT_ABS/runner/smoke.extension.recipe.json"
|
|
106
106
|
fi
|
|
107
107
|
ARTIFACTS="${ARTIFACTS:-$HARNESS_DIR/verify/$(date -u +%Y%m%dT%H%M%SZ)}"
|
|
108
108
|
mkdir -p "$ARTIFACTS/logs"
|
|
@@ -385,6 +385,14 @@ if [ "$STATIC_ONLY" = false ]; then
|
|
|
385
385
|
else
|
|
386
386
|
live_mode="live"
|
|
387
387
|
cdp_holder_json "$CDP_PORT" > "$ARTIFACTS/logs/cdp-holder.json"
|
|
388
|
+
if "$RUNNER_BIN" ensure-ready --adapter extension --target "$TARGET" --cdp-port "$CDP_PORT" --json > "$ARTIFACTS/logs/extension-ensure-ready.json" 2>&1; then
|
|
389
|
+
checks+=("{\"name\":\"home-tab convergence\",\"status\":\"pass\",\"detail\":\"see logs/extension-ensure-ready.json\"}")
|
|
390
|
+
refresh_extension_id
|
|
391
|
+
else
|
|
392
|
+
checks+=("{\"name\":\"home-tab convergence\",\"status\":\"fail\",\"detail\":\"see logs/extension-ensure-ready.json\"}")
|
|
393
|
+
status="fail"
|
|
394
|
+
fi
|
|
395
|
+
|
|
388
396
|
if node "$READINESS_MJS" --target "$TARGET" --cdp-port "$CDP_PORT" --json > "$ARTIFACTS/logs/extension-readiness.json" 2>&1; then
|
|
389
397
|
checks+=("{\"name\":\"live extension readiness\",\"status\":\"pass\"}")
|
|
390
398
|
# extension-readiness.mjs may repair $RUNTIME_DIR/extension.id when the
|
|
@@ -438,6 +446,8 @@ try { distFreshness = JSON.parse(fs.readFileSync(path.join(artifacts, 'logs/dist
|
|
|
438
446
|
try { fixtureStatus = JSON.parse(fs.readFileSync(path.join(artifacts, 'logs/fixture-status.json'), 'utf8')); } catch {}
|
|
439
447
|
try { cdpHolder = JSON.parse(fs.readFileSync(path.join(artifacts, 'logs/cdp-holder.json'), 'utf8')); } catch {}
|
|
440
448
|
try { readinessReport = JSON.parse(fs.readFileSync(path.join(artifacts, 'logs/extension-readiness.json'), 'utf8')); } catch {}
|
|
449
|
+
let ensureReadyReport = null;
|
|
450
|
+
try { ensureReadyReport = JSON.parse(fs.readFileSync(path.join(artifacts, 'logs/extension-ensure-ready.json'), 'utf8')); } catch {}
|
|
441
451
|
function runGit(args) {
|
|
442
452
|
try {
|
|
443
453
|
return cp.execFileSync('git', ['-C', target, ...args], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
|
|
@@ -498,6 +508,7 @@ fs.writeFileSync(path.join(artifacts, 'summary.json'), `${JSON.stringify({
|
|
|
498
508
|
},
|
|
499
509
|
fixtureStatus,
|
|
500
510
|
cdpHolder,
|
|
511
|
+
ensureReady: ensureReadyReport,
|
|
501
512
|
cdpTarget,
|
|
502
513
|
checks: parsedChecks,
|
|
503
514
|
generatedAt: new Date().toISOString(),
|
|
@@ -20,6 +20,7 @@ const crypto = require('node:crypto');
|
|
|
20
20
|
const fs = require('node:fs');
|
|
21
21
|
const http = require('node:http');
|
|
22
22
|
const path = require('node:path');
|
|
23
|
+
const { extensionIdFromManifestKey } = require('./lib/extension-id.cjs');
|
|
23
24
|
|
|
24
25
|
const EOA_METHODS = [
|
|
25
26
|
'personal_sign',
|
|
@@ -573,16 +574,6 @@ function extensionIdFromUrl(url) {
|
|
|
573
574
|
return String(url).split('/')[2] || '';
|
|
574
575
|
}
|
|
575
576
|
|
|
576
|
-
function extensionIdFromManifestKey(key) {
|
|
577
|
-
if (!key) {
|
|
578
|
-
return '';
|
|
579
|
-
}
|
|
580
|
-
const digest = crypto.createHash('sha256').update(Buffer.from(key, 'base64')).digest();
|
|
581
|
-
return [...digest.subarray(0, 16)]
|
|
582
|
-
.map((byte) => `${'abcdefghijklmnop'[byte >> 4]}${'abcdefghijklmnop'[byte & 0x0f]}`)
|
|
583
|
-
.join('');
|
|
584
|
-
}
|
|
585
|
-
|
|
586
577
|
function versionedStorageState(fixtureState) {
|
|
587
578
|
return fixtureState.data
|
|
588
579
|
? { data: fixtureState.data, meta: { ...(fixtureState.meta || {}), storageKind: 'data' } }
|
|
@@ -639,6 +630,10 @@ async function detectExtension(context, extensionDir, extensionIdFile) {
|
|
|
639
630
|
const expectedServiceWorker = manifest.background?.service_worker || '';
|
|
640
631
|
const manifestId = extensionIdFromManifestKey(manifest.key);
|
|
641
632
|
const rejected = new Set();
|
|
633
|
+
if (manifestId && extensionIdFile) {
|
|
634
|
+
fs.mkdirSync(path.dirname(extensionIdFile), { recursive: true });
|
|
635
|
+
fs.writeFileSync(extensionIdFile, `${manifestId}\n`);
|
|
636
|
+
}
|
|
642
637
|
|
|
643
638
|
for (let attempt = 0; attempt < 30; attempt += 1) {
|
|
644
639
|
const candidates = [];
|
|
@@ -648,10 +643,10 @@ async function detectExtension(context, extensionDir, extensionIdFile) {
|
|
|
648
643
|
}
|
|
649
644
|
candidates.push({ id, reason });
|
|
650
645
|
};
|
|
646
|
+
push(manifestId, 'manifest key');
|
|
651
647
|
if (extensionIdFile && fs.existsSync(extensionIdFile)) {
|
|
652
648
|
push(fs.readFileSync(extensionIdFile, 'utf8').trim(), 'extension id marker');
|
|
653
649
|
}
|
|
654
|
-
push(manifestId, 'manifest key');
|
|
655
650
|
for (const worker of context.serviceWorkers()) {
|
|
656
651
|
const id = extensionIdFromUrl(worker.url());
|
|
657
652
|
if (expectedServiceWorker && worker.url().endsWith(`/${expectedServiceWorker}`)) {
|
|
@@ -106,20 +106,47 @@ sim_udid_for_target() {
|
|
|
106
106
|
if printf '%s' "$target" | grep -Eq '^[0-9A-Fa-f-]{36}$'; then
|
|
107
107
|
printf '%s\n' "$target"; return 0
|
|
108
108
|
fi
|
|
109
|
-
xcrun simctl list devices available 2>/dev/null
|
|
110
|
-
|
|
109
|
+
xcrun simctl list devices available --json 2>/dev/null | TARGET_SIMULATOR="$target" node -e '
|
|
110
|
+
let input = "";
|
|
111
|
+
process.stdin.setEncoding("utf8");
|
|
112
|
+
process.stdin.on("data", (chunk) => { input += chunk; });
|
|
113
|
+
process.stdin.on("end", () => {
|
|
114
|
+
const target = process.env.TARGET_SIMULATOR || "";
|
|
115
|
+
try {
|
|
116
|
+
const parsed = JSON.parse(input);
|
|
117
|
+
for (const devices of Object.values(parsed.devices || {})) {
|
|
118
|
+
const found = Array.isArray(devices)
|
|
119
|
+
? devices.find((device) => device && device.isAvailable !== false && device.name === target)
|
|
120
|
+
: null;
|
|
121
|
+
if (found && found.udid) {
|
|
122
|
+
process.stdout.write(`${found.udid}\n`);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
} catch {
|
|
127
|
+
// Empty stdout means unresolved; callers print the teaching error.
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
'
|
|
111
131
|
}
|
|
112
132
|
|
|
113
133
|
boot_simulator_if_needed() {
|
|
114
134
|
local target="$1" state=""
|
|
115
135
|
[ "$target" != "booted" ] || return 0
|
|
136
|
+
local udid=""
|
|
137
|
+
udid="$(sim_udid_for_target "$target")"
|
|
138
|
+
if [ -z "$udid" ]; then
|
|
139
|
+
printf "open-device: configured iOS simulator '%s' does not exist.\n" "$target" >&2
|
|
140
|
+
printf "Next: mm-harness provision runway ios --adapter mobile --target '%s' --device '%s'\n" "$TARGET" "$target" >&2
|
|
141
|
+
return 1
|
|
142
|
+
fi
|
|
116
143
|
local line=""
|
|
117
144
|
line="$(xcrun simctl list devices available 2>/dev/null | grep -F "${target} (" | head -1 || true)"
|
|
118
145
|
case "$line" in *"(Booted)"*) state="Booted" ;; *"(Shutdown)"*) state="Shutdown" ;; esac
|
|
119
146
|
[ "$state" = "Booted" ] && return 0
|
|
120
147
|
printf 'Booting iOS simulator %s%s\n' "$target" "${state:+ ($state)}" >&2
|
|
121
|
-
xcrun simctl boot "$
|
|
122
|
-
xcrun simctl bootstatus "$
|
|
148
|
+
xcrun simctl boot "$udid" 2>/dev/null || true
|
|
149
|
+
xcrun simctl bootstatus "$udid" -b >/dev/null
|
|
123
150
|
}
|
|
124
151
|
|
|
125
152
|
show_simulator() {
|
|
@@ -192,7 +219,7 @@ if [ "$PLATFORM" = "ios" ]; then
|
|
|
192
219
|
# Expo dev-client URL scheme — drives both the deep link and the scheme-approval key.
|
|
193
220
|
DEV_CLIENT_SCHEME="${IOS_DEV_CLIENT_SCHEME:-expo-metamask}"
|
|
194
221
|
|
|
195
|
-
boot_simulator_if_needed "$SIM_TARGET"
|
|
222
|
+
boot_simulator_if_needed "$SIM_TARGET" || exit 1
|
|
196
223
|
show_simulator "$SIM_TARGET"
|
|
197
224
|
|
|
198
225
|
LAUNCHED=false
|
|
@@ -86,6 +86,20 @@ start_console_forwarder() {
|
|
|
86
86
|
local fwd_pid_file="$LOG_DIR/console-forwarder.pid"
|
|
87
87
|
[ "${METAMASK_RECIPE_CONSOLE_FORWARD:-1}" != "0" ] || return 0
|
|
88
88
|
{ [ -f "$forwarder" ] && command -v node >/dev/null 2>&1; } || return 0
|
|
89
|
+
# Only one debugger can own a React Native page. Reap any forwarder already
|
|
90
|
+
# attached to this Metro port, even if it was launched from another checkout or
|
|
91
|
+
# package install path and therefore does not own this runtime dir's pid file.
|
|
92
|
+
while IFS= read -r line; do
|
|
93
|
+
local pid cmd
|
|
94
|
+
pid="${line%% *}"
|
|
95
|
+
cmd="${line#* }"
|
|
96
|
+
[ "$pid" != "$line" ] || continue
|
|
97
|
+
case "$cmd" in
|
|
98
|
+
*console-forwarder.cjs*" --port $PORT "*|*console-forwarder.cjs*" --port=$PORT "*)
|
|
99
|
+
kill "$pid" 2>/dev/null || true
|
|
100
|
+
;;
|
|
101
|
+
esac
|
|
102
|
+
done < <(ps -axo pid=,command= 2>/dev/null || true)
|
|
89
103
|
if [ -f "$fwd_pid_file" ]; then
|
|
90
104
|
local old_fwd
|
|
91
105
|
old_fwd="$(cat "$fwd_pid_file" 2>/dev/null || true)"
|
|
@@ -126,7 +140,7 @@ set +a
|
|
|
126
140
|
export EXPO_NO_TYPESCRIPT_SETUP=1
|
|
127
141
|
export WATCHER_PORT="$(printf '%q' "$PORT")" METRO_PORT="$(printf '%q' "$PORT")"
|
|
128
142
|
export METRO_MAX_WORKERS="$(printf '%q' "$METRO_WORKERS")"
|
|
129
|
-
|
|
143
|
+
yarn expo start --port "$(printf '%q' "$PORT")"$clear_flag 2>&1 | tee -a "$(printf '%q' "$LOG_FILE")"
|
|
130
144
|
EOF
|
|
131
145
|
chmod +x "$runner"
|
|
132
146
|
}
|
|
@@ -216,6 +216,34 @@ process.stdout.write(value);
|
|
|
216
216
|
NODE
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
+
runtime_context_value() {
|
|
220
|
+
TARGET_FOR_RUNTIME_CONTEXT="$TARGET" RUNTIME_DIR_FOR_RUNTIME_CONTEXT="$RUNTIME_DIR" RUNTIME_CONTEXT_NAME="$1" node <<'NODE'
|
|
221
|
+
const fs = require('fs');
|
|
222
|
+
const path = require('path');
|
|
223
|
+
const target = process.env.TARGET_FOR_RUNTIME_CONTEXT;
|
|
224
|
+
const runtimeDir = process.env.RUNTIME_DIR_FOR_RUNTIME_CONTEXT;
|
|
225
|
+
const names = process.env.RUNTIME_CONTEXT_NAME.split(',');
|
|
226
|
+
const file = path.join(target, runtimeDir, 'agentic-runtime.json');
|
|
227
|
+
let data = {};
|
|
228
|
+
try { data = JSON.parse(fs.readFileSync(file, 'utf8')); } catch {}
|
|
229
|
+
for (const name of names) {
|
|
230
|
+
const value = data && data[name];
|
|
231
|
+
if (value !== undefined && value !== null && value !== '') {
|
|
232
|
+
process.stdout.write(String(value));
|
|
233
|
+
process.exit(0);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
NODE
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
mobile_device_target() {
|
|
240
|
+
if [ "$PLATFORM" = "android" ]; then
|
|
241
|
+
printf '%s' "${ADB_SERIAL:-${ANDROID_SERIAL:-${ANDROID_DEVICE:-$(runtime_context_value adbSerial,androidDevice,device)}}}"
|
|
242
|
+
return
|
|
243
|
+
fi
|
|
244
|
+
printf '%s' "${SIM_UDID:-${IOS_SIMULATOR:-$(runtime_context_value simulator,iosSimulator,device)}}"
|
|
245
|
+
}
|
|
246
|
+
|
|
219
247
|
check_file() {
|
|
220
248
|
local rel="$1"
|
|
221
249
|
if [ -e "$TARGET/$rel" ]; then
|
|
@@ -331,6 +359,7 @@ run_with_timeout() {
|
|
|
331
359
|
|
|
332
360
|
live_status_ok() {
|
|
333
361
|
local log_path="$1"
|
|
362
|
+
local device_target="$2"
|
|
334
363
|
cat > "$ARTIFACTS/mobile-status-smoke.recipe.json" <<'JSON'
|
|
335
364
|
{
|
|
336
365
|
"schema_version": 1,
|
|
@@ -349,12 +378,15 @@ live_status_ok() {
|
|
|
349
378
|
JSON
|
|
350
379
|
(
|
|
351
380
|
cd "$TARGET"
|
|
352
|
-
|
|
381
|
+
device_args=()
|
|
382
|
+
[ -n "$device_target" ] && device_args=(--device "$device_target")
|
|
383
|
+
"$RUNNER_BIN" run "$ARTIFACTS/mobile-status-smoke.recipe.json" --adapter mobile --project-root "$TARGET" "${device_args[@]}" --artifacts-dir "$ARTIFACTS/runner-status-smoke" --json
|
|
353
384
|
) > "$log_path" 2>&1
|
|
354
385
|
}
|
|
355
386
|
|
|
356
387
|
ensure_live_runtime() {
|
|
357
|
-
|
|
388
|
+
local device_target="$1"
|
|
389
|
+
if live_status_ok "$ARTIFACTS/logs/app-status-precheck.log" "$device_target"; then
|
|
358
390
|
return 0
|
|
359
391
|
fi
|
|
360
392
|
if [ "$AUTO_START" = true ]; then
|
|
@@ -366,7 +398,17 @@ EOF
|
|
|
366
398
|
}
|
|
367
399
|
|
|
368
400
|
if [ "$STATIC_ONLY" = false ]; then
|
|
369
|
-
|
|
401
|
+
device_target="$(mobile_device_target)"
|
|
402
|
+
if [ -z "$device_target" ]; then
|
|
403
|
+
if [ "$PLATFORM" = "android" ]; then
|
|
404
|
+
device_target="$(jsenv_value ADB_SERIAL)"
|
|
405
|
+
[ -n "$device_target" ] || device_target="$(jsenv_value ANDROID_DEVICE)"
|
|
406
|
+
else
|
|
407
|
+
device_target="$(jsenv_value IOS_SIMULATOR)"
|
|
408
|
+
fi
|
|
409
|
+
fi
|
|
410
|
+
|
|
411
|
+
if ensure_live_runtime "$device_target"; then
|
|
370
412
|
checks+=('{"name":"mobile runtime controllable precheck","status":"pass"}')
|
|
371
413
|
else
|
|
372
414
|
checks+=('{"name":"mobile runtime controllable precheck","status":"fail","detail":"see logs/app-status-precheck.log"}')
|
|
@@ -394,20 +436,13 @@ if [ "$STATIC_ONLY" = false ]; then
|
|
|
394
436
|
{
|
|
395
437
|
"schema_version": 1,
|
|
396
438
|
"title": "Mobile v1 runner live bridge smoke",
|
|
397
|
-
"description": "Verifies the installed MetaMask runner can
|
|
439
|
+
"description": "Verifies the installed MetaMask runner can read the React Native debug bridge without mutating wallet state.",
|
|
398
440
|
"validate": {
|
|
399
441
|
"workflow": {
|
|
400
442
|
"entry": "status",
|
|
401
443
|
"nodes": {
|
|
402
444
|
"status": { "action": "app.status", "intent": "Read Mobile app status through the v1 runner", "next": "cdp-probe" },
|
|
403
|
-
"cdp-probe": { "action": "cdp.target", "intent": "Verify the React Native debug bridge target is reachable", "required": true, "timeout_ms": 15000, "next": "
|
|
404
|
-
"wallet-setup": { "action": "metamask.wallet.setup", "intent": "Prepare the wallet fixture for the live bridge smoke", "timeout_ms": 45000, "next": "wallet-unlock" },
|
|
405
|
-
"wallet-unlock": { "action": "metamask.wallet.ensure_unlocked", "intent": "Unlock the wallet through the manifest-declared action", "timeout_ms": 45000, "next": "wallet-read" },
|
|
406
|
-
"wallet-read": { "action": "metamask.wallet.read_state", "intent": "Read wallet state before navigating to the wallet view", "timeout_ms": 45000, "next": "navigate-wallet" },
|
|
407
|
-
"navigate-wallet": { "action": "ui.navigate", "intent": "Open the wallet view through the UI navigation action", "route": "WalletView", "timeout_ms": 45000, "next": "wait-wallet" },
|
|
408
|
-
"wait-wallet": { "action": "ui.wait_for", "intent": "Wait until the wallet screen is present", "test_id": "wallet-screen", "expected": "present", "timeout_ms": 45000, "next": "hud-smoke" },
|
|
409
|
-
"hud-smoke": { "action": "app.hud", "status": "running", "intent": "Show the Mobile live bridge smoke HUD", "progress": { "current": 1, "total": 1 }, "timeout_ms": 45000, "next": "screenshot" },
|
|
410
|
-
"screenshot": { "action": "ui.screenshot", "intent": "Capture proof that the wallet screen is visible", "path": "screenshots/mobile-v1-live-smoke.png", "timeout_ms": 45000, "next": "done" },
|
|
445
|
+
"cdp-probe": { "action": "cdp.target", "intent": "Verify the React Native debug bridge target is reachable", "required": true, "timeout_ms": 15000, "next": "done" },
|
|
411
446
|
"done": { "action": "end", "status": "pass" }
|
|
412
447
|
}
|
|
413
448
|
}
|
|
@@ -417,13 +452,15 @@ JSON
|
|
|
417
452
|
|
|
418
453
|
ios_simulator_resolved="$(jsenv_value IOS_SIMULATOR)"
|
|
419
454
|
adb_serial_resolved="$(jsenv_value ADB_SERIAL)"
|
|
455
|
+
device_args=()
|
|
456
|
+
[ -n "$device_target" ] && device_args=(--device "$device_target")
|
|
420
457
|
if (
|
|
421
458
|
cd "$TARGET"
|
|
422
459
|
METAMASK_RECIPE_AUTO_HUD=0 \
|
|
423
460
|
IOS_SIMULATOR="${IOS_SIMULATOR:-$ios_simulator_resolved}" \
|
|
424
461
|
ADB_SERIAL="${ADB_SERIAL:-$adb_serial_resolved}" \
|
|
425
462
|
ANDROID_SERIAL="${ANDROID_SERIAL:-$adb_serial_resolved}" \
|
|
426
|
-
"$RUNNER_BIN" run "$ARTIFACTS/mobile-v1-live-smoke.recipe.json" --adapter mobile --project-root "$TARGET" --artifacts-dir "$ARTIFACTS/runner-live-smoke" --json
|
|
463
|
+
"$RUNNER_BIN" run "$ARTIFACTS/mobile-v1-live-smoke.recipe.json" --adapter mobile --project-root "$TARGET" "${device_args[@]}" --artifacts-dir "$ARTIFACTS/runner-live-smoke" --json
|
|
427
464
|
) > "$ARTIFACTS/logs/runner-live-smoke.log" 2>&1; then
|
|
428
465
|
checks+=("{\"name\":\"runner v1 live bridge smoke\",\"status\":\"pass\"}")
|
|
429
466
|
else
|
|
@@ -222,13 +222,17 @@ async function watchLog(options) {
|
|
|
222
222
|
const chunk = readNewContent(logPath, offset);
|
|
223
223
|
offset = chunk.offset;
|
|
224
224
|
const allText = fs.existsSync(logPath) ? fs.readFileSync(logPath, 'utf8') : '';
|
|
225
|
+
// Match readiness only against bytes read after this watch started. This
|
|
226
|
+
// assumes line-oriented log writes; a marker split across partial writes
|
|
227
|
+
// waits for a later complete marker instead of stale-passing.
|
|
228
|
+
const matchText = chunk.text;
|
|
225
229
|
const events = extractEvents(allText, eventsLimit);
|
|
226
230
|
|
|
227
|
-
if (matchesPattern(
|
|
231
|
+
if (matchesPattern(matchText, 'Module build failed|^ERROR in |compiled with [1-9][0-9]* error|FAIL runtime-launch|Extension harness launch fail')) {
|
|
228
232
|
renderCompact({ label, logPath, events, quiet: mode === 'quiet' });
|
|
229
233
|
return 1;
|
|
230
234
|
}
|
|
231
|
-
if (matchesPattern(
|
|
235
|
+
if (matchesPattern(matchText, successPattern)) {
|
|
232
236
|
renderCompact({ label, logPath, events, quiet: mode === 'quiet' });
|
|
233
237
|
if (mode !== 'quiet') {
|
|
234
238
|
const elapsed = Math.round((Date.now() - started) / 1000);
|
|
@@ -357,4 +361,4 @@ async function main() {
|
|
|
357
361
|
main().catch((error) => {
|
|
358
362
|
console.error(error instanceof Error ? error.message : String(error));
|
|
359
363
|
process.exit(1);
|
|
360
|
-
});
|
|
364
|
+
});
|
|
@@ -31,6 +31,8 @@ export function formatKvLines(kv) {
|
|
|
31
31
|
if (kv.WATCHER_PORT !== undefined) lines.push(`WATCHER_PORT=${kv.WATCHER_PORT}`);
|
|
32
32
|
if (kv.SLOT_ID) lines.push(`SLOT_ID=${kv.SLOT_ID}`);
|
|
33
33
|
if (kv.IOS_SIMULATOR) lines.push(`IOS_SIMULATOR=${kv.IOS_SIMULATOR}`);
|
|
34
|
+
if (kv.ADB_SERIAL) lines.push(`ADB_SERIAL=${kv.ADB_SERIAL}`);
|
|
35
|
+
if (kv.ANDROID_DEVICE) lines.push(`ANDROID_DEVICE=${kv.ANDROID_DEVICE}`);
|
|
34
36
|
return lines.length ? `${lines.join('\n')}\n` : '';
|
|
35
37
|
}
|
|
36
38
|
|
|
@@ -54,6 +56,8 @@ function scoreSlot(slot, slotSuffix) {
|
|
|
54
56
|
const cdp = resources.browser?.cdp_port;
|
|
55
57
|
const port = resources['dev-server']?.port;
|
|
56
58
|
const simulator = resources['ios-sim']?.simulator;
|
|
59
|
+
const adbSerial = resources['android-emu']?.adb_serial ?? resources.android?.adb_serial;
|
|
60
|
+
const androidDevice = resources['android-emu']?.device ?? resources.android?.device;
|
|
57
61
|
const session = slot.session ?? '';
|
|
58
62
|
const slotId = slot.id ?? '';
|
|
59
63
|
let score = 0;
|
|
@@ -64,7 +68,7 @@ function scoreSlot(slot, slotSuffix) {
|
|
|
64
68
|
if (slotId.endsWith(`mme-${slotSuffix}`) || slotId.includes(`-mme-${slotSuffix}`)) score += 40;
|
|
65
69
|
}
|
|
66
70
|
if (!slotId.toLowerCase().includes('demo')) score += 5;
|
|
67
|
-
return [score, cdp, port, slotId, simulator];
|
|
71
|
+
return [score, cdp, port, slotId, simulator, adbSerial, androidDevice];
|
|
68
72
|
}
|
|
69
73
|
|
|
70
74
|
export function resolveSlotPortsByRepo(repo) {
|
|
@@ -111,12 +115,14 @@ export function resolveSlotPortsByRepo(repo) {
|
|
|
111
115
|
}
|
|
112
116
|
|
|
113
117
|
if (best) {
|
|
114
|
-
const [, cdp, port, slotId, simulator] = best;
|
|
118
|
+
const [, cdp, port, slotId, simulator, adbSerial, androidDevice] = best;
|
|
115
119
|
return formatKvLines({
|
|
116
120
|
...(cdp !== undefined ? { CDP_PORT: cdp } : {}),
|
|
117
121
|
...(port !== undefined ? { WATCHER_PORT: port } : {}),
|
|
118
122
|
...(slotId ? { SLOT_ID: slotId } : {}),
|
|
119
123
|
...(simulator ? { IOS_SIMULATOR: simulator } : {}),
|
|
124
|
+
...(adbSerial ? { ADB_SERIAL: adbSerial } : {}),
|
|
125
|
+
...(androidDevice ? { ANDROID_DEVICE: androidDevice } : {}),
|
|
120
126
|
});
|
|
121
127
|
}
|
|
122
128
|
}
|
|
@@ -152,10 +158,14 @@ export function resolveMobileRuntimeContext(repo) {
|
|
|
152
158
|
if (fs.existsSync(ctxPath)) {
|
|
153
159
|
try {
|
|
154
160
|
const c = JSON.parse(fs.readFileSync(ctxPath, 'utf8'));
|
|
155
|
-
|
|
161
|
+
const adbSerial = c.adbSerial ?? c.androidSerial;
|
|
162
|
+
const androidDevice = c.androidDevice;
|
|
163
|
+
if (c.simulator || adbSerial || androidDevice || (c.metroPort != null && c.metroPort !== '')) {
|
|
156
164
|
return formatKvLines({
|
|
157
165
|
...(c.metroPort != null && c.metroPort !== '' ? { WATCHER_PORT: c.metroPort } : {}),
|
|
158
166
|
...(c.simulator ? { IOS_SIMULATOR: c.simulator } : {}),
|
|
167
|
+
...(adbSerial ? { ADB_SERIAL: adbSerial } : {}),
|
|
168
|
+
...(androidDevice ? { ANDROID_DEVICE: androidDevice } : {}),
|
|
159
169
|
...(c.slotId ? { SLOT_ID: c.slotId } : {}),
|
|
160
170
|
});
|
|
161
171
|
}
|
|
@@ -210,4 +220,4 @@ export const cliFns = {
|
|
|
210
220
|
resolve_mobile_runtime_context: resolveMobileRuntimeContext,
|
|
211
221
|
resolve_mobile_slot_defaults: resolveMobileSlotDefaults,
|
|
212
222
|
resolve_mobile_runtime_ports: (repo) => resolveMobileRuntimePorts(repo) || null,
|
|
213
|
-
};
|
|
223
|
+
};
|
|
@@ -25,7 +25,8 @@ async function prepareExtensionRuntime(options) {
|
|
|
25
25
|
const health = await assertHealthyExtensionRuntime({
|
|
26
26
|
projectRoot,
|
|
27
27
|
cdpPort,
|
|
28
|
-
timeoutMs: options.healthTimeoutMs
|
|
28
|
+
timeoutMs: options.healthTimeoutMs,
|
|
29
|
+
pageMode: options.pageMode
|
|
29
30
|
});
|
|
30
31
|
return { launch, health };
|
|
31
32
|
}
|
|
@@ -34,14 +35,16 @@ async function assertHealthyExtensionRuntime(options) {
|
|
|
34
35
|
const deadline = Date.now() + timeoutMs;
|
|
35
36
|
let lastReport = null;
|
|
36
37
|
while (Date.now() <= deadline) {
|
|
37
|
-
lastReport = await checkExtensionRuntimeHealth(options.projectRoot, options.cdpPort
|
|
38
|
+
lastReport = await checkExtensionRuntimeHealth(options.projectRoot, options.cdpPort, {
|
|
39
|
+
pageMode: options.pageMode
|
|
40
|
+
});
|
|
38
41
|
if (lastReport.status === "PASS") return lastReport;
|
|
39
42
|
await sleep(500);
|
|
40
43
|
}
|
|
41
44
|
const report = lastReport ?? failReport(options.cdpPort, ["CDP runtime was not probed."], {});
|
|
42
45
|
throw new Error(formatHealthFailure(report, options.projectRoot));
|
|
43
46
|
}
|
|
44
|
-
async function checkExtensionRuntimeHealth(projectRoot, cdpPort) {
|
|
47
|
+
async function checkExtensionRuntimeHealth(projectRoot, cdpPort, options = {}) {
|
|
45
48
|
const findings = [];
|
|
46
49
|
const warnings = [];
|
|
47
50
|
let targets = [];
|
|
@@ -52,11 +55,17 @@ async function checkExtensionRuntimeHealth(projectRoot, cdpPort) {
|
|
|
52
55
|
} catch (error) {
|
|
53
56
|
return failReport(cdpPort, [`CDP is not reachable on port ${cdpPort}: ${messageOf(error)}`], {});
|
|
54
57
|
}
|
|
55
|
-
const
|
|
58
|
+
const pageMode = options.pageMode ?? "home";
|
|
59
|
+
const homeTargets = targets.filter(
|
|
56
60
|
(target2) => target2.type === "page" && String(target2.url ?? "").startsWith("chrome-extension://") && String(target2.url ?? "").includes("/home.html") && Boolean(target2.webSocketDebuggerUrl)
|
|
57
61
|
);
|
|
62
|
+
const sidepanelTargets = targets.filter(
|
|
63
|
+
(target2) => target2.type === "page" && String(target2.url ?? "").startsWith("chrome-extension://") && String(target2.url ?? "").includes("/sidepanel.html") && Boolean(target2.webSocketDebuggerUrl)
|
|
64
|
+
);
|
|
65
|
+
const extensionTargets = pageMode === "home-or-sidepanel" && homeTargets.length === 0 ? sidepanelTargets : homeTargets;
|
|
58
66
|
if (extensionTargets.length !== 1) {
|
|
59
|
-
|
|
67
|
+
const expected = pageMode === "home-or-sidepanel" ? "exactly one MetaMask extension home page target, or one sidepanel target when home is intentionally closed" : "exactly one MetaMask extension home page target";
|
|
68
|
+
findings.push(`Expected ${expected}, found ${extensionTargets.length}.`);
|
|
60
69
|
}
|
|
61
70
|
const target = extensionTargets[0];
|
|
62
71
|
if (!target?.webSocketDebuggerUrl) {
|