e2e 0.16.0 → 0.18.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 (256) hide show
  1. package/README.md +10 -0
  2. package/dist/.build.json +1 -1
  3. package/dist/agent/act.js +7 -4
  4. package/dist/agent/action-dispatcher.js +4 -5
  5. package/dist/agent/actions.d.ts +32 -23
  6. package/dist/agent/actions.js +38 -24
  7. package/dist/agent/ai-sdk.js +5 -4
  8. package/dist/agent/error.js +3 -3
  9. package/dist/agent/invocation.js +3 -4
  10. package/dist/agent/model/sdk.js +2 -0
  11. package/dist/agent/model/tool-choice.d.ts +9 -0
  12. package/dist/agent/model/tool-choice.js +14 -0
  13. package/dist/agent/observation-feed.d.ts +5 -6
  14. package/dist/agent/observation-feed.js +8 -11
  15. package/dist/agent/observation.d.ts +40 -11
  16. package/dist/agent/observation.js +109 -59
  17. package/dist/agent/phases.d.ts +7 -1
  18. package/dist/agent/phases.js +12 -3
  19. package/dist/agent/point-tap.d.ts +8 -8
  20. package/dist/agent/point-tap.js +1 -1
  21. package/dist/agent/prompts.js +3 -0
  22. package/dist/agent/replay.d.ts +19 -8
  23. package/dist/agent/replay.js +29 -29
  24. package/dist/agent/scroll-target.d.ts +2 -3
  25. package/dist/agent/step-cache.d.ts +43 -15
  26. package/dist/agent/step-cache.js +156 -67
  27. package/dist/agent/tool-loop.js +94 -5
  28. package/dist/cache/anchors.d.ts +72 -23
  29. package/dist/cache/anchors.js +233 -60
  30. package/dist/cache/context.d.ts +25 -14
  31. package/dist/cache/context.js +50 -8
  32. package/dist/cache/decide.d.ts +2 -2
  33. package/dist/cache/decide.js +4 -6
  34. package/dist/cache/identity.d.ts +18 -4
  35. package/dist/cache/identity.js +9 -4
  36. package/dist/cache/recorder.d.ts +15 -1
  37. package/dist/cache/recorder.js +36 -23
  38. package/dist/cache/rekeyed.d.ts +46 -0
  39. package/dist/cache/rekeyed.js +89 -0
  40. package/dist/cache/relocate.d.ts +14 -14
  41. package/dist/cache/relocate.js +33 -25
  42. package/dist/cache/route.d.ts +32 -33
  43. package/dist/cache/route.js +94 -50
  44. package/dist/cache/store.d.ts +6 -0
  45. package/dist/cache/store.js +21 -1
  46. package/dist/cache/trace.d.ts +53 -14
  47. package/dist/cache/trace.js +76 -30
  48. package/dist/cli/bin.js +2 -2
  49. package/dist/cli/index.js +43 -32
  50. package/dist/cli/init/engines.d.ts +1 -3
  51. package/dist/cli/init/engines.js +2 -2
  52. package/dist/cli/init/gateways.d.ts +1 -1
  53. package/dist/cli/init/gateways.js +11 -1
  54. package/dist/cli/init/mcp-config.d.ts +2 -1
  55. package/dist/cli/init/mcp-config.js +8 -3
  56. package/dist/cli/init/sibling-versions.json +1 -1
  57. package/dist/cli/init/versions.d.ts +0 -8
  58. package/dist/cli/init/versions.js +1 -10
  59. package/dist/cli/mcp.d.ts +3 -2
  60. package/dist/cli/mcp.js +52 -16
  61. package/dist/cli/run-output.d.ts +17 -0
  62. package/dist/cli/run-output.js +45 -0
  63. package/dist/collect/collect.js +8 -4
  64. package/dist/collect/registry.js +27 -16
  65. package/dist/config/compiled-files.d.ts +36 -0
  66. package/dist/config/compiled-files.js +50 -0
  67. package/dist/config/diagnose.js +21 -19
  68. package/dist/config/esm-hooks.d.ts +39 -44
  69. package/dist/config/esm-hooks.js +238 -63
  70. package/dist/config/load.d.ts +1 -7
  71. package/dist/config/load.js +23 -66
  72. package/dist/config/register.d.ts +8 -0
  73. package/dist/config/register.js +9 -0
  74. package/dist/config/resolve.d.ts +1 -0
  75. package/dist/config/resolve.js +33 -1
  76. package/dist/config/specifiers.d.ts +19 -0
  77. package/dist/config/specifiers.js +33 -0
  78. package/dist/config/tsconfig.d.ts +56 -0
  79. package/dist/config/tsconfig.js +115 -0
  80. package/dist/config/typescript.d.ts +19 -0
  81. package/dist/config/typescript.js +253 -0
  82. package/dist/engine/contract.d.ts +1 -0
  83. package/dist/engine/index.d.ts +4 -2
  84. package/dist/engine/session.js +2 -2
  85. package/dist/expect/async.js +4 -2
  86. package/dist/expect/poll-scope.d.ts +52 -0
  87. package/dist/expect/poll-scope.js +113 -0
  88. package/dist/expect/poll.js +33 -6
  89. package/dist/expect/values.js +11 -4
  90. package/dist/internal/abandoned.d.ts +11 -0
  91. package/dist/internal/abandoned.js +23 -0
  92. package/dist/internal/brands.d.ts +2 -0
  93. package/dist/internal/brands.js +2 -0
  94. package/dist/internal/client-identity.d.ts +9 -0
  95. package/dist/internal/client-identity.js +14 -0
  96. package/dist/internal/errors.d.ts +10 -3
  97. package/dist/internal/errors.js +23 -8
  98. package/dist/internal/node-version.d.ts +16 -7
  99. package/dist/internal/node-version.js +50 -15
  100. package/dist/internal/package-manager.js +1 -1
  101. package/dist/internal/realm-slot.d.ts +5 -4
  102. package/dist/internal/redact.d.ts +9 -4
  103. package/dist/internal/redact.js +120 -23
  104. package/dist/internal/time.d.ts +26 -4
  105. package/dist/internal/time.js +58 -14
  106. package/dist/internal/urls.d.ts +9 -5
  107. package/dist/internal/urls.js +19 -10
  108. package/dist/locator/engine.d.ts +12 -2
  109. package/dist/locator/engine.js +55 -14
  110. package/dist/locator/screen.js +59 -9
  111. package/dist/mcp/catalog.d.ts +3 -2
  112. package/dist/mcp/catalog.js +6 -6
  113. package/dist/mcp/output.d.ts +59 -0
  114. package/dist/mcp/output.js +129 -0
  115. package/dist/mcp/server.d.ts +4 -0
  116. package/dist/mcp/server.js +9 -12
  117. package/dist/mcp/session.d.ts +8 -1
  118. package/dist/mcp/session.js +56 -15
  119. package/dist/mcp/tools.js +7 -5
  120. package/dist/oauth/chatgpt.js +3 -23
  121. package/dist/oauth/cli.js +1 -0
  122. package/dist/oauth/copilot.d.ts +5 -4
  123. package/dist/oauth/copilot.js +80 -7
  124. package/dist/oauth/grok.js +2 -1
  125. package/dist/oauth/models.js +2 -1
  126. package/dist/oauth/opencode-console.d.ts +10 -0
  127. package/dist/oauth/opencode-console.js +133 -0
  128. package/dist/oauth/providers/github-copilot.d.ts +12 -0
  129. package/dist/oauth/providers/github-copilot.js +77 -23
  130. package/dist/oauth/providers/opencode-console.d.ts +31 -0
  131. package/dist/oauth/providers/opencode-console.js +148 -0
  132. package/dist/oauth/providers.d.ts +2 -6
  133. package/dist/oauth/providers.js +3 -7
  134. package/dist/oauth/responses.d.ts +9 -0
  135. package/dist/oauth/responses.js +23 -0
  136. package/dist/report/build.d.ts +26 -0
  137. package/dist/report/build.js +16 -8
  138. package/dist/report/failure-text.d.ts +6 -1
  139. package/dist/report/failure-text.js +14 -4
  140. package/dist/report/format.d.ts +10 -5
  141. package/dist/report/format.js +12 -5
  142. package/dist/report/junit.js +11 -12
  143. package/dist/report/list.d.ts +12 -0
  144. package/dist/report/list.js +61 -24
  145. package/dist/report/markdown.d.ts +1 -1
  146. package/dist/report/markdown.js +37 -14
  147. package/dist/report/repeats.d.ts +6 -1
  148. package/dist/report/repeats.js +12 -3
  149. package/dist/run/artifacts.d.ts +22 -4
  150. package/dist/run/artifacts.js +71 -3
  151. package/dist/run/execute.d.ts +34 -5
  152. package/dist/run/execute.js +93 -44
  153. package/dist/run/extended-fixtures.js +6 -2
  154. package/dist/run/failure-evidence.js +10 -4
  155. package/dist/run/fixtures.js +6 -8
  156. package/dist/run/in-process.d.ts +2 -0
  157. package/dist/run/in-process.js +52 -10
  158. package/dist/run/last-run.d.ts +42 -8
  159. package/dist/run/last-run.js +121 -14
  160. package/dist/run/process-output.d.ts +73 -0
  161. package/dist/run/process-output.js +130 -0
  162. package/dist/run/provision.d.ts +2 -0
  163. package/dist/run/provision.js +1 -0
  164. package/dist/run/realm.d.ts +4 -1
  165. package/dist/run/realm.js +20 -6
  166. package/dist/run/records.d.ts +20 -4
  167. package/dist/run/records.js +26 -0
  168. package/dist/run/retry.d.ts +10 -1
  169. package/dist/run/retry.js +30 -12
  170. package/dist/run/runner.d.ts +9 -1
  171. package/dist/run/runner.js +101 -39
  172. package/dist/run/scheduler.js +255 -19
  173. package/dist/run/secrecy.d.ts +22 -19
  174. package/dist/run/secrecy.js +22 -18
  175. package/dist/run/serial.d.ts +75 -2
  176. package/dist/run/serial.js +243 -59
  177. package/dist/run/standalone.js +1 -1
  178. package/dist/run/steps.d.ts +0 -6
  179. package/dist/run/steps.js +3 -20
  180. package/dist/run/trace-redaction.d.ts +5 -6
  181. package/dist/run/trace-redaction.js +5 -6
  182. package/dist/run/worker/entry.js +40 -18
  183. package/dist/run/worker/handle.js +10 -5
  184. package/dist/run/worker/protocol.d.ts +85 -3
  185. package/dist/run/worker/session.d.ts +2 -0
  186. package/dist/run/worker/session.js +29 -0
  187. package/dist/telemetry/environment.d.ts +2 -1
  188. package/dist/telemetry/environment.js +12 -2
  189. package/dist/telemetry/events.js +1 -0
  190. package/dist/telemetry/project.d.ts +5 -6
  191. package/dist/telemetry/project.js +6 -11
  192. package/dist/telemetry/store.d.ts +5 -5
  193. package/dist/telemetry/store.js +5 -7
  194. package/dist/telemetry/telemetry.d.ts +2 -3
  195. package/dist/telemetry/telemetry.js +2 -4
  196. package/dist/types.d.ts +15 -6
  197. package/docs/agent-steps.mdx +1 -0
  198. package/docs/agents.mdx +7 -3
  199. package/docs/api-testing.mdx +2 -1
  200. package/docs/authentication.mdx +6 -3
  201. package/docs/browser.mdx +49 -12
  202. package/docs/bug-bash.mdx +2 -1
  203. package/docs/cache.mdx +66 -19
  204. package/docs/ci.mdx +14 -9
  205. package/docs/coding-agents.mdx +2 -1
  206. package/docs/core-concepts.mdx +208 -0
  207. package/docs/debugging.mdx +25 -12
  208. package/docs/decision-models.mdx +142 -0
  209. package/docs/example-projects.mdx +32 -0
  210. package/docs/executors.mdx +7 -2
  211. package/docs/explore.mdx +1 -0
  212. package/docs/github.mdx +14 -5
  213. package/docs/index.mdx +9 -5
  214. package/docs/integrations/eas.mdx +1 -0
  215. package/docs/integrations/index.mdx +1 -0
  216. package/docs/integrations/kernel.mdx +3 -2
  217. package/docs/migrate/cypress.mdx +26 -3
  218. package/docs/migrate/detox.mdx +24 -1
  219. package/docs/migrate/maestro.mdx +28 -4
  220. package/docs/migrate/playwright.mdx +44 -18
  221. package/docs/migrate/selenium.mdx +27 -3
  222. package/docs/mobile-ci.mdx +6 -5
  223. package/docs/mobile.mdx +96 -8
  224. package/docs/models.mdx +60 -9
  225. package/docs/quickstart.mdx +28 -16
  226. package/docs/reference/agent.mdx +13 -3
  227. package/docs/reference/app.mdx +2 -1
  228. package/docs/reference/cli.mdx +41 -18
  229. package/docs/reference/config.mdx +67 -13
  230. package/docs/reference/engine.mdx +25 -13
  231. package/docs/reference/environment.mdx +10 -2
  232. package/docs/reference/errors.mdx +13 -10
  233. package/docs/reference/expect.mdx +36 -13
  234. package/docs/reference/mcp.mdx +21 -8
  235. package/docs/reference/mobile.mdx +24 -13
  236. package/docs/reference/reporters.mdx +9 -2
  237. package/docs/reference/screen.mdx +49 -16
  238. package/docs/reference/test.mdx +32 -5
  239. package/docs/reference/web.mdx +133 -23
  240. package/docs/security.mdx +58 -42
  241. package/docs/subscriptions.mdx +92 -12
  242. package/docs/telemetry.mdx +9 -6
  243. package/docs/tools.mdx +1 -0
  244. package/docs/{starting-your-app.mdx → web.mdx} +157 -11
  245. package/docs/writing-an-engine.mdx +2 -1
  246. package/package.json +25 -8
  247. package/schema/report-v1.schema.json +14 -1
  248. package/skills/e2e/SKILL.md +5 -3
  249. package/skills/e2e/references/agent.md +33 -11
  250. package/skills/e2e/references/debugging.md +4 -4
  251. package/skills/e2e/references/mcp.md +8 -3
  252. package/skills/e2e/references/running.md +15 -9
  253. package/skills/e2e/references/setup.md +45 -16
  254. package/skills/e2e/references/writing-tests.md +35 -18
  255. package/docs/viewports.mdx +0 -146
  256. package/docs/writing-tests.mdx +0 -256
