@deeeed/metamask-harness 0.17.5 → 0.19.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.
Files changed (169) hide show
  1. package/CHANGELOG.md +1059 -0
  2. package/README.md +91 -140
  3. package/adapters/core/inject.sh +1 -6
  4. package/adapters/extension/console-tail.mjs +55 -24
  5. package/adapters/extension/ensure-browser.sh +6 -1
  6. package/adapters/extension/inject.mjs +7 -13
  7. package/adapters/extension/launch-browser.cjs +7 -1
  8. package/adapters/extension/launch-webpack.cjs +83 -0
  9. package/adapters/extension/launch.sh +8 -11
  10. package/adapters/extension/lib/chrome-args.cjs +8 -1
  11. package/adapters/extension/lib/macos-focus.cjs +32 -0
  12. package/adapters/extension/live.sh +12 -21
  13. package/adapters/extension/reattach.sh +1 -0
  14. package/adapters/extension/seed-fixture.sh +4 -12
  15. package/adapters/extension/sidepanel-toggle.sh +4 -1
  16. package/adapters/extension/stamp-runtime-title.cjs +58 -0
  17. package/adapters/extension/start-watch.sh +17 -4
  18. package/adapters/extension/stop-viewers.sh +1 -1
  19. package/adapters/extension/sync-webpack-dist.cjs +107 -0
  20. package/adapters/extension/verify.sh +2 -2
  21. package/adapters/manifest.json +53 -37
  22. package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +16 -0
  23. package/adapters/mobile/bridge-runtime/console-forwarder.cjs +1 -1
  24. package/adapters/mobile/bridge-runtime/lib/match-bridge-target.cjs +3 -1
  25. package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +2 -3
  26. package/adapters/mobile/cleanup.sh +16 -176
  27. package/adapters/mobile/inject.sh +91 -671
  28. package/adapters/mobile/launch-console-forwarder.cjs +39 -0
  29. package/adapters/mobile/open-device.sh +45 -7
  30. package/adapters/mobile/start-console-forwarder.sh +70 -0
  31. package/adapters/mobile/start-metro.sh +0 -41
  32. package/adapters/mobile/verify.sh +34 -83
  33. package/adapters/mobile/wait-for-bridge.sh +22 -8
  34. package/adapters/shared/harness-source-fingerprint.mjs +14 -12
  35. package/adapters/shared/resolve-slot-ports-core.mjs +17 -6
  36. package/adapters/shared/resolve-slot-ports.sh +17 -5
  37. package/adapters/shared/update-check-worker.mjs +43 -0
  38. package/bin/mm-harness +21 -2
  39. package/dist/adapters/core/surface.js +4 -1
  40. package/dist/adapters/extension/console-capture.js +95 -0
  41. package/dist/adapters/extension/product-config.js +110 -0
  42. package/dist/adapters/extension/runtime-decision.js +20 -2
  43. package/dist/adapters/extension/surface.js +22 -3
  44. package/dist/adapters/harness-freshness.js +49 -0
  45. package/dist/adapters/mobile/perps-env.js +43 -3
  46. package/dist/adapters/mobile/prepare.js +39 -7
  47. package/dist/adapters/mobile/surface.js +5 -2
  48. package/dist/adapters/resolve-slot-ports.js +2 -2
  49. package/dist/adapters/slot-ports.js +13 -16
  50. package/dist/adapters.js +56 -16
  51. package/dist/checkout-lock.js +27 -2
  52. package/dist/cli-color.js +19 -0
  53. package/dist/cli-commands.js +1 -1
  54. package/dist/cli.js +7 -18
  55. package/dist/command-contract.js +462 -0
  56. package/dist/command-journal.js +263 -0
  57. package/dist/commands/call.js +252 -57
  58. package/dist/commands/check.js +10 -4
  59. package/dist/commands/completion-candidates.js +17 -22
  60. package/dist/commands/device-target.js +27 -12
  61. package/dist/commands/doctor.js +106 -25
  62. package/dist/commands/fixtures.js +142 -35
  63. package/dist/commands/last.js +60 -0
  64. package/dist/commands/launch/extension.js +40 -17
  65. package/dist/commands/launch/index.js +207 -63
  66. package/dist/commands/launch/mobile.js +2 -0
  67. package/dist/commands/list-executables.js +187 -29
  68. package/dist/commands/logs.js +8 -6
  69. package/dist/commands/manifest.js +286 -35
  70. package/dist/commands/parse-args.js +68 -4
  71. package/dist/commands/provision.js +10 -4
  72. package/dist/commands/run-engine.js +772 -264
  73. package/dist/commands/run-report.js +12 -3
  74. package/dist/commands/run.js +378 -85
  75. package/dist/commands/shared.js +76 -4
  76. package/dist/commands/status-probe.js +4 -1
  77. package/dist/commands/status.js +2 -1
  78. package/dist/commands/stop.js +7 -2
  79. package/dist/commands/update.js +16 -28
  80. package/dist/completions-cache.js +1 -1
  81. package/dist/doctor.js +57 -10
  82. package/dist/harness.js +47 -137
  83. package/dist/heal-bounds.js +7 -2
  84. package/dist/json-stream.js +57 -0
  85. package/dist/live-adapter-contract.js +138 -32
  86. package/dist/manifest.js +161 -1
  87. package/dist/mm-harness-cli.js +146 -73
  88. package/dist/paths.js +2 -5
  89. package/dist/recipe-security.js +178 -0
  90. package/dist/run-diagnostics.js +261 -0
  91. package/dist/run-recording.js +1 -1
  92. package/dist/runner.js +140 -9
  93. package/docs/CONTRIBUTING.md +136 -0
  94. package/docs/QA.md +184 -0
  95. package/docs/RECIPES.md +122 -0
  96. package/docs/SECURITY.md +88 -0
  97. package/library/README.md +7 -3
  98. package/library/actions/core/perps/_controller.mjs +43 -56
  99. package/library/actions/core/perps/assert_orders.mjs +6 -7
  100. package/library/actions/core/perps/assert_positions.mjs +6 -7
  101. package/library/actions/core/perps/close_orders.mjs +2 -0
  102. package/library/actions/core/perps/close_positions.mjs +2 -0
  103. package/library/actions/core/perps/ensure_orders.mjs +4 -2
  104. package/library/actions/core/perps/ensure_positions.mjs +4 -2
  105. package/library/actions/core/perps/place_order.mjs +7 -3
  106. package/library/actions/core/perps/read_account.mjs +2 -2
  107. package/library/actions/core/perps/read_orders.mjs +2 -1
  108. package/library/actions/core/perps/read_positions.mjs +2 -1
  109. package/library/actions/core/wallet/list_accounts.mjs +95 -0
  110. package/library/actions/extension/perps/assert_orders.mjs +2 -1
  111. package/library/actions/extension/perps/assert_positions.mjs +2 -1
  112. package/library/actions/extension/perps/perps.mjs +43 -14
  113. package/library/actions/extension/platform/cdp.mjs +1 -0
  114. package/library/actions/extension/wallet/list_accounts.mjs +41 -0
  115. package/library/actions/mobile/perps/assert_orders.mjs +2 -1
  116. package/library/actions/mobile/perps/assert_positions.mjs +2 -1
  117. package/library/actions/mobile/perps/perps.mjs +40 -12
  118. package/library/actions/mobile/wallet/list_accounts.mjs +37 -0
  119. package/library/library.json +1 -1
  120. package/library/manifests/core.action-manifest.json +1214 -396
  121. package/library/manifests/extension.action-manifest.json +1635 -728
  122. package/library/manifests/mobile.action-manifest.json +1740 -727
  123. package/library/recipes/app/lifecycle.android-smoke.mobile.recipe.json +63 -81
  124. package/library/recipes/perps/clean-market-testnet.core.recipe.json +44 -0
  125. package/library/recipes/perps/clean-market-testnet.recipe.json +49 -0
  126. package/library/recipes/perps/lifecycle.recipe.json +136 -180
  127. package/library/recipes/perps/order-lifecycle.core.recipe.json +71 -67
  128. package/library/recipes/perps/performance.background-resume.mobile.recipe.json +51 -67
  129. package/library/recipes/perps/performance.cold-start.mobile.recipe.json +51 -67
  130. package/library/recipes/perps/performance.mobile.recipe.json +37 -51
  131. package/library/recipes/perps/performance.warm-start.mobile.recipe.json +44 -59
  132. package/library/recipes/perps/read-markets.core.recipe.json +29 -31
  133. package/library/recipes/perps/smoke.core.recipe.json +29 -32
  134. package/library/recipes/perps/smoke.extension.recipe.json +41 -44
  135. package/library/recipes/perps/smoke.mobile.recipe.json +42 -44
  136. package/library/recipes/perps/trading-lifecycle.core.recipe.json +69 -65
  137. package/library/recipes/runner/action-validation.extension.recipe.json +312 -398
  138. package/library/recipes/runner/action-validation.mobile.recipe.json +316 -402
  139. package/library/recipes/runner/smoke.core.recipe.json +25 -0
  140. package/library/recipes/runner/smoke.extension.recipe.json +23 -24
  141. package/library/recipes/runner/smoke.mobile.recipe.json +23 -24
  142. package/library/recipes/wallet/smoke.extension.recipe.json +40 -0
  143. package/library/recipes/wallet/smoke.mobile.recipe.json +40 -0
  144. package/package.json +12 -5
  145. package/scripts/completions.sh +8 -11
  146. package/scripts/validate-human-outcomes.mjs +169 -0
  147. package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +0 -185
  148. package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgenticService.ts.patch +0 -1662
  149. package/adapters/mobile/overlay/compat/README.md +0 -28
  150. package/adapters/mobile/overlay/compat/rn81-message-event-source.patch +0 -42
  151. package/adapters/shared/resolve-farmslot-ports-core.mjs +0 -3
  152. package/adapters/shared/resolve-farmslot-ports.mjs +0 -5
  153. package/adapters/shared/resolve-farmslot-ports.sh +0 -7
  154. package/dist/adapters/extension/harness-freshness.js +0 -39
  155. package/dist/commands/flows.js +0 -62
  156. package/docs/ADAPTER-SURFACE.md +0 -119
  157. package/docs/CHEATSHEET.md +0 -61
  158. package/docs/CLI-SPEC.md +0 -1098
  159. package/docs/CODE-MAP.md +0 -62
  160. package/docs/DEBUG-HANDOVER.md +0 -36
  161. package/docs/MENTAL-MODEL.md +0 -295
  162. package/docs/UX-PRINCIPLES.md +0 -64
  163. package/docs/architecture.md +0 -398
  164. package/docs/live-adapter-contract.md +0 -188
  165. package/docs/package-boundaries.md +0 -47
  166. package/docs/perps-flow-catalog.md +0 -235
  167. package/docs/recipe-libraries.md +0 -301
  168. package/docs/runtime-file-conventions.md +0 -36
  169. package/library/flows/perps.flows.json +0 -64
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const { spawn } = require('node:child_process');
5
+ const fs = require('node:fs');
6
+ const path = require('node:path');
7
+
8
+ const args = {};
9
+ for (let index = 2; index < process.argv.length; index += 1) {
10
+ const key = process.argv[index];
11
+ if (key === '--help' || key === '-h') {
12
+ process.stdout.write('Usage: launch-webpack.cjs --target <path> --log <path> --pid-file <path> --sync-script <path> --sync-pid-file <path> --sync-log <path> --dist <path> --runtime-dist <path> [--port <port>] [--path-prefix <path>]\n');
13
+ process.exit(0);
14
+ }
15
+ if (!key.startsWith('--') || index + 1 >= process.argv.length) throw new Error(`invalid argument: ${key}`);
16
+ args[key.slice(2)] = process.argv[++index];
17
+ }
18
+
19
+ async function main() {
20
+ for (const required of ['target', 'log', 'pid-file', 'sync-script', 'sync-pid-file', 'sync-log', 'dist', 'runtime-dist']) {
21
+ if (!args[required]) throw new Error(`--${required} is required`);
22
+ }
23
+
24
+ const target = path.resolve(args.target);
25
+ const log = path.resolve(args.log);
26
+ const pidFile = path.resolve(args['pid-file']);
27
+ const syncScript = path.resolve(args['sync-script']);
28
+ const syncPidFile = path.resolve(args['sync-pid-file']);
29
+ const syncLog = path.resolve(args['sync-log']);
30
+ fs.mkdirSync(path.dirname(log), { recursive: true });
31
+ fs.mkdirSync(path.dirname(pidFile), { recursive: true });
32
+ fs.mkdirSync(path.dirname(syncLog), { recursive: true });
33
+ if (!fs.existsSync(syncScript)) throw new Error(`sync script not found: ${syncScript}`);
34
+ const logFd = fs.openSync(log, 'a');
35
+ const syncLogFd = fs.openSync(syncLog, 'a');
36
+ const env = { ...process.env };
37
+ if (args.port) env.PORT = String(args.port);
38
+ if (args['path-prefix']) env.PATH = `${args['path-prefix']}${env.PATH || ''}`;
39
+
40
+ try {
41
+ const child = spawn('yarn', ['start'], {
42
+ cwd: target,
43
+ detached: true,
44
+ env,
45
+ stdio: ['ignore', logFd, logFd],
46
+ });
47
+ await new Promise((resolve, reject) => {
48
+ child.once('spawn', resolve);
49
+ child.once('error', reject);
50
+ });
51
+ fs.writeFileSync(pidFile, `${child.pid}\n`);
52
+ const syncChild = spawn(process.execPath, [
53
+ syncScript,
54
+ '--watcher-pid', String(child.pid),
55
+ '--watch-log', log,
56
+ '--target', target,
57
+ '--dist', path.resolve(args.dist),
58
+ '--runtime-dist', path.resolve(args['runtime-dist']),
59
+ '--pid-file', syncPidFile,
60
+ ], {
61
+ cwd: target,
62
+ detached: true,
63
+ env,
64
+ stdio: ['ignore', syncLogFd, syncLogFd],
65
+ });
66
+ await new Promise((resolve, reject) => {
67
+ syncChild.once('spawn', resolve);
68
+ syncChild.once('error', reject);
69
+ });
70
+ fs.writeFileSync(syncPidFile, `${syncChild.pid}\n`);
71
+ process.stdout.write(`${child.pid}\n`);
72
+ syncChild.unref();
73
+ child.unref();
74
+ } finally {
75
+ fs.closeSync(logFd);
76
+ fs.closeSync(syncLogFd);
77
+ }
78
+ }
79
+
80
+ main().catch((error) => {
81
+ process.stderr.write(`launch-webpack: ${error.message}\n`);
82
+ process.exit(1);
83
+ });
@@ -114,24 +114,21 @@ else
114
114
  status="fail"
