@deeeed/metamask-harness 0.90.0 → 0.91.1

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,29 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.91.1 - 2026-10-12
6
+
7
+ ### Fixed
8
+
9
+ - Restore the selected web-dapp venue policy, digest fence and MetaMask signer on each provider invocation, including live conformance after static preflight.
10
+ - Use adapter-web 0.7.2 so Terminal recipes record their owned browser through the shared recorder.
11
+
12
+ ## 0.91.0 - 2026-10-12
13
+
14
+ - Support Extension `ui.navigate page=settings` through the visible account menu and verify the Settings page before returning.
15
+
16
+ - Declare bounded `watch_logs.timeout_ms` for Core, Mobile, Extension and web-dapp, including Terminal inheritance. Use the supporting protocol 0.40.0 and runner 0.31.0 through coordinated runtime pins.
17
+
18
+ ### Changed
19
+
20
+ - Breaking correction inherited from runner 0.31.0: run-scoped log observations require paths resolved before producers execute; output-derived paths must use `scope=file`. Nested output assertions and static JSON paths now receive complete preflight validation.
21
+
22
+ ### Fixed
23
+
24
+ - Run the declared Oxlint script for changed-source checks and explicit fixes instead of skipping lint when ESLint is absent.
25
+
26
+ - Mobile Perps position selectors now enforce long/short selection when the controller returns signed size without a side label. Core and Extension reuse the same side normalization while preserving their existing selection rules.
27
+
5
28
  ## 0.90.0 - 2026-10-12
6
29
 
7
30
  - Use typed iOS simulator recording targets, retain original Android recording segments, and use the shared action-context snapshot API for Extension evidence.
@@ -1,7 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { fencePolicy, POLICY_DIGEST_ENV, POLICY_ENV, PolicyFenceError } from "@farmslot/adapter-web/web-dapp";
4
- import { adapterPlugin, AdapterPluginError } from "@farmslot/recipe-cli/harness";
4
+ import { adapterPlugin, AdapterPluginError, harnessAdapter } from "@farmslot/recipe-cli/harness";
5
5
  import { libraryAdapterFiles } from "@farmslot/recipe-runner";
6
6
  import { configPath, declaredLibraryPathEnv, readConfig } from "../review/knowledge.js";
7
7
  import { bindWebDappPolicy } from "./web-dapp/surface.js";
@@ -41,7 +41,8 @@ async function fenceBoundPolicy() {
41
41
  throw error;
42
42
  }
43
43
  }
