@deeeed/metamask-harness 0.50.3 → 0.50.4

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,12 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.50.4 - 2026-09-10
6
+
7
+ - Distinguish Extension startup/provider absence from attempted RPC failures and preserve diagnosed failed tabs during verification.
8
+ - Bind Mobile native Perps observations to a stable development navigation route when available, and include route/screen context in surface errors.
9
+ - Add an interactive workflow map and sourced template previews to the human tutorial, connecting tasks, skills, checklists, recipes, evidence, and optional Farmslot coordination.
10
+
5
11
  ## 0.50.3 - 2026-09-09
6
12
 
7
13
  - Recognize enabled form controls through recipe-harness 0.16.1; report Extension startup crashes without reload-based RPC probes or automatic relaunch recovery.
@@ -148,7 +148,7 @@ async function ensureExtensionReady(target, options) {
148
148
  }
149
149
  };
150
150
  let health = await checkHealth();
151
- if (action === "pruned" && (health.status !== "PASS" || after !== 1)) {
151
+ if (action === "pruned" && !health.errorCode && (health.status !== "PASS" || after !== 1)) {
152
152
  const listing = await jsonList(cdpPort);
153
153
  if (listing.ok) {
154
154
  const existingHomeIds = new Set(
@@ -442,7 +442,7 @@ async function decideExtensionReadiness(target, options = {}) {
442
442
  actions: []
443
443
  },
444
444
  {
445
- when: cdp.errorCode === "EXTENSION_BACKGROUND_UNRESPONSIVE",
445
+ when: cdp.errorCode === "EXTENSION_BACKGROUND_UNRESPONSIVE" || cdp.errorCode === "EXTENSION_STARTUP_UNVERIFIED",
446
446
  decision: "blocked",
447
447
  reasonCode: "background-unresponsive",
448
448
  reasons: [
@@ -503,7 +503,7 @@ async function decideExtensionReadiness(target, options = {}) {
503
503
  actions: []
504
504
  },
505
505
  {
506
- when: cdp.errorCode === "EXTENSION_BACKGROUND_UNRESPONSIVE",
506
+ when: cdp.errorCode === "EXTENSION_BACKGROUND_UNRESPONSIVE" || cdp.errorCode === "EXTENSION_STARTUP_UNVERIFIED",
507
507
  decision: "blocked",
508
508
  reasonCode: "background-unresponsive",
509
509
  reasons: ["Extension startup failed; inspect the background error before relaunching or resetting wallet state.", ...cdp.findings?.slice(0, 3) ?? []],
@@ -151,16 +151,17 @@ async function checkExtensionRuntimeHealth(projectRoot, cdpPort, options = {}) {
151
151
  };
152
152
  }
153
153
  const runtime = await evaluateHealth(session, cdpCallTimeoutMs);
154
- if (runtime.backgroundUnresponsive === true) {
154
+ if (runtime.backgroundUnresponsive === true || runtime.hasPageContent === false) {
155
+ const code = runtime.backgroundUnresponsive === true ? "EXTENSION_BACKGROUND_UNRESPONSIVE" : "EXTENSION_STARTUP_UNVERIFIED";
155
156
  return {
156
157
  status: "FAIL",
157
- errorCode: "EXTENSION_BACKGROUND_UNRESPONSIVE",
158
+ errorCode: code,
158
159
  userAction: `mm-harness logs --target ${shellQuote(projectRoot)} --full # inspect the startup error before restarting or resetting an authorized dev fixture`,
159
160
  cdpPort,
160
161
  targetUrl: target.url,
161
162
  extensionId: safeExtensionId(target),
162
163
  extensionPageTargets: extensionTargets.length,
163
- findings: ["EXTENSION_BACKGROUND_UNRESPONSIVE: Extension UI reports a startup failure or unresponsive background; RPC readiness cannot be established."],
164
+ findings: [`${code}: ${runtime.backgroundUnresponsive === true ? "Extension UI reports a startup failure or unresponsive background" : "Extension page has not rendered its UI"}; RPC readiness cannot be established.`],
164
165
  details: {
165
166
  projectRoot,
166
167
  targetUrls: targets.map((entry) => entry.url).filter(Boolean),
@@ -173,6 +174,7 @@ async function checkExtensionRuntimeHealth(projectRoot, cdpPort, options = {}) {
173
174
  runtime.evmRpcProbeOk = providerProbe.ok;
174
175
  runtime.evmRpcProbeError = providerProbe.error;
175
176
  runtime.evmRpcProbeSource = providerProbe.source;
177
+ runtime.evmRpcProbeAttempted = providerProbe.requestAttempted;
176
178
  }
177
179
  if (runtime.href && !String(runtime.href).startsWith("chrome-extension://")) {
178
180
  findings.push(`Extension page href is not an extension URL: ${runtime.href}`);
@@ -181,10 +183,11 @@ async function checkExtensionRuntimeHealth(projectRoot, cdpPort, options = {}) {
181
183
  findings.push('Extension UI reports "Unable to connect to Ethereum".');
182
184
  }
183
185
  const rpcExecutionFailed = runtime.evmRpcProbeOk !== true && /Failed to execute 'fetch' on 'WorkerGlobalScope': Illegal invocation/u.test(String(runtime.evmRpcProbeError));
186
+ const startupUnverified = runtime.evmRpcProbeOk !== true && runtime.evmRpcProbeAttempted === false;
184
187
  if (runtime.evmRpcProbeOk !== true) {
185
188
  const probeError = String(runtime.evmRpcProbeError ?? "unknown error").replace(/[.\s]+$/u, "");
186
189
  findings.push(
187
- rpcExecutionFailed ? `EVM_RPC_EXECUTION_FAILED: background RPC fetch threw before an HTTP response: ${probeError}.` : `EVM RPC readiness probe failed: ${probeError}. Infura configured \u2260 RPC reachable.`
190
+ startupUnverified ? `EXTENSION_STARTUP_UNVERIFIED: no completed provider probe established RPC readiness: ${probeError}.` : rpcExecutionFailed ? `EVM_RPC_EXECUTION_FAILED: background RPC fetch threw before an HTTP response: ${probeError}.` : `EVM RPC readiness probe failed: ${probeError}. Infura configured \u2260 RPC reachable.`
188
191
  );
189
192
  }
190
193
  if (runtime.hasSubmitRequest === true && runtime.backgroundProbeOk !== true) {
@@ -195,8 +198,8 @@ async function checkExtensionRuntimeHealth(projectRoot, cdpPort, options = {}) {
195
198
  return {
196
199
  status: findings.length === 0 ? "PASS" : "FAIL",
197
200
  ...evmRpcUnreachable ? {
198
- errorCode: rpcExecutionFailed ? "EVM_RPC_EXECUTION_FAILED" : "EVM_RPC_UNREACHABLE",
199
- userAction: rpcExecutionFailed ? `mm-harness logs --target ${shellQuote(projectRoot)} --full # inspect the background fetch stack; credential resync is not indicated by this error` : evmRpcRecoveryAction(projectRoot)
201
+ errorCode: startupUnverified ? "EXTENSION_STARTUP_UNVERIFIED" : rpcExecutionFailed ? "EVM_RPC_EXECUTION_FAILED" : "EVM_RPC_UNREACHABLE",
202
+ userAction: startupUnverified ? `mm-harness logs --target ${shellQuote(projectRoot)} --full # inspect Extension startup; upstream RPC failure has not been established` : rpcExecutionFailed ? `mm-harness logs --target ${shellQuote(projectRoot)} --full # inspect the background fetch stack; credential resync is not indicated by this error` : evmRpcRecoveryAction(projectRoot)
200
203
  } : {},
201
204
  warnings,
202
205
  cdpPort,
@@ -332,6 +335,7 @@ async function evaluateHealth(session, timeoutMs) {
332
335
  return {
333
336
  href: location.href,
334
337
  title: document.title,
338
+ hasPageContent: bodyText.trim().length > 0,
335
339
  hookKeys: Object.keys(hooks),
336
340
  hasSubmitRequest: typeof hooks.submitRequestToBackground === 'function',
337
341
  hasStore: Boolean(hooks.store),
@@ -353,6 +357,7 @@ async function evaluateHealth(session, timeoutMs) {
353
357
  new Promise((resolve) => setTimeout(() => resolve({
354
358
  href: location.href,
355
359
  title: document.title,
360
+ hasPageContent: bodyText.trim().length > 0,
356
361
  hookKeys: Object.keys(hooks),
357
362
  hasSubmitRequest: typeof hooks.submitRequestToBackground === 'function',
358
363
  hasStore: Boolean(hooks.store),
@@ -395,7 +400,7 @@ async function probeUiEthereumProvider(session, timeoutMs) {
395
400
  value: {
396
401
  async probe() {
397
402
  if (!provider || typeof provider.request !== 'function') {
398
- return { ok: false, error: 'Ethereum provider is unavailable' };
403
+ return { ok: false, error: 'Ethereum provider is unavailable', requestAttempted: false };
399
404
  }
400
405
  try {
401
406
  const code = await provider.request({
@@ -405,10 +410,11 @@ async function probeUiEthereumProvider(session, timeoutMs) {
405
410
  const ok = typeof code === 'string' && /^0x[0-9a-f]*$/iu.test(code);
406
411
  return {
407
412
  ok,
413
+ requestAttempted: true,
408
414
  error: ok ? null : 'Ethereum provider returned invalid contract code',
409
415
  };
410
416
  } catch (error) {
411
- return { ok: false, error: String(error?.message || error) };
417
+ return { ok: false, error: String(error?.message || error), requestAttempted: true };
412
418
  }
413
419
  },
414
420
  cleanup() {
@@ -432,13 +438,15 @@ async function probeUiEthereumProvider(session, timeoutMs) {
432
438
  return {
433
439
  ok: false,
434
440
  error: "Ethereum provider preload was not installed",
435
- source: "ui-ethereum-provider"
441
+ source: "ui-ethereum-provider",
442
+ requestAttempted: false
436
443
  };
437
444
  }
438
445
  let outcome = {
439
446
  ok: false,
440
447
  error: "Ethereum provider is unavailable",
441
- source: "ui-ethereum-provider"
448
+ source: "ui-ethereum-provider",
449
+ requestAttempted: false
442
450
  };
443
451
  const cleanupErrors = [];
444
452
  try {
@@ -453,10 +461,11 @@ async function probeUiEthereumProvider(session, timeoutMs) {
453
461
  }, Math.max(1, deadline - Date.now()));
454
462
  const value = result.result?.value;
455
463
  if (value?.ok === true) {
456
- outcome = { ok: true, error: null, source: "ui-ethereum-provider" };
464
+ outcome = { ok: true, error: null, source: "ui-ethereum-provider", requestAttempted: true };
457
465
  break;
458
466
  }
459
467
  if (typeof value?.error === "string") outcome.error = value.error;
468
+ outcome.requestAttempted ||= value?.requestAttempted === true;
460
469
  } catch (error) {
461
470
  outcome.error = messageOf(error);
462
471
  }
@@ -464,7 +473,7 @@ async function probeUiEthereumProvider(session, timeoutMs) {
464
473
  await sleep(100);
465
474
  }
466
475
  } catch (error) {
467
- outcome = { ok: false, error: messageOf(error), source: "ui-ethereum-provider" };
476
+ outcome = { ...outcome, ok: false, error: messageOf(error) };
468
477
  } finally {
469
478
  try {
470
479
  const cleanup = await boundedCdpCall(session, "Runtime.evaluate", {
@@ -494,7 +503,8 @@ async function probeUiEthereumProvider(session, timeoutMs) {
494
503
  return cleanupErrors.length > 0 ? {
495
504
  ok: false,
496
505
  error: `Ethereum provider probe cleanup failed: ${cleanupErrors.join("; ")}`,
497
- source: "ui-ethereum-provider"
506
+ source: "ui-ethereum-provider",
507
+ requestAttempted: outcome.requestAttempted
498
508
  } : outcome;
499
509
  }
500
510
  function extensionBackgroundProbeTimeoutMs(cdpCallTimeoutMs) {
@@ -310,10 +310,10 @@ async function handleLaunchLocked(argv, stream) {
310
310
  exitCode: EXIT.infra
311
311
  });
312
312
  }
313
- if (adapter === "extension" && attempt.output.includes("EXTENSION_BACKGROUND_UNRESPONSIVE:")) {
313
+ if (adapter === "extension" && (attempt.output.includes("EXTENSION_BACKGROUND_UNRESPONSIVE:") || attempt.output.includes("EXTENSION_STARTUP_UNVERIFIED:"))) {
314
314
  return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
315
- code: "EXTENSION_BACKGROUND_UNRESPONSIVE",
316
- message: "Extension startup failed; inspect the background error before restarting or resetting wallet state.",
315
+ code: attempt.output.includes("EXTENSION_BACKGROUND_UNRESPONSIVE:") ? "EXTENSION_BACKGROUND_UNRESPONSIVE" : "EXTENSION_STARTUP_UNVERIFIED",
316
+ message: "Extension startup is not healthy; inspect the background error before restarting or resetting wallet state.",
317
317
  recoverable: false,
318
318
  userAction: `mm-harness logs --target ${shellQuote(target)} --full`,
319
319
  exitCode: EXIT.runtime,
@@ -43,7 +43,7 @@ export async function readVisiblePerpsState(
43
43
  action: input.action,
44
44
  ...normalizeVisiblePerpsState({
45
45
  route: screen.route,
46
- title: screen.title,
46
+ title: screen.name,
47
47
  items: visible.items,
48
48
  offscreenItems: visible.hidden_or_offscreen,
49
49
  truncated: visible.truncated,
@@ -27,7 +27,24 @@ export async function observeNativeUi(payload, context) {
27
27
  if (supported.length === 0) return warnings.length ? { warnings } : {};
28
28
 
29
29
  try {
30
+ const env = { ...process.env, ...record(context?.env) };
31
+ const routeInput = { action: 'ui.observe', node: { ...record(payload?.node), bridge_timeout_ms: 2_000, cdp_timeout_ms: 2_000 }, context };
32
+ let routeBefore;
33
+ if (supported.includes('ui.screen') && !isOpaqueRuntime(env) && (env.METRO_PORT || env.WATCHER_PORT)) {
34
+ try {
35
+ routeBefore = await bridgeCommand(routeInput, ['get-route']);
36
+ } catch (error) {
37
+ warnings.push({ ref: 'ui.screen', message: `Navigation route unavailable: ${error.message}` });
38
+ }
39
+ }
30
40
  const hierarchy = await readNativeHierarchy(payload, context);
41
+ if (routeBefore?.name) {
42
+ const routeAfter = await bridgeCommand(routeInput, ['get-route']);
43
+ if (routeAfter?.name !== routeBefore.name || routeAfter?.key !== routeBefore.key) {
44
+ throw new Error('Navigation changed during native UI observation; the snapshot cannot be bound to one route.');
45
+ }
46
+ hierarchy.screen.route = routeAfter.name;
47
+ }
31
48
  const observations = {};
32
49
  for (const ref of supported) {
33
50
  observations[ref] = ref === 'ui.screen' ? hierarchy.screen : hierarchy.visible;
@@ -144,7 +144,15 @@ function marketFromTestId(testId) {
144
144
  return undefined;
145
145
  }
146
146
 
147
- function classifySurface(route, testIds) {
147
+ function classifySurface(route, testIds, platform) {
148
+ if (platform === 'mobile') {
149
+ const routes = {
150
+ PerpsMarketListView: 'home',
151
+ PerpsTrendingView: 'market-list',
152
+ PerpsMarketDetails: 'market-details',
153
+ };
154
+ if (Object.hasOwn(routes, route)) return routes[route];
155
+ }
148
156
  const joined = [...testIds].join('\n');
149
157
  if (/confirm-transaction\/.*[?&]goBackTo=%2Fperps-home(?:&|$)/u.test(route)) {
150
158
  return 'funds';
@@ -213,7 +221,7 @@ export function normalizeVisiblePerpsState(raw, options, platform) {
213
221
  ? /^remove from /iu.test(favoriteLabel) : null;
214
222
  const testIds = new Set(unique.map((item) => item.testId));
215
223
  const route = text(raw.route) ?? '';
216
- const surface = classifySurface(route, testIds);
224
+ const surface = classifySurface(route, testIds, platform);
217
225
  const marketItems = selected(
218
226
  unique,
219
227
  (item) => Boolean(marketFromTestId(item.testId)),
@@ -316,7 +324,7 @@ function assertVisibleState(state, options) {
316
324
  }
317
325
  if (options.surface !== 'auto' && state.surface !== options.surface) {
318
326
  throw new Error(
319
- `Expected visible Perps surface ${options.surface}, but observed ${state.surface}.`,
327
+ `Expected visible Perps surface ${options.surface}, but observed ${state.surface} (route: ${state.route ?? 'unavailable'}; title: ${state.title ?? 'unavailable'}).`,
320
328
  );
321
329
  }
322
330
  if (state.markets.length < options.minimumMarketCount) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deeeed/metamask-harness",
3
- "version": "0.50.3",
3
+ "version": "0.50.4",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mm-harness": "bin/mm-harness"