@deeeed/metamask-harness 0.17.1 → 0.17.3

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.
Files changed (79) hide show
  1. package/CHANGELOG.md +36 -10
  2. package/README.md +12 -6
  3. package/adapters/core/cleanup.sh +0 -0
  4. package/adapters/core/inject.sh +0 -0
  5. package/adapters/extension/cleanup.mjs +0 -0
  6. package/adapters/extension/ensure-browser.sh +0 -0
  7. package/adapters/extension/inject.mjs +0 -0
  8. package/adapters/extension/launch-browser.cjs +0 -0
  9. package/adapters/extension/launch.sh +0 -0
  10. package/adapters/extension/live.sh +0 -0
  11. package/adapters/extension/readiness.mjs +0 -0
  12. package/adapters/extension/reattach.sh +0 -0
  13. package/adapters/extension/refresh-build.sh +0 -0
  14. package/adapters/extension/seed-fixture.sh +0 -0
  15. package/adapters/extension/sidepanel-toggle.sh +0 -0
  16. package/adapters/extension/snapshot-dist.sh +0 -0
  17. package/adapters/extension/start-watch.sh +1 -0
  18. package/adapters/extension/stop-viewers.sh +0 -0
  19. package/adapters/extension/sync-webpack-dist.cjs +1 -1
  20. package/adapters/extension/verify.sh +0 -0
  21. package/adapters/extension/wallet-fixture-state.cjs +0 -0
  22. package/adapters/manifest.json +9 -1
  23. package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +0 -0
  24. package/adapters/mobile/bridge-runtime/lib/match-bridge-target.cjs +3 -1
  25. package/adapters/mobile/bridge-runtime/setup-wallet.sh +0 -0
  26. package/adapters/mobile/cleanup.sh +0 -0
  27. package/adapters/mobile/inject.sh +13 -11
  28. package/adapters/mobile/launch-console-forwarder.cjs +0 -0
  29. package/adapters/mobile/lib/metro-listener.sh +0 -0
  30. package/adapters/mobile/lib/tmux-viewer.sh +0 -0
  31. package/adapters/mobile/open-device.sh +0 -0
  32. package/adapters/mobile/prewarm-bundle.sh +0 -0
  33. package/adapters/mobile/start-console-forwarder.sh +3 -3
  34. package/adapters/mobile/start-metro.sh +0 -0
  35. package/adapters/mobile/stop-metro.sh +0 -0
  36. package/adapters/mobile/verify.sh +0 -0
  37. package/adapters/mobile/wait-for-bridge.sh +22 -8
  38. package/adapters/mobile/yarn-setup.sh +0 -0
  39. package/adapters/shared/activate-repo-node.sh +0 -0
  40. package/adapters/shared/activate-repo-ruby.sh +0 -0
  41. package/adapters/shared/cli-ux.sh +0 -0
  42. package/adapters/shared/ensure-runner-deps.sh +0 -0
  43. package/adapters/shared/harness-path.sh +0 -0
  44. package/adapters/shared/hash-helpers.sh +0 -0
  45. package/adapters/shared/install-repo-deps.sh +0 -0
  46. package/adapters/shared/json-field.sh +0 -0
  47. package/adapters/shared/open-debug.mjs +7 -1
  48. package/adapters/shared/open-log-window.sh +0 -0
  49. package/adapters/shared/reap-checkout-metros.sh +0 -0
  50. package/adapters/shared/resolve-slot-ports.mjs +0 -0
  51. package/adapters/shared/resolve-slot-ports.sh +0 -0
  52. package/adapters/shared/sync-wallet-fixture.sh +0 -0
  53. package/adapters/shared/tmux-session.sh +0 -0
  54. package/adapters/shared/tmux-viewer.sh +0 -0
  55. package/adapters/shared/update-check-worker.mjs +35 -0
  56. package/dist/adapters/extension/console-capture.js +10 -1
  57. package/dist/adapters/mobile/prepare.js +5 -5
  58. package/dist/adapters/slot-ports.js +8 -0
  59. package/dist/cli.js +1 -1
  60. package/dist/commands/doctor.js +33 -8
  61. package/dist/commands/run-engine.js +2 -4
  62. package/dist/commands/update.js +16 -28
  63. package/dist/harness.js +24 -9
  64. package/dist/mm-harness-cli.js +8 -7
  65. package/dist/run-diagnostics.js +1 -6
  66. package/docs/ADAPTER-SURFACE.md +19 -112
  67. package/docs/CLI-ERGONOMICS-HUMAN-QA.md +16 -0
  68. package/docs/UX-PRINCIPLES.md +1 -1
  69. package/docs/VIDEO-DEMO-VALIDATION.md +56 -44
  70. package/docs/recipe-libraries.md +45 -275
  71. package/library/README.md +4 -0
  72. package/library/manifests/core.action-manifest.json +7 -7
  73. package/library/recipes/runner/smoke.core.recipe.json +27 -0
  74. package/library/recipes/wallet/smoke.extension.recipe.json +42 -0
  75. package/library/recipes/wallet/smoke.mobile.recipe.json +42 -0
  76. package/package.json +4 -2
  77. package/scripts/completions.sh +0 -0
  78. package/scripts/install-completions.sh +0 -0
  79. package/scripts/validate-human-outcomes.mjs +169 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,37 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.17.3 - 2026-07-15