@@ -1,36 +1,85 @@
1
1
  /**
2
- * End anchors.
2
+ * Anchors: the step's recorded delta.
3
3
  *
4
4
  * A recording run does not end when its last action commits; it ends when the
5
5
  * executor looks at the screen and judges the step done. The trace used to
6
6
  * keep only the actions and drop that look, so a replay could run every
7
7
  * recorded action mechanically — a fill that never committed, a save that
8
8
  * produced an unnamed record — and self-finalize with nothing checking the
9
- * effect. Anchors restore the check as data: the descriptors of nodes that
10
- * were on screen when the step passed and were not there when it began. They
11
- * are the step's own delta, checked by the relocation vocabulary
12
- * (`relocate.ts`) with two deliberate differences: every recorded field must
13
- * be equal, text included, and presence is enough — uniqueness is not asked.
9
+ * effect. Anchors restore the check as data: the descriptors of nodes the
10
+ * step made appear (`endAnchors`) and of nodes it made vanish
11
+ * (`goneAnchors`), each with the states a step sets on a control it leaves
12
+ * on screen (a switch turned on, a field typed into). They are checked by
13
+ * the relocation vocabulary (`relocate.ts`) with two deliberate differences:
14
+ * every recorded field must be equal, text, value, and states included, and
15
+ * presence is enough, uniqueness is not asked.
16
+ *
17
+ * A replay passes on its own only when the delta happened again: everything
18
+ * that appeared is there, everything that vanished is gone, and at least one
19
+ * of those changes, or the route, actually moved during the replay
20
+ * (`deltaEvidenced`), so a status that already read "Submitted" before a
21
+ * submit that did nothing is no proof. An alert the replay raised that the
22
+ * recording never saw is an outcome the recording did not have, whatever
23
+ * else matched (`deltaHolds`).
14
24
  */
