@deeeed/metamask-harness 0.84.0 → 0.86.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 +65 -0
- package/README.md +5 -3
- package/adapters/extension/console-tail.mjs +11 -7
- package/adapters/extension/ensure-browser.sh +17 -18
- package/adapters/extension/refresh-build.sh +1 -1
- package/adapters/extension/start-watch.sh +17 -8
- package/adapters/manifest.json +1 -1
- package/adapters/mobile/reload-app.mjs +2 -1
- package/adapters/mobile/start-console-forwarder.sh +8 -0
- package/adapters/shared/resolve-slot-ports-core.mjs +48 -6
- package/dist/adapters/console-capture.js +3 -1
- package/dist/adapters/extension/actions.js +5 -0
- package/dist/adapters/extension/console-capture.js +18 -0
- package/dist/adapters/extension/fixtures.js +2 -0
- package/dist/adapters/extension/overlay.js +5 -4
- package/dist/adapters/extension/runtime.js +2 -0
- package/dist/adapters/mobile/launch.js +19 -2
- package/dist/adapters/mobile/provision.js +4 -2
- package/dist/adapters/mobile/run.js +3 -2
- package/dist/adapters/mobile/verify.js +15 -7
- package/dist/adapters/web-dapp/actions.js +7 -5
- package/dist/adapters.js +5 -3
- package/dist/command-contract.js +32 -20
- package/dist/commands/check.js +5 -7
- package/dist/commands/debug.js +2 -2
- package/dist/commands/doctor.js +2 -3
- package/dist/commands/domain.js +4 -3
- package/dist/commands/fixtures.js +3 -3
- package/dist/commands/help.js +6 -5
- package/dist/commands/launch/extension.js +9 -7
- package/dist/commands/logs.js +2 -2
- package/dist/commands/prepare.js +4 -4
- package/dist/commands/provision.js +2 -2
- package/dist/commands/review.js +8 -10
- package/dist/commands/runtime-launch.js +2 -0
- package/dist/commands/status-probe.js +8 -1
- package/dist/commands/status.js +4 -2
- package/dist/commands/task-init.js +2 -3
- package/dist/console-classifier.js +3 -2
- package/dist/doctor.js +6 -2
- package/dist/mm-harness-cli.js +19 -6
- package/dist/paths.js +9 -4
- package/dist/recipe-security.js +1 -1
- package/dist/review/knowledge.js +16 -4
- package/dist/runner.js +18 -3
- package/docs/CONTRIBUTING.md +11 -0
- package/docs/QA.md +1 -0
- package/docs/RECIPES.md +18 -2
- package/docs/cli-contract.md +8 -6
- package/library/actions/core/perps/_controller.mjs +2 -2
- package/library/actions/extension/app/assert_no_console_errors.mjs +95 -0
- package/library/actions/extension/platform/cdp.mjs +72 -2
- package/library/actions/extension/ui/navigate.mjs +3 -1
- package/library/actions/extension/wallet/select_account.mjs +17 -17
- package/library/actions/extension/wallet/state.mjs +75 -21
- package/library/actions/mobile/app/assert_no_console_errors.mjs +129 -0
- package/library/actions/mobile/wallet/select_account.mjs +34 -18
- package/library/actions/shared/console-assert.mjs +174 -0
- package/library/actions/shared/console-collector.mjs +10 -3
- package/library/actions/shared/console-findings.mjs +108 -7
- package/library/actions/web-dapp/app/assert_no_console_errors.mjs +10 -54
- package/library/actions/web-dapp/platform/hud.mjs +2 -9
- package/library/actions/web-dapp/platform/page.mjs +62 -16
- package/library/actions/web-dapp/platform/runtime.mjs +1 -1
- package/library/diagnostics/console-allowlist.json +13 -0
- package/library/manifests/core.action-manifest.json +6 -6
- package/library/manifests/extension.action-manifest.json +37 -1
- package/library/manifests/mobile.action-manifest.json +40 -4
- package/library/manifests/web-dapp.action-manifest.json +1 -1
- package/library/recipes/core/health/check.recipe.json +40 -0
- package/library/recipes/extension/health/check.recipe.json +71 -0
- package/library/recipes/mobile/health/check.recipe.json +71 -0
- package/package.json +9 -9
- package/scripts/completions.sh +2 -2
- package/site/assets/help-recipes.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,71 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.86.0 - 2026-10-09
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `MM_HARNESS_SIBLING_LIBRARIES=0` turns off sibling discovery: libraries and reference checkouts beside the checkout or under `~/shared-library` are no longer found.
|
|
10
|
+
- `call` takes `--hud show|hide` like `run`. With recipe-cli 0.11.1 a call draws the HUD by default, and its HUD text is its `intent=` input (`mm-harness call ui.screenshot intent="Show the order filled"`).
|
|
11
|
+
- `MOBILE_RELOAD_VERIFY_TIMEOUT_MS` sets how long `adapters/mobile/reload-app.mjs` waits to verify a reload (default 30 000), and `MM_HARNESS_CONSOLE_ATTACH_TIMEOUT_MS` how long `launch` waits for the Extension console collector to attach (default about 10 s). A value that is not a positive number uses the default. Contract tests use them where no app or extension target can answer.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- Depend on `@farmslot/recipe-runner` 0.28.1, `@farmslot/recipe-cli` 0.11.1, `@farmslot/adapter-sdk` 0.8.1, `@farmslot/adapter-web` 0.6.5, `@farmslot/adapter-node` 0.1.7 and `@farmslot/adapter-rn` 0.19.4 (were 0.28.0, 0.11.0, 0.8.0, 0.6.4, 0.1.6 and 0.19.3), pinned together; the install holds one `@farmslot/recipe-runner` and one `@farmslot/adapter-sdk`. Farmslot #869 and #870:
|
|
16
|
+
- `run` and `call` draw the HUD for every recipe source, an untrusted or inherited one (a task's `recipe-source.json`) included, unless `--hud hide`. The HUD is part of the plan an approval covers, so an untrusted-source approval made before this release without `--hud hide` (on an adapter that draws the HUD), or with another `--hud` setting, must be approved again; the refusal prints the new digest.
|
|
17
|
+
- The HUD on Extension pages is drawn in a closed shadow root with its text capped at 180 characters, so a node's own intent can no longer satisfy its `ui.wait_for text=` check or catch a text-target press.
|
|
18
|
+
- `call app.hud` drives the HUD itself, with no automatic updates.
|
|
19
|
+
- A Mobile `run` arms and collects the app's issue buffer through `METAMASK_RECIPE_MOBILE_BRIDGE_SCRIPT` when it is set, as launch, status and the action library already do (it always used the bundled `cdp-bridge.cjs`). An empty value means the bundled bridge, as in the action library.
|
|
20
|
+
- Extension `start-watch.sh` checks for the webpack compile marker every 0.5 s (was 2 s) and still refreshes the watcher pid every 2 s; its 480 s limit is now wall-clock time. It waits for stale watchers to stop instead of a fixed 2 s.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- Mobile `status` and `doctor` pair a physical Android device whose bridge entry carries a manufacturer prefix. A Pixel 6a over USB lists as `model:Pixel_6a` in adb while its bridge entry says `Google Pixel 6a`; it read `liveState: no-bridge`, so `doctor --print-ready` printed `not-ready` and Farmslot slot.check health failed on an unlocked wallet. The prefixed name pairs only the sole physical device (not `emulator-*`) of that model, so a same-model emulator cannot take the phone's entry; bare-model matching is unchanged.
|
|
25
|
+
- Extension `ui.navigate hash=` and `url=` accept a route the Extension router rewrites in the same document. Extension 13.52 canonicalizes Perps symbols (`#/perps/trade/sol` becomes `#/perps/trade/SOL`, `XYZ%3Atsla` becomes `xyz%3ATSLA`, query kept), and readiness waited for the requested URL until it timed out. Readiness for a requested URL now takes the rewritten URL once it holds for two polls, if only the hash changed on the same extension page (same origin, path and query). Another origin, extension page or query, a `chrome-error://` page or a document that never becomes ready still fails; the error names the last URL (`Extension page did not reach <url> within <n>ms (last URL <url>)`). A lock screen that redirects to `#/unlock` is reported, not failed, so assert the route on `href`. `hash=` returns `href` (where the page landed), `requestedHref` and `redirected`; `url=` returns `href`.
|
|
26
|
+
- `metamask.wallet.select_account name=` on Extension and Mobile also matches account group names. Multichain wallets (Extension 13.52) keep fixture names such as `dev1` on the account group while the internal account stays `Account 1`, so `name=dev1` failed with `Requested account was not found in internalAccounts`. A name with no internal account match now selects the EVM account of the account group with that name; a name two account groups share, or a group with no EVM account, fails without selecting anything. Extension, like Mobile, returns `alreadySelected: true` without a write when the requested account is already selected.
|
|
27
|
+
|
|
28
|
+
- Docs, `help` and the Mobile, Extension and Core Perps action descriptions no longer say mainnet is read-only; they state the real mainnet rule (the shared Perps order rule, Core's `CORE_PERPS_ALLOW_MAINNET_WRITES=1`, Mobile cleanup acting on the app's current network, Extension cancel refused on mainnet outside the funded release lane). Core close, ensure and teardown descriptions no longer claim testnet only.
|
|
29
|
+
- Contributors: the feature-guard scripts (`differential.mjs`, `release-smoke.mjs`, `builds.mjs`) reach a remote host with `-o ConnectTimeout=10` on every `ssh` and `scp`, and `BatchMode=yes` on `scp` as on `ssh`. A host name whose first address does not answer (`ssh macpro.local` from macwork: `connect to host macpro.local port 22: Undefined error: 0`) failed or hung without it; with it, ssh moves on to the next address.
|
|
30
|
+
- Extension `refresh-build.sh` writes its build log under `$TMPDIR` as `refresh-build.XXXXXX`, without the `.log` suffix (macOS `mktemp` takes no suffix after the X's); macOS `mktemp -t` ignored `TMPDIR`, so each contract run left two logs in the machine's temp dir.
|
|
31
|
+
- Contributors: a unit run gets one TMPDIR that every worker inherits. When the run ends, interrupted (Ctrl-C, SIGTERM) or not, the processes still naming it are stopped and it is removed; an interrupted run used to leave its per-file temp dirs, vitest's transform dir and detached test browsers behind. Every unit run also left a CDP broker socket in `/tmp` (the console-forwarder test killed the forwarder before it could release it), and the mutation-receipt test now writes under TMPDIR, not the checkout. Two contract tests whose bare `mktemp -d` ignored the runner's TMPDIR on macOS now create under it.
|
|
32
|
+
- Contributors: contract and unit tests no longer see the operator's recipe libraries. `ct_init` and the unit setup point `MM_HARNESS_CONFIG` at a missing file, set `MM_HARNESS_SIBLING_LIBRARIES=0`, point `FARMSLOT_HOME` into the sandbox (the personal library) and clear `RECIPE_LIBRARY_PATH` and any exported `MM_HARNESS_REF_<ADAPTER>`. Port claims go to a sandbox `MM_HARNESS_PORT_REGISTRY`, not `~/.cache/mm-harness/ports`. From a checkout with perps and terminal configured, or checked out beside it, `run-name-resolution` and `extension-perps-g005` failed (`mm-harness-wave2` failed with the config). A test that covers one of these sources sets it up itself.
|
|
33
|
+
|
|
34
|
+
## 0.85.0 - 2026-10-09
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
|
|
38
|
+
- `health.check` recipes for Mobile, Extension and Core: a read-only readiness check to run before ticket work. A failed precondition stops at a node named after its error (for example `WALLET_LOCKED`, `NOT_ON_HOME` or, on Mobile and Extension, `CONSOLE_ERRORS`), whose intent gives the recovery command. `mm-harness help` lists it under the target step.
|
|
39
|
+
- `metamask.app.assert_no_console_errors` on Mobile and Extension, as on `web-dapp`: it fails on a console error or exception that no console allowlist covers, judging the events since the launch began by their own timestamps, and fails as unavailable unless the capture is proven (the slot's console forwarder or collector is running and a control line logged in the app or MetaMask reaches the log). `launch` records when it starts, and console lines replayed from before then (by an app runtime or browser the launch kept) are left out. Mobile reads `app-console.log`, which every launch now restarts empty along with the forwarder's replay cursor, and judges only the device the bridge selects; Extension reads `extension-console.log` from the offset `launch` records, and its collector now stamps each line with the event's own time (UTC, with milliseconds). Known app noise is accepted only when it also appears on the app's unchanged base build: the bundled library ships `diagnostics/console-allowlist.json` (the Extension unlock page's `UNSAFE_componentWillMount` warning), read ahead of other libraries' by these actions and by run diagnostics, and the classifier carries pattern rules for base-reproduced errors one exact entry cannot cover: Mobile's SES lockdown intrinsic reports and its notifications startup failure (`createOnChainTriggers`), whose message carries the bundle line; Extension's `getBearerToken` while locked (MetaMask/metamask-extension#46688) and the Money query adapter's `mutations` TypeError (TAT-4063), whose stacks name build-specific chunks. Allowlist entries can name the `app` source Mobile lines carry, and Mobile console lines match without the forwarder's time and device prefix.
|
|
40
|
+
|
|
41
|
+
### Changed
|
|
42
|
+
|
|
43
|
+
- **BREAKING:** `run --hud` takes `show` or `hide`, default `show`. `auto` is gone: the CLI refuses it as an invalid choice (`--hud must be show or hide; received 'auto'.`); drop the flag to get the default. The recipe HUD, each node's intent, now draws on every adapter that implements `app.hud`, headless browsers included. A headless web-dapp (Terminal) browser gets it where `auto` skipped it; Mobile and Extension already drew it by default. A Mobile release artifact still runs without it, since it has no dev bridge to draw with. `METAMASK_RECIPE_AUTO_HUD=0` stays the internal off switch for self-test, verify and the test sandboxes, and `--hud show` wins over it.
|
|
44
|
+
- **BREAKING:** web-dapp `ui.screenshot` keeps the HUD in the capture (it hid it while capturing), so Terminal screenshot evidence shows the node's intent as Mobile and Extension evidence does. `--hud hide` is the way to a HUD-free screenshot.
|
|
45
|
+
- A `surface=wallet` web-dapp `ui.screenshot` (the MetaMask confirmation window or side panel) shows the running node's intent too: the HUD is a bar docked to that MetaMask page's bottom edge for the capture only, which reserves its own height so the request and the footer buttons move up rather than being covered (pointer-events none), and is removed after it.
|
|
46
|
+
- A run with the HUD off (`--hud hide`, a `call`, an untrusted source) first clears the HUD an earlier run left on the slot, so its screenshots never show a previous recipe's intent: web-dapp deletes `hud.json` (the wallet host redraws from it) and the drawn HUD on the app tabs; Extension removes the runner HUD from its pages. Nothing is launched to do it, `METAMASK_RECIPE_AUTO_HUD=0` skips it, and a failed clear prints one `WARN:` line on stderr.
|
|
47
|
+
- Inside a checkout, every command resolves its adapter, target and slot once, through recipe-cli's context resolver, so `status`, `doctor`, `run` and the rest need no `--adapter`. The order is: flag, then the checkout's runtime binding, then its slot, then a unique detect match among the built-ins and the operator's library plugins (matched by their declared `detect`, before any plugin code loads).
|
|
48
|
+
- A checkout that more than one adapter matches stops with `ADAPTER_AMBIGUOUS` (exit 2) and lists the candidates. With no match, a command keeps its "could not detect the MetaMask repo type" error.
|
|
49
|
+
- `status`, `doctor`, `run`, `call` and discovery (`actions`, `run --list`/`--describe`, `call --list`) carry `context` in their `--json` envelopes (each value and its source), as does a command's own `USAGE` error once its context has resolved. Grammar refusals (raised before resolution), the task view (`status --task`/`--watch`) and the other commands' envelopes, `launch` on every adapter included, are unchanged. Human output adds one `context:` line on stderr when the adapter was not given by a flag.
|
|
50
|
+
- Terminal detection needs a recipe-terminal library that declares `detect` (recipe-terminal #8, 8a44aee); with an older library, pass `--adapter terminal`.
|
|
51
|
+
- `status` accepts `--adapter` and `--platform` (it refused them with `CLI_UNKNOWN_OPTION`).
|
|
52
|
+
- The slot comes from mm-harness's own pool resolution (the configured Farmslot root, else the nearest Farmslot checkout's `pool/`) unless `FARMSLOT_POOL_DIR` or `FARMSLOT_ROOT` names one, so a slot checkout beside a Farmslot checkout resolves its slot, and its ports fill `--cdp-port`/`--watcher-port`, with no env.
|
|
53
|
+
- Commands no longer detect the adapter themselves: status, doctor, check, provision, prepare, help, task init, `verify`'s overlay, debug, logs, fixtures, review and domain all read the resolved one. `check` still refuses `--platform ios|android`.
|
|
54
|
+
- `review` and `domain` scope to the resolved adapter when review covers it (mobile, extension, core, terminal), else run unscoped. Only an explicit `--adapter` that names another adapter is refused.
|
|
55
|
+
- Depend on `@farmslot/recipe-cli` 0.11.0 and `@farmslot/adapter-sdk` 0.8.0 (were 0.10.0 and 0.7.1) for the context resolver (deeeed/farmslot#864), on `@farmslot/recipe-runner` 0.28.0, `@farmslot/protocol` 0.35.0 and `@farmslot/agent-runtime` 0.17.1 (were 0.27.0, 0.34.0 and 0.17.0), and on `@farmslot/adapter-web` 0.6.4, `@farmslot/adapter-rn` 0.19.3 and `@farmslot/adapter-node` 0.1.6 (were 0.6.2, 0.19.1 and 0.1.4). With them, a web-dapp launch stops waiting for CDP when its browser dies (deeeed/farmslot#865). The install holds one `@farmslot/adapter-sdk` and one `@farmslot/recipe-runner`.
|
|
56
|
+
- Help lists the selectable adapters (the registered ones, then those the operator's libraries and the invocation's `--library` entries declare) wherever it names `--adapter` or `--platform`, instead of a fixed `mobile|extension|core|terminal`.
|
|
57
|
+
- **BREAKING:** Mobile `verify` takes `--watcher-port` (the public `verify` grammar accepted it; the Mobile leaf refused it) and reads `RECIPE_WATCHER_PORT`: the Metro port is `--watcher-port`, then `RECIPE_WATCHER_PORT`, then `WATCHER_PORT`, then the slot's `.js.env`. With no port env set, a checkout the slot pool maps gets the pool slot's port through `WATCHER_PORT`, so that port now wins over the checkout's `.js.env` `WATCHER_PORT`.
|
|
58
|
+
- **BREAKING:** A `RECIPE_RUNTIME_CONTEXT` that belongs to another checkout no longer sets the Extension slot's ports (`CDP_PORT`, `WATCHER_PORT`, `SLOT_ID`), Extension `verify`'s `RECIPE_SLOT_ID` and `RECIPE_HARNESS_EXTENSION_ID` (it reads the checkout's own context instead), or the Mobile provision slot (simulator, slot id). It counts when its `repoRoot` is the checkout's Git top level (so a clone nested inside another checkout never takes that checkout's context), or, without a `repoRoot`, when the file sits inside the checkout: the rule recipe-cli applies to the runtime binding. A context outside every checkout that names no `repoRoot` used to be authoritative; an orchestrator that keeps the file elsewhere sets `repoRoot`.
|
|
59
|
+
- Contributors: `bash tests/contract/run.sh` runs half the cores' worth of tests at once (`-j N`), each in its own process group, `TMPDIR` and output file, and prints each test's duration and the 15 slowest. A test gets 60 s (`CONTRACT_TEST_TIMEOUT`, or `# contract: timeout=<s>` in its header) and fails as `timed out`; what it leaves running is killed and named on a `LEAK` line; a test that rebuilds `dist/` declares `# contract: serial`. An unfiltered run over `CONTRACT_BUDGET` (180 s) fails, and a stopped run first lets each running test clean up (its EXIT trap reaps the leaves its CLI detached), then kills every process group left in the test's tree. A process a test leaves behind can no longer stall the run.
|
|
60
|
+
- Contributors: `yarn test:goldens` runs its families through `run.sh` and fails over `GOLDENS_BUDGET` (120 s); a golden case gets 60 s (`GOLDENS_CASE_TIMEOUT`, was 600 s). Docs: CONTRIBUTING and cli-contract describe both.
|
|
61
|
+
- Contributors: `AGENTS.md` has a `## Test speed` section: the default and CI suite must take 5 minutes or less (the contract and goldens runs check their own budgets; build and unit are not checked), and a new or changed test uses no fixed waits of 1 s or more, moves to `tests/live/` when it takes over 5 s (a golden case over 30 s), tears down every background process, and never rebuilds `dist/` under concurrent readers.
|
|
62
|
+
- Site: the platform map (`site/platform.html`) is a static page instead of the zoomable canvas. It shows the five layers (Libraries, MetaMask preset, Adapters, CLI, Engine) with their packages, one `mm-harness run` traced through them, and who changes what. It renders from `file://` (no `fetch`), so `site/assets/platform.json` is removed.
|
|
63
|
+
|
|
64
|
+
### Fixed
|
|
65
|
+
|
|
66
|
+
- A command no longer runs `git rev-parse` on every library it finds before dispatch. Library path hydration ran on every command, `--version` included, and pinned each discovered library (a sibling checkout, a config entry, the fetch cache) to its revision, which it never reads. The revision is now looked up only by what reports it (`status`, `domain`, `review`), as before. In a workspace with recipe-library checkouts beside the product checkout, that was one git process per library on every start.
|
|
67
|
+
- `tests/contract/run.sh`, and `yarn test:goldens` through it, refuse to start (exit 2, `Next: npm run build`) when `dist/` is missing or older than any `src/**/*.ts`. `bin/mm-harness` then runs the TypeScript source on every start, type-stripped each time (about 260 ms more per start, over a thousand starts per run). CI and the full local suite build first, so only a bare `run.sh` or `yarn test:goldens` was affected.
|
|
68
|
+
- Extension reopen (`adapters/extension/ensure-browser.sh`) no longer builds its `node -e` script or its `python3` profile cleanup from the slot id, checkout, runtime, profile, extension and runner paths, CDP port, webpack pid or wallet fixture path. The scripts are fixed text and get those values as argv. Before, a checkout path with an apostrophe (`/tmp/Arthur's checkout`) broke the reopen script's syntax and silently skipped the profile cleanup, and a crafted value could change the code the reopen ran.
|
|
69
|
+
|
|
5
70
|
## 0.84.0 - 2026-10-08
|
|
6
71
|
|
|
7
72
|
### Changed
|
package/README.md
CHANGED
|
@@ -419,9 +419,11 @@ A team library supplies `review/antipatterns.md` (one `##` section per family),
|
|
|
419
419
|
optional `review/antipatterns.<adapter>.md` families appended for that adapter only,
|
|
420
420
|
`review/parity.md`, and `owned-paths.json`; the harness composes them and never
|
|
421
421
|
copies them. Locations resolve env (`RECIPE_LIBRARY_PATH`, `MM_HARNESS_REF_<ADAPTER>`)
|
|
422
|
-
→ `mm-harness config` → sibling directory beside the checkout
|
|
423
|
-
`<checkout>/.skills-cache/<name>`.
|
|
424
|
-
|
|
422
|
+
→ `mm-harness config` → sibling directory beside the checkout or under
|
|
423
|
+
`~/shared-library` → shallow fetch into `<checkout>/.skills-cache/<name>`.
|
|
424
|
+
`MM_HARNESS_SIBLING_LIBRARIES=0` turns the sibling step off, for libraries and
|
|
425
|
+
reference checkouts alike (the test sandboxes set it). A missing reference
|
|
426
|
+
checkout degrades the parity step to "not checked" with the reason.
|
|
425
427
|
|
|
426
428
|
## Recover
|
|
427
429
|
|
|
@@ -90,10 +90,14 @@ function out(event, destination = null) {
|
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
93
|
+
// A record's time: the UTC time of day of the event's own timestamp (CDP
|
|
94
|
+
// Runtime.Timestamp, ms since the epoch), as the Mobile console forwarder
|
|
95
|
+
// writes it, or of the write when the event carries none. Events the browser
|
|
96
|
+
// replays when a collector attaches keep their own time, so
|
|
97
|
+
// metamask.app.assert_no_console_errors can leave out those from before a
|
|
98
|
+
// launch.
|
|
99
|
+
function stamp(timestamp) {
|
|
100
|
+
return new Date(Number.isFinite(timestamp) ? timestamp : Date.now()).toISOString().slice(11, 23);
|
|
97
101
|
}
|
|
98
102
|
|
|
99
103
|
function shortLabel(target) {
|
|
@@ -160,7 +164,7 @@ function emitConsole(label, params, destination) {
|
|
|
160
164
|
const level = String(params.type || 'log').toUpperCase();
|
|
161
165
|
const text = (params.args || []).map(previewArg).join(' ');
|
|
162
166
|
const [first, ...rest] = text.split('\n');
|
|
163
|
-
out(`${stamp()} [${label}] ${level} ${first}${atSuffix(frameText(params.stackTrace?.callFrames?.[0]))}${rest.length ? `\n${rest.join('\n')}` : ''}`, destination);
|
|
167
|
+
out(`${stamp(params.timestamp)} [${label}] ${level} ${first}${atSuffix(frameText(params.stackTrace?.callFrames?.[0]))}${rest.length ? `\n${rest.join('\n')}` : ''}`, destination);
|
|
164
168
|
}
|
|
165
169
|
|
|
166
170
|
function emitException(label, params, destination) {
|
|
@@ -172,7 +176,7 @@ function emitException(label, params, destination) {
|
|
|
172
176
|
'uncaught exception';
|
|
173
177
|
const frame = frameText(d.stackTrace?.callFrames?.[0]) || frameText({ url: d.url, lineNumber: d.lineNumber, columnNumber: d.columnNumber });
|
|
174
178
|
const [first, ...rest] = String(text).split('\n');
|
|
175
|
-
out(`${stamp()} [${label}] EXCEPTION ${first}${atSuffix(frame)}${rest.length ? `\n${rest.join('\n')}` : ''}`, destination);
|
|
179
|
+
out(`${stamp(params.timestamp)} [${label}] EXCEPTION ${first}${atSuffix(frame)}${rest.length ? `\n${rest.join('\n')}` : ''}`, destination);
|
|
176
180
|
}
|
|
177
181
|
|
|
178
182
|
// Browser log entries (network failures, interventions, deprecations) are
|
|
@@ -186,7 +190,7 @@ function emitLogEntry(label, params, destination) {
|
|
|
186
190
|
// A Log entry's location is its URL as is: a line number appended to a URL
|
|
187
191
|
// could not be told apart from the URL's own text.
|
|
188
192
|
const where = e.url ? e.url : frameText(e.stackTrace?.callFrames?.[0]);
|
|
189
|
-
out(`${stamp()} [${label}] [browser:${source}] ${level} ${e.text || ''}${atSuffix(where)}`, destination);
|
|
193
|
+
out(`${stamp(e.timestamp)} [${label}] [browser:${source}] ${level} ${e.text || ''}${atSuffix(where)}`, destination);
|
|
190
194
|
}
|
|
191
195
|
|
|
192
196
|
function handleMessage(label, destination, data) {
|
|
@@ -235,9 +235,10 @@ echo "[cleanup] Reusing profile ${PROFILE}"
|
|
|
235
235
|
# shutdowns; --load-extension re-advertises the path but does not flip it back,
|
|
236
236
|
# leaving MV3 service workers inert).
|
|
237
237
|
if [ -f "${PROFILE}/Default/Preferences" ]; then
|
|
238
|
-
python3 -
|
|
238
|
+
python3 - "${PROFILE}/Default/Preferences" 2>/dev/null <<'PY' && echo "[cleanup] Cleared crash recovery state + re-enabled extensions"
|
|
239
239
|
import json
|
|
240
|
-
|
|
240
|
+
import sys
|
|
241
|
+
pf = sys.argv[1]
|
|
241
242
|
prefs = json.load(open(pf))
|
|
242
243
|
prefs['profile'] = prefs.get('profile', {})
|
|
243
244
|
prefs['profile']['exit_type'] = 'Normal'
|
|
@@ -248,7 +249,7 @@ for ext_id, ext_prefs in ext_settings.items():
|
|
|
248
249
|
ext_prefs['state'] = 1
|
|
249
250
|
ext_prefs.pop('disable_reasons', None)
|
|
250
251
|
json.dump(prefs, open(pf, 'w'))
|
|
251
|
-
|
|
252
|
+
PY
|
|
252
253
|
fi
|
|
253
254
|
|
|
254
255
|
# Readiness gate: wait until webpack emits a loadable background entry before
|
|
@@ -289,10 +290,13 @@ echo "[reopen] ${SLOT_ID} — CDP:${CDP_PORT:-off}"
|
|
|
289
290
|
cd "${REPO}"
|
|
290
291
|
|
|
291
292
|
trap - EXIT
|
|
292
|
-
|
|
293
|
+
# The reopen script is fixed text; paths, ids and ports reach it as argv, never as source.
|
|
294
|
+
IFS= read -r -d '' REOPEN_SCRIPT <<'NODE' || true
|
|
295
|
+
const [SCRIPT_DIR, SLOT_ID, AGENT_DIR, EXTENSION, REPO, RUNNER_BIN, PROFILE, cdpPortArg, webpackPidArg, WALLET_FIXTURE] =
|
|
296
|
+
process.argv.slice(1);
|
|
293
297
|
const path = require('path');
|
|
294
298
|
const fs = require('fs');
|
|
295
|
-
const adapterWeb = (name) => require('module').createRequire(path.join(
|
|
299
|
+
const adapterWeb = (name) => require('module').createRequire(path.join(SCRIPT_DIR, 'package.json'))('@farmslot/adapter-web/' + name);
|
|
296
300
|
const http = require('http');
|
|
297
301
|
const { execFileSync } = require('child_process');
|
|
298
302
|
let chromium; try { chromium = require('@playwright/test').chromium; } catch { chromium = require('playwright').chromium; }
|
|
@@ -318,15 +322,9 @@ const {
|
|
|
318
322
|
resolveBrowser,
|
|
319
323
|
} = adapterWeb('browser-resolver');
|
|
320
324
|
|
|
321
|
-
const
|
|
322
|
-
const
|
|
323
|
-
const
|
|
324
|
-
const REPO = '${REPO}';
|
|
325
|
-
const RUNNER_BIN = '${RUNNER_BIN}';
|
|
326
|
-
const PROFILE = '${PROFILE}';
|
|
327
|
-
const CDP_PORT = '${CDP_PORT}' || null;
|
|
328
|
-
const WEBPACK_PID = '${WEBPACK_PID}' || null;
|
|
329
|
-
const walletFixture = JSON.parse(fs.readFileSync('${WALLET_FIXTURE}', 'utf8'));
|
|
325
|
+
const CDP_PORT = cdpPortArg || null;
|
|
326
|
+
const WEBPACK_PID = webpackPidArg || null;
|
|
327
|
+
const walletFixture = JSON.parse(fs.readFileSync(WALLET_FIXTURE, 'utf8'));
|
|
330
328
|
const PASSWORD = walletFixture.password;
|
|
331
329
|
|
|
332
330
|
if (!PASSWORD) {
|
|
@@ -522,8 +520,8 @@ const releaseFocusHold = () => {
|
|
|
522
520
|
await evaluatePageViaCdp(page, applyPersistentSlotTitle, { slotId: SLOT_ID, defaultTitle: 'MetaMask' });
|
|
523
521
|
|
|
524
522
|
// Wait for runtime state to settle, then unlock if needed.
|
|
525
|
-
const unlockSelector = '[data-testid
|
|
526
|
-
const homeSelector = '[data-testid
|
|
523
|
+
const unlockSelector = '[data-testid="unlock-password"]';
|
|
524
|
+
const homeSelector = '[data-testid="account-menu-icon"]';
|
|
527
525
|
let detectedState = 'unknown';
|
|
528
526
|
for (let i = 0; i < 15; i++) {
|
|
529
527
|
if (await page.locator(homeSelector).count()) {
|
|
@@ -544,7 +542,7 @@ const releaseFocusHold = () => {
|
|
|
544
542
|
if (detectedState === 'locked') {
|
|
545
543
|
console.log('[reopen] Unlocking wallet...');
|
|
546
544
|
await page.fill(unlockSelector, PASSWORD);
|
|
547
|
-
await page.click('[data-testid
|
|
545
|
+
await page.click('[data-testid="unlock-submit"]');
|
|
548
546
|
await page.waitForSelector(homeSelector, { timeout: 30000 });
|
|
549
547
|
console.log('[reopen] Unlocked');
|
|
550
548
|
} else if (detectedState === 'onboarding') {
|
|
@@ -611,4 +609,5 @@ const releaseFocusHold = () => {
|
|
|
611
609
|
console.error(e);
|
|
612
610
|
process.exit(1);
|
|
613
611
|
});
|
|
614
|
-
|
|
612
|
+
NODE
|
|
613
|
+
exec node -e "$REOPEN_SCRIPT" "$SCRIPT_DIR" "$SLOT_ID" "$AGENT_DIR" "$EXTENSION" "$REPO" "$RUNNER_BIN" "$PROFILE" "$CDP_PORT" "$WEBPACK_PID" "$WALLET_FIXTURE"
|
|
@@ -103,7 +103,7 @@ fi
|
|
|
103
103
|
|
|
104
104
|
echo "[refresh] Using node $(node -v 2>/dev/null || echo 'not found')"
|
|
105
105
|
echo "[refresh] Starting yarn start (watcher-port=${WATCHER_PORT:-default})..."
|
|
106
|
-
LOG="$(mktemp
|
|
106
|
+
LOG="$(mktemp "${TMPDIR:-/tmp}/refresh-build.XXXXXX")"
|
|
107
107
|
echo "[refresh] Log: $LOG"
|
|
108
108
|
env ${PORT_ARG} yarn start > "$LOG" 2>&1 &
|
|
109
109
|
BUILD_PID=$!
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
# <runtime-dir>/recipe-harness-webpack.{pid,log}; optional --summary file
|
|
20
20
|
# {feature,status,inputs,outputs,generatedAt}.
|
|
21
21
|
# Exit 0 — compiled; 1 — slot watcher owns the checkout, build failed, or
|
|
22
|
-
# compile-marker timeout (
|
|
22
|
+
# compile-marker timeout (480 s); 2 — bad args.
|
|
23
23
|
#
|
|
24
24
|
# Never touches: the slot-owned webpack.pid watcher process (refusal only,
|
|
25
25
|
# removes the pid file only when stale); product source files.
|
|
@@ -229,8 +229,11 @@ existing_watch_pids="$(watcher_pids || true)"
|
|
|
229
229
|
if [ -n "$existing_watch_pids" ]; then
|
|
230
230
|
echo "[recipe-harness] Stopping stale target-scoped watcher(s): $(printf '%s' "$existing_watch_pids" | tr '\n' ' ')"
|
|
231
231
|
printf '%s\n' "$existing_watch_pids" | xargs kill 2>/dev/null || true
|
|
232
|
-
|
|
233
|
-
|
|
232
|
+
for _ in 1 2 3 4 5 6 7 8 9 10; do
|
|
233
|
+
existing_watch_pids="$(watcher_pids || true)"
|
|
234
|
+
[ -z "$existing_watch_pids" ] && break
|
|
235
|
+
sleep 0.2
|
|
236
|
+
done
|
|
234
237
|
if [ -n "$existing_watch_pids" ]; then
|
|
235
238
|
printf '%s\n' "$existing_watch_pids" | xargs kill -9 2>/dev/null || true
|
|
236
239
|
fi
|
|
@@ -290,7 +293,13 @@ elif [ -n "$LIB_DIR" ] && [ "${RECIPE_LOG_TUI_PARENT:-}" != "1" ]; then
|
|
|
290
293
|
watch_tail_pid=$!
|
|
291
294
|
fi
|
|
292
295
|
compiled=false
|
|
293
|
-
|
|
296
|
+
# The log is checked every 0.5 s, so a marker is seen at once. The watcher pid
|
|
297
|
+
# needs a full process listing, so it is refreshed every 2 s. Bounded by time:
|
|
298
|
+
# 480 s for a compile marker, 20 s for yarn start's watcher to appear.
|
|
299
|
+
poll_started=$SECONDS
|
|
300
|
+
i=0
|
|
301
|
+
while [ $((SECONDS - poll_started)) -lt 480 ]; do
|
|
302
|
+
i=$((i + 1))
|
|
294
303
|
if [ -n "$watch_tail_pid" ] && ! kill -0 "$watch_tail_pid" >/dev/null 2>&1; then
|
|
295
304
|
wait "$watch_tail_pid" 2>/dev/null || true
|
|
296
305
|
if grep -Eq 'compiled successfully|compiled with [0-9]+ warning|MetaMask .* compiled|Bundle end: service worker|Bundle end:.*app-init' "$watch_log" 2>/dev/null; then
|
|
@@ -300,9 +309,9 @@ for i in {1..240}; do
|
|
|
300
309
|
echo '[recipe-harness] compact build watcher exited before compile marker; continuing to poll webpack log' >&2
|
|
301
310
|
watch_tail_pid=""
|
|
302
311
|
fi
|
|
303
|
-
if ! write_canonical_watch_pid; then
|
|
304
|
-
if [
|
|
305
|
-
sleep
|
|
312
|
+
if [ $((i % 4)) -eq 1 ] && ! write_canonical_watch_pid; then
|
|
313
|
+
if [ $((SECONDS - poll_started)) -lt 20 ]; then
|
|
314
|
+
sleep 0.5
|
|
306
315
|
continue
|
|
307
316
|
fi
|
|
308
317
|
[ -n "$watch_tail_pid" ] && kill "$watch_tail_pid" >/dev/null 2>&1 || true
|
|
@@ -331,7 +340,7 @@ for i in {1..240}; do
|
|
|
331
340
|
[ -n "$watch_tail_pid" ] && wait "$watch_tail_pid" 2>/dev/null || true
|
|
332
341
|
break
|
|
333
342
|
fi
|
|
334
|
-
sleep
|
|
343
|
+
sleep 0.5
|
|
335
344
|
done
|
|
336
345
|
if [ "$compiled" != true ]; then
|
|
337
346
|
[ -n "$watch_tail_pid" ] && kill "$watch_tail_pid" >/dev/null 2>&1 || true
|
package/adapters/manifest.json
CHANGED
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"entry": "adapters/mobile/reload-app.mjs",
|
|
48
48
|
"kind": "node",
|
|
49
49
|
"purpose": "Request an in-place React Native app reload through Metro's protocol-v2 message endpoint.",
|
|
50
|
-
"inputs": "--port --json; env WATCHER_PORT, METRO_PORT",
|
|
50
|
+
"inputs": "--port --json; env WATCHER_PORT, METRO_PORT, MOBILE_RELOAD_VERIFY_TIMEOUT_MS (default 30000)",
|
|
51
51
|
"outputs": "reload result on stdout; exit 0 requested / 1 unavailable or invalid input"
|
|
52
52
|
},
|
|
53
53
|
{
|
|
@@ -146,7 +146,8 @@ async function reload(port) {
|
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
async function waitForReload(port, targetBefore, processBefore, observer) {
|
|
149
|
-
const
|
|
149
|
+
const timeoutMs = Number(process.env.MOBILE_RELOAD_VERIFY_TIMEOUT_MS);
|
|
150
|
+
const deadline = Date.now() + (timeoutMs > 0 ? timeoutMs : 30_000);
|
|
150
151
|
let lastError;
|
|
151
152
|
while (Date.now() < deadline) {
|
|
152
153
|
if (processBefore && readAndroidPid() !== processBefore) {
|
|
@@ -72,7 +72,15 @@ if [ -f "$PID_FILE" ]; then
|
|
|
72
72
|
rm -f "$PID_FILE"
|
|
73
73
|
fi
|
|
74
74
|
|
|
75
|
+
# Each launch starts a fresh log. The replay cursor goes with it: it marks
|
|
76
|
+
# lines as written that are no longer in the file, such as the relaunched
|
|
77
|
+
# app's first lines the previous forwarder wrote, so the new forwarder replays
|
|
78
|
+
# the app runtime's console buffer into the new log instead of skipping them.
|
|
79
|
+
# A runtime the launch kept replays lines from before the launch too;
|
|
80
|
+
# metamask.app.assert_no_console_errors leaves those out by their own time
|
|
81
|
+
# against the mark each launch attempt records (app-console.launch.json).
|
|
75
82
|
: > "$APP_LOG_FILE"
|
|
83
|
+
rm -f "$APP_LOG_FILE.forwarder-state.json"
|
|
76
84
|
pid="$(node "$SCRIPT_DIR/launch-console-forwarder.cjs" \
|
|
77
85
|
--port "$PORT" \
|
|
78
86
|
--out "$APP_LOG_FILE" \
|
|
@@ -198,15 +198,57 @@ function mergeKvLayers(...layers) {
|
|
|
198
198
|
return Object.keys(result).length ? formatKvLines(result) : '';
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
+
// Whether a runtime context belongs to the checkout at `repo` (recipe-cli's
|
|
202
|
+
// binding rule): its repoRoot is the checkout's Git top level, so a clone
|
|
203
|
+
// nested inside another checkout never takes that checkout's context; without
|
|
204
|
+
// a repoRoot, the file sits inside that top level. An orchestrator may keep the
|
|
205
|
+
// file elsewhere as long as it names the checkout. Twin of recipe-cli's
|
|
206
|
+
// runtimeContextOwned: this module stays dependency-free for the shell paths
|
|
207
|
+
// that load it, so the rule is repeated here rather than imported.
|
|
208
|
+
export function ownsRuntimeContext(file, repo) {
|
|
209
|
+
let repoRoot;
|
|
210
|
+
try {
|
|
211
|
+
const value = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
212
|
+
repoRoot = typeof value?.repoRoot === 'string' ? value.repoRoot : undefined;
|
|
213
|
+
} catch {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
if (repoRoot === undefined) return within(gitTopLevel(repo), file);
|
|
217
|
+
try {
|
|
218
|
+
return fs.realpathSync(repoRoot) === fs.realpathSync(gitTopLevel(repo));
|
|
219
|
+
} catch {
|
|
220
|
+
// A path that does not exist owns nothing.
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function gitTopLevel(dir) {
|
|
226
|
+
try {
|
|
227
|
+
return execFileSync('git', ['-C', dir, 'rev-parse', '--show-toplevel'], {
|
|
228
|
+
encoding: 'utf8',
|
|
229
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
230
|
+
}).trim();
|
|
231
|
+
} catch {
|
|
232
|
+
// Not a Git checkout: the directory is its own root.
|
|
233
|
+
return dir;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function within(outer, inner) {
|
|
238
|
+
try {
|
|
239
|
+
const relative = path.relative(fs.realpathSync(outer), fs.realpathSync(inner));
|
|
240
|
+
return !(relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative));
|
|
241
|
+
} catch {
|
|
242
|
+
// A path that does not exist owns nothing.
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
201
247
|
function extensionRuntimeContextPath(repo) {
|
|
202
248
|
const repoRoot = path.resolve(repo);
|
|
203
249
|
const envCtx = process.env.RECIPE_RUNTIME_CONTEXT;
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
// explicit environment override as authoritative; containment is not a
|
|
207
|
-
// security boundary and silently dropping it selects the wrong port.
|
|
208
|
-
return path.resolve(envCtx);
|
|
209
|
-
}
|
|
250
|
+
// Another checkout's context would set this checkout's ports.
|
|
251
|
+
if (envCtx && ownsRuntimeContext(path.resolve(envCtx), repoRoot)) return path.resolve(envCtx);
|
|
210
252
|
return path.join(repoRoot, recipeRuntimeDir(), 'agentic-runtime.json');
|
|
211
253
|
}
|
|
212
254
|
|
|
@@ -58,7 +58,9 @@ async function ensureConsoleCollector(options) {
|
|
|
58
58
|
fs.writeFileSync(pidFile, `${child.pid}
|
|
59
59
|
`);
|
|
60
60
|
const attachmentFrom = { [extensionLog]: fileSize(extensionLog), [pageLog]: fileSize(pageLog) };
|
|
61
|
-
|
|
61
|
+
const timeoutMs = Number(process.env.MM_HARNESS_CONSOLE_ATTACH_TIMEOUT_MS);
|
|
62
|
+
const attempts = Math.ceil((timeoutMs > 0 ? timeoutMs : 1e4) / 100);
|
|
63
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
62
64
|
const proc = processArgs(child.pid);
|
|
63
65
|
if (!proc) break;
|
|
64
66
|
if (collectorMatches(proc, identity)) {
|
|
@@ -43,6 +43,11 @@ const extensionActions = {
|
|
|
43
43
|
}
|
|
44
44
|
})
|
|
45
45
|
}),
|
|
46
|
+
// The HUD lives only in the extension pages' DOM.
|
|
47
|
+
async clearHud() {
|
|
48
|
+
const { clearExtensionHud } = await import("../../../library/actions/extension/platform/cdp.mjs");
|
|
49
|
+
await clearExtensionHud();
|
|
50
|
+
},
|
|
46
51
|
trustedMutation: true,
|
|
47
52
|
// ensure_positions and ensure_orders only check state here.
|
|
48
53
|
inputFindings: (nodeId, node) => [
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
2
3
|
import { recipeRuntimePath } from "@farmslot/recipe-cli/harness";
|
|
3
4
|
import { verifyCapture } from "../../../library/actions/shared/console-collector.mjs";
|
|
4
5
|
import { ensureConsoleCollector } from "../console-capture.js";
|
|
@@ -10,6 +11,21 @@ function extensionCaptureFiles(target) {
|
|
|
10
11
|
pidFile: recipeRuntimePath(target, "console-tail.pid")
|
|
11
12
|
};
|
|
12
13
|
}
|
|
14
|
+
const EXTENSION_CONSOLE_LAUNCH_FILE = "extension-console.launch.json";
|
|
15
|
+
function recordConsoleLaunchIn(runtimeDir) {
|
|
16
|
+
const log = path.join(runtimeDir, "extension-console.log");
|
|
17
|
+
const stat = fs.existsSync(log) ? fs.statSync(log) : null;
|
|
18
|
+
fs.mkdirSync(runtimeDir, { recursive: true });
|
|
19
|
+
const launchedAtMs = Date.now();
|
|
20
|
+
fs.writeFileSync(
|
|
21
|
+
path.join(runtimeDir, EXTENSION_CONSOLE_LAUNCH_FILE),
|
|
22
|
+
`${JSON.stringify({ offset: stat?.size ?? 0, inode: stat?.ino ?? null, launchedAtMs, recordedAt: new Date(launchedAtMs).toISOString() })}
|
|
23
|
+
`
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
function recordExtensionConsoleLaunch(target) {
|
|
27
|
+
recordConsoleLaunchIn(path.dirname(extensionCaptureFiles(target).extensionLog));
|
|
28
|
+
}
|
|
13
29
|
function extensionCdpPort() {
|
|
14
30
|
return process.env.CDP_PORT ?? process.env.RECIPE_CDP_PORT;
|
|
15
31
|
}
|
|
@@ -37,8 +53,10 @@ async function verifyExtensionConsoleControl(capture) {
|
|
|
37
53
|
return verifyCapture({ cdpPort: capture.cdpPort, log: capture.extensionLog, target: "extension", extensionId });
|
|
38
54
|
}
|
|
39
55
|
export {
|
|
56
|
+
EXTENSION_CONSOLE_LAUNCH_FILE,
|
|
40
57
|
ensureExtensionConsoleCapture,
|
|
41
58
|
extensionCaptureFiles,
|
|
42
59
|
extensionCdpPort,
|
|
60
|
+
recordExtensionConsoleLaunch,
|
|
43
61
|
verifyExtensionConsoleControl
|
|
44
62
|
};
|
|
@@ -4,6 +4,7 @@ import { colorHumanMessage, EXIT, shellQuote, spawnScriptStreaming } from "@farm
|
|
|
4
4
|
import { FIXTURE_STREAM_TIMEOUT_MS } from "../../commands/fixtures.js";
|
|
5
5
|
import { sameFeatureFlags } from "../../feature-flags.js";
|
|
6
6
|
import { runnerDir } from "../../paths.js";
|
|
7
|
+
import { recordExtensionConsoleLaunch } from "./console-capture.js";
|
|
7
8
|
import { readExtensionPinnedFlags } from "./feature-flags.js";
|
|
8
9
|
import { releaseArtifactState } from "./runtime-decision.js";
|
|
9
10
|
async function applyExtensionFixture({
|
|
@@ -96,6 +97,7 @@ async function applyExtensionFixture({
|
|
|
96
97
|
}
|
|
97
98
|
process.stderr.write(`${colorHumanMessage(`\u2192 fixtures ${action} extension \u2014 resetting and seeding the slot-owned profile from the existing build\u2026`)}
|
|
98
99
|
`);
|
|
100
|
+
recordExtensionConsoleLaunch(target);
|
|
99
101
|
const liveScript = path.join(runnerDir, "adapters/extension/live.sh");
|
|
100
102
|
const args = ["--target", target, "--launch-existing-dist", "--launch-only", "--reset-profile"];
|
|
101
103
|
const flagKeys = Object.keys(fixtureFlags);
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import { readRuntimeContextField, resolveRuntimeContextPath } from "@farmslot/recipe-cli/harness";
|
|
1
|
+
import { readRuntimeContextField, recipeRuntimeDir, resolveRuntimeContextPath } from "@farmslot/recipe-cli/harness";
|
|
2
2
|
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { ownsRuntimeContext } from "../../../adapters/shared/resolve-slot-ports-core.mjs";
|
|
3
5
|
import { resolveExtensionSlotPorts } from "../slot-ports.js";
|
|
4
6
|
function hasArg(args, needle) {
|
|
5
7
|
return args.some((arg) => arg === needle || arg.startsWith(`${needle}=`));
|
|
6
8
|
}
|
|
7
9
|
function applyExtensionRuntimeEnv(target, args) {
|
|
8
|
-
const contextPath = resolveRuntimeContextPath(target);
|
|
9
|
-
|
|
10
|
-
if (contextExists) {
|
|
10
|
+
const contextPath = [resolveRuntimeContextPath(target), path.join(target, recipeRuntimeDir(), "agentic-runtime.json")].find((file) => fs.existsSync(file) && ownsRuntimeContext(file, target));
|
|
11
|
+
if (contextPath) {
|
|
11
12
|
process.env.RECIPE_RUNTIME_CONTEXT = contextPath;
|
|
12
13
|
if (!process.env.RECIPE_SLOT_ID) {
|
|
13
14
|
const slotId = readRuntimeContextField(contextPath, "slotId");
|
|
@@ -4,6 +4,7 @@ import fs from "node:fs";
|
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { importRecipeHarnessRuntimeBrowserExtension, importRecipeHarnessRuntimeCdp, resolveFarmslotPoolDir, runnerDir } from "../../paths.js";
|
|
7
|
+
import { recordExtensionConsoleLaunch } from "./console-capture.js";
|
|
7
8
|
import { poolFilesForHost } from "../resolve-slot-ports.js";
|
|
8
9
|
import { macosSessionLocked, SESSION_LOCKED_MESSAGE, SESSION_LOCKED_USER_ACTION } from "../../../adapters/shared/session-lock.mjs";
|
|
9
10
|
import { profileProcessPids } from "./validation-process-ownership.js";
|
|
@@ -666,6 +667,7 @@ async function launchLockedValidationBrowser(options) {
|
|
|
666
667
|
fs.mkdirSync(validationProfileRoot, { recursive: true });
|
|
667
668
|
fs.mkdirSync(validationArtifactsRoot, { recursive: true });
|
|
668
669
|
assertNoQuarantinedValidationProfiles(validationProfileRoot);
|
|
670
|
+
recordExtensionConsoleLaunch(options.projectRoot);
|
|
669
671
|
await stopOwnedCdpListener(options.cdpPort, [
|
|
670
672
|
validationRuntimePath,
|
|
671
673
|
path.join(options.projectRoot, recipeRuntimeDir())
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
2
3
|
import { recipeRuntimePath, EXIT, str, writeInteractiveProgress } from "@farmslot/recipe-cli/harness";
|
|
3
4
|
import { applyDeviceTargeting } from "../../commands/device-target.js";
|
|
4
5
|
import { launchMobile } from "../../commands/launch/mobile.js";
|
|
@@ -49,6 +50,14 @@ function metroLaunchEvidence(target) {
|
|
|
49
50
|
if (descriptor !== void 0) fs.closeSync(descriptor);
|
|
50
51
|
}
|
|
51
52
|
}
|
|
53
|
+
function recordMobileConsoleLaunch(target) {
|
|
54
|
+
const file = recipeRuntimePath(target, "app-console.launch.json");
|
|
55
|
+
const launchedAtMs = Date.now();
|
|
56
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
57
|
+
fs.writeFileSync(file, `${JSON.stringify({ launchedAtMs, recordedAt: new Date(launchedAtMs).toISOString() })}
|
|
58
|
+
`);
|
|
59
|
+
}
|
|
60
|
+
const markedLaunches = /* @__PURE__ */ new WeakSet();
|
|
52
61
|
const mobileLaunchProfile = {
|
|
53
62
|
refuseFlags({ platformTarget }) {
|
|
54
63
|
if (!platformTarget) {
|
|
@@ -104,7 +113,14 @@ const mobileLaunchProfile = {
|
|
|
104
113
|
},
|
|
105
114
|
nativeInputs: ["ios", "android", "package.json", "yarn.lock"],
|
|
106
115
|
rebuildCommand: ({ platformTarget }) => `mm-harness launch ${platformTarget} --build`,
|
|
107
|
-
compose: (
|
|
116
|
+
compose: (launch, tier, restartApp, clearMetro) => {
|
|
117
|
+
const { target, platformTarget, machine, remoteFlagRequest } = launch;
|
|
118
|
+
if (restartApp || !markedLaunches.has(launch)) {
|
|
119
|
+
markedLaunches.add(launch);
|
|
120
|
+
recordMobileConsoleLaunch(target);
|
|
121
|
+
}
|
|
122
|
+
return launchMobile(target, platformTarget, tier, machine, restartApp, clearMetro, remoteFlagRequest?.flags);
|
|
123
|
+
},
|
|
108
124
|
classify(_launch, output) {
|
|
109
125
|
if (output.includes("open-device: Android device is locked.")) {
|
|
110
126
|
return {
|
|
@@ -187,5 +203,6 @@ const mobileLaunchProfile = {
|
|
|
187
203
|
evidence: metroLaunchEvidence
|
|
188
204
|
};
|
|
189
205
|
export {
|
|
190
|
-
mobileLaunchProfile
|
|
206
|
+
mobileLaunchProfile,
|
|
207
|
+
recordMobileConsoleLaunch
|
|
191
208
|
};
|
|
@@ -4,7 +4,7 @@ import crypto from "node:crypto";
|
|
|
4
4
|
import fs from "node:fs";
|
|
5
5
|
import os from "node:os";
|
|
6
6
|
import path from "node:path";
|
|
7
|
-
import { resolveSlotPortsByRepo } from "../../../adapters/shared/resolve-slot-ports-core.mjs";
|
|
7
|
+
import { ownsRuntimeContext, resolveSlotPortsByRepo } from "../../../adapters/shared/resolve-slot-ports-core.mjs";
|
|
8
8
|
const RUNWAY_IOS_METADATA = {
|
|
9
9
|
artifactName: "ios-app-main-dev-expo",
|
|
10
10
|
workflow: "expo-dev-build.yml",
|
|
@@ -177,8 +177,10 @@ function fail(target, platform, code, message, rerunCommand, slot) {
|
|
|
177
177
|
};
|
|
178
178
|
}
|
|
179
179
|
function readSlotContext(target, runtimeDir) {
|
|
180
|
+
const envContext = process.env.RECIPE_RUNTIME_CONTEXT;
|
|
180
181
|
const candidates = [
|
|
181
|
-
|
|
182
|
+
// Another checkout's context would name its slot and simulator.
|
|
183
|
+
envContext && ownsRuntimeContext(envContext, target) ? envContext : void 0,
|
|
182
184
|
runtimeContextPath(target, runtimeDir),
|
|
183
185
|
path.join(target, recipeRuntimeDir(), "agentic-runtime.json"),
|
|
184
186
|
path.join(target, "temp/recipe/runtime/agentic-runtime.json"),
|
|
@@ -57,7 +57,8 @@ const mobileRun = {
|
|
|
57
57
|
// matches its target against it.
|
|
58
58
|
RECIPE_RN_EXPLICIT_PLATFORM: process.env.RECIPE_RN_EXPLICIT_PLATFORM
|
|
59
59
|
}),
|
|
60
|
-
// An installed release artifact is opaque: no
|
|
60
|
+
// An installed release artifact is opaque: it has no dev bridge and no
|
|
61
|
+
// AgentStepHud, so there is nothing to draw the HUD with.
|
|
61
62
|
autoHud: () => process.env.METAMASK_RECIPE_MOBILE_OPAQUE_RUNTIME !== "1",
|
|
62
63
|
// Always clear a HUD step the engine left on-device (a failed run strands
|
|
63
64
|
// the FAIL banner). Best-effort — never masks the run's real outcome.
|
|
@@ -157,7 +158,7 @@ const mobileObservation = {
|
|
|
157
158
|
}
|
|
158
159
|
};
|
|
159
160
|
function runMobileIssueCommand(projectRoot, command) {
|
|
160
|
-
const bridge = path.join(runnerDir, "adapters", "mobile", "bridge-runtime", "cdp-bridge.cjs");
|
|
161
|
+
const bridge = process.env.METAMASK_RECIPE_MOBILE_BRIDGE_SCRIPT || path.join(runnerDir, "adapters", "mobile", "bridge-runtime", "cdp-bridge.cjs");
|
|
161
162
|
const result = spawnSync(process.execPath, [bridge, command], {
|
|
162
163
|
cwd: projectRoot,
|
|
163
164
|
env: { ...process.env, APP_ROOT: projectRoot, CDP_TIMEOUT: "5000" },
|