@reticlehq/server 3.9.0 → 3.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (351) hide show
  1. package/SKILL.md +13 -12
  2. package/dist/command/cli/adhoc-suite.js +2 -0
  3. package/dist/command/cli/adhoc-verdict.d.ts +2 -43
  4. package/dist/command/cli/adhoc-verdict.js +50 -187
  5. package/dist/command/cli/answers/mcp-connection-hint.d.ts +2 -0
  6. package/dist/command/cli/answers/mcp-connection-hint.js +6 -0
  7. package/dist/command/cli/cli-doctor.js +1 -1
  8. package/dist/command/cli/cli-flow-commands.js +2 -9
  9. package/dist/command/cli/cli-kill.js +1 -1
  10. package/dist/command/cli/cli-parse-compare.d.ts +22 -0
  11. package/dist/command/cli/cli-parse-compare.js +75 -0
  12. package/dist/command/cli/cli-parse-grade.d.ts +36 -0
  13. package/dist/command/cli/cli-parse-grade.js +75 -0
  14. package/dist/command/cli/cli-parse-grammar.d.ts +2 -0
  15. package/dist/command/cli/cli-parse-grammar.js +18 -20
  16. package/dist/command/cli/cli-parse-verify.d.ts +10 -0
  17. package/dist/command/cli/cli-parse-verify.js +63 -14
  18. package/dist/command/cli/cli-parse.d.ts +17 -15
  19. package/dist/command/cli/cli-parse.js +44 -27
  20. package/dist/command/cli/cli-update-commands.d.ts +1 -1
  21. package/dist/command/cli/cli-update-commands.js +29 -20
  22. package/dist/command/cli/cli-usage.js +58 -29
  23. package/dist/command/cli/cli-verify.d.ts +11 -2
  24. package/dist/command/cli/cli-verify.js +27 -8
  25. package/dist/command/cli/cloud-cli.js +2 -119
  26. package/dist/command/cli/daemon-link/daemon-client.d.ts +45 -0
  27. package/dist/command/cli/daemon-link/daemon-client.js +205 -0
  28. package/dist/command/cli/doctor/doctor-daemon-line.js +6 -3
  29. package/dist/command/cli/doctor-repairs.js +1 -1
  30. package/dist/command/cli/ensure-daemon.d.ts +26 -0
  31. package/dist/command/cli/ensure-daemon.js +57 -0
  32. package/dist/command/cli/finished-run/grade-command.d.ts +23 -0
  33. package/dist/command/cli/finished-run/grade-command.js +180 -0
  34. package/dist/command/cli/lifecycle/daemon-lifecycle.d.ts +1 -1
  35. package/dist/command/cli/lifecycle/daemon-lifecycle.js +1 -1
  36. package/dist/command/cli/ports/port-holder.d.ts +9 -0
  37. package/dist/command/cli/ports/port-holder.js +27 -3
  38. package/dist/command/cli/status/status-lines.d.ts +1 -0
  39. package/dist/command/cli/status/status-lines.js +22 -0
  40. package/dist/command/cli/status-fields.d.ts +5 -1
  41. package/dist/command/cli/status-fields.js +7 -3
  42. package/dist/command/cli/two-builds/compare-command.d.ts +48 -0
  43. package/dist/command/cli/two-builds/compare-command.js +314 -0
  44. package/dist/command/cli/verify-against.d.ts +61 -0
  45. package/dist/command/cli/verify-against.js +187 -0
  46. package/dist/command/cli/verify-no-session.d.ts +2 -0
  47. package/dist/command/cli/verify-no-session.js +13 -0
  48. package/dist/command/cli/verify-results.d.ts +35 -2
  49. package/dist/command/cli/verify-results.js +76 -10
  50. package/dist/command/cli.js +46 -41
  51. package/dist/command/daemon/daemon-resolve.d.ts +8 -1
  52. package/dist/command/daemon/daemon-resolve.js +27 -1
  53. package/dist/command/daemon/daemon.d.ts +12 -1
  54. package/dist/command/daemon/daemon.js +35 -4
  55. package/dist/command/daemon/lifetime/death-cause.js +21 -2
  56. package/dist/command/daemon/lifetime/heartbeat.js +7 -2
  57. package/dist/command/daemon/lifetime/startup-failure.d.ts +29 -0
  58. package/dist/command/daemon/lifetime/startup-failure.js +62 -0
  59. package/dist/command/setup/bringup/dev-server-files.d.ts +9 -0
  60. package/dist/command/setup/bringup/dev-server-files.js +17 -0
  61. package/dist/command/setup/bringup/static-page-server.d.ts +14 -2
  62. package/dist/command/setup/bringup/static-page-server.js +43 -9
  63. package/dist/command/setup/init/init-runtime.js +43 -2
  64. package/dist/command/setup/node-effects.d.ts +3 -2
  65. package/dist/command/setup/node-effects.js +9 -3
  66. package/dist/command/setup/remaining-steps.d.ts +8 -0
  67. package/dist/command/setup/remaining-steps.js +10 -0
  68. package/dist/command/setup/run-setup.d.ts +2 -0
  69. package/dist/command/setup/run-setup.js +9 -1
  70. package/dist/command/setup/setup-command.js +6 -3
  71. package/dist/command/update/updater.js +6 -0
  72. package/dist/command/version/version-skew.js +1 -1
  73. package/dist/features/compare/action-plan.d.ts +260 -0
  74. package/dist/features/compare/action-plan.js +543 -0
  75. package/dist/features/compare/behaviour-compare.d.ts +24 -0
  76. package/dist/features/compare/behaviour-compare.js +85 -0
  77. package/dist/features/compare/behaviour-diff.d.ts +97 -0
  78. package/dist/features/compare/behaviour-diff.js +202 -0
  79. package/dist/features/compare/behaviour-report.d.ts +40 -0
  80. package/dist/features/compare/behaviour-report.js +128 -0
  81. package/dist/features/compare/build-readings.d.ts +106 -0
  82. package/dist/features/compare/build-readings.js +250 -0
  83. package/dist/features/compare/compare-diff.d.ts +62 -0
  84. package/dist/features/compare/compare-diff.js +177 -0
  85. package/dist/features/compare/compare-report-html.d.ts +6 -0
  86. package/dist/features/compare/compare-report-html.js +102 -0
  87. package/dist/features/compare/compare-report.d.ts +15 -0
  88. package/dist/features/compare/compare-report.js +73 -0
  89. package/dist/features/compare/compare-run.d.ts +60 -0
  90. package/dist/features/compare/compare-run.js +123 -0
  91. package/dist/features/compare/outside/effect-lines.d.ts +26 -0
  92. package/dist/features/compare/outside/effect-lines.js +103 -0
  93. package/dist/features/compare/outside/state-sources.d.ts +76 -0
  94. package/dist/features/compare/outside/state-sources.js +164 -0
  95. package/dist/features/compare/outside/value-dictionary.d.ts +51 -0
  96. package/dist/features/compare/outside/value-dictionary.js +227 -0
  97. package/dist/features/compare/visible-content.d.ts +20 -0
  98. package/dist/features/compare/visible-content.js +125 -0
  99. package/dist/features/crawl/crawl-empty.js +2 -1
  100. package/dist/features/crawl/crawl-exhaustive.d.ts +55 -0
  101. package/dist/features/crawl/crawl-exhaustive.js +138 -0
  102. package/dist/features/crawl/crawl-tools.d.ts +9 -2
  103. package/dist/features/crawl/crawl-tools.js +28 -12
  104. package/dist/features/crawl/crawl.d.ts +8 -9
  105. package/dist/features/crawl/crawl.js +61 -65
  106. package/dist/features/exhaust/app-coverage.d.ts +5 -1
  107. package/dist/features/exhaust/app-coverage.js +3 -1
  108. package/dist/features/exhaust/code-coverage.js +8 -4
  109. package/dist/features/exhaust/edge-cases.d.ts +133 -0
  110. package/dist/features/exhaust/edge-cases.js +310 -0
  111. package/dist/features/exhaust/edges/edge-drive.d.ts +42 -0
  112. package/dist/features/exhaust/edges/edge-drive.js +364 -0
  113. package/dist/features/exhaust/edges/edge-run.d.ts +59 -0
  114. package/dist/features/exhaust/edges/edge-run.js +169 -0
  115. package/dist/features/exhaust/explorer.d.ts +87 -14
  116. package/dist/features/exhaust/explorer.js +159 -25
  117. package/dist/features/exhaust/failure-branches.d.ts +5 -0
  118. package/dist/features/exhaust/failure-branches.js +29 -11
  119. package/dist/features/exhaust/invariants.d.ts +112 -0
  120. package/dist/features/exhaust/invariants.js +188 -0
  121. package/dist/features/exhaust/ledger.d.ts +68 -0
  122. package/dist/features/exhaust/ledger.js +121 -13
  123. package/dist/features/exhaust/session-fold.d.ts +10 -0
  124. package/dist/features/exhaust/session-fold.js +46 -3
  125. package/dist/features/exhaust/session-port.d.ts +3 -1
  126. package/dist/features/exhaust/session-port.js +97 -28
  127. package/dist/features/grade/grade-answer.d.ts +99 -0
  128. package/dist/features/grade/grade-answer.js +112 -0
  129. package/dist/features/grade/grade-noise.d.ts +27 -0
  130. package/dist/features/grade/grade-noise.js +55 -0
  131. package/dist/features/grade/grade-renumber.d.ts +12 -0
  132. package/dist/features/grade/grade-renumber.js +86 -0
  133. package/dist/features/grade/grade-state.d.ts +50 -0
  134. package/dist/features/grade/grade-state.js +93 -0
  135. package/dist/features/grade/grade-suggest.d.ts +33 -0
  136. package/dist/features/grade/grade-suggest.js +98 -0
  137. package/dist/features/visual/visual-tools.js +66 -19
  138. package/dist/index.js +7 -4
  139. package/dist/judgement/capsule/causal-summary.js +9 -2
  140. package/dist/judgement/runs/artifact/replay-mapping.d.ts +13 -1
  141. package/dist/judgement/runs/artifact/replay-mapping.js +1 -1
  142. package/dist/judgement/runs/reticle-runner.js +16 -2
  143. package/dist/judgement/runs/runner-port.d.ts +2 -0
  144. package/dist/judgement/runs/runner-port.js +23 -5
  145. package/dist/language/flows/affected-tools.d.ts +1 -1
  146. package/dist/language/flows/affected-tools.js +49 -30
  147. package/dist/language/flows/change/git-changed.d.ts +2 -1
  148. package/dist/language/flows/change/git-changed.js +19 -2
  149. package/dist/language/flows/fields/flow-secret-field.d.ts +1 -13
  150. package/dist/language/flows/fields/flow-secret-field.js +77 -21
  151. package/dist/language/flows/flow-anchor.d.ts +60 -2
  152. package/dist/language/flows/flow-anchor.js +125 -20
  153. package/dist/language/flows/flow-classify.d.ts +20 -0
  154. package/dist/language/flows/flow-classify.js +90 -12
  155. package/dist/language/flows/flow-cross-step.d.ts +7 -1
  156. package/dist/language/flows/flow-cross-step.js +25 -1
  157. package/dist/language/flows/flow-journey.d.ts +38 -4
  158. package/dist/language/flows/flow-journey.js +138 -8
  159. package/dist/language/flows/flow-replay-run.d.ts +0 -16
  160. package/dist/language/flows/flow-replay-run.js +76 -14
  161. package/dist/language/flows/flow-replay-types.d.ts +4 -2
  162. package/dist/language/flows/flow-replay.d.ts +19 -1
  163. package/dist/language/flows/flow-replay.js +169 -52
  164. package/dist/language/flows/flow-replayable.js +7 -1
  165. package/dist/language/flows/flow-start-path.d.ts +34 -5
  166. package/dist/language/flows/flow-start-path.js +202 -15
  167. package/dist/language/flows/flow-step-runners.d.ts +14 -13
  168. package/dist/language/flows/flow-step-runners.js +184 -109
  169. package/dist/language/flows/flow-store-for-session.d.ts +26 -25
  170. package/dist/language/flows/flow-store-for-session.js +44 -0
  171. package/dist/language/flows/flow-tools.js +46 -33
  172. package/dist/language/flows/flows.js +176 -76
  173. package/dist/language/flows/recording/tape/recordings.d.ts +19 -3
  174. package/dist/language/flows/recording/tape/recordings.js +71 -5
  175. package/dist/language/flows/replay.d.ts +19 -5
  176. package/dist/language/flows/replay.js +99 -7
  177. package/dist/language/flows/steps/page-steps.d.ts +68 -0
  178. package/dist/language/flows/steps/page-steps.js +198 -0
  179. package/dist/language/flows/suite/sequential-run.d.ts +68 -0
  180. package/dist/language/flows/suite/sequential-run.js +96 -0
  181. package/dist/language/flows/verify-change-tools.js +26 -15
  182. package/dist/memory/cloud/background-sync.d.ts +13 -0
  183. package/dist/memory/cloud/background-sync.js +24 -3
  184. package/dist/memory/cloud/sync-daemon.js +26 -27
  185. package/dist/memory/journal/attach-journal.js +2 -1
  186. package/dist/memory/journal/drive-flow.d.ts +7 -0
  187. package/dist/memory/journal/drive-flow.js +44 -13
  188. package/dist/memory/journal/journal-recorder.d.ts +4 -2
  189. package/dist/memory/journal/session-journal.d.ts +7 -0
  190. package/dist/memory/journal/session-journal.js +58 -10
  191. package/dist/memory/project/root-claims.d.ts +9 -0
  192. package/dist/memory/project/root-claims.js +9 -4
  193. package/dist/portal/input/download-capture.d.ts +7 -0
  194. package/dist/portal/input/download-capture.js +92 -0
  195. package/dist/portal/input/network-detail.d.ts +6 -0
  196. package/dist/portal/input/network-detail.js +12 -1
  197. package/dist/portal/input/network-mock-tools.js +49 -13
  198. package/dist/portal/input/network-mock.d.ts +8 -0
  199. package/dist/portal/input/network-mock.js +28 -9
  200. package/dist/portal/input/page-capture.d.ts +24 -0
  201. package/dist/portal/input/page-capture.js +33 -0
  202. package/dist/portal/input/real-input.d.ts +22 -20
  203. package/dist/portal/input/real-input.js +41 -31
  204. package/dist/portal/input/viewport-tools.d.ts +8 -0
  205. package/dist/portal/input/viewport-tools.js +75 -18
  206. package/dist/portal/pool/browser-pool.d.ts +11 -29
  207. package/dist/portal/pool/browser-pool.js +35 -17
  208. package/dist/portal/pool/context-permissions.d.ts +17 -3
  209. package/dist/portal/pool/context-permissions.js +36 -5
  210. package/dist/portal/pool/determinism.d.ts +52 -0
  211. package/dist/portal/pool/determinism.js +259 -0
  212. package/dist/portal/pool/lease-emulation.d.ts +34 -1
  213. package/dist/portal/pool/lease-emulation.js +18 -0
  214. package/dist/portal/pool/lease-marker-stamp.d.ts +6 -0
  215. package/dist/portal/pool/lease-marker-stamp.js +15 -1
  216. package/dist/portal/pool/lease-page-error.d.ts +44 -17
  217. package/dist/portal/pool/lease-page-error.js +147 -5
  218. package/dist/portal/pool/page-error-journal.d.ts +7 -1
  219. package/dist/portal/pool/page-error-journal.js +27 -0
  220. package/dist/portal/pool/playwright-launcher.js +51 -8
  221. package/dist/portal/pool/pool-contract.d.ts +27 -1
  222. package/dist/portal/pool/pool-lease.d.ts +36 -1
  223. package/dist/portal/pool/zero-install.d.ts +8 -0
  224. package/dist/portal/pool/zero-install.js +23 -1
  225. package/dist/portal/session/dev-server/dev-command.d.ts +7 -6
  226. package/dist/portal/session/dev-server/dev-command.js +61 -15
  227. package/dist/portal/session/dev-server/dev-server-probe.d.ts +7 -0
  228. package/dist/portal/session/dev-server/dev-server-probe.js +9 -0
  229. package/dist/portal/session/dev-server/project-ports.d.ts +21 -0
  230. package/dist/portal/session/dev-server/project-ports.js +62 -0
  231. package/dist/portal/session/facts/departed-network.d.ts +66 -0
  232. package/dist/portal/session/facts/departed-network.js +142 -0
  233. package/dist/portal/session/no-session-diagnosis.d.ts +9 -0
  234. package/dist/portal/session/no-session-diagnosis.js +67 -25
  235. package/dist/portal/session/no-session-next-action.d.ts +2 -0
  236. package/dist/portal/session/no-session-next-action.js +12 -3
  237. package/dist/portal/session/no-session-watch.d.ts +6 -0
  238. package/dist/portal/session/no-session-watch.js +38 -3
  239. package/dist/portal/session/serving-directory.d.ts +10 -0
  240. package/dist/portal/session/serving-directory.js +39 -8
  241. package/dist/portal/session/session-manager.d.ts +10 -0
  242. package/dist/portal/session/session-manager.js +174 -63
  243. package/dist/start-wiring.d.ts +2 -1
  244. package/dist/start-wiring.js +2 -1
  245. package/dist/surface/mcp/mcp.js +3 -2
  246. package/dist/surface/mcp/shared-params.d.ts +2 -0
  247. package/dist/surface/mcp/shared-params.js +31 -0
  248. package/dist/surface/tools/act/act-observation.d.ts +40 -2
  249. package/dist/surface/tools/act/act-observation.js +118 -12
  250. package/dist/surface/tools/act/act-preflight.js +1 -1
  251. package/dist/surface/tools/act/act-sequence-retry.d.ts +23 -2
  252. package/dist/surface/tools/act/act-sequence-retry.js +61 -5
  253. package/dist/surface/tools/act/after-match.d.ts +4 -1
  254. package/dist/surface/tools/act/after-match.js +8 -1
  255. package/dist/surface/tools/act/already-true.js +1 -1
  256. package/dist/surface/tools/act/navigation/reload-result.d.ts +12 -1
  257. package/dist/surface/tools/act/navigation/reload-result.js +25 -2
  258. package/dist/surface/tools/act/resolve-target.d.ts +15 -1
  259. package/dist/surface/tools/act/resolve-target.js +24 -1
  260. package/dist/surface/tools/act/sequence-grade.d.ts +12 -0
  261. package/dist/surface/tools/act/sequence-grade.js +37 -26
  262. package/dist/surface/tools/act/settle-in-flight.js +27 -15
  263. package/dist/surface/tools/act-command.d.ts +23 -0
  264. package/dist/surface/tools/act-command.js +50 -0
  265. package/dist/surface/tools/act-sequence-tool.js +78 -50
  266. package/dist/surface/tools/act-tools.d.ts +1 -13
  267. package/dist/surface/tools/act-tools.js +74 -86
  268. package/dist/surface/tools/agent/build-staleness.d.ts +1 -1
  269. package/dist/surface/tools/agent/build-staleness.js +1 -1
  270. package/dist/surface/tools/assert/assert-verdict.js +16 -37
  271. package/dist/surface/tools/browser-tools.js +12 -6
  272. package/dist/surface/tools/compare-tools.d.ts +11 -0
  273. package/dist/surface/tools/compare-tools.js +94 -0
  274. package/dist/surface/tools/coverage-tools.js +1 -1
  275. package/dist/surface/tools/dynamic-tools.js +9 -2
  276. package/dist/surface/tools/error-recovery.d.ts +12 -1
  277. package/dist/surface/tools/error-recovery.js +91 -21
  278. package/dist/surface/tools/explore-tools.js +1 -1
  279. package/dist/surface/tools/invoke-tool.js +3 -2
  280. package/dist/surface/tools/lease-coverage-tools.d.ts +49 -0
  281. package/dist/surface/tools/lease-coverage-tools.js +128 -0
  282. package/dist/surface/tools/lease-hint.d.ts +35 -1
  283. package/dist/surface/tools/lease-hint.js +42 -0
  284. package/dist/surface/tools/lease-readiness.d.ts +26 -0
  285. package/dist/surface/tools/lease-readiness.js +79 -0
  286. package/dist/surface/tools/lease-seed.d.ts +14 -1
  287. package/dist/surface/tools/lease-seed.js +35 -3
  288. package/dist/surface/tools/lease-tools.d.ts +3 -41
  289. package/dist/surface/tools/lease-tools.js +62 -110
  290. package/dist/surface/tools/lease-url-identity.d.ts +46 -0
  291. package/dist/surface/tools/lease-url-identity.js +97 -0
  292. package/dist/surface/tools/merge-tools.js +99 -2
  293. package/dist/surface/tools/merged-name-redirect.js +5 -1
  294. package/dist/surface/tools/observe-tools.js +15 -9
  295. package/dist/surface/tools/read/console-pre-attach.d.ts +19 -0
  296. package/dist/surface/tools/read/console-pre-attach.js +23 -0
  297. package/dist/surface/tools/read-tools.js +33 -30
  298. package/dist/surface/tools/real-input-attempt.d.ts +0 -8
  299. package/dist/surface/tools/real-input-attempt.js +143 -28
  300. package/dist/surface/tools/session-ports.d.ts +14 -0
  301. package/dist/surface/tools/session-ports.js +53 -0
  302. package/dist/surface/tools/tool-kit.d.ts +6 -0
  303. package/dist/surface/tools/tool-kit.js +2 -3
  304. package/dist/surface/tools/tool-surface.js +0 -1
  305. package/dist/surface/tools/tools.js +28 -5
  306. package/dist/surface/tools/type-spelling.d.ts +13 -0
  307. package/dist/surface/tools/type-spelling.js +36 -0
  308. package/dist/telemetry/agent-client.d.ts +6 -0
  309. package/dist/telemetry/agent-client.js +10 -0
  310. package/dist/telemetry/argument-shape.js +1 -0
  311. package/dist/telemetry/tool-refused.d.ts +6 -3
  312. package/dist/telemetry/tool-refused.js +8 -3
  313. package/docs/actions.mdx +3 -3
  314. package/docs/agent-cheatsheet.md +1 -1
  315. package/docs/capabilities.mdx +61 -1
  316. package/docs/cli/affected.mdx +9 -7
  317. package/docs/cli/cloud.mdx +3 -3
  318. package/docs/cli/compare.mdx +94 -0
  319. package/docs/cli/doctor.mdx +1 -1
  320. package/docs/cli/grade.mdx +97 -0
  321. package/docs/cli/init.mdx +2 -2
  322. package/docs/cli/license.mdx +9 -7
  323. package/docs/cli/restart.mdx +7 -5
  324. package/docs/cli/rollback.mdx +5 -5
  325. package/docs/cli/stop.mdx +3 -3
  326. package/docs/cli/update.mdx +2 -2
  327. package/docs/cli/verify.mdx +23 -1
  328. package/docs/cli.mdx +33 -23
  329. package/docs/deploy-checks.md +1 -1
  330. package/docs/desktop.mdx +1 -1
  331. package/docs/enterprise.md +2 -2
  332. package/docs/environment.mdx +1 -1
  333. package/docs/flows.md +11 -1
  334. package/docs/frameworks.mdx +9 -9
  335. package/docs/getting-started.md +1 -1
  336. package/docs/install-manual.mdx +1 -1
  337. package/docs/license-activation.mdx +3 -3
  338. package/docs/packages/tauri.mdx +1 -1
  339. package/docs/packages.mdx +1 -1
  340. package/docs/predicates.mdx +1 -1
  341. package/docs/quickstart.mdx +68 -49
  342. package/docs/skill-setup.md +1 -1
  343. package/docs/skill.md +14 -13
  344. package/docs/telemetry.md +2 -2
  345. package/docs/tools/navigate.mdx +1 -1
  346. package/docs/tools/verify.mdx +4 -1
  347. package/docs/troubleshooting.mdx +30 -1
  348. package/docs/usage.md +17 -14
  349. package/package.json +11 -14
  350. package/dist/features/exhaust/exhaust-run.d.ts +0 -33
  351. package/dist/features/exhaust/exhaust-run.js +0 -82
