@deeeed/metamask-harness 0.57.0 → 0.59.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 CHANGED
@@ -2,6 +2,25 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.59.0 - 2026-09-18
6
+
7
+ ### Changed
8
+
9
+ - Extension `verify` is runner-owned TypeScript: `adapters/extension/verify.sh` (572 lines, with node heredocs assembling `summary.json`, the fixture status and the CDP holder probe) is now `src/adapters/extension/verify.ts`, run in-process by `mm-harness verify --adapter extension`. Same flags, env, exit codes, check names and artifacts (`summary.json`, `artifact-manifest.json`, `logs/`, `runner-smoke/`); a live run against the same checkout produced a byte-identical normalised summary. Deliberate differences: `install` no longer copies a `scripts/verify.sh` into the checkout and dispatch no longer prefers one, so an older overlay cannot shadow the current runner's verify; the two shell-lib preflights (`harness-path.sh`, `json-field.sh`) are gone because the typed helpers cannot be missing; usage now names `mm-harness verify --adapter extension`; a `--target` that does not exist is a named exit-1 refusal instead of a bare `cd` failure; and `live.sh` reaches verify through the checkout's runner delegate, refusing early when that delegate is absent. Three more deliberate differences: `--artifacts-dir` is resolved to an absolute path once (the shell resolved a relative value against the cwd for its own files and against the target for the smoke run); `readiness.mjs` runs under the harness's own node instead of `PATH` `node`; and `live.sh`, having just installed the overlay, tells verify to skip the overlay re-ensure (`MM_HARNESS_SKIP_OVERLAY_ENSURE=1`) instead of reinstalling it between launch and the live probe.
10
+
11
+ ### Fixed
12
+
13
+ - Slot identity resolves against this machine's pool first. Every farm node keeps the same checkout paths, so a repo path matched a slot in every `pool/*.json` and the alphabetically first pool won: `doctor --fix` on macwork stamped `metamask-extension-2` as `macpro-mme-2` on CDP 7662 instead of `macwork-mmedev-2` on 6662. Pools whose `machine` or `host` is this host are consulted first; other machines' pools only when none of ours names the repo. A malformed pool file is skipped like the port resolver already did, and `*.bak` files never count.
14
+ - An invalid `RECIPE_RUNTIME_DIR` no longer crashes every command at startup with a stack trace. The extension runtime decision read it while loading; it reads it when a build-log check needs it, so `--help` still works and the command that needs the value reports it.
15
+
16
+ ## 0.58.0 - 2026-09-18
17
+
18
+ ### Changed
19
+
20
+ - Pins `@farmslot/agent-runtime` 0.12.0, whose `mark` records step names through the protocol's `checklistStepName`, so SIGNAL.json events and `status --watch` name a step identically.
21
+ - Mobile `verify` is runner-owned TypeScript: `adapters/mobile/verify.sh` (557 lines, with node heredocs assembling `summary.json`) is now `src/adapters/mobile/verify.ts`, run in-process by `mm-harness verify --adapter mobile`. Same flags, env, exit codes, check names and artifacts (`summary.json`, `artifact-manifest.json`, `logs/`, smoke recipes, `runner-live-smoke/`); a live run against the same checkout produced a byte-identical normalised summary. `install` no longer copies a `scripts/verify.sh` into the checkout and dispatch no longer prefers one, so an older overlay cannot shadow the current runner's verify; the mobile overlay's `scripts/` tree is gone and its manifest advertises `runtimeHelpers: {}`, like Core. Extension verify is unchanged.
22
+ - The execution-provenance record is indexed into `artifact-manifest.json` through the same hardened manifest writer every other artifact uses (`indexArtifactManifest`), instead of a private copy that skipped a missing or unreadable manifest without a word. A manifest the run said it wrote but cannot be read is now the error it is.
23
+
5
24
  ## 0.57.0 - 2026-09-18
6
25
 
7
26
  ### Changed