15
- import type { SemanticNode } from '../engine/surface.ts';
16
- import { type DescriptorMatchOptions } from './relocate.ts';
25
+ import type { RedactedNode } from '../agent/observation.ts';
17
26
  import { type TraceTargetDescriptor } from './trace.ts';
18
- export type AnchorOptions = DescriptorMatchOptions;
27
+ /** The two sides of one step's delta, each projected, deduplicated, ordered, and capped. */
28
+ export interface StepDelta {
29
+ /** On screen when the step passed, not when it began. */
30
+ readonly appeared: TraceTargetDescriptor[];
31
+ /** On screen when the step began, not when it passed. */
32
+ readonly gone: TraceTargetDescriptor[];
33
+ }
34
+ /** The recorded delta a replay checks, as a trace carries it. */
35
+ export interface RecordedDelta {
36
+ readonly endAnchors?: readonly TraceTargetDescriptor[];
37
+ readonly goneAnchors?: readonly TraceTargetDescriptor[];
38
+ }
19
39
  /**
20
- * Derives the end anchors of one step: relocatable descriptors present in the
21
- * passing observation and absent from the starting one, deduplicated, capped.
22
- * Leaves come first, then containers, each in document order: a container's
23
- * accessible name is usually the concatenation of its children's, so it
24
- * repeats what the leaves already say — and on a list-heavy screen those
25
- * repeats would crowd the one status line that names the effect out of the
26
- * cap.
40
+ * Derives one step's delta between its starting observation and its passing
41
+ * one. Each side is ordered announcements first, then leaves, then
42
+ * containers, each in document order: a container's accessible name is
43
+ * usually the concatenation of its children's, so it repeats what the
44
+ * leaves already say, and on a list-heavy screen those repeats would crowd
45
+ * the one status line that names the effect out of the cap. `routeMoved`
46
+ * says the step ended on another route than it began on, whose counts are
47
+ * that screen's data rather than the step's result (`COUNT_TEXT`).
48
+ *
49
+ * A node counts as gone only when no end node matches its anchor the way
50
+ * `deltaHolds` checks one, on the recorded fields alone: a radio label
51
+ * "Express" the pick replaced with a status reading "Express" vanished, but
52
+ * an anchor of that text would still match the status, and the recording
53
+ * would fail its own check on every replay.
27
54
  */