115
115
  fi
116
116
 
117
- # Live extension-console viewer tab (default-on): tail the running extension's
118
- # service worker + UI-page console in a tmux window, mirroring the Metro/webpack
119
- # tabs. Skipped silently when there is no tmux session or CDP (headless/CI), so
120
- # it never affects the launch result.
117
+ # The public CLI owns console collection after every successful launch path.
118
+ # This leaf only adds an optional viewer when an explicitly run-owned tmux
119
+ # session exists; the viewer tails the file and never owns the collector.
121
120
  if [ "$status" = "pass" ] && [ -n "$CDP_PORT" ] && command -v tmux >/dev/null 2>&1; then
121
+ console_runtime="$TARGET/$( (cd "$TARGET" && recipe_runtime_dir) 2>/dev/null || echo temp/recipe/runtime)"
122
+ mkdir -p "$console_runtime"
123
+ console_log="$console_runtime/extension-console.log"
122
124
  for _tv_src in "$SCRIPT_DIR/lib/tmux-viewer.sh" "$SCRIPT_DIR/../shared/tmux-viewer.sh"; do
123
125
  # shellcheck disable=SC1091
124
126
  [ -f "$_tv_src" ] && { . "$_tv_src"; break; }
125
127
  done
126
128
  unset _tv_src