@@ -114,7 +114,6 @@ copyFile(path.join(runnerDir, 'adapters/extension/sidepanel-toggle.sh'), path.jo
114
114
  copyFile(path.join(runnerDir, 'adapters/extension/reattach.sh'), path.join(harnessDir, 'scripts/reattach.sh'));
115
115
  copyFile(path.join(runnerDir, 'adapters/extension/readiness.mjs'), path.join(harnessDir, 'scripts/readiness.mjs'));
116
116
  copyFile(path.join(runnerDir, 'adapters/extension/wallet-fixture-state.cjs'), path.join(harnessDir, 'scripts/wallet-fixture-state.cjs'));
117
- copyFile(path.join(runnerDir, 'adapters/extension/verify.sh'), path.join(harnessDir, 'scripts/verify.sh'));
118
117
  copyFile(path.join(runnerDir, 'adapters/extension/refresh-build.sh'), path.join(harnessDir, 'scripts/refresh-build.sh'));
119
118
  copyFile(path.join(runnerDir, 'adapters/extension/ensure-browser.sh'), path.join(harnessDir, 'scripts/reopen-browser.sh'));
120
119
  copyFile(path.join(runnerDir, 'adapters/shared/harness-path.sh'), path.join(harnessDir, 'scripts/lib/harness-path.sh'));
@@ -6,8 +6,8 @@
6
6
  # Purpose:
7
7
  # Builds the per-run prepare command from the named feature scripts
8
8
  # (watch, dist snapshot, fixture seed, detached chrome, CDP poll), then
9
- # runs launch.sh and, unless --launch-only is set, verify.sh under a
10
- # timestamped artifact dir.
9
+ # runs launch.sh and, unless --launch-only is set, `mm-harness verify` under
10
+ # a timestamped artifact dir.
11
11
  #
12
12
  # Inputs (flags / env):
13
13
  # --target <metamask-extension> (default $PWD)
@@ -128,17 +128,6 @@ case "$CDP_PORT" in
128
128
  *[!0-9]*) echo "Invalid --cdp-port (must be numeric): $CDP_PORT" >&2; exit 2 ;;
129
129
  esac
130
130
 
131
- # verify.sh is a recipe-tree feature: co-located in installed copies, under
132
- # adapters/extension in a repo checkout.
133
- VERIFY_SH=""
134
- for _v in "$SCRIPT_DIR/verify.sh" "$SCRIPT_DIR/../../adapters/extension/verify.sh"; do
135
- [ -f "$_v" ] && { VERIFY_SH="$_v"; break; }
136
- done
137
- unset _v
138
- if [ -z "$VERIFY_SH" ]; then
139
- echo "mm-harness: extension verify.sh not found next to $SCRIPT_DIR; reinstall the runner." >&2
140
- exit 1
141
- fi
142
131
  RUNTIME_DIR="$(recipe_runtime_dir)"
143
132
  RUNTIME_DIST_DIR="${RECIPE_RUNTIME_DIST_DIR:-runtime-dist}"
144
133
  case "$RUNTIME_DIST_DIR" in
@@ -152,6 +141,12 @@ CHROME_PROFILE_NAME="${RECIPE_CHROME_PROFILE_NAME:-chrome-profile}"
152
141
  # to defer the cache-clear DECISION to `runtime-decision` (single source) and to
153
142
  # record the deps/cache baseline after a confirmed-good build.
154
143
  RUNNER_BIN="$(harness_dir "$TARGET" extension)/runner/bin/mm-harness"
144
+ # Verify is runner-owned typed code reached through the runner delegate, not a
145
+ # shipped leaf this sequencer could resolve next to itself.
146
+ if $RUN_VERIFY && [ ! -x "$RUNNER_BIN" ]; then
147
+ echo "mm-harness: runner delegate missing at $RUNNER_BIN; run mm-harness install --adapter extension --target $TARGET." >&2
148
+ exit 1
149
+ fi
155
150
  OUT="${OUT:-$(harness_root)/extension/runner/recipes}"
156
151
  ARTIFACTS="${ARTIFACTS:-$(harness_dir "$TARGET" extension)/live/$(date -u +%Y%m%dT%H%M%SZ)}"
157
152
  mkdir -p "$ARTIFACTS/logs"
@@ -390,7 +385,9 @@ verify_status=0
390
385
  if [ "$launch_status" -eq 0 ] && $RUN_VERIFY; then
391
386
  echo "[recipe-harness] phase 2/2: live verify (readiness + smoke recipe)" >&2
392
387
  set +e