28
- export declare function describeAnchors(startNodes: ReadonlyMap<string, SemanticNode>, endNodes: ReadonlyMap<string, SemanticNode>, options: AnchorOptions): TraceTargetDescriptor[];
55
+ export declare function describeDelta(startNodes: ReadonlyMap<string, RedactedNode>, endNodes: ReadonlyMap<string, RedactedNode>, routeMoved: boolean): StepDelta;
29
56
  /**
30
- * Whether every anchor is present in a fresh observation: some tier of the
31
- * anchor (`descriptorTiers` — so a test id that churned per render is
32
- * forgiven when the remaining fields still identify the node) equals some
33
- * candidate on every anchor field. Two matching nodes are the effect twice
34
- * over, not an ambiguity.
57
+ * Whether the recorded end state holds on a fresh observation: every
58
+ * appeared anchor present (some tier of it, `descriptorTiers`, so a test id
59
+ * that churned per render is forgiven when the remaining fields still
60
+ * identify the node, equal on every anchor field and state), no gone anchor
61
+ * present, and no alert on screen that was not on `beforeNodes` and is not
62
+ * one the recording saw appear. Two nodes matching one anchor are the effect
63
+ * twice over, not an ambiguity.
64
+ */
65
+ export declare function deltaHolds(delta: RecordedDelta, nodes: ReadonlyMap<string, RedactedNode>, beforeNodes: ReadonlyMap<string, RedactedNode>): boolean;
66
+ /**
67
+ * Whether the replay itself produced some of the recorded delta, measured
68
+ * from `baselineNodes`, the first screen the replay saw on the route it
69
+ * ended on: an anchor that appeared was absent there, or a gone one was
70
+ * there. Without that, every check `deltaHolds` makes could have held before
71
+ * the actions that should have produced it: the screen already read the
72
+ * outcome, and nothing the replay did is proven to have done anything. A
73
+ * recording with no delta at all is never evidence. Undefined `baselineNodes`
74
+ * means the replay saw no screen on that route before its last action, so
75
+ * that action moved the route, which is the evidence.
76
+ *
77
+ * A control one of the trace's own actions targeted (`inputTargets`) that
78
+ * shows on both sides of the delta, a field typed into or a switch flipped,
79
+ * echoes the input: the replay typed, so the value is there, whatever the
80
+ * step was for. Such echoes are evidence only when the delta holds nothing
81
+ * else; otherwise the rest of the delta must have moved, so a subscribe
82
+ * form typed into on a page that already read "You're subscribed" proves
83
+ * nothing.
35
84
  */
36
- export declare function anchorsPresent(anchors: readonly TraceTargetDescriptor[], nodes: ReadonlyMap<string, SemanticNode>, options: AnchorOptions): boolean;
85
+ export declare function deltaEvidenced(delta: RecordedDelta, baselineNodes: ReadonlyMap<string, RedactedNode> | undefined, inputTargets: readonly TraceTargetDescriptor[]): boolean;
@@ -1,20 +1,31 @@
1
1
  /**
2
- * End anchors.
2
+ * Anchors: the step's recorded delta.
3
3
  *
4
4
  * A recording run does not end when its last action commits; it ends when the
5
5
  * executor looks at the screen and judges the step done. The trace used to
6
6
  * keep only the actions and drop that look, so a replay could run every
7
7
  * recorded action mechanically — a fill that never committed, a save that
8
8
  * produced an unnamed record — and self-finalize with nothing checking the
9
- * effect. Anchors restore the check as data: the descriptors of nodes that
10
- * were on screen when the step passed and were not there when it began. They
11
- * are the step's own delta, checked by the relocation vocabulary
12
- * (`relocate.ts`) with two deliberate differences: every recorded field must
13
- * be equal, text included, and presence is enough — uniqueness is not asked.
9
+ * effect. Anchors restore the check as data: the descriptors of nodes the
10
+ * step made appear (`endAnchors`) and of nodes it made vanish
11
+ * (`goneAnchors`), each with the states a step sets on a control it leaves
12
+ * on screen (a switch turned on, a field typed into). They are checked by
13
+ * the relocation vocabulary (`relocate.ts`) with two deliberate differences:
14
+ * every recorded field must be equal, text, value, and states included, and
15
+ * presence is enough, uniqueness is not asked.
16
+ *
17
+ * A replay passes on its own only when the delta happened again: everything
18
+ * that appeared is there, everything that vanished is gone, and at least one
19
+ * of those changes, or the route, actually moved during the replay
20
+ * (`deltaEvidenced`), so a status that already read "Submitted" before a
21
+ * submit that did nothing is no proof. An alert the replay raised that the
22
+ * recording never saw is an outcome the recording did not have, whatever
23
+ * else matched (`deltaHolds`).
14
24
  */
15
25
  import { describeTarget } from '../agent/actions.js';
16
- import { describeNodes, descriptorTiers, fieldsEqual, } from './relocate.js';
17
- import { MAX_TRACE_ANCHORS } from './trace.js';
26
+ import { collapseText } from '../internal/text.js';
27
+ import { descriptorTiers, fieldsEqual } from './relocate.js';
28
+ import { bound, MAX_TRACE_ANCHORS, MAX_TRACE_DESCRIPTOR_CHARS, TRACE_ANCHOR_STATES, } from './trace.js';
18
29
  /**
19
30
  * Every field an anchor records. Unlike target relocation, text is never
20
31
  * optional here: for a target, text is a fallback identity and a relabeled
@@ -25,57 +36,95 @@ import { MAX_TRACE_ANCHORS } from './trace.js';
25
36
  */