@@ -28,6 +28,16 @@ export const AMBIENT_RECORDING = '__ambient__';
28
28
  export const ambientRecordingOf = (session) => session === undefined ? AMBIENT_RECORDING : `${AMBIENT_RECORDING}:${session}`;
29
29
  /** Whether a recording is an ambient tape: the daemon's own, never one somebody opened. */
30
30
  export const isAmbient = (name) => AMBIENT_RECORDING === name || name.startsWith(`${AMBIENT_RECORDING}:`);
31
+ /**
32
+ * Whether a step, or one of its sub-steps, declared a consequence.
33
+ * ponytail: the same test as `carriesAnAssertion` in memory/journal/drive-flow.ts, which this
34
+ * directory may not import. Move both into core if a third caller needs it.
35
+ */
36
+ const isChecked = (step) => {
37
+ const subs = Array.isArray(step.args['steps']) ? step.args['steps'] : [];
38
+ return (step.expect !== undefined ||
39
+ subs.some((sub) => 'object' === typeof sub && null !== sub && 'expect' in sub));
40
+ };
31
41
  /**
32
42
  * How long a journey the ambient tape will hold.
33
43
  *
@@ -36,17 +46,43 @@ export const isAmbient = (name) => AMBIENT_RECORDING === name || name.startsWith
36
46
  * 400-step regression test: the longest saved flow in the corpus is a small fraction of this.
37
47
  */
