@deeeed/metamask-harness 0.17.4 → 0.18.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 +775 -0
- package/README.md +65 -71
- package/adapters/extension/ensure-browser.sh +12 -1
- package/adapters/extension/inject.mjs +7 -0
- package/adapters/extension/launch-browser.cjs +10 -3
- package/adapters/extension/lib/chrome-args.cjs +31 -0
- package/adapters/extension/lib/macos-focus.cjs +32 -0
- package/adapters/extension/live.sh +10 -20
- package/adapters/manifest.json +8 -0
- package/adapters/mobile/open-device.sh +45 -7
- package/adapters/mobile/verify.sh +15 -3
- package/adapters/shared/harness-source-fingerprint.mjs +49 -0
- package/adapters/shared/install-repo-deps.sh +1 -5
- package/adapters/shared/open-debug.mjs +184 -103
- package/adapters/shared/resolve-slot-ports-core.mjs +23 -6
- package/adapters/shared/resolve-slot-ports.sh +22 -5
- package/bin/mm-harness +15 -3
- package/dist/adapters/core/surface.js +4 -1
- package/dist/adapters/extension/console-capture.js +3 -1
- package/dist/adapters/extension/harness-freshness.js +39 -0
- package/dist/adapters/extension/product-config.js +110 -0
- package/dist/adapters/extension/runtime-decision.js +20 -71
- package/dist/adapters/extension/surface.js +19 -1
- package/dist/adapters/mobile/prepare.js +17 -0
- package/dist/adapters/mobile/source-freshness.js +26 -41
- package/dist/adapters/mobile/surface.js +4 -1
- package/dist/adapters/resolve-slot-ports.js +2 -0
- package/dist/adapters/slot-ports.js +13 -32
- package/dist/adapters.js +50 -17
- package/dist/checkout-lock.js +27 -2
- package/dist/cli-color.js +19 -0
- package/dist/cli-commands.js +1 -1
- package/dist/cli.js +2 -3
- package/dist/command-contract.js +13 -3
- package/dist/commands/call.js +115 -29
- package/dist/commands/checklist.js +4 -1
- package/dist/commands/completion-candidates.js +20 -13
- package/dist/commands/debug.js +31 -38
- package/dist/commands/doctor.js +33 -6
- package/dist/commands/fixtures.js +65 -17
- package/dist/commands/flows.js +39 -10
- package/dist/commands/launch/extension.js +40 -15
- package/dist/commands/launch/index.js +41 -5
- package/dist/commands/list-executables.js +151 -29
- package/dist/commands/manifest.js +127 -18
- package/dist/commands/parse-args.js +11 -1
- package/dist/commands/run-engine.js +384 -56
- package/dist/commands/run.js +112 -17
- package/dist/commands/shared.js +22 -2
- package/dist/commands/status-probe.js +3 -0
- package/dist/commands/status.js +1 -0
- package/dist/completions-cache.js +1 -1
- package/dist/doctor.js +56 -6
- package/dist/harness.js +6 -5
- package/dist/heal-bounds.js +1 -1
- package/dist/live-adapter-contract.js +132 -12
- package/dist/manifest.js +161 -1
- package/dist/mm-harness-cli.js +13 -7
- package/dist/recipe-security.js +178 -0
- package/dist/runner.js +87 -13
- package/dist/runtime-context.js +8 -26
- package/docs/CONTRIBUTING.md +137 -0
- package/docs/QA.md +185 -0
- package/docs/RECIPES.md +161 -0
- package/docs/SECURITY.md +88 -0
- package/library/actions/core/perps/read_account.mjs +2 -2
- package/library/actions/core/perps/read_orders.mjs +2 -1
- package/library/actions/core/perps/read_positions.mjs +2 -1
- package/library/actions/core/wallet/list_accounts.mjs +95 -0
- package/library/actions/extension/platform/cdp.mjs +1 -0
- package/library/actions/extension/wallet/list_accounts.mjs +41 -0
- package/library/actions/mobile/platform/bridge.mjs +1 -5
- package/library/actions/mobile/wallet/list_accounts.mjs +37 -0
- package/library/manifests/core.action-manifest.json +61 -0
- package/library/manifests/extension.action-manifest.json +53 -0
- package/library/manifests/mobile.action-manifest.json +53 -0
- package/library/recipes/runner/action-validation.extension.recipe.json +8 -1
- package/library/recipes/runner/action-validation.mobile.recipe.json +8 -1
- package/package.json +10 -6
- package/scripts/completions.sh +7 -7
- package/docs/ADAPTER-SURFACE.md +0 -26
- package/docs/CHEATSHEET.md +0 -48
- package/docs/CLI-ERGONOMICS-AUDIT.md +0 -32
- package/docs/CLI-ERGONOMICS-HUMAN-QA.md +0 -64
- package/docs/CODE-MAP.md +0 -62
- package/docs/UX-PRINCIPLES.md +0 -66
- package/docs/VIDEO-DEMO-VALIDATION.md +0 -74
- package/docs/architecture.md +0 -88
- package/docs/live-adapter-contract.md +0 -190
- package/docs/package-boundaries.md +0 -47
- package/docs/perps-flow-catalog.md +0 -235
- package/docs/recipe-libraries.md +0 -71
- package/docs/runtime-file-conventions.md +0 -36
package/README.md
CHANGED
|
@@ -1,22 +1,15 @@
|
|
|
1
1
|
# mm-harness
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
One CLI for operating MetaMask Extension, Mobile, and Core and producing
|
|
4
|
+
reviewable recipe evidence. Run it inside a checkout; the product, slot, ports,
|
|
5
|
+
and runtime paths are detected automatically.
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
- **Action:** one typed operation.
|
|
8
|
+
- **Flow:** a reusable, parameterized sequence of actions.
|
|
9
|
+
- **Recipe:** the complete proof for a task or regression.
|
|
7
10
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
- execute deterministic recipes and save reviewable proof.
|
|
11
|
-
|
|
12
|
-
The generic graph engine lives in `@farmslot/recipe-harness`. This package owns
|
|
13
|
-
only MetaMask-specific actions and runtime control.
|
|
14
|
-
|
|
15
|
-
One mental model:
|
|
16
|
-
|
|
17
|
-
- **Action** — one typed operation, such as reading wallet state.
|
|
18
|
-
- **Flow** — a reusable, parameterized sequence of actions.
|
|
19
|
-
- **Recipe** — the complete executable proof for a task or regression.
|
|
11
|
+
The generic graph engine and evidence schemas live in Farmslot packages.
|
|
12
|
+
`mm-harness` owns MetaMask runtime control and domain capabilities.
|
|
20
13
|
|
|
21
14
|
## Install
|
|
22
15
|
|
|
@@ -26,112 +19,113 @@ mm-harness --version
|
|
|
26
19
|
mm-harness doctor
|
|
27
20
|
```
|
|
28
21
|
|
|
29
|
-
|
|
22
|
+
`doctor` is read-only. `doctor --fix` repairs harness-owned runtime state but
|
|
23
|
+
does not launch an app, invent credentials, or choose a wallet fixture.
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
mm-harness doctor --fix
|
|
27
|
+
mm-harness fixtures init --from /secure/path/wallet-fixture.json
|
|
28
|
+
# Disposable public testing only; never fund this wallet:
|
|
29
|
+
mm-harness fixtures init --dev
|
|
30
|
+
```
|
|
30
31
|
|
|
31
|
-
|
|
32
|
+
Use `mm-harness update` to update a published installation.
|
|
32
33
|
|
|
33
|
-
|
|
34
|
+
## Operate
|
|
34
35
|
|
|
35
36
|
```bash
|
|
36
37
|
# Extension
|
|
37
|
-
mm-harness launch
|
|
38
|
-
mm-harness launch --sidepanel
|
|
38
|
+
mm-harness launch
|
|
39
|
+
mm-harness launch --sidepanel
|
|
39
40
|
|
|
40
41
|
# Mobile
|
|
41
42
|
mm-harness launch ios
|
|
42
43
|
mm-harness launch android
|
|
43
44
|
|
|
44
|
-
# Any
|
|
45
|
+
# Any checkout
|
|
45
46
|
mm-harness status
|
|
47
|
+
mm-harness stop
|
|
46
48
|
mm-harness logs
|
|
47
49
|
mm-harness debug
|
|
48
50
|
mm-harness fixtures set
|
|
49
51
|
```
|
|
50
52
|
|
|
51
|
-
`launch`
|
|
52
|
-
|
|
53
|
-
edits compile incrementally into the isolated runtime; refresh the active page or
|
|
54
|
-
sidepanel to see them. Use `launch --build` only when a clean rebuild is needed.
|
|
53
|
+
Extension `launch` keeps its incremental watcher running. Refresh the active
|
|
54
|
+
page after a successful rebuild; use `launch --build` only for a clean rebuild.
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
profile. Mobile opens the React Native developer surface.
|
|
58
|
-
|
|
59
|
-
Filter logs when needed:
|
|
56
|
+
Log sources stay separate:
|
|
60
57
|
|
|
61
58
|
```bash
|
|
62
|
-
mm-harness logs --source extension # Extension
|
|
63
|
-
mm-harness logs --source dapp # active dapp
|
|
64
|
-
mm-harness logs --source webpack #
|
|
65
|
-
mm-harness logs --source
|
|
59
|
+
mm-harness logs --source extension # Extension page
|
|
60
|
+
mm-harness logs --source dapp # active dapp
|
|
61
|
+
mm-harness logs --source webpack # compiler
|
|
62
|
+
mm-harness logs --source watcher # watcher lifecycle
|
|
63
|
+
mm-harness logs --source rebuild # incremental rebuilds
|
|
64
|
+
mm-harness logs --source app # Mobile app
|
|
66
65
|
mm-harness logs --source metro # Mobile bundler
|
|
67
66
|
```
|
|
68
67
|
|
|
69
|
-
##
|
|
68
|
+
## Discover and prove
|
|
70
69
|
|
|
71
70
|
```bash
|
|
72
71
|
mm-harness actions positions
|
|
73
72
|
mm-harness actions --action metamask.wallet.ensure_unlocked
|
|
74
73
|
mm-harness call metamask.wallet.ensure_unlocked
|
|
75
74
|
mm-harness flows
|
|
75
|
+
mm-harness flows describe perps.clean_market_testnet
|
|
76
76
|
|
|
77
|
+
mm-harness run --list
|
|
78
|
+
mm-harness run wallet.smoke --describe
|
|
77
79
|
mm-harness run path/to/recipe.json --plan
|
|
78
|
-
mm-harness run path/to/recipe.json
|
|
79
|
-
mm-harness run wallet.smoke --artifacts-dir temp/wallet-proof
|
|
80
|
+
mm-harness run path/to/recipe.json
|
|
80
81
|
```
|
|
81
82
|
|
|
82
|
-
`
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
directory contains the executed recipe, summary, trace, diagnostics when
|
|
87
|
-
applicable, screenshots/logs, and `artifact-manifest.json`. Core is headless, so
|
|
88
|
-
browser/device diagnostics do not apply.
|
|
83
|
+
`run` selects a checkout-local artifact directory unless
|
|
84
|
+
`--artifacts-dir <dir>` overrides it. Human output prints diagnostics and
|
|
85
|
+
absolute paths to the report, trace, executed recipe, and artifact manifest.
|
|
86
|
+
Use `mm-harness last --json` to resume without repeating the last operation.
|
|
89
87
|
|
|
90
|
-
|
|
91
|
-
|
|
88
|
+
For automation, `--json` emits one stable document. `run --json-stream` emits
|
|
89
|
+
line-buffered JSONL progress and a terminal event.
|
|
92
90
|
|
|
93
|
-
## Team
|
|
94
|
-
|
|
95
|
-
Add a named shared directory with `RECIPE_LIBRARY_PATH`:
|
|
91
|
+
## Team libraries
|
|
96
92
|
|
|
97
93
|
```bash
|
|
98
94
|
export RECIPE_LIBRARY_PATH="team=$HOME/shared-library/team-recipes"
|
|
99
95
|
mm-harness flows
|
|
96
|
+
mm-harness run --list
|
|
100
97
|
```
|
|
101
98
|
|
|
102
|
-
|
|
103
|
-
|
|
99
|
+
Shared libraries hold durable actions, flows, and recipes. Task acceptance
|
|
100
|
+
criteria remain task-local. See [Recipes](docs/RECIPES.md).
|
|
104
101
|
|
|
105
|
-
##
|
|
102
|
+
## Recover
|
|
106
103
|
|
|
107
104
|
```bash
|
|
108
|
-
mm-harness doctor
|
|
109
|
-
mm-harness doctor --fix
|
|
110
|
-
mm-harness verify
|
|
111
|
-
mm-harness cleanup
|
|
105
|
+
mm-harness doctor
|
|
106
|
+
mm-harness doctor --fix
|
|
107
|
+
mm-harness verify
|
|
108
|
+
mm-harness cleanup
|
|
112
109
|
```
|
|
113
110
|
|
|
114
|
-
|
|
115
|
-
and
|
|
116
|
-
|
|
111
|
+
Failures name one exact next action. Core is headless; browser, device, logs,
|
|
112
|
+
and debugger capabilities are reported as unavailable instead of fabricated.
|
|
113
|
+
Stable exit codes are `1` runtime/action failure, `2` invalid CLI usage, `3`
|
|
114
|
+
infrastructure failure, `4` bounded recovery refusal, and `5` validation/trust
|
|
115
|
+
failure.
|
|
117
116
|
|
|
118
|
-
##
|
|
117
|
+
## Reference
|
|
119
118
|
|
|
120
|
-
|
|
119
|
+
- [Recipes](docs/RECIPES.md) — discover, compose, author, and share proof.
|
|
120
|
+
- [Security](docs/SECURITY.md) — trust, approval, fixtures, and evidence safety.
|
|
121
|
+
- [QA](docs/QA.md) — clean-machine and human release checks.
|
|
122
|
+
- [Contributing](docs/CONTRIBUTING.md) — ownership, layout, and change gates.
|
|
123
|
+
|
|
124
|
+
For development, point the installed command at a source checkout:
|
|
121
125
|
|
|
122
126
|
```bash
|
|
123
127
|
export MM_HARNESS_BIN=/path/to/metamask-harness/bin/mm-harness
|
|
124
128
|
mm-harness --version
|
|
125
129
|
```
|
|
126
130
|
|
|
127
|
-
Unset `MM_HARNESS_BIN` to return to the published
|
|
128
|
-
|
|
129
|
-
Validate changes with:
|
|
130
|
-
|
|
131
|
-
```bash
|
|
132
|
-
yarn check
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
Current references: [cheatsheet](docs/CHEATSHEET.md), [recipe libraries](docs/recipe-libraries.md),
|
|
136
|
-
and [human QA](docs/CLI-ERGONOMICS-HUMAN-QA.md). Maintainers also have the
|
|
137
|
-
[architecture](docs/architecture.md) and [video gate](docs/VIDEO-DEMO-VALIDATION.md).
|
|
131
|
+
Unset `MM_HARNESS_BIN` to return to the published installation.
|
|
@@ -277,6 +277,8 @@ const fs = require('fs');
|
|
|
277
277
|
const http = require('http');
|
|
278
278
|
const { execSync, execFileSync } = require('child_process');
|
|
279
279
|
let chromium; try { chromium = require('@playwright/test').chromium; } catch { chromium = require('playwright').chromium; }
|
|
280
|
+
const { isolatedProfileArgs, remoteDebuggingArgs } = require(path.join('${SCRIPT_DIR}', 'lib/chrome-args.cjs'));
|
|
281
|
+
const { captureMacFrontmostProcess, restoreMacFrontmostProcess } = require(path.join('${SCRIPT_DIR}', 'lib/macos-focus.cjs'));
|
|
280
282
|
|
|
281
283
|
const SLOT_ID = '${SLOT_ID}';
|
|
282
284
|
const AGENT_DIR = '${AGENT_DIR}';
|
|
@@ -307,11 +309,18 @@ const resumeWebpack = () => {
|
|
|
307
309
|
'--disable-renderer-backgrounding',
|
|
308
310
|
'--no-first-run',
|
|
309
311
|
'--no-default-browser-check',
|
|
312
|
+
...isolatedProfileArgs(),
|
|
310
313
|
'--window-size=420,800',
|
|
311
314
|
];
|
|
312
|
-
|
|
315
|
+
// Debug-port trio (address + port + scoped allow-origins) from the shared module
|
|
316
|
+
// so the DevTools origin allow-list can never drift away from the port; without
|
|
317
|
+
// it Chrome 403s every browser DevTools WebSocket and mm-harness debug can't attach.
|
|
318
|
+
if (CDP_PORT) {
|
|
319
|
+
args.push(...remoteDebuggingArgs(CDP_PORT));
|
|
320
|
+
}
|
|
313
321
|
|
|
314
322
|
const chromiumApp = path.dirname(path.dirname(path.dirname(chromium.executablePath())));
|
|
323
|
+
const previousFrontmostPid = captureMacFrontmostProcess();
|
|
315
324
|
execFileSync('open', ['-g', '-n', '-a', chromiumApp, '--args', ...args], { stdio: 'ignore' });
|
|
316
325
|
for (let i = 0; i < 60; i++) {
|
|
317
326
|
try {
|
|
@@ -414,6 +423,8 @@ const resumeWebpack = () => {
|
|
|
414
423
|
}
|
|
415
424
|
if (process.env.MM_HARNESS_FOCUS_BROWSER === '1') {
|
|
416
425
|
await page.bringToFront().catch(() => {});
|
|
426
|
+
} else {
|
|
427
|
+
restoreMacFrontmostProcess(previousFrontmostPid);
|
|
417
428
|
}
|
|
418
429
|
|
|
419
430
|
fs.writeFileSync(path.join(AGENT_DIR, 'extension.id'), extId);
|
|
@@ -24,6 +24,7 @@ import path from 'node:path';
|
|
|
24
24
|
import { spawnSync } from 'node:child_process';
|
|
25
25
|
import process from 'node:process';
|
|
26
26
|
import { recipeHarnessRoot } from '../shared/recipe-harness-root.mjs';
|
|
27
|
+
import { harnessSourceFingerprint } from '../shared/harness-source-fingerprint.mjs';
|
|
27
28
|
|
|
28
29
|
function usage() {
|
|
29
30
|
console.error('Usage: inject-extension-harness.mjs [--target <metamask-extension>] [--no-git-exclude]');
|
|
@@ -127,6 +128,8 @@ copyFile(path.join(runnerDir, 'adapters/shared/tmux-viewer.sh'), path.join(harne
|
|
|
127
128
|
copyFile(path.join(runnerDir, 'adapters/shared/log-tui.mjs'), path.join(harnessDir, 'scripts/lib/log-tui.mjs'));
|
|
128
129
|
copyFile(path.join(runnerDir, 'adapters/extension/lib/extension-id.cjs'), path.join(harnessDir, 'scripts/lib/extension-id.cjs'));
|
|
129
130
|
copyFile(path.join(runnerDir, 'adapters/extension/lib/slot-title.cjs'), path.join(harnessDir, 'scripts/lib/slot-title.cjs'));
|
|
131
|
+
copyFile(path.join(runnerDir, 'adapters/extension/lib/chrome-args.cjs'), path.join(harnessDir, 'scripts/lib/chrome-args.cjs'));
|
|
132
|
+
copyFile(path.join(runnerDir, 'adapters/extension/lib/macos-focus.cjs'), path.join(harnessDir, 'scripts/lib/macos-focus.cjs'));
|
|
130
133
|
makeExecutableTree(path.join(harnessDir, 'scripts'));
|
|
131
134
|
fs.writeFileSync(path.join(harnessDir, 'installed-scripts.sha256'), `${dirContentHash(path.join(harnessDir, 'scripts'))}\n`);
|
|
132
135
|
|
|
@@ -136,6 +139,10 @@ const manifest = {
|
|
|
136
139
|
adapter: 'extension',
|
|
137
140
|
installedAt: new Date().toISOString(),
|
|
138
141
|
runnerPackageVersion,
|
|
142
|
+
// Fingerprint of the runner source that produced this overlay. The launch-time
|
|
143
|
+
// freshness guard re-injects when the runner's fingerprint no longer matches,
|
|
144
|
+
// so a slot never silently runs a stale injected launcher.
|
|
145
|
+
sourceFingerprint: harnessSourceFingerprint(runnerDir),
|
|
139
146
|
source: {
|
|
140
147
|
runnerDir,
|
|
141
148
|
runnerRevision,
|
|
@@ -24,6 +24,8 @@ const os = require('node:os');
|
|
|
24
24
|
const path = require('node:path');
|
|
25
25
|
const { execFileSync, spawn, spawnSync } = require('node:child_process');
|
|
26
26
|
const { extensionIdFromExtensionDir } = require('./lib/extension-id.cjs');
|
|
27
|
+
const { isolatedProfileArgs, remoteDebuggingArgs } = require('./lib/chrome-args.cjs');
|
|
28
|
+
const { captureMacFrontmostProcess, restoreMacFrontmostProcess } = require('./lib/macos-focus.cjs');
|
|
27
29
|
|
|
28
30
|
if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
|
29
31
|
console.log(
|
|
@@ -92,10 +94,12 @@ if (args['stop-only'] !== undefined) {
|
|
|
92
94
|
const initialUrl = args['start-url'] || extensionHomeUrl(args['extension-dir']) || 'chrome://extensions/';
|
|
93
95
|
const chromeArgs = [
|
|
94
96
|
`--user-data-dir=${args.profile}`,
|
|
95
|
-
|
|
96
|
-
|
|
97
|
+
// Debug-port trio (address + port + scoped allow-origins) from the shared module,
|
|
98
|
+
// so the DevTools origin allow-list can never drift away from the port.
|
|
99
|
+
...remoteDebuggingArgs(cdpPort),
|
|
97
100
|
'--no-first-run',
|
|
98
101
|
'--disable-first-run-ui',
|
|
102
|
+
...isolatedProfileArgs(),
|
|
99
103
|
'--disable-default-apps',
|
|
100
104
|
'--disable-popup-blocking',
|
|
101
105
|
'--disable-extensions-file-access-check',
|
|
@@ -109,11 +113,11 @@ const chromeArgs = [
|
|
|
109
113
|
initialUrl,
|
|
110
114
|
];
|
|
111
115
|
const logFd = fs.openSync(args['chrome-log'], 'a');
|
|
116
|
+
const previousFrontmostPid = captureMacFrontmostProcess();
|
|
112
117
|
let browserPid;
|
|
113
118
|
try {
|
|
114
119
|
const application = process.platform === 'darwin' ? macApplicationForExecutable(args['chrome-bin']) : null;
|
|
115
120
|
if (application) {
|
|
116
|
-
// Launch Services does not attach Chrome's stdio, so chrome.log may stay empty; CDP ownership is the readiness signal.
|
|
117
121
|
execFileSync('open', ['-g', '-n', '-a', application, '--args', ...chromeArgs], {
|
|
118
122
|
env: sanitizedChildEnv(),
|
|
119
123
|
stdio: ['ignore', logFd, logFd],
|
|
@@ -135,6 +139,9 @@ try {
|
|
|
135
139
|
fs.closeSync(logFd);
|
|
136
140
|
}
|
|
137
141
|
fs.writeFileSync(args['chrome-pid'], `${browserPid}\n`);
|
|
142
|
+
if (process.env.MM_HARNESS_FOCUS_BROWSER !== '1') {
|
|
143
|
+
restoreMacFrontmostProcess(previousFrontmostPid);
|
|
144
|
+
}
|
|
138
145
|
|
|
139
146
|
function sanitizedChildEnv() {
|
|
140
147
|
return {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// chrome-args.cjs — single source of truth for the remote-debugging launch flags
|
|
3
|
+
// shared by BOTH extension launchers: the fresh spawn (launch-browser.cjs) and the
|
|
4
|
+
// reopen path (ensure-browser.sh → reopen-browser.sh). Keeping the debug-port trio
|
|
5
|
+
// here means `--remote-allow-origins` can never drift away from the port it scopes:
|
|
6
|
+
// the two are declared once and always travel together. Without the allow-origins
|
|
7
|
+
// entry Chrome 403s every browser DevTools WebSocket on the debug port and
|
|
8
|
+
// `mm-harness debug` cannot attach a visible console.
|
|
9
|
+
|
|
10
|
+
// The exact origin allowed is the locally-served bundled DevTools frontend, which
|
|
11
|
+
// Chrome serves at http://127.0.0.1:<cdpPort>. Scoped to that origin — never `*`.
|
|
12
|
+
function remoteDebuggingArgs(cdpPort) {
|
|
13
|
+
const port = Number(cdpPort);
|
|
14
|
+
if (!Number.isInteger(port) || port <= 0) {
|
|
15
|
+
throw new Error(`remoteDebuggingArgs: invalid cdp port: ${cdpPort}`);
|
|
16
|
+
}
|
|
17
|
+
return [
|
|
18
|
+
'--remote-debugging-address=127.0.0.1',
|
|
19
|
+
`--remote-debugging-port=${port}`,
|
|
20
|
+
`--remote-allow-origins=http://127.0.0.1:${port}`,
|
|
21
|
+
];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// An isolated automation profile must never ask for or write credentials in the
|
|
25
|
+
// operator's macOS login keychain. Chromium's mock keychain keeps profile-local
|
|
26
|
+
// encryption deterministic and prevents modal prompts from blocking startup.
|
|
27
|
+
function isolatedProfileArgs(platform = process.platform) {
|
|
28
|
+
return platform === 'darwin' ? ['--use-mock-keychain'] : [];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = { isolatedProfileArgs, remoteDebuggingArgs };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { execFileSync } = require('node:child_process');
|
|
4
|
+
|
|
5
|
+
function captureMacFrontmostProcess() {
|
|
6
|
+
if (process.platform !== 'darwin') return null;
|
|
7
|
+
try {
|
|
8
|
+
const value = execFileSync('osascript', [
|
|
9
|
+
'-e',
|
|
10
|
+
'tell application "System Events" to get unix id of first application process whose frontmost is true',
|
|
11
|
+
], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
12
|
+
const pid = Number.parseInt(value, 10);
|
|
13
|
+
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
14
|
+
} catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function restoreMacFrontmostProcess(pid) {
|
|
20
|
+
if (process.platform !== 'darwin' || !Number.isInteger(pid) || pid <= 0) return false;
|
|
21
|
+
try {
|
|
22
|
+
execFileSync('osascript', [
|
|
23
|
+
'-e',
|
|
24
|
+
`tell application "System Events" to set frontmost of first application process whose unix id is ${pid} to true`,
|
|
25
|
+
], { stdio: 'ignore' });
|
|
26
|
+
return true;
|
|
27
|
+
} catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = { captureMacFrontmostProcess, restoreMacFrontmostProcess };
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
set -euo pipefail
|
|
33
33
|
|
|
34
34
|
TARGET="$PWD"
|
|
35
|
+
INVOCATION_DIR="$PWD"
|
|
35
36
|
CDP_PORT=""
|
|
36
37
|
ARTIFACTS=""
|
|
37
38
|
OUT=""
|
|
@@ -39,7 +40,7 @@ PREPARE_CMD="${RECIPE_HARNESS_EXTENSION_LAUNCH_CMD:-}"
|
|
|
39
40
|
LAUNCH_EXISTING_DIST=false
|
|
40
41
|
START_WATCH=false
|
|
41
42
|
DIST_DIR="dist/chrome"
|
|
42
|
-
CHROME_USER_DATA_DIR=""
|
|
43
|
+
CHROME_USER_DATA_DIR="${CHROME_USER_DATA_DIR:-}"
|
|
43
44
|
REMOTE_FLAGS=""
|
|
44
45
|
REMOTE_FLAGS_APPLIED=false
|
|
45
46
|
while [ "$#" -gt 0 ]; do
|
|
@@ -60,6 +61,11 @@ while [ "$#" -gt 0 ]; do
|
|
|
60
61
|
esac
|
|
61
62
|
done
|
|
62
63
|
|
|
64
|
+
case "$CHROME_USER_DATA_DIR" in
|
|
65
|
+
""|/*) ;;
|
|
66
|
+
*) CHROME_USER_DATA_DIR="$INVOCATION_DIR/$CHROME_USER_DATA_DIR" ;;
|
|
67
|
+
esac
|
|
68
|
+
|
|
63
69
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
64
70
|
# shellcheck disable=SC1091
|
|
65
71
|
for _hp in "$SCRIPT_DIR/lib/harness-path.sh" "$SCRIPT_DIR/../shared/harness-path.sh"; do
|
|
@@ -211,11 +217,11 @@ try {
|
|
|
211
217
|
executable = chromium.executablePath();
|
|
212
218
|
} catch (error) {
|
|
213
219
|
const message = error && error.message ? error.message : String(error);
|
|
214
|
-
console.error(`[recipe-harness] Could not resolve Playwright Chromium executable: ${message}. Manual approval required before installing the Playwright Chromium browser cache (no package.json changes); ask the user before running yarn
|
|
220
|
+
console.error(`[recipe-harness] Could not resolve Playwright Chromium executable: ${message}. Manual approval required before installing the Playwright Chromium browser cache (no package.json changes); ask the user before running yarn playwright install chromium.`);
|
|
215
221
|
process.exit(1);
|
|
216
222
|
}
|
|
217
223
|
if (!fs.existsSync(executable)) {
|
|
218
|
-
console.error(`[recipe-harness] Playwright Chromium is not installed at ${executable}. Manual approval required before installing the Playwright Chromium browser cache (no package.json changes). Ask the user for approval; if they agree, run: cd ${shellQuote(process.cwd())} && yarn
|
|
224
|
+
console.error(`[recipe-harness] Playwright Chromium is not installed at ${executable}. Manual approval required before installing the Playwright Chromium browser cache (no package.json changes). Ask the user for approval; if they agree, run: cd ${shellQuote(process.cwd())} && yarn playwright install chromium`);
|
|
219
225
|
console.error('[recipe-harness] To use a browser that is already installed, set RECIPE_HARNESS_CHROME_BIN=/path/to/chrome explicitly.');
|
|
220
226
|
process.exit(1);
|
|
221
227
|
}
|
|
@@ -227,25 +233,9 @@ NODE
|
|
|
227
233
|
rm -f "$_chrome_probe"
|
|
228
234
|
fi
|
|
229
235
|
|
|
230
|
-
if [ -z "${CHROME_BIN:-}" ]; then
|
|
231
|
-
for candidate in \
|
|
232
|
-
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
|
|
233
|
-
"/Applications/Chromium.app/Contents/MacOS/Chromium" \
|
|
234
|
-
"/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary" \
|
|
235
|
-
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"; do
|
|
236
|
-
if [ -x "$candidate" ]; then
|
|
237
|
-
CHROME_BIN="$candidate"
|
|
238
|
-
break
|
|
239
|
-
fi
|
|
240
|
-
done
|
|
241
|
-
if [ -n "${CHROME_BIN:-}" ]; then
|
|
242
|
-
echo "[recipe-harness] WARN: Playwright Chromium unavailable; falling back to system browser: $CHROME_BIN" >&2
|
|
243
|
-
fi
|
|
244
|
-
fi
|
|
245
|
-
|
|
246
236
|
if [ -z "${CHROME_BIN:-}" ]; then
|
|
247
237
|
[ -n "${CHROME_PROBE_LOG:-}" ] && cat "$CHROME_PROBE_LOG" >&2 2>/dev/null || true
|
|
248
|
-
echo "[recipe-harness] No
|
|
238
|
+
echo "[recipe-harness] No compatible Chromium binary selected; stopping before build or live Extension launch." >&2
|
|
249
239
|
exit 1
|
|
250
240
|
fi
|
|
251
241
|
quoted_chrome="$(printf '%q' "$CHROME_BIN")"
|
package/adapters/manifest.json
CHANGED
|
@@ -487,6 +487,14 @@
|
|
|
487
487
|
"inputs": "--adapter extension|mobile [--cdp-port|--port] [--target page|worker] [--json] [--no-open]",
|
|
488
488
|
"outputs": "debug URL/report on stdout; exit 0/1"
|
|
489
489
|
},
|
|
490
|
+
{
|
|
491
|
+
"id": "lib/harness-source-fingerprint",
|
|
492
|
+
"entry": "adapters/shared/harness-source-fingerprint.mjs",
|
|
493
|
+
"kind": "lib",
|
|
494
|
+
"purpose": "Compute the deterministic source fingerprint stamped into injected extension harness manifests.",
|
|
495
|
+
"inputs": "imported by injection and launch freshness checks",
|
|
496
|
+
"outputs": "stable SHA-256 fingerprint"
|
|
497
|
+
},
|
|
490
498
|
{
|
|
491
499
|
"id": "lib/update-check-worker",
|
|
492
500
|
"entry": "adapters/shared/update-check-worker.mjs",
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
# --simulator <udid|name|"booted"> iOS simulator (default: IOS_SIMULATOR env, else "booted")
|
|
14
14
|
# --adb-serial <serial> Android device serial (default: ADB_SERIAL env)
|
|
15
15
|
# --preflight-mode fast|auto|rebuild-native|clean (default: fast; env MOBILE_PREFLIGHT_MODE)
|
|
16
|
+
# --ui-only iOS only: ensure Simulator.app is visible; do not relaunch the app
|
|
16
17
|
# Outputs:
|
|
17
18
|
# Progress on stderr; exit 0 pass; 1 open failed; 2 bad args.
|
|
18
19
|
#
|
|
@@ -25,6 +26,7 @@ SIMULATOR="${IOS_SIMULATOR:-${SIM_UDID:-booted}}"
|
|
|
25
26
|
ADB_SERIAL_ARG="${ADB_SERIAL:-${ANDROID_SERIAL:-}}"
|
|
26
27
|
PREFLIGHT_MODE="${MOBILE_PREFLIGHT_MODE:-fast}"
|
|
27
28
|
PORT="${WATCHER_PORT:-${METRO_PORT:-8081}}"
|
|
29
|
+
UI_ONLY=false
|
|
28
30
|
|
|
29
31
|
while [ "$#" -gt 0 ]; do
|
|
30
32
|
case "$1" in
|
|
@@ -34,10 +36,11 @@ while [ "$#" -gt 0 ]; do
|
|
|
34
36
|
--adb-serial) ADB_SERIAL_ARG="$2"; shift 2 ;;
|
|
35
37
|
--preflight-mode) PREFLIGHT_MODE="$2"; shift 2 ;;
|
|
36
38
|
--port) PORT="$2"; shift 2 ;;
|
|
39
|
+
--ui-only) UI_ONLY=true; shift ;;
|
|
37
40
|
-h|--help)
|
|
38
41
|
printf 'Usage: open-device.sh [--platform ios|android] [--target <dir>]\n'
|
|
39
42
|
printf ' [--simulator <udid|name|booted>] [--adb-serial <serial>]\n'
|
|
40
|
-
printf ' [--preflight-mode fast|auto|rebuild-native|clean] [--port <n>]\n'
|
|
43
|
+
printf ' [--preflight-mode fast|auto|rebuild-native|clean] [--port <n>] [--ui-only]\n'
|
|
41
44
|
exit 0
|
|
42
45
|
;;
|
|
43
46
|
*) printf 'open-device: unknown arg: %s\n' "$1" >&2; exit 2 ;;
|
|
@@ -48,6 +51,10 @@ case "$PLATFORM" in
|
|
|
48
51
|
ios|android) ;;
|
|
49
52
|
*) printf 'open-device: --platform must be ios or android (got: %s)\n' "$PLATFORM" >&2; exit 2 ;;
|
|
50
53
|
esac
|
|
54
|
+
if $UI_ONLY && [ "$PLATFORM" != "ios" ]; then
|
|
55
|
+
printf 'open-device: --ui-only is supported only for ios.\n' >&2
|
|
56
|
+
exit 2
|
|
57
|
+
fi
|
|
51
58
|
|
|
52
59
|
TARGET="$(cd "$TARGET" && pwd)"
|
|
53
60
|
|
|
@@ -149,19 +156,46 @@ boot_simulator_if_needed() {
|
|
|
149
156
|
xcrun simctl bootstatus "$udid" -b >/dev/null
|
|
150
157
|
}
|
|
151
158
|
|
|
152
|
-
|
|
159
|
+
simulator_ui_running() {
|
|
160
|
+
pgrep -x Simulator >/dev/null 2>&1
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
open_simulator_in_background_if_needed() {
|
|
153
164
|
local target="$1" udid=""
|
|
165
|
+
[ "${MM_HARNESS_SHOW_SIMULATOR:-1}" != "0" ] || return 0
|
|
166
|
+
simulator_ui_running && return 0
|
|
167
|
+
printf 'Opening Simulator UI in the background\n' >&2
|
|
154
168
|
if [ "$target" = "booted" ]; then
|
|
155
|
-
|
|
156
|
-
|
|
169
|
+
open -g -a Simulator >/dev/null 2>&1 || {
|
|
170
|
+
printf 'open-device: failed to open Simulator UI.\n' >&2
|
|
171
|
+
return 1
|
|
172
|
+
}
|
|
157
173
|
else
|
|
158
174
|
udid="$(sim_udid_for_target "$target")"
|
|
159
175
|
if [ -n "$udid" ]; then
|
|
160
|
-
open -a Simulator --args -CurrentDeviceUDID "$udid" >/dev/null 2>&1 ||
|
|
176
|
+
open -g -a Simulator --args -CurrentDeviceUDID "$udid" >/dev/null 2>&1 || {
|
|
177
|
+
printf 'open-device: failed to open Simulator UI for %s.\n' "$target" >&2
|
|
178
|
+
return 1
|
|
179
|
+
}
|
|
161
180
|
else
|
|
162
|
-
open -a Simulator >/dev/null 2>&1 ||
|
|
181
|
+
open -g -a Simulator >/dev/null 2>&1 || {
|
|
182
|
+
printf 'open-device: failed to open Simulator UI.\n' >&2
|
|
183
|
+
return 1
|
|
184
|
+
}
|
|
163
185
|
fi
|
|
164
186
|
fi
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
focus_simulator() {
|
|
190
|
+
local target="$1" udid=""
|
|
191
|
+
if [ "$target" != "booted" ]; then
|
|
192
|
+
udid="$(sim_udid_for_target "$target")"
|
|
193
|
+
fi
|
|
194
|
+
if [ -n "$udid" ]; then
|
|
195
|
+
open -a Simulator --args -CurrentDeviceUDID "$udid" >/dev/null 2>&1 || true
|
|
196
|
+
else
|
|
197
|
+
open -a Simulator >/dev/null 2>&1 || true
|
|
198
|
+
fi
|
|
165
199
|
osascript -e 'tell application "Simulator" to activate' >/dev/null 2>&1 || true
|
|
166
200
|
}
|
|
167
201
|
|
|
@@ -220,8 +254,12 @@ if [ "$PLATFORM" = "ios" ]; then
|
|
|
220
254
|
DEV_CLIENT_SCHEME="${IOS_DEV_CLIENT_SCHEME:-expo-metamask}"
|
|
221
255
|
|
|
222
256
|
boot_simulator_if_needed "$SIM_TARGET" || exit 1
|
|
257
|
+
open_simulator_in_background_if_needed "$SIM_TARGET" || exit 1
|
|
223
258
|
if [ "${MM_HARNESS_FOCUS_SIMULATOR:-0}" = "1" ]; then
|
|
224
|
-
|
|
259
|
+
focus_simulator "$SIM_TARGET"
|
|
260
|
+
fi
|
|
261
|
+
if $UI_ONLY; then
|
|
262
|
+
exit 0
|
|
225
263
|
fi
|
|
226
264
|
|
|
227
265
|
LAUNCHED=false
|
|
@@ -329,9 +329,21 @@ JSON
|
|
|
329
329
|
|
|
330
330
|
ensure_live_runtime() {
|
|
331
331
|
local device_target="$1"
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
332
|
+
local attempt
|
|
333
|
+
for attempt in 1 2 3 4 5; do
|
|
334
|
+
if live_status_ok "$ARTIFACTS/logs/app-status-precheck.log" "$device_target"; then
|
|
335
|
+
return 0
|
|
336
|
+
fi
|
|
337
|
+
# A freshly booted simulator can expose its Metro bridge just before
|
|
338
|
+
# `simctl list devices booted` publishes the device. Bound that launch→verify
|
|
339
|
+
# race instead of reporting a false DEVICE_NOT_FOUND immediately.
|
|
340
|
+
if [ "$attempt" -lt 5 ] && grep -q '"code"[[:space:]]*:[[:space:]]*"DEVICE_NOT_FOUND"' "$ARTIFACTS/logs/app-status-precheck.log"; then
|
|
341
|
+
echo "Waiting for Mobile device discovery (${attempt}/5): ${device_target:-configured target}" >&2
|
|
342
|
+
sleep 1
|
|
343
|
+
continue
|
|
344
|
+
fi
|
|
345
|
+
break
|
|
346
|
+
done
|
|
335
347
|
if [ "$AUTO_START" = true ]; then
|
|
336
348
|
cat >&2 <<'EOF'
|
|
337
349
|
Mobile auto-start is not allowed from product-local scripts. Start or prepare the app through the runner/slot runtime, then rerun verify with --no-auto-start.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// harness-source-fingerprint.mjs — deterministic fingerprint of the runner files
|
|
2
|
+
// that get injected into a slot's harness overlay.
|
|
3
|
+
//
|
|
4
|
+
// inject.mjs stamps this into the slot manifest at install time; the launch-time
|
|
5
|
+
// freshness guard recomputes it and re-injects when they differ, so a slot can
|
|
6
|
+
// never silently run a stale injected launcher after the runner changes. This is
|
|
7
|
+
// what makes the runner the single source of truth: edit a runner adapter file and
|
|
8
|
+
// the next launch refreshes every slot from it.
|
|
9
|
+
//
|
|
10
|
+
// stat-only (size + mtime) so it is cheap enough to run on every launch; no file
|
|
11
|
+
// contents are read and no Date.now()/random is used (both would break determinism).
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import crypto from 'node:crypto';
|
|
15
|
+
|
|
16
|
+
// Trees whose contents are copied into <slot>/harness by inject.mjs. Any change
|
|
17
|
+
// here means injected slots are stale.
|
|
18
|
+
const SOURCE_DIRS = ['adapters/extension', 'adapters/shared', 'library'];
|
|
19
|
+
|
|
20
|
+
export function harnessSourceFingerprint(runnerDir) {
|
|
21
|
+
const entries = [];
|
|
22
|
+
for (const rel of SOURCE_DIRS) {
|
|
23
|
+
collect(path.join(runnerDir, rel), runnerDir, entries);
|
|
24
|
+
}
|
|
25
|
+
entries.sort();
|
|
26
|
+
return crypto.createHash('sha256').update(entries.join('\n')).digest('hex');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function collect(dir, root, out) {
|
|
30
|
+
let dirents;
|
|
31
|
+
try {
|
|
32
|
+
dirents = fs.readdirSync(dir, { withFileTypes: true });
|
|
33
|
+
} catch {
|
|
34
|
+
return; // absent tree contributes nothing
|
|
35
|
+
}
|
|
36
|
+
for (const dirent of dirents) {
|
|
37
|
+
// Skip hidden noise (.DS_Store, editor swap files) so the fingerprint tracks
|
|
38
|
+
// only real source and does not thrash re-injects.
|
|
39
|
+
if (dirent.name.startsWith('.')) continue;
|
|
40
|
+
const full = path.join(dir, dirent.name);
|
|
41
|
+
if (dirent.isDirectory()) {
|
|
42
|
+
collect(full, root, out);
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (!dirent.isFile()) continue;
|
|
46
|
+
const stat = fs.statSync(full);
|
|
47
|
+
out.push(`${path.relative(root, full)}|${stat.size}|${Math.floor(stat.mtimeMs)}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -4,11 +4,7 @@ set -euo pipefail
|
|
|
4
4
|
TARGET=""
|
|
5
5
|
while [[ $# -gt 0 ]]; do
|
|
6
6
|
case "$1" in
|
|
7
|
-
--target)
|
|
8
|
-
[[ $# -ge 2 ]] || { echo "Usage: install-repo-deps.sh --target <checkout>" >&2; exit 2; }
|
|
9
|
-
TARGET="$2"
|
|
10
|
-
shift 2
|
|
11
|
-
;;
|
|
7
|
+
--target) TARGET="$2"; shift 2 ;;
|
|
12
8
|
-h|--help)
|
|
13
9
|
echo "Usage: install-repo-deps.sh --target <checkout>"
|
|
14
10
|
exit 0
|