@deeeed/metamask-harness 0.47.4 → 0.49.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 (261) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +3 -2
  3. package/adapters/extension/lib/macos-focus.cjs +2 -2
  4. package/adapters/extension/live.sh +6 -8
  5. package/adapters/mobile/bridge-runtime/lib/bridge-errors.cjs +1 -0
  6. package/adapters/shared/ensure-runner-deps.sh +6 -0
  7. package/dist/adapters/extension/runtime-decision.js +18 -1
  8. package/dist/adapters/extension/runtime.js +5 -5
  9. package/dist/adapters/extension/surface.js +1 -0
  10. package/dist/app-lifecycle.js +3 -1
  11. package/dist/command-contract.js +2 -0
  12. package/dist/commands/call.js +1 -8
  13. package/dist/commands/device-target.js +12 -3
  14. package/dist/commands/doctor.js +7 -5
  15. package/dist/commands/fixtures.js +1 -1
  16. package/dist/commands/help.js +17 -5
  17. package/dist/commands/launch/index.js +10 -0
  18. package/dist/commands/manifest.js +3 -1
  19. package/dist/commands/mobile-device-view.js +13 -3
  20. package/dist/commands/parse-args.js +2 -0
  21. package/dist/commands/run-engine.js +29 -4
  22. package/dist/commands/run.js +1 -1
  23. package/dist/commands/runtime-launch.js +10 -1
  24. package/dist/commands/status.js +8 -4
  25. package/dist/devices.js +31 -9
  26. package/dist/execution-provenance.js +8 -7
  27. package/dist/funding-execution-context.js +62 -17
  28. package/dist/heal-bounds.js +3 -3
  29. package/dist/live-adapter-contract.js +4 -0
  30. package/dist/manifest.js +2 -13
  31. package/dist/metamask-action-validation.js +3 -2
  32. package/dist/mm-harness-cli.js +4 -2
  33. package/dist/runner.js +14 -3
  34. package/docs/RECIPES.md +135 -24
  35. package/docs/RELEASE-QA-CAPABILITY-MAP.md +3 -2
  36. package/library/actions/core/perps/_controller.mjs +24 -24
  37. package/library/actions/core/perps/assert_orders.mjs +3 -5
  38. package/library/actions/core/perps/assert_positions.mjs +3 -5
  39. package/library/actions/core/perps/close_orders.mjs +39 -19
  40. package/library/actions/core/perps/close_positions.mjs +53 -12
  41. package/library/actions/core/perps/edit_order.mjs +2 -0
  42. package/library/actions/core/perps/ensure_orders.mjs +3 -4
  43. package/library/actions/core/perps/ensure_positions.mjs +4 -5
  44. package/library/actions/core/perps/place_order.mjs +7 -14
  45. package/library/actions/core/perps/read_account.mjs +2 -0
  46. package/library/actions/core/perps/read_orders.mjs +2 -0
  47. package/library/actions/core/perps/read_positions.mjs +3 -1
  48. package/library/actions/core/perps/read_snapshot.mjs +3 -0
  49. package/library/actions/core/perps/start_state.mjs +3 -14
  50. package/library/actions/core/perps/teardown_state.mjs +3 -14
  51. package/library/actions/core/perps/update_position_tpsl.mjs +13 -19
  52. package/library/actions/core/wallet/list_accounts.mjs +3 -0
  53. package/library/actions/extension/analytics/consent.mjs +2 -0
  54. package/library/actions/extension/analytics/set_consent.mjs +2 -0
  55. package/library/actions/extension/assets/home-token-identity.mjs +31 -0
  56. package/library/actions/extension/assets/import_custom_token.mjs +63 -79
  57. package/library/actions/extension/assets/open_details.mjs +19 -7
  58. package/library/actions/extension/assets/prepare_send.mjs +1 -0
  59. package/library/actions/extension/assets/read_details.mjs +62 -29
  60. package/library/actions/extension/assets/read_visible_state.mjs +5 -1
  61. package/library/actions/extension/assets/review_send.mjs +1 -0
  62. package/library/actions/extension/assets/select_network_scope.mjs +17 -9
  63. package/library/actions/extension/assets/send.mjs +2 -0
  64. package/library/actions/extension/assets/set_custom_gas.mjs +1 -0
  65. package/library/actions/extension/assets/set_token_visibility.mjs +86 -17
  66. package/library/actions/extension/assets/verify_sorting.mjs +63 -14
  67. package/library/actions/extension/deeplink/open.mjs +1 -0
  68. package/library/actions/extension/networks/add_chainlist.mjs +2 -0
  69. package/library/actions/extension/networks/add_custom.mjs +21 -17
  70. package/library/actions/extension/networks/custom_network.mjs +10 -3
  71. package/library/actions/extension/networks/read_visible_state.mjs +2 -0
  72. package/library/actions/extension/networks/remove_custom.mjs +1 -0
  73. package/library/actions/extension/performance/_navigation-memory.mjs +37 -6
  74. package/library/actions/extension/performance/compare_idle_navigation_memory.mjs +5 -2
  75. package/library/actions/extension/performance/measure_detached_dom.mjs +5 -2
  76. package/library/actions/extension/performance/measure_navigation_memory.mjs +5 -2
  77. package/library/actions/extension/perps/assert_orders.mjs +1 -0
  78. package/library/actions/extension/perps/assert_positions.mjs +1 -0
  79. package/library/actions/extension/perps/assert_visible_consistency.mjs +11 -4
  80. package/library/actions/extension/perps/close_orders.mjs +4 -1
  81. package/library/actions/extension/perps/close_positions.mjs +1 -0
  82. package/library/actions/extension/perps/close_visible_position.mjs +160 -11
  83. package/library/actions/extension/perps/compare_provider_market.mjs +2 -0
  84. package/library/actions/extension/perps/edit_margin.mjs +3 -1
  85. package/library/actions/extension/perps/ensure_orders.mjs +1 -0
  86. package/library/actions/extension/perps/ensure_positions.mjs +1 -0
  87. package/library/actions/extension/perps/mutation-receipt.mjs +54 -3
  88. package/library/actions/extension/perps/open_balance_action.mjs +17 -7
  89. package/library/actions/extension/perps/open_position_action.mjs +1 -0
  90. package/library/actions/extension/perps/perps.mjs +550 -69
  91. package/library/actions/extension/perps/place_order.mjs +1 -0
  92. package/library/actions/extension/perps/read_eligibility.mjs +1 -0
  93. package/library/actions/extension/perps/read_funds_confirmation.mjs +10 -0
  94. package/library/actions/extension/perps/read_orders.mjs +1 -0
  95. package/library/actions/extension/perps/read_positions.mjs +1 -0
  96. package/library/actions/extension/perps/read_snapshot.mjs +4 -1
  97. package/library/actions/extension/perps/read_visible_state.mjs +12 -6
  98. package/library/actions/extension/perps/search_markets.mjs +8 -2
  99. package/library/actions/extension/perps/select_activity_filter.mjs +2 -1
  100. package/library/actions/extension/perps/select_market_filter.mjs +75 -45
  101. package/library/actions/extension/perps/set_market_favorite.mjs +7 -3
  102. package/library/actions/extension/perps/state.mjs +2 -0
  103. package/library/actions/extension/perps/update_position_tpsl.mjs +3 -1
  104. package/library/actions/extension/perps/visible-mutation-identity.mjs +6 -2
  105. package/library/actions/extension/platform/cdp.mjs +50 -142
  106. package/library/actions/extension/settings/set_basic_functionality.mjs +1 -0
  107. package/library/actions/extension/swap_bridge/quote-state.mjs +56 -0
  108. package/library/actions/extension/swap_bridge/read_visible_state.mjs +5 -1
  109. package/library/actions/extension/swap_bridge/select_assets.mjs +12 -1
  110. package/library/actions/extension/swap_bridge/set_amount.mjs +11 -8
  111. package/library/actions/extension/swap_bridge/set_max_amount.mjs +18 -18
  112. package/library/actions/extension/swap_bridge/set_slippage.mjs +1 -9
  113. package/library/actions/extension/ui/locators.mjs +1 -0
  114. package/library/actions/extension/ui/navigate.mjs +60 -14
  115. package/library/actions/extension/wallet/ensure_unlocked.mjs +2 -0
  116. package/library/actions/extension/wallet/import.mjs +2 -0
  117. package/library/actions/extension/wallet/list_accounts.mjs +1 -0
  118. package/library/actions/extension/wallet/lock.mjs +4 -1
  119. package/library/actions/extension/wallet/read_state.mjs +1 -0
  120. package/library/actions/extension/wallet/reset.mjs +2 -0
  121. package/library/actions/extension/wallet/secret-input.mjs +2 -0
  122. package/library/actions/extension/wallet/select_account.mjs +2 -0
  123. package/library/actions/extension/wallet/setup.mjs +1 -0
  124. package/library/actions/extension/wallet/state.mjs +2 -0
  125. package/library/actions/extension/wallet/visible-session.mjs +2 -0
  126. package/library/actions/mobile/analytics/consent-settings.mjs +1 -0
  127. package/library/actions/mobile/analytics/set_consent.mjs +1 -0
  128. package/library/actions/mobile/app/network-control.mjs +2 -0
  129. package/library/actions/mobile/app/network.mjs +1 -0
  130. package/library/actions/mobile/app/network_assert.mjs +1 -0
  131. package/library/actions/mobile/app/network_capture.mjs +1 -0
  132. package/library/actions/mobile/assets/import_custom_token.mjs +3 -0
  133. package/library/actions/mobile/assets/open_details.mjs +1 -0
  134. package/library/actions/mobile/assets/read_details.mjs +1 -0
  135. package/library/actions/mobile/assets/read_visible_state.mjs +4 -1
  136. package/library/actions/mobile/assets/set_token_visibility.mjs +15 -9
  137. package/library/actions/mobile/assets/verify_sorting.mjs +90 -7
  138. package/library/actions/mobile/deeplink/open.mjs +1 -0
  139. package/library/actions/mobile/networks/add_custom.mjs +1 -0
  140. package/library/actions/mobile/networks/network-management.mjs +23 -15
  141. package/library/actions/mobile/networks/read_visible_state.mjs +1 -0
  142. package/library/actions/mobile/networks/remove_custom.mjs +7 -2
  143. package/library/actions/mobile/perps/assert_orders.mjs +1 -0
  144. package/library/actions/mobile/perps/assert_positions.mjs +1 -0
  145. package/library/actions/mobile/perps/capture_performance.mjs +1 -0
  146. package/library/actions/mobile/perps/close_orders.mjs +1 -0
  147. package/library/actions/mobile/perps/close_positions.mjs +1 -0
  148. package/library/actions/mobile/perps/ensure_orders.mjs +1 -0
  149. package/library/actions/mobile/perps/ensure_positions.mjs +1 -0
  150. package/library/actions/mobile/perps/measure_homepage_visible.mjs +1 -0
  151. package/library/actions/mobile/perps/performance-capture.mjs +2 -0
  152. package/library/actions/mobile/perps/perps.mjs +8 -1
  153. package/library/actions/mobile/perps/place_order.mjs +1 -0
  154. package/library/actions/mobile/perps/prepare_local_snapshot_endpoint.mjs +2 -0
  155. package/library/actions/mobile/perps/read-visible-state-loop.mjs +3 -0
  156. package/library/actions/mobile/perps/read_orders.mjs +1 -0
  157. package/library/actions/mobile/perps/read_positions.mjs +1 -0
  158. package/library/actions/mobile/perps/read_visible_state.mjs +1 -0
  159. package/library/actions/mobile/perps/search_markets.mjs +1 -0
  160. package/library/actions/mobile/perps/set_market_favorite.mjs +17 -19
  161. package/library/actions/mobile/platform/bridge.mjs +16 -4
  162. package/library/actions/mobile/platform/native-session-name.mjs +1 -0
  163. package/library/actions/mobile/platform/native-session.mjs +28 -17
  164. package/library/actions/mobile/platform/observe-ui.mjs +4 -2
  165. package/library/actions/mobile/platform/tool-paths.mjs +1 -0
  166. package/library/actions/mobile/swap_bridge/native-session.mjs +1 -0
  167. package/library/actions/mobile/swap_bridge/read_visible_state.mjs +1 -0
  168. package/library/actions/mobile/swap_bridge/select_assets.mjs +2 -0
  169. package/library/actions/mobile/swap_bridge/set_amount.mjs +1 -0
  170. package/library/actions/mobile/swap_bridge/set_max_amount.mjs +1 -0
  171. package/library/actions/mobile/swap_bridge/set_slippage.mjs +1 -0
  172. package/library/actions/mobile/swap_bridge/submit_transaction.mjs +2 -0
  173. package/library/actions/mobile/ui/locators.mjs +1 -0
  174. package/library/actions/mobile/ui/native-navigation.mjs +2 -0
  175. package/library/actions/mobile/ui/navigate.mjs +3 -1
  176. package/library/actions/mobile/wallet/ensure_unlocked.mjs +2 -0
  177. package/library/actions/mobile/wallet/home.mjs +1 -0
  178. package/library/actions/mobile/wallet/import.mjs +2 -0
  179. package/library/actions/mobile/wallet/list_accounts.mjs +1 -0
  180. package/library/actions/mobile/wallet/lock.mjs +1 -0
  181. package/library/actions/mobile/wallet/native-ui.mjs +2 -0
  182. package/library/actions/mobile/wallet/read_state.mjs +2 -0
  183. package/library/actions/mobile/wallet/reset-helper.mjs +1 -0
  184. package/library/actions/mobile/wallet/reset.mjs +1 -0
  185. package/library/actions/mobile/wallet/select_account.mjs +2 -0
  186. package/library/actions/mobile/wallet/setup.mjs +2 -0
  187. package/library/actions/shared/analytics/assert_events.mjs +3 -0
  188. package/library/actions/shared/analytics/consent.mjs +3 -0
  189. package/library/actions/shared/analytics/read_events.mjs +2 -0
  190. package/library/actions/shared/analytics/start_capture.mjs +3 -0
  191. package/library/actions/shared/app/network-artifact.mjs +2 -0
  192. package/library/actions/shared/app/network-assert.mjs +2 -0
  193. package/library/actions/shared/assets/visible-state.mjs +2 -0
  194. package/library/actions/shared/deeplink/url.mjs +3 -0
  195. package/library/actions/shared/networks/visible-state.mjs +2 -0
  196. package/library/actions/shared/perps/visible-state.mjs +16 -3
  197. package/library/actions/shared/swap-bridge/transaction.mjs +3 -0
  198. package/library/actions/shared/swap-bridge/visible-state.mjs +2 -0
  199. package/library/actions/shared/ui/locators.mjs +2 -0
  200. package/library/actions/shared/wallet/import-source.mjs +3 -0
  201. package/library/manifests/core.action-manifest.json +34 -2
  202. package/library/manifests/extension.action-manifest.json +50 -17
  203. package/library/manifests/mobile.action-manifest.json +11 -10
  204. package/library/recipes/core/perps/smoke.recipe.json +23 -0
  205. package/library/recipes/core/perps/snapshot.recipe.json +26 -1
  206. package/library/recipes/extension/assets/release-custom-network-token-persistence.recipe.json +25 -8
  207. package/library/recipes/extension/performance/navigation-memory-suite.recipe.json +219 -0
  208. package/library/recipes/extension/performance/navigation-memory.recipe.json +61 -156
  209. package/library/recipes/extension/wallet/smoke.recipe.json +17 -8
  210. package/library/recipes/mobile/app/lifecycle-smoke.recipe.json +89 -0
  211. package/library/recipes/mobile/networks/release-custom-network-persistence.recipe.json +31 -6
  212. package/library/recipes/mobile/networks/release-multichain-visibility.recipe.json +5 -4
  213. package/library/recipes/mobile/wallet/smoke.recipe.json +17 -8
  214. package/library/recipes/{assets → shared/assets}/release-token-address-import.recipe.json +7 -0
  215. package/library/recipes/{assets → shared/assets}/release-token-details.recipe.json +2 -2
  216. package/library/recipes/shared/assets/release-token-hide-readd.recipe.json +91 -0
  217. package/library/recipes/{assets → shared/assets}/release-token-persistence.recipe.json +3 -0
  218. package/library/recipes/{assets → shared/assets}/release-token-search-import.recipe.json +4 -1
  219. package/library/recipes/{assets → shared/assets}/release-token-sorting.recipe.json +13 -2
  220. package/library/recipes/{swap-bridge → shared/swap-bridge}/release-quote.recipe.json +29 -3
  221. package/package.json +3 -3
  222. package/site/assets/help-recipes.json +220 -27
  223. package/library/recipes/assets/release-token-hide-readd.recipe.json +0 -57
  224. package/library/recipes/core/perps/clean-market-testnet.recipe.json +0 -44
  225. package/library/recipes/core/perps/order-lifecycle.recipe.json +0 -82
  226. package/library/recipes/core/perps/read-markets.recipe.json +0 -36
  227. package/library/recipes/core/perps/trading-lifecycle.recipe.json +0 -80
  228. package/library/recipes/extension/perps/release-activity-filters.recipe.json +0 -51
  229. package/library/recipes/extension/perps/release-funds-flows.recipe.json +0 -76
  230. package/library/recipes/extension/perps/release-live-limit-order.recipe.json +0 -109
  231. package/library/recipes/extension/perps/release-market-filters.recipe.json +0 -97
  232. package/library/recipes/extension/perps/release-order-entry.recipe.json +0 -158
  233. package/library/recipes/extension/perps/release-order-validation.recipe.json +0 -259
  234. package/library/recipes/extension/perps/release-visible-consistency.recipe.json +0 -47
  235. package/library/recipes/extension/perps/source-dev-snapshot-consistency.recipe.json +0 -76
  236. package/library/recipes/mobile/app/lifecycle.android-smoke.recipe.json +0 -69
  237. package/library/recipes/mobile/perps/chase-assert-running.recipe.json +0 -110
  238. package/library/recipes/mobile/perps/chase-place.recipe.json +0 -145
  239. package/library/recipes/mobile/perps/chase-terminate.recipe.json +0 -98
  240. package/library/recipes/mobile/perps/performance.recipe.json +0 -722
  241. package/library/recipes/mobile/perps/pro-order-setup.recipe.json +0 -111
  242. package/library/recipes/mobile/perps/pro-order-start-state.recipe.json +0 -53
  243. package/library/recipes/mobile/perps/scale-assert-orders.recipe.json +0 -126
  244. package/library/recipes/mobile/perps/scale-place.recipe.json +0 -186
  245. package/library/recipes/mobile/perps/twap-assert-active.recipe.json +0 -97
  246. package/library/recipes/mobile/perps/twap-place.recipe.json +0 -146
  247. package/library/recipes/mobile/runner/action-validation.recipe.json +0 -336
  248. package/library/recipes/perps/clean-market-testnet.recipe.json +0 -49
  249. package/library/recipes/perps/lifecycle.recipe.json +0 -144
  250. package/library/recipes/perps/release-live-market-order.recipe.json +0 -139
  251. package/library/recipes/perps/release-watchlist.recipe.json +0 -111
  252. package/library/recipes/swap-bridge/release-max-quote.recipe.json +0 -72
  253. /package/library/recipes/{assets → shared/assets}/release-token-list.recipe.json +0 -0
  254. /package/library/recipes/{perps → shared/perps}/release-market-details.recipe.json +0 -0
  255. /package/library/recipes/{perps → shared/perps}/release-market-search.recipe.json +0 -0
  256. /package/library/recipes/{swap-bridge → shared/swap-bridge}/release-custom-slippage.recipe.json +0 -0
  257. /package/library/recipes/{swap-bridge → shared/swap-bridge}/release-live-max-swap.recipe.json +0 -0
  258. /package/library/recipes/{swap-bridge → shared/swap-bridge}/release-live-swap.recipe.json +0 -0
  259. /package/library/recipes/{swap-bridge → shared/swap-bridge}/release-surface.recipe.json +0 -0
  260. /package/library/recipes/{wallet → shared/wallet}/import.recipe.json +0 -0
  261. /package/library/recipes/{wallet → shared/wallet}/reset-import.recipe.json +0 -0