44
- async function afterAdapterLoad() {
44
+ async function afterAdapterLoad(adapterId) {
45
+ if (adapterId) bindPluginPolicy(harnessAdapter(adapterId));
45
46
  try {
46
47
  await fenceBoundPolicy();
47
48
  } catch (error) {
@@ -51,12 +52,16 @@ async function afterAdapterLoad() {
51
52
  throw error;
52
53
  }
53
54
  }
54
- function adoptPluginAdapter(adapter) {
55
+ function bindPluginPolicy(adapter) {
55
56
  const surface = bindWebDappPolicy(adapter);
56
57
  const policy = adapter;
57
58
  if (policy.extends === "web-dapp" && typeof policy.policy?.module === "string") {
58
59
  boundPolicy = { adapterId: adapter.id, file: policy.policy.module };
59
60
  }
61
+ return surface;
62
+ }
63
+ function adoptPluginAdapter(adapter) {
64
+ const surface = bindPluginPolicy(adapter);
60
65
  return {
61
66
  ...surface,
62
67
  run: {
@@ -62,34 +62,32 @@ async function handleCheck(argv) {
62
62
  });
63
63
  const policyFailed = suppressionCheck.status === "fail";
64
64
  const fixes = [];
65
- const eslintConfigArgs = resolveEslintConfigArgs(packageJson);
65
+ const usesOxlint = typeof packageJson?.scripts?.["lint:oxlint"] === "string";
66
+ const lintCommandArgs = usesOxlint ? ["lint:oxlint"] : resolveEslintConfigArgs(packageJson);
67
+ const lintCacheArgs = usesOxlint ? [] : ["--cache", "--cache-location", path.join(artifactsDir, "eslintcache")];
68
+ const lintCheck = {
69
+ id: usesOxlint ? "oxlint" : "eslint",
70
+ label: usesOxlint ? "Changed-file Oxlint" : "Changed-file ESLint",
71
+ target,
72
+ artifactsDir,
73
+ files: existingChangedFiles.filter((file) => JS_TS_EXT_RE.test(file)),
74
+ command: usesOxlint ? "yarn" : executable(target, "eslint"),
75
+ args: [...lintCommandArgs, ...lintCacheArgs]
76
+ };
66
77
  if (fix && !policyFailed) {
67
78
  fixes.push(
68
79
  runToolCheck({
69
- id: "eslint-fix",
70
- label: "Changed-file ESLint fix",
71
- target,
72
- artifactsDir,
73
- files: existingChangedFiles.filter((file) => JS_TS_EXT_RE.test(file)),
74
- command: executable(target, "eslint"),
75
- args: [...eslintConfigArgs, "--fix", "--cache", "--cache-location", path.join(artifactsDir, "eslintcache")]
80
+ ...lintCheck,
81
+ id: `${lintCheck.id}-fix`,
82
+ label: `${lintCheck.label} fix`,
83
+ args: [...lintCommandArgs, "--fix", ...lintCacheArgs]
76
84
  })
77
85
  );
78
86
  fixes.push(runFormatterFix({ target, artifactsDir, files: existingChangedFiles.filter((file) => FORMAT_EXT_RE.test(file)) }));
79
87
  }
80
88
  const checks = [suppressionCheck];
81
89
  if (!policyFailed) {
82
- checks.push(
83
- runToolCheck({
84
- id: "eslint",
85
- label: "Changed-file ESLint",
86
- target,
87
- artifactsDir,
88
- files: existingChangedFiles.filter((file) => JS_TS_EXT_RE.test(file)),
89
- command: executable(target, "eslint"),
90
- args: [...eslintConfigArgs, "--cache", "--cache-location", path.join(artifactsDir, "eslintcache")]
91
- })
92
- );
90
+ checks.push(runToolCheck(lintCheck));
93
91
  checks.push(runFormatterCheck({ target, artifactsDir, files: existingChangedFiles.filter((file) => FORMAT_EXT_RE.test(file)) }));
94
92
  checks.push(
95
93
  ...runJestChecks({
@@ -115,7 +113,7 @@ async function handleCheck(argv) {
115
113
  }
116
114
  const status = [...fixes, ...checks].some((check) => check.status === "fail") ? "fail" : "pass";
117
115
  const exitCode = status === "pass" ? EXIT.ok : EXIT.validation;
118
- const guidance = policyFailed ? ["Remove the newly added eslint-disable directive(s). Fix the underlying violation with existing named styles, project tokens, or another established source pattern; then rerun mm-harness check diff [--fix]. Other checks were skipped until this policy failure is removed."] : checks.some((check) => check.id === "eslint" && check.status === "fail") ? ["Fix the reported violations in changed source using existing project patterns and tokens; do not suppress rules, hide violations behind indirection, weaken configuration, or bypass check diff. Then rerun mm-harness check diff [--fix]."] : [];
116
+ const guidance = policyFailed ? ["Remove the newly added eslint-disable directive(s). Fix the underlying violation with existing named styles, project tokens, or another established source pattern; then rerun mm-harness check diff [--fix]. Other checks were skipped until this policy failure is removed."] : checks.some((check) => check.id === lintCheck.id && check.status === "fail") ? ["Fix the reported violations in changed source using existing project patterns and tokens; do not suppress rules, hide violations behind indirection, weaken configuration, or bypass check diff. Then rerun mm-harness check diff [--fix]."] : [];
119
117
  const failureUserAction = guidance[0] ?? `inspect ${path.join(artifactsDir, "validation-summary.json")}, fix the failing check, then retry: mm-harness check diff --target ${shellQuote(target)} --base ${shellQuote(baseRef)} --profile ${profile} --json`;
120
118
  const summary = {
121
119
  schemaVersion: 1,
package/dist/provider.js CHANGED
@@ -38,7 +38,7 @@ async function createProvider(context) {
38
38
  env: operatorLibraryEnv(),
39
39
  adopt: adoptPluginAdapter
40
40
  });
41
- await afterAdapterLoad();
41
+ await afterAdapterLoad(adapter);
42
42
  const runtime = getAdapterSurface(adapter);
43
43
  configureProviderReadiness(runtime);
44
44
  const executes = isRecipeExecution(context.command, options);
@@ -6,6 +6,7 @@ import path from 'node:path';
6
6
  import { pathToFileURL } from 'node:url';
7
7
 
8
8
  import { walletFixturePath } from '../../harness-exports.mjs';
9
+ import { normalizeSide, sideForItem } from '../../shared/perps/item-side.mjs';
9
10
  import { readFixtureAccounts } from '../../shared/wallet/fixture-accounts.mjs';
10
11
  import { createRewardsClient, rewardsApiUrl } from './_rewards.mjs';
11
12
 
@@ -597,25 +598,6 @@ export function symbolForItem(item) {
597
598
  return normalizeMarketSymbol(item?.symbol ?? item?.coin ?? '');
598
599
  }
599
600
 
600
- // Orders carry buy/sell sides; Position.size is signed and authoritative.
601
- function normalizeSide(rawSide) {
602
- const side = String(rawSide ?? '').toLowerCase();
603
- if (side === 'buy' || side === 'b') return 'long';
604
- if (side === 'sell' || side === 'a' || side === 's') return 'short';
605
- return side;
606
- }
607
-
608
- function sideForItem(item, itemType) {
609
- if (itemType === 'position') {
610
- const rawSize = item?.size ?? item?.szi;
611
- if (typeof rawSize !== 'string' && typeof rawSize !== 'number') return '';
612
- const size = Number(rawSize);
613
- if (!Number.isFinite(size) || size === 0) return '';
614
- return size > 0 ? 'long' : 'short';
615
- }
616
- return normalizeSide(item?.side ?? item?.direction ?? '');
617
- }
618
-
619
601
  function uniqueSymbols(symbols) {
620
602
  return Array.from(new Set(symbols.filter(Boolean)));
621
603
  }
@@ -1,6 +1,7 @@
1
1
  // Implement Extension Perps state reads, UI setup and funded trade operations.
2
2
  // Funded paths check runtime identity and retain mutation receipts for cleanup.
3
3
 
4
+ import { sideForItem } from '../../shared/perps/item-side.mjs';
4
5
  import { pathToFileURL } from 'node:url';
5
6
  import { createHash } from 'node:crypto';
6
7
  import { readFile, writeFile } from 'node:fs/promises';
@@ -66,17 +67,6 @@ function symbolForItem(item) {
66
67
  return normalizeMarketSymbol(item?.coin ?? item?.symbol ?? '');
67
68
  }
68
69
 
69
- function sideForItem(item) {
70
- const side = String(item?.side ?? item?.direction ?? '').toLowerCase();
71
- if (side === 'buy' || side === 'b') return 'long';
72
- if (side === 'sell' || side === 'a' || side === 's') return 'short';
73
- if (side) return side;
74
- // Positions carry no side, only a signed size (long > 0, short < 0).
75
- const size = Number(item?.size ?? item?.szi);
76
- if (Number.isFinite(size) && size !== 0) return size > 0 ? 'long' : 'short';
77
- return side;
78
- }
79
-
80
70
  function uniqueSymbols(symbols) {
81
71
  return Array.from(new Set(symbols.filter(Boolean)));
82
72
  }
@@ -46,7 +46,7 @@ function text(value) {
46
46
  function pageIntent(node) {
47
47
  const page = text(node?.page);
48
48
  if (!page) return undefined;
49
- if (page === 'back' || page === 'home' || page === 'perps' || page === 'swap') return { page };
49
+ if (page === 'back' || page === 'home' || page === 'perps' || page === 'swap' || page === 'settings') return { page };
50
50
  if (page === 'perps-market-list' || page === 'perps-activity') return { page };
51
51
  if (page === 'network') {
52
52
  const network = text(node.network);
@@ -61,7 +61,7 @@ function pageIntent(node) {
61
61
  if (!market) throw new Error('extension ui.navigate page=perps-market requires market or symbol.');
62
62
  return { page, market };
63
63
  }
64
- throw new Error('extension ui.navigate supported page intents: back, home, network, perps, perps-market-list, perps-market, perps-activity, swap.');
64
+ throw new Error('extension ui.navigate supported page intents: back, home, settings, network, perps, perps-market-list, perps-market, perps-activity, swap.');
65
65
  }
66
66
 
67
67
  async function goBack(page, timeoutMs) {
@@ -503,6 +503,27 @@ export async function openNetwork(page, network, chainId, addIfMissing, timeoutM
503
503
  throw new Error(`Network ${network} (${chainId}) did not become the selected wallet filter.`);
504
504
  }
505
505
 
506
+ export async function openSettings(page, timeoutMs) {
507
+ const destination = dataTestId('parent-selector-settings-page');
508
+ if (await hasVisibleSelector(page, destination)) {
509
+ return { method: 'visible-ui', href: await currentHref(page), steps: [] };
510
+ }
511
+ const deadline = Date.now() + timeoutMs;
512
+ const menu = dataTestId('account-options-menu-button');
513
+ const settings = dataTestId('global-menu-settings');
514
+ const steps = [];
515
+ if (!(await hasVisibleSelector(page, settings))) {
516
+ await page.waitFor({ selector: menu, expected: 'visible', timeoutMs: Math.max(1, deadline - Date.now()) });
517
+ await activateSemanticControl(page, menu);
518
+ steps.push(menu);
519
+ }
520
+ await page.waitFor({ selector: settings, expected: 'visible', timeoutMs: Math.max(1, deadline - Date.now()) });
521
+ const navigation = await activateSemanticControl(page, settings);
522
+ steps.push(settings);
523
+ await page.waitFor({ selector: destination, expected: 'visible', timeoutMs: Math.max(1, deadline - Date.now()) });
524
+ return { navigation, method: 'visible-ui', href: await currentHref(page), selector: settings, steps };
525
+ }
526
+
506
527
  export async function openSwap(page, timeoutMs) {
507
528
  const sourceAmount = dataTestId('from-amount');
508
529
  const sourceToken = dataTestId('bridge-source-button');
@@ -588,6 +609,10 @@ export async function navigateExtensionUi(input) {
588
609
  );
589
610
  return { action: input.action, ...intent, ...result, proofPath: 'ui-navigation' };
590
611
  }
612
+ if (intent.page === 'settings') {
613
+ const result = await openSettings(page, timeoutMs);
614
+ return { action: input.action, ...intent, ...result, proofPath: 'ui-navigation' };
615
+ }
591
616
  if (intent.page === 'swap') {
592
617
  const result = await openSwap(page, timeoutMs);
593
618
  return { action: input.action, ...intent, ...result, proofPath: 'ui-navigation' };
@@ -1,5 +1,6 @@
1
1
  // Implement Mobile Perps navigation, trading mutations, assertions, and setup or cleanup through the bridge.
2
2
  // Strategy getters used during cleanup can settle tracked collateral.
3
+ import { sideForItem } from '../../shared/perps/item-side.mjs';
3
4
  import { pathToFileURL } from 'node:url';
4
5
  import {
5
6
  MOBILE_BRIDGE_RESULT_ERROR_CODES,
@@ -300,13 +301,6 @@ function symbolForItem(item) {
300
301
  return normalizeMarketSymbol(item?.symbol ?? item?.coin ?? '');
301
302
  }
302
303
 
303
- function sideForItem(item) {
304
- const side = String(item?.side ?? item?.direction ?? '').toLowerCase();
305
- if (side === 'buy' || side === 'b') return 'long';
306
- if (side === 'sell' || side === 'a' || side === 's') return 'short';
307
- return side;
308
- }
309
-
310
304
  function configuredSymbols(input, items) {
311
305
  const selector = input.node?.selector && typeof input.node.selector === 'object' ? input.node.selector : {};
312
306
  const mode = String(input.node?.mode ?? selector.mode ?? 'matching').toLowerCase();
@@ -0,0 +1,18 @@
1
+ // Normalize explicit order sides and the signed sizes returned for Perps positions.
2
+ export function normalizeSide(rawSide) {
3
+ const side = String(rawSide ?? '').toLowerCase();
4
+ if (side === 'buy' || side === 'b') return 'long';
5
+ if (side === 'sell' || side === 'a' || side === 's') return 'short';
6
+ return side;
7
+ }
8
+
9
+ export function sideForItem(item, itemType) {
10
+ if (itemType !== 'position') {
11
+ const side = normalizeSide(item?.side ?? item?.direction);
12
+ if (side || itemType !== undefined) return side;
13
+ }
14
+ const rawSize = item?.size ?? item?.szi;
15
+ if (typeof rawSize !== 'string' && typeof rawSize !== 'number') return '';
16
+ const size = Number(rawSize);
17
+ return Number.isFinite(size) && size !== 0 ? (size > 0 ? 'long' : 'short') : '';
18
+ }
@@ -362,7 +362,7 @@
362
362
  }
363
363
  },
364
364
  "watch_logs": {
365
- "description": "Base log assertion/capture action.",
365
+ "description": "Observe log text immediately or within a declared timeout_ms window.",
366
366
  "examples": [
367
367
  {
368
368
  "action": "watch_logs",
@@ -386,6 +386,11 @@
386
386
  "run",
387
387
  "file"
388
388
  ]
389
+ },
390
+ "timeout_ms": {
391
+ "type": "integer",
392
+ "minimum": 0,
393
+ "description": "Observation window from 0 through 60000 ms; a positive window requires nonempty contains."
389
394
  }
390
395
  },
391
396
  "required": [
@@ -342,7 +342,7 @@
342
342
  }
343
343
  },
344
344
  "watch_logs": {
345
- "description": "Base log assertion/capture action.",
345
+ "description": "Observe log text immediately or within a declared timeout_ms window.",
346
346
  "examples": [
347
347
  {
348
348
  "action": "watch_logs",
@@ -366,6 +366,11 @@
366
366
  "run",
367
367
  "file"
368
368
  ]
369
+ },
370
+ "timeout_ms": {
371
+ "type": "integer",
372
+ "minimum": 0,
373
+ "description": "Observation window from 0 through 60000 ms; a positive window requires nonempty contains."
369
374
  }
370
375
  },
371
376
  "required": [
@@ -509,7 +514,8 @@
509
514
  "perps-market-list",
510
515
  "perps-market",
511
516
  "perps-activity",
512
- "swap"
517
+ "swap",
518
+ "settings"
513
519
  ]
514
520
  },
515
521
  "market": {
@@ -345,7 +345,7 @@
345
345
  }
346
346
  },
347
347
  "watch_logs": {
348
- "description": "Base log assertion/capture action.",
348
+ "description": "Observe log text immediately or within a declared timeout_ms window.",
349
349
  "examples": [
350
350
  {
351
351
  "action": "watch_logs",
@@ -365,10 +365,20 @@
365
365
  },
366
366
  "scope": {
367
367
  "type": "string",
368
- "enum": ["run", "file"]
368
+ "enum": [
369
+ "run",
370
+ "file"
371
+ ]
372
+ },
373
+ "timeout_ms": {
374
+ "type": "integer",
375
+ "minimum": 0,
376
+ "description": "Observation window from 0 through 60000 ms; a positive window requires nonempty contains."
369
377
  }
370
378
  },
371
- "required": ["path"],
379
+ "required": [
380
+ "path"
381
+ ],
372
382
  "additionalProperties": false
373
383
  }
374
384
  },
@@ -1097,6 +1097,44 @@
1097
1097
  },
1098
1098
  "additionalProperties": false
1099
1099
  }
1100
+ },
1101
+ "watch_logs": {
1102
+ "description": "Observe log text immediately or within a declared timeout_ms window.",
1103
+ "examples": [
1104
+ {
1105
+ "action": "watch_logs",
1106
+ "path": "logs/app.log",
1107
+ "intent": "Observe the runtime behavior needed for this proof.",
1108
+ "next": "done"
1109
+ }
1110
+ ],
1111
+ "schema": {
1112
+ "type": "object",
1113
+ "properties": {
1114
+ "path": {
1115
+ "type": "string"
1116
+ },
1117
+ "contains": {
1118
+ "type": "string"
1119
+ },
1120
+ "scope": {
1121
+ "type": "string",
1122
+ "enum": [
1123
+ "run",
1124
+ "file"
1125
+ ]
1126
+ },
1127
+ "timeout_ms": {
1128
+ "type": "integer",
1129
+ "minimum": 0,
1130
+ "description": "Observation window from 0 through 60000 ms; a positive window requires nonempty contains."
1131
+ }
1132
+ },
1133
+ "required": [
1134
+ "path"
1135
+ ],
1136
+ "additionalProperties": false
1137
+ }
1100
1138
  }
1101
1139
  }
1102
1140
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deeeed/metamask-harness",
3
- "version": "0.90.0",
3
+ "version": "0.91.1",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mm-harness": "bin/mm-harness"
@@ -21,15 +21,15 @@
21
21
  "check:syntax": "find . -name '*.mjs' -print0 | xargs -0 -n1 node --check"
22
22
  },
23
23
  "dependencies": {
24
- "@farmslot/adapter-node": "0.1.13",
25
- "@farmslot/adapter-rn": "0.20.0",
26
- "@farmslot/adapter-sdk": "0.9.0",
27
- "@farmslot/adapter-web": "0.7.0",
28
- "@farmslot/agent-runtime": "0.18.0",
24
+ "@farmslot/adapter-node": "0.1.14",
25
+ "@farmslot/adapter-rn": "0.20.1",
26
+ "@farmslot/adapter-sdk": "0.9.1",
27
+ "@farmslot/adapter-web": "0.7.2",
28
+ "@farmslot/agent-runtime": "0.18.1",
29
29
  "@farmslot/handoff": "^0.3.1",
30
- "@farmslot/protocol": "0.39.0",
31
- "@farmslot/recipe-cli": "0.14.1",
32
- "@farmslot/recipe-runner": "0.30.0",
30
+ "@farmslot/protocol": "0.40.0",
31
+ "@farmslot/recipe-cli": "0.14.2",
32
+ "@farmslot/recipe-runner": "0.31.0",
33
33
  "agent-device": "0.19.3",
34
34
  "commander": "^12.0.0",
35
35
  "esbuild": "0.28.1",