@deeeed/metamask-harness 0.47.3 → 0.48.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 +12 -0
- package/README.md +3 -2
- package/adapters/extension/lib/macos-focus.cjs +2 -2
- package/adapters/extension/live.sh +6 -8
- package/adapters/mobile/bridge-runtime/lib/bridge-errors.cjs +1 -0
- package/adapters/mobile/bridge-runtime/lib/cdp-broker.cjs +4 -0
- package/adapters/mobile/reload-app.mjs +92 -28
- package/adapters/shared/ensure-runner-deps.sh +6 -0
- package/dist/adapters/extension/runtime-decision.js +18 -1
- package/dist/adapters/extension/runtime.js +5 -5
- package/dist/adapters/extension/surface.js +1 -0
- package/dist/app-lifecycle.js +3 -1
- package/dist/command-contract.js +2 -0
- package/dist/commands/call.js +1 -8
- package/dist/commands/device-target.js +12 -3
- package/dist/commands/doctor.js +7 -5
- package/dist/commands/fixtures.js +1 -1
- package/dist/commands/launch/index.js +10 -0
- package/dist/commands/manifest.js +3 -1
- package/dist/commands/mobile-device-view.js +13 -3
- package/dist/commands/parse-args.js +2 -0
- package/dist/commands/run-engine.js +29 -4
- package/dist/commands/run.js +1 -1
- package/dist/commands/runtime-launch.js +10 -1
- package/dist/commands/status.js +8 -4
- package/dist/devices.js +31 -9
- package/dist/execution-provenance.js +8 -7
- package/dist/funding-execution-context.js +43 -13
- package/dist/heal-bounds.js +3 -3
- package/dist/live-adapter-contract.js +4 -0
- package/dist/manifest.js +2 -13
- package/dist/mm-harness-cli.js +2 -1
- package/dist/runner.js +14 -3
- package/docs/RECIPES.md +133 -22
- package/docs/RELEASE-QA-CAPABILITY-MAP.md +3 -2
- package/library/actions/core/perps/_controller.mjs +10 -0
- package/library/actions/core/perps/close_orders.mjs +13 -17
- package/library/actions/extension/assets/home-token-identity.mjs +30 -0
- package/library/actions/extension/assets/import_custom_token.mjs +61 -79
- package/library/actions/extension/assets/open_details.mjs +18 -7
- package/library/actions/extension/assets/read_details.mjs +61 -29
- package/library/actions/extension/assets/read_visible_state.mjs +4 -1
- package/library/actions/extension/assets/select_network_scope.mjs +16 -9
- package/library/actions/extension/assets/set_token_visibility.mjs +84 -17
- package/library/actions/extension/assets/verify_sorting.mjs +62 -14
- package/library/actions/extension/networks/add_custom.mjs +19 -17
- package/library/actions/extension/networks/custom_network.mjs +8 -3
- package/library/actions/extension/networks/read_visible_state.mjs +1 -0
- package/library/actions/extension/performance/_navigation-memory.mjs +35 -6
- package/library/actions/extension/performance/compare_idle_navigation_memory.mjs +3 -2
- package/library/actions/extension/performance/measure_detached_dom.mjs +3 -2
- package/library/actions/extension/performance/measure_navigation_memory.mjs +3 -2
- package/library/actions/extension/perps/assert_visible_consistency.mjs +9 -4
- package/library/actions/extension/perps/close_visible_position.mjs +3 -3
- package/library/actions/extension/perps/edit_margin.mjs +1 -1
- package/library/actions/extension/perps/open_balance_action.mjs +16 -7
- package/library/actions/extension/perps/perps.mjs +150 -45
- package/library/actions/extension/perps/read_funds_confirmation.mjs +8 -0
- package/library/actions/extension/perps/read_snapshot.mjs +2 -1
- package/library/actions/extension/perps/read_visible_state.mjs +10 -6
- package/library/actions/extension/perps/search_markets.mjs +7 -2
- package/library/actions/extension/perps/select_activity_filter.mjs +1 -1
- package/library/actions/extension/perps/select_market_filter.mjs +74 -45
- package/library/actions/extension/perps/set_market_favorite.mjs +6 -3
- package/library/actions/extension/perps/state.mjs +1 -0
- package/library/actions/extension/perps/update_position_tpsl.mjs +1 -1
- package/library/actions/extension/perps/visible-mutation-identity.mjs +4 -2
- package/library/actions/extension/platform/cdp.mjs +23 -142
- package/library/actions/extension/swap_bridge/quote-state.mjs +54 -0
- package/library/actions/extension/swap_bridge/read_visible_state.mjs +4 -1
- package/library/actions/extension/swap_bridge/select_assets.mjs +11 -1
- package/library/actions/extension/swap_bridge/set_amount.mjs +10 -8
- package/library/actions/extension/swap_bridge/set_max_amount.mjs +17 -18
- package/library/actions/extension/swap_bridge/set_slippage.mjs +0 -9
- package/library/actions/extension/ui/navigate.mjs +58 -14
- package/library/actions/extension/wallet/lock.mjs +2 -1
- package/library/actions/mobile/assets/import_custom_token.mjs +1 -0
- package/library/actions/mobile/assets/read_visible_state.mjs +3 -1
- package/library/actions/mobile/assets/set_token_visibility.mjs +13 -9
- package/library/actions/mobile/networks/network-management.mjs +22 -15
- package/library/actions/mobile/networks/remove_custom.mjs +6 -2
- package/library/actions/mobile/perps/perps.mjs +3 -0
- package/library/actions/mobile/perps/read-visible-state-loop.mjs +1 -0
- package/library/actions/mobile/perps/set_market_favorite.mjs +15 -19
- package/library/actions/mobile/platform/native-session.mjs +26 -17
- package/library/actions/mobile/platform/observe-ui.mjs +2 -2
- package/library/actions/mobile/ui/navigate.mjs +1 -1
- package/library/actions/shared/perps/visible-state.mjs +14 -3
- package/library/manifests/core.action-manifest.json +14 -2
- package/library/manifests/extension.action-manifest.json +49 -16
- package/library/manifests/mobile.action-manifest.json +8 -7
- package/library/recipes/core/perps/smoke.recipe.json +23 -0
- package/library/recipes/core/perps/snapshot.recipe.json +26 -1
- package/library/recipes/extension/assets/release-custom-network-token-persistence.recipe.json +25 -8
- package/library/recipes/extension/performance/navigation-memory-suite.recipe.json +219 -0
- package/library/recipes/extension/performance/navigation-memory.recipe.json +61 -156
- package/library/recipes/extension/wallet/smoke.recipe.json +17 -8
- package/library/recipes/mobile/app/lifecycle.android-smoke.recipe.json +23 -4
- package/library/recipes/mobile/networks/release-custom-network-persistence.recipe.json +31 -6
- package/library/recipes/mobile/networks/release-multichain-visibility.recipe.json +5 -4
- package/library/recipes/mobile/wallet/smoke.recipe.json +17 -8
- package/library/recipes/{assets → shared/assets}/release-token-address-import.recipe.json +7 -0
- package/library/recipes/{assets → shared/assets}/release-token-details.recipe.json +2 -2
- package/library/recipes/shared/assets/release-token-hide-readd.recipe.json +91 -0
- package/library/recipes/{assets → shared/assets}/release-token-persistence.recipe.json +3 -0
- package/library/recipes/{assets → shared/assets}/release-token-search-import.recipe.json +4 -1
- package/library/recipes/{assets → shared/assets}/release-token-sorting.recipe.json +13 -2
- package/library/recipes/{swap-bridge → shared/swap-bridge}/release-quote.recipe.json +29 -3
- package/package.json +3 -3
- package/site/assets/help-recipes.json +63 -23
- package/library/recipes/assets/release-token-hide-readd.recipe.json +0 -57
- package/library/recipes/core/perps/clean-market-testnet.recipe.json +0 -44
- package/library/recipes/core/perps/order-lifecycle.recipe.json +0 -82
- package/library/recipes/core/perps/read-markets.recipe.json +0 -36
- package/library/recipes/core/perps/trading-lifecycle.recipe.json +0 -80
- package/library/recipes/extension/perps/release-activity-filters.recipe.json +0 -51
- package/library/recipes/extension/perps/release-funds-flows.recipe.json +0 -76
- package/library/recipes/extension/perps/release-live-limit-order.recipe.json +0 -109
- package/library/recipes/extension/perps/release-market-filters.recipe.json +0 -97
- package/library/recipes/extension/perps/release-order-entry.recipe.json +0 -158
- package/library/recipes/extension/perps/release-order-validation.recipe.json +0 -259
- package/library/recipes/extension/perps/release-visible-consistency.recipe.json +0 -47
- package/library/recipes/extension/perps/source-dev-snapshot-consistency.recipe.json +0 -76
- package/library/recipes/mobile/perps/chase-assert-running.recipe.json +0 -110
- package/library/recipes/mobile/perps/chase-place.recipe.json +0 -145
- package/library/recipes/mobile/perps/chase-terminate.recipe.json +0 -98
- package/library/recipes/mobile/perps/performance.recipe.json +0 -722
- package/library/recipes/mobile/perps/pro-order-setup.recipe.json +0 -111
- package/library/recipes/mobile/perps/pro-order-start-state.recipe.json +0 -53
- package/library/recipes/mobile/perps/scale-assert-orders.recipe.json +0 -126
- package/library/recipes/mobile/perps/scale-place.recipe.json +0 -186
- package/library/recipes/mobile/perps/twap-assert-active.recipe.json +0 -97
- package/library/recipes/mobile/perps/twap-place.recipe.json +0 -146
- package/library/recipes/mobile/runner/action-validation.recipe.json +0 -336
- package/library/recipes/perps/clean-market-testnet.recipe.json +0 -49
- package/library/recipes/perps/lifecycle.recipe.json +0 -144
- package/library/recipes/perps/release-live-market-order.recipe.json +0 -139
- package/library/recipes/perps/release-watchlist.recipe.json +0 -111
- package/library/recipes/swap-bridge/release-max-quote.recipe.json +0 -72
- /package/library/recipes/{assets → shared/assets}/release-token-list.recipe.json +0 -0
- /package/library/recipes/{perps → shared/perps}/release-market-details.recipe.json +0 -0
- /package/library/recipes/{perps → shared/perps}/release-market-search.recipe.json +0 -0
- /package/library/recipes/{swap-bridge → shared/swap-bridge}/release-custom-slippage.recipe.json +0 -0
- /package/library/recipes/{swap-bridge → shared/swap-bridge}/release-live-max-swap.recipe.json +0 -0
- /package/library/recipes/{swap-bridge → shared/swap-bridge}/release-live-swap.recipe.json +0 -0
- /package/library/recipes/{swap-bridge → shared/swap-bridge}/release-surface.recipe.json +0 -0
- /package/library/recipes/{wallet → shared/wallet}/import.recipe.json +0 -0
- /package/library/recipes/{wallet → shared/wallet}/reset-import.recipe.json +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.48.0 - 2026-09-07
|
|
6
|
+
|
|
7
|
+
- Consolidate parameterized recipes, move specialized Perps journeys into the team library, and separate short performance checks from full coverage suites.
|
|
8
|
+
- Correct wallet, token, quote and Perps validation; honor network-addition opt-in and preserve identity-bound cleanup and failure evidence. Bound macOS focus calls, avoid duplicate Android preparation and native-session ownership, and refuse dependency repairs through shared directory links.
|
|
9
|
+
- Add compact, searchable tutorials and version-matched recipe guidance for branching, composition, performance, and diagnosing drift.
|
|
10
|
+
|
|
11
|
+
## 0.47.4 - 2026-09-05
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- Verify Mobile reloads through execution-context replacement when React Native retains its Hermes target ID, and advance broker runtime identity on context reset for lifecycle readiness checks.
|
|
16
|
+
|
|
5
17
|
## 0.47.3 - 2026-09-05
|
|
6
18
|
|
|
7
19
|
### Fixed
|
package/README.md
CHANGED
|
@@ -129,7 +129,7 @@ mm-harness actions --action metamask.wallet.ensure_unlocked
|
|
|
129
129
|
mm-harness call metamask.wallet.ensure_unlocked
|
|
130
130
|
|
|
131
131
|
mm-harness run --list
|
|
132
|
-
mm-harness run perps.
|
|
132
|
+
mm-harness run perps.smoke --describe
|
|
133
133
|
mm-harness run wallet.smoke --describe
|
|
134
134
|
mm-harness run wallet.import method=auto
|
|
135
135
|
|
|
@@ -145,7 +145,8 @@ absolute paths to the report, trace, executed recipe, and artifact manifest.
|
|
|
145
145
|
Use `mm-harness last --json` to resume without repeating the last operation.
|
|
146
146
|
|
|
147
147
|
For automation, `--json` emits one stable document. `run --json-stream` emits
|
|
148
|
-
line-buffered JSONL progress and a terminal event.
|
|
148
|
+
line-buffered JSONL progress and a terminal event. Its status matches the recipe's
|
|
149
|
+
`pass`, `fail`, or `unknown` verdict; `unknown` still exits nonzero.
|
|
149
150
|
|
|
150
151
|
## Team libraries
|
|
151
152
|
|
|
@@ -8,7 +8,7 @@ function captureMacFrontmostProcess() {
|
|
|
8
8
|
const value = execFileSync('osascript', [
|
|
9
9
|
'-e',
|
|
10
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();
|
|
11
|
+
], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 1_000 }).trim();
|
|
12
12
|
const pid = Number.parseInt(value, 10);
|
|
13
13
|
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
14
14
|
} catch {
|
|
@@ -22,7 +22,7 @@ function restoreMacFrontmostProcess(pid) {
|
|
|
22
22
|
execFileSync('osascript', [
|
|
23
23
|
'-e',
|
|
24
24
|
`tell application "System Events" to set frontmost of first application process whose unix id is ${pid} to true`,
|
|
25
|
-
], { stdio: 'ignore' });
|
|
25
|
+
], { stdio: 'ignore', timeout: 1_000 });
|
|
26
26
|
return true;
|
|
27
27
|
} catch {
|
|
28
28
|
return false;
|
|
@@ -45,7 +45,7 @@ START_WATCH=false
|
|
|
45
45
|
BUILD_LAVAMOAT=false
|
|
46
46
|
EXTERNAL_ARTIFACT=false
|
|
47
47
|
ARTIFACT_PROVENANCE=""
|
|
48
|
-
RESET_PROFILE=
|
|
48
|
+
RESET_PROFILE=false
|
|
49
49
|
DIST_DIR="dist/chrome"
|
|
50
50
|
CHROME_USER_DATA_DIR="${CHROME_USER_DATA_DIR:-}"
|
|
51
51
|
REMOTE_FLAGS=""
|
|
@@ -62,6 +62,7 @@ while [ "$#" -gt 0 ]; do
|
|
|
62
62
|
--external-artifact) EXTERNAL_ARTIFACT=true; LAUNCH_EXISTING_DIST=true; shift ;;
|
|
63
63
|
--artifact-provenance) [ "$#" -ge 2 ] || { echo "Missing value for $1" >&2; exit 2; }; ARTIFACT_PROVENANCE="$2"; shift 2 ;;
|
|
64
64
|
--preserve-profile) RESET_PROFILE=false; shift ;;
|
|
65
|
+
--reset-profile) RESET_PROFILE=true; shift ;;
|
|
65
66
|
--start-url) [ "$#" -ge 2 ] || { echo "Missing value for $1" >&2; exit 2; }; START_URL="$2"; shift 2 ;;
|
|
66
67
|
--start-watch|--start-test-watch) START_WATCH=true; LAUNCH_EXISTING_DIST=true; shift ;;
|
|
67
68
|
--build-lavamoat) BUILD_LAVAMOAT=true; LAUNCH_EXISTING_DIST=true; shift ;;
|
|
@@ -69,7 +70,7 @@ while [ "$#" -gt 0 ]; do
|
|
|
69
70
|
--dist-dir) [ "$#" -ge 2 ] || { echo "Missing value for $1" >&2; exit 2; }; DIST_DIR="$2"; shift 2 ;;
|
|
70
71
|
--chrome-user-data-dir) [ "$#" -ge 2 ] || { echo "Missing value for $1" >&2; exit 2; }; CHROME_USER_DATA_DIR="$2"; shift 2 ;;
|
|
71
72
|
--remote-flag) [ "$#" -ge 2 ] || { echo "Missing value for $1" >&2; exit 2; }; REMOTE_FLAGS="$2"; shift 2 ;;
|
|
72
|
-
-h|--help) echo "Usage: live.sh [--target <metamask-extension>] [--out <recipes-dir>] [--cdp-port <port>] [--launch-existing-dist|--start-watch|--build-lavamoat|--external-artifact --dist-dir <absolute-dir> --artifact-provenance <absolute-file> [--preserve-profile
|
|
73
|
+
-h|--help) echo "Usage: live.sh [--target <metamask-extension>] [--out <recipes-dir>] [--cdp-port <port>] [--launch-existing-dist|--start-watch|--build-lavamoat|--external-artifact --dist-dir <absolute-dir> --artifact-provenance <absolute-file>|--prepare-cmd <cmd>] [--preserve-profile|--reset-profile] [--launch-only] [--dist-dir dist/chrome] [--remote-flag KEY=VARIANT] [--artifacts-dir <dir>]"; exit 0 ;;
|
|
73
74
|
*) echo "Unknown arg: $1" >&2; exit 2 ;;
|
|
74
75
|
esac
|
|
75
76
|
done
|
|
@@ -86,10 +87,6 @@ if $EXTERNAL_ARTIFACT && [ ! -f "$ARTIFACT_PROVENANCE" ]; then
|
|
|
86
87
|
echo "--external-artifact requires --artifact-provenance <file>" >&2
|
|
87
88
|
exit 2
|
|
88
89
|
fi
|
|
89
|
-
if ! $RESET_PROFILE && ! $EXTERNAL_ARTIFACT; then
|
|
90
|
-
echo "--preserve-profile is only valid for a verified external artifact" >&2
|
|
91
|
-
exit 2
|
|
92
|
-
fi
|
|
93
90
|
if $EXTERNAL_ARTIFACT; then
|
|
94
91
|
case "$ARTIFACT_PROVENANCE" in
|
|
95
92
|
/*) ;;
|
|
@@ -200,11 +197,11 @@ if $LAUNCH_EXISTING_DIST && [ -z "$PREPARE_CMD" ]; then
|
|
|
200
197
|
FIXTURE_STATE_ABS="$ARTIFACTS/fixture-state.json"
|
|
201
198
|
FIXTURE_VALIDATION_ABS="$ARTIFACTS/logs/fixture-account-parity.json"
|
|
202
199
|
WALLET_FIXTURE_ABS=""
|
|
203
|
-
if ! $EXTERNAL_ARTIFACT; then
|
|
200
|
+
if $RESET_PROFILE && ! $EXTERNAL_ARTIFACT; then
|
|
204
201
|
# Wallet fixture resolution chain + provenance live in seed-fixture.sh.
|
|
205
202
|
WALLET_FIXTURE_ABS="$(bash "$SCRIPT_DIR/seed-fixture.sh" resolve --target "$TARGET" --cdp-port "$CDP_PORT" --source-out "$ARTIFACTS/logs/fixture-source.json")"
|
|
206
203
|
fi
|
|
207
|
-
if ! $EXTERNAL_ARTIFACT && [ ! -f "$ARTIFACTS/logs/fixture-source.json" ]; then
|
|
204
|
+
if $RESET_PROFILE && ! $EXTERNAL_ARTIFACT && [ ! -f "$ARTIFACTS/logs/fixture-source.json" ]; then
|
|
208
205
|
# resolve failed before writing provenance (invalid RECIPE_WALLET_FIXTURE);
|
|
209
206
|
# keep the documented missing-provenance artifact.
|
|
210
207
|
node - "$ARTIFACTS/logs/fixture-source.json" "" <<'NODE'
|
|
@@ -370,6 +367,7 @@ display_args=(mm-harness runtime-launch --adapter extension --target "$TARGET" -
|
|
|
370
367
|
$START_WATCH && display_args+=(--start-watch)
|
|
371
368
|
$BUILD_LAVAMOAT && display_args+=(--build-lavamoat)
|
|
372
369
|
$EXTERNAL_ARTIFACT && display_args+=(--external-artifact --dist-dir "$DIST_DIR" --artifact-provenance "$ARTIFACT_PROVENANCE")
|
|
370
|
+
$RESET_PROFILE && display_args+=(--reset-profile)
|
|
373
371
|
$RESET_PROFILE || display_args+=(--preserve-profile)
|
|
374
372
|
[ -n "$REMOTE_FLAGS" ] && display_args+=(--remote-flag "$REMOTE_FLAGS")
|
|
375
373
|
[ -n "$CHROME_USER_DATA_DIR" ] && display_args+=(--chrome-user-data-dir "$CHROME_USER_DATA_DIR")
|
|
@@ -64,6 +64,7 @@ const NEEDLE_CODES = [
|
|
|
64
64
|
['pinned android device', BRIDGE_ERROR_CODES.NO_TARGET],
|
|
65
65
|
['no react native bridge target', BRIDGE_ERROR_CODES.NO_TARGET],
|
|
66
66
|
['cdp broker target unavailable', BRIDGE_ERROR_CODES.NO_TARGET],
|
|
67
|
+
['cdp broker never observed the requested target', BRIDGE_ERROR_CODES.NO_TARGET],
|
|
67
68
|
['websocket closed', BRIDGE_ERROR_CODES.WS_CLOSED],
|
|
68
69
|
['websocket error', BRIDGE_ERROR_CODES.WS_CLOSED],
|
|
69
70
|
['cdp connection timeout', BRIDGE_ERROR_CODES.CDP_TIMEOUT],
|
|
@@ -862,6 +862,10 @@ function createCdpBroker({
|
|
|
862
862
|
}
|
|
863
863
|
},
|
|
864
864
|
onCdpEvent(deviceId, method, params) {
|
|
865
|
+
if (method === 'Runtime.executionContextsCleared') {
|
|
866
|
+
const target = knownTargets.get(deviceId);
|
|
867
|
+
if (target) knownTargets.set(deviceId, { ...target, generation: target.generation + 1 });
|
|
868
|
+
}
|
|
865
869
|
for (const socket of clients) {
|
|
866
870
|
if (subscriptions.get(socket)?.has(`${deviceId}\u0000${method}`)) {
|
|
867
871
|
writeMessage(socket, { type: 'event', deviceId, method, params });
|
|
@@ -1,12 +1,54 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
4
6
|
import { createRequire } from 'node:module';
|
|
5
7
|
import { resolveMobileToolPath } from '../../library/actions/mobile/platform/tool-paths.mjs';
|
|
6
8
|
|
|
7
9
|
const require = createRequire(import.meta.url);
|
|
8
10
|
const WebSocket = require('ws');
|
|
9
|
-
const { discoverTarget } = require('./bridge-runtime/lib/target-discovery.cjs');
|
|
11
|
+
const { discoverTarget, rankRuntimeCandidates } = require('./bridge-runtime/lib/target-discovery.cjs');
|
|
12
|
+
const { brokerSocketPath, createBrokerClient, deviceIdFromUrl } = require('./bridge-runtime/lib/cdp-broker.cjs');
|
|
13
|
+
const { createWSClient } = require('./bridge-runtime/lib/ws-client.cjs');
|
|
14
|
+
|
|
15
|
+
async function observeRuntimeReset(port, target) {
|
|
16
|
+
const socketPath = brokerSocketPath(
|
|
17
|
+
process.env.RECIPE_RUNTIME_DIR || path.join(process.cwd(), 'temp/recipe/runtime'),
|
|
18
|
+
port,
|
|
19
|
+
);
|
|
20
|
+
const client = fs.existsSync(socketPath)
|
|
21
|
+
? await createBrokerClient(socketPath, deviceIdFromUrl(target.wsUrl), 5000)
|
|
22
|
+
: await createWSClient(target.wsUrl, 5000);
|
|
23
|
+
let armed = false;
|
|
24
|
+
let cleared = false;
|
|
25
|
+
let recreated = false;
|
|
26
|
+
client.on('Runtime.executionContextsCleared', () => {
|
|
27
|
+
if (armed) cleared = true;
|
|
28
|
+
});
|
|
29
|
+
client.on('Runtime.executionContextCreated', () => {
|
|
30
|
+
if (armed && cleared) recreated = true;
|
|
31
|
+
});
|
|
32
|
+
try {
|
|
33
|
+
await client.send('Runtime.enable');
|
|
34
|
+
// Finish subscription/bootstrap before the reload request can generate events.
|
|
35
|
+
await client.send('Runtime.evaluate', { expression: 'true', returnByValue: true });
|
|
36
|
+
armed = true;
|
|
37
|
+
} catch (error) {
|
|
38
|
+
client.close();
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
get recreated() { return recreated; },
|
|
43
|
+
async responsive(timeoutMs) {
|
|
44
|
+
const result = await client.send('Runtime.evaluate', {
|
|
45
|
+
expression: 'true', returnByValue: true,
|
|
46
|
+
}, timeoutMs);
|
|
47
|
+
return result?.result?.value === true;
|
|
48
|
+
},
|
|
49
|
+
close() { client.close(); },
|
|
50
|
+
};
|
|
51
|
+
}
|
|
10
52
|
|
|
11
53
|
function parseArgs(argv) {
|
|
12
54
|
const args = {
|
|
@@ -46,30 +88,38 @@ async function reload(port) {
|
|
|
46
88
|
'The Android Hermes sampling profiler is active, so a runtime reload can abort the app. Use an explicit app.lifecycle restart.',
|
|
47
89
|
);
|
|
48
90
|
}
|
|
49
|
-
await
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
socket.on('open', () => {
|
|
56
|
-
socket.send(JSON.stringify({ method: 'reload', version: 2 }));
|
|
57
|
-
setTimeout(() => {
|
|
58
|
-
clearTimeout(timeout);
|
|
91
|
+
const observer = await observeRuntimeReset(numericPort, targetBefore);
|
|
92
|
+
let verification;
|
|
93
|
+
try {
|
|
94
|
+
await new Promise((resolve, reject) => {
|
|
95
|
+
const socket = new WebSocket(`ws://127.0.0.1:${numericPort}/message`);
|
|
96
|
+
const timeout = setTimeout(() => {
|
|
59
97
|
socket.close();
|
|
60
|
-
|
|
61
|
-
},
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
98
|
+
reject(new Error(`Metro reload timed out on port ${numericPort}`));
|
|
99
|
+
}, 5000);
|
|
100
|
+
socket.on('open', () => {
|
|
101
|
+
socket.send(JSON.stringify({ method: 'reload', version: 2 }));
|
|
102
|
+
setTimeout(() => {
|
|
103
|
+
clearTimeout(timeout);
|
|
104
|
+
socket.close();
|
|
105
|
+
resolve();
|
|
106
|
+
}, 50);
|
|
107
|
+
});
|
|
108
|
+
socket.on('error', () => {
|
|
109
|
+
clearTimeout(timeout);
|
|
110
|
+
reject(new Error(`Metro is not reachable on port ${numericPort}`));
|
|
111
|
+
});
|
|
66
112
|
});
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
113
|
+
verification = await waitForReload(
|
|
114
|
+
numericPort,
|
|
115
|
+
targetBefore,
|
|
116
|
+
processBefore,
|
|
117
|
+
observer,
|
|
118
|
+
);
|
|
119
|
+
} finally {
|
|
120
|
+
observer.close();
|
|
121
|
+
}
|
|
122
|
+
const { target: targetAfter, proof } = verification;
|
|
73
123
|
const processAfter = processBefore ? readAndroidPid() : null;
|
|
74
124
|
if (processBefore && processAfter !== processBefore) {
|
|
75
125
|
throw new Error(
|
|
@@ -84,6 +134,7 @@ async function reload(port) {
|
|
|
84
134
|
port: numericPort,
|
|
85
135
|
targetBefore: targetBefore.id,
|
|
86
136
|
targetAfter: targetAfter.id,
|
|
137
|
+
verification: proof,
|
|
87
138
|
...(processBefore
|
|
88
139
|
? {
|
|
89
140
|
processBefore,
|
|
@@ -94,20 +145,33 @@ async function reload(port) {
|
|
|
94
145
|
};
|
|
95
146
|
}
|
|
96
147
|
|
|
97
|
-
async function waitForReload(port, targetBefore, processBefore) {
|
|
148
|
+
async function waitForReload(port, targetBefore, processBefore, observer) {
|
|
98
149
|
const deadline = Date.now() + 30_000;
|
|
150
|
+
let lastError;
|
|
99
151
|
while (Date.now() < deadline) {
|
|
100
152
|
if (processBefore && readAndroidPid() !== processBefore) {
|
|
101
153
|
throw new Error(
|
|
102
154
|
`Android app process ${processBefore} exited during reload. Inspect the native crash before using an explicit app.lifecycle restart.`,
|
|
103
155
|
);
|
|
104
156
|
}
|
|
105
|
-
|
|
106
|
-
|
|
157
|
+
try {
|
|
158
|
+
const response = await fetch(`http://127.0.0.1:${port}/json/list`, {
|
|
159
|
+
signal: AbortSignal.timeout(Math.max(1, Math.min(1000, deadline - Date.now()))),
|
|
160
|
+
});
|
|
161
|
+
const targets = rankRuntimeCandidates(await response.json());
|
|
162
|
+
const target = targets.find((item) => item.deviceName === targetBefore.deviceName);
|
|
163
|
+
if (target && target.id !== targetBefore.id) return { target, proof: 'target-replaced' };
|
|
164
|
+
if (target && observer.recreated && await observer.responsive(Math.max(1, Math.min(1000, deadline - Date.now())))) {
|
|
165
|
+
return { target, proof: 'execution-context-recreated' };
|
|
166
|
+
}
|
|
167
|
+
} catch (error) {
|
|
168
|
+
// A reload temporarily removes the runtime; preserve the last failure if it never returns.
|
|
169
|
+
lastError = error;
|
|
170
|
+
}
|
|
107
171
|
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
108
172
|
}
|
|
109
173
|
throw new Error(
|
|
110
|
-
`React Native
|
|
174
|
+
`React Native reload was not verified on port ${port}: no target replacement or responsive new execution context${lastError ? ` (${lastError.message})` : ''}.\nNext: mm-harness status --json`,
|
|
111
175
|
);
|
|
112
176
|
}
|
|
113
177
|
|
|
@@ -155,7 +219,7 @@ const args = parseArgs(process.argv.slice(2));
|
|
|
155
219
|
reload(args.port)
|
|
156
220
|
.then((result) => {
|
|
157
221
|
if (args.json) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
158
|
-
else console.log(`Reload
|
|
222
|
+
else console.log(`Reload verified through Metro :${result.port} (${result.verification})`);
|
|
159
223
|
})
|
|
160
224
|
.catch((error) => {
|
|
161
225
|
const result = { ok: false, adapter: 'mobile', error: String(error?.message || error) };
|
|
@@ -60,6 +60,12 @@ if [ ! -f "$RUNNER_DIR/package.json" ]; then
|
|
|
60
60
|
exit 1
|
|
61
61
|
fi
|
|
62
62
|
|
|
63
|
+
if [ -L "$RUNNER_DIR/node_modules" ]; then
|
|
64
|
+
echo "mm-harness: cannot repair dependencies through a shared node_modules symlink." >&2
|
|
65
|
+
echo " Next: readlink '$RUNNER_DIR/node_modules' # inspect the shared target, then prepare a private dependency directory" >&2
|
|
66
|
+
exit 1
|
|
67
|
+
fi
|
|
68
|
+
|
|
63
69
|
if [ -n "$PROTOCOL_ROOT" ] && [ -f "$RUNNER_DIR/scripts/link-local-farmslot.mjs" ]; then
|
|
64
70
|
if [ -f "$PROTOCOL_ROOT/packages/recipe-harness/package.json" ]; then
|
|
65
71
|
FARMSLOT_ROOT="$(cd "$PROTOCOL_ROOT" && pwd -P)" \
|
|
@@ -356,7 +356,7 @@ async function cdpCheck(target, cdpPort, pageMode) {
|
|
|
356
356
|
try {
|
|
357
357
|
const { checkExtensionRuntimeHealth } = await import("./runtime.js");
|
|
358
358
|
const report = await checkExtensionRuntimeHealth(target, cdpPort, { pageMode });
|
|
359
|
-
return report.status === "PASS" ? { status: "pass" } : { status: "fail", findings: report.findings };
|
|
359
|
+
return report.status === "PASS" ? { status: "pass" } : { status: "fail", findings: report.findings, errorCode: report.errorCode };
|
|
360
360
|
} catch (error) {
|
|
361
361
|
return { status: "fail", findings: [error instanceof Error ? error.message : String(error)] };
|
|
362
362
|
}
|
|
@@ -441,6 +441,16 @@ async function decideExtensionReadiness(target, options = {}) {
|
|
|
441
441
|
reasons: ["Build is fresh and the extension runtime is healthy over CDP."],
|
|
442
442
|
actions: []
|
|
443
443
|
},
|
|
444
|
+
{
|
|
445
|
+
when: cdp.errorCode === "EVM_RPC_EXECUTION_FAILED",
|
|
446
|
+
decision: "blocked",
|
|
447
|
+
reasonCode: "rpc-execution-failed",
|
|
448
|
+
reasons: [
|
|
449
|
+
"Background RPC fetch failed before an HTTP response; inspect its JavaScript stack before relaunching.",
|
|
450
|
+
...cdp.findings?.slice(0, 3) ?? []
|
|
451
|
+
],
|
|
452
|
+
actions: []
|
|
453
|
+
},
|
|
444
454
|
{
|
|
445
455
|
when: cdp.status === "fail",
|
|
446
456
|
decision: "relaunch",
|
|
@@ -482,6 +492,13 @@ async function decideExtensionReadiness(target, options = {}) {
|
|
|
482
492
|
reasons: [`Release artifact ${releaseArtifact.expectedVersion} is intact and healthy over CDP.`],
|
|
483
493
|
actions: []
|
|
484
494
|
},
|
|
495
|
+
{
|
|
496
|
+
when: cdp.errorCode === "EVM_RPC_EXECUTION_FAILED",
|
|
497
|
+
decision: "blocked",
|
|
498
|
+
reasonCode: "rpc-execution-failed",
|
|
499
|
+
reasons: ["Background RPC fetch failed before an HTTP response; inspect its JavaScript stack before relaunching.", ...cdp.findings?.slice(0, 3) ?? []],
|
|
500
|
+
actions: []
|
|
501
|
+
},
|
|
485
502
|
{
|
|
486
503
|
when: cdp.status === "fail",
|
|
487
504
|
decision: "relaunch",
|
|
@@ -166,10 +166,11 @@ async function checkExtensionRuntimeHealth(projectRoot, cdpPort, options = {}) {
|
|
|
166
166
|
if (runtime.ethereumConnectionUnavailable === true) {
|
|
167
167
|
findings.push('Extension UI reports "Unable to connect to Ethereum".');
|
|
168
168
|
}
|
|
169
|
+
const rpcExecutionFailed = runtime.evmRpcProbeOk !== true && /Failed to execute 'fetch' on 'WorkerGlobalScope': Illegal invocation/u.test(String(runtime.evmRpcProbeError));
|
|
169
170
|
if (runtime.evmRpcProbeOk !== true) {
|
|
170
171
|
const probeError = String(runtime.evmRpcProbeError ?? "unknown error").replace(/[.\s]+$/u, "");
|
|
171
172
|
findings.push(
|
|
172
|
-
`EVM RPC readiness probe failed: ${probeError}. Infura configured \u2260 RPC reachable.`
|
|
173
|
+
rpcExecutionFailed ? `EVM_RPC_EXECUTION_FAILED: background RPC fetch threw before an HTTP response: ${probeError}.` : `EVM RPC readiness probe failed: ${probeError}. Infura configured \u2260 RPC reachable.`
|
|
173
174
|
);
|
|
174
175
|
}
|
|
175
176
|
if (runtime.hasSubmitRequest === true && runtime.backgroundProbeOk !== true) {
|
|
@@ -180,8 +181,8 @@ async function checkExtensionRuntimeHealth(projectRoot, cdpPort, options = {}) {
|
|
|
180
181
|
return {
|
|
181
182
|
status: findings.length === 0 ? "PASS" : "FAIL",
|
|
182
183
|
...evmRpcUnreachable ? {
|
|
183
|
-
errorCode: "EVM_RPC_UNREACHABLE",
|
|
184
|
-
userAction: evmRpcRecoveryAction(projectRoot)
|
|
184
|
+
errorCode: rpcExecutionFailed ? "EVM_RPC_EXECUTION_FAILED" : "EVM_RPC_UNREACHABLE",
|
|
185
|
+
userAction: rpcExecutionFailed ? `mm-harness logs --target ${shellQuote(projectRoot)} --full # inspect the background fetch stack; credential resync is not indicated by this error` : evmRpcRecoveryAction(projectRoot)
|
|
185
186
|
} : {},
|
|
186
187
|
warnings,
|
|
187
188
|
cdpPort,
|
|
@@ -497,8 +498,7 @@ function formatHealthFailure(report, projectRoot) {
|
|
|
497
498
|
].join("\n");
|
|
498
499
|
}
|
|
499
500
|
function compositorRecoveryAction(projectRoot) {
|
|
500
|
-
|
|
501
|
-
return process.platform === "darwin" ? `Unlock the macOS session, then rerun: ${recovery}` : `Restore an active display compositor, then rerun: ${recovery}`;
|
|
501
|
+
return `Make the MetaMask tab visible in an unlocked desktop session, then rerun: mm-harness verify --adapter extension --target ${shellQuote(projectRoot)}`;
|
|
502
502
|
}
|
|
503
503
|
function evmRpcRecoveryAction(projectRoot) {
|
|
504
504
|
return `Confirm ${shellQuote(path.join(projectRoot, ".metamaskrc"))} has a real INFURA_PROJECT_ID (not 00000000000), then rerun: mm-harness launch --verify --target ${shellQuote(projectRoot)}`;
|
|
@@ -16,6 +16,7 @@ function watcherStatus(buildLog) {
|
|
|
16
16
|
}
|
|
17
17
|
function extensionRuntimeNextAction(reasonCode, target) {
|
|
18
18
|
const resolved = shellQuote(path.resolve(target));
|
|
19
|
+
if (reasonCode === "rpc-execution-failed") return `mm-harness logs --target ${resolved} --full`;
|
|
19
20
|
return reasonCode === "deps-missing" || reasonCode === "deps-stale" ? `cd ${resolved} && yarn install --immutable` : `mm-harness launch --adapter extension --target ${resolved}`;
|
|
20
21
|
}
|
|
21
22
|
const extensionSurface = {
|
package/dist/app-lifecycle.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
3
|
import { promisify } from "node:util";
|
|
4
|
+
const MOBILE_LIFECYCLE_PREPARED = /* @__PURE__ */ Symbol("mobileLifecyclePrepared");
|
|
4
5
|
const MOBILE_LIFECYCLE_DEADLINE_FIELD = "_mobile_lifecycle_deadline_epoch_ms";
|
|
5
6
|
const lifecycleDeadline = new AsyncLocalStorage();
|
|
6
7
|
const execFileAsync = promisify(execFile);
|
|
@@ -64,7 +65,7 @@ function resolveMetaMaskMobileLifecycleTarget(node, rawContext) {
|
|
|
64
65
|
appId: scalar(node.package_id ?? node.packageName ?? node.app_id, "app.lifecycle.package_id") ?? context.env.ANDROID_PACKAGE_ID ?? process.env.ANDROID_PACKAGE_ID ?? "io.metamask",
|
|
65
66
|
launchUrl: opaqueRuntime ? void 0 : launchUrl,
|
|
66
67
|
metroPort: opaqueRuntime ? void 0 : port,
|
|
67
|
-
prelaunchCalls: opaqueRuntime ? [] : expoBundlePrewarmCalls(port, "android")
|
|
68
|
+
prelaunchCalls: opaqueRuntime || node[MOBILE_LIFECYCLE_PREPARED] ? [] : expoBundlePrewarmCalls(port, "android")
|
|
68
69
|
};
|
|
69
70
|
}
|
|
70
71
|
return {
|
|
@@ -123,6 +124,7 @@ function scalar(value, label) {
|
|
|
123
124
|
}
|
|
124
125
|
export {
|
|
125
126
|
MOBILE_LIFECYCLE_DEADLINE_FIELD,
|
|
127
|
+
MOBILE_LIFECYCLE_PREPARED,
|
|
126
128
|
bindMetaMaskMobileLifecycleDeadline,
|
|
127
129
|
createMetaMaskMobileLifecycleCommandRunner,
|
|
128
130
|
resolveMetaMaskMobileLifecycleTarget
|
package/dist/command-contract.js
CHANGED
|
@@ -258,6 +258,8 @@ const PUBLIC_COMMAND_CONTRACTS = {
|
|
|
258
258
|
options: options(HELP, JSON, TARGET, ADAPTER, DEVICE, MOBILE_PLATFORM, {
|
|
259
259
|
"--cdp-port": value(),
|
|
260
260
|
"--chrome-user-data-dir": value(),
|
|
261
|
+
"--reset-profile": bool(),
|
|
262
|
+
"--preserve-profile": bool(),
|
|
261
263
|
"--artifacts-dir": value(),
|
|
262
264
|
"--start-watch": bool(),
|
|
263
265
|
"--build-lavamoat": bool(),
|
package/dist/commands/call.js
CHANGED
|
@@ -377,14 +377,7 @@ async function handleCall(argv) {
|
|
|
377
377
|
persistRunEffects(result.summaryPath, result.artifactManifestPath, state);
|
|
378
378
|
if (violation !== null) {
|
|
379
379
|
const conciseFailure = violation.originalError ? conciseFailureForHuman(violation.originalError) : "";
|
|
380
|
-
const
|
|
381
|
-
describedAction,
|
|
382
|
-
adapter,
|
|
383
|
-
target,
|
|
384
|
-
process.env.MM_HARNESS_INVOKED_AS ?? process.env.MM_HARNESS_EXECUTABLE ?? "mm-harness",
|
|
385
|
-
args
|
|
386
|
-
) : void 0;
|
|
387
|
-
const taughtViolation = violation.code === "APP_LOGIC_FAILURE" && conciseFailure.includes(" requires ") && example ? { ...violation, userAction: example } : violation;
|
|
380
|
+
const taughtViolation = violation.code === "APP_LOGIC_FAILURE" && conciseFailure.includes(" requires ") && describedAction ? { ...violation, userAction: `mm-harness actions ${resolvedAction} --adapter ${adapter} --json` } : violation;
|
|
388
381
|
return emitHealViolation(json, "call", result, taughtViolation, state, adapter);
|
|
389
382
|
}
|
|
390
383
|
const callOutput = readCallOutput(result.tracePath);
|
|
@@ -109,8 +109,13 @@ function applyDeviceTargeting(command, adapter, options, opts) {
|
|
|
109
109
|
}
|
|
110
110
|
return { ok: true };
|
|
111
111
|
}
|
|
112
|
+
const discoveryErrors = [];
|
|
113
|
+
const discoveryFailure = () => {
|
|
114
|
+
const error = discoveryErrors.find((error2) => !platformPreference || error2.platform === platformPreference);
|
|
115
|
+
return error ? { ok: false, code: "DEVICE_DISCOVERY_FAILED", message: error.message, userAction: error.userAction } : void 0;
|
|
116
|
+
};
|
|
112
117
|
if (device !== void 0) {
|
|
113
|
-
const connected2 = listConnectedDevices();
|
|
118
|
+
const connected2 = listConnectedDevices(void 0, discoveryErrors);
|
|
114
119
|
const setResolvedDevice = (matched) => {
|
|
115
120
|
if (platformPreference && matched.platform !== platformPreference) {
|
|
116
121
|
return {
|
|
@@ -130,10 +135,12 @@ function applyDeviceTargeting(command, adapter, options, opts) {
|
|
|
130
135
|
if (androidById) {
|
|
131
136
|
return setResolvedDevice(androidById);
|
|
132
137
|
}
|
|
133
|
-
const iosById = connected2.find((d) => d.platform === "ios" && d.id === device) ?? (command === "launch" && platformPreference === "ios" ? findAvailableIosSimulatorById(device) : void 0);
|
|
138
|
+
const iosById = connected2.find((d) => d.platform === "ios" && d.id === device) ?? (command === "launch" && platformPreference === "ios" ? findAvailableIosSimulatorById(device, discoveryErrors) : void 0);
|
|
134
139
|
if (iosById) {
|
|
135
140
|
return setResolvedDevice(iosById);
|
|
136
141
|
}
|
|
142
|
+
const probeFailure2 = discoveryFailure();
|
|
143
|
+
if (probeFailure2) return probeFailure2;
|
|
137
144
|
const normalizedDevice = normalizeDeviceName(device);
|
|
138
145
|
const byName = connected2.filter((d) => {
|
|
139
146
|
const name = d.name ?? "";
|
|
@@ -161,7 +168,9 @@ ${formatConnectedDevices(connected2)}` : ` No devices are currently connected (
|
|
|
161
168
|
userAction: deviceRecovery(options, opts.rerun)
|
|
162
169
|
};
|
|
163
170
|
}
|
|
164
|
-
const connected = listConnectedDevices();
|
|
171
|
+
const connected = listConnectedDevices(void 0, discoveryErrors);
|
|
172
|
+
const probeFailure = discoveryFailure();
|
|
173
|
+
if (probeFailure) return opts.gate || command === "launch" ? probeFailure : { ok: true };
|
|
165
174
|
const targetable = connected.filter(isTargetable);
|
|
166
175
|
if (platformPreference) {
|
|
167
176
|
const selectedTargetable2 = targetable.filter(deviceSelected);
|
package/dist/commands/doctor.js
CHANGED
|
@@ -188,6 +188,7 @@ async function handleDoctor({ options }) {
|
|
|
188
188
|
const liveView = deviceView ? await mobileDeviceLiveView(target, deviceView) : null;
|
|
189
189
|
const devices = liveView?.devicesWithLive ?? deviceView?.devices ?? [];
|
|
190
190
|
const additionalReachableDevices = liveView?.additionalReachableDevices ?? [];
|
|
191
|
+
const discovery = deviceView?.deviceDiscoveryErrors.length ? { deviceDiscoveryErrors: deviceView.deviceDiscoveryErrors } : {};
|
|
191
192
|
if (expectLive) {
|
|
192
193
|
return emitExpectLive(
|
|
193
194
|
adapter,
|
|
@@ -199,15 +200,16 @@ async function handleDoctor({ options }) {
|
|
|
199
200
|
devices,
|
|
200
201
|
additionalReachableDevices,
|
|
201
202
|
json,
|
|
202
|
-
printReady
|
|
203
|
+
printReady,
|
|
204
|
+
discovery
|
|
203
205
|
);
|
|
204
206
|
}
|
|
205
207
|
const doctorUserAction = doctorResult.checks.find(
|
|
206
208
|
(check) => check.required && check.status === "fail" && check.userAction
|
|
207
209
|
)?.userAction ?? `mm-harness doctor --fix --adapter ${adapter} --target ${shellQuote(target)} --json`;
|
|
208
210
|
const doctorError = doctorResult.status === "fail" ? { code: "DOCTOR_CHECKS_FAILED", message: "one or more required doctor checks failed", userAction: doctorUserAction } : void 0;
|
|
209
|
-
const next = doctorError?.userAction ?? (runtime?.decision === "ready" ? void 0 : runtime?.nextAction);
|
|
210
|
-
if (json) console.log(JSON.stringify({ ...doctorResult, ready: runtime?.decision === "ready", runtime, orphanMetros, capture, devices, additionalReachableDevices, ...next ? { next } : {}, ...doctorError ? { error: doctorError } : {} }, null, 2));
|
|
211
|
+
const next = doctorError?.userAction ?? discovery.deviceDiscoveryErrors?.[0]?.userAction ?? (runtime?.decision === "ready" ? void 0 : runtime?.nextAction);
|
|
212
|
+
if (json) console.log(JSON.stringify({ ...doctorResult, ready: runtime?.decision === "ready", runtime, orphanMetros, capture, devices, additionalReachableDevices, ...discovery, ...next ? { next } : {}, ...doctorError ? { error: doctorError } : {} }, null, 2));
|
|
211
213
|
else {
|
|
212
214
|
const out = (style, text) => color(style, text, { stream: process.stdout });
|
|
213
215
|
const stateStyle = (value, good) => value === good ? "ok" : "warn";
|
|
@@ -298,7 +300,7 @@ function emitExpectLiveVerdict(adapter, target, runtime, verdict) {
|
|
|
298
300
|
for (const reason of runtime?.reasons ?? []) console.error(` ${out("dim", reason)}`);
|
|
299
301
|
console.error(` Next: ${runtime?.nextAction ?? getAdapterSurface(adapter).hints.runtimeProbeRecovery(target)}`);
|
|
300
302
|
}
|
|
301
|
-
function emitExpectLive(adapter, target, runtime, result, orphanMetros, capture, devices, additionalReachableDevices, json, printReady) {
|
|
303
|
+
function emitExpectLive(adapter, target, runtime, result, orphanMetros, capture, devices, additionalReachableDevices, json, printReady, discovery) {
|
|
302
304
|
const live = runtime?.decision === "ready";
|
|
303
305
|
if (printReady) {
|
|
304
306
|
if (live) {
|
|
@@ -316,7 +318,7 @@ function emitExpectLive(adapter, target, runtime, result, orphanMetros, capture,
|
|
|
316
318
|
if (json) {
|
|
317
319
|
const userAction = runtime?.nextAction ?? getAdapterSurface(adapter).hints.runtimeProbeRecovery(target);
|
|
318
320
|
const error = live ? void 0 : { code: "RUNTIME_NOT_LIVE", message: `${adapter} runtime is not live`, userAction };
|
|
319
|
-
console.log(JSON.stringify({ ...result, ready: live, runtime, orphanMetros, capture, devices, additionalReachableDevices, ...error ? { error } : {} }, null, 2));
|
|
321
|
+
console.log(JSON.stringify({ ...result, ready: live, runtime, orphanMetros, capture, devices, additionalReachableDevices, ...discovery, ...error ? { error } : {} }, null, 2));
|
|
320
322
|
return live ? EXIT.ok : EXIT.runtime;
|
|
321
323
|
}
|
|
322
324
|
const out = (style, text) => color(style, text, { stream: process.stdout });
|
|
@@ -328,7 +328,7 @@ async function applyExtensionFixture(target, fixturePath, canonicalFixture, json
|
|
|
328
328
|
process.stderr.write(`${colorHumanMessage(`\u2192 fixtures ${action} extension \u2014 resetting and seeding the slot-owned profile from the existing build\u2026`)}
|
|
329
329
|
`);
|
|
330
330
|
const liveScript = path.join(runnerDir, "adapters/extension/live.sh");
|
|
331
|
-
const args = ["--target", target, "--launch-existing-dist", "--launch-only"];
|
|
331
|
+
const args = ["--target", target, "--launch-existing-dist", "--launch-only", "--reset-profile"];
|
|
332
332
|
if (process.env.CDP_PORT) args.push("--cdp-port", process.env.CDP_PORT);
|
|
333
333
|
const result = await spawnScriptStreaming(liveScript, args, target, {
|
|
334
334
|
env: { RECIPE_WALLET_FIXTURE: fixturePath },
|
|
@@ -310,6 +310,16 @@ async function handleLaunchLocked(argv, stream) {
|
|
|
310
310
|
exitCode: EXIT.infra
|
|
311
311
|
});
|
|
312
312
|
}
|
|
313
|
+
if (adapter === "extension" && attempt.output.includes("EVM_RPC_EXECUTION_FAILED:")) {
|
|
314
|
+
return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
|
|
315
|
+
code: "EVM_RPC_EXECUTION_FAILED",
|
|
316
|
+
message: "Background RPC fetch failed during JavaScript execution; this does not establish an Infura authentication or connectivity failure.",
|
|
317
|
+
recoverable: false,
|
|
318
|
+
userAction: `mm-harness logs --target ${shellQuote(target)} --full`,
|
|
319
|
+
exitCode: EXIT.runtime,
|
|
320
|
+
originalError: attempt.output.trim() || void 0
|
|
321
|
+
});
|
|
322
|
+
}
|
|
313
323
|
if (adapter === "extension" && extensionEvmRpcUnreachable(attempt.output)) {
|
|
314
324
|
return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
|
|
315
325
|
code: "EVM_RPC_UNREACHABLE",
|
|
@@ -358,10 +358,11 @@ function renderHumanActionCatalog(actions, options) {
|
|
|
358
358
|
for (const entry of entries) {
|
|
359
359
|
const fieldNames = humanFieldNames(entry);
|
|
360
360
|
const fields = fieldNames.length ? ` ${out("dim", `fields=${fieldNames.join(",")}`)}` : "";
|
|
361
|
+
const cases = entry.result_cases?.length ? ` ${out("dim", `cases=${entry.result_cases.join(",")}`)}` : "";
|
|
361
362
|
const description = entry.description ? ` ${out("comment", `\u2014 ${entry.description}`)}` : "";
|
|
362
363
|
const source = ` ${out("dim", `[${entry.source}]`)}`;
|
|
363
364
|
const risk = entry.capabilities.length ? ` ${out("dim", `risk=${entry.capabilities.join(",")}`)}` : "";
|
|
364
|
-
lines.push(` ${out("cmd", options.displayName?.(entry) ?? entry.name)}${source}${fields}${risk}${description}`);
|
|
365
|
+
lines.push(` ${out("cmd", options.displayName?.(entry) ?? entry.name)}${source}${fields}${cases}${risk}${description}`);
|
|
365
366
|
}
|
|
366
367
|
}
|
|
367
368
|
return lines.join("\n");
|
|
@@ -519,6 +520,7 @@ function describeManifestAction(name, kind, metadata, source) {
|
|
|
519
520
|
fields: properties,
|
|
520
521
|
schema,
|
|
521
522
|
examples: record.examples,
|
|
523
|
+
...Array.isArray(record.result_cases) ? { result_cases: record.result_cases.filter((value) => typeof value === "string") } : {},
|
|
522
524
|
source: source?.name ?? (kind === "official" ? "official" : "metamask"),
|
|
523
525
|
sourceTier: source?.tier ?? (kind === "official" ? "official" : "canonical"),
|
|
524
526
|
sourceManifest: source?.manifestPath ?? "",
|
|
@@ -4,10 +4,12 @@ import { listConnectedDevices } from "../devices.js";
|
|
|
4
4
|
import { renderDeviceList, scopedDevices } from "./device-target.js";
|
|
5
5
|
import { probeMobileLiveState } from "./status-probe.js";
|
|
6
6
|
function mobileDeviceView(allDevices) {
|
|
7
|
-
const
|
|
7
|
+
const deviceDiscoveryErrors = [];
|
|
8
|
+
const allConnectedDevices = listConnectedDevices(void 0, deviceDiscoveryErrors);
|
|
8
9
|
return {
|
|
9
10
|
allConnectedDevices,
|
|
10
|
-
devices: scopedDevices(allConnectedDevices, allDevices)
|
|
11
|
+
devices: scopedDevices(allConnectedDevices, allDevices),
|
|
12
|
+
deviceDiscoveryErrors
|
|
11
13
|
};
|
|
12
14
|
}
|
|
13
15
|
async function mobileDeviceLiveView(target, view) {
|
|
@@ -39,7 +41,15 @@ function detectAdditionalReachableDevices(scoped, allConnected, liveMap) {
|
|
|
39
41
|
}).filter((device) => device !== null);
|
|
40
42
|
}
|
|
41
43
|
function renderMobileDeviceList(view, out) {
|
|
42
|
-
|
|
44
|
+
if (view.devices.length === 0 && view.deviceDiscoveryErrors.length > 0) {
|
|
45
|
+
console.log(`${out("label", "devices:")} ${out("warn", "discovery incomplete")}`);
|
|
46
|
+
} else {
|
|
47
|
+
renderDeviceList(view.devices, out);
|
|
48
|
+
}
|
|
49
|
+
for (const error of view.deviceDiscoveryErrors) {
|
|
50
|
+
console.log(`${out("warn", error.message)}
|
|
51
|
+
Next: ${error.userAction}`);
|
|
52
|
+
}
|
|
43
53
|
}
|
|
44
54
|
function renderAdditionalReachableDevices(devices, out) {
|
|
45
55
|
console.log(`${out("warn", "additional reachable target:")} ${devices.length} device(s) answered on this Metro but are not the active target for this command`);
|