@deeeed/metamask-harness 0.14.4 → 0.14.5

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 (53) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/adapters/core/cleanup.sh +0 -0
  3. package/adapters/core/inject.sh +0 -0
  4. package/adapters/extension/cleanup.mjs +0 -0
  5. package/adapters/extension/ensure-browser.sh +20 -4
  6. package/adapters/extension/inject.mjs +1 -0
  7. package/adapters/extension/launch-browser.cjs +0 -0
  8. package/adapters/extension/launch.sh +0 -0
  9. package/adapters/extension/lib/slot-title.cjs +294 -0
  10. package/adapters/extension/live.sh +0 -0
  11. package/adapters/extension/readiness.mjs +7 -28
  12. package/adapters/extension/reattach.sh +13 -15
  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 +0 -0
  18. package/adapters/extension/stop-viewers.sh +0 -0
  19. package/adapters/extension/verify.sh +0 -0
  20. package/adapters/extension/wallet-fixture-state.cjs +0 -0
  21. package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +0 -0
  22. package/adapters/mobile/bridge-runtime/setup-wallet.sh +0 -0
  23. package/adapters/mobile/cleanup.sh +0 -0
  24. package/adapters/mobile/inject.sh +0 -0
  25. package/adapters/mobile/lib/metro-listener.sh +0 -0
  26. package/adapters/mobile/lib/tmux-viewer.sh +0 -0
  27. package/adapters/mobile/open-device.sh +0 -0
  28. package/adapters/mobile/prewarm-bundle.sh +0 -0
  29. package/adapters/mobile/start-metro.sh +0 -0
  30. package/adapters/mobile/stop-metro.sh +0 -0
  31. package/adapters/mobile/verify.sh +0 -0
  32. package/adapters/mobile/wait-for-bridge.sh +0 -0
  33. package/adapters/mobile/yarn-setup.sh +0 -0
  34. package/adapters/shared/activate-repo-node.sh +0 -0
  35. package/adapters/shared/activate-repo-ruby.sh +0 -0
  36. package/adapters/shared/cli-ux.sh +0 -0
  37. package/adapters/shared/ensure-runner-deps.sh +0 -0
  38. package/adapters/shared/harness-path.sh +0 -0
  39. package/adapters/shared/hash-helpers.sh +0 -0
  40. package/adapters/shared/json-field.sh +0 -0
  41. package/adapters/shared/open-log-window.sh +0 -0
  42. package/adapters/shared/reap-checkout-metros.sh +0 -0
  43. package/adapters/shared/resolve-farmslot-ports.mjs +0 -0
  44. package/adapters/shared/resolve-farmslot-ports.sh +0 -0
  45. package/adapters/shared/resolve-slot-ports.mjs +0 -0
  46. package/adapters/shared/resolve-slot-ports.sh +0 -0
  47. package/adapters/shared/sync-wallet-fixture.sh +0 -0
  48. package/adapters/shared/tmux-session.sh +0 -0
  49. package/adapters/shared/tmux-viewer.sh +0 -0
  50. package/dist/adapters/extension/ensure-ready.js +19 -0
  51. package/package.json +4 -3
  52. package/scripts/completions.sh +0 -0
  53. package/scripts/install-completions.sh +0 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.14.5 - 2026-07-10
6
+
7
+ ### Fixed
8
+ - Extension `ensure-browser` closes disposable New Tab / blank / extensions pages after reopen, matching reattach/ensure-ready hygiene.
9
+ - Extension slot browser titles are stamped through one shared helper (`adapters/extension/lib/slot-title.cjs`) with a persistent `MutationObserver`, and re-applied after `ensureExtensionReady` open/prune/reopen so relaunch and recipe paths keep `<slot-id> — MetaMask` instead of a bare MetaMask window.
10
+ - Extension slot-title contract test is hermetic (local CDP/WebSocket stub + fake DOM); Playwright and CDP stamps share one function source so they cannot drift.
11
+ - Extension inject copies `scripts/lib/slot-title.cjs` with the other installed helpers so reattach/readiness/reopen do not `MODULE_NOT_FOUND` on injected checkouts.
12
+
5
13
  ## 0.14.4 - 2026-07-09
