@deeeed/metamask-harness 0.13.0 → 0.14.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 +17 -4
- package/adapters/core/cleanup.sh +0 -0
- package/adapters/core/inject.sh +0 -0
- package/adapters/extension/cleanup.mjs +0 -0
- package/adapters/extension/ensure-browser.sh +0 -0
- package/adapters/extension/inject.mjs +0 -0
- package/adapters/extension/launch-browser.cjs +0 -0
- package/adapters/extension/launch.sh +0 -0
- package/adapters/extension/live.sh +0 -0
- package/adapters/extension/readiness.mjs +0 -0
- package/adapters/extension/reattach.sh +0 -0
- package/adapters/extension/refresh-build.sh +0 -0
- package/adapters/extension/seed-fixture.sh +0 -0
- package/adapters/extension/sidepanel-toggle.sh +0 -0
- package/adapters/extension/snapshot-dist.sh +0 -0
- package/adapters/extension/start-watch.sh +0 -0
- package/adapters/extension/verify.sh +4 -4
- package/adapters/extension/wallet-fixture-state.cjs +0 -0
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +0 -0
- package/adapters/mobile/bridge-runtime/setup-wallet.sh +0 -0
- package/adapters/mobile/cleanup.sh +0 -0
- package/adapters/mobile/inject.sh +0 -0
- package/adapters/mobile/lib/metro-listener.sh +0 -0
- package/adapters/mobile/lib/tmux-viewer.sh +0 -0
- package/adapters/mobile/open-device.sh +0 -0
- package/adapters/mobile/prewarm-bundle.sh +0 -0
- package/adapters/mobile/start-metro.sh +15 -1
- package/adapters/mobile/stop-metro.sh +0 -0
- package/adapters/mobile/verify.sh +50 -13
- package/adapters/mobile/wait-for-bridge.sh +0 -0
- package/adapters/mobile/yarn-setup.sh +0 -0
- package/adapters/shared/activate-repo-node.sh +0 -0
- package/adapters/shared/activate-repo-ruby.sh +0 -0
- package/adapters/shared/cli-ux.sh +0 -0
- package/adapters/shared/ensure-runner-deps.sh +0 -0
- package/adapters/shared/harness-path.sh +0 -0
- package/adapters/shared/hash-helpers.sh +0 -0
- package/adapters/shared/json-field.sh +0 -0
- package/adapters/shared/log-tui.mjs +7 -3
- package/adapters/shared/open-log-window.sh +0 -0
- package/adapters/shared/reap-checkout-metros.sh +0 -0
- package/adapters/shared/resolve-farmslot-ports.mjs +0 -0
- package/adapters/shared/resolve-farmslot-ports.sh +0 -0
- package/adapters/shared/resolve-slot-ports.mjs +0 -0
- package/adapters/shared/resolve-slot-ports.sh +0 -0
- package/adapters/shared/sync-wallet-fixture.sh +0 -0
- package/adapters/shared/tmux-session.sh +0 -0
- package/dist/cli-commands.js +5 -4
- package/dist/commands/call.js +61 -5
- package/dist/commands/device-target.js +10 -3
- package/dist/commands/fixtures.js +2 -1
- package/dist/commands/run-engine.js +62 -3
- package/dist/commands/run.js +2 -0
- package/dist/commands/self-test.js +1 -1
- package/dist/commands/status.js +19 -4
- package/dist/mm-harness-cli.js +7 -5
- package/dist/recipe-files.js +14 -0
- package/docs/architecture.md +2 -2
- package/docs/live-adapter-contract.md +1 -1
- package/docs/recipe-libraries.md +21 -18
- package/library/recipes/{app-lifecycle-android-smoke.mobile.recipe.json → app/lifecycle.android-smoke.mobile.recipe.json} +1 -1
- package/library/recipes/{perps-performance.mobile.recipe.json → perps/performance.mobile.recipe.json} +1 -1
- package/library/recipes/perps/smoke.core.recipe.json +39 -0
- package/library/recipes/perps/smoke.extension.recipe.json +51 -0
- package/library/recipes/perps/smoke.mobile.recipe.json +51 -0
- package/library/recipes/{action-validation.extension.recipe.json → runner/action-validation.extension.recipe.json} +7 -7
- package/library/recipes/{action-validation.mobile.recipe.json → runner/action-validation.mobile.recipe.json} +7 -7
- package/package.json +2 -2
- package/scripts/completions.sh +0 -0
- package/scripts/install-completions.sh +0 -0
- /package/library/recipes/{perps-lifecycle.recipe.json → perps/lifecycle.recipe.json} +0 -0
- /package/library/recipes/{order-lifecycle.core.recipe.json → perps/order-lifecycle.core.recipe.json} +0 -0
- /package/library/recipes/{perps-performance-background-resume.mobile.recipe.json → perps/performance.background-resume.mobile.recipe.json} +0 -0
- /package/library/recipes/{perps-performance-cold-start.mobile.recipe.json → perps/performance.cold-start.mobile.recipe.json} +0 -0
- /package/library/recipes/{perps-performance-warm-start.mobile.recipe.json → perps/performance.warm-start.mobile.recipe.json} +0 -0
- /package/library/recipes/{read-markets.core.recipe.json → perps/read-markets.core.recipe.json} +0 -0
- /package/library/recipes/{trading-lifecycle.core.recipe.json → perps/trading-lifecycle.core.recipe.json} +0 -0
- /package/library/recipes/{smoke.extension.recipe.json → runner/smoke.extension.recipe.json} +0 -0
- /package/library/recipes/{smoke.mobile.recipe.json → runner/smoke.mobile.recipe.json} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.14.0 - 2026-07-08
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- `mm-harness call` accepts action fields as direct `key=value` shorthand in addition to repeated `--arg k=v`, so commands like `mm-harness call ui.navigate page=perps` now match the examples agents naturally write.
|
|
7
|
+
- Packaged recipes are grouped by domain under `library/recipes/<domain>/` and still resolve by dotted names (`perps.smoke`, `runner.smoke`) with legacy aliases for the former flat names.
|
|
8
|
+
- Packaged `perps.smoke` recipes for mobile, extension, and core provide an explicit non-mutating Perps domain smoke. The existing bare `smoke` alias remains a runner/transport smoke for compatibility.
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- `status` now hydrates slot runtime context before device enumeration, so a Farmslot checkout pinned to `IOS_SIMULATOR=mm-1` reports only the selected slot simulator instead of every booted simulator/device on the machine. Live status also updates `Next:` to `mm-harness logs` when the selected bridge is reachable.
|
|
12
|
+
- The tmux Metro window opened by `launch ios|android` now streams the Metro output it writes to `metro.log` instead of opening a blank server window.
|
|
13
|
+
- `log-tui watch` now matches success/failure patterns only against log content emitted after the watcher starts, so stale markers already present in long-lived Metro/webpack logs cannot false-pass readiness checks.
|
|
14
|
+
- `start-metro` now reaps any existing mobile console-forwarder attached to the same Metro port before spawning this slot's forwarder, even when the old process was launched from another checkout or package install path.
|
|
15
|
+
|
|
3
16
|
## 0.13.0 - 2026-07-08
|
|
4
17
|
|
|
5
18
|
### Added
|
|
@@ -29,7 +42,7 @@
|
|
|
29
42
|
### Added
|
|
30
43
|
- Mobile recipes can now use the standard outer `app.lifecycle` action for deterministic performance start states without rebuilding (Android background/foreground/terminate/restart, iOS simulator launch/terminate/restart through the shared Farmslot lifecycle adapter).
|
|
31
44
|
- `fixtures set --device <serial|udid|name>` now pins mobile fixture sync to the requested device using the same targeting rules as `run`, `call`, and `doctor`.
|
|
32
|
-
- New lifecycle-controlled measured flows in the packaged library: `app
|
|
45
|
+
- New lifecycle-controlled measured flows in the packaged library: `app.lifecycle.android-smoke`, `perps.performance.warm-start`, `perps.performance.background-resume`, `perps.performance.cold-start`; performance recipes now keep lifecycle/unlock in `setup[]`/`startState` so measured nodes carry only CUF timings.
|
|
33
46
|
|
|
34
47
|
### Changed
|
|
35
48
|
- `@farmslot/recipe-harness` dependency raised to `^0.4.0` (ships the `app.lifecycle` adapter — the `FARMSLOT_ROOT` local-source fallback is no longer needed at runtime).
|
|
@@ -46,9 +59,9 @@
|
|
|
46
59
|
## 0.10.0 - 2026-07-07
|
|
47
60
|
|
|
48
61
|
### Added
|
|
49
|
-
- **Canonical `perps
|
|
62
|
+
- **Canonical `perps.performance` measured flow** (`library/recipes/perps/performance.mobile.recipe.json`): unlock → open the Perps market list → read live state → open a market detail — one node per user-visible step with stable node names, so the per-node `duration`s in `trace.json` are the timings you monitor. Run pinned: `mm-harness run perps.performance --device <serial> --heal off`. Device-proven end-to-end on a physical Pixel.
|
|
50
63
|
- **`run <name>` resolves personal/team recipe libraries.** Previously only the packaged library was probed by name; custom recipes ran by path. Sources resolve in library-precedence order (personal > team shadow the packaged canonical — a same-named personal recipe wins), path-shaped args never probe libraries, and a miss teaches which sources were searched. Zero-flag default: `$FARMSLOT_HOME/recipe-library`.
|
|
51
|
-
- **Custom-library walkthrough** ("Your own measured flow" in `docs/recipe-libraries.md`): a peer engineer scaffolds a personal library, copies the canonical flow, retargets the nodes to their journey, and runs it by name. Every step is executed by the `perps
|
|
64
|
+
- **Custom-library walkthrough** ("Your own measured flow" in `docs/recipe-libraries.md`): a peer engineer scaffolds a personal library, copies the canonical flow, retargets the nodes to their journey, and runs it by name. Every step is executed by the `perps.performance-recipe` contract test, so the doc cannot drift from reality.
|
|
52
65
|
- **`call` declared on mobile and extension manifests** (was core-only) with self-discovery metadata — personal `flows/` segments are now usable from mobile/extension recipes. Honest limitation documented: flows carry no adapter dimension in the protocol yet, so a cross-adapter `call` fails at live-run rather than plan time.
|
|
53
66
|
|
|
54
67
|
### Fixed
|
|
@@ -71,7 +84,7 @@
|
|
|
71
84
|
## 0.8.0 - 2026-07-07
|
|
72
85
|
|
|
73
86
|
### Added
|
|
74
|
-
- **`run <name>` — the recipe positional resolves packaged-library recipe names.** An existing file path (absolute or cwd-relative) wins; otherwise the arg is a library recipe name, adapter-scoped first (`run smoke` on mobile → `smoke.mobile.recipe.json`), then adapterless (`run perps
|
|
87
|
+
- **`run <name>` — the recipe positional resolves packaged-library recipe names.** An existing file path (absolute or cwd-relative) wins; otherwise the arg is a library recipe name, adapter-scoped first (`run smoke` on mobile → `library/recipes/runner/smoke.mobile.recipe.json`), then adapterless (`run perps.lifecycle` → `library/recipes/perps/lifecycle.recipe.json`), then the exact library filename. A miss teaches `RECIPE_NOT_FOUND` naming the library recipes available for the adapter — no more `$LIB`-style path prefixes in docs or demo scripts. A directory shadowing a library name never wins, and path-shaped args (containing a separator) never fall through to the library.
|
|
75
88
|
- **`--device <udid|serial|name>` — first-class mobile device targeting on `run`, `call`, and `doctor`.** Resolves against connected devices (adb + booted simulators; exact id first, then exact name with an ambiguity teaching error) and sets the same env `launch --device` does. Mobile `run`/`call` **without** `--device` while more than one targetable device is connected (across android + ios; android state `device`, iOS `Booted`) fail fast listing the devices and the `--device` hint, so a recipe never lands on the wrong device silently. `run --plan`/`--list` are exempt; extension/core teach. Errors honor `--json` with structured envelopes.
|
|
76
89
|
- **`status` gains `devices[]`** (additive): `{platform, id, name, state, selected}` for connected android devices and booted iOS simulators on mobile checkouts — the `yarn a:status` successor; `selected` reflects the current env pinning.
|
|
77
90
|
|
package/adapters/core/cleanup.sh
CHANGED
|
File without changes
|
package/adapters/core/inject.sh
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -88,7 +88,7 @@ RUNNER_BIN="$HARNESS_DIR/runner/bin/mm-harness"
|
|
|
88
88
|
# --out (optional): a task-local recipes dir. Resolve it safely within the target
|
|
89
89
|
# (resolve_harness_out rejects absolute/.. escapes) and prefer its smoke recipe so
|
|
90
90
|
# `live --out <dir>` does not silently fall back to the installed default.
|
|
91
|
-
SMOKE_RECIPE="$HARNESS_DIR/runner/recipes/smoke.extension.recipe.json"
|
|
91
|
+
SMOKE_RECIPE="$HARNESS_DIR/runner/recipes/runner/smoke.extension.recipe.json"
|
|
92
92
|
if [ -n "$OUT" ]; then
|
|
93
93
|
# Fail fast on a task-local --out that does not contain the requested recipe.
|
|
94
94
|
# Silently falling back to the installed default would validate a different
|
|
@@ -98,11 +98,11 @@ if [ -n "$OUT" ]; then
|
|
|
98
98
|
echo "extension verify: --out '$OUT' did not resolve to a safe path under the target." >&2
|
|
99
99
|
exit 2
|
|
100
100
|
fi
|
|
101
|
-
if [ ! -f "$OUT_ABS/smoke.extension.recipe.json" ]; then
|
|
102
|
-
echo "extension verify: --out '$OUT' (resolved: $OUT_ABS) has no smoke.extension.recipe.json. Refusing to fall back to the installed default recipe; place the recipe under --out or omit --out." >&2
|
|
101
|
+
if [ ! -f "$OUT_ABS/runner/smoke.extension.recipe.json" ]; then
|
|
102
|
+
echo "extension verify: --out '$OUT' (resolved: $OUT_ABS) has no runner/smoke.extension.recipe.json. Refusing to fall back to the installed default recipe; place the recipe under --out or omit --out." >&2
|
|
103
103
|
exit 2
|
|
104
104
|
fi
|
|
105
|
-
SMOKE_RECIPE="$OUT_ABS/smoke.extension.recipe.json"
|
|
105
|
+
SMOKE_RECIPE="$OUT_ABS/runner/smoke.extension.recipe.json"
|
|
106
106
|
fi
|
|
107
107
|
ARTIFACTS="${ARTIFACTS:-$HARNESS_DIR/verify/$(date -u +%Y%m%dT%H%M%SZ)}"
|
|
108
108
|
mkdir -p "$ARTIFACTS/logs"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -86,6 +86,20 @@ start_console_forwarder() {
|
|
|
86
86
|
local fwd_pid_file="$LOG_DIR/console-forwarder.pid"
|
|
87
87
|
[ "${METAMASK_RECIPE_CONSOLE_FORWARD:-1}" != "0" ] || return 0
|
|
88
88
|
{ [ -f "$forwarder" ] && command -v node >/dev/null 2>&1; } || return 0
|
|
89
|
+
# Only one debugger can own a React Native page. Reap any forwarder already
|
|
90
|
+
# attached to this Metro port, even if it was launched from another checkout or
|
|
91
|
+
# package install path and therefore does not own this runtime dir's pid file.
|
|
92
|
+
while IFS= read -r line; do
|
|
93
|
+
local pid cmd
|
|
94
|
+
pid="${line%% *}"
|
|
95
|
+
cmd="${line#* }"
|
|
96
|
+
[ "$pid" != "$line" ] || continue
|
|
97
|
+
case "$cmd" in
|
|
98
|
+
*console-forwarder.cjs*" --port $PORT "*|*console-forwarder.cjs*" --port=$PORT "*)
|
|
99
|
+
kill "$pid" 2>/dev/null || true
|
|
100
|
+
;;
|
|
101
|
+
esac
|
|
102
|
+
done < <(ps -axo pid=,command= 2>/dev/null || true)
|
|
89
103
|
if [ -f "$fwd_pid_file" ]; then
|
|
90
104
|
local old_fwd
|
|
91
105
|
old_fwd="$(cat "$fwd_pid_file" 2>/dev/null || true)"
|
|
@@ -126,7 +140,7 @@ set +a
|
|
|
126
140
|
export EXPO_NO_TYPESCRIPT_SETUP=1
|
|
127
141
|
export WATCHER_PORT="$(printf '%q' "$PORT")" METRO_PORT="$(printf '%q' "$PORT")"
|
|
128
142
|
export METRO_MAX_WORKERS="$(printf '%q' "$METRO_WORKERS")"
|
|
129
|
-
|
|
143
|
+
yarn expo start --port "$(printf '%q' "$PORT")"$clear_flag 2>&1 | tee -a "$(printf '%q' "$LOG_FILE")"
|
|
130
144
|
EOF
|
|
131
145
|
chmod +x "$runner"
|
|
132
146
|
}
|
|
File without changes
|
|
@@ -216,6 +216,34 @@ process.stdout.write(value);
|
|
|
216
216
|
NODE
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
+
runtime_context_value() {
|
|
220
|
+
TARGET_FOR_RUNTIME_CONTEXT="$TARGET" RUNTIME_DIR_FOR_RUNTIME_CONTEXT="$RUNTIME_DIR" RUNTIME_CONTEXT_NAME="$1" node <<'NODE'
|
|
221
|
+
const fs = require('fs');
|
|
222
|
+
const path = require('path');
|
|
223
|
+
const target = process.env.TARGET_FOR_RUNTIME_CONTEXT;
|
|
224
|
+
const runtimeDir = process.env.RUNTIME_DIR_FOR_RUNTIME_CONTEXT;
|
|
225
|
+
const names = process.env.RUNTIME_CONTEXT_NAME.split(',');
|
|
226
|
+
const file = path.join(target, runtimeDir, 'agentic-runtime.json');
|
|
227
|
+
let data = {};
|
|
228
|
+
try { data = JSON.parse(fs.readFileSync(file, 'utf8')); } catch {}
|
|
229
|
+
for (const name of names) {
|
|
230
|
+
const value = data && data[name];
|
|
231
|
+
if (value !== undefined && value !== null && value !== '') {
|
|
232
|
+
process.stdout.write(String(value));
|
|
233
|
+
process.exit(0);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
NODE
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
mobile_device_target() {
|
|
240
|
+
if [ "$PLATFORM" = "android" ]; then
|
|
241
|
+
printf '%s' "${ADB_SERIAL:-${ANDROID_SERIAL:-${ANDROID_DEVICE:-$(runtime_context_value adbSerial,androidDevice,device)}}}"
|
|
242
|
+
return
|
|
243
|
+
fi
|
|
244
|
+
printf '%s' "${SIM_UDID:-${IOS_SIMULATOR:-$(runtime_context_value simulator,iosSimulator,device)}}"
|
|
245
|
+
}
|
|
246
|
+
|
|
219
247
|
check_file() {
|
|
220
248
|
local rel="$1"
|
|
221
249
|
if [ -e "$TARGET/$rel" ]; then
|
|
@@ -331,6 +359,7 @@ run_with_timeout() {
|
|
|
331
359
|
|
|
332
360
|
live_status_ok() {
|
|
333
361
|
local log_path="$1"
|
|
362
|
+
local device_target="$2"
|
|
334
363
|
cat > "$ARTIFACTS/mobile-status-smoke.recipe.json" <<'JSON'
|
|
335
364
|
{
|
|
336
365
|
"schema_version": 1,
|
|
@@ -349,12 +378,15 @@ live_status_ok() {
|
|
|
349
378
|
JSON
|
|
350
379
|
(
|
|
351
380
|
cd "$TARGET"
|
|
352
|
-
|
|
381
|
+
device_args=()
|
|
382
|
+
[ -n "$device_target" ] && device_args=(--device "$device_target")
|
|
383
|
+
"$RUNNER_BIN" run "$ARTIFACTS/mobile-status-smoke.recipe.json" --adapter mobile --project-root "$TARGET" "${device_args[@]}" --artifacts-dir "$ARTIFACTS/runner-status-smoke" --json
|
|
353
384
|
) > "$log_path" 2>&1
|
|
354
385
|
}
|
|
355
386
|
|
|
356
387
|
ensure_live_runtime() {
|
|
357
|
-
|
|
388
|
+
local device_target="$1"
|
|
389
|
+
if live_status_ok "$ARTIFACTS/logs/app-status-precheck.log" "$device_target"; then
|
|
358
390
|
return 0
|
|
359
391
|
fi
|
|
360
392
|
if [ "$AUTO_START" = true ]; then
|
|
@@ -366,7 +398,17 @@ EOF
|
|
|
366
398
|
}
|
|
367
399
|
|
|
368
400
|
if [ "$STATIC_ONLY" = false ]; then
|
|
369
|
-
|
|
401
|
+
device_target="$(mobile_device_target)"
|
|
402
|
+
if [ -z "$device_target" ]; then
|
|
403
|
+
if [ "$PLATFORM" = "android" ]; then
|
|
404
|
+
device_target="$(jsenv_value ADB_SERIAL)"
|
|
405
|
+
[ -n "$device_target" ] || device_target="$(jsenv_value ANDROID_DEVICE)"
|
|
406
|
+
else
|
|
407
|
+
device_target="$(jsenv_value IOS_SIMULATOR)"
|
|
408
|
+
fi
|
|
409
|
+
fi
|
|
410
|
+
|
|
411
|
+
if ensure_live_runtime "$device_target"; then
|
|
370
412
|
checks+=('{"name":"mobile runtime controllable precheck","status":"pass"}')
|
|
371
413
|
else
|
|
372
414
|
checks+=('{"name":"mobile runtime controllable precheck","status":"fail","detail":"see logs/app-status-precheck.log"}')
|
|
@@ -394,20 +436,13 @@ if [ "$STATIC_ONLY" = false ]; then
|
|
|
394
436
|
{
|
|
395
437
|
"schema_version": 1,
|
|
396
438
|
"title": "Mobile v1 runner live bridge smoke",
|
|
397
|
-
"description": "Verifies the installed MetaMask runner can
|
|
439
|
+
"description": "Verifies the installed MetaMask runner can read the React Native debug bridge without mutating wallet state.",
|
|
398
440
|
"validate": {
|
|
399
441
|
"workflow": {
|
|
400
442
|
"entry": "status",
|
|
401
443
|
"nodes": {
|
|
402
444
|
"status": { "action": "app.status", "intent": "Read Mobile app status through the v1 runner", "next": "cdp-probe" },
|
|
403
|
-
"cdp-probe": { "action": "cdp.target", "intent": "Verify the React Native debug bridge target is reachable", "required": true, "timeout_ms": 15000, "next": "
|
|
404
|
-
"wallet-setup": { "action": "metamask.wallet.setup", "intent": "Prepare the wallet fixture for the live bridge smoke", "timeout_ms": 45000, "next": "wallet-unlock" },
|
|
405
|
-
"wallet-unlock": { "action": "metamask.wallet.ensure_unlocked", "intent": "Unlock the wallet through the manifest-declared action", "timeout_ms": 45000, "next": "wallet-read" },
|
|
406
|
-
"wallet-read": { "action": "metamask.wallet.read_state", "intent": "Read wallet state before navigating to the wallet view", "timeout_ms": 45000, "next": "navigate-wallet" },
|
|
407
|
-
"navigate-wallet": { "action": "ui.navigate", "intent": "Open the wallet view through the UI navigation action", "route": "WalletView", "timeout_ms": 45000, "next": "wait-wallet" },
|
|
408
|
-
"wait-wallet": { "action": "ui.wait_for", "intent": "Wait until the wallet screen is present", "test_id": "wallet-screen", "expected": "present", "timeout_ms": 45000, "next": "hud-smoke" },
|
|
409
|
-
"hud-smoke": { "action": "app.hud", "status": "running", "intent": "Show the Mobile live bridge smoke HUD", "progress": { "current": 1, "total": 1 }, "timeout_ms": 45000, "next": "screenshot" },
|
|
410
|
-
"screenshot": { "action": "ui.screenshot", "intent": "Capture proof that the wallet screen is visible", "path": "screenshots/mobile-v1-live-smoke.png", "timeout_ms": 45000, "next": "done" },
|
|
445
|
+
"cdp-probe": { "action": "cdp.target", "intent": "Verify the React Native debug bridge target is reachable", "required": true, "timeout_ms": 15000, "next": "done" },
|
|
411
446
|
"done": { "action": "end", "status": "pass" }
|
|
412
447
|
}
|
|
413
448
|
}
|
|
@@ -417,13 +452,15 @@ JSON
|
|
|
417
452
|
|
|
418
453
|
ios_simulator_resolved="$(jsenv_value IOS_SIMULATOR)"
|
|
419
454
|
adb_serial_resolved="$(jsenv_value ADB_SERIAL)"
|
|
455
|
+
device_args=()
|
|
456
|
+
[ -n "$device_target" ] && device_args=(--device "$device_target")
|
|
420
457
|
if (
|
|
421
458
|
cd "$TARGET"
|
|
422
459
|
METAMASK_RECIPE_AUTO_HUD=0 \
|
|
423
460
|
IOS_SIMULATOR="${IOS_SIMULATOR:-$ios_simulator_resolved}" \
|
|
424
461
|
ADB_SERIAL="${ADB_SERIAL:-$adb_serial_resolved}" \
|
|
425
462
|
ANDROID_SERIAL="${ANDROID_SERIAL:-$adb_serial_resolved}" \
|
|
426
|
-
"$RUNNER_BIN" run "$ARTIFACTS/mobile-v1-live-smoke.recipe.json" --adapter mobile --project-root "$TARGET" --artifacts-dir "$ARTIFACTS/runner-live-smoke" --json
|
|
463
|
+
"$RUNNER_BIN" run "$ARTIFACTS/mobile-v1-live-smoke.recipe.json" --adapter mobile --project-root "$TARGET" "${device_args[@]}" --artifacts-dir "$ARTIFACTS/runner-live-smoke" --json
|
|
427
464
|
) > "$ARTIFACTS/logs/runner-live-smoke.log" 2>&1; then
|
|
428
465
|
checks+=("{\"name\":\"runner v1 live bridge smoke\",\"status\":\"pass\"}")
|
|
429
466
|
else
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -222,13 +222,17 @@ async function watchLog(options) {
|
|
|
222
222
|
const chunk = readNewContent(logPath, offset);
|
|
223
223
|
offset = chunk.offset;
|
|
224
224
|
const allText = fs.existsSync(logPath) ? fs.readFileSync(logPath, 'utf8') : '';
|
|
225
|
+
// Match readiness only against bytes read after this watch started. This
|
|
226
|
+
// assumes line-oriented log writes; a marker split across partial writes
|
|
227
|
+
// waits for a later complete marker instead of stale-passing.
|
|
228
|
+
const matchText = chunk.text;
|
|
225
229
|
const events = extractEvents(allText, eventsLimit);
|
|
226
230
|
|
|
227
|
-
if (matchesPattern(
|
|
231
|
+
if (matchesPattern(matchText, 'Module build failed|^ERROR in |compiled with [1-9][0-9]* error|FAIL runtime-launch|Extension harness launch fail')) {
|
|
228
232
|
renderCompact({ label, logPath, events, quiet: mode === 'quiet' });
|
|
229
233
|
return 1;
|
|
230
234
|
}
|
|
231
|
-
if (matchesPattern(
|
|
235
|
+
if (matchesPattern(matchText, successPattern)) {
|
|
232
236
|
renderCompact({ label, logPath, events, quiet: mode === 'quiet' });
|
|
233
237
|
if (mode !== 'quiet') {
|
|
234
238
|
const elapsed = Math.round((Date.now() - started) / 1000);
|
|
@@ -357,4 +361,4 @@ async function main() {
|
|
|
357
361
|
main().catch((error) => {
|
|
358
362
|
console.error(error instanceof Error ? error.message : String(error));
|
|
359
363
|
process.exit(1);
|
|
360
|
-
});
|
|
364
|
+
});
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/dist/cli-commands.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { listRecipeFiles } from "./recipe-files.js";
|
|
4
5
|
const SPEC = {
|
|
5
6
|
shim: [
|
|
6
7
|
{ name: "sync", desc: "Copy fixture, write slot context, install harness", flags: ["--dry-run", "--no-install"] },
|
|
@@ -16,7 +17,7 @@ const SPEC = {
|
|
|
16
17
|
{ name: "fixtures", desc: "Manage the canonical wallet fixture (sync/set/generate)", args: ["sync", "set", "generate"], flags: ["--fixture", "--out", "--adapter", "--target", "--device", "--json"] },
|
|
17
18
|
{ name: "actions", desc: "List runnable recipe actions", flags: ["--json"] },
|
|
18
19
|
{ name: "doctor", desc: "Check harness/orchestration health", flags: ["--json", "--target", "--adapter", "--runtime-dir", "--expect-live", "--cdp-port", "--device"] },
|
|
19
|
-
{ name: "run", desc: "Execute a proof recipe (path or library name, e.g. run smoke)", args: ["recipe.json|name"], flags: ["--list", "--device"] },
|
|
20
|
+
{ name: "run", desc: "Execute a proof recipe (path or library name, e.g. run perps.smoke)", args: ["recipe.json|name"], flags: ["--list", "--device"] },
|
|
20
21
|
{ name: "recipe-quality", desc: "Build the recipe-quality artifact from compact JSON", args: ["build"], flags: ["--input", "--output", "--json"] },
|
|
21
22
|
{ name: "interactive", aliases: ["menu"], desc: "Interactive command menu" },
|
|
22
23
|
{ name: "prepare", desc: "Install harness (+ optional validate)", flags: ["--target", "--runtime-dir", "--json"] },
|
|
@@ -124,9 +125,9 @@ function listRunRecipes(adapter, root, cwd = process.cwd()) {
|
|
|
124
125
|
for (const resolveDir of RUN_RECIPE_DIRS) {
|
|
125
126
|
const dir = resolveDir(adapter, root);
|
|
126
127
|
if (!fs.existsSync(dir)) continue;
|
|
127
|
-
for (const
|
|
128
|
-
if (
|
|
129
|
-
byBase.set(
|
|
128
|
+
for (const rel of listRecipeFiles(dir)) {
|
|
129
|
+
if (byBase.has(rel)) continue;
|
|
130
|
+
byBase.set(rel, path.join(dir, rel));
|
|
130
131
|
}
|
|
131
132
|
}
|
|
132
133
|
return [...byBase.values()].map((abs) => path.relative(cwd, abs)).sort();
|
package/dist/commands/call.js
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
runtimeOptionsFromCli,
|
|
17
17
|
usageError
|
|
18
18
|
} from "./parse-args.js";
|
|
19
|
+
import { getAdapterSurface } from "../adapters/surface.js";
|
|
19
20
|
import { handleListExecutables } from "./list-executables.js";
|
|
20
21
|
import { applyDeviceTargeting } from "./device-target.js";
|
|
21
22
|
import {
|
|
@@ -33,7 +34,7 @@ async function handleCall(argv) {
|
|
|
33
34
|
return handleListExecutables("call", options2);
|
|
34
35
|
}
|
|
35
36
|
if (argv.length > 0 && argv[0].startsWith("--")) {
|
|
36
|
-
const message = "call requires <action> first: mm-harness call <action> [--arg k=v ...] [flags]";
|
|
37
|
+
const message = "call requires <action> first: mm-harness call <action> [key=value ...] [--arg k=v ...] [flags]";
|
|
37
38
|
console.error(message);
|
|
38
39
|
return EXIT.usage;
|
|
39
40
|
}
|
|
@@ -60,6 +61,7 @@ async function handleCall(argv) {
|
|
|
60
61
|
return EXIT.usage;
|
|
61
62
|
}
|
|
62
63
|
const { adapter, target } = resolveAdapter(options);
|
|
64
|
+
getAdapterSurface(adapter).resolveSlotPorts(target);
|
|
63
65
|
const dtResult = applyDeviceTargeting("call", adapter, options, { gate: true, rerun: "" });
|
|
64
66
|
if ("code" in dtResult) {
|
|
65
67
|
if (json) console.log(JSON.stringify({ schemaVersion: 1, command: "call", adapter, error: { code: dtResult.code, message: dtResult.message } }, null, 2));
|
|
@@ -127,6 +129,7 @@ async function handleCall(argv) {
|
|
|
127
129
|
state
|
|
128
130
|
);
|
|
129
131
|
if (violation !== null) return emitHealViolation(json, "call", result, violation, state, adapter);
|
|
132
|
+
const callOutput = readCallOutput(result.tracePath);
|
|
130
133
|
if (json) {
|
|
131
134
|
console.log(
|
|
132
135
|
JSON.stringify(
|
|
@@ -141,6 +144,7 @@ async function handleCall(argv) {
|
|
|
141
144
|
summaryPath: result.summaryPath,
|
|
142
145
|
tracePath: result.tracePath,
|
|
143
146
|
artifactManifestPath: result.artifactManifestPath,
|
|
147
|
+
...callOutput !== void 0 ? { output: callOutput } : {},
|
|
144
148
|
recovered: state.recovered,
|
|
145
149
|
mutations: state.mutations,
|
|
146
150
|
exitCode: result.status === "pass" ? EXIT.ok : EXIT.runtime
|
|
@@ -150,11 +154,29 @@ async function handleCall(argv) {
|
|
|
150
154
|
)
|
|
151
155
|
);
|
|
152
156
|
} else {
|
|
153
|
-
|
|
157
|
+
const rendered = callOutput !== void 0 ? `
|
|
158
|
+
Result:
|
|
159
|
+
${formatCallOutput(callOutput)}` : "";
|
|
160
|
+
console.log(`call ${resolvedAction}: ${result.status}${rendered}
|
|
154
161
|
Artifacts: ${result.artifactManifestPath}`);
|
|
155
162
|
}
|
|
156
163
|
return result.status === "pass" ? EXIT.ok : EXIT.runtime;
|
|
157
164
|
}
|
|
165
|
+
function readCallOutput(tracePath) {
|
|
166
|
+
try {
|
|
167
|
+
const trace = JSON.parse(fs.readFileSync(tracePath, "utf8"));
|
|
168
|
+
const entries = Array.isArray(trace) ? trace : isRecord(trace) && Array.isArray(trace.entries) ? trace.entries : [];
|
|
169
|
+
const entry = entries.find((item) => isRecord(item) && item.nodeId === "call");
|
|
170
|
+
return isRecord(entry) && Object.prototype.hasOwnProperty.call(entry, "output") ? entry.output : void 0;
|
|
171
|
+
} catch {
|
|
172
|
+
return void 0;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
function formatCallOutput(value) {
|
|
176
|
+
if (typeof value === "string") return value;
|
|
177
|
+
if (typeof value === "number" || typeof value === "boolean" || value === null) return String(value);
|
|
178
|
+
return JSON.stringify(value, null, 2);
|
|
179
|
+
}
|
|
158
180
|
async function handleCallHelp(argv, genericHelp) {
|
|
159
181
|
const { action: shortName, rest } = parseCallArgs(argv.filter((arg) => arg !== "--help" && arg !== "-h"));
|
|
160
182
|
const { options } = parseArgs(rest, "call");
|
|
@@ -194,13 +216,13 @@ function renderCallActionHelp(entry) {
|
|
|
194
216
|
const required = new Set(
|
|
195
217
|
Array.isArray(schema.required) ? schema.required.filter((r) => typeof r === "string") : []
|
|
196
218
|
);
|
|
197
|
-
const lines = [`mm-harness call ${entry.name} [--arg k=v ...] [flags]`, ""];
|
|
219
|
+
const lines = [`mm-harness call ${entry.name} [key=value ...] [--arg k=v ...] [flags]`, ""];
|
|
198
220
|
if (entry.description) lines.push(` ${entry.description}`, "");
|
|
199
221
|
const names = Object.keys(properties).sort();
|
|
200
222
|
if (names.length === 0) {
|
|
201
223
|
lines.push(" Fields: (none)");
|
|
202
224
|
} else {
|
|
203
|
-
lines.push(" Fields (pass
|
|
225
|
+
lines.push(" Fields (pass as <name>=<value> or --arg <name>=<value>):");
|
|
204
226
|
const width = Math.max(...names.map((name) => name.length));
|
|
205
227
|
for (const name of names) {
|
|
206
228
|
const prop = isRecord(properties[name]) ? properties[name] : {};
|
|
@@ -226,7 +248,7 @@ function renderCallExamples(short, examples) {
|
|
|
226
248
|
for (const example of examples.slice(0, 2)) {
|
|
227
249
|
const node = isRecord(example) && isRecord(example.node) ? example.node : void 0;
|
|
228
250
|
if (!node) continue;
|
|
229
|
-
const tokens = Object.entries(node).filter(([key]) => key !== "action" && key !== "intent").map(([key, value]) =>
|
|
251
|
+
const tokens = Object.entries(node).filter(([key]) => key !== "action" && key !== "intent").map(([key, value]) => `${key}=${typeof value === "string" ? value : JSON.stringify(value)}`);
|
|
230
252
|
out.push(`mm-harness call ${short} ${tokens.join(" ")}`.trim());
|
|
231
253
|
}
|
|
232
254
|
return out;
|
|
@@ -249,10 +271,44 @@ function parseCallArgs(argv) {
|
|
|
249
271
|
action = arg;
|
|
250
272
|
continue;
|
|
251
273
|
}
|
|
274
|
+
if (action !== void 0 && !isForwardedOptionValue(argv, i) && isArgPair(arg)) {
|
|
275
|
+
const eq = arg.indexOf("=");
|
|
276
|
+
args[arg.slice(0, eq)] = arg.slice(eq + 1);
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
252
279
|
rest.push(arg);
|
|
253
280
|
}
|
|
254
281
|
return { action, args, rest };
|
|
255
282
|
}
|
|
283
|
+
function isArgPair(value) {
|
|
284
|
+
const eq = value.indexOf("=");
|
|
285
|
+
return eq > 0;
|
|
286
|
+
}
|
|
287
|
+
function isForwardedOptionValue(argv, index) {
|
|
288
|
+
if (index === 0) return false;
|
|
289
|
+
const previous = argv[index - 1];
|
|
290
|
+
if (!previous.startsWith("--") || previous.includes("=")) return false;
|
|
291
|
+
return VALUE_TAKING_CALL_FLAGS.has(normalizeFlagName(previous));
|
|
292
|
+
}
|
|
293
|
+
const VALUE_TAKING_CALL_FLAGS = /* @__PURE__ */ new Set([
|
|
294
|
+
"action-manifest",
|
|
295
|
+
"adapter",
|
|
296
|
+
"artifacts-dir",
|
|
297
|
+
"cdp-port",
|
|
298
|
+
"device",
|
|
299
|
+
"heal",
|
|
300
|
+
"library",
|
|
301
|
+
"metro-port",
|
|
302
|
+
"platform",
|
|
303
|
+
"project-root",
|
|
304
|
+
"slot",
|
|
305
|
+
"target",
|
|
306
|
+
"validation-runtime-dir",
|
|
307
|
+
"watcher-port"
|
|
308
|
+
]);
|
|
309
|
+
function normalizeFlagName(flag) {
|
|
310
|
+
return flag.replace(/^--/u, "");
|
|
311
|
+
}
|
|
256
312
|
function resolveActionName(shortName, names) {
|
|
257
313
|
if (names.includes(shortName)) return { status: "ok", resolved: shortName, candidates: [shortName] };
|
|
258
314
|
const finalSegment = (name) => name.split(".").pop() ?? name;
|
|
@@ -111,9 +111,16 @@ function applyDeviceTargeting(command, adapter, options, opts) {
|
|
|
111
111
|
${formatConnectedDevices(connected2)}` : ` No devices are currently connected (adb devices / booted iOS simulators).`)
|
|
112
112
|
};
|
|
113
113
|
}
|
|
114
|
-
if (!opts.gate) return { ok: true };
|
|
115
114
|
const connected = listConnectedDevices();
|
|
116
115
|
const targetable = connected.filter(isTargetable);
|
|
116
|
+
const selectedTargetable = targetable.filter(deviceSelected);
|
|
117
|
+
if (selectedTargetable.length === 1) {
|
|
118
|
+
const selected = selectedTargetable[0];
|
|
119
|
+
if (selected.platform === "android") setAndroidDeviceEnv(selected.id, selected.name);
|
|
120
|
+
else setIosDeviceEnv(selected.id, selected.name);
|
|
121
|
+
return { ok: true };
|
|
122
|
+
}
|
|
123
|
+
if (!opts.gate) return { ok: true };
|
|
117
124
|
if (targetable.length > 1) {
|
|
118
125
|
return {
|
|
119
126
|
ok: false,
|
|
@@ -121,8 +128,8 @@ ${formatConnectedDevices(connected2)}` : ` No devices are currently connected (
|
|
|
121
128
|
message: `${targetable.length} mobile devices available \u2014 ${command} needs exactly one target.
|
|
122
129
|
Connected devices:
|
|
123
130
|
${formatConnectedDevices(connected)}
|
|
124
|
-
Add --device <id> to disambiguate, e.g.:${targetable.map((d) => `
|
|
125
|
-
--device ${d.id}`).join("")}`
|
|
131
|
+
` + (selectedTargetable.length > 1 ? ` The current slot context selects more than one target; fix the slot device pins or add --device <id>.` : ` Add --device <id> to disambiguate, e.g.:${targetable.map((d) => `
|
|
132
|
+
--device ${d.id}`).join("")}`)
|
|
126
133
|
};
|
|
127
134
|
}
|
|
128
135
|
return { ok: true };
|
|
@@ -39,13 +39,14 @@ async function handleFixtures(argv, deps) {
|
|
|
39
39
|
if (!adapter) {
|
|
40
40
|
return usageOut(json, "fixtures", `could not detect the MetaMask repo type for ${target}`, ADAPTER_DETECT_NEXT);
|
|
41
41
|
}
|
|
42
|
+
const surface = getAdapterSurface(adapter);
|
|
43
|
+
surface.resolveSlotPorts(target);
|
|
42
44
|
const dtResult = applyDeviceTargeting("fixtures", adapter, options, { gate: false, rerun: "" });
|
|
43
45
|
if ("code" in dtResult) {
|
|
44
46
|
return usageOut(json, "fixtures", dtResult.message, "mm-harness fixtures set --adapter mobile --device <adb-serial|simulator-udid>");
|
|
45
47
|
}
|
|
46
48
|
if (sub === "generate") return fixturesGenerate(adapter, target, options, json);
|
|
47
49
|
if (sub === "finalize") return fixturesFinalize(adapter, target, options, json);
|
|
48
|
-
const surface = getAdapterSurface(adapter);
|
|
49
50
|
if (surface.headless) return usageOut(json, "fixtures", "core is headless; it has no wallet fixture.", surface.hints.launch);
|
|
50
51
|
const canonicalFixture = walletFixturePath(target);
|
|
51
52
|
const retryHint = `${surface.hints.relaunch} # relaunch, then retry: mm-harness fixtures set`;
|