@@ -1,3 +1,4 @@
1
+ // Delete a custom network through native Settings and wait for its row to disappear.
1
2
  import { runAdapter } from '../platform/bridge.mjs';
2
3
  import {
3
4
  closeNativeSession,
@@ -32,6 +33,8 @@ runAdapter(async (input) => {
32
33
  (snapshot) => {
33
34
  const container = nativeNode(snapshot, 'network-details-view-container');
34
35
  if (!container) return null;
36
+ const remove = nativeNode(snapshot, 'network-details-view-remove-network-button');
37
+ if (remove?.enabled === true && remove.visibleToUser !== false && remove.hittable === true) return remove;
35
38
  const labeled = snapshot.nodes.find((node) =>
36
39
  node.hittable === true && /delete|remove|trash/iu.test(nativeLabel(node))
37
40
  );
@@ -84,8 +87,10 @@ runAdapter(async (input) => {
84
87
  (snapshot) => nativeNode(snapshot, 'networks-settings-container'),
85
88
  'Mobile Networks settings did not reopen after deletion.',
86
89
  );
87
- if (await findNetwork(session, network, chainId, timeoutMs)) {
88
- throw new Error(`${network} (${chainId}) remained after deletion.`);
90
+ const removalDeadline = Date.now() + timeoutMs;
91
+ while (await findNetwork(session, network, chainId, Math.max(1, removalDeadline - Date.now()))) {
92
+ if (Date.now() >= removalDeadline) throw new Error(`${network} (${chainId}) remained after deletion.`);
93
+ await new Promise((resolve) => setTimeout(resolve, 250));
89
94
  }
90
95
  return {
91
96
  action: input.action,
@@ -1,3 +1,4 @@
1
+ // Assert that selected Perps orders are open or absent using the Mobile controller.
1
2
  import { runAdapter } from '../platform/bridge.mjs';
2
3
  import { assertOrders } from './perps.mjs';
3
4
 
@@ -1,3 +1,4 @@
1
+ // Assert that selected Perps positions are open or absent using the Mobile controller.
1
2
  import { runAdapter } from '../platform/bridge.mjs';
2
3
  import { assertPositions } from './perps.mjs';
3
4
 
@@ -1,3 +1,4 @@
1
+ // Start or end a Perps performance log capture, writing artifacts and checking required events at end.
1
2
  import { runAdapter } from '../platform/bridge.mjs';
2
3
  import { capturePerpsPerformance } from './performance-capture.mjs';
3
4
 
@@ -1,3 +1,4 @@
1
+ // Cancel selected Perps orders through the Mobile controller and assert their absence.
1
2
  import { runAdapter } from '../platform/bridge.mjs';
2
3
  import { assertOrders, closeOrders } from './perps.mjs';
3
4
 
@@ -1,3 +1,4 @@
1
+ // Close selected Perps positions through the Mobile controller and assert their absence.
1
2
  import { runAdapter } from '../platform/bridge.mjs';
2
3
  import { assertPositions, closePositions } from './perps.mjs';
3
4
 
@@ -1,3 +1,4 @@
1
+ // Create a limit order when none matches, or cancel selected orders, then assert the requested state.
1
2
  import { runAdapter } from '../platform/bridge.mjs';
2
3
  import { ensureOrders } from './perps.mjs';
3
4
 
@@ -1,3 +1,4 @@
1
+ // Submit an order when no position matches, or close selected positions, then assert the requested state.
1
2
  import { runAdapter } from '../platform/bridge.mjs';
2
3
  import { ensurePositions } from './perps.mjs';
3
4
 
@@ -1,3 +1,4 @@
1
+ // Unlock the wallet and measure the scroll transition until a requested homepage target becomes visible.
1
2
  import { runAdapter } from '../platform/bridge.mjs';
2
3
  import { measureHomepageVisible } from './perps.mjs';
3
4
 
@@ -1,3 +1,5 @@
1
+ // Capture a bounded section of Perps console logs and write performance measurements and validation artifacts.
2
+ // Persist the start offset and file identity to reject replaced or rewritten logs when the capture ends.
1
3
  import { createHash } from "node:crypto";
2
4
  import { mkdir, open, readFile, rm, stat, writeFile } from "node:fs/promises";
3
5
  import path from "node:path";
@@ -1,3 +1,5 @@
1
+ // Implement Mobile Perps navigation, trading mutations, assertions, and setup or cleanup through the bridge.
2
+ // Strategy getters used during cleanup can settle tracked collateral.
1
3
  import { pathToFileURL } from 'node:url';
2
4
  import {
3
5
  MOBILE_BRIDGE_RESULT_ERROR_CODES,
@@ -1770,6 +1772,9 @@ async function applyTutorialState(input, config) {
1770
1772
 
1771
1773
 
1772
1774
  export async function startState(input) {
1775
+ if (input.node?.account !== undefined && (typeof input.node.account !== 'string' || !input.node.account.trim())) {
1776
+ throw new Error('Perps start state requires a non-empty account name or address when account is supplied.\nNext: mm-harness actions metamask.perps.start_state --adapter mobile --json');
1777
+ }
1773
1778
  const params = paramsForState(input);
1774
1779
  const config = applyTradingStatePolicy(
1775
1780
  mergeStateConfig(profileDefaults(params.profile), params),
@@ -1828,7 +1833,9 @@ export async function startState(input) {
1828
1833
  node: {
1829
1834
  ...accountInput.node,
1830
1835
  action: 'metamask.wallet.select_account',
1831
- name: config.account.trim(),
1836
+ ...(/^0x[0-9a-f]{40}$/iu.test(config.account.trim())
1837
+ ? { address: config.account.trim() }
1838
+ : { name: config.account.trim() }),
1832
1839
  },
1833
1840
  })
1834
1841
  : { skipped: true };
@@ -1,3 +1,4 @@
1
+ // Submit a Perps order through the Mobile controller and assert a matching open position.
1
2
  import { runAdapter } from '../platform/bridge.mjs';
2
3
  import { assertPositions, placeOrder } from './perps.mjs';
3
4
  runAdapter(async (input) => {
@@ -1,3 +1,5 @@
1
+ // Apply and verify an adb reverse mapping for a local snapshot port on the selected Android device.
2
+ // Skip the mapping when no Android serial is configured.
1
3
  import { execFile } from 'node:child_process';
2
4
  import { promisify } from 'node:util';
3
5
 
@@ -1,3 +1,5 @@
1
+ // Poll native screen and accessibility observations until Perps state satisfies the requested checks.
2
+ // Return normalized state or report the last assertion or observation failure at the deadline.
1
3
  import { observeNativeUi } from '../platform/observe-ui.mjs';
2
4
  import {
3
5
  normalizeVisiblePerpsState,
@@ -47,6 +49,7 @@ export async function readVisiblePerpsState(
47
49
  truncated: visible.truncated,
48
50
  }, options, 'mobile'),
49
51
  proofPath: 'native-accessibility',
52
+ observationProvider: visible.provider,
50
53
  };
51
54
  } catch (error) {
52
55
  lastAssertionError = error;
@@ -1,3 +1,4 @@
1
+ // Return selected open Perps orders with redacted fields and counts from the Mobile controller.
1
2
  import { runAdapter } from '../platform/bridge.mjs';
2
3
  import { readOrders } from './perps.mjs';
3
4
 
@@ -1,3 +1,4 @@
1
+ // Return selected Perps positions with redacted fields and counts from the Mobile controller.
1
2
  import { runAdapter } from '../platform/bridge.mjs';
2
3
  import { readPerpsPositions } from './perps.mjs';
3
4
  runAdapter(readPerpsPositions);
@@ -1,3 +1,4 @@
1
+ // Read Perps state from native accessibility observations, waiting for requested assertions to pass.
1
2
  import { runAdapter } from '../platform/bridge.mjs';
2
3
  import { readVisiblePerpsState } from './read-visible-state-loop.mjs';
3
4
 
@@ -1,3 +1,4 @@
1
+ // Enter a query in the native Perps market search and wait for visible market rows.
1
2
  import { runAdapter } from '../platform/bridge.mjs';
2
3
  import {
3
4
  closeNativeSession,
@@ -1,3 +1,5 @@
1
+ // Set a visible market's watchlist state through native controls and verify the result.
2
+ // With require_unchanged, fail on a mismatched state without toggling the control.
1
3
  import { runAdapter } from '../platform/bridge.mjs';
2
4
  import {
3
5
  closeNativeSession,
@@ -11,7 +13,14 @@ const FAVORITE_IDS = [
11
13
  'perps-pro-market-header-favorite-button',
12
14
  ];
13
15
 
14
- function favoriteControl(snapshot) {
16
+ function favoriteControl(snapshot, market) {
17
+ const subtitle = nativeLabel(
18
+ nativeNode(snapshot, 'perps-market-header-subtitle')
19
+ ?? nativeNode(snapshot, 'perps-pro-market-header-subtitle'),
20
+ ).toUpperCase();
21
+ if (!subtitle.startsWith(`${market}-`)) {
22
+ throw new Error(`Visible Perps market ${JSON.stringify(subtitle)} does not match ${market}.`);
23
+ }
15
24
  for (const testId of FAVORITE_IDS) {
16
25
  const node = nativeNode(snapshot, testId);
17
26
  if (/(?:add to|remove from) watchlist/iu.test(nativeLabel(node))) {
@@ -39,18 +48,11 @@ runAdapter(async (input) => {
39
48
  if (typeof desired !== 'boolean') {
40
49
  throw new Error('metamask.perps.set_market_favorite requires favorite=true or false.');
41
50
  }
42
- let session = createNativeSession(input, 'Perps', `favorite-${process.pid}`);
51
+ const session = createNativeSession(input, 'Perps', `favorite-${process.pid}`);
43
52
  try {
44
53
  await session.open();
45
54
  let snapshot = await session.snapshot();
46
- const subtitle = nativeLabel(
47
- nativeNode(snapshot, 'perps-market-header-subtitle')
48
- ?? nativeNode(snapshot, 'perps-pro-market-header-subtitle'),
49
- ).toUpperCase();
50
- if (!subtitle.startsWith(`${market}-`)) {
51
- throw new Error(`Visible Perps market ${JSON.stringify(subtitle)} does not match ${market}.`);
52
- }
53
- let control = favoriteControl(snapshot);
55
+ let control = favoriteControl(snapshot, market);
54
56
  const controlDeadline = Date.now() + timeoutMs;
55
57
  let lastControls = [];
56
58
  while (Date.now() <= controlDeadline && !control) {
@@ -60,37 +62,33 @@ runAdapter(async (input) => {
60
62
  .filter((node) => /(?:favorite|mode-toggle|market-header)/iu.test(String(node.identifier ?? '')))
61
63
  .map((node) => ({ id: String(node.identifier ?? ''), label: nativeLabel(node) }))
62
64
  .slice(0, 20);
63
- control = favoriteControl(snapshot);
65
+ control = favoriteControl(snapshot, market);
64
66
  }
65
67
  if (!control) {
66
68
  throw new Error(`Perps favorite control is not visible in Lite or Pro mode; observed ${JSON.stringify(lastControls)}.`);
67
69
  }
68
70
  const before = favoriteState(control);
71
+ if (requireUnchanged && before !== desired) {
72
+ throw new Error(`Perps favorite state for ${market} did not persist across navigation; no change was made.`);
73
+ }
69
74
  if (before !== desired) {
70
75
  await session.execute('ui.press', {
71
76
  test_id: control.testId,
72
77
  timeout_ms: timeoutMs,
73
78
  settle: false,
74
79
  });
75
- await closeNativeSession(session);
76
- await new Promise((resolve) => setTimeout(resolve, 500));
77
- session = createNativeSession(input, 'Perps', `favorite-proof-${process.pid}`);
78
- await session.open();
79
80
  }
80
81
  const deadline = Date.now() + timeoutMs;
81
82
  let after = before;
82
83
  while (Date.now() <= deadline) {
83
84
  snapshot = await session.snapshot();
84
- control = favoriteControl(snapshot);
85
+ control = favoriteControl(snapshot, market);
85
86
  if (control) after = favoriteState(control);
86
87
  if (after === desired) break;
87
88
  await new Promise((resolve) => setTimeout(resolve, 200));
88
89
  }
89
90
  if (after !== desired) throw new Error(`Perps favorite state did not become ${desired}.`);
90
91
  const changed = before !== after;
91
- if (requireUnchanged && changed) {
92
- throw new Error(`Perps favorite state for ${market} did not persist across navigation.`);
93
- }
94
92
  return {
95
93
  action: input.action,
96
94
  market,
@@ -1,3 +1,5 @@
1
+ // Run Mobile actions and device-targeted bridge commands with deadlines and classified errors.
2
+ // Provide runtime evaluation, verified navigation, external deeplink launch, and screenshot capture.
1
3
  import { randomUUID } from 'node:crypto';
2
4
  import { constants as fsConstants, existsSync } from 'node:fs';
3
5
  import { mkdir, mkdtemp, open, readFile, rename, rm, writeFile } from 'node:fs/promises';
@@ -666,8 +668,16 @@ async function waitForNextRouteProbe(deadline) {
666
668
 
667
669
  export async function navigate(input, route, params = {}, expectedRoute) {
668
670
  const timeoutMs = Number(input.node?.navigation_timeout_ms ?? 15000);
669
- const requestedRoute = String(expectedRoute ?? route);
670
- const requestedParams = requestedRoute === String(route) ? params : undefined;
671
+ let requestedRoute = String(expectedRoute ?? route);
672
+ let requestedParams = requestedRoute === String(route) ? params : undefined;
673
+ const nestedTarget = expectedRoute == null && typeof params?.screen === 'string';
674
+ if (nestedTarget) {
675
+ // React Navigation reports the active leaf, not its enclosing navigator.
676
+ while (typeof requestedParams?.screen === 'string') {
677
+ requestedRoute = requestedParams.screen;
678
+ requestedParams = requestedParams.params ?? {};
679
+ }
680
+ }
671
681
  const transitionCodes = [];
672
682
  let lastTransitionError = null;
673
683
  let navigateAttempts = 0;
@@ -754,13 +764,15 @@ export async function navigate(input, route, params = {}, expectedRoute) {
754
764
  input,
755
765
  ['navigate', route, JSON.stringify(params)],
756
766
  );
757
- const verifiedRoute = String(
767
+ const verifiedRoute = nestedTarget ? requestedRoute : String(
758
768
  expectedRoute ??
759
769
  (navigation && typeof navigation === 'object' && navigation.navigated
760
770
  ? navigation.navigated
761
771
  : route),
762
772
  );
763
- const verifiedParams = verifiedRoute === String(route) ? params : undefined;
773
+ const verifiedParams = nestedTarget
774
+ ? requestedParams
775
+ : verifiedRoute === String(route) ? params : undefined;
764
776
  const currentRoute = await waitForRoute(
765
777
  input,
766
778
  verifiedRoute,
@@ -1,3 +1,4 @@
1
+ // Derive native session names and device-specific state directories, honoring configured overrides.
1
2
  import path from 'node:path';
2
3
 
3
4
  export function nativeSessionName(env, device, app = 'io.metamask') {
@@ -1,6 +1,11 @@
1
+ // Create device-targeted native UI sessions for actions and accessibility snapshots.
2
+ // Opening an instrumented iOS session hides the accessibility-masking HUD.
1
3
  import { createAgentDeviceUiTransport } from '@farmslot/expo-recipe';
2
4
  import { createAgentDeviceClient } from 'agent-device';
3
5
 
6
+ import { resolveMobileTarget } from './bridge.mjs';
7
+ import { hideAccessibilityMaskingHud } from './observe-ui.mjs';
8
+
4
9
  import {
5
10
  nativeAgentDeviceStateDir,
6
11
  nativeSessionName,
@@ -8,17 +13,27 @@ import {
8
13
 
9
14
  export function createNativeSession(input, domain, sessionSuffix = '') {
10
15
  const env = { ...process.env, ...(input.context?.env ?? {}) };
11
- const device = String(env.ADB_SERIAL ?? env.ANDROID_SERIAL ?? '').trim();
12
- if (!device) throw new Error(`Mobile ${domain} native actions require one explicit Android device serial.`);
13
- const app = String(env.ANDROID_PACKAGE_ID ?? 'io.metamask');
16
+ const target = resolveMobileTarget(input);
17
+ const platform = target.requestedPlatform || (target.adbSerial ? 'android' : 'ios');
18
+ const device = String(platform === 'android'
19
+ ? target.adbSerial ?? ''
20
+ : input.node?.sim_udid ?? env.SIM_UDID ?? target.iosSimulator ?? '').trim();
21
+ if (!device) throw new Error(`Mobile ${domain} native actions require one explicit ${platform} device. Next: pass --device <device>.`);
22
+ const app = String(platform === 'android'
23
+ ? env.ANDROID_PACKAGE_ID ?? 'io.metamask'
24
+ : env.IOS_BUNDLE_ID ?? 'io.metamask.MetaMask');
14
25
  const baseSession = nativeSessionName(env, device, app);
15
26
  const session = sessionSuffix
16
27
  ? `${baseSession}-${String(sessionSuffix).replace(/[^A-Za-z0-9._-]/gu, '-')}`.slice(0, 120)
17
28
  : baseSession;
18
29
  const stateDir = nativeAgentDeviceStateDir(env, input.context.projectRoot, device);
19
- const client = createAgentDeviceClient({ session, stateDir, lockPolicy: 'reject', lockPlatform: 'android' });
20
- const selection = { platform: 'android', target: 'mobile', serial: device, session };
21
- const transport = createAgentDeviceUiTransport({ platform: 'android', device, app, session, stateDir, client });
30
+ const client = createAgentDeviceClient({ session, stateDir, lockPolicy: 'reject', lockPlatform: platform });
31
+ const selection = {
32
+ platform, target: 'mobile', session,
33
+ ...(platform === 'android' ? { serial: device }
34
+ : /^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/iu.test(device) ? { udid: device } : { device }),
35
+ };
36
+ const transport = createAgentDeviceUiTransport({ platform, device, app, session, stateDir, client });
22
37
  const context = {
23
38
  nodeId: String(input.context?.nodeId ?? 'action'),
24
39
  projectRoot: input.context.projectRoot,
@@ -31,23 +46,19 @@ export function createNativeSession(input, domain, sessionSuffix = '') {
31
46
  selection,
32
47
  context,
33
48
  app,
34
- open: () => client.apps.open({ ...selection, app, noRecord: true }),
49
+ open: async () => {
50
+ if (platform === 'ios' && env.METAMASK_RECIPE_MOBILE_OPAQUE_RUNTIME !== '1') {
51
+ await hideAccessibilityMaskingHud({ node: input.node }, input.context);
52
+ }
53
+ await transport.open();
54
+ },
35
55
  execute: (action, node) => transport.execute(action, node, context),
36
56
  snapshot: () => client.capture.snapshot({ ...selection, app, interactiveOnly: false, forceFull: true }),
37
57
  };
38
58
  }
39
59
 
40
60
  export async function closeNativeSession(session) {
41
- try {
42
- await session.transport.close();
43
- } catch (error) {
44
- if (!/No active session|SESSION_NOT_FOUND/iu.test(String(error?.message ?? error))) throw error;
45
- }
46
- try {
47
- await session.client.sessions.close();
48
- } catch (error) {
49
- if (!/No active session|SESSION_NOT_FOUND/iu.test(String(error?.message ?? error))) throw error;
50
- }
61
+ await session.transport.close();
51
62
  }
52
63
 
53
64
  export function nativeNode(snapshot, identifier) {
@@ -1,3 +1,5 @@
1
+ // Read and normalize native accessibility hierarchies into screen and visible-item observations.
2
+ // Attempt to hide the masking HUD first and return observation failures as warnings.
1
3
  import { execFile } from 'node:child_process';
2
4
  import { promisify } from 'node:util';
3
5
  import { createAgentDeviceClient } from 'agent-device';
@@ -45,7 +47,7 @@ export async function observeNativeUi(payload, context) {
45
47
  }
46
48
  }
47
49
 
48
- async function hideAccessibilityMaskingHud(payload, context) {
50
+ export async function hideAccessibilityMaskingHud(payload, context) {
49
51
  try {
50
52
  await bridgeCommand(
51
53
  {
@@ -429,7 +431,7 @@ function parseAndroidHierarchy(output) {
429
431
  isActionableRole(role) ||
430
432
  attributes.clickable === 'true' ||
431
433
  attributes['long-clickable'] === 'true';
432
- if (isActionable) {
434
+ if (isActionable || item.test_id) {
433
435
  if (isVisible) {
434
436
  visible.push(item);
435
437
  } else {
@@ -1,3 +1,4 @@
1
+ // Locate executable adb and idb clients, cache their paths, and export tool-path environment overrides.
1
2
  import { constants as fsConstants, accessSync, readdirSync, realpathSync, statSync } from 'node:fs';
2
3
  import os from 'node:os';
3
4
  import path from 'node:path';
@@ -1,3 +1,4 @@
1
+ // Bind shared native UI session helpers to Swap actions.
1
2
  import {
2
3
  closeNativeSession,
3
4
  createNativeSession,
@@ -1,3 +1,4 @@
1
+ // Normalize Mobile accessibility observations into visible Swap and Bridge quote state.
1
2
  import { runAdapter } from '../platform/bridge.mjs';
2
3
  import { observeNativeUi } from '../platform/observe-ui.mjs';
3
4
  import {
@@ -1,3 +1,5 @@
1
+ // Select source and destination assets through native token pickers and verify their symbols.
2
+ // Exact asset-ID selection is unsupported.
1
3
  import { runAdapter } from '../platform/bridge.mjs';
2
4
  import { resolveAssetSelectionOptions } from '../../shared/swap-bridge/transaction.mjs';
3
5
  import {
@@ -1,3 +1,4 @@
1
+ // Replace the Swap source amount through its keypad and optionally wait for a quote or no-quote state.
1
2
  import { runAdapter } from '../platform/bridge.mjs';
2
3
  import { resolveAmountOptions } from '../../shared/swap-bridge/transaction.mjs';
3
4
  import {
@@ -1,3 +1,4 @@
1
+ // Press Swap Max, verify a positive source amount, and optionally wait for the quote result.
1
2
  import { runAdapter } from '../platform/bridge.mjs';
2
3
  import { resolveAmountOptions } from '../../shared/swap-bridge/transaction.mjs';
3
4
  import {
@@ -1,3 +1,4 @@
1
+ // Apply custom Swap slippage through native controls and verify the refreshed quote displays it.
1
2
  import { runAdapter } from '../platform/bridge.mjs';
2
3
  import {
3
4
  closeNativeSession,
@@ -1,3 +1,5 @@
1
+ // Submit a live Swap or Bridge with confirm_live, including any price-impact confirmation.
2
+ // Require a stable matching quote, a completion status, and a visible Activity entry.
1
3
  import { runAdapter } from '../platform/bridge.mjs';
2
4
  import {
3
5
  closeNativeSession,
@@ -1,3 +1,4 @@
1
+ // Turn visible Mobile accessibility observations into UI locator candidates.
1
2
  import { observeNativeUi } from '../platform/observe-ui.mjs';
2
3
  import { runAdapter } from '../platform/bridge.mjs';
3
4
  import {
@@ -1,3 +1,5 @@
1
+ // Navigate Android Home, Swap, and Perps through native controls and verify visible destinations.
2
+ // Switch Perps Lite or Pro mode through visible controls on Android and iOS.
1
3
  import { createAgentDeviceUiTransport } from '@farmslot/expo-recipe';
2
4
  import { createAgentDeviceClient } from 'agent-device';
3
5
  import {
@@ -1,3 +1,5 @@
1
+ // Resolve page intents or raw routes into Mobile navigation, using native controls in opaque mode.
2
+ // Verify requested Perps market mode through visible controls.
1
3
  import {
2
4
  navigate,
3
5
  resolveMobileTarget,
@@ -12,7 +14,7 @@ import {
12
14
  const PAGE_ROUTES = {
13
15
  home: { route: 'WalletView', params: {} },
14
16
  perps: { route: 'PerpsMarketListView', params: {} },
15
- 'perps-market-list': { route: 'PerpsMarketListView', params: {} },
17
+ 'perps-market-list': { route: 'PerpsTrendingView', params: {} },
16
18
  swap: { route: 'BridgeView', params: {} },
17
19
  };
18
20
 
@@ -1,3 +1,5 @@
1
+ // Unlock an existing wallet with a supplied or fixture password through the bridge or native UI.
2
+ // Verify wallet readiness and, for bridge sessions, a stable unlocked controller state.
1
3
  import { readFile } from 'node:fs/promises';
2
4
  import { pathToFileURL } from 'node:url';
3
5
  import {
@@ -1,3 +1,4 @@
1
+ // Open wallet home through WalletView, with a verified HomeNav fallback for older navigators.
1
2
  import { bridgeCommand, navigate, waitForRoute } from '../platform/bridge.mjs';
2
3
 
3
4
  /**
@@ -1,3 +1,5 @@
1
+ // Import a recovery phrase through onboarding or unlock an existing profile, then verify the expected account.
2
+ // Fresh imports also handle consent, interests, and supported onboarding prompts.
1
3
  import { createHash } from 'node:crypto';
2
4
  import { mkdtemp, rm, writeFile } from 'node:fs/promises';
3
5
  import os from 'node:os';
@@ -1,3 +1,4 @@
1
+ // List controller accounts with selection markers, filtered by selected, named, EVM, or all accounts.
1
2
  import { bridgeCommand, runAdapter } from '../platform/bridge.mjs';
2
3
 
3
4
  runAdapter(async (input) => {
@@ -1,3 +1,4 @@
1
+ // Lock the wallet through Android native UI and verify the login screen, or accept an already locked wallet.
1
2
  import { runAdapter } from '../platform/bridge.mjs';
2
3
  import { lockWalletThroughNativeUi } from './native-ui.mjs';
3
4
 
@@ -1,3 +1,5 @@
1
+ // Drive Android wallet onboarding, lock, unlock, and reset through native accessibility.
2
+ // Wallet observations can navigate home and open the address list to verify the selected account.
1
3
  import { createAgentDeviceUiTransport } from '@farmslot/expo-recipe';
2
4
  import { createAgentDeviceClient } from 'agent-device';
3
5
  import {
@@ -1,3 +1,5 @@
1
+ // Report the selected account and route from bridge status or native accessibility.
2
+ // Native reads navigate to wallet home and may open the address list to observe the account.
1
3
  import { bridgeCommand, runAdapter, selectBridgeStatusEntry } from '../platform/bridge.mjs';
2
4
  import { isOpaqueMobileRuntime, readWalletThroughNativeUi } from './native-ui.mjs';
3
5
 
@@ -1,3 +1,4 @@
1
+ // Reset the wallet through bridge-driven login UI and wait for the fresh onboarding control.
1
2
  import {
2
3
  bridgeCommand,
3
4
  selectBridgeStatusEntry,
@@ -1,3 +1,4 @@
1
+ // Reset the wallet through visible UI and verify fresh onboarding using the bridge or native accessibility.
1
2
  import { runAdapter } from '../platform/bridge.mjs';
2
3
  import { resetMobileWalletThroughUi } from './reset-helper.mjs';
3
4
  import { isOpaqueMobileRuntime, resetWalletThroughNativeUi } from './native-ui.mjs';
@@ -1,3 +1,5 @@
1
+ // Select a wallet account by address, ID, or name and verify the resulting selection.
2
+ // Observe ambiguous bridge write failures until the deadline without repeating the switch.
1
3
  import { pathToFileURL } from 'node:url';
2
4
  import {
3
5
  bridgeCommand,
@@ -1,3 +1,5 @@
1
+ // Validate the wallet fixture, run the Mobile setup script, and verify a selected account.
2
+ // Unlock with the fixture password when setup leaves no selected account observable.
1
3
  import { readFile } from 'node:fs/promises';
2
4
  import { spawn } from 'node:child_process';
3
5
  import path from 'node:path';
@@ -1,3 +1,6 @@
1
+ // Assert captured event counts and property values within the requested flow window.
2
+ // Observe the settling interval before deciding whether duplicate emissions occurred.
3
+
1
4
  import { runAdapter } from './_adapter.mjs';
2
5
  import { readCollected } from './collector.mjs';
3
6
 
@@ -1,3 +1,6 @@
1
+ // Normalize analytics consent inputs and reject marketing consent without participation.
2
+ // This helper validates options; platform actions perform the settings changes.
3
+
1
4
  export function consentParams(node) {
2
5
  const optional = (value, fallback) =>
3
6
  value === undefined || value === null || value === '' ? fallback : value;
@@ -1,3 +1,5 @@
1
+ // Read checkout-local collected analytics events, optionally filtered by cursor and event name.
2
+
1
3
  import { runAdapter } from './_adapter.mjs';
2
4
  import { readCollected } from './collector.mjs';
3
5
 
@@ -1,3 +1,6 @@
1
+ // Start or reuse the local analytics collector and return a cursor for this flow.
2
+ // Client routing and consent must be configured separately.
3
+
1
4
  import { runAdapter } from './_adapter.mjs';
2
5
  import { startCollector } from './collector.mjs';
3
6
 
@@ -1,3 +1,5 @@
1
+ // Resolve a network-evidence path without allowing it to escape the run's artifact directory.
2
+
1
3
  import path from 'node:path';
2
4
 
3
5
  export function resolveNetworkArtifact(artifactsDir, relativePath) {
@@ -1,3 +1,5 @@
1
+ // Validate a captured network summary and assert requested status and request-count bounds.
2
+
1
3
  import { constants } from 'node:fs';
2
4
  import { open } from 'node:fs/promises';
3
5
 
@@ -1,3 +1,5 @@
1
+ // Normalize platform asset observations and enforce symbol, count and display requirements.
2
+
1
3
  function boundedInteger(value, fallback, minimum, maximum, name) {
2
4
  const resolved = value == null ? fallback : Number(value);
3
5
  if (!Number.isInteger(resolved) || resolved < minimum || resolved > maximum) {
@@ -1,3 +1,6 @@
1
+ // Validate external HTTPS deeplink inputs and their bounded timeout.
2
+ // Signature authorization belongs to the receiving product, not this URL-shape check.
3
+
1
4
  export function externalDeeplinkUrl(node) {
2
5
  const raw = node?.url;
3
6
  if (typeof raw !== 'string' || raw.trim().length === 0) {
@@ -1,3 +1,5 @@
1
+ // Normalize network observations and compare asset state across the requested observation window.
2
+
1
3
  import {
2
4
  normalizeVisibleAssetsState,
3
5
  resolveVisibleAssetsOptions,