393
- "$VERIFY_SH" --target "$TARGET" --out "$OUT" --cdp-port "$CDP_PORT" --artifacts-dir "$ARTIFACTS/verify"
388
+ # The overlay was installed by this sequencer moments ago; verify must not
389
+ # reinstall it between launch and the live probe.
390
+ MM_HARNESS_SKIP_OVERLAY_ENSURE=1 "$RUNNER_BIN" verify --adapter extension --target "$TARGET" -- --out "$OUT" --cdp-port "$CDP_PORT" --artifacts-dir "$ARTIFACTS/verify"
394
391
  verify_status=$?
395
392
  set -e
396
393
  elif [ "$launch_status" -ne 0 ]; then
@@ -140,9 +140,9 @@
140
140
  },
141
141
  {
142
142
  "id": "mobile/verify",
143
- "entry": "adapters/mobile/verify.sh",
144
- "kind": "bash",
145
- "purpose": "Verify the mobile harness/runtime is present and healthy (no app launch).",
143
+ "entry": "src/adapters/mobile/verify.ts",
144
+ "kind": "module",
145
+ "purpose": "Verify the mobile harness/runtime is present and healthy (no app launch). Runner-owned: mm-harness verify runs it in-process.",
146
146
  "inputs": "--target --artifacts-dir --static-only --platform ios|android --preflight-mode --no-auto-start",
147
147
  "outputs": "verify report + evidence; exit 0/1/2"
148
148
  },
@@ -164,10 +164,10 @@
164
164
  },
165
165
  {
166
166
  "id": "extension/verify",
167
- "entry": "adapters/extension/verify.sh",
168
- "kind": "bash",
169
- "purpose": "Verify the extension harness/runtime is present and healthy (no app launch).",
170
- "inputs": "--target --out --cdp-port --static-only",
167
+ "entry": "src/adapters/extension/verify.ts",
168
+ "kind": "module",
169
+ "purpose": "Verify the extension harness/runtime is present and healthy (no app launch). Runner-owned: mm-harness verify runs it in-process.",
170
+ "inputs": "--target --out --cdp-port --static-only --artifacts-dir",
171
171
  "outputs": "verify report + evidence; exit 0/1/2"
172
172
  },