38
48
  const AMBIENT_STEP_CAP = 400;
49
+ /** How many ended pages keep their unchecked steps for a successor. See takeAmbient. */
50
+ const CARRY_CAP = 32;
39
51
  export class RecordingStore {
40
52
  byHarness;
53
+ successorOf;
41
54
  #active = new Map();
42
55
  #compiled = new Map();
43
56
  /** A navigation came after the last captured step — see markNavigated. */
44
57
  #navigatedSinceStep = false;
45
58
  /** Tabs with network mocks installed now, keyed by session ('' for none named). See markMocked. */
46
59
  #mocked = new Set();
47
- /** `byHarness`: whether the step being captured was driven by the Harness, which tapes its own. */
48
- constructor(byHarness = () => false) {
60
+ /**
61
+ * Steps a tab took after its last check, kept when the APP loaded a new page, by the session that
62
+ * ended. The page that loaded takes them when its own tape opens. See takeAmbient.
63
+ */
64
+ #carried = new Map();
65
+ /**
66
+ * `byHarness`: whether the step being captured was driven by the Harness, which tapes its own.
67
+ * `successorOf`: the session that the same tab connected as after a page load, if any.
68
+ */
69
+ constructor(byHarness = () => false, successorOf = () => undefined) {
49
70
  this.byHarness = byHarness;
71
+ this.successorOf = successorOf;
72
+ }
73
+ /** The carried steps that this session continues, taken. */
74
+ #takeCarried(session) {
75
+ if (session === undefined) {
76
+ return [];
77
+ }
78
+ for (const [ended, steps] of this.#carried) {
79
+ // A leased tab comes back from a page load with its own id; any other tab with a new one.
80
+ if (ended === session || this.successorOf(ended) === session) {
81
+ this.#carried.delete(ended);
82
+ return steps;
83
+ }
84
+ }
85
+ return [];
50
86
  }
51
87
  start(name, cursor, startPath, session, document) {
52
88
  const openedOver = new Map();
@@ -114,7 +150,7 @@ export class RecordingStore {
114
150
  if (!this.#active.has(ambient)) {
115
151
  this.#active.set(ambient, {
116
152
  cursor: 0,
117
- steps: [],
153
+ steps: this.#takeCarried(session),
118
154
  openedOver: new Map(),
119
155
  ...(session === undefined ? {} : { session }),
120
156
  });
@@ -265,9 +301,39 @@ export class RecordingStore {
265
301
  // One tab's tape is that tab's: the list names what somebody opened, and the plain tape as before.
266
302
  return [...this.#active.keys()].filter((name) => !name.startsWith(`${AMBIENT_RECORDING}:`));
267
303
  }
268
- /** The ambient tape of one tab (or of steps no tab named), taken: what its session end saves. */
304
+ /**
305
+ * The ambient tape of one tab (or of steps no tab named), taken: what its session end saves.
306
+ *
307
+ * A session also ends when the app itself loads a page (a form post on a server-rendered app), and
308
+ * the journey goes on in the page that loaded. A tape with no check is not saved, so the next
309
+ * page's tape starts with a copy of it: a fill and the submit that proves it are then one flow, not
310
+ * a fill that is dropped and a submit on an empty form. When Reticle navigated away, the agent
311
+ * started a new journey, and nothing is carried.
312
+ * ponytail: only a tape with NO check is carried; unchecked steps after a check stay behind.
313
+ * ponytail: kept for the last CARRY_CAP ended pages; a tab that never loads a successor drops them.
314
+ */
269
315
  takeAmbient(session) {
270
- return this.stop(ambientRecordingOf(session));
316
+ const tape = this.stop(ambientRecordingOf(session));
317
+ const unchecked = tape !== undefined && !tape.steps.some(isChecked);
318
+ if (tape === undefined || session === undefined || this.#navigatedSinceStep || !unchecked) {
319
+ return tape;
320
+ }
321
+ // The next page may already have a tape: its step was recorded before this session ended.
322
+ const next = this.successorOf(session);
323
+ const nextTape = next === undefined ? undefined : this.#active.get(ambientRecordingOf(next));
324
+ if (nextTape !== undefined) {
325
+ nextTape.steps.unshift(...tape.steps);
326
+ return tape;
327
+ }
328
+ this.#carried.set(session, [...tape.steps]);
329
+ while (this.#carried.size > CARRY_CAP) {
330
+ const oldest = this.#carried.keys().next().value;
331
+ if (oldest === undefined) {
332
+ break;
333
+ }
334
+ this.#carried.delete(oldest);
335
+ }
336
+ return tape;
271
337
  }
272
338
  /**
273
339
  * Names of recordings that have been STOPPED and compiled.
@@ -1,4 +1,4 @@
1
- import { pathOf, type Predicate, type ReticleEvent } from '@reticlehq/core';
1
+ import { type Predicate, type ReticleEvent } from '@reticlehq/core';
2
2
  import { type CommandResult } from '@reticlehq/core';
3
3
  import type { RecordedStep, CompiledProgram } from './recording/tape/recordings.js';
4
4
  import type { Session } from '../../portal/session/session.js';
@@ -13,6 +13,15 @@ export declare function ambiguousTestidNote(value: string): string;
13
13
  * replay engines (replayProgram + flow-replay's step runner) so element extraction — the core of anchor
14
14
  * resolution — can't drift between them.
15
15
  */
16
+ /**
17
+ * The refs of a role query's matches that have NO accessible name, in page order.
18
+ *
19
+ * A role query without a name returns every control of the role, named ones included. A nameless
20
+ * anchor's recorded position (`nth` of `of`) counts only the nameless ones — an icon-only edit
21
+ * button is the 2nd nameless button of its row, not the 5th button — so the named ones are dropped
22
+ * before the position is read.
23
+ */
24
+ export declare function namelessRefs(result: CommandResult): string[];
16
25
  export declare function queryRefs(result: CommandResult): string[];
17
26
  /**
18
27
  * A destructive-action confirmation is one-shot and must never persist into a recording.
@@ -29,12 +38,16 @@ export declare function queryRefs(result: CommandResult): string[];
29
38
  */
30
39
  export declare function replayActionArgs(value: unknown, confirmDangerous?: boolean, field?: string): Record<string, unknown>;
31
40
  /**
32
- * The pathname of a session's live url, or undefined when it has none yet.
41
+ * The page a session's live url is on, as a NAVIGABLE path: pathname + query + hash, with
42
+ * Reticle's own params dropped. Undefined when the session has no url yet.
33
43
  *
34
- * Pathname rather than the whole url, because that is what `startPath` is compared against and what
35
- * a hash-routed app makes meaningless in the document location — see routeOfEvent.
44
+ * Every recorded page comes from here: a step's page, the route a tape is cut on, and the
45
+ * `startPath` and route `requires` of a saved flow. The pathname alone dropped the query, so a
46
+ * journey leased on `/?showModal=login` saved `startPath: "/"`, and its replay asserted "the modal
47
+ * is absent" on a page where the modal never opens. Under a hash router it dropped the hash too,
48
+ * which made every page `/` (#1411).
36
49
  */
37
- export { pathOf };
50
+ export declare function pageOf(url: string | undefined): string | undefined;
38
51
  /**
39
52
  * Did the app send a write request in this window? A step that did is saved `writes: true`, so a
40
53
  * replay does not repeat it unasked. The dev server's own traffic is not the app's.
@@ -134,3 +147,4 @@ via?: readonly {
134
147
  flow: string;
135
148
  step: number;
136
149
  }[]): Promise<ReplayStepResult[]>;
150
+ export {};
@@ -1,9 +1,10 @@
1
1
  import { SECRET_FIELD_ARG, SECRET_NAME_ARG, secretKeyForFill, secretValue, } from './fields/flow-secret-field.js';
2
- import { EventType, FlowPredicateSchema, MUTATING_METHODS, isDevToolingUrl, pathOf, } from '@reticlehq/core';
2
+ import { EventType, FlowPredicateSchema, MUTATING_METHODS, isDevToolingUrl, } from '@reticlehq/core';
3
3
  import { DANGEROUS_ACTION_CONFIRM_ARG, DOCUMENT_ANCHOR_ROLE, ReticleCommand, QueryBy, isGlobalPressCall, } from '@reticlehq/core';
4
4
  import { ReticleTool, invokedFlowMissing } from '@reticlehq/core';
5
5
  import { formatStepAddress } from 'open-verification';
6
6
  import { refFor } from './flow-anchor.js';
7
+ import { navigablePath, routeOfUrl } from '@reticlehq/engine/question/predicate/predicate-route.js';
7
8
  import { asRecord, asString } from '@reticlehq/core';
8
9
  /**
9
10
  * The note attached when a testid resolves to multiple live elements (the first match is used).
@@ -21,6 +22,32 @@ function ambiguousRoleNote(role, name) {
21
22
  * replay engines (replayProgram + flow-replay's step runner) so element extraction — the core of anchor
22
23
  * resolution — can't drift between them.
23
24
  */
25
+ /**
26
+ * The refs of a role query's matches that have NO accessible name, in page order.
27
+ *
28
+ * A role query without a name returns every control of the role, named ones included. A nameless
29
+ * anchor's recorded position (`nth` of `of`) counts only the nameless ones — an icon-only edit
30
+ * button is the 2nd nameless button of its row, not the 5th button — so the named ones are dropped
31
+ * before the position is read.
32
+ */
33
+ export function namelessRefs(result) {
34
+ if (!result.ok) {
35
+ return [];
36
+ }
37
+ const elements = asRecord(result.result)['elements'];
38
+ if (!Array.isArray(elements)) {
39
+ return [];
40
+ }
41
+ const refs = [];
42
+ for (const element of elements) {
43
+ const e = asRecord(element);
44
+ const ref = asString(e['ref']);
45
+ if (0 === (asString(e['name']) ?? '').trim().length && ref !== undefined) {
46
+ refs.push(ref);
47
+ }
48
+ }
49
+ return refs;
50
+ }
24
51
  export function queryRefs(result) {
25
52
  if (!result.ok) {
26
53
  return [];
@@ -86,12 +113,22 @@ function sourceFromResult(res) {
86
113
  return out;
87
114
  }
88
115
  /**
89
- * The pathname of a session's live url, or undefined when it has none yet.
116
+ * The page a session's live url is on, as a NAVIGABLE path: pathname + query + hash, with
117
+ * Reticle's own params dropped. Undefined when the session has no url yet.
90
118
  *
91
- * Pathname rather than the whole url, because that is what `startPath` is compared against and what
92
- * a hash-routed app makes meaningless in the document location — see routeOfEvent.
119
+ * Every recorded page comes from here: a step's page, the route a tape is cut on, and the
120
+ * `startPath` and route `requires` of a saved flow. The pathname alone dropped the query, so a
121
+ * journey leased on `/?showModal=login` saved `startPath: "/"`, and its replay asserted "the modal
122
+ * is absent" on a page where the modal never opens. Under a hash router it dropped the hash too,
123
+ * which made every page `/` (#1411).
93
124
  */
94
- export { pathOf };
125
+ export function pageOf(url) {
126
+ if (url === undefined) {
127
+ return undefined;
128
+ }
129
+ const parts = routeOfUrl(url);
130
+ return parts === undefined ? undefined : navigablePath(parts);
131
+ }
95
132
  /**
96
133
  * Did the app send a write request in this window? A step that did is saved `writes: true`, so a
97
134
  * replay does not repeat it unasked. The dev server's own traffic is not the app's.
@@ -300,6 +337,9 @@ function anchorArgs(r, ref, action, actArgs) {
300
337
  }
301
338
  if (testidOf !== undefined) {
302
339
  testidArgs['of'] = testidOf;
340
+ if ('number' === typeof r['testidNth'] && 0 <= r['testidNth']) {
341
+ testidArgs['nth'] = r['testidNth'];
342
+ }
303
343
  }
304
344
  return { args: testidArgs, stable: true };
305
345
  }
@@ -324,8 +364,33 @@ function anchorArgs(r, ref, action, actArgs) {
324
364
  roleArgs['nth'] = r['nth'];
325
365
  roleArgs['of'] = r['of'];
326
366
  }
367
+ if (Array.isArray(r['within'])) {
368
+ roleArgs['within'] = r['within'];
369
+ }
327
370
  return { args: roleArgs, stable: true };
328
371
  }
372
+ // A role and NO accessible name (an icon-only button): the role plus the control's position among
373
+ // the nameless controls of that role. Without this branch the step fell to a component anchor or
374
+ // to the stale ref, and a replay on a fresh page could not find the control again.
375
+ if (role !== undefined &&
376
+ role !== 'generic' &&
377
+ name === undefined &&
378
+ asString(r['component']) === undefined) {
379
+ const namelessArgs = {
380
+ by: QueryBy.ROLE,
381
+ value: role,
382
+ action,
383
+ args: actArgs,
384
+ };
385
+ if (source !== undefined) {
386
+ namelessArgs['source'] = source;
387
+ }
388
+ if ('number' === typeof r['nth'] && 'number' === typeof r['of']) {
389
+ namelessArgs['nth'] = r['nth'];
390
+ namelessArgs['of'] = r['of'];
391
+ }
392
+ return { args: namelessArgs, stable: true };
393
+ }
329
394
  const component = asString(r['component']);
330
395
  if (component !== undefined || source !== undefined) {
331
396
  const componentArgs = { by: QueryBy.COMPONENT, action, args: actArgs };
@@ -335,6 +400,21 @@ function anchorArgs(r, ref, action, actArgs) {
335
400
  if (source !== undefined) {
336
401
  componentArgs['source'] = source;
337
402
  }
403
+ // Which of the elements this anchor matches was acted on: a list renders every item from one site.
404
+ // The element's own words, when it has no accessible name: they survive a change of layout.
405
+ const ownText = asString(r['ownText']);
406
+ if (ownText !== undefined && ownText.length > 0) {
407
+ componentArgs['text'] = ownText;
408
+ }
409
+ // The role, too: a nameless control with no own words has no other identity a layout keeps.
410
+ const role = asString(r['role']);
411
+ if (role !== undefined && role.length > 0) {
412
+ componentArgs['role'] = role;
413
+ }
414
+ if ('number' === typeof r['componentNth'] && 'number' === typeof r['componentOf']) {
415
+ componentArgs['nth'] = r['componentNth'];
416
+ componentArgs['of'] = r['componentOf'];
417
+ }
338
418
  return { args: componentArgs, stable: true };
339
419
  }
340
420
  return { args: { ref, action, args: actArgs }, stable: false };
@@ -350,6 +430,8 @@ export function compileSequenceStep(args, res) {
350
430
  ? asRecord(res)['steps']
351
431
  : [];
352
432
  let stable = inputSteps.length > 0;
433
+ // A declared `expect` that a flow cannot keep (a session ref), said on save as for a single act (#2188).
434
+ let unkept = false;
353
435
  const subSteps = inputSteps.flatMap((raw, i) => {
354
436
  // A step on Reticle's own panel is not a step of the app's journey, as for a single act.
355
437
  if (true === asRecord(resolved[i])['reticleUi']) {
@@ -364,14 +446,22 @@ export function compileSequenceStep(args, res) {
364
446
  // one batch, but what each step CLAIMED is per step -- folding them together would let one
365
447
  // signal answer for every click in the journey.
366
448
  const expect = enforceableExpect(step['expect']);
449
+ if (expect === undefined && step['expect'] !== undefined) {
450
+ unkept = true;
451
+ }
367
452
  return [expect === undefined ? compiled.args : { ...compiled.args, expect }];
368
453
  });
369
454
  const timeoutMs = explicitTimeoutMs(args);
455
+ // The pace is part of what the sequence did: replay at the default settle answered differently (#2130).
456
+ const burst = args['burst'];
457
+ const burstMs = 'number' === typeof burst && Number.isFinite(burst) && 0 <= burst ? burst : undefined;
370
458
  return {
371
459
  tool: ReticleTool.ACT_SEQUENCE,
372
460
  stable,
373
461
  args: { steps: subSteps },
374
462
  ...(timeoutMs === undefined ? {} : { timeoutMs }),
463
+ ...(burstMs === undefined ? {} : { burstMs }),
464
+ ...(unkept ? { unkeptExpect: UNKEPT_EXPECT } : {}),
375
465
  };
376
466
  }
377
467
  /**
@@ -412,11 +502,13 @@ async function resolveRef(session, step) {
412
502
  if (!result.ok) {
413
503
  throw new Error(result.error ?? 'query failed');
414
504
  }
415
- const refs = queryRefs(result);
505
+ const refs = name === undefined ? namelessRefs(result) : queryRefs(result);
416
506
  const meant = refFor(step, refs);
417
507
  const ref = meant ?? refs[0];
418
508
  if (ref === undefined) {
419
- throw new Error(`${value} named '${String(name)}' did not resolve in current page`);
509
+ throw new Error(name === undefined
510
+ ? `no ${value} with no accessible name resolved in current page`
511
+ : `${value} named '${String(name)}' did not resolve in current page`);
420
512
  }
421
513
  return meant === undefined ? { ref, note: ambiguousRoleNote(value, String(name)) } : { ref };
422
514
  }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Steps that reach past the page the journey is on: one opens another page (`navigate`), one sends a
3
+ * request to the app (`request`). Neither acts on an element, and both depend on a caller that owns
4
+ * what the replay cannot reach by itself: the sessions, the origin, the cookies.
5
+ */
6
+ import { type Drift, type FlowRequest, type FlowStep, type FlowStepResult } from '@reticlehq/core';
7
+ /**
8
+ * Opens the page a `navigate` step names and returns the session of the page that loaded, or why no
9
+ * page of the app answered. A full page load ends the session that asked for it, so only the caller
10
+ * that owns the sessions can do this.
11
+ */
12
+ export type OpenPage<S> = (url: string) => Promise<{
13
+ session: S;
14
+ } | {
15
+ error: string;
16
+ }>;
17
+ /**
18
+ * Sends the request a `request` step names and returns the answer, or why nothing could be sent.
19
+ * Supplied by the caller that knows the app's origin and the page's cookies.
20
+ */
21
+ export type SendRequest = (request: FlowRequest) => Promise<{
22
+ status: number;
23
+ body: string;
24
+ } | {
25
+ error: string;
26
+ }>;
27
+ /** Where the page's cookies come from: the driven browser's storage state. */
28
+ interface CookieSource {
29
+ storageState?(sessionUrl: string): Promise<unknown>;
30
+ }
31
+ /** The step's result before its `expect` is judged, and the session to go on with. */
32
+ export declare function openStepPage<S>(step: FlowStep, index: number, open: OpenPage<S> | undefined): Promise<{
33
+ result: FlowStepResult;
34
+ session?: S;
35
+ }>;
36
+ export declare function runRequestStep(step: FlowStep, index: number, send: SendRequest | undefined, judged: boolean,
37
+ /**
38
+ * The run's own yes to steps that change data, never the flow file's: flow files are shared, and a
39
+ * saved `DELETE` would otherwise run with the user's cookies on any app the flow is pointed at.
40
+ */
41
+ confirmDangerous: boolean): Promise<FlowStepResult>;
42
+ /** True for a step that reaches past the page: `navigate` or `request`. */
43
+ export declare function isPageStep(step: FlowStep): boolean;
44
+ /**
45
+ * Run a `navigate` or `request` step. `judge` evaluates an `expect` on a page; it comes from the
46
+ * replay so this file does not import the replay back. `checked` is false for a setup step, whose
47
+ * consequence is not checked. A navigate step returns the session to go on with.
48
+ */
49
+ export declare function runPageStep<S>(step: FlowStep, index: number, ctx: {
50
+ open?: OpenPage<S> | undefined;
51
+ send?: SendRequest | undefined;
52
+ checked: boolean;
53
+ confirmDangerous: boolean;
54
+ judge: (session: S, expect: NonNullable<FlowStep['expect']>) => Promise<Drift | undefined>;
55
+ route: (session: S) => string | undefined;
56
+ }): Promise<{
57
+ result: FlowStepResult;
58
+ session?: S;
59
+ }>;
60
+ /**
61
+ * The sender a `request` step uses: same origin as the page only, the page's cookies unless the step
62
+ * is anonymous, no redirects followed (a 302 is the answer to judge). `pageUrl` is read at send time,
63
+ * because a `navigate` step changes the page.
64
+ */
65
+ export declare function requestSender(pageUrl: () => string | undefined, provider: CookieSource | undefined,
66
+ /** The `Cookie` header of the account a step names in `as`, or undefined when not supplied. */
67
+ accountCookie?: (name: string) => string | undefined): SendRequest;
68
+ export {};
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Steps that reach past the page the journey is on: one opens another page (`navigate`), one sends a
3
+ * request to the app (`request`). Neither acts on an element, and both depend on a caller that owns
4
+ * what the replay cannot reach by itself: the sessions, the origin, the cookies.
5
+ */
6
+ import { FlowStepTool, } from '@reticlehq/core';
7
+ /*
8
+ * A step that opens a page in the middle of a journey. The replay goes on with the page that loaded,
9
+ * and the step's `expect` is judged there (see `OpenPage`).
10
+ */
11
+ /** The step's result before its `expect` is judged, and the session to go on with. */
12
+ export async function openStepPage(step, index, open) {
13
+ const url = step.navigate ?? '';
14
+ const base = { step: index, tool: FlowStepTool.NAVIGATE, anchor: url };
15
+ // Never skipped: a journey that reports green without the page it was meant to check is a false
16
+ // green arriving through the step meant to make the check stronger.
17
+ if (open === undefined) {
18
+ return {
19
+ result: {
20
+ ...base,
21
+ ok: false,
22
+ error: `this replay cannot open a page, so the step that opens ${url} did not run`,
23
+ },
24
+ };
25
+ }
26
+ const opened = await open(url);
27
+ if ('error' in opened) {
28
+ return { result: { ...base, ok: false, error: opened.error } };
29
+ }
30
+ return { result: { ...base, ok: true }, session: opened.session };
31
+ }
32
+ /** Why an answer does not match the step's `response`, or undefined when it does. */
33
+ function responseMismatch(response, answer) {
34
+ const statuses = response.status === undefined ? [] : [response.status].flat();
35
+ if (statuses.length > 0 && !statuses.includes(answer.status)) {
36
+ return `the answer was ${String(answer.status)}, expected ${statuses.join(' or ')}`;
37
+ }
38
+ if (response.contains !== undefined && !answer.body.includes(response.contains)) {
39
+ return `the answer (${String(answer.status)}) does not contain "${response.contains}"`;
40
+ }
41
+ if (response.absent !== undefined && answer.body.includes(response.absent)) {
42
+ return `the answer (${String(answer.status)}) contains "${response.absent}", which it must not`;
43
+ }
44
+ return undefined;
45
+ }
46
+ /*
47
+ * A step that sends one request and judges the answer. It never skips: a rule the flow says it
48
+ * checks and did not is a false green.
49
+ */
50
+ /** Methods that only read. Any other method changes data and needs the run's confirmation. */
51
+ const READ_METHODS = new Set(['GET', 'HEAD', 'OPTIONS']);
52
+ export async function runRequestStep(step, index, send, judged,
53
+ /**
54
+ * The run's own yes to steps that change data, never the flow file's: flow files are shared, and a
55
+ * saved `DELETE` would otherwise run with the user's cookies on any app the flow is pointed at.
56
+ */
57
+ confirmDangerous) {
58
+ const request = step.request;
59
+ const label = request === undefined ? '' : `${request.method} ${request.url}`;
60
+ const base = { step: index, tool: FlowStepTool.REQUEST, anchor: label };
61
+ if (request === undefined || send === undefined) {
62
+ return {
63
+ ...base,
64
+ ok: false,
65
+ error: `this replay cannot send a request, so ${label} did not run`,
66
+ };
67
+ }
68
+ if (!READ_METHODS.has(request.method.toUpperCase()) && !confirmDangerous) {
69
+ return {
70
+ ...base,
71
+ ok: false,
72
+ error: `potentially destructive request blocked: ${label} changes data, so it was not sent. ` +
73
+ 'Confirm it for this run: reticle_verify{action:"flows", confirmDangerous: true}, or ' +
74
+ '`reticle verify <url> --confirm-dangerous`. It cannot be confirmed in the flow file.',
75
+ };
76
+ }
77
+ const answer = await send(request);
78
+ if ('error' in answer) {
79
+ return { ...base, ok: false, error: answer.error };
80
+ }
81
+ const mismatch = !judged || step.response === undefined ? undefined : responseMismatch(step.response, answer);
82
+ return mismatch === undefined ? { ...base, ok: true } : { ...base, ok: false, error: mismatch };
83
+ }
84
+ /** True for a step that reaches past the page: `navigate` or `request`. */
85
+ export function isPageStep(step) {
86
+ return step.navigate !== undefined || step.request !== undefined;
87
+ }
88
+ /**
89
+ * Run a `navigate` or `request` step. `judge` evaluates an `expect` on a page; it comes from the
90
+ * replay so this file does not import the replay back. `checked` is false for a setup step, whose
91
+ * consequence is not checked. A navigate step returns the session to go on with.
92
+ */
93
+ export async function runPageStep(step, index, ctx) {
94
+ if (step.request !== undefined) {
95
+ return {
96
+ result: await runRequestStep(step, index, ctx.send, ctx.checked, ctx.confirmDangerous),
97
+ };
98
+ }
99
+ const opened = await openStepPage(step, index, ctx.open);
100
+ if (opened.session === undefined) {
101
+ return opened;
102
+ }
103
+ const result = opened.result;
104
+ const page = ctx.route(opened.session);
105
+ if (page !== undefined) {
106
+ result.page = page;
107
+ }
108
+ const drift = step.expect === undefined || !ctx.checked
109
+ ? undefined
110
+ : await ctx.judge(opened.session, step.expect);
111
+ if (drift !== undefined) {
112
+ result.ok = false;
113
+ result.drift = drift;
114
+ }
115
+ return { result, session: opened.session };
116
+ }
117
+ /** How long a `request` step waits for the answer. */
118
+ const REQUEST_TIMEOUT_MS = 15_000;
119
+ /** The `Cookie` header the page would send to `host`, from the driven browser's storage state. */
120
+ function cookieHeader(state, host) {
121
+ const cookies = state?.cookies;
122
+ if (!Array.isArray(cookies)) {
123
+ return '';
124
+ }
125
+ return cookies
126
+ .filter((c) => {
127
+ const domain = c.domain;
128
+ if (typeof domain !== 'string') {
129
+ return false;
130
+ }
131
+ const bare = domain.replace(/^\./, '');
132
+ return host === bare || host.endsWith(`.${bare}`);
133
+ })
134
+ .map((c) => `${c.name}=${c.value}`)
135
+ .join('; ');
136
+ }
137
+ /**
138
+ * The sender a `request` step uses: same origin as the page only, the page's cookies unless the step
139
+ * is anonymous, no redirects followed (a 302 is the answer to judge). `pageUrl` is read at send time,
140
+ * because a `navigate` step changes the page.
141
+ */
142
+ export function requestSender(pageUrl, provider,
143
+ /** The `Cookie` header of the account a step names in `as`, or undefined when not supplied. */
144
+ accountCookie = () => undefined) {
145
+ return async (request) => {
146
+ const url = pageUrl();
147
+ if (url === undefined) {
148
+ return { error: 'the page has no known address, so the request has nowhere to go' };
149
+ }
150
+ const origin = new URL(url).origin;
151
+ const target = new URL(request.url, origin);
152
+ if (target.origin !== origin) {
153
+ return {
154
+ error: `${request.url} is not on the app's origin ${origin}; a flow sends requests only to its app`,
155
+ };
156
+ }
157
+ const headers = {};
158
+ if (request.body !== undefined) {
159
+ headers['content-type'] = request.contentType ?? 'application/json';
160
+ }
161
+ if (request.as !== undefined) {
162
+ const cookie = accountCookie(request.as);
163
+ if (cookie === undefined || '' === cookie) {
164
+ return {
165
+ error: `the session of the account "${request.as}" was not supplied, so the request was not ` +
166
+ 'sent. Supply its Cookie header as a secret for this run.',
167
+ };
168
+ }
169
+ headers['cookie'] = cookie;
170
+ }
171
+ else if (true !== request.anonymous) {
172
+ const state = await provider?.storageState?.(url);
173
+ if (state === undefined) {
174
+ return {
175
+ error: 'the page cookies cannot be read here (this is not a driven browser), so the request ' +
176
+ 'cannot go as the signed-in user. Mark it `anonymous`, or replay with `reticle verify`.',
177
+ };
178
+ }
179
+ const cookie = cookieHeader(state, target.hostname);
180
+ if (cookie !== '') {
181
+ headers['cookie'] = cookie;
182
+ }
183
+ }
184
+ try {
185
+ const answer = await fetch(target, {
186
+ method: request.method,
187
+ headers,
188
+ ...(request.body === undefined ? {} : { body: request.body }),
189
+ redirect: 'manual',
190
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
191
+ });
192
+ return { status: answer.status, body: await answer.text() };
193
+ }
194
+ catch (error) {
195
+ return { error: `the request ${request.method} ${target.pathname} failed: ${String(error)}` };
196
+ }
197
+ };
198
+ }