6
+
7
+ ### Added
8
+
9
+ - Added a real-checkout human-outcome gate covering live readiness, runtime verification, debugger target resolution, smoke evidence, and unchanged tracked product source across Extension, Mobile, and Core.
10
+ - Added generic Core runner smoke and fixture-backed wallet smoke recipes so the reusable examples are not tied to Perps.
11
+ - Added nine-slot isolation coverage for platform ports, runtime files, logs, and headless Core behavior.
12
+
13
+ ### Changed
14
+
15
+ - `doctor` now reports CDP or simulator/ADB screenshot fallback when `capture-helper` is absent or unhealthy, while identifying video as unavailable separately.
16
+ - Action, flow, and recipe discovery/documentation now use one concise scope model; the default recipe-library and adapter-surface references were reduced to current behavior.
17
+ - Fixture discovery now uses one canonical checkout path instead of legacy fallback locations.
18
+
19
+ ### Fixed
20
+
21
+ - Mobile launch now waits for two consecutive platform-matched agentic bridge snapshots, preventing a transient startup route from being reported ready before an immediate verify.
22
+ - `verify --json` keeps stdout to one machine-readable JSON document even when the platform harness must first be installed.
23
+ - Mobile install metadata safely preserves checkout paths containing shell-significant characters.
24
+ - Cleanup discovery no longer advertises a removed compatibility flag, and Core recovery guidance now points to the canonical wallet fixture.
25
+ - Mobile console forwarding now uses canonical checkout paths, readiness rejects impossible one-poll stability checks, and Extension debug handles malformed WebSocket frames without crashing.
26
+ - Extension watch refreshes remove stale runtime files, delay file replacement until transfer completion, allow slower console attachment, and stop the detached webpack process group.
27
+
28
+ ## 0.17.2 - 2026-07-15
29
+
30
+ ### Fixed
31
+
32
+ - Passive update discovery now runs in a detached, once-daily registry probe, so fast commands populate the cache without adding latency or contaminating machine output.
33
+
34
+ ## 0.17.1 - 2026-07-15
35
+
5
36
  ### Fixed
6
37
 
7
38
  - Extension `debug` now uses Chrome's native DevTools command for the active slot target and opens directly on Console; `debug --worker` targets the service worker without creating a frontend browser tab.
@@ -9,6 +40,11 @@
9
40
  - Successful Extension watch rebuilds now update the isolated loaded runtime in place, so a normal refresh of the current fullscreen or sidepanel UI shows source edits without another harness build or relaunch.
10
41
  - Extension slot titles are stamped into the isolated runtime after snapshots and successful rebuilds, preserving the slot identifier across an operator's normal browser refresh without changing product source.
11
42
  - Extension sidepanel opening no longer waits for the harness-owned user-gesture button to pass visual actionability checks; occluded wallet loading surfaces cannot block the trusted click.
43
+ - Human recipe results now print an absolute-path artifact inventory and expand run-scoped application diagnostics inline; single-action calls show the same `CLEAN`/`REVIEW` diagnostics instead of requiring another query.
44
+ - Extension console collection now starts after every successful public launch path, including quick reattach, and persists as one checkout-scoped process until `stop`. MetaMask UI/service-worker events and dapp-page events are routed to separate `extension-console.log` and `dapp-console.log` files; bare `logs` defaults to MetaMask while `--source dapp` and `--source webpack` remain explicit.
45
+ - Mobile CDP-forwarded app events now write to `app-console.log`, so `logs --source app` and run diagnostics are no longer mixed with Metro bundler output; warm and cold launches start a process-group-detached collector only after bridge readiness so capture remains live after the launch command exits.
46
+ - Extension `debug` now opens Chrome's local DevTools frontend inside the exact slot-owned profile, targets the MetaMask extension page rather than the dapp, and verifies the debugging connection before reporting success.
47
+ - Mobile fixture application now fails immediately when the checkout fixture is absent, rejects the misplaced `fixtures set --dev` flag with the correct initialization command, and never prints a success message after failure.
12
48
 
13
49
  ### Changed
14
50
 
