@deeeed/metamask-harness 0.45.0 → 0.46.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,26 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.46.0 - 2026-09-04
6
+
7
+ ### Added
8
+
9
+ - Add parameterized Mobile Pro TWAP, Chase, and Scale recipes plus an offline advanced-orders QA walkthrough. Advanced-order recipes keep the current account when omitted, use stated provider/network/market defaults, default to testnet, and allow explicit mainnet selection.
10
+
11
+ ### Fixed
12
+
13
+ - Make Mobile Perps start-state navigation keep an already-matching market, keep advanced-order controls reachable, select a market visibly from Perps Home, and clean matching active TWAP plus active or backgrounded Chase strategies with ordinary orders and positions; teardown now defaults to a verified Hyperliquid testnet environment, honors its declared timeout while canceling strategies, and TWAP placement defaults above the provider minimum.
14
+ - Position active TWAP and Chase evidence inside the viewport before capture.
15
+
16
+ ## 0.45.1 - 2026-09-01
17
+
18
+ ### Fixed
19
+
20
+ - Stop Mobile Perps order actions from forcing a connection reset after submission; completion now relies on observed position or open-order state.
21
+ - Preserve the first recipe failure on every adapter instead of reloading or retrying the product runtime behind the proof.
22
+ - Keep Extension TP/SL updates on the current page and require persistence reloads to be explicit recipe lifecycle steps.
23
+ - Document and enforce the rule that ordinary proof actions cannot hide lifecycle recovery, cache clearing, state resets, or journey retries.
24
+
5
25
  ## 0.45.0 - 2026-08-28
6
26
 
7
27
  ### Changed
@@ -645,7 +645,10 @@ const COMMANDS = {
645
645
  if (walkSiblings === undefined) walkSiblings = true;
646
646
  var current = fiber;
647
647
  while (current) {
648
- var sn = current.stateNode;
648
+ var rawStateNode = current.stateNode;
649
+ var sn = rawStateNode && rawStateNode.canonical && rawStateNode.canonical.publicInstance
650
+ ? rawStateNode.canonical.publicInstance
651
+ : rawStateNode;
649
652
  if (sn) {
650
653
  if (typeof sn.scrollTo === 'function') { sn.scrollTo({ y: opts.offset, animated: opts.animated }); return true; }
651
654
  if (typeof sn.scrollToOffset === 'function') { sn.scrollToOffset({ offset: opts.offset, animated: opts.animated }); return true; }
@@ -657,7 +660,10 @@ const COMMANDS = {
657
660
  }
658
661
  function findMeasurable(fiber) {
659
662
  if (!fiber) return null;
660
- var sn = fiber.stateNode;
663
+ var rawStateNode = fiber.stateNode;
664
+ var sn = rawStateNode && rawStateNode.canonical && rawStateNode.canonical.publicInstance
665
+ ? rawStateNode.canonical.publicInstance
666
+ : rawStateNode;
661
667
  if (sn && typeof sn.measureLayout === 'function') return sn;
662
668
  return findMeasurable(fiber.child);
663
669
  }
@@ -666,7 +672,10 @@ const COMMANDS = {
666
672
  var target = opts.intoView ? findMeasurable(anchor) : null;
667
673
  var current = anchor ? anchor.return : null;
668
674
  while (current) {
669
- var sn = current.stateNode;
675
+ var rawStateNode = current.stateNode;
676
+ var sn = rawStateNode && rawStateNode.canonical && rawStateNode.canonical.publicInstance
677
+ ? rawStateNode.canonical.publicInstance
678
+ : rawStateNode;
670
679
  var currentProps = current.memoizedProps;
671
680
  if (sn) {
672
681
  if (typeof sn.scrollTo === 'function' && !(currentProps && currentProps.horizontal === true)) {
@@ -34,7 +34,6 @@ import {
34
34
  executeWithHealBounds,
35
35
  prepareHeal,
36
36
  persistRunEffects,
37
- recoverRunInfra,
38
37
  resolveMetaMaskLibrarySources,
39
38
  preflightRecipe,
40
39
  runRecipe,
@@ -289,7 +288,7 @@ async function handleCall(argv) {
289
288
  skipMobileSourceFreshness: authoredMobileRestart || adapter === "mobile" && readMobileReleaseArtifactState(target) !== null
290
289
  });
291
290
  if (typeof prepared === "number") return prepared;
292
- const { state, heal } = prepared;
291
+ const { state } = prepared;
293
292
  preflightedExecution = await preflightRecipe(
294
293
  adapter,
295
294
  recipe,
@@ -335,14 +334,8 @@ async function handleCall(argv) {
335
334
  state
336
335
  );
337
336
  },
338
- adapter,
339
337
  target,
340
- heal,
341
- state,
342
- () => recoverRunInfra(adapter, target, json, {
343
- cdpPort: optionString(options, "cdpPort"),
344
- watcherPort: optionString(options, "watcherPort") ?? optionString(options, "metroPort")
345
- })
338
+ state
346
339
  );
347
340
  } catch (error) {
348
341
  await networkObservation?.finalize().catch(() => void 0);
@@ -1358,44 +1358,6 @@ async function ensureExtensionProofRuntime(target, heal, state, json, deps = {})
1358
1358
  });
1359
1359
  return null;
1360
1360
  }
1361
- const RUN_RECOVERY_CODE = {
1362
- mobile: "mobile.runtime_recovered",
1363
- extension: "extension.runtime_recovered",
1364
- core: "run.retried"
1365
- };
1366
- async function recoverRunInfra(adapter, target, json, runtimeOptions = {}) {
1367
- getAdapterSurface(adapter).resolveSlotPorts(target);
1368
- const previousCdpPort = process.env.CDP_PORT;
1369
- const previousRecipeCdpPort = process.env.RECIPE_CDP_PORT;
1370
- if (runtimeOptions.cdpPort) {
1371
- process.env.CDP_PORT = runtimeOptions.cdpPort;
1372
- process.env.RECIPE_CDP_PORT = runtimeOptions.cdpPort;
1373
- }
1374
- try {
1375
- if (adapter === "extension") {
1376
- const { releaseArtifactState } = await import("../adapters/extension/runtime-decision.js");
1377
- const artifact = releaseArtifactState(target);
1378
- if (artifact.status !== "none" && artifact.status !== "valid") {
1379
- return {
1380
- status: 1,
1381
- output: "Release artifact recovery cannot trust the recorded runtime identity.\nNext: rerun runtime-launch with the same artifact source."
1382
- };
1383
- }
1384
- const { launchExtension } = await import("./launch/extension.js");
1385
- return launchExtension(target, "quick", false);
1386
- }
1387
- if (adapter === "mobile") {
1388
- return {
1389
- status: 1,
1390
- output: "Automatic Mobile runtime recovery is disabled.\nNext: use an explicit app.lifecycle action when the recipe intends to relaunch the app."
1391
- };
1392
- }
1393
- return { status: 0, output: "headless run retry" };
1394
- } finally {
1395
- restoreEnv("CDP_PORT", previousCdpPort);
1396
- restoreEnv("RECIPE_CDP_PORT", previousRecipeCdpPort);
1397
- }
1398
- }
1399
1361
  function readRunFailureText(result) {
1400
1362
  try {
1401
1363
  const trace = JSON.parse(fs.readFileSync(result.tracePath, "utf8"));
@@ -1407,38 +1369,13 @@ function readRunFailureText(result) {
1407
1369
  return "";
1408
1370
  }
1409
1371
  }
1410
- async function executeWithHealBounds(exec, adapter, target, heal, state, recover = () => recoverRunInfra(adapter, target, false), onRecovery = () => void 0) {
1411
- let result = await exec();
1412
- for (; ; ) {
1413
- if (result.status === "pass") return { result, violation: null };
1414
- const violation = checkHealBounds(
1415
- target,
1416
- readRunFailureText(result),
1417
- state
1418
- );
1419
- if (violation !== null) return { result, violation };
1420
- if (adapter === "mobile") return { result, violation: null };
1421
- if (heal === "off") return { result, violation: null };
1422
- const recoveryCode = RUN_RECOVERY_CODE[adapter];
1423
- state.attemptedRecoveries.push(recoveryCode);
1424
- onRecovery(recoveryCode);
1425
- const recovery = await recover();
1426
- if (recovery.status !== 0) {
1427
- return { result, violation: null };
1428
- }
1429
- if (adapter === "extension") {
1430
- state.mutations.push({
1431
- type: "runtime",
1432
- action: "reloaded",
1433
- reasonCode: recoveryCode
1434
- });
1435
- }
1436
- result = await exec();
1437
- if (result.status === "pass") {
1438
- state.recovered.push(recoveryCode);
1439
- return { result, violation: null };
1440
- }
1441
- }
1372
+ async function executeWithHealBounds(exec, target, state) {
1373
+ const result = await exec();
1374
+ if (result.status === "pass") return { result, violation: null };
1375
+ return {
1376
+ result,
1377
+ violation: checkHealBounds(target, readRunFailureText(result), state)
1378
+ };
1442
1379
  }
1443
1380
  function emitHealViolation(json, command, result, violation, state, adapter) {
1444
1381
  const userAction = adapter === "core" && violation.code === "WALLET_STATE_REQUIRED" ? 'create temp/recipe/runtime/wallet-fixture.json, or add "account": "<0x\u2026>" to the node block in the recipe' : violation.userAction ?? `inspect ${shellQuoteArg(result.summaryPath)} and ${shellQuoteArg(result.tracePath)}; fix the application or recipe failure before retrying`;
@@ -1499,7 +1436,6 @@ export {
1499
1436
  preflightRecipe,
1500
1437
  prepareHeal,
1501
1438
  prepareRuntimeIfNeeded,
1502
- recoverRunInfra,
1503
1439
  resolveMetaMaskLibrarySources,
1504
1440
  resolveRunRecipeArg,
1505
1441
  runOneNode,
@@ -22,7 +22,6 @@ import {
22
22
  preflightRecipe,
23
23
  prepareHeal,
24
24
  persistRunEffects,
25
- recoverRunInfra,
26
25
  runRecipe,
27
26
  validateRunRecipeStatic
28
27
  } from "./run-engine.js";
@@ -304,7 +303,7 @@ async function handleRunInner({ positional, options }, stream) {
304
303
  });
305
304
  return prepared;
306
305
  }
307
- const { state, heal } = prepared;
306
+ const { state } = prepared;
308
307
  preflightedExecution = await preflightRecipe(
309
308
  adapter,
310
309
  validated.recipeFile,
@@ -353,12 +352,8 @@ async function handleRunInner({ positional, options }, stream) {
353
352
  state
354
353
  );
355
354
  },
356
- adapter,
357
355
  target,
358
- heal,
359
- state,
360
- () => recoverRunInfra(adapter, target, machine, runtimeOptions),
361
- (code) => stream.phase("recover", { code })
356
+ state
362
357
  );
363
358
  } catch (error) {
364
359
  await networkObservation?.finalize().catch(() => void 0);
@@ -108,6 +108,14 @@ Write evidence under `context.artifactsDir` and return relative artifact paths.
108
108
  `METAMASK_RECIPE_LIVE_ADAPTER_DIR` is the explicit task-local implementation
109
109
  root; configured libraries are the durable sharing mechanism.
110
110
 
111
+ An ordinary action performs only its declared operation and observes the
112
+ product's result. It must not restart or reload the app, reconnect providers or
113
+ streams, clear caches, reset state, or retry a failed journey. Those operations
114
+ can hide the product failure the recipe exists to catch. If lifecycle is the
115
+ subject, expose it as an explicit recipe node with separate postconditions. If
116
+ transport or lifecycle fails during another action, return the failure and keep
117
+ the evidence. Do not repair and continue.
118
+
111
119
  Add a bundled action only when it is reusable, typed, stable, reduces inference
112
120
  or risk, and has a real postcondition. Otherwise use a team library or task-local
113
121
  recipe. Follow [Recipes](RECIPES.md) and [Security](SECURITY.md).
package/docs/RECIPES.md CHANGED
@@ -211,6 +211,17 @@ Rules:
211
211
 
212
212
  - Validate with `--plan` before side effects.
213
213
  - Use real product paths; never mutate hidden state to fabricate proof.
214
+ - Never repair the product while proving it. A normal action must not restart,
215
+ reload, foreground, reconnect, clear caches, reset state, or rerun the
216
+ journey after failure. Observe the product's natural result and fail with the
217
+ last observed state when it does not arrive.
218
+ - Put intended setup and lifecycle transitions in their own recipe nodes. Use
219
+ an explicit lifecycle node when restart, reload, reconnect, background, or
220
+ foreground behavior is the claim. A persistence proof must show the
221
+ transition as a node between the immediate and persisted assertions.
222
+ - Keep setup outside the proof window. Setup may converge fixture-backed state
223
+ when the node names the mutation and the trace records it, but setup evidence
224
+ cannot prove the behavior under test.
214
225
  - Prove preparation with an independent read/assert or visible UI postcondition.
215
226
  - Keep ticket-specific claims in task-local recipes.
216
227
  - Compose, repair, or parameterize repeated behavior instead of multiplying names.
@@ -248,7 +259,8 @@ Keep responsibilities narrow:
248
259
  - **Recipes** compose actions and own journey order, safe defaults, invariants,
249
260
  and proof.
250
261
  - **Actions** translate one stable operation to existing UI, CDP, or controller
251
- capabilities; they do not reimplement product business logic.
262
+ capabilities; they do not reimplement product business logic or hide
263
+ lifecycle recovery.
252
264
  - **Product/controllers** remain the source of truth for state transitions,
253
265
  validation, transactions, and domain behavior.
254
266
 
@@ -92,12 +92,8 @@ async function waitForExactPrices(page, identity, facts, timeoutMs) {
92
92
  throw new Error(`exact TP/SL postcondition was not visible: ${JSON.stringify(after)}.`);
93
93
  }
94
94
 
95
- async function observePersistedPrices(page, identity, facts, timeoutMs) {
95
+ async function observeUpdatedPrices(page, identity, facts, timeoutMs) {
96
96
  const immediate = await waitForExactPrices(page, identity, facts, timeoutMs);
97
- await page.session.call('Page.reload', { ignoreCache: true });
98
- await new Promise((resolve) => setTimeout(resolve, 500));
99
- await page.waitForSelector(dataTestId('perps-position-size-value'), { timeoutMs: 20_000 });
100
- const persisted = await waitForExactPrices(page, identity, facts, timeoutMs);
101
97
  return {
102
98
  success: true,
103
99
  positionBefore: facts.positionBefore,
@@ -107,7 +103,6 @@ async function observePersistedPrices(page, identity, facts, timeoutMs) {
107
103
  stopLossPrice: facts.stopLossPrice,
108
104
  },
109
105
  immediate,
110
- persistedAfterReload: persisted,
111
106
  };
112
107
  }
113
108
 
@@ -191,7 +186,7 @@ export async function updatePositionTpsl(input) {
191
186
  input,
192
187
  identity,
193
188
  dispatch: async () => { throw new Error('resumed TP/SL mutation must not dispatch'); },
194
- observe: async ({ receipt }) => observePersistedPrices(
189
+ observe: async ({ receipt }) => observeUpdatedPrices(
195
190
  page,
196
191
  identity,
197
192
  receipt.facts,
@@ -264,7 +259,7 @@ export async function updatePositionTpsl(input) {
264
259
  button: 'left', buttons: 0, clickCount: 1,
265
260
  });
266
261
  },
267
- observe: async ({ receipt }) => observePersistedPrices(
262
+ observe: async ({ receipt }) => observeUpdatedPrices(
268
263
  page,
269
264
  identity,
270
265
  receipt.facts,