6
14
 
7
15
  ### Fixed
File without changes
File without changes
File without changes
@@ -358,8 +358,9 @@ const resumeWebpack = () => {
358
358
  const page = ctx.pages().find(p => p.url().includes('chrome-extension://')) || ctx.pages()[0];
359
359
  await page.goto(homeUrl, { waitUntil: 'load', timeout: 15000 });
360
360
 
361
- // Set window title to slot ID
362
- await page.evaluate((id) => { document.title = id + ' \u2014 ' + document.title; }, SLOT_ID);
361
+ // Set window title to slot ID (persists across SPA navigations)
362
+ const { applyPersistentSlotTitle } = require(path.join('${SCRIPT_DIR}', 'lib/slot-title.cjs'));
363
+ await page.evaluate(applyPersistentSlotTitle, SLOT_ID);
363
364
 
364
365
  // Wait for runtime state to settle, then unlock if needed.
365
366
  const unlockSelector = '[data-testid=\"unlock-password\"]';
@@ -393,10 +394,25 @@ const resumeWebpack = () => {
393
394
  console.log('[reopen] Screen: ' + page.url());
394
395
  }
395
396
 
396
- // Close extra extension tabs
397
+ // Close extra extension tabs and disposable Chrome pages (New Tab / blank).
397
398
  for (const p of ctx.pages()) {
398
- if (p !== page && p.url().includes('chrome-extension://')) await p.close().catch(() => {});
399
+ if (p === page) continue;
400
+ const url = p.url();
401
+ if (url.includes('chrome-extension://')) {
402
+ await p.close().catch(() => {});
403
+ continue;
404
+ }
405
+ if (
406
+ url === 'chrome://newtab/' ||
407
+ url === 'chrome://new-tab-page/' ||
408
+ url.startsWith('chrome://new-tab-page') ||
409
+ url === 'about:blank' ||
410
+ url === 'chrome://extensions/'
411
+ ) {
412
+ await p.close().catch(() => {});
413
+ }
399
414
  }
415
+ await page.bringToFront().catch(() => {});
400
416
 
401
417
  fs.writeFileSync(path.join(AGENT_DIR, 'extension.id'), extId);
402
418
  console.log('[reopen] Ready \u2014 ' + SLOT_ID + (CDP_PORT ? ' CDP:' + CDP_PORT : ''));
@@ -130,6 +130,7 @@ copyFile(path.join(runnerDir, 'adapters/shared/tmux-session.sh'), path.join(harn
130
130
  copyFile(path.join(runnerDir, 'adapters/shared/tmux-viewer.sh'), path.join(harnessDir, 'scripts/lib/tmux-viewer.sh'));
131
131
  copyFile(path.join(runnerDir, 'adapters/shared/log-tui.mjs'), path.join(harnessDir, 'scripts/lib/log-tui.mjs'));
132
132
  copyFile(path.join(runnerDir, 'adapters/extension/lib/extension-id.cjs'), path.join(harnessDir, 'scripts/lib/extension-id.cjs'));
133
+ copyFile(path.join(runnerDir, 'adapters/extension/lib/slot-title.cjs'), path.join(harnessDir, 'scripts/lib/slot-title.cjs'));
133
134
  makeExecutableTree(path.join(harnessDir, 'scripts'));
134
135
  fs.writeFileSync(path.join(harnessDir, 'installed-scripts.sha256'), `${dirContentHash(path.join(harnessDir, 'scripts'))}\n`);
135
136
 
File without changes
File without changes
@@ -0,0 +1,294 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Operator slot title for headed extension Chrome.
5
+ *
6
+ * Chrome's window label is the active tab's document.title. Farmslot stamps
7
+ * home.html to "<slot-id> — MetaMask" so operators can tell which slot owns a
8
+ * window when several farms run side by side.
9
+ *
10
+ * Stamping is best-effort: missing slot context or an uninspectable tab must
11
+ * never fail launch/readiness. A MutationObserver keeps the prefix across
12
+ * MetaMask SPA title resets.
13
+ */
14
+
15
+ const fs = require('node:fs');
16
+ const http = require('node:http');
17
+ const path = require('node:path');
18
+
19
+ function sanitizeSlotId(value) {
20
+ const slotId = typeof value === 'string' ? value.trim() : '';
21
+ return /^[A-Za-z0-9._:-]{1,64}$/u.test(slotId) ? slotId : '';
22
+ }
23
+
24
+ function readSlotId(target, runtimeDir) {
25
+ for (const key of ['RECIPE_SLOT_ID', 'SLOT_ID', 'FARMSLOT_SLOT_ID']) {
26
+ const value = sanitizeSlotId(process.env[key]);
27
+ if (value) return value;
28
+ }
29
+ if (!target || !runtimeDir) return '';
30
+ try {
31
+ const runtimeContext = JSON.parse(
32
+ fs.readFileSync(path.join(target, runtimeDir, 'agentic-runtime.json'), 'utf8'),
33
+ );
34
+ return sanitizeSlotId(runtimeContext.slotId);
35
+ } catch {
36
+ // Standalone harness users do not need a custom browser title.
37
+ return '';
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Playwright page.evaluate callback. Must stay a plain function (no closure)
43
+ * so Playwright can serialize it into the page.
44
+ */
45
+ function applyPersistentSlotTitle(slot) {
46
+ // Inline sanitize — this function is serialized into the page for Playwright
47
+ // and CDP, so it cannot call Node helpers.
48
+ const id =
49
+ typeof slot === 'string' && /^[A-Za-z0-9._:-]{1,64}$/u.test(slot.trim())
50
+ ? slot.trim()
51
+ : '';
52
+ if (!id) return document.title;
53
+ window.__farmslotSlotId = id;
54
+
55
+ const stripSlotPrefixes = (title) => {
56
+ let base = String(title || 'MetaMask');
57
+ // Accept em dash, en dash, or hyphen — historical stampers mixed them.
58
+ for (let i = 0; i < 32; i += 1) {
59
+ const stripped = base.replace(/^.+?\s+[—–-]\s+/u, '');
60
+ if (stripped === base) break;
61
+ base = stripped;
62
+ }
63
+ return base || 'MetaMask';
64
+ };
65
+
66
+ const desiredTitle = () => {
67
+ const id = window.__farmslotSlotId;
68
+ if (!id) return document.title;
69
+ return `${id} — ${stripSlotPrefixes(document.title)}`;
70
+ };
71
+
72
+ const setTitle = () => {
73
+ if (window.__farmslotSlotTitleLock) {
74
+ window.__farmslotSlotTitlePending = true;
75
+ return;
76
+ }
77
+ const next = desiredTitle();
78
+ if (document.title === next) return;
79
+ window.__farmslotSlotTitleLock = true;
80
+ try {
81
+ document.title = next;
82
+ } finally {
83
+ // Release after the title mutation is delivered so the observer cannot
84
+ // re-enter and stack prefixes while React also mutates the DOM. If a
85
+ // mutation arrived while locked, re-apply once after unlock.
86
+ queueMicrotask(() => {
87
+ window.__farmslotSlotTitleLock = false;
88
+ if (window.__farmslotSlotTitlePending) {
89
+ window.__farmslotSlotTitlePending = false;
90
+ setTitle();
91
+ }
92
+ });
93
+ }
94
+ };
95
+
96
+ setTitle();
97
+ if (!window.__farmslotSlotTitleObserver) {
98
+ let node = document.querySelector('title');
99
+ if (!node) {
100
+ node = document.createElement('title');
101
+ (document.head || document.documentElement).appendChild(node);
102
+ }
103
+ window.__farmslotSlotTitleObserver = new MutationObserver(setTitle);
104
+ // Observe only the <title> element — watching head/documentElement with
105
+ // subtree:true re-fires on every React DOM mutation and can livelock CDP.
106
+ window.__farmslotSlotTitleObserver.observe(node, {
107
+ childList: true,
108
+ characterData: true,
109
+ subtree: true,
110
+ });
111
+ }
112
+ return document.title;
113
+ }
114
+
115
+ function buildStampExpression(slotId) {
116
+ const slot = sanitizeSlotId(slotId);
117
+ if (!slot) return 'document.title';
118
+ // Single source of truth: serialize the Playwright callback for CDP evaluate.
119
+ return `(${applyPersistentSlotTitle.toString()})(${JSON.stringify(slot)})`;
120
+ }
121
+
122
+ function assertLocalUrl(url) {
123
+ if (!/^http:\/\/127\.0\.0\.1:\d+\//u.test(url)) {
124
+ throw new Error(`slot-title: refusing non-local URL: ${url}`);
125
+ }
126
+ }
127
+
128
+ function assertLocalWebSocketUrl(url) {
129
+ if (!/^ws:\/\/(?:127\.0\.0\.1|localhost):\d+\//u.test(String(url))) {
130
+ throw new Error(`slot-title: refusing non-local websocket URL: ${url}`);
131
+ }
132
+ }
133
+
134
+ function httpJson(url, timeoutMs = 3000) {
135
+ assertLocalUrl(url);
136
+ return new Promise((resolve, reject) => {
137
+ const req = http.get(url, (res) => {
138
+ let data = '';
139
+ res.setEncoding('utf8');
140
+ res.on('data', (chunk) => {
141
+ data += chunk;
142
+ });
143
+ res.on('end', () => {
144
+ try {
145
+ resolve(JSON.parse(data));
146
+ } catch (err) {
147
+ reject(new Error(`invalid JSON from ${url}: ${err.message}`));
148
+ }
149
+ });
150
+ });
151
+ req.setTimeout(timeoutMs, () => {
152
+ req.destroy(new Error(`timeout from ${url}`));
153
+ });
154
+ req.on('error', reject);
155
+ });
156
+ }
157
+
158
+ function resolveWebSocket(target) {
159
+ try {
160
+ // Prefer the target checkout's ws, then the harness install.
161
+ const { createRequire } = require('node:module');
162
+ const req = createRequire(__filename);
163
+ if (target) {
164
+ try {
165
+ return req(req.resolve('ws', { paths: [target] }));
166
+ } catch {
167
+ // Fall through to harness / global.
168
+ }
169
+ }
170
+ return req('ws');
171
+ } catch {
172
+ return typeof WebSocket === 'function' ? WebSocket : null;
173
+ }
174
+ }
175
+
176
+ async function cdpEvaluate(target, webSocketDebuggerUrl, expression, timeoutMs = 5000) {
177
+ assertLocalWebSocketUrl(webSocketDebuggerUrl);
178
+ const WebSocketImpl = resolveWebSocket(target);
179
+ if (!WebSocketImpl) return { skipped: true, reason: 'WebSocket unavailable' };
180
+ return new Promise((resolve, reject) => {
181
+ const ws = new WebSocketImpl(webSocketDebuggerUrl);
182
+ const timer = setTimeout(() => {
183
+ try {
184
+ ws.close();
185
+ } catch {
186
+ // Best-effort timeout cleanup.
187
+ }
188
+ reject(new Error('timeout evaluating extension page via CDP'));
189
+ }, timeoutMs);
190
+ const onOpen = () => {
191
+ ws.send(
192
+ JSON.stringify({
193
+ id: 1,
194
+ method: 'Runtime.evaluate',
195
+ params: { expression, awaitPromise: true, returnByValue: true },
196
+ }),
197
+ );
198
+ };
199
+ const onMessage = (event) => {
200
+ const raw = event?.data ?? event;
201
+ const msg = JSON.parse(Buffer.isBuffer(raw) ? raw.toString('utf8') : String(raw));
202
+ if (msg.id !== 1) return;
203
+ clearTimeout(timer);
204
+ ws.close();
205
+ if (msg.error) {
206
+ reject(new Error(msg.error.message || JSON.stringify(msg.error)));
207
+ return;
208
+ }
209
+ resolve(msg.result?.result?.value ?? null);
210
+ };
211
+ const onError = (err) => {
212
+ clearTimeout(timer);
213
+ reject(new Error(`CDP websocket error while stamping slot title: ${err?.message || err || 'unknown'}`));
214
+ };
215
+ if (typeof ws.on === 'function') {
216
+ ws.on('open', onOpen);
217
+ ws.on('message', onMessage);
218
+ ws.on('error', onError);
219
+ } else {
220
+ ws.addEventListener('open', onOpen);
221
+ ws.addEventListener('message', onMessage);
222
+ ws.addEventListener('error', onError);
223
+ }
224
+ });
225
+ }
226
+
227
+ function isHomePage(target, extensionId) {
228
+ if (!target || target.type !== 'page' || typeof target.url !== 'string') return false;
229
+ if (!target.url.startsWith(`chrome-extension://${extensionId}/`)) return false;
230
+ return target.url.includes('/home.html');
231
+ }
232
+
233
+ /**
234
+ * Stamp every inspectable home.html tab for this extension over CDP.
235
+ * Attached tabs (no webSocketDebuggerUrl) are skipped — another client owns them.
236
+ */
237
+ async function stampHomeTabsViaCdp(options) {
238
+ const {
239
+ target = process.cwd(),
240
+ cdpPort,
241
+ extensionId,
242
+ slotId: slotIdOption,
243
+ runtimeDir,
244
+ } = options || {};
245
+ const slotId = sanitizeSlotId(slotIdOption) || readSlotId(target, runtimeDir);
246
+ const empty = { slotId: slotId || '', stamped: 0, skipped: 0, titles: [] };
247
+ if (!slotId || !cdpPort || !extensionId) return empty;
248
+
249
+ let targets;
250
+ try {
251
+ targets = await httpJson(`http://127.0.0.1:${cdpPort}/json/list`);
252
+ } catch {
253
+ return empty;
254
+ }
255
+ if (!Array.isArray(targets)) return empty;
256
+
257
+ const homes = targets.filter((t) => isHomePage(t, extensionId));
258
+ const titles = [];
259
+ let stamped = 0;
260
+ let skipped = 0;
261
+ const expression = buildStampExpression(slotId);
262
+
263
+ for (const home of homes) {
264
+ if (typeof home.webSocketDebuggerUrl !== 'string') {
265
+ skipped += 1;
266
+ continue;
267
+ }
268
+ try {
269
+ const title = await cdpEvaluate(target, home.webSocketDebuggerUrl, expression);
270
+ if (title && typeof title === 'object' && title.skipped) {
271
+ skipped += 1;
272
+ continue;
273
+ }
274
+ if (typeof title === 'string') {
275
+ titles.push(title);
276
+ stamped += 1;
277
+ } else {
278
+ skipped += 1;
279
+ }
280
+ } catch {
281
+ skipped += 1;
282
+ }
283
+ }
284
+
285
+ return { slotId, stamped, skipped, titles };
286
+ }
287
+
288
+ module.exports = {
289
+ sanitizeSlotId,
290
+ readSlotId,
291
+ applyPersistentSlotTitle,
292
+ buildStampExpression,
293
+ stampHomeTabsViaCdp,
294
+ };
File without changes
@@ -22,6 +22,10 @@ import { fileURLToPath } from 'node:url';
22
22
 
23
23
  const require = createRequire(import.meta.url);
24
24
  const scriptDir = path.dirname(fileURLToPath(import.meta.url));
25
+ const {
26
+ readSlotId: readSlotIdFromLib,
27
+ buildStampExpression,
28
+ } = require(path.join(scriptDir, 'lib/slot-title.cjs'));
25
29
 
26
30
  function pathDefault(key) {
27
31
  for (const candidate of [
@@ -101,25 +105,7 @@ function readExpectedExtensionId(target) {
101
105
  }
102
106
 
103
107
  function readSlotId(target) {
104
- for (const key of ['RECIPE_SLOT_ID', 'SLOT_ID', 'FARMSLOT_SLOT_ID']) {
105
- const value = sanitizeSlotId(process.env[key]);
106
- if (value) return value;
107
- }
108
- try {
109
- const runtimeContext = JSON.parse(fs.readFileSync(path.join(target, recipeRuntimeDir(), 'agentic-runtime.json'), 'utf8'));
110
- const value = sanitizeSlotId(runtimeContext.slotId);
111
- if (value) {
112
- return value;
113
- }
114
- } catch {
115
- // No slot context: standalone harness users do not need a custom browser title.
116
- }
117
- return '';
118
- }
119
-
120
- function sanitizeSlotId(value) {
121
- const slotId = typeof value === 'string' ? value.trim() : '';
122
- return /^[A-Za-z0-9._:-]{1,64}$/u.test(slotId) ? slotId : '';
108
+ return readSlotIdFromLib(target, recipeRuntimeDir());
123
109
  }
124
110
 
125
111
  function writeExtensionId(target, extensionId) {
@@ -407,16 +393,9 @@ async function inspectCdp(target, cdpPort, expectedExtensionId, expectedServiceW
407
393
  const stampedTitle = await cdpEvaluate(
408
394
  target,
409
395
  pageTarget.webSocketDebuggerUrl,
410
- `(() => {
411
- const slot = ${JSON.stringify(slotId)};
412
- const current = document.title || 'MetaMask';
413
- const base = current.replace(/^.+?\\s+—\\s+/, '') || 'MetaMask';
414
- const next = slot + ' — ' + base;
415
- if (document.title !== next) document.title = next;
416
- return document.title;
417
- })()`,
396
+ buildStampExpression(slotId),
418
397
  );
419
- if (stampedTitle && !stampedTitle.skipped) {
398
+ if (typeof stampedTitle === 'string') {
420
399
  ui.title = stampedTitle;
421
400
  ui.slotTitleApplied = true;
422
401
  }
@@ -123,6 +123,7 @@ let chromium; try { chromium = require('@playwright/test').chromium; } catch { c
123
123
  const fs = require('node:fs');
124
124
  const path = require('node:path');
125
125
  const { extensionIdFromManifestFile } = require(path.join(process.env.SCRIPT_DIR, 'lib/extension-id.cjs'));
126
+ const { readSlotId, applyPersistentSlotTitle } = require(path.join(process.env.SCRIPT_DIR, 'lib/slot-title.cjs'));
126
127
 
127
128
  const port = process.env.CDP_PORT;
128
129
  const target = process.env.TARGET;
@@ -131,22 +132,13 @@ const runtimeDistDir = process.env.RUNTIME_DIST_DIR;
131
132
  const startUrl = process.env.START_URL || '';
132
133
  const settleMs = Number(process.env.SETTLE_MS || '8000') || 8000;
133
134
  const displayMode = process.env.DISPLAY_MODE || 'fullscreen';
134
- const slotId = process.env.RECIPE_SLOT_ID || process.env.SLOT_ID || process.env.FARMSLOT_SLOT_ID || readRuntimeSlotId();
135
+ const slotId = readSlotId(target, runtimeDir);
135
136
 
136
137
  if (!runtimeDir || !runtimeDistDir) {
137
138
  console.error('reattach: resolved runtime paths were not provided.');
138
139
  process.exit(2);
139
140
  }
140
141
 
141
- function readRuntimeSlotId() {
142
- try {
143
- const runtimeContext = JSON.parse(fs.readFileSync(path.join(target, runtimeDir, 'agentic-runtime.json'), 'utf8'));
144
- return typeof runtimeContext.slotId === 'string' ? runtimeContext.slotId.trim() : '';
145
- } catch {
146
- return '';
147
- }
148
- }
149
-
150
142
  function extensionIdFromRuntimeDist() {
151
143
  return extensionIdFromManifestFile(path.join(target, runtimeDir, runtimeDistDir, 'manifest.json'));
152
144
  }
@@ -204,11 +196,7 @@ async function connect() {
204
196
 
205
197
  async function stampSlotTitle(page) {
206
198
  if (!slotId || !page) return;
207
- await page.evaluate((slot) => {
208
- const base = (document.title || 'MetaMask').replace(/^.+?\s+—\s+/, '') || 'MetaMask';
209
- const next = `${slot} — ${base}`;
210
- if (document.title !== next) document.title = next;
211
- }, slotId).catch(() => {
199
+ await page.evaluate(applyPersistentSlotTitle, slotId).catch(() => {
212
200
  // Best-effort operator affordance; reload success is verified by CDP state.
213
201
  });
214
202
  }
@@ -285,6 +273,16 @@ async function closePages(context, predicate) {
285
273
  .catch(() => { /* wallet home is best-effort; the reload already applied */ });
286
274
  }
287
275
  await stampSlotTitle(home);
276
+ for (const page of allPages(context)) {
277
+ const url = page.url();
278
+ if (
279
+ page !== home &&
280
+ url.startsWith(`chrome-extension://${extId}/`) &&
281
+ url.includes('/home.html')
282
+ ) {
283
+ await stampSlotTitle(page);
284
+ }
285
+ }
288
286
  await closeDisposableChromeTabs(context);
289
287
  if (startUrl) {
290
288
  const dapp = allPages(context)
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
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
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
@@ -1,5 +1,11 @@
1
+ import { createRequire } from "node:module";
1
2
  import path from "node:path";
3
+ import { recipeRuntimeDir, runnerDir } from "../../paths.js";
2
4
  import { resolveExtensionId } from "./extension-id.js";
5
+ const requireFromHarness = createRequire(import.meta.url);
6
+ const { stampHomeTabsViaCdp } = requireFromHarness(
7
+ path.join(runnerDir, "adapters/extension/lib/slot-title.cjs")
8
+ );
3
9
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
4
10
  async function jsonList(port) {
5
11
  for (let attempt = 0; attempt < 3; attempt += 1) {
@@ -161,12 +167,25 @@ async function ensureExtensionReady(target, options) {
161
167
  health = await checkHealth();
162
168
  }
163
169
  }
170
+ let slotTitle;
171
+ if (after >= 1) {
172
+ const stamp = await stampHomeTabsViaCdp({
173
+ target: resolved,
174
+ cdpPort,
175
+ extensionId,
176
+ runtimeDir: recipeRuntimeDir()
177
+ });
178
+ if (stamp.slotId || stamp.stamped > 0 || stamp.skipped > 0) {
179
+ slotTitle = stamp;
180
+ }
181
+ }
164
182
  const ready = health.status === "PASS" && after === 1;
165
183
  return base({
166
184
  extensionId,
167
185
  opened,
168
186
  action,
169
187
  homeTabs: { before, closed, after },
188
+ ...slotTitle ? { slotTitle } : {},
170
189
  ready,
171
190
  reasonCode: ready ? "ready" : after !== 1 ? "tab-count" : "unhealthy",
172
191
  health
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deeeed/metamask-harness",
3
- "version": "0.14.4",
3
+ "version": "0.14.5",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mm-harness": "bin/mm-harness"
@@ -20,7 +20,8 @@
20
20
  "@farmslot/protocol": "^0.7.6",
21
21
  "@farmslot/recipe-harness": "^0.4.3",
22
22
  "commander": "^12.0.0",
23
- "viem": "^2.54.3"
23
+ "viem": "^2.54.3",
24
+ "ws": "8.21.0"
24
25
  },
25
26
  "resolutions": {
26
27
  "esbuild": "0.28.1",
@@ -69,4 +70,4 @@
69
70
  "url": "https://github.com/MetaMask/experimental-metamask-harness/issues"
70
71
  },
71
72
  "homepage": "https://github.com/MetaMask/experimental-metamask-harness#readme"
72
- }
73
+ }
File without changes
File without changes