@@ -20,16 +56,6 @@
20
56
  - Removed compatibility-only runtime-start fields, hidden `live` execution, removed-command handlers, undomained recipe aliases, flat/non-ESM action lookup, old port-resolver filenames, duplicate Extension launcher naming, obsolete fixture search locations, and the alternate Mobile product-source bridge. Mobile install now requires the current product-owned bridge and writes metadata only.
21
57
  - Removed the historical CLI migration/spec documents; current behavior is documented in the README, cheatsheet, architecture, and human QA checklist.
22
58
 
23
- ## 0.17.1 - 2026-07-14
24
-
25
- ### Fixed
26
-
27
- - Human recipe results now print an absolute-path artifact inventory and expand run-scoped application diagnostics inline; single-action calls show the same `CLEAN`/`REVIEW` diagnostics instead of requiring another query.
28
- - Extension console collection now starts after every successful public launch path, including quick reattach, and persists as one checkout-scoped process until `stop`. MetaMask UI/service-worker events and dapp-page events are routed to separate `extension-console.log` and `dapp-console.log` files; bare `logs` defaults to MetaMask while `--source dapp` and `--source webpack` remain explicit.
29
- - Mobile CDP-forwarded app events now write to `app-console.log`, so `logs --source app` and run diagnostics are no longer mixed with Metro bundler output; warm and cold launches start a process-group-detached collector only after bridge readiness so capture remains live after the launch command exits.
30
- - Extension `debug` now opens Chrome's local DevTools frontend inside the exact slot-owned profile, targets the MetaMask extension page rather than the dapp, and verifies the debugging connection before reporting success.
31
- - Mobile fixture application now fails immediately when the checkout fixture is absent, rejects the misplaced `fixtures set --dev` flag with the correct initialization command, and never prints a success message after failure.
32
-
33
59
  ## 0.17.0 - 2026-07-14
34
60
 
35
61
  ### Added
package/README.md CHANGED
@@ -12,6 +12,12 @@ It has two jobs:
12
12
  The generic graph engine lives in `@farmslot/recipe-harness`. This package owns
13
13
  only MetaMask-specific actions and runtime control.
14
14
 
15
+ One mental model:
16
+
17
+ - **Action** — one typed operation, such as reading wallet state.
18
+ - **Flow** — a reusable, parameterized sequence of actions.
19
+ - **Recipe** — the complete executable proof for a task or regression.
20
+
15
21
  ## Install
16
22
 
17
23
  ```bash
@@ -70,11 +76,11 @@ mm-harness flows
70
76
 
71
77
  mm-harness run path/to/recipe.json --plan
72
78
  mm-harness run path/to/recipe.json --artifacts-dir temp/recipe-proof
79
+ mm-harness run wallet.smoke --artifacts-dir temp/wallet-proof
73
80
  ```
74
81
 
75
- `actions` is the executable vocabulary. `call` exercises one action. `flows`
76
- lists reusable parameterized behavior from personal, team, and bundled recipe
77
- libraries. A recipe connects those capabilities into a deterministic proof.
82
+ `call` exercises one action. A recipe composes actions and flows into a
83
+ deterministic proof.
78
84
 
79
85
  A run prints its findings, diagnostics, and absolute evidence paths. The artifact
80
86
  directory contains the executed recipe, summary, trace, diagnostics when
@@ -126,6 +132,6 @@ Validate changes with:
126
132
  yarn check