173
173
  {
@@ -34,7 +34,7 @@ fi
34
34
  HARNESS_ROOT="$(harness_root)"
35
35
  HARNESS_REL="$HARNESS_ROOT/mobile"
36
36
  HARNESS_DIR="$(harness_dir "$TARGET" mobile)"
37
- for rel in "$HARNESS_REL" "$HARNESS_REL/runner" "$HARNESS_REL/scripts"; do
37
+ for rel in "$HARNESS_REL" "$HARNESS_REL/runner"; do
38
38
  current="$TARGET"
39
39
  IFS='/' read -r -a parts <<< "$rel"
40
40
  for part in "${parts[@]}"; do
@@ -45,7 +45,7 @@ for rel in "$HARNESS_REL" "$HARNESS_REL/runner" "$HARNESS_REL/scripts"; do
45
45
  done
46
46
 
47
47
  rm -rf "$HARNESS_DIR/runner" "$HARNESS_DIR/scripts" "$HARNESS_DIR/bridge-runtime"
48
- mkdir -p "$HARNESS_DIR/runner/bin" "$HARNESS_DIR/runner/manifests" "$HARNESS_DIR/runner/recipes" "$HARNESS_DIR/scripts/lib"
48
+ mkdir -p "$HARNESS_DIR/runner/bin" "$HARNESS_DIR/runner/manifests" "$HARNESS_DIR/runner/recipes"
49
49
 
50
50
  RUNNER_EXEC_Q="$(printf '%q' "$RUNNER_DIR/bin/mm-harness")"
51
51
  TARGET_Q="$(printf '%q' "$TARGET")"
@@ -76,10 +76,7 @@ cp "$RUNNER_DIR/library/manifests/extension.action-manifest.json" "$HARNESS_DIR/
76
76
  rsync -a --delete "$RUNNER_DIR/library/recipes/" "$HARNESS_DIR/runner/recipes/"
77
77
  rm -rf "$HARNESS_DIR/runner/flows"
78
78
  rm -f "$HARNESS_DIR/runner/library.json"
79
- cp "$RUNNER_DIR/adapters/mobile/verify.sh" "$HARNESS_DIR/scripts/verify.sh"
80
79
  cp -R "$RUNNER_DIR/adapters/mobile/bridge-runtime" "$HARNESS_DIR/bridge-runtime"
81
- cp "$RUNNER_DIR/adapters/shared/harness-path.sh" "$HARNESS_DIR/scripts/lib/harness-path.sh"
82
- cp "$RUNNER_DIR/adapters/shared/path-defaults.json" "$HARNESS_DIR/scripts/lib/path-defaults.json"
83
80
  chmod -R u+rwX,go+rX "$HARNESS_DIR"
84
81
 
85
82
  add_git_exclude() {
@@ -114,8 +111,8 @@ fs.writeFileSync(manifestPath, JSON.stringify({
114
111
  protocolVersion: 'v1',
115
112
  actionManifestPath: `${harnessRel}/action-manifest.json`,
116
113
  runnerEntrypoint: `${harnessRel}/runner/bin/mm-harness`,
117
- runtimeHelpers: { verify: `${harnessRel}/scripts/verify.sh` },
118
- harnessInstalledPaths: [`${harnessRel}/runner`, `${harnessRel}/scripts`, `${harnessRel}/bridge-runtime`, `${harnessRel}/action-manifest.json`],
114
+ runtimeHelpers: {},
115
+ harnessInstalledPaths: [`${harnessRel}/runner`, `${harnessRel}/bridge-runtime`, `${harnessRel}/action-manifest.json`],
119
116
  productOwnedPaths: ['app/dev-tools/AgenticService', 'app/core/NavigationService/NavigationService.ts'],
120
117
  cleanupCommand,
121
118
  productDiffExcludes: [`:(exclude)${harnessRoot}`],
@@ -73,6 +73,35 @@ function scoreSlot(slot, slotSuffix) {
73
73
  return [score, cdp, port, slotId, simulator, adbSerial, androidDevice];
74
74
  }
75
75
 
76
+ // Pool files that describe this machine, then the rest. Every farm node keeps
77
+ // the same checkout paths, so a repo path alone matches a slot in every pool;
78
+ // the pool whose `machine`/`host` is this host is the one that owns the slot.
79
+ export function poolFilesForHost(poolDir, hostname = os.hostname()) {
80
+ // macOS may report a capitalised or FQDN hostname; pools carry lowercase names.
81
+ const canonical = (value) => String(value ?? '').toLowerCase().replace(/\.$/u, '');
82
+ const full = canonical(hostname);
83
+ const short = full.split('.')[0];
84
+ const names = (value) => {
85
+ const name = canonical(value);
86
+ return name ? [name, name.split('.')[0]] : [];
87
+ };
88
+ const local = [];
89
+ const other = [];
90
+ for (const name of fs.readdirSync(poolDir).sort()) {
91
+ // Backups (`*.json.bak.<ts>`, `*.bak`) never define a live slot.
92
+ if (!name.endsWith('.json') || name.includes('.bak')) continue;
93
+ let data;
94
+ try {
95
+ data = JSON.parse(fs.readFileSync(path.join(poolDir, name), 'utf8'));
96
+ } catch {
97
+ continue;
98
+ }
99
+ const isLocal = [...names(data.machine), ...names(data.host)].some((name) => name === full || name === short);
100
+ (isLocal ? local : other).push({ name, data });
101
+ }
102
+ return { local, other };
103
+ }
104
+
76
105
  export function resolveSlotPortsByRepo(repo) {
77
106
  const realRepo = realRepoPath(repo);
78
107
  if (!realRepo) return null;
@@ -91,29 +120,28 @@ export function resolveSlotPortsByRepo(repo) {
91
120
  for (const root of roots) {
92
121
  if (!root || !fs.existsSync(path.join(root, 'pool'))) continue;
93
122
  const poolDir = path.join(root, 'pool');
123
+ const pools = poolFilesForHost(poolDir);
94
124
  let best = null;
95
125
 
96
- for (const name of fs.readdirSync(poolDir).sort()) {
97
- if (!name.endsWith('.json') || name.includes('.bak.')) continue;
98
- let data;
99
- try {
100
- data = JSON.parse(fs.readFileSync(path.join(poolDir, name), 'utf8'));
101
- } catch {
102
- continue;
103
- }
104
- for (const slot of data.slots ?? []) {
105
- const slotRepo = slot.repo ?? '';
106
- if (!slotRepo) continue;
107
- let slotReal;
108
- try {
109
- slotReal = fs.realpathSync(slotRepo);
110
- } catch {
111
- continue;
126
+ // This host's pools decide first; other machines' pools only when none of
127
+ // ours names the repo (single-node and demo setups).
128
+ for (const group of [pools.local, pools.other]) {
129
+ for (const { data } of group) {
130
+ for (const slot of data.slots ?? []) {
131
+ const slotRepo = slot.repo ?? '';
132
+ if (!slotRepo) continue;
133
+ let slotReal;
134
+ try {
135
+ slotReal = fs.realpathSync(slotRepo);
136
+ } catch {
137
+ continue;
138
+ }
139
+ if (slotReal !== realRepo) continue;
140
+ const scored = scoreSlot(slot, slotSuffix);
141
+ if (!best || scored[0] > best[0]) best = scored;
112
142
  }
113
- if (slotReal !== realRepo) continue;
114
- const scored = scoreSlot(slot, slotSuffix);
115
- if (!best || scored[0] > best[0]) best = scored;
116
143
  }
144
+ if (best) break;
117
145
  }
118
146
 
119
147
  if (best) {
@@ -11,7 +11,6 @@ import { recipeHarnessPath, recipeRuntimeDir, recipeWatchLogCandidates } from ".
11
11
  const WEBPACK_DIRECT_INPUTS = ["package.json", "yarn.lock", ".yarnrc.yml", ".tool-versions"];
12
12
  const WEBPACK_RECURSIVE_INPUTS = ["development/webpack"];
13
13
  const WEBPACK_CACHE_DIR = "node_modules/.cache/webpack";
14
- const WATCH_LOG_CANDIDATES = recipeWatchLogCandidates();
15
14
  function git(target, args) {
16
15
  try {
17
16
  return execFileSync("git", ["-C", target, ...args], {
@@ -95,7 +94,7 @@ function webpackCacheCheck(target) {
95
94
  return { cachePresent: true, status, hasBaseline: true };
96
95
  }
97
96
  function buildLogCheck(target, watchLog) {
98
- const candidates = (watchLog ? [path.isAbsolute(watchLog) ? watchLog : path.join(target, watchLog)] : WATCH_LOG_CANDIDATES.map((rel) => path.join(target, rel))).filter((file) => fs.existsSync(file));
97
+ const candidates = (watchLog ? [path.isAbsolute(watchLog) ? watchLog : path.join(target, watchLog)] : recipeWatchLogCandidates().map((rel) => path.join(target, rel))).filter((file) => fs.existsSync(file));
99
98
  if (!candidates.length) return { status: "no-watch" };
100
99
  const newestFirst = [...candidates].sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs);
101
100
  const lines = fs.readFileSync(newestFirst[0], "utf8").split("\n");
@@ -3,6 +3,7 @@ import fs from "node:fs";
3
3
  import { createRequire } from "node:module";
4
4
  import path from "node:path";
5
5
  import { importRecipeHarnessRuntimeBrowserExtension, importRecipeHarnessRuntimeCdp, recipeHarnessPath, recipeRuntimeDir, resolveLocalProtocolRoot, runnerDir } from "../../paths.js";
6
+ import { poolFilesForHost } from "../resolve-slot-ports.js";
6
7
  import { profileProcessPids } from "./validation-process-ownership.js";
7
8
  const require2 = createRequire(import.meta.url);
8
9
  const { detachedLaunchUnprovenPath, hasDetachedLaunchUnproven, shellQuote } = require2(
@@ -543,9 +544,9 @@ function resolveExtensionSlot(projectRoot, requestedSlot) {
543
544
  }
544
545
  const poolsDir = path.join(protocolRoot, "pool");
545
546
  if (!fs.existsSync(poolsDir)) return null;
546
- for (const file of fs.readdirSync(poolsDir)) {
547
- if (!file.endsWith(".json") || file.includes(".bak")) continue;
548
- const pool = JSON.parse(fs.readFileSync(path.join(poolsDir, file), "utf8"));
547
+ const pools = poolFilesForHost(poolsDir);
548
+ for (const { data } of [...pools.local, ...pools.other]) {
549
+ const pool = data;
549
550
  for (const rawSlot of pool.slots ?? []) {
550
551
  if (!rawSlot || typeof rawSlot !== "object") continue;
551
552
  const slot = rawSlot;