26
37
  const ANCHOR_FIELDS = ['role', 'name', 'text', 'testId', 'placeholder', 'inputPurpose'];
27
38
  /**
28
- * Derives the end anchors of one step: relocatable descriptors present in the
29
- * passing observation and absent from the starting one, deduplicated, capped.
30
- * Leaves come first, then containers, each in document order: a container's
31
- * accessible name is usually the concatenation of its children's, so it
32
- * repeats what the leaves already say — and on a list-heavy screen those
33
- * repeats would crowd the one status line that names the effect out of the
34
- * cap.
39
+ * Roles an app announces an outcome in: the ARIA live regions a screen
40
+ * reader reads out on change, and the alert dialog. The app itself says
41
+ * this text reports what just happened, so these are recorded before any
42
+ * other anchor and the cap never crowds them out.
35
43
  */
36
- export function describeAnchors(startNodes, endNodes, options) {
37
- const before = new Set();
38
- for (const node of startNodes.values()) {
39
- const descriptor = anchorDescriptor(node, options);
40
- if (descriptor !== undefined)
41
- before.add(anchorKey(descriptor));
42
- }
44
+ const ANNOUNCEMENT_ROLES = new Set(['alert', 'alertdialog', 'status']);
45
+ /**
46
+ * The announcements that interrupt: an error, a warning, a confirmation the
47
+ * user must answer. A replay that raises one the recording never saw has
48
+ * another outcome than the recording, so every one that appears is recorded
49
+ * whatever its text, and checked both ways.
50
+ */
51
+ const ALERT_ROLES = new Set(['alert', 'alertdialog']);
52
+ /**
53
+ * Derives one step's delta between its starting observation and its passing
54
+ * one. Each side is ordered announcements first, then leaves, then
55
+ * containers, each in document order: a container's accessible name is
56
+ * usually the concatenation of its children's, so it repeats what the
57
+ * leaves already say, and on a list-heavy screen those repeats would crowd
58
+ * the one status line that names the effect out of the cap. `routeMoved`
59
+ * says the step ended on another route than it began on, whose counts are
60
+ * that screen's data rather than the step's result (`COUNT_TEXT`).
61
+ *
62
+ * A node counts as gone only when no end node matches its anchor the way
63
+ * `deltaHolds` checks one, on the recorded fields alone: a radio label
64
+ * "Express" the pick replaced with a status reading "Express" vanished, but
65
+ * an anchor of that text would still match the status, and the recording
66
+ * would fail its own check on every replay.
67
+ */
68
+ export function describeDelta(startNodes, endNodes, routeMoved) {
69
+ const start = projectAnchors(startNodes);
70
+ const end = projectAnchors(endNodes);
71
+ const startKeys = new Set(start.map((anchor) => anchor.key));
72
+ const endKeys = new Set(end.map((anchor) => anchor.key));
73
+ return {
74
+ appeared: deltaSide(end, start, (anchor) => startKeys.has(anchor.key), routeMoved),
75
+ gone: deltaSide(start, end, (anchor) => endKeys.has(anchor.key) || anchorIn(anchor.descriptor, end), routeMoved),
76
+ };
77
+ }
78
+ /** The side of the delta `nodes` holds and `other` does not (`inOther`), ordered and capped. */
79
+ function deltaSide(nodes, other, inOther, routeMoved) {
80
+ const otherShapes = new Set(other.map((anchor) => countShape(anchor.descriptor)));
81
+ const volatile = (anchor) => isVolatileAnchor(anchor, (count) => routeMoved || otherShapes.has(countShape(count)));
43
82
  const seen = new Set();
83
+ const announcements = [];
44
84
  const leaves = [];
45
85
  const containers = [];
46
- for (const node of endNodes.values()) {
47
- const descriptor = anchorDescriptor(node, options);
48
- if (descriptor === undefined)
49
- continue;
50
- const key = anchorKey(descriptor);
51
- if (before.has(key) || seen.has(key))
86
+ for (const anchor of nodes) {
87
+ if (seen.has(anchor.key) || inOther(anchor))
52
88
  continue;
53
- seen.add(key);
54
- (node.children === undefined || node.children.length === 0 ? leaves : containers).push(descriptor);
89
+ seen.add(anchor.key);
90
+ const role = anchor.descriptor.role ?? '';
91
+ (ANNOUNCEMENT_ROLES.has(role) ? announcements : anchor.leaf ? leaves : containers).push(anchor);
55
92
  }
56
- const all = [...leaves, ...containers];
57
- const stable = all.filter((anchor) => !isVolatileAnchor(anchor));
58
- return (stable.length > 0 ? stable : all).slice(0, MAX_TRACE_ANCHORS);
93
+ const all = [...announcements, ...leaves, ...containers];
94
+ const stable = all.filter((anchor) => isAlert(anchor.descriptor) || !volatile(anchor.descriptor));
95
+ return (stable.length > 0 ? stable : all).slice(0, MAX_TRACE_ANCHORS).map((anchor) => anchor.descriptor);
59
96
  }
60
97
  /**
61
98
  * Text that cannot read the same on the next run: a minted key prefix or id,
62
- * a countdown or age, a date, a clock time, a timing in milliseconds, a
63
- * pagination range or record count that grows with the data every run
64
- * leaves behind. An anchor made of it hands every replay off, so it is
65
- * skipped while some stable anchor exists; with nothing else, the volatile
66
- * ones stay, because a replay that always hands off is still safer than
67
- * one that passes on mechanics alone.
99
+ * a countdown or age, a date, a clock time, a timing in milliseconds. An
100
+ * anchor made of it hands every replay off, so it is skipped while some
101
+ * stable anchor exists; with nothing else, the volatile ones stay, because a
102
+ * replay that always hands off is still safer than one that passes on
103
+ * mechanics alone. An alert keeps it, and is compared with these parts read
104
+ * as placeholders (`alertShape`).
68
105
  */
69
106
  const VOLATILE_TEXT = [
70
107
  /\b(?=[A-Za-z0-9_-]*\d)(?=[A-Za-z0-9_-]*[A-Za-z])[A-Za-z0-9_-]{12,}\b/,
71
108
  /\b\d+\s*(?:ms|s|secs?|seconds?|m|mins?|minutes?|h|hrs?|hours?|d|days?|weeks?|months?|years?)\b/i,
72
- /\b\d+\s*(?:to|-|\u2013)\s*\d+\s+of\s+\d+\b/i,
73
- /\b\d+\s+(?:results?|items?|rows?|entries|records?|matches|total)\b/i,
74
109
  /\b(?:jan|feb|mar|apr|may|jun|jul|aug|sep|sept|oct|nov|dec)[a-z]*\.?\s+\d{1,2}(?:,?\s+\d{4})?\b/i,
75
110
  /\b\d{1,2}\s+(?:jan|feb|mar|apr|may|jun|jul|aug|sep|sept|oct|nov|dec)[a-z]*\.?(?:\s+\d{4})?\b/i,
76
111
  /\b\d{4}-\d{2}-\d{2}\b/,
77
112
  /\b\d{1,2}:\d{2}(?::\d{2})?\b/,
78
113
  ];
114
+ /**
115
+ * A count that names what it counts: a pagination range, `12 items`, `3
116
+ * records imported`. Such a count is the step's own result when the step
117
+ * made it appear on the screen it acted on, and must read the same again:
118
+ * `0 records imported` is not the import the recording saw. It is the data
119
+ * instead, which the next run, with what an earlier run left behind, reads
120
+ * differently, when the other side of the delta already showed the same
121
+ * text with other numbers (`41 items` became `42 items`), or when the step
122
+ * moved to another route and the count is part of the screen it opened.
123
+ */
124
+ const COUNT_TEXT = [
125
+ /\b\d+\s*(?:to|-|\u2013)\s*\d+\s+of\s+\d+\b/i,
126
+ /\b\d+\s+(?:results?|items?|rows?|entries|records?|matches|total)\b/i,
127
+ ];
79
128
  /**
80
129
  * A bare number is a badge or a tally, `11` unread, and reads differently
81
130
  * once the data changes; it is volatile when it is all the anchor has to say.
@@ -86,38 +135,162 @@ function isBareNumber(anchor) {
86
135
  const label = anchor.name ?? anchor.text;
87
136
  return label !== undefined && /^\d+$/.test(label) && anchor.testId === undefined && (anchor.name === undefined || anchor.text === undefined || anchor.name === anchor.text);
88
137
  }
89
- function isVolatileAnchor(anchor) {
90
- return (isBareNumber(anchor) ||
91
- [anchor.text, anchor.name].some((value) => value !== undefined && VOLATILE_TEXT.some((pattern) => pattern.test(value))));
138
+ /** Whether an anchor's text cannot read the same on the next run; `countIsData` decides an anchor holding a count. */
139
+ function isVolatileAnchor(anchor, countIsData) {
140
+ const texts = [anchor.text, anchor.name, anchor.value].filter((value) => value !== undefined);
141
+ if (isBareNumber(anchor) || texts.some((value) => VOLATILE_TEXT.some((pattern) => pattern.test(value))))
142
+ return true;
143
+ return texts.some((value) => COUNT_TEXT.some((pattern) => pattern.test(value))) && countIsData(anchor);
144
+ }
145
+ /** An anchor with every digit run read as one placeholder: the text a running count keeps while its numbers move. */
146
+ function countShape(anchor) {
147
+ return anchorKey({ ...anchor, ...digitless('name', anchor.name), ...digitless('text', anchor.text), ...digitless('value', anchor.value) });
148
+ }
149
+ function digitless(field, value) {
150
+ return value === undefined ? {} : { [field]: value.replace(/\d+/g, '#') };
151
+ }
152
+ function isAlert(anchor) {
153
+ return anchor.role !== undefined && ALERT_ROLES.has(anchor.role);
154
+ }
155
+ const VOLATILE_SPANS = VOLATILE_TEXT.map((pattern) => new RegExp(pattern.source, `${pattern.flags}g`));
156
+ /**
157
+ * An alert with its volatile parts read as one placeholder each: `Session
158
+ * expires at 17:42` and `Session expires at 17:45` are the same alert, which
159
+ * a replay must neither miss nor mistake for one the recording never saw.
160
+ */
161
+ function alertShape(anchor) {
162
+ const mask = (text) => VOLATILE_SPANS.reduce((masked, span) => masked.replace(span, '#'), text);
163
+ return {
164
+ ...anchor,
165
+ ...(anchor.name === undefined ? {} : { name: mask(anchor.name) }),
166
+ ...(anchor.text === undefined ? {} : { text: mask(anchor.text) }),
167
+ };
168
+ }
169
+ /**
170
+ * Whether the recorded end state holds on a fresh observation: every
171
+ * appeared anchor present (some tier of it, `descriptorTiers`, so a test id
172
+ * that churned per render is forgiven when the remaining fields still
173
+ * identify the node, equal on every anchor field and state), no gone anchor
174
+ * present, and no alert on screen that was not on `beforeNodes` and is not
175
+ * one the recording saw appear. Two nodes matching one anchor are the effect
176
+ * twice over, not an ambiguity.
177
+ */
178
+ export function deltaHolds(delta, nodes, beforeNodes) {
179
+ const live = projectAnchors(nodes);
180
+ const appeared = delta.endAnchors ?? [];
181
+ if (!appeared.every((anchor) => anchorIn(anchor, live)))
182
+ return false;
183
+ if ((delta.goneAnchors ?? []).some((anchor) => anchorIn(anchor, live)))
184
+ return false;
185
+ const before = projectAnchors(beforeNodes).filter((anchor) => isAlert(anchor.descriptor));
186
+ return live.every((anchor) => !isAlert(anchor.descriptor) ||
187
+ before.some((earlier) => anchorIn(anchor.descriptor, [earlier])) ||
188
+ appeared.some((recorded) => anchorIn(recorded, [anchor])));
92
189
  }
93
190
  /**
94
- * Whether every anchor is present in a fresh observation: some tier of the
95
- * anchor (`descriptorTiers` — so a test id that churned per render is
96
- * forgiven when the remaining fields still identify the node) equals some
97
- * candidate on every anchor field. Two matching nodes are the effect twice
98
- * over, not an ambiguity.
191
+ * Whether the replay itself produced some of the recorded delta, measured
192
+ * from `baselineNodes`, the first screen the replay saw on the route it
193
+ * ended on: an anchor that appeared was absent there, or a gone one was
194
+ * there. Without that, every check `deltaHolds` makes could have held before
195
+ * the actions that should have produced it: the screen already read the
196
+ * outcome, and nothing the replay did is proven to have done anything. A
197
+ * recording with no delta at all is never evidence. Undefined `baselineNodes`
198
+ * means the replay saw no screen on that route before its last action, so
199
+ * that action moved the route, which is the evidence.
200
+ *
201
+ * A control one of the trace's own actions targeted (`inputTargets`) that
202
+ * shows on both sides of the delta, a field typed into or a switch flipped,
203
+ * echoes the input: the replay typed, so the value is there, whatever the
204
+ * step was for. Such echoes are evidence only when the delta holds nothing
205
+ * else; otherwise the rest of the delta must have moved, so a subscribe
206
+ * form typed into on a page that already read "You're subscribed" proves
207
+ * nothing.
99
208
  */
100
- export function anchorsPresent(anchors, nodes, options) {
101
- const candidates = describeNodes(nodes, options).map((node) => node.descriptor);
102
- return anchors.every((anchor) => descriptorTiers(anchor).some((tier) => candidates.some((candidate) => fieldsEqual(tier, candidate, ANCHOR_FIELDS))));
209
+ export function deltaEvidenced(delta, baselineNodes, inputTargets) {
210
+ if (baselineNodes === undefined)
211
+ return true;
212
+ const appeared = delta.endAnchors ?? [];
213
+ const gone = delta.goneAnchors ?? [];
214
+ const targets = new Set(inputTargets.map(identityKey));
215
+ const changed = new Set(appeared.map(identityKey).filter((key) => gone.some((anchor) => identityKey(anchor) === key)));
216
+ const echoes = (anchor) => changed.has(identityKey(anchor)) && targets.has(identityKey(anchor));
217
+ const outcome = [...appeared, ...gone].some((anchor) => !echoes(anchor));
218
+ const counted = (anchor) => !outcome || !echoes(anchor);
219
+ const baseline = projectAnchors(baselineNodes);
220
+ return (appeared.some((anchor) => counted(anchor) && !anchorIn(anchor, baseline)) ||
221
+ gone.some((anchor) => counted(anchor) && anchorIn(anchor, baseline)));
222
+ }
223
+ /** A control's key without what a step sets on it, its value and states: the same control before and after. */
224
+ function identityKey(descriptor) {
225
+ const { value: _value, states: _states, ...control } = descriptor;
226
+ return anchorKey(control);
227
+ }
228
+ /**
229
+ * Whether some tier of a recorded anchor equals some projected node on every
230
+ * anchor field, and on its value and states exactly: an empty field is not
231
+ * the field filled in, nor an unchecked switch the switch turned on. An
232
+ * alert is compared by its shape (`alertShape`).
233
+ */
234
+ function anchorIn(anchor, nodes) {
235
+ const states = statesKey(anchor.states);
236
+ const shape = isAlert(anchor) ? alertShape : (descriptor) => descriptor;
237
+ return descriptorTiers(anchor).some((tier) => nodes.some((node) => node.descriptor.value === anchor.value &&
238
+ statesKey(node.descriptor.states) === states &&
239
+ fieldsEqual(shape(tier), shape(node.descriptor), ANCHOR_FIELDS)));
240
+ }
241
+ /**
242
+ * Anchor projections per observation: a replay checks the same end screen
243
+ * once per poll and the start screen for every check, while the projection
244
+ * of a node is a pure function of the node.
245
+ */
246
+ const projections = new WeakMap();
247
+ /** Every node of an observation that can serve as an anchor, in document order. */
248
+ function projectAnchors(nodes) {
249
+ const cached = projections.get(nodes);
250
+ if (cached !== undefined)
251
+ return cached;
252
+ const anchors = [];
253
+ for (const node of nodes.values()) {
254
+ const descriptor = anchorDescriptor(node);
255
+ if (descriptor === undefined)
256
+ continue;
257
+ // An alert keyed by its shape: one whose countdown ticked while the step
258
+ // ran stayed on screen, and is on neither side of the delta.
259
+ const key = anchorKey(isAlert(descriptor) ? alertShape(descriptor) : descriptor);
260
+ anchors.push({ descriptor, key, leaf: node.children === undefined || node.children.length === 0 });
261
+ }
262
+ projections.set(nodes, anchors);
263
+ return anchors;
103
264
  }
104
265
  /** One node's anchor projection, or undefined when it could identify nothing. */
105
- function anchorDescriptor(node, options) {
106
- const described = describeTarget(node, options.redact);
266
+ function anchorDescriptor(node) {
267
+ const described = describeTarget(node);
107
268
  if (described === undefined || descriptorTiers(described).length === 0)
108
269
  return undefined;
109
270
  const { selector: _selector, ...anchor } = described;
110
- return anchor;
271
+ const states = TRACE_ANCHOR_STATES.filter((state) => node.states?.[state] === true);
272
+ const value = node.value === undefined || node.states?.secure === true ? '' : collapseText(node.value);
273
+ return {
274
+ ...anchor,
275
+ ...(value === '' ? {} : { value: bound(value, MAX_TRACE_DESCRIPTOR_CHARS) }),
276
+ ...(states.length === 0 ? {} : { states }),
277
+ };
278
+ }
279
+ function statesKey(states) {
280
+ return states === undefined ? '' : states.join(',');
111
281
  }
112
282
  /**
113
283
  * Set key for one descriptor: its loosest identifying tier over the anchor
114
- * fields, in fixed order. Keying on the semantic tier whenever it identifies
115
- * the node means an app that mints test ids per render cannot make every
116
- * unchanged control look new after a re-render and crowd the real effect out
117
- * of the capped list; a node only a test id identifies keeps it.
284
+ * fields, in fixed order, and its states. Keying on the semantic tier
285
+ * whenever it identifies the node means an app that mints test ids per
286
+ * render cannot make every unchanged control look new after a re-render and
287
+ * crowd the real effect out of the capped list; a node only a test id
288
+ * identifies keeps it. The states make a control that changed state a node
289
+ * of the delta on both sides: the switch as it was, gone, and as it is,
290
+ * appeared; the value does the same for a field typed into.
118
291
  */
119
292
  function anchorKey(descriptor) {
120
293
  const tiers = descriptorTiers(descriptor);
121
294
  const loosest = tiers[tiers.length - 1] ?? descriptor;
122
- return JSON.stringify(ANCHOR_FIELDS.map((field) => loosest[field]));
295
+ return JSON.stringify([...ANCHOR_FIELDS.map((field) => loosest[field]), descriptor.value, statesKey(descriptor.states)]);
123
296
  }
@@ -8,9 +8,10 @@
8
8
  * re-records.
9
9
  */
10
10
  import type { ResolvedCacheConfig } from '../config/resolve.ts';
11
- import { type CacheTargetIdentity, type TraceCacheKind } from './identity.ts';
11
+ import { type CacheAgentIdentity, type CacheTargetIdentity, type TraceCacheKind } from './identity.ts';
12
+ import type { StoredRecordings } from './rekeyed.ts';
12
13
  import { type CacheStore } from './store.ts';
13
- import type { ActionTrace } from './trace.ts';
14
+ import type { ActionTrace, TraceProvenance } from './trace.ts';
14
15
  import type { JsonValue } from '../types.ts';
15
16
  /**
16
17
  * One cache entry held back until the attempt confirms or implicates it: a
@@ -18,7 +19,9 @@ import type { JsonValue } from '../types.ts';
18
19
  * stands. A kept entry carries no payload, so the replay's expansion of it
19
20
  * (every `unique()` slot filled with this run's value) can never be written;
20
21
  * confirmed, the file keeps its bytes and `createdAt`, and a committed cache
21
- * directory stays clean. Unconfirmed, both kinds are evicted alike.
22
+ * directory stays clean, unless its provenance predates the step's
23
+ * occurrence fields (`recordedFor`, as stored), which the replay proved and
24
+ * are then written in. Unconfirmed, both kinds are evicted alike.
22
25
  */
23
26
  export type StagedTrace = {
24
27
  readonly keyHash: string;
@@ -29,34 +32,40 @@ export type StagedTrace = {
29
32
  readonly trace: ActionTrace;
30
33
  } | {
31
34
  readonly kind: 'keep';
35
+ readonly recordedFor: TraceProvenance;
32
36
  });
37
+ /** One step's claimed key: its hash, and the step it names as an entry records it. */
38
+ export interface ClaimedKey {
39
+ readonly keyHash: string;
40
+ /** The step's identity before redaction; the recorder redacts it on the way to disk. */
41
+ readonly step: TraceProvenance;
42
+ }
33
43
  export interface AgentCacheContext {
34
44
  readonly mode: 'read-only' | 'read-write';
35
45
  readonly store: CacheStore;
36
- /** Test and target a write is recorded for, as `e2e cache ls` prints them. */
37
- readonly identity: {
38
- readonly testId: string;
39
- readonly targetId: string;
40
- };
41
46
  /** Whether this attempt may replay; writes are governed by `mode` alone. */
42
47
  readonly replayEligible: boolean;
43
48
  /**
44
49
  * A recording that no longer replays fails its step (`REPLAY_STALE`)
45
50
  * instead of handing it to the executor, with `advice` on how to re-record
46
- * it: the knobs to turn off and where the entry lives.
51
+ * it: the knobs to turn off and where the entry lives. `recordings`, set
52
+ * when the store can be listed, finds the recording of a step whose key
53
+ * changed under it, which is stale too rather than missing.
47
54
  */
48
55
  readonly strict: false | {
49
56
  readonly advice: string;
57
+ readonly recordings?: StoredRecordings;
50
58
  };
51
59
  /**
52
- * Claims one step's key hash. Not a pure derivation: each claim advances
53
- * the per-attempt occurrence index for its signature, which is what lets a
54
- * test repeat the same instruction and cache each occurrence separately.
60
+ * Claims one step's key. Not a pure derivation: each claim advances
61
+ * the per-attempt occurrence index for its agent and signature, which is
62
+ * what lets a test repeat the same instruction and cache each occurrence
63
+ * separately.
55
64
  * Exactly one claim per dispatched step, in execution order — the
56
65
  * `StepTraceSession` constructor is the sole caller and owns that
57
66
  * invariant structurally.
58
67
  */
59
- claimKeyHash(kind: TraceCacheKind, instruction: string, params: Readonly<Record<string, JsonValue>> | undefined): string;
68
+ claimKey(kind: TraceCacheKind, instruction: string, params: Readonly<Record<string, JsonValue>> | undefined, agent: CacheAgentIdentity): ClaimedKey;
60
69
  /**
61
70
  * Trace writes staged during the attempt. A trace is not trusted the moment
62
71
  * its own step passes — the verification step after it is what proves the
@@ -93,7 +102,7 @@ export interface AttemptSettlement {
93
102
  * for an interrupted attempt: interruption implicates nothing, so it writes
94
103
  * nothing and evicts nothing. An entry a step replayed whole is staged too,
95
104
  * so the same rule evicts it when nothing confirmed it; when something did,
96
- * it is left exactly as it was found.
105
+ * it is left exactly as it was found, but for provenance it lacked.
97
106
  */
98
107
  export declare function flushStagedTraces(context: AgentCacheContext, settlement: AttemptSettlement): Promise<void>;
99
108
  /**
@@ -110,4 +119,6 @@ export declare function createAgentCacheContext(options: {
110
119
  readonly testId: string;
111
120
  readonly target: CacheTargetIdentity;
112
121
  readonly attemptIndex: number;
122
+ /** The file store's listing under `cache.strict`, shared by the attempts of one worker and target (`storedRecordingsFor`). */
123
+ readonly recordings?: StoredRecordings | undefined;
113
124
  }): AgentCacheContext | undefined;