127
133
  ```
128
134
 
129
- Current references: [cheatsheet](docs/CHEATSHEET.md), [architecture](docs/architecture.md),
130
- [recipe libraries](docs/recipe-libraries.md), [human QA](docs/CLI-ERGONOMICS-HUMAN-QA.md),
131
- and the checked [video demo validation](docs/VIDEO-DEMO-VALIDATION.md).
135
+ Current references: [cheatsheet](docs/CHEATSHEET.md), [recipe libraries](docs/recipe-libraries.md),
136
+ and [human QA](docs/CLI-ERGONOMICS-HUMAN-QA.md). Maintainers also have the
137
+ [architecture](docs/architecture.md) and [video gate](docs/VIDEO-DEMO-VALIDATION.md).
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
@@ -182,6 +182,7 @@ rm -rf node_modules/.cache/webpack
182
182
  killed_harness_pid=""
183
183
  if [ -f "$RUNTIME_DIR/recipe-harness-webpack.pid" ]; then
184
184
  killed_harness_pid="$(cat "$RUNTIME_DIR/recipe-harness-webpack.pid" 2>/dev/null || true)"
185
+ [ -z "$killed_harness_pid" ] || kill -- "-$killed_harness_pid" >/dev/null 2>&1 || true
185
186
  [ -z "$killed_harness_pid" ] || kill "$killed_harness_pid" >/dev/null 2>&1 || true
186
187
  rm -f "$RUNTIME_DIR/recipe-harness-webpack.pid"
187
188
  fi
File without changes
@@ -51,7 +51,7 @@ function removeOwnPidFile() {
51
51
 
52
52
  function syncRuntimeDist() {
53
53
  if (!fs.existsSync(path.join(dist, 'manifest.json')) || !fs.existsSync(runtimeDist)) return;
54
- const result = spawnSync('rsync', ['-a', '--exclude', '_metadata', `${dist}${path.sep}`, `${runtimeDist}${path.sep}`], {
54
+ const result = spawnSync('rsync', ['-a', '--delete', '--delay-updates', '--exclude', '_metadata', `${dist}${path.sep}`, `${runtimeDist}${path.sep}`], {
55
55
  encoding: 'utf8',
56
56
  });
57
57
  if (result.status !== 0) {
File without changes
File without changes
@@ -87,7 +87,7 @@
87
87
  "entry": "adapters/mobile/cleanup.sh",
88
88
  "kind": "bash",
89
89
  "purpose": "Remove Mobile harness metadata and recorded git excludes without touching product source.",
90
- "inputs": "--target --allow-managed-changes; env RECIPE_HARNESS_ROOT",
90
+ "inputs": "--target; env RECIPE_HARNESS_ROOT",
91
91
  "outputs": "removes <harness>/; exit 0/1/2"
92
92
  },
93
93
  {
@@ -486,6 +486,14 @@
486
486
  "purpose": "Open human-facing debugger UIs for extension CDP or mobile Metro runtimes.",
487
487
  "inputs": "--adapter extension|mobile [--cdp-port|--port] [--target page|worker] [--json] [--no-open]",
488
488
  "outputs": "debug URL/report on stdout; exit 0/1"
489
+ },
490
+ {
491
+ "id": "lib/update-check-worker",
492
+ "entry": "adapters/shared/update-check-worker.mjs",
493
+ "kind": "node",
494
+ "purpose": "Detached, silent registry probe used by the daily passive update notice.",
495
+ "inputs": "<cache-file> <timeout-ms> [registry-endpoint]",
496
+ "outputs": "atomic update-cache refresh; always silent and best-effort"
489
497
  }
490
498
  ]
491
499
  }
File without changes
@@ -50,7 +50,9 @@ function matchesBridgeTarget(target, env) {
50
50
  // (an in-app agentic bridge is live), not merely a registered debug target.
51
51
  function hasMatchingRoute(value, env) {
52
52
  const targets = Array.isArray(value) ? value : [value];
53
- return targets.some((t) => matchesBridgeTarget(t, env) && t && t.route);
53
+ return targets.some(
54
+ (t) => matchesBridgeTarget(t, env) && t && t.agenticPresent === true && Boolean(t.route),
55
+ );
54
56
  }
55
57
 
56
58
  // Human summary of the targets that answered (platform / deviceName), for teaching on
File without changes
File without changes
@@ -98,23 +98,25 @@ fi
98
98
  RUNNER_REV="$(git -C "$RUNNER_DIR" rev-parse HEAD 2>/dev/null || echo unknown)"
99
99
  CLEANUP_Q="$(printf '%q' "$SCRIPT_DIR/cleanup.sh")"
100
100
  TARGET_CLEANUP_Q="$(printf '%q' "$TARGET")"
101
- node - "$HARNESS_DIR/manifest.json" <<NODE
101
+ CLEANUP_COMMAND="RECIPE_HARNESS_ROOT=$(printf '%q' "$HARNESS_ROOT") $CLEANUP_Q --target $TARGET_CLEANUP_Q"
102
+ node - "$HARNESS_DIR/manifest.json" "$TARGET_REV" "$RUNNER_DIR" "$RUNNER_REV" "$TARGET" "$HARNESS_REL" "$HARNESS_ROOT" "$CLEANUP_COMMAND" <<'NODE'
102
103
  const fs = require('node:fs');
103
- fs.writeFileSync(process.argv[2], JSON.stringify({
104
+ const [manifestPath, targetRevision, runnerDir, runnerRevision, target, harnessRel, harnessRoot, cleanupCommand] = process.argv.slice(2);
105
+ fs.writeFileSync(manifestPath, JSON.stringify({
104
106
  adapter: 'mobile',
105
107
  installMode: 'product-owned',
106
- targetRevision: '$TARGET_REV',
108
+ targetRevision,
107
109
  installedAt: new Date().toISOString(),
108
- source: { runnerDir: '$RUNNER_DIR', runnerRevision: '$RUNNER_REV' },
109
- target: '$TARGET',
110
+ source: { runnerDir, runnerRevision },
111
+ target,
110
112
  protocolVersion: 'v1',
111
- actionManifestPath: '$HARNESS_REL/action-manifest.json',
112
- runnerEntrypoint: '$HARNESS_REL/runner/bin/mm-harness',
113
- runtimeHelpers: { verify: '$HARNESS_REL/scripts/verify.sh' },
114
- harnessInstalledPaths: ['$HARNESS_REL/runner', '$HARNESS_REL/scripts', '$HARNESS_REL/action-manifest.json'],
113
+ actionManifestPath: `${harnessRel}/action-manifest.json`,
114
+ runnerEntrypoint: `${harnessRel}/runner/bin/mm-harness`,
115
+ runtimeHelpers: { verify: `${harnessRel}/scripts/verify.sh` },
116
+ harnessInstalledPaths: [`${harnessRel}/runner`, `${harnessRel}/scripts`, `${harnessRel}/action-manifest.json`],
115
117
  productOwnedPaths: ['app/dev-tools/AgenticService', 'app/core/NavigationService/NavigationService.ts'],
116
- cleanupCommand: 'RECIPE_HARNESS_ROOT=$HARNESS_ROOT $CLEANUP_Q --target $TARGET_CLEANUP_Q',
117
- productDiffExcludes: [':(exclude)$HARNESS_ROOT'],
118
+ cleanupCommand,
119
+ productDiffExcludes: [`:(exclude)${harnessRoot}`],
118
120
  }, null, 2) + '\n');
119
121
  NODE
120
122
 
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -8,8 +8,8 @@ QUIET=false
8
8
 
9
9
  while [ "$#" -gt 0 ]; do
10
10
  case "$1" in
11
- --target) TARGET="$2"; shift 2 ;;
12
- --port) PORT="$2"; shift 2 ;;
11
+ --target) [ "$#" -ge 2 ] || { echo "Missing value for --target" >&2; exit 2; }; TARGET="$2"; shift 2 ;;
12
+ --port) [ "$#" -ge 2 ] || { echo "Missing value for --port" >&2; exit 2; }; PORT="$2"; shift 2 ;;
13
13
  --quiet) QUIET=true; shift ;;
14
14
  -h|--help)
15
15
  printf 'Usage: start-console-forwarder.sh [--target <dir>] [--port <port>] [--quiet]\n'
@@ -20,7 +20,7 @@ while [ "$#" -gt 0 ]; do
20
20
  done
21
21
 
22
22
  [ "${METAMASK_RECIPE_CONSOLE_FORWARD:-1}" != "0" ] || exit 0
23
- TARGET="$(cd "$TARGET" && pwd)"
23
+ TARGET="$(cd "$TARGET" && pwd -P)"
24
24
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
25
25
  # shellcheck disable=SC1091
26
26
  . "$SCRIPT_DIR/../shared/harness-path.sh"
File without changes
File without changes
File without changes
@@ -24,10 +24,10 @@ REQUIRE_PLATFORM=""
24
24
 
25
25
  while [ "$#" -gt 0 ]; do
26
26
  case "$1" in
27
- --target) TARGET="$2"; shift 2 ;;
28
- --port) PORT="$2"; shift 2 ;;
29
- --max-polls) MAX_POLLS="$2"; shift 2 ;;
30
- --platform) REQUIRE_PLATFORM="$2"; shift 2 ;;
27
+ --target) [ "$#" -ge 2 ] || { echo "Missing value for --target" >&2; exit 2; }; TARGET="$2"; shift 2 ;;
28
+ --port) [ "$#" -ge 2 ] || { echo "Missing value for --port" >&2; exit 2; }; PORT="$2"; shift 2 ;;
29
+ --max-polls) [ "$#" -ge 2 ] || { echo "Missing value for --max-polls" >&2; exit 2; }; MAX_POLLS="$2"; shift 2 ;;
30
+ --platform) [ "$#" -ge 2 ] || { echo "Missing value for --platform" >&2; exit 2; }; REQUIRE_PLATFORM="$2"; shift 2 ;;
31
31
  -h|--help)
32
32
  printf 'Usage: wait-for-bridge.sh [--target <dir>] [--port <n>] [--max-polls <n>] [--platform ios|android]\n'
33
33
  exit 0
@@ -36,7 +36,12 @@ while [ "$#" -gt 0 ]; do
36
36
  esac
37
37
  done
38
38
 
39
- TARGET="$(cd "$TARGET" && pwd)"
39
+ case "$MAX_POLLS" in
40
+ ''|*[!0-9]*) echo "wait-for-bridge: --max-polls must be an integer of at least 2." >&2; exit 2 ;;
41
+ esac
42
+ [ "$MAX_POLLS" -ge 2 ] || { echo "wait-for-bridge: --max-polls must be at least 2 for stable readiness." >&2; exit 2; }
43
+
44
+ TARGET="$(cd "$TARGET" && pwd -P)"
40
45
 
41
46
  # The matcher reads the required platform from the environment so the value survives
42
47
  # into the node subprocess that runs the shared matcher against the status output.
@@ -128,15 +133,24 @@ try {
128
133
  ' "$MATCH_LIB" "$STATUS_LOG" 2>/dev/null || printf 'none'
129
134
  }
130
135
 
136
+ READY_STREAK=0
131
137
  for ATTEMPT in $(seq 1 "$MAX_POLLS"); do
132
138
  if bridge_has_route; then
133
- printf 'Mobile bridge ready\n' >&2
134
- exit 0
139
+ READY_STREAK=$((READY_STREAK + 1))
140
+ # The bridge can appear briefly while React Native is still completing its
141
+ # startup navigation/reload. Require two consecutive healthy snapshots so a
142
+ # following command never inherits that transient gap.
143
+ if [ "$READY_STREAK" -ge 2 ]; then
144
+ printf 'Mobile bridge ready\n' >&2
145
+ exit 0
146
+ fi
147
+ else
148
+ READY_STREAK=0
135
149
  fi
136
150
  if [ "$ATTEMPT" = "1" ] || [ $((ATTEMPT % 5)) -eq 0 ]; then
137
151
  printf 'Waiting for Mobile bridge (%s/%s): %s\n' "$ATTEMPT" "$MAX_POLLS" "$(bridge_wait_reason)" >&2
138
152
  fi
139
- sleep 2
153
+ [ "$ATTEMPT" -ge "$MAX_POLLS" ] || sleep 2
140
154
  done
141
155
 
142
156
  cat "$STATUS_LOG" >&2 || true
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
@@ -123,7 +123,13 @@ function sendBrowserCommand(webSocketDebuggerUrl, method, params) {
123
123
  }));
124
124
  });
125
125
  socket.on('message', (raw) => {
126
- const message = JSON.parse(String(raw));
126
+ let message;
127
+ try {
128
+ message = JSON.parse(String(raw));
129
+ } catch {
130
+ finish({ error: { message: `${method} returned a non-JSON WebSocket frame` } });
131
+ return;
132
+ }
127
133
  if (message.id !== 1) return;
128
134
  finish(message);
129
135
  });
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+
5
+ const [cacheFile, timeoutText, endpoint] = process.argv.slice(2);
6
+ const timeoutMs = Number.parseInt(timeoutText ?? '', 10);
7
+
8
+ async function main() {
9
+ if (!cacheFile || !endpoint || !Number.isInteger(timeoutMs) || timeoutMs < 1) return;
10
+ const controller = new AbortController();
11
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
12
+ try {
13
+ const response = await fetch(endpoint, { signal: controller.signal });
14
+ if (!response.ok) return;
15
+ const data = await response.json();
16
+ if (!data || typeof data.latest !== 'string' || data.latest.length === 0) return;
17
+ let lastCheck = Date.now();
18
+ try {
19
+ const cached = JSON.parse(fs.readFileSync(cacheFile, 'utf8'));
20
+ if (typeof cached.lastCheck === 'number') lastCheck = cached.lastCheck;
21
+ } catch {
22
+ // The parent may have exited before creating the cache; recreate it below.
23
+ }
24
+ fs.mkdirSync(path.dirname(cacheFile), { recursive: true });
25
+ const temporary = `${cacheFile}.${process.pid}.tmp`;
26
+ fs.writeFileSync(temporary, JSON.stringify({ lastCheck, latest: data.latest }));
27
+ fs.renameSync(temporary, cacheFile);
28
+ } catch {
29
+ // Passive checks are intentionally silent; the explicit update command teaches failures.
30
+ } finally {
31
+ clearTimeout(timer);
32
+ }
33
+ }
34
+
35
+ await main();
@@ -47,12 +47,21 @@ async function ensureExtensionConsoleCapture(target) {
47
47
  child.unref();
48
48
  fs.writeFileSync(pidFile, `${child.pid}