127
- console_tail="$SCRIPT_DIR/console-tail.mjs"
128
- if command -v tmux_viewer_open >/dev/null 2>&1 && [ -f "$console_tail" ]; then
129
- console_runtime="$TARGET/$( (cd "$TARGET" && recipe_runtime_dir) 2>/dev/null || echo temp/recipe/runtime)"
130
- mkdir -p "$console_runtime"
131
- console_log="$console_runtime/extension-console.log"
132
- : > "$console_log"
129
+ if command -v tmux_viewer_open >/dev/null 2>&1; then
133
130
  tmux_viewer_open "console" "$CDP_PORT" "$console_log" "$console_runtime" "$console_runtime/console.tmux" \
134
- "exec node $(printf '%q' "$console_tail") --cdp-port ${CDP_PORT} --log $(printf '%q' "$console_log")"
131
+ "exec tail -n +1 -F $(printf '%q' "$console_log")"
135
132
  fi
136
133
  fi
137
134
 
@@ -21,4 +21,11 @@ function remoteDebuggingArgs(cdpPort) {
21
21
  ];
22
22
  }
23
23
 
24
- module.exports = { remoteDebuggingArgs };
24
+ // An isolated automation profile must never ask for or write credentials in the
25
+ // operator's macOS login keychain. Chromium's mock keychain keeps profile-local
26
+ // encryption deterministic and prevents modal prompts from blocking startup.
27
+ function isolatedProfileArgs(platform = process.platform) {
28
+ return platform === 'darwin' ? ['--use-mock-keychain'] : [];
29
+ }
30
+
31
+ module.exports = { isolatedProfileArgs, remoteDebuggingArgs };
@@ -0,0 +1,32 @@
1
+ 'use strict';
2
+
3
+ const { execFileSync } = require('node:child_process');
4
+
5
+ function captureMacFrontmostProcess() {
6
+ if (process.platform !== 'darwin') return null;
7
+ try {
8
+ const value = execFileSync('osascript', [
9
+ '-e',
10
+ 'tell application "System Events" to get unix id of first application process whose frontmost is true',
11
+ ], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
12
+ const pid = Number.parseInt(value, 10);
13
+ return Number.isInteger(pid) && pid > 0 ? pid : null;
14
+ } catch {
15
+ return null;
16
+ }
17
+ }
18
+
19
+ function restoreMacFrontmostProcess(pid) {
20
+ if (process.platform !== 'darwin' || !Number.isInteger(pid) || pid <= 0) return false;
21
+ try {
22
+ execFileSync('osascript', [
23
+ '-e',
24
+ `tell application "System Events" to set frontmost of first application process whose unix id is ${pid} to true`,
25
+ ], { stdio: 'ignore' });
26
+ return true;
27
+ } catch {
28
+ return false;
29
+ }
30
+ }
31
+
32
+ module.exports = { captureMacFrontmostProcess, restoreMacFrontmostProcess };
@@ -32,6 +32,7 @@
32
32
  set -euo pipefail
33
33
 
34
34
  TARGET="$PWD"
35
+ INVOCATION_DIR="$PWD"
35
36
  CDP_PORT=""
36
37
  ARTIFACTS=""
37
38
  OUT=""
@@ -39,7 +40,7 @@ PREPARE_CMD="${RECIPE_HARNESS_EXTENSION_LAUNCH_CMD:-}"
39
40
  LAUNCH_EXISTING_DIST=false
40
41
  START_WATCH=false
41
42
  DIST_DIR="dist/chrome"
42
- CHROME_USER_DATA_DIR=""
43
+ CHROME_USER_DATA_DIR="${CHROME_USER_DATA_DIR:-}"
43
44
  REMOTE_FLAGS=""
44
45
  REMOTE_FLAGS_APPLIED=false
45
46
  while [ "$#" -gt 0 ]; do
@@ -60,6 +61,11 @@ while [ "$#" -gt 0 ]; do
60
61
  esac
61
62
  done
62
63
 
64
+ case "$CHROME_USER_DATA_DIR" in
65
+ ""|/*) ;;
66
+ *) CHROME_USER_DATA_DIR="$INVOCATION_DIR/$CHROME_USER_DATA_DIR" ;;
67
+ esac
68
+
63
69
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
64
70
  # shellcheck disable=SC1091
65
71
  for _hp in "$SCRIPT_DIR/lib/harness-path.sh" "$SCRIPT_DIR/../shared/harness-path.sh"; do
@@ -100,7 +106,6 @@ if [ -z "$VERIFY_SH" ]; then
100
106
  echo "mm-harness: extension verify.sh not found next to $SCRIPT_DIR; reinstall the runner." >&2
101
107
  exit 1
102
108
  fi
103
- TARGET="$(cd "$TARGET" && pwd)"
104
109
  RUNTIME_DIR="$(recipe_runtime_dir)"
105
110
  RUNTIME_DIST_DIR="${RECIPE_RUNTIME_DIST_DIR:-runtime-dist}"
106
111
  CHROME_PROFILE_NAME="${RECIPE_CHROME_PROFILE_NAME:-chrome-profile}"
@@ -163,6 +168,7 @@ NODE
163
168
  quoted_seed_fixture="$(printf '%q' "$SCRIPT_DIR/seed-fixture.sh")"
164
169
  quoted_start_watch="$(printf '%q' "$SCRIPT_DIR/start-watch.sh")"
165
170
  quoted_snapshot_dist="$(printf '%q' "$SCRIPT_DIR/snapshot-dist.sh")"
171
+ quoted_stamp_title="$(printf '%q' "$SCRIPT_DIR/stamp-runtime-title.cjs")"
166
172
  quoted_chrome_launcher="$(printf '%q' "$SCRIPT_DIR/launch-browser.cjs")"
167
173
  quoted_fixture_state="$(printf '%q' "$FIXTURE_STATE_ABS")"
168
174
  quoted_fixture_validation="$(printf '%q' "$FIXTURE_VALIDATION_ABS")"
@@ -211,11 +217,11 @@ try {
211
217
  executable = chromium.executablePath();
212
218
  } catch (error) {
213
219
  const message = error && error.message ? error.message : String(error);
214
- console.error(`[recipe-harness] Could not resolve Playwright Chromium executable: ${message}. Manual approval required before installing the Playwright Chromium browser cache (no package.json changes); ask the user before running yarn exec playwright install chromium.`);
220
+ console.error(`[recipe-harness] Could not resolve Playwright Chromium executable: ${message}. Manual approval required before installing the Playwright Chromium browser cache (no package.json changes); ask the user before running yarn playwright install chromium.`);
215
221
  process.exit(1);
216
222
  }
217
223
  if (!fs.existsSync(executable)) {
218
- console.error(`[recipe-harness] Playwright Chromium is not installed at ${executable}. Manual approval required before installing the Playwright Chromium browser cache (no package.json changes). Ask the user for approval; if they agree, run: cd ${shellQuote(process.cwd())} && yarn exec playwright install chromium`);
224
+ console.error(`[recipe-harness] Playwright Chromium is not installed at ${executable}. Manual approval required before installing the Playwright Chromium browser cache (no package.json changes). Ask the user for approval; if they agree, run: cd ${shellQuote(process.cwd())} && yarn playwright install chromium`);
219
225
  console.error('[recipe-harness] To use a browser that is already installed, set RECIPE_HARNESS_CHROME_BIN=/path/to/chrome explicitly.');
220
226
  process.exit(1);
221
227
  }
@@ -227,25 +233,9 @@ NODE
227
233
  rm -f "$_chrome_probe"
228
234
  fi
229
235
 
230
- if [ -z "${CHROME_BIN:-}" ]; then
231
- for candidate in \
232
- "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
233
- "/Applications/Chromium.app/Contents/MacOS/Chromium" \
234
- "/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary" \
235
- "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"; do
236
- if [ -x "$candidate" ]; then
237
- CHROME_BIN="$candidate"
238
- break
239
- fi
240
- done
241
- if [ -n "${CHROME_BIN:-}" ]; then
242
- echo "[recipe-harness] WARN: Playwright Chromium unavailable; falling back to system browser: $CHROME_BIN" >&2
243
- fi
244
- fi
245
-
246
236
  if [ -z "${CHROME_BIN:-}" ]; then
247
237
  [ -n "${CHROME_PROBE_LOG:-}" ] && cat "$CHROME_PROBE_LOG" >&2 2>/dev/null || true
248
- echo "[recipe-harness] No approved Chromium binary selected; stopping before live Extension launch." >&2
238
+ echo "[recipe-harness] No compatible Chromium binary selected; stopping before build or live Extension launch." >&2
249
239
  exit 1
250
240
  fi
251
241
  quoted_chrome="$(printf '%q' "$CHROME_BIN")"
@@ -257,6 +247,7 @@ NODE
257
247
  fi
258
248
  prepare_parts+=("node ${quoted_chrome_launcher} --stop-only 1 --reset-profile 1 --chrome-bin ${quoted_chrome} --profile ${quoted_profile} --cdp-port ${CDP_PORT} --extension-dir ${quoted_runtime_dist} --chrome-log ${quoted_chrome_log} --chrome-pid ${quoted_chrome_pid}")
259
249
  prepare_parts+=("bash ${quoted_snapshot_dist} --dist ${quoted_dist} --runtime-dist ${quoted_runtime_dist}")
250
+ prepare_parts+=("node ${quoted_stamp_title} --target ${quoted_target} --runtime-dist ${quoted_runtime_dist} --runtime-dir ${quoted_runtime_dir}")
260
251
  # Optional A/B feature-flag pinning: patch the ephemeral snapshot manifest so
261
252
  # manifest._flags wins over the fetched ClientConfigApi value. No-op unless
262
253
  # --remote-flag was passed; the snapshot is re-created each run, so no revert.
@@ -103,6 +103,7 @@ echo "[reattach] reusing loaded runtime-dist: $RUNTIME_DIST_ABS" >&2
103
103
  if [ -d "$DIST_ABS" ] && [ -d "$RUNTIME_DIST_ABS" ]; then
104
104
  echo "[reattach] refreshing loaded runtime-dist in place" >&2
105
105
  rsync -a --exclude _metadata "$DIST_ABS/" "$RUNTIME_DIST_ABS/" >&2
106
+ node "$SCRIPT_DIR/stamp-runtime-title.cjs" --target "$TARGET" --runtime-dist "$RUNTIME_DIST_ABS" --runtime-dir "$RUNTIME_DIR" >&2
106
107
  fi
107
108
 
108
109
  # Resolve the extension id: explicit flag, then the recorded id file, else CDP.
@@ -4,15 +4,13 @@
4
4
  #
5
5
  # Purpose:
6
6
  # Resolves which wallet fixture a slot run should use (env override then
7
- # runtime-dir/legacy/port-scoped candidates) and drives the recipe-tree
7
+ # the checkout's canonical runtime fixture) and drives the recipe-tree
8
8
  # wallet-fixture-state.cjs phases around the browser launch.
9
9
  #
10
10
  # Subcommands (flags):
11
11
  # resolve --target <repo> --cdp-port <port> [--source-out <file>]
12
12
  # Prints the fixture path (empty if none). Chain:
13
- # RECIPE_WALLET_FIXTURE env > <runtime-dir>/wallet-fixture.json >
14
- # temp/runtime/ > temp/.recipe-validation-<port>/ >
15
- # temp/.agent-validation/.
13
+ # RECIPE_WALLET_FIXTURE env > <runtime-dir>/wallet-fixture.json.
16
14
  # prefill --fixture <json> --target <repo> --state <file> --profile <dir>
17
15
  # --extension-dir <dist> --extension-id-file <file>
18
16
  # Runs fixture-state generate then prefill-profile (pre-launch).
@@ -131,14 +129,8 @@ if [ "$SUBCOMMAND" = resolve ]; then
131
129
  fi
132
130
  resolved="$RECIPE_WALLET_FIXTURE"
133
131
  else
134
- for candidate in \
135
- ${RUNTIME_DIR:+"$TARGET/$RUNTIME_DIR/wallet-fixture.json"} \
136
- "$TARGET/temp/runtime/wallet-fixture.json" \
137
- ${CDP_PORT:+"$TARGET/temp/.recipe-validation-${CDP_PORT}/wallet-fixture.json"} \
138
- "$TARGET/temp/.agent-validation/wallet-fixture.json"
139
- do
140
- [ -f "$candidate" ] && { resolved="$candidate"; break; }
141
- done
132
+ candidate="${RUNTIME_DIR:+$TARGET/$RUNTIME_DIR/wallet-fixture.json}"
133
+ [ -n "$candidate" ] && [ -f "$candidate" ] && resolved="$candidate"
142
134
  fi
143
135
  [ -n "$resolved" ] || resolve_status=missing
144
136
  if [ -n "$SOURCE_OUT" ]; then
@@ -289,7 +289,10 @@ const { chromium } = require('playwright');
289
289
  return { windowId: currentWindow.id };
290
290
  });
291
291
 
292
- await page.locator('#__recipe_open_sidepanel__').click({ timeout: 5000 });
292
+ // This harness-owned button exists only to provide Chrome's trusted user
293
+ // gesture. Its visual layout is irrelevant and can be occluded by the app's
294
+ // full-screen loading surface, so do not wait on Playwright actionability.
295
+ await page.locator('#__recipe_open_sidepanel__').click({ timeout: 5000, force: true });
293
296
  const clickResult = await page
294
297
  .locator('#__recipe_open_sidepanel__')
295
298
  .evaluate((button) => button.dataset.result || '');
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('node:fs');
5
+ const path = require('node:path');
6
+
7
+ const { readSlotId } = require('./lib/slot-title.cjs');
8
+
9
+ function stampRuntimeTitles({ target, runtimeDist, runtimeDir = process.env.RECIPE_RUNTIME_DIR || 'temp/recipe/runtime' }) {
10
+ const slotId = readSlotId(target, runtimeDir);
11
+ if (!slotId) return { slotId: '', stamped: [] };
12
+ const stamped = [];
13
+ for (const name of ['home.html', 'sidepanel.html']) {
14
+ const file = path.join(runtimeDist, name);
15
+ let fd;
16
+ try {
17
+ fd = fs.openSync(file, 'r+');
18
+ const source = fs.readFileSync(fd, 'utf8');
19
+ const next = source.replace(/<title>([^<]*)<\/title>/iu, (_match, title) => {
20
+ const base = String(title || 'MetaMask').replace(/^.+?\s+[—–-]\s+/u, '') || 'MetaMask';
21
+ return `<title>${slotId} — ${base}</title>`;
22
+ });
23
+ if (next === source) continue;
24
+ const bytes = Buffer.from(next, 'utf8');
25
+ fs.writeSync(fd, bytes, 0, bytes.length, 0);
26
+ fs.ftruncateSync(fd, bytes.length);
27
+ stamped.push(name);
28
+ } catch (error) {
29
+ if (error?.code !== 'ENOENT') throw error;
30
+ } finally {
31
+ if (fd !== undefined) fs.closeSync(fd);
32
+ }
33
+ }
34
+ return { slotId, stamped };
35
+ }
36
+
37
+ if (require.main === module) {
38
+ const args = {};
39
+ for (let index = 2; index < process.argv.length; index += 1) {
40
+ const key = process.argv[index];
41
+ if (key === '--help' || key === '-h') {
42
+ process.stdout.write('Usage: stamp-runtime-title.cjs --target <path> --runtime-dist <path> [--runtime-dir <path>]\n');
43
+ process.exit(0);
44
+ }
45
+ if (!key.startsWith('--') || index + 1 >= process.argv.length) throw new Error(`invalid argument: ${key}`);
46
+ args[key.slice(2)] = process.argv[++index];
47
+ }
48
+ if (!args.target) throw new Error('--target is required');
49
+ if (!args['runtime-dist']) throw new Error('--runtime-dist is required');
50
+ const result = stampRuntimeTitles({
51
+ target: path.resolve(args.target),
52
+ runtimeDist: path.resolve(args['runtime-dist']),
53
+ runtimeDir: args['runtime-dir'],
54
+ });
55
+ if (result.stamped.length > 0) process.stdout.write(`slot title ${result.slotId}: ${result.stamped.join(', ')}\n`);
56
+ }
57
+
58
+ module.exports = { stampRuntimeTitles };
@@ -182,9 +182,15 @@ 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
189
+ if [ -f "$RUNTIME_DIR/recipe-harness-webpack-sync.pid" ]; then
190
+ sync_pid="$(cat "$RUNTIME_DIR/recipe-harness-webpack-sync.pid" 2>/dev/null || true)"
191
+ [ -z "$sync_pid" ] || kill "$sync_pid" >/dev/null 2>&1 || true
192
+ rm -f "$RUNTIME_DIR/recipe-harness-webpack-sync.pid"
193
+ fi
188
194
 
189
195
  # Do not start a second writer for dist/chrome. Slot-owned watches use
190
196
  # webpack.pid; harness-owned watches use recipe-harness-webpack.pid.
@@ -219,6 +225,8 @@ fi
219
225
  # unrelated repo and leave this target validating stale dist/chrome output.
220
226
  watch_pid_file="$RUNTIME_DIR/recipe-harness-webpack.pid"
221
227
  watch_log="$RUNTIME_DIR/recipe-harness-webpack.log"
228
+ sync_pid_file="$RUNTIME_DIR/recipe-harness-webpack-sync.pid"
229
+ sync_log="$RUNTIME_DIR/recipe-harness-webpack-sync.log"
222
230
  rm -f "$watch_pid_file"
223
231
  : > "$watch_log"
224
232
  LIB_DIR=""
@@ -229,11 +237,16 @@ unset _lib
229
237
  echo "[recipe-harness] Starting yarn start (watcher-port=${WATCHER_PORT:-default})"
230
238
  echo "[recipe-harness] compact build view - full log: $watch_log (mm-harness logs --full)"
231
239
  started_pid=""
232
- # Launch the watcher detached writing to the log; the tmux window is a read-only tail viewer.
233
240
  path_prefix="${REPO_NODE_PATH_PREFIX:-}"
234
- nohup env -u BUNDLED_DEBUGPY_PATH ${path_prefix:+PATH="${path_prefix}${PATH}"} ${WATCHER_PORT:+PORT="$WATCHER_PORT"} yarn start > "$watch_log" 2>&1 &
235
- started_pid=$!
236
- printf '%s\n' "$started_pid" > "$watch_pid_file"
241
+ launcher="$SCRIPT_DIR/launch-webpack.cjs"
242
+ [ -f "$launcher" ] || { echo "launch-webpack.cjs missing from harness; run: mm-harness install" >&2; exit 1; }
243
+ sync_script="$SCRIPT_DIR/sync-webpack-dist.cjs"
244
+ [ -f "$sync_script" ] || { echo "sync-webpack-dist.cjs missing from harness; run: mm-harness install" >&2; exit 1; }
245
+ launch_args=(--target "$PWD" --log "$watch_log" --pid-file "$watch_pid_file" --sync-script "$sync_script" --sync-pid-file "$sync_pid_file" --sync-log "$sync_log" --dist "$PWD/dist/chrome" --runtime-dist "$PWD/$RUNTIME_DIR/${RECIPE_RUNTIME_DIST_DIR:-runtime-dist}")
246
+ [ -z "$WATCHER_PORT" ] || launch_args+=(--port "$WATCHER_PORT")
247
+ [ -z "$path_prefix" ] || launch_args+=(--path-prefix "$path_prefix")
248
+ env -u BUNDLED_DEBUGPY_PATH node "$launcher" "${launch_args[@]}" >/dev/null
249
+ started_pid="$(cat "$watch_pid_file")"
237
250
  # The orchestrator names the session; the harness only populates windows. Resolve
238
251
  # via the run-owned ladder (RECIPE_TMUX_SESSION → agentic-runtime.json session) —
239
252
  # no current-session fallback or slot-number derivation.
@@ -52,5 +52,5 @@ ps -axo pid=,command= 2>/dev/null | while read -r pid command; do
52
52
  esac
53
53
  done
54
54
 
55
- rm -f "$runtime_dir/console.tmux" "$runtime_dir/webpack.tmux"
55
+ rm -f "$runtime_dir/console.tmux" "$runtime_dir/webpack.tmux" "$runtime_dir/console-tail.pid"
56
56
  exit 0
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const { spawnSync } = require('node:child_process');
5
+ const fs = require('node:fs');
6
+ const path = require('node:path');
7
+ const { stampRuntimeTitles } = require('./stamp-runtime-title.cjs');
8
+
9
+ const args = {};
10
+ for (let index = 2; index < process.argv.length; index += 1) {
11
+ const key = process.argv[index];
12
+ if (key === '--help' || key === '-h') {
13
+ process.stdout.write('Usage: sync-webpack-dist.cjs --watcher-pid <pid> --watch-log <path> --target <path> --dist <path> --runtime-dist <path> --pid-file <path>\n');
14
+ process.exit(0);
15
+ }
16
+ if (!key.startsWith('--') || index + 1 >= process.argv.length) throw new Error(`invalid argument: ${key}`);
17
+ args[key.slice(2)] = process.argv[++index];
18
+ }
19
+ for (const required of ['watcher-pid', 'watch-log', 'target', 'dist', 'runtime-dist', 'pid-file']) {
20
+ if (!args[required]) throw new Error(`--${required} is required`);
21
+ }
22
+
23
+ const watcherPid = Number(args['watcher-pid']);
24
+ if (!Number.isInteger(watcherPid) || watcherPid <= 0) throw new Error('--watcher-pid must be a positive integer');
25
+
26
+ const watchLog = path.resolve(args['watch-log']);
27
+ const dist = path.resolve(args.dist);
28
+ const runtimeDist = path.resolve(args['runtime-dist']);
29
+ const pidFile = path.resolve(args['pid-file']);
30
+ const compilePattern = /compiled successfully|compiled with \d+ warning|MetaMask .* compiled|Bundle end: service worker|Bundle end:.*app-init/u;
31
+ let offset = 0;
32
+ let carry = '';
33
+ let timer;
34
+
35
+ function processAlive(pid) {
36
+ try {
37
+ process.kill(pid, 0);
38
+ return true;
39
+ } catch {
40
+ return false;
41
+ }
42
+ }
43
+
44
+ function removeOwnPidFile() {
45
+ try {
46
+ if (fs.readFileSync(pidFile, 'utf8').trim() === String(process.pid)) fs.rmSync(pidFile, { force: true });
47
+ } catch {
48
+ // The launcher may have already removed or replaced it.
49
+ }
50
+ }
51
+
52
+ function syncRuntimeDist() {
53
+ if (!fs.existsSync(path.join(dist, 'manifest.json')) || !fs.existsSync(runtimeDist)) return;
54
+ const result = spawnSync('rsync', ['-a', '--delete', '--delay-updates', '--exclude', '_metadata', `${dist}${path.sep}`, `${runtimeDist}${path.sep}`], {
55
+ encoding: 'utf8',
56
+ });
57
+ if (result.status !== 0) {
58
+ process.stderr.write(`runtime-dist sync failed: ${(result.stderr || result.stdout || `rsync exited ${result.status}`).trim()}\n`);
59
+ return;
60
+ }
61
+ stampRuntimeTitles({ target: path.resolve(args.target), runtimeDist });
62
+ process.stdout.write(`runtime-dist updated after webpack compile (${new Date().toISOString()})\n`);
63
+ }
64
+
65
+ function poll() {
66
+ if (!processAlive(watcherPid)) {
67
+ removeOwnPidFile();
68
+ process.exit(0);
69
+ }
70
+ let fd;
71
+ try {
72
+ fd = fs.openSync(watchLog, 'r');
73
+ const size = fs.fstatSync(fd).size;
74
+ if (size < offset) {
75
+ offset = 0;
76
+ carry = '';
77
+ }
78
+ if (size > offset) {
79
+ const length = size - offset;
80
+ const chunk = Buffer.alloc(length);
81
+ const bytesRead = fs.readSync(fd, chunk, 0, length, offset);
82
+ offset += bytesRead;
83
+ const text = carry + chunk.subarray(0, bytesRead).toString('utf8');
84
+ const lines = text.split(/\r?\n/u);
85
+ carry = lines.pop() || '';
86
+ if (lines.some((line) => compilePattern.test(line))) syncRuntimeDist();
87
+ }
88
+ } catch (error) {
89
+ if (error?.code !== 'ENOENT') process.stderr.write(`runtime-dist sync monitor: ${error.message}\n`);
90
+ } finally {
91
+ if (fd !== undefined) fs.closeSync(fd);
92
+ }
93
+ timer = setTimeout(poll, 250);
94
+ }
95
+
96
+ fs.mkdirSync(path.dirname(pidFile), { recursive: true });
97
+ process.on('SIGTERM', () => {
98
+ clearTimeout(timer);
99
+ removeOwnPidFile();
100
+ process.exit(0);
101
+ });
102
+ process.on('SIGINT', () => {
103
+ clearTimeout(timer);
104
+ removeOwnPidFile();
105
+ process.exit(0);
106
+ });
107
+ poll();
@@ -58,7 +58,7 @@ if ! command -v harness_root >/dev/null 2>&1; then
58
58
  exit 1
59
59
  fi
60
60
 
61
- # Safe task-local --out resolution (kept from the legacy extension path.sh).
61
+ # Safe task-local --out resolution.
62
62
  resolve_harness_out() {
63
63
  local target="$1"
64
64
  local out="$2"
@@ -434,7 +434,7 @@ if [ "$STATIC_ONLY" = false ]; then
434
434
 
435
435
  if (
436
436
  cd "$TARGET"
437
- "$RUNNER_BIN" run "$SMOKE_RECIPE" --adapter extension --project-root "$TARGET" --cdp-port "$CDP_PORT" --artifacts-dir "$ARTIFACTS/runner-smoke" --json
437
+ "$RUNNER_BIN" run "$SMOKE_RECIPE" --adapter extension --target "$TARGET" --cdp-port "$CDP_PORT" --artifacts-dir "$ARTIFACTS/runner-smoke" --json
438
438
  ) > "$ARTIFACTS/logs/runner-smoke.log" 2>&1; then
439
439
  checks+=("{\"name\":\"runner v1 smoke\",\"status\":\"pass\"}")
440
440
  else