@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 +20 -0
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +12 -3
- package/dist/commands/call.js +2 -9
- package/dist/commands/run-engine.js +7 -71
- package/dist/commands/run.js +2 -7
- package/docs/CONTRIBUTING.md +8 -0
- package/docs/RECIPES.md +13 -1
- package/library/actions/extension/perps/update_position_tpsl.mjs +3 -8
- package/library/actions/mobile/perps/perps.mjs +750 -39
- package/library/manifests/mobile.action-manifest.json +101 -6
- package/library/recipes/mobile/perps/chase-assert-running.recipe.json +110 -0
- package/library/recipes/mobile/perps/chase-place.recipe.json +145 -0
- package/library/recipes/mobile/perps/chase-terminate.recipe.json +98 -0
- package/library/recipes/mobile/perps/pro-order-setup.recipe.json +111 -0
- package/library/recipes/mobile/perps/pro-order-start-state.recipe.json +53 -0
- package/library/recipes/mobile/perps/scale-assert-orders.recipe.json +126 -0
- package/library/recipes/mobile/perps/scale-place.recipe.json +186 -0
- package/library/recipes/mobile/perps/twap-assert-active.recipe.json +97 -0
- package/library/recipes/mobile/perps/twap-place.recipe.json +146 -0
- package/package.json +1 -1
- package/site/perps-advanced-orders-qa.html +96 -0
- package/site/perps.html +11 -0
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
|
|
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
|
|
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
|
|
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)) {
|
package/dist/commands/call.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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,
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
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,
|
package/dist/commands/run.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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);
|
package/docs/CONTRIBUTING.md
CHANGED
|
@@ -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
|
|
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 }) =>
|
|
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 }) =>
|
|
262
|
+
observe: async ({ receipt }) => observeUpdatedPrices(
|
|
268
263
|
page,
|
|
269
264
|
identity,
|
|
270
265
|
receipt.facts,
|