49
49
  `);
50
- for (let attempt = 0; attempt < 30; attempt += 1) {
50
+ for (let attempt = 0; attempt < 100; attempt += 1) {
51
+ if (!processAlive(child.pid)) break;
51
52
  if (collectorOwnsLog(child.pid, extensionLog) && containsAttachment(extensionLog, dappLog)) return;
52
53
  await new Promise((resolve) => setTimeout(resolve, 100));
53
54
  }
54
55
  throw new Error(`Extension console collector ${child.pid} did not attach to CDP ${cdpPort}.`);
55
56
  }
57
+ function processAlive(pid) {
58
+ try {
59
+ process.kill(pid, 0);
60
+ return true;
61
+ } catch {
62
+ return false;
63
+ }
64
+ }
56
65
  function readPid(pidFile) {
57
66
  try {
58
67
  const pid = Number.parseInt(fs.readFileSync(pidFile, "utf8").trim(), 10);
@@ -56,7 +56,7 @@ async function prepareMobile(target, opts = {}) {
56
56
  json,
57
57
  preflightMode
58
58
  );
59
- if (confirm.status === 0) return startMobileConsoleForwarder(target, json);
59
+ if (confirm.status === 0) return startMobileConsoleForwarder(target, platform, json);
60
60
  if (!json) {
61
61
  process.stderr.write(
62
62
  `launch: runtime looked ready but no ${platform} bridge target answered; launching the app.
@@ -70,7 +70,7 @@ async function prepareMobile(target, opts = {}) {
70
70
  json,
71
71
  preflightMode
72
72
  );
73
- return launch.status === 0 ? startMobileConsoleForwarder(target, json) : launch;
73
+ return launch.status === 0 ? startMobileConsoleForwarder(target, platform, json) : launch;
74
74
  }
75
75
  if (report.decision === "unknown") {
76
76
  const msg = "mobile prepare: runtime state unknown\n Next: run mm-harness verify --adapter mobile --target <checkout>";
@@ -118,13 +118,13 @@ async function prepareMobile(target, opts = {}) {
118
118
  }
119
119
  }
120
120
  }
121
- return startMobileConsoleForwarder(target, json);
121
+ return startMobileConsoleForwarder(target, platform, json);
122
122
  }
123
- function startMobileConsoleForwarder(target, json) {
123
+ function startMobileConsoleForwarder(target, platform, json) {
124
124
  return dispatchAction(
125
125
  { id: "start-console-forwarder", cwd: target, argv: json ? ["--quiet"] : [] },
126
126
  target,
127
- "ios",
127
+ platform,
128
128
  json
129
129
  );
130
130
  }
@@ -113,6 +113,10 @@ function stopExtensionWatcher(target) {
113
113
  } catch {
114
114
  }
115
115
  for (const pid of ownedPids) {
116
+ try {
117
+ process.kill(-pid, "SIGTERM");
118
+ } catch {
119
+ }
116
120
  try {
117
121
  process.kill(pid, "SIGTERM");
118
122
  } catch {
@@ -123,6 +127,10 @@ function stopExtensionWatcher(target) {
123
127
  if (ownedPids.length > 0) {
124
128
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 2e3);
125
129
  for (const pid of ownedPids) {
130
+ try {
131
+ process.kill(-pid, "SIGKILL");
132
+ } catch {
133
+ }
126
134
  try {
127
135
  process.kill(pid, "SIGKILL");
128
136
  } catch {
package/dist/cli.js CHANGED
@@ -104,7 +104,7 @@ async function main(argv) {
104
104
  const target = targetPath(options);
105
105
  const adapterRaw = (typeof options["adapter"] === "string" ? options["adapter"] : void 0) ?? (typeof options["platform"] === "string" ? options["platform"] : void 0) ?? detectAdapter(target);
106
106
  if (adapterRaw === "mobile" || adapterRaw === "extension" || adapterRaw === "core") {
107
- await handleHarness(["install", "--platform", adapterRaw, "--target", target]);
107
+ await handleHarness(["install", "--platform", adapterRaw, "--target", target, "--quiet"]);
108
108
  }
109
109
  } catch {
110
110
  }
@@ -140,7 +140,7 @@ async function handleDoctor({ options }) {
140
140
  requiredChecks
141
141
  };
142
142
  const orphanMetros = adapter === "mobile" ? detectOrphanMetros(target, process.env.WATCHER_PORT) : [];
143
- const capture = captureHelperHealth();
143
+ const capture = captureHealth(adapter);
144
144
  const deviceView = adapter === "mobile" ? mobileDeviceView(allDevices) : null;
145
145
  const liveView = deviceView ? await mobileDeviceLiveView(target, deviceView) : null;
146
146
  const devices = liveView?.devicesWithLive ?? deviceView?.devices ?? [];
@@ -192,13 +192,7 @@ async function handleDoctor({ options }) {
192
192
  );
193
193
  console.log(` ${out("dim", "Next: mm-harness stop # reaps every bundler this checkout leaked")}`);
194
194
  }
195
- if (capture) {
196
- console.log(`${out("label", "capture:")} ${out(capture.status === "pass" ? "ok" : "warn", capture.status)} ${out("dim", "(capture-helper: screenshots + --record video)")}`);
197
- if (capture.status !== "pass") {
198
- if (capture.failing.length > 0) console.log(` ${out("dim", `failing: ${capture.failing.join(", ")}`)}`);
199
- console.log(` ${out("dim", "Next: grant Screen Recording (System Settings \u2192 Privacy & Security \u2192 Screen Recording), or run: capture-helper doctor --open-permissions")}`);
200
- }
201
- }
195
+ if (capture) renderCaptureHealth(capture, out);
202
196
  if (adapter === "mobile") {
203
197
  if (deviceView) renderMobileDeviceList(deviceView, out);
204
198
  if (additionalReachableDevices.length > 0) renderAdditionalReachableDevices(additionalReachableDevices, out);
@@ -280,6 +274,25 @@ function emitExpectLive(adapter, target, runtime, result, orphanMetros, capture,
280
274
  emitExpectLiveVerdict(adapter, target, runtime, "not-live");
281
275
  return EXIT.runtime;
282
276
  }
277
+ function captureHealth(adapter) {
278
+ if (adapter === "core") return null;
279
+ const fallback = adapter === "extension" ? ["cdp"] : ["simctl", "adb"];
280
+ const helper = captureHelperHealth();
281
+ if (!helper) {
282
+ return {
283
+ status: "fallback",
284
+ failing: [],
285
+ screenshots: { available: true, providers: fallback },
286
+ recording: { available: false, provider: null }
287
+ };
288
+ }
289
+ return {
290
+ status: helper.status,
291
+ failing: helper.failing,
292
+ screenshots: { available: true, providers: ["capture-helper", ...fallback] },
293
+ recording: { available: helper.status === "pass", provider: helper.status === "pass" ? "capture-helper" : null }
294
+ };
295
+ }
283
296
  function captureHelperHealth() {
284
297
  if (process.platform !== "darwin") return null;
285
298
  const bin = process.env.CAPTURE_HELPER_PATH || "capture-helper";
@@ -292,6 +305,18 @@ function captureHelperHealth() {
292
305
  return null;
293
306
  }
294
307
  }
308
+ function renderCaptureHealth(capture, out) {
309
+ const video = capture.recording.available ? "capture-helper" : "unavailable";
310
+ console.log(
311
+ `${out("label", "capture:")} ${out(capture.status === "pass" ? "ok" : "warn", capture.status)} ${out("dim", `(screenshots: ${capture.screenshots.providers.join(" \u2192 ")}; video: ${video})`)}`
312
+ );
313
+ if (capture.status === "warn") {
314
+ if (capture.failing.length > 0) console.log(` ${out("dim", `failing: ${capture.failing.join(", ")}`)}`);
315
+ console.log(` ${out("dim", "Next: grant Screen Recording (System Settings \u2192 Privacy & Security \u2192 Screen Recording), or run: capture-helper doctor --open-permissions")}`);
316
+ } else if (capture.status === "fallback") {
317
+ console.log(` ${out("dim", "Next: screenshots work without capture-helper; install/configure capture-helper only when recipe video is required")}`);
318
+ }
319
+ }
295
320
  function detectOrphanMetros(target, livePort) {
296
321
  try {
297
322
  const script = path.join(runnerDir, "adapters/shared/reap-checkout-metros.sh");
@@ -22,8 +22,7 @@ import { listRecipeFiles } from "../recipe-files.js";
22
22
  import { startRecipeRecording, stopRecipeRecording } from "../run-recording.js";
23
23
  import {
24
24
  beginRunDiagnostics,
25
- finishRunDiagnostics,
26
- stopRunDiagnostics
25
+ finishRunDiagnostics
27
26
  } from "../run-diagnostics.js";
28
27
  import { EXIT } from "./shared.js";
29
28
  import {
@@ -90,7 +89,6 @@ async function runRecipe(adapter, recipe, artifactsDir, projectRoot, actionManif
90
89
  await stopRecipeRecording(recording, result);
91
90
  return finishRunDiagnostics(diagnosticBaseline, result);
92
91
  } finally {
93
- stopRunDiagnostics(diagnosticBaseline);
94
92
  await stopRecipeRecording(recording);
95
93
  }
96
94
  } finally {
@@ -645,7 +643,7 @@ async function executeWithHealBounds(exec, adapter, target, heal, state, recover
645
643
  }
646
644
  }
647
645
  function emitHealViolation(json, command, result, violation, state, adapter) {
648
- const userAction = adapter === "core" && violation.code === "WALLET_STATE_REQUIRED" ? 'set MM_TEST_ACCOUNT_ADDRESS=<0x\u2026> in env, or add "account": "<0x\u2026>" to the node block in the recipe' : violation.userAction ?? `inspect ${shellQuoteArg(result.summaryPath)} and ${shellQuoteArg(result.tracePath)}; fix the application or recipe failure before retrying`;
646
+ const userAction = adapter === "core" && violation.code === "WALLET_STATE_REQUIRED" ? 'create temp/recipe/runtime/wallet-fixture.json, or add "account": "<0x\u2026>" to the node block in the recipe' : violation.userAction ?? `inspect ${shellQuoteArg(result.summaryPath)} and ${shellQuoteArg(result.tracePath)}; fix the application or recipe failure before retrying`;
649
647
  if (json) {
650
648
  console.log(
651
649
  JSON.stringify(