@deeeed/metamask-harness 0.68.0 → 0.70.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 +21 -0
- package/README.md +97 -0
- package/adapters/extension/ensure-browser.sh +85 -15
- package/adapters/extension/inject.mjs +2 -0
- package/adapters/extension/launch-browser.cjs +140 -18
- package/adapters/extension/lib/macos-focus.cjs +67 -195
- package/adapters/extension/live.sh +29 -48
- package/adapters/manifest.json +20 -4
- package/adapters/shared/browser-cdp.cjs +528 -0
- package/adapters/shared/browser-resolver.cjs +996 -0
- package/adapters/shared/open-debug.mjs +6 -1
- package/dist/adapters/mobile/prepare.js +7 -4
- package/dist/adapters/mobile/runtime-decision.js +8 -10
- package/dist/command-contract.js +7 -0
- package/dist/commands/call.js +3 -0
- package/dist/commands/launch/index.js +19 -0
- package/dist/commands/list-executables.js +62 -11
- package/dist/commands/manifest.js +11 -4
- package/dist/commands/run-engine.js +8 -1
- package/dist/commands/run-report.js +96 -3
- package/dist/commands/run.js +5 -1
- package/dist/doctor.js +48 -1
- package/dist/mm-harness-cli.js +7 -0
- package/docs/QA.md +3 -0
- package/docs/RECIPES.md +10 -3
- package/library/actions/extension/platform/cdp.mjs +87 -46
- package/package.json +1 -1
- package/scripts/completions.sh +2 -2
- package/scripts/site-contrast.mjs +16 -3
package/docs/RECIPES.md
CHANGED
|
@@ -542,12 +542,19 @@ selected deterministically from `recipes/<adapter>/<domain>/`. Identical
|
|
|
542
542
|
cross-adapter graphs live once in `recipes/shared/<domain>/`; an adapter-specific
|
|
543
543
|
variant takes precedence. Both resolve as `<domain>.<recipe>`, without a platform
|
|
544
544
|
or `shared` prefix. Keep domains below `core`, `extension`, `mobile`, or `shared`.
|
|
545
|
-
|
|
546
|
-
`*.<adapter>.recipe.json` paths remain readable during migration, but new
|
|
547
|
-
migrated libraries use the adapter-first layout. A file cannot declare its
|
|
545
|
+
|
|
546
|
+
Legacy `*.<adapter>.recipe.json` paths remain readable during migration, but new
|
|
547
|
+
and migrated libraries use the adapter-first layout. A file cannot declare its
|
|
548
548
|
adapter through both forms. Every run records the root recipe, exact resolved
|
|
549
549
|
dependency documents, their digests, call edges, selected sources, and shadows.
|
|
550
550
|
|
|
551
|
+
`mm-harness run --list` and `mm-harness call --list` group recipes/actions by domain
|
|
552
|
+
and show their library origins separately.
|
|
553
|
+
Use `--domain swap-bridge` or `--source perps` to filter, and `--sort name|domain|library`
|
|
554
|
+
to change the order. These options also apply to `--json` output, which keeps name
|
|
555
|
+
ordering unless `--sort` is supplied. Domain filters accept hyphens or underscores,
|
|
556
|
+
so `--domain swap-bridge` includes `metamask.swap_bridge.*` actions.
|
|
557
|
+
|
|
551
558
|
## Direct product access
|
|
552
559
|
|
|
553
560
|
The bundled library is intentionally small. A task may use a verified controller
|
|
@@ -19,6 +19,7 @@ import { closeSync, openSync } from 'node:fs';
|
|
|
19
19
|
import { spawn } from 'node:child_process';
|
|
20
20
|
import { createRequire } from 'node:module';
|
|
21
21
|
import path from 'node:path';
|
|
22
|
+
import { fileURLToPath } from 'node:url';
|
|
22
23
|
|
|
23
24
|
import {
|
|
24
25
|
importRecipeHarnessRuntimeBrowserExtension,
|
|
@@ -368,24 +369,26 @@ function autolaunchEnabled(input) {
|
|
|
368
369
|
process.env.METAMASK_RECIPE_EXTENSION_LAUNCH_EXISTING_DIST === '1';
|
|
369
370
|
}
|
|
370
371
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
372
|
+
const runnerRequire = createRequire(import.meta.url);
|
|
373
|
+
const runnerRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../../..');
|
|
374
|
+
const browserResolver = runnerRequire(path.join(runnerRoot, 'adapters/shared/browser-resolver.cjs'));
|
|
375
|
+
const { stopProfileProcessesSync } = runnerRequire(path.join(runnerRoot, 'adapters/extension/lib/validation-process-ownership.cjs'));
|
|
374
376
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
}
|
|
377
|
+
// Same browser choice as the live launcher (RECIPE_HARNESS_BROWSER /
|
|
378
|
+
// RECIPE_HARNESS_CHROME_BIN); branded Chrome loads the dist over CDP. This
|
|
379
|
+
// launcher spawns a headful browser directly, so the probe does too, and a
|
|
380
|
+
// reused profile keeps its recorded browser.
|
|
381
|
+
async function resolveChromeBinary(projectRoot, recorded) {
|
|
382
|
+
try {
|
|
383
|
+
return await browserResolver.resolveBrowser({
|
|
384
|
+
cft: () => browserResolver.playwrightChromiumExecutable(projectRoot),
|
|
385
|
+
launchMethod: browserResolver.SPAWN,
|
|
386
|
+
display: browserResolver.HEADFUL,
|
|
387
|
+
recorded,
|
|
388
|
+
});
|
|
389
|
+
} catch (error) {
|
|
390
|
+
throw new Error(`Extension autolaunch could not select a browser: ${error.message}`);
|
|
387
391
|
}
|
|
388
|
-
throw new Error('Extension autolaunch requires RECIPE_HARNESS_CHROME_BIN or Playwright installed in the target checkout.');
|
|
389
392
|
}
|
|
390
393
|
|
|
391
394
|
async function execNodeScript(script, args, options) {
|
|
@@ -446,7 +449,7 @@ async function findWalletFixture(projectRoot) {
|
|
|
446
449
|
return null;
|
|
447
450
|
}
|
|
448
451
|
|
|
449
|
-
async function launchExistingDistRuntime(input, port) {
|
|
452
|
+
export async function launchExistingDistRuntime(input, port) {
|
|
450
453
|
const projectRoot = input.context.projectRoot;
|
|
451
454
|
const artifactsDir = input.context.artifactsDir;
|
|
452
455
|
const distDir = path.resolve(projectRoot, input.node?.dist_dir || process.env.METAMASK_RECIPE_EXTENSION_DIST_DIR || 'dist/chrome');
|
|
@@ -476,12 +479,23 @@ async function launchExistingDistRuntime(input, port) {
|
|
|
476
479
|
fixtureSeeded = true;
|
|
477
480
|
}
|
|
478
481
|
|
|
479
|
-
|
|
482
|
+
// Never start a second browser on a port something else holds: whatever
|
|
483
|
+
// answers there afterwards would not be ours to load or seed.
|
|
484
|
+
const holders = browserResolver.cdpListenerPids(port);
|
|
485
|
+
if (holders.length > 0) {
|
|
486
|
+
throw new Error(`Extension autolaunch refused: CDP port ${port} is already held by pid ${holders.join(', ')}. Next: stop that browser or choose a free --cdp-port.`);
|
|
487
|
+
}
|
|
488
|
+
// Next to the profile, so a reused profile (METAMASK_RECIPE_EXTENSION_PROFILE_DIR)
|
|
489
|
+
// finds the browser it was created with.
|
|
490
|
+
const browserRecord = `${profileDir}.browser.json`;
|
|
491
|
+
const profileExisted = await canAccess(path.join(profileDir, 'Local State'));
|
|
492
|
+
const browser = await resolveChromeBinary(projectRoot, profileExisted ? browserRecord : null);
|
|
493
|
+
const loadsOverCdp = browser.extensionLoading === browserResolver.CDP_LOAD_UNPACKED;
|
|
480
494
|
const stdoutFd = openSync(path.join(logsDir, 'chrome.log'), 'a');
|
|
481
495
|
const stderrFd = openSync(path.join(logsDir, 'chrome.log'), 'a');
|
|
482
496
|
let child;
|
|
483
497
|
try {
|
|
484
|
-
child = spawn(
|
|
498
|
+
child = spawn(browser.bin, [
|
|
485
499
|
`--user-data-dir=${profileDir}`,
|
|
486
500
|
...(process.platform === 'darwin' ? ['--use-mock-keychain'] : []),
|
|
487
501
|
'--remote-debugging-address=127.0.0.1',
|
|
@@ -493,8 +507,7 @@ async function launchExistingDistRuntime(input, port) {
|
|
|
493
507
|
'--disable-extensions-file-access-check',
|
|
494
508
|
'--disable-extensions-content-verification',
|
|
495
509
|
'--disable-features=ExtensionContentVerification,DisableLoadExtensionCommandLineSwitch',
|
|
496
|
-
|
|
497
|
-
`--load-extension=${runtimeDist}`,
|
|
510
|
+
...browserResolver.extensionLaunchArgs(runtimeDist, browser.extensionLoading),
|
|
498
511
|
'chrome://extensions/',
|
|
499
512
|
], {
|
|
500
513
|
cwd: projectRoot,
|
|
@@ -516,32 +529,58 @@ async function launchExistingDistRuntime(input, port) {
|
|
|
516
529
|
}
|
|
517
530
|
child.unref();
|
|
518
531
|
await writeFile(path.join(logsDir, 'chrome.pid'), `${child.pid}\n`);
|
|
519
|
-
await retryJsonGet(`http://127.0.0.1:${port}/json/version`, 45000);
|
|
520
|
-
|
|
521
532
|
let fixtureValidation = null;
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
await
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
533
|
+
let launchedBrowser = null;
|
|
534
|
+
try {
|
|
535
|
+
await retryJsonGet(`http://127.0.0.1:${port}/json/version`, 45000);
|
|
536
|
+
const owner = browserResolver.waitForCdpOwner(port, profileDir);
|
|
537
|
+
const cdpLoad = loadsOverCdp ? await browserResolver.loadUnpackedOverPort(port, runtimeDist, { profile: profileDir }) : null;
|
|
538
|
+
launchedBrowser = {
|
|
539
|
+
...browser,
|
|
540
|
+
launch: {
|
|
541
|
+
pid: owner.pid,
|
|
542
|
+
cdpPort: port,
|
|
543
|
+
startedAt: new Date().toISOString(),
|
|
544
|
+
profile: owner.profile,
|
|
545
|
+
processStartedAt: owner.startedAt,
|
|
546
|
+
...(cdpLoad ? { extensionId: cdpLoad.id, otherExtensions: cdpLoad.otherExtensions } : {}),
|
|
547
|
+
},
|
|
548
|
+
};
|
|
549
|
+
await writeFile(browserRecord, `${JSON.stringify(launchedBrowser, null, 2)}\n`);
|
|
550
|
+
if (fixtureSeeded) {
|
|
551
|
+
fixtureValidation = path.join(logsDir, 'fixture-account-parity.json');
|
|
552
|
+
await execNodeScript(
|
|
553
|
+
fixtureScript,
|
|
554
|
+
[
|
|
555
|
+
'seed-cdp',
|
|
556
|
+
'--target',
|
|
557
|
+
projectRoot,
|
|
558
|
+
'--fixture',
|
|
559
|
+
fixture,
|
|
560
|
+
'--state',
|
|
561
|
+
fixtureState,
|
|
562
|
+
'--cdp-port',
|
|
563
|
+
String(port),
|
|
564
|
+
'--extension-dir',
|
|
565
|
+
runtimeDist,
|
|
566
|
+
'--extension-id-file',
|
|
567
|
+
extensionIdFile,
|
|
568
|
+
'--out',
|
|
569
|
+
fixtureValidation,
|
|
570
|
+
],
|
|
571
|
+
{ cwd: projectRoot },
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
} catch (error) {
|
|
575
|
+
// The launch failed after the browser started: stop it before reporting,
|
|
576
|
+
// so no detached browser outlives this action. A failed stop is reported
|
|
577
|
+
// with the original failure, never swallowed.
|
|
578
|
+
try {
|
|
579
|
+
stopProfileProcessesSync(profileDir, { extraPids: [child.pid] });
|
|
580
|
+
} catch (stopError) {
|
|
581
|
+
throw new Error(`${error.message}\nStopping the autolaunched browser (pid ${child.pid}, profile ${profileDir}) also failed: ${stopError.message}`, { cause: error });
|
|
582
|
+
}
|
|
583
|
+
throw error;
|
|
545
584
|
}
|
|
546
585
|
|
|
547
586
|
await writeFile(path.join(runtimeRoot, 'runtime.json'), `${JSON.stringify({
|
|
@@ -553,6 +592,8 @@ async function launchExistingDistRuntime(input, port) {
|
|
|
553
592
|
profileDir,
|
|
554
593
|
fixtureSeeded,
|
|
555
594
|
fixtureValidation,
|
|
595
|
+
browser: launchedBrowser,
|
|
596
|
+
browserRecord,
|
|
556
597
|
launchedAt: new Date().toISOString(),
|
|
557
598
|
}, null, 2)}\n`);
|
|
558
599
|
return { launched: true, pid: child.pid, runtimeRoot, fixtureSeeded };
|
package/package.json
CHANGED
package/scripts/completions.sh
CHANGED
|
@@ -44,8 +44,8 @@ _mmh_flags_for() {
|
|
|
44
44
|
execution-template) printf '%s' "--dir --domain-dir --project-worker --project-name --package-templates --package-id --flow --run-mode --platform --domain --id --provenance --include-shadowed --no-include-shadowed --title --force --json" ;;
|
|
45
45
|
recipe-quality) printf '%s' "--input --output --recipe --task --evidence --params --adapter --library --advisor --advisor-model --advisor-stages --json" ;;
|
|
46
46
|
actions) printf '%s' "--adapter --action --kind --raw --action-manifest --target --json" ;;
|
|
47
|
-
call) printf '%s' "--list --arg --adapter --target --artifacts-dir --action-manifest --heal --json" ;;
|
|
48
|
-
run) printf '%s' "--list --describe --plan --adapter --artifacts-dir --target --library --cdp-port --heal --record-video --json" ;;
|
|
47
|
+
call) printf '%s' "--list --domain --source --sort --arg --adapter --target --artifacts-dir --action-manifest --heal --json" ;;
|
|
48
|
+
run) printf '%s' "--list --domain --source --sort --describe --plan --adapter --artifacts-dir --target --library --cdp-port --heal --record-video --json" ;;
|
|
49
49
|
doctor) printf '%s' "--adapter --target --fix --json" ;;
|
|
50
50
|
install|verify|cleanup) printf '%s' "--adapter --target --json" ;;
|
|
51
51
|
*) printf '%s' "" ;;
|
|
@@ -449,8 +449,12 @@ async function checkBehavior(browserEndpoint, port, pages) {
|
|
|
449
449
|
return checked;
|
|
450
450
|
})()
|
|
451
451
|
`);
|
|
452
|
-
if (!
|
|
453
|
-
throw new Error('
|
|
452
|
+
if (!['fix-bug/extension', 'fix-bug/mobile', 'qa/default'].every(id => previews.includes(id))) {
|
|
453
|
+
throw new Error('Runtime workflow previews must be available');
|
|
454
|
+
}
|
|
455
|
+
const reviewSource = await page.evaluate("document.querySelector('[data-static-review-source]')?.href");
|
|
456
|
+
if (!reviewSource?.endsWith('/domains/perps/skills/perps-review-pr/skill.md')) {
|
|
457
|
+
throw new Error('Static review must link to its owning skill');
|
|
454
458
|
}
|
|
455
459
|
const progress = await page.evaluate(String.raw`
|
|
456
460
|
(() => {
|
|
@@ -514,7 +518,16 @@ async function checkBehavior(browserEndpoint, port, pages) {
|
|
|
514
518
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
515
519
|
}
|
|
516
520
|
}
|
|
517
|
-
if (!response?.ok)
|
|
521
|
+
if (!response?.ok) {
|
|
522
|
+
failures.push(url.pathname + ' (' + (response?.status ?? 'network') + ')');
|
|
523
|
+
} else if (url.hash) {
|
|
524
|
+
const target = url.pathname === location.pathname
|
|
525
|
+
? document
|
|
526
|
+
: new DOMParser().parseFromString(await response.text(), 'text/html');
|
|
527
|
+
if (!target.getElementById(decodeURIComponent(url.hash.slice(1)))) {
|
|
528
|
+
failures.push(url.pathname + url.hash + ' (missing anchor)');
|
|
529
|
+
}
|
|
530
|
+
}
|
|
518
531
|
}
|
|
519
532
|
return failures;
|
|
520
533
|
})()
|