@deeeed/metamask-harness 0.86.2 → 0.88.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.
Files changed (61) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/adapters/extension/ensure-browser.sh +6 -10
  3. package/adapters/extension/inject.mjs +1 -0
  4. package/adapters/extension/launch-browser.cjs +31 -4
  5. package/adapters/extension/profile-extension-state.cjs +156 -0
  6. package/adapters/extension/readiness.mjs +21 -2
  7. package/adapters/manifest.json +8 -0
  8. package/adapters/mobile/wait-for-bridge.cjs +9 -3
  9. package/dist/adapters/extension/actions.js +2 -1
  10. package/dist/adapters/extension/run.js +6 -0
  11. package/dist/adapters/mobile/action-inputs.js +20 -0
  12. package/dist/adapters/mobile/actions.js +18 -1
  13. package/dist/adapters/mobile/android-screenrecord.js +274 -0
  14. package/dist/adapters/mobile/readiness.js +2 -1
  15. package/dist/adapters/mobile/recording-outcome.js +54 -0
  16. package/dist/adapters/mobile/recording-target.js +11 -0
  17. package/dist/adapters/mobile/run.js +58 -15
  18. package/dist/adapters/mobile/ui.js +96 -2
  19. package/dist/adapters/perps-mainnet-cleanup.js +122 -0
  20. package/dist/adapters/perps-order-inputs.js +43 -28
  21. package/dist/adapters.js +4 -1
  22. package/dist/commands/doctor.js +33 -14
  23. package/dist/commands/status-probe.js +2 -1
  24. package/dist/engine.js +11 -2
  25. package/dist/funding-execution-context.js +13 -8
  26. package/dist/host.js +6 -2
  27. package/dist/index.js +3 -0
  28. package/dist/mm-harness-cli.js +4 -2
  29. package/dist/provider.js +72 -0
  30. package/dist/recipe-security.js +1 -0
  31. package/docs/CONTRIBUTING.md +7 -0
  32. package/docs/RECIPES.md +96 -9
  33. package/docs/RELEASE-QA-CAPABILITY-MAP.md +5 -1
  34. package/docs/cli-contract.md +1 -1
  35. package/library/actions/core/perps/read_account.mjs +4 -2
  36. package/library/actions/core/perps/read_orders.mjs +4 -2
  37. package/library/actions/core/perps/read_positions.mjs +4 -2
  38. package/library/actions/extension/analytics/consent.mjs +7 -2
  39. package/library/actions/extension/perps/compare_provider_market.mjs +12 -8
  40. package/library/actions/extension/perps/mutation-receipt.mjs +124 -26
  41. package/library/actions/extension/perps/perps.mjs +298 -39
  42. package/library/actions/extension/perps/read_snapshot.mjs +6 -2
  43. package/library/actions/extension/perps/set_network.mjs +6 -0
  44. package/library/actions/extension/platform/cdp.mjs +6 -2
  45. package/library/actions/extension/ui/navigate.mjs +8 -0
  46. package/library/actions/mobile/perps/perps.mjs +264 -102
  47. package/library/actions/mobile/perps/place_order.mjs +7 -4
  48. package/library/actions/mobile/platform/bridge.mjs +171 -10
  49. package/library/actions/mobile/wallet/home.mjs +7 -2
  50. package/library/actions/mobile/wallet/select_account.mjs +4 -0
  51. package/library/actions/shared/app/live-adapter-deadline.mjs +8 -0
  52. package/library/actions/shared/perps/mainnet-order-ledger.mjs +378 -0
  53. package/library/actions/shared/perps/order-policy.mjs +56 -19
  54. package/library/actions/shared/perps/rate-limit.mjs +93 -0
  55. package/library/actions/shared/perps/visible-state.mjs +3 -2
  56. package/library/actions/shared/wallet/fixture-accounts.mjs +9 -3
  57. package/library/manifests/extension.action-manifest.json +36 -5
  58. package/library/manifests/mobile.action-manifest.json +48 -10
  59. package/package.json +14 -9
  60. package/scripts/validate-release-report.mjs +14 -2
  61. package/site/assets/help-recipes.json +2 -1
@@ -3788,7 +3788,7 @@
3788
3788
  ]
3789
3789
  },
3790
3790
  "metamask.perps.read_snapshot": {
3791
- "description": "Read a bounded, redacted source-dev snapshot of Extension Perps state through the optional product background hook.",
3791
+ "description": "Read a bounded, redacted source-dev snapshot of Extension Perps state through the optional product background hook. The history section is the funds ledger from perpsGetUserHistory (deposits, withdrawals and transfers): it holds no trades or orders, so zero history items does not mean no trading history. Read trade and order rows with metamask.perps.read_visible_state activity.",
3792
3792
  "schema": {
3793
3793
  "type": "object",
3794
3794
  "properties": {
@@ -3830,6 +3830,7 @@
3830
3830
  },
3831
3831
  "history": {
3832
3832
  "type": "object",
3833
+ "description": "Funds-ledger window (deposits, withdrawals and transfers) passed to perpsGetUserHistory; trades and orders are not part of it.",
3833
3834
  "properties": {
3834
3835
  "start_time_ms": {
3835
3836
  "type": "integer",
@@ -4131,7 +4132,7 @@
4131
4132
  "execution_capabilities": []
4132
4133
  },
4133
4134
  "metamask.perps.close_orders": {
4134
- "description": "extension Cancel selected live Perps open orders. Use selector/mode params to cancel matching symbols or all orders. On mainnet, cancel is refused in the fixture-policy lane and needs the funded release lane. Extension ensure_* only assert existing state; converge an owned testnet account through an explicitly funded leaf action.",
4135
+ "description": "extension Cancel selected live Perps open orders. Use selector/mode params to cancel matching symbols or all orders. On mainnet, the fixture-policy lane cancels only orders this run placed; any other cancel needs the funded release lane. Extension ensure_* only assert existing state; converge an owned testnet account through an explicitly funded leaf action.",
4135
4136
  "schema": {
4136
4137
  "type": "object",
4137
4138
  "properties": {
@@ -4252,7 +4253,7 @@
4252
4253
  },
4253
4254
  "order_id": {
4254
4255
  "type": "string",
4255
- "description": "Exact product order ID required for receipt-bound background cancellation."
4256
+ "description": "Exact product order ID required for receipt-bound background cancellation. On mainnet without funding files, only an order this run placed."
4256
4257
  },
4257
4258
  "offset_pct": {
4258
4259
  "type": "number"
@@ -4409,7 +4410,7 @@
4409
4410
  },
4410
4411
  "close_immediately": {
4411
4412
  "type": "boolean",
4412
- "description": "Close the opened position again inside this action and prove the market flat (market orders only; the market must start flat). Required on mainnet, with leverage 1 and notional at most $15 from a fixture account."
4413
+ "description": "Close the opened position again inside this action and prove the market flat (market orders only; the market must start flat). A mainnet market order needs it, with leverage 1 and notional at most $15 from a fixture account; a mainnet limit order takes none (the run cancels it at its end)."
4413
4414
  },
4414
4415
  "provider": {
4415
4416
  "type": "string",
@@ -4421,7 +4422,7 @@
4421
4422
  "mainnet",
4422
4423
  "testnet"
4423
4424
  ],
4424
- "description": "Exact Perps network required before background dispatch; the action never toggles it."
4425
+ "description": "Exact Perps network required before background dispatch; the action never toggles it (metamask.perps.set_network does). A mainnet order is a market order with close_immediately, or a dev1 limit order away from the market at 1x and at most $15 notional at its limit price, which the run cancels at its end (success, failure or abort) and records in perps-mainnet-orders.json."
4425
4426
  },
4426
4427
  "order_type": {
4427
4428
  "type": "string",
@@ -5160,6 +5161,36 @@
5160
5161
  "app-mutation"
5161
5162
  ]
5162
5163
  },
5164
+ "metamask.perps.set_network": {
5165
+ "description": "Switch the Extension's Perps network through the background perpsToggleTestnet request and wait until the app reports it. The fixture loads testnet; network=mainnet is the explicit opt-in and lasts one run: the run's teardown switches back to testnet (success, failure or abort).",
5166
+ "schema": {
5167
+ "type": "object",
5168
+ "properties": {
5169
+ "network": {
5170
+ "type": "string",
5171
+ "enum": ["testnet", "mainnet"],
5172
+ "default": "testnet",
5173
+ "description": "Perps network to select; unchanged when already selected."
5174
+ },
5175
+ "timeout_ms": {
5176
+ "type": "number",
5177
+ "description": "How long to wait for the app to report the new network."
5178
+ }
5179
+ },
5180
+ "additionalProperties": false
5181
+ },
5182
+ "examples": [
5183
+ {
5184
+ "action": "metamask.perps.set_network",
5185
+ "network": "mainnet",
5186
+ "intent": "Switch Perps to mainnet for this run",
5187
+ "next": "done"
5188
+ }
5189
+ ],
5190
+ "execution_capabilities": [
5191
+ "app-mutation"
5192
+ ]
5193
+ },
5163
5194
  "metamask.deeplink.open": {
5164
5195
  "description": "Open an HTTPS deeplink through real browser ingress so the Extension owns parsing, verification, and destination navigation.",
5165
5196
  "execution_capabilities": [
@@ -930,7 +930,7 @@
930
930
  ]
931
931
  },
932
932
  "ui.wait_for": {
933
- "description": "Wait for a React Native testID/test_id or text target. expected=visible requires a measured viewport intersection; expected=present only requires the mounted tree.",
933
+ "description": "Wait for a React Native testID/test_id or text target. expected=visible requires a measured viewport intersection; expected=present only requires the mounted tree. expected=absent also passes when every mounted match is on a screen the navigator hides (an inactive tab, or a non-modal stack screen under a non-modal screen) and a native observation does not show the test ID.",
934
934
  "schema": {
935
935
  "type": "object",
936
936
  "properties": {
@@ -960,7 +960,8 @@
960
960
  "type": "boolean"
961
961
  },
962
962
  "timeout_ms": {
963
- "type": "number"
963
+ "type": "number",
964
+ "description": "Total wait in milliseconds (default 10000). The hidden-screen check for expected=absent needs about 3 s of it left for its native read."
964
965
  }
965
966
  },
966
967
  "additionalProperties": false
@@ -2613,7 +2614,7 @@
2613
2614
  "execution_capabilities": ["app-mutation", "external-mutation"]
2614
2615
  },
2615
2616
  "metamask.perps.close_orders": {
2616
- "description": "mobile Cancel selected live Perps open orders on the app's current Perps network, with no mainnet opt-in. Use selector/mode params to cancel matching symbols or all orders. Pre-existing testnet state on an account the task owns is converged, not refused.",
2617
+ "description": "mobile Cancel selected live Perps open orders on the app's current Perps network; on mainnet only orders this run placed, anything else is refused before dispatch. Use selector/mode params to cancel matching symbols or all orders. Pre-existing testnet state on an account the task owns is converged, not refused.",
2617
2618
  "schema": {
2618
2619
  "type": "object",
2619
2620
  "properties": {
@@ -2818,11 +2819,24 @@
2818
2819
  "mainnet",
2819
2820
  "testnet"
2820
2821
  ],
2821
- "description": "Perps network the order must run on; checked against the live app. A mainnet order must declare mainnet."
2822
+ "description": "Perps network the order must run on; checked against the live app. A mainnet order must declare mainnet: a market order with close_immediately, or a dev1 limit order away from the market at 1x and at most $15 notional at its limit price, which the run cancels at its end (success, failure or abort) and records in perps-mainnet-orders.json."
2822
2823
  },
2823
2824
  "close_immediately": {
2824
2825
  "type": "boolean",
2825
- "description": "Close the opened position again inside this action and prove the market flat (market orders only; the market must start flat). Required on mainnet, with leverage 1 and notional at most $15 from a fixture account."
2826
+ "description": "Close the opened position again inside this action and prove the market flat (market orders only; the market must start flat). A mainnet market order needs it, with leverage 1 and notional at most $15 from a fixture account; a mainnet limit order takes none (the run cancels it at its end)."
2827
+ },
2828
+ "order_type": {
2829
+ "type": "string",
2830
+ "enum": ["market", "limit"],
2831
+ "default": "market"
2832
+ },
2833
+ "limit_price": {
2834
+ "type": ["string", "number"],
2835
+ "description": "Limit price; omit it to offset from the current market price by offset_pct."
2836
+ },
2837
+ "offset_pct": {
2838
+ "type": "number",
2839
+ "description": "Limit price offset from the current market price when limit_price is omitted (default -30 for a buy, 30 for a sell)."
2826
2840
  }
2827
2841
  },
2828
2842
  "additionalProperties": false
@@ -3141,7 +3155,7 @@
3141
3155
  "execution_capabilities": ["app-mutation", "external-mutation"]
3142
3156
  },
3143
3157
  "metamask.perps.ensure_orders": {
3144
- "description": "mobile Converge selected testnet orders to open or absent, then independently assert the final state. Creating an open state uses a resting limit order, which the shared Perps order rule refuses on mainnet; cancelling acts on the app's current Perps network with no mainnet opt-in. Pre-existing testnet state on an account the task owns is converged, not refused.",
3158
+ "description": "mobile Converge selected testnet orders to open or absent, then independently assert the final state. Creating an open state uses a resting limit order, which the shared Perps order rule refuses on mainnet; cancelling acts on the app's current Perps network, and on mainnet only on orders this run placed. Pre-existing testnet state on an account the task owns is converged, not refused.",
3145
3159
  "schema": {
3146
3160
  "type": "object",
3147
3161
  "properties": {
@@ -3510,7 +3524,7 @@
3510
3524
  "network": {
3511
3525
  "type": "string",
3512
3526
  "enum": ["testnet", "mainnet"],
3513
- "description": "Requested Perps network. Omit it to keep the current network unless a named profile supplies one."
3527
+ "description": "Requested Perps network; network=mainnet is the opt-in for mainnet orders. Omit it to keep the current network unless a named profile supplies one."
3514
3528
  },
3515
3529
  "market": {
3516
3530
  "type": "string",
@@ -3544,7 +3558,7 @@
3544
3558
  "trading_state": {
3545
3559
  "type": "string",
3546
3560
  "enum": ["preserve", "clean_selected_testnet_market"],
3547
- "description": "High-level state policy. The clean policy removes matching positions, ordinary orders, TWAPs, and Chase sessions only on testnet and preserves them on mainnet."
3561
+ "description": "High-level state policy that sets positions, orders and strategies itself: preserve leaves them untouched, and the clean policy removes matching positions, ordinary orders, TWAPs, and Chase sessions only on testnet and preserves them on mainnet. Cannot be combined with an explicit positions, orders or strategies request other than false; omit it to apply such a request."
3548
3562
  },
3549
3563
  "positions": {
3550
3564
  "description": "Requested position state. Omit it or pass false to preserve current positions.",
@@ -3768,7 +3782,19 @@
3768
3782
  "additionalProperties": false
3769
3783
  },
3770
3784
  "timeout_ms": {
3771
- "type": "number"
3785
+ "type": "number",
3786
+ "minimum": 1,
3787
+ "description": "Overall budget for the whole start state setup: every phase runs inside it. Default 30000 on emulators and simulators, 60000 on a physical Android device (explicit non-emulator adb serial), or MM_HARNESS_MOBILE_SETUP_TIMEOUT_MS. A phase that runs out fails as DOMAIN_SETUP_DEADLINE naming the phase."
3788
+ },
3789
+ "navigation_timeout_ms": {
3790
+ "type": "number",
3791
+ "minimum": 1,
3792
+ "description": "Slice of the setup budget for each navigation phase. Default 15000 on emulators and simulators, 30000 on a physical Android device, or MM_HARNESS_MOBILE_NAVIGATION_TIMEOUT_MS."
3793
+ },
3794
+ "cdp_timeout_ms": {
3795
+ "type": "number",
3796
+ "minimum": 1,
3797
+ "description": "Limit for each bridge command inside the setup, capped by the phase deadline. Default 15000. A command that hits it fails as CDP_TIMEOUT (CDP_COMMAND_TIMEOUT in the message)."
3772
3798
  },
3773
3799
  "side": {
3774
3800
  "type": "string",
@@ -4104,7 +4130,19 @@
4104
4130
  "additionalProperties": false
4105
4131
  },
4106
4132
  "timeout_ms": {
4107
- "type": "number"
4133
+ "type": "number",
4134
+ "minimum": 1,
4135
+ "description": "Overall budget for the whole teardown: every phase runs inside it. Default 30000 on emulators and simulators, 60000 on a physical Android device (explicit non-emulator adb serial), or MM_HARNESS_MOBILE_SETUP_TIMEOUT_MS. A phase that runs out fails as DOMAIN_SETUP_DEADLINE naming the phase."
4136
+ },
4137
+ "navigation_timeout_ms": {
4138
+ "type": "number",
4139
+ "minimum": 1,
4140
+ "description": "Slice of the setup budget for each navigation phase. Default 15000 on emulators and simulators, 30000 on a physical Android device, or MM_HARNESS_MOBILE_NAVIGATION_TIMEOUT_MS."
4141
+ },
4142
+ "cdp_timeout_ms": {
4143
+ "type": "number",
4144
+ "minimum": 1,
4145
+ "description": "Limit for each bridge command inside the setup, capped by the phase deadline. Default 15000. A command that hits it fails as CDP_TIMEOUT (CDP_COMMAND_TIMEOUT in the message)."
4108
4146
  },
4109
4147
  "side": {
4110
4148
  "type": "string",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deeeed/metamask-harness",
3
- "version": "0.86.2",
3
+ "version": "0.88.0",
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.7",
25
- "@farmslot/adapter-rn": "0.19.4",
26
- "@farmslot/adapter-sdk": "0.8.1",
27
- "@farmslot/adapter-web": "0.6.5",
28
- "@farmslot/agent-runtime": "0.17.1",
24
+ "@farmslot/adapter-node": "0.1.11",
25
+ "@farmslot/adapter-rn": "0.19.9",
26
+ "@farmslot/adapter-sdk": "0.8.5",
27
+ "@farmslot/adapter-web": "0.6.9",
28
+ "@farmslot/agent-runtime": "0.17.5",
29
29
  "@farmslot/handoff": "^0.3.1",
30
- "@farmslot/protocol": "0.35.0",
31
- "@farmslot/recipe-cli": "0.11.1",
32
- "@farmslot/recipe-runner": "0.28.1",
30
+ "@farmslot/protocol": "0.37.0",
31
+ "@farmslot/recipe-cli": "0.12.0",
32
+ "@farmslot/recipe-runner": "0.28.5",
33
33
  "agent-device": "0.19.3",
34
34
  "commander": "^12.0.0",
35
35
  "esbuild": "0.28.1",
@@ -55,6 +55,11 @@
55
55
  },
56
56
  "main": "./dist/index.js",
57
57
  "exports": {
58
+ "./provider": {
59
+ "development": "./src/provider.ts",
60
+ "import": "./dist/provider.js",
61
+ "default": "./dist/provider.js"
62
+ },
58
63
  ".": {
59
64
  "development": "./src/index.ts",
60
65
  "import": "./dist/index.js",
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
- import { validateRecipeArtifactPackage } from '@farmslot/protocol';
4
+ import { recipeTraceEntries, validateRecipeArtifactPackage } from '@farmslot/protocol';
5
+ import { CAPTURE_EVIDENCE_INCOMPLETE, onlyCaptureInterrupted } from '@farmslot/recipe-runner';
5
6
 
6
7
  const options = parseArgs(process.argv.slice(2));
7
8
  const inventoryPath = path.resolve(options.inventory);
@@ -71,6 +72,7 @@ for (const entry of verdictCases) {
71
72
  errors.push(`${entry.id ?? '<missing-id>'} must include evidence`);
72
73
  continue;
73
74
  }
75
+ const interruptedRuns = [];
74
76
  for (const evidence of entry.evidence) {
75
77
  if (typeof evidence !== 'string' || evidence.length === 0) {
76
78
  errors.push(`${entry.id ?? '<missing-id>'} contains invalid evidence`);
@@ -121,7 +123,8 @@ for (const entry of verdictCases) {
121
123
  }
122
124
  documents[required] = requiredDocument.value;
123
125
  }
124
- if (documents['summary.json'] && entry.status === 'PASS' && documents['summary.json'].status !== 'pass') {
126
+ const captureInterrupted = onlyCaptureInterrupted(recipeTraceEntries(documents['trace.json']) ?? []);
127
+ if (documents['summary.json'] && entry.status === 'PASS' && documents['summary.json'].status !== 'pass' && !captureInterrupted) {
125
128
  errors.push(`${entry.id ?? '<missing-id>'} PASS evidence summary.json must have status pass: ${resolved}`);
126
129
  }
127
130
  if (requiredDocuments.every((required) => required in documents)) {
@@ -147,11 +150,20 @@ for (const entry of verdictCases) {
147
150
  .join(', ');
148
151
  errors.push(`${entry.id ?? '<missing-id>'} evidence directory is not a valid Recipe Protocol artifact package: ${resolved} (${codes})`);
149
152
  }
153
+ interruptedRuns.push(captureInterrupted);
150
154
  }
151
155
  } else if (!stat.isFile()) {
152
156
  errors.push(`${entry.id ?? '<missing-id>'} evidence must be a file or run directory: ${resolved}`);
153
157
  }
154
158
  }
159
+ // Retained interrupted attempts must not override a retry's product verdict.
160
+ if (interruptedRuns.length > 0 && interruptedRuns.every(Boolean)) {
161
+ if (entry.status !== 'BLOCKED_ENVIRONMENT') {
162
+ errors.push(`${entry.id ?? '<missing-id>'} evidence failed only because its recording was interrupted (CAPTURE_INTERRUPTED): report BLOCKED_ENVIRONMENT, ${CAPTURE_EVIDENCE_INCOMPLETE}, with the partial video, not ${entry.status}`);
163
+ } else if (!entry.summary.includes(CAPTURE_EVIDENCE_INCOMPLETE)) {
164
+ errors.push(`${entry.id ?? '<missing-id>'} summary must say "${CAPTURE_EVIDENCE_INCOMPLETE}" for a case whose recordings were interrupted`);
165
+ }
166
+ }
155
167
  }
156
168
 
157
169
  const counts = Object.fromEntries([...allowedStatuses].map((status) => [
@@ -201,7 +201,8 @@
201
201
  "Analytics: verify the collection destination and existing consent before capture. Changing participation or marketing consent requires operator approval and restoration of the prior settings; a recipe's hardcoded opt-in is not permission. Missing collection prerequisites are not missing product events.",
202
202
  "Flake risk: wait on observable state instead of sleeping, keep device and runtime identity explicit, and never overwrite a prior run's artifacts.",
203
203
  "Mutation: require an independent state assertion and identity-bound receipt. Bind activity to that operation's accepted ID; an old row or nonzero count is insufficient. Opening and closing orders have different IDs. Schedule authorized cleanup in teardown, verify its result, and report any residue. Testnet cleanup closes what the task opened and never silently undoes another task's live proof on a reserved account or market.",
204
- "Testnet account state: on a testnet account or market the task owns for its run (the slot's fixture account, or one reserved with exclusive_testnet_market=true), converging is an explicit, ownership-scoped setup choice, not a violation. Use the team library's perps.ensure-market-state or perps.clean-market-testnet, or on Mobile and Core metamask.perps.ensure_positions / metamask.perps.ensure_orders (mode=all for strays), record what was closed in the trace, then prove. A pre-existing position or order on an account the task owns is never a reason to block, skip, or refuse, and a fixture that demands an empty account must converge rather than refuse. Extension ensure_* and teardown_state only assert; converge there through an explicitly funded leaf action. Never broaden cleanup onto an account or market the task does not own; obtain an owned one instead of blocking. On mainnet, orders follow the shared Perps order rule (PERPS_MAINNET_ORDER_REFUSED), Core needs CORE_PERPS_ALLOW_MAINNET_WRITES=1 for any write, Mobile cleanup acts on whatever network the app is on, and Extension refuses mainnet writes other than orders and `close_visible_position` in the fixture-policy lane.",
204
+ "Testnet account state: on a testnet account or market the task owns for its run (the slot's fixture account, or one reserved with exclusive_testnet_market=true), converging is an explicit, ownership-scoped setup choice, not a violation. Use the team library's perps.ensure-market-state or perps.clean-market-testnet, or on Mobile and Core metamask.perps.ensure_positions / metamask.perps.ensure_orders (mode=all for strays), record what was closed in the trace, then prove. A pre-existing position or order on an account the task owns is never a reason to block, skip, or refuse, and a fixture that demands an empty account must converge rather than refuse. Extension ensure_* and teardown_state only assert; converge there through an explicitly funded leaf action. Never broaden cleanup onto an account or market the task does not own; obtain an owned one instead of blocking. On mainnet, orders follow the shared Perps order rule (PERPS_MAINNET_ORDER_REFUSED), Core needs CORE_PERPS_ALLOW_MAINNET_WRITES=1 for any write, Mobile close_positions acts on whatever network the app is on, close_orders there cancels only the run's own orders, and Extension refuses mainnet writes other than orders, `close_visible_position` and cancelling the run's own orders in the fixture-policy lane.",
205
+ "Rate limits: RATE_LIMITED means Hyperliquid answered 429 Too Many Requests and bounded retries did not get through; it says nothing about the product. Wait a minute and rerun a read. A placement left outcome-unknown is never rerun before the market's positions, orders and fills are checked.",
205
206
  {
206
207
  "text": "An accepted order can fill before the UI updates. Preserve its receipt even when the expected resting-order assertion fails; one empty read does not prove cleanup. Check the read API's cache and freshness options before adding polling. Reconcile returned margin against the pre-placement balance. A successful close does not turn the original failed proof into a pass.",
207
208
  "adapters": [