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,5 +1,6 @@
1
1
  /** What `--last-failed` reads back from the previous run's report. */
2
2
  import type { Report1Document } from '../report/build.ts';
3
+ type ReportCarried = NonNullable<Report1Document['run']['carried']>;
3
4
  /**
4
5
  * The previous run's report, read from where this run will write its own. It
5
6
  * is what `--last-failed` selects from, and reporters receive it as
@@ -9,13 +10,46 @@ import type { Report1Document } from '../report/build.ts';
9
10
  export declare function readLastRun(reportPath: string): Promise<Report1Document>;
10
11
  /**
11
12
  * The result ids of a report's tests that did not pass: failed, timed out,
12
- * interrupted, or skipped because a setup, a serial predecessor, a hook, or
13
- * the worker failed, and every selected test in the scope of a `beforeAll` or
14
- * `afterAll` that failed, until a run where that hook passes. A report older
15
- * than `selected` counts every result as selected. An explicit
16
- * skip and a filtered test passed in the sense that matters here: nothing to
17
- * run again. The ids are `resultId(testId, target, agent)`, so a test is
18
- * named per target and agent whichever of its `--repeat-each` runs did not
19
- * pass.
13
+ * interrupted, or skipped because a setup, a serial predecessor, a hook, the
14
+ * worker, or the failure limit stopped it, every selected test in the scope
15
+ * of a `beforeAll` or `afterAll` that failed, until a run where that hook
16
+ * passes, and every test the report carries from an earlier run that it did
17
+ * not run again. A report older than `selected` counts every result as
18
+ * selected. An explicit skip and a filtered test passed in the sense that
19
+ * matters here: nothing to run again. The ids are
20
+ * `resultId(testId, target, agent)`, so a test is named per target and agent
21
+ * whichever of its `--repeat-each` runs did not pass.
20
22
  */
21
23
  export declare function lastFailedIds(document: Report1Document): ReadonlySet<string>;
24
+ /** What a rerun's collection found: the tests it collected by id, and the files it discovered but could not collect. */
25
+ export interface RerunCollection {
26
+ readonly testIds: ReadonlySet<string>;
27
+ readonly uncollectedFiles: ReadonlySet<string>;
28
+ }
29
+ /**
30
+ * What a `--last-failed` rerun carries forward from the report it selected
31
+ * from: every result that report owed a rerun and this run did not run, as
32
+ * the run that last ran it reported it; the serial groups those results'
33
+ * attempts live on; and the suite hook failures whose scope still holds one
34
+ * of them. A result is owed when it did not pass, or when it is in the scope
35
+ * of a failed `beforeAll` or `afterAll` this run did not see pass: a hook
36
+ * whose scope this run ran again without it failing is resolved, and one
37
+ * that failed again is this run's own error, which still makes its left-out
38
+ * tests owed. A test this run did not run is one it collected (`collected`,
39
+ * by test id), on a target the config still has, that it did not select:
40
+ * left out by a filter, on a target or agent the run did not ask for, or a
41
+ * setup no selected test needed, which the report lists no row for. A test in
42
+ * a file the run found but could not collect (`uncollectedFiles`, a narrowed
43
+ * run past a file that fails to import) is still there and stays carried. A
44
+ * test the run no longer collects cannot run again and is dropped. A test
45
+ * this run selected is no longer carried, whatever its outcome: its own
46
+ * result says it now. Undefined when nothing is carried.
47
+ */
48
+ export declare function carryForward(lastRun: Report1Document, current: Report1Document, collected: RerunCollection): ReportCarried | undefined;
49
+ /**
50
+ * The artifact paths a report names, relative to the artifacts root: every
51
+ * attempt's of its results and serial groups, and of what it carries. What a
52
+ * `--last-failed` rerun keeps of the tree when it starts.
53
+ */
54
+ export declare function reportArtifactPaths(document: Report1Document): ReadonlySet<string>;
55
+ export {};
@@ -8,7 +8,16 @@ const UNDELIVERED_SKIPS = new Set([
8
8
  'serial-predecessor-failed',
9
9
  'hook-failed',
10
10
  'infrastructure-unavailable',
11
+ 'failure-limit',
11
12
  ]);
13
+ /** The id `--last-failed` names a result by: its test, target, and agent, whichever its repeat. */
14
+ function rerunId(result) {
15
+ return resultId(result.testId, result.targetId, result.agent);
16
+ }
17
+ /** Whether a run error is a failed `beforeAll` or `afterAll`, the failures `--last-failed` runs the scope of again. */
18
+ function isSuiteHookFailure(error) {
19
+ return error.phase === 'beforeAll' || error.phase === 'afterAll';
20
+ }
12
21
  /** Whether a result is one `--last-failed` runs again: it did not pass, or the run never got to it. */
13
22
  function didNotPass(result) {
14
23
  switch (result.status) {
@@ -72,19 +81,88 @@ export async function readLastRun(reportPath) {
72
81
  }
73
82
  /**
74
83
  * The result ids of a report's tests that did not pass: failed, timed out,
75
- * interrupted, or skipped because a setup, a serial predecessor, a hook, or
76
- * the worker failed, and every selected test in the scope of a `beforeAll` or
77
- * `afterAll` that failed, until a run where that hook passes. A report older
78
- * than `selected` counts every result as selected. An explicit
79
- * skip and a filtered test passed in the sense that matters here: nothing to
80
- * run again. The ids are `resultId(testId, target, agent)`, so a test is
81
- * named per target and agent whichever of its `--repeat-each` runs did not
82
- * pass.
84
+ * interrupted, or skipped because a setup, a serial predecessor, a hook, the
85
+ * worker, or the failure limit stopped it, every selected test in the scope
86
+ * of a `beforeAll` or `afterAll` that failed, until a run where that hook
87
+ * passes, and every test the report carries from an earlier run that it did
88
+ * not run again. A report older than `selected` counts every result as
89
+ * selected. An explicit skip and a filtered test passed in the sense that
90
+ * matters here: nothing to run again. The ids are
91
+ * `resultId(testId, target, agent)`, so a test is named per target and agent
92
+ * whichever of its `--repeat-each` runs did not pass.
83
93
  */
84
94
  export function lastFailedIds(document) {
85
- const hookFailures = document.run.errors.filter((error) => error.phase === 'beforeAll' || error.phase === 'afterAll');
95
+ const hookFailures = document.run.errors.filter(isSuiteHookFailure);
86
96
  const rerun = (result) => didNotPass(result) || (result.selected !== false && hookFailures.some((error) => inHookScope(result, error)));
87
- return new Set(document.run.results.filter(rerun).map((result) => resultId(result.testId, result.targetId, result.agent)));
97
+ const ids = new Set(document.run.results.filter(rerun).map(rerunId));
98
+ for (const result of document.run.carried?.results ?? [])
99
+ ids.add(rerunId(result));
100
+ return ids;
101
+ }
102
+ /** Whether two suite hook failures are the same hook's: one phase, one scope. */
103
+ function sameHook(a, b) {
104
+ if (a.phase !== b.phase)
105
+ return false;
106
+ if (a.scope === undefined || b.scope === undefined)
107
+ return a.scope === b.scope;
108
+ return (a.scope.file === b.scope.file &&
109
+ a.scope.targetId === b.scope.targetId &&
110
+ a.scope.titlePath.length === b.scope.titlePath.length &&
111
+ a.scope.titlePath.every((title, index) => b.scope.titlePath[index] === title));
112
+ }
113
+ /**
114
+ * What a `--last-failed` rerun carries forward from the report it selected
115
+ * from: every result that report owed a rerun and this run did not run, as
116
+ * the run that last ran it reported it; the serial groups those results'
117
+ * attempts live on; and the suite hook failures whose scope still holds one
118
+ * of them. A result is owed when it did not pass, or when it is in the scope
119
+ * of a failed `beforeAll` or `afterAll` this run did not see pass: a hook
120
+ * whose scope this run ran again without it failing is resolved, and one
121
+ * that failed again is this run's own error, which still makes its left-out
122
+ * tests owed. A test this run did not run is one it collected (`collected`,
123
+ * by test id), on a target the config still has, that it did not select:
124
+ * left out by a filter, on a target or agent the run did not ask for, or a
125
+ * setup no selected test needed, which the report lists no row for. A test in
126
+ * a file the run found but could not collect (`uncollectedFiles`, a narrowed
127
+ * run past a file that fails to import) is still there and stays carried. A
128
+ * test the run no longer collects cannot run again and is dropped. A test
129
+ * this run selected is no longer carried, whatever its outcome: its own
130
+ * result says it now. Undefined when nothing is carried.
131
+ */
132
+ export function carryForward(lastRun, current, collected) {
133
+ const selected = new Set(current.run.results.filter((result) => result.selected !== false).map(rerunId));
134
+ const targets = new Set(current.run.targets.map((target) => target.id));
135
+ const stillThere = (result) => collected.testIds.has(result.testId) || collected.uncollectedFiles.has(result.file);
136
+ const notRun = (result) => !selected.has(rerunId(result)) && stillThere(result) && targets.has(result.targetId);
137
+ const failedAgain = current.run.errors.filter(isSuiteHookFailure);
138
+ const hooks = [...lastRun.run.errors.filter(isSuiteHookFailure), ...(lastRun.run.carried?.errors ?? [])];
139
+ const reportedAgain = (error) => failedAgain.some((again) => sameHook(again, error));
140
+ const passedAgain = (error) => !reportedAgain(error) &&
141
+ current.run.results.some((result) => result.selected !== false && result.status !== 'skipped' && inHookScope(result, error));
142
+ const unresolved = hooks.filter((error) => !passedAgain(error));
143
+ // A test the run before left out is known by the row it carried, not by its filtered one.
144
+ const history = [...lastRun.run.results.filter((result) => result.selected !== false), ...(lastRun.run.carried?.results ?? [])];
145
+ const results = history.filter((result) => notRun(result) && (didNotPass(result) || unresolved.some((error) => inHookScope(result, error))));
146
+ if (results.length === 0)
147
+ return undefined;
148
+ const groupIds = new Set(results.flatMap((result) => (result.serialGroupId === undefined ? [] : [result.serialGroupId])));
149
+ const currentGroupIds = new Set(current.run.serialGroups.map((group) => group.id));
150
+ const serialGroups = [...lastRun.run.serialGroups, ...(lastRun.run.carried?.serialGroups ?? [])].filter((group) => groupIds.has(group.id) && !currentGroupIds.has(group.id));
151
+ const errors = unresolved.filter((error) => !reportedAgain(error) && results.some((result) => inHookScope(result, error)));
152
+ return { results, serialGroups, errors };
153
+ }
154
+ /**
155
+ * The artifact paths a report names, relative to the artifacts root: every
156
+ * attempt's of its results and serial groups, and of what it carries. What a
157
+ * `--last-failed` rerun keeps of the tree when it starts.
158
+ */
159
+ export function reportArtifactPaths(document) {
160
+ const parts = [document.run, ...(document.run.carried === undefined ? [] : [document.run.carried])];
161
+ const attempts = parts.flatMap((part) => [
162
+ ...part.results.flatMap((result) => result.attempts),
163
+ ...part.serialGroups.flatMap((group) => group.attempts),
164
+ ]);
165
+ return new Set(attempts.flatMap((attempt) => attempt.artifacts.flatMap((artifact) => (artifact.path === undefined ? [] : [artifact.path]))));
88
166
  }
89
167
  /**
90
168
  * The shape the read relies on: a report-1 envelope whose every result has
@@ -96,10 +174,38 @@ function isReport(value) {
96
174
  return false;
97
175
  const document = value;
98
176
  return (document.schemaVersion === 'report-1' &&
99
- Array.isArray(document.run?.results) &&
100
- document.run.results.every(isResult) &&
101
- Array.isArray(document.run.errors) &&
102
- document.run.errors.every(isError));
177
+ isRunPart(document.run) &&
178
+ (document.run.carried === undefined || isRunPart(document.run.carried)));
179
+ }
180
+ /** A run, or what one carries: results and errors of the shape read here, and its serial groups. */
181
+ function isRunPart(value) {
182
+ if (typeof value !== 'object' || value === null)
183
+ return false;
184
+ const part = value;
185
+ return (Array.isArray(part.results) &&
186
+ part.results.every(isResult) &&
187
+ Array.isArray(part.errors) &&
188
+ part.errors.every(isError) &&
189
+ Array.isArray(part.serialGroups) &&
190
+ part.serialGroups.every((group) => typeof group === 'object' && group !== null && hasAttempts(group)));
191
+ }
192
+ /** Whether a result or serial group lists its attempts, each with its artifacts, the way a rerun reads them. */
193
+ function hasAttempts(value) {
194
+ const { attempts } = value;
195
+ return Array.isArray(attempts) && attempts.every(hasArtifacts);
196
+ }
197
+ /** Whether an attempt lists its artifacts as records whose path, when there is one, is a string. */
198
+ function hasArtifacts(attempt) {
199
+ if (typeof attempt !== 'object' || attempt === null)
200
+ return false;
201
+ const { artifacts } = attempt;
202
+ return (Array.isArray(artifacts) &&
203
+ artifacts.every((artifact) => {
204
+ if (typeof artifact !== 'object' || artifact === null)
205
+ return false;
206
+ const { path } = artifact;
207
+ return path === undefined || typeof path === 'string';
208
+ }));
103
209
  }
104
210
  function isResult(value) {
105
211
  if (typeof value !== 'object' || value === null)
@@ -110,6 +216,7 @@ function isResult(value) {
110
216
  typeof result.status === 'string' &&
111
217
  typeof result.file === 'string' &&
112
218
  Array.isArray(result.titlePath) &&
219
+ hasAttempts(result) &&
113
220
  (skip === undefined || (typeof skip === 'object' && skip !== null && typeof skip.cause === 'string')));
114
221
  }
115
222
  function isError(value) {
@@ -0,0 +1,73 @@
1
+ /**
2
+ * What user code in an `e2e run` process prints outside a test body: the
3
+ * config's top-level code, a test file's top level while it is collected,
4
+ * and reporters in the runner; the config's top-level code again in each
5
+ * worker. It is redacted with every secret value the process knows, as a
6
+ * test's output is. Until a config has loaded its secrets are unknown, so
7
+ * what is printed meanwhile is held, and withheld if the load fails.
8
+ */
9
+ import { type SecretLedger } from '../internal/redact.ts';
10
+ import type { ListReporterOutput } from '../report/list.ts';
11
+ export type OutputStream = 'stdout' | 'stderr';
12
+ /** Writes text to one stream of the terminal; the callback fires once the stream took it. Answers false while the stream is backed up, like `Writable.write`. */
13
+ export type TerminalWrite = (text: string, callback?: () => void) => boolean;
14
+ /** Where runner output goes instead of the terminal while the list reporter shows the run. */
15
+ export interface OutputView {
16
+ /** Prints redacted text from `stream`. */
17
+ show(stream: OutputStream, text: string): void;
18
+ /** Prints what `show` left unfinished; nothing more follows. */
19
+ end(): void;
20
+ }
21
+ /**
22
+ * Holds what is written while a config loads. Released once the load
23
+ * registered the config's secrets; dropped for a warning when it failed.
24
+ */
25
+ export declare class ConfigLoadHold {
26
+ private held;
27
+ /** Holds `chunk` if a load is in flight; answers whether it did. */
28
+ hold(stream: OutputStream, chunk: string | Uint8Array): boolean;
29
+ /**
30
+ * Runs `load`, which loads a config and registers its secrets, holding
31
+ * what is written meanwhile. Then hands each held write to `release` in
32
+ * order, or, if `load` threw, `warn`s with how much was withheld.
33
+ */
34
+ during<T>(load: () => Promise<T>, release: (stream: OutputStream, chunk: string | Uint8Array) => void, warn: (line: string) => void): Promise<T>;
35
+ }
36
+ /**
37
+ * The runner process's user output, on its way to the terminal or, while
38
+ * the list reporter shows it, through the reporter, which prints it above
39
+ * its live window instead of letting it land inside.
40
+ */
41
+ export declare class RunnerOutput {
42
+ private readonly terminal;
43
+ /** Each stream redacted on its own, across writes: a value split over two writes is still caught. */
44
+ private readonly redactors;
45
+ private readonly loadHold;
46
+ private view;
47
+ /** The terminal's stdout for the list reporter: its own lines and live window go straight there. */
48
+ readonly listOutput: ListReporterOutput;
49
+ constructor(terminal: Readonly<Record<OutputStream, TerminalWrite>>, ledger: SecretLedger);
50
+ /**
51
+ * One write of user code to `stream`, redacted: the tail a later write
52
+ * could complete into a value waits for it, or for `end`. Held while a
53
+ * config loads. Answers what the terminal did.
54
+ */
55
+ write(stream: OutputStream, chunk: string | Uint8Array, callback?: () => void): boolean;
56
+ /**
57
+ * Runs `load`, a config load that registers the config's secrets, holding
58
+ * what is printed until it ends: the config's top-level code may print a
59
+ * value before the process knows it is one. Released redacted once it
60
+ * loaded; withheld with a warning when it failed.
61
+ */
62
+ withholdDuring<T>(load: () => Promise<T>): Promise<T>;
63
+ /**
64
+ * Passes what is printed to `view` from now on, or to the terminal again
65
+ * when undefined. What the previous view was given is released to it
66
+ * first, unfinished lines included, and that view ended.
67
+ */
68
+ showThrough(view: OutputView | undefined): void;
69
+ /** Releases what is still held, redacted, to the terminal. Later writes stay redacted; only a tail a later write could complete into a value is held. */
70
+ end(): void;
71
+ private release;
72
+ private emit;
73
+ }
@@ -0,0 +1,130 @@
1
+ /**
2
+ * What user code in an `e2e run` process prints outside a test body: the
3
+ * config's top-level code, a test file's top level while it is collected,
4
+ * and reporters in the runner; the config's top-level code again in each
5
+ * worker. It is redacted with every secret value the process knows, as a
6
+ * test's output is. Until a config has loaded its secrets are unknown, so
7
+ * what is printed meanwhile is held, and withheld if the load fails.
8
+ */
9
+ import { StreamRedactor } from '../internal/redact.js';
10
+ /** The warning that stands in for what a config printed before it failed to load. */
11
+ function withheldWarning(bytes) {
12
+ return `withheld ${bytes} bytes of output printed while the config failed to load: its secrets are unknown, so the output cannot be redacted`;
13
+ }
14
+ /**
15
+ * Holds what is written while a config loads. Released once the load
16
+ * registered the config's secrets; dropped for a warning when it failed.
17
+ */
18
+ export class ConfigLoadHold {
19
+ held;
20
+ /** Holds `chunk` if a load is in flight; answers whether it did. */
21
+ hold(stream, chunk) {
22
+ if (this.held === undefined)
23
+ return false;
24
+ // A copy: the writer may reuse its buffer once the write returns.
25
+ this.held.push({ stream, chunk: typeof chunk === 'string' ? chunk : Buffer.from(chunk) });
26
+ return true;
27
+ }
28
+ /**
29
+ * Runs `load`, which loads a config and registers its secrets, holding
30
+ * what is written meanwhile. Then hands each held write to `release` in
31
+ * order, or, if `load` threw, `warn`s with how much was withheld.
32
+ */
33
+ async during(load, release, warn) {
34
+ this.held = [];
35
+ let loaded = false;
36
+ try {
37
+ const value = await load();
38
+ loaded = true;
39
+ return value;
40
+ }
41
+ finally {
42
+ const held = this.held;
43
+ this.held = undefined;
44
+ if (loaded) {
45
+ for (const { stream, chunk } of held)
46
+ release(stream, chunk);
47
+ }
48
+ else {
49
+ const bytes = held.reduce((total, { chunk }) => total + Buffer.byteLength(chunk), 0);
50
+ if (bytes > 0)
51
+ warn(withheldWarning(bytes));
52
+ }
53
+ }
54
+ }
55
+ }
56
+ /**
57
+ * The runner process's user output, on its way to the terminal or, while
58
+ * the list reporter shows it, through the reporter, which prints it above
59
+ * its live window instead of letting it land inside.
60
+ */
61
+ export class RunnerOutput {
62
+ terminal;
63
+ /** Each stream redacted on its own, across writes: a value split over two writes is still caught. */
64
+ redactors;
65
+ loadHold = new ConfigLoadHold();
66
+ view;
67
+ /** The terminal's stdout for the list reporter: its own lines and live window go straight there. */
68
+ listOutput;
69
+ constructor(terminal, ledger) {
70
+ this.terminal = terminal;
71
+ this.redactors = { stdout: new StreamRedactor(ledger), stderr: new StreamRedactor(ledger) };
72
+ this.listOutput = {
73
+ write: (line) => void terminal.stdout(`${line}\n`),
74
+ raw: (text) => void terminal.stdout(text),
75
+ };
76
+ }
77
+ /**
78
+ * One write of user code to `stream`, redacted: the tail a later write
79
+ * could complete into a value waits for it, or for `end`. Held while a
80
+ * config loads. Answers what the terminal did.
81
+ */
82
+ write(stream, chunk, callback) {
83
+ if (this.loadHold.hold(stream, chunk)) {
84
+ if (callback !== undefined)
85
+ process.nextTick(callback);
86
+ return true;
87
+ }
88
+ return this.emit(stream, this.redactors[stream].push(chunk), callback);
89
+ }
90
+ /**
91
+ * Runs `load`, a config load that registers the config's secrets, holding
92
+ * what is printed until it ends: the config's top-level code may print a
93
+ * value before the process knows it is one. Released redacted once it
94
+ * loaded; withheld with a warning when it failed.
95
+ */
96
+ withholdDuring(load) {
97
+ return this.loadHold.during(load, (stream, chunk) => this.write(stream, chunk), (line) => this.emit('stderr', `e2e: [warning] ${line}\n`));
98
+ }
99
+ /**
100
+ * Passes what is printed to `view` from now on, or to the terminal again
101
+ * when undefined. What the previous view was given is released to it
102
+ * first, unfinished lines included, and that view ended.
103
+ */
104
+ showThrough(view) {
105
+ if (this.view !== undefined) {
106
+ this.release();
107
+ this.view.end();
108
+ }
109
+ this.view = view;
110
+ }
111
+ /** Releases what is still held, redacted, to the terminal. Later writes stay redacted; only a tail a later write could complete into a value is held. */
112
+ end() {
113
+ this.showThrough(undefined);
114
+ this.release();
115
+ }
116
+ release() {
117
+ this.emit('stdout', this.redactors.stdout.flush());
118
+ this.emit('stderr', this.redactors.stderr.flush());
119
+ }
120
+ emit(stream, text, callback) {
121
+ if (text === '' || this.view !== undefined) {
122
+ if (text !== '')
123
+ this.view?.show(stream, text);
124
+ if (callback !== undefined)
125
+ process.nextTick(callback);
126
+ return true;
127
+ }
128
+ return this.terminal[stream](text, callback);
129
+ }
130
+ }
@@ -55,6 +55,8 @@ export interface PrepareScope {
55
55
  /** The same `env` the workers are started with: what prepare provisions must be where a launch will look for it. */
56
56
  readonly env: NodeJS.ProcessEnv;
57
57
  readonly signal: AbortSignal;
58
+ /** Whether the run asked for a visible surface, so a hook provisions for the browser it will launch. */
59
+ readonly headed: boolean;
58
60
  /** Where a hook's progress lines go, under the target they concern. */
59
61
  readonly notice: (targetName: string, line: string) => void;
60
62
  }
@@ -129,6 +129,7 @@ export class PreparedEngines {
129
129
  runId: scope.runId,
130
130
  targetName: target.name,
131
131
  projectRoot: scope.projectRoot,
132
+ headed: scope.headed,
132
133
  app: engineAppInfo(target.app),
133
134
  slots,
134
135
  env: scope.env,
@@ -33,7 +33,10 @@ export declare function findRegistered(realm: Realm, test: CollectedTest): Regis
33
33
  /**
34
34
  * Runs one hook, or a fixture teardown, within its budget. One that overruns
35
35
  * fails with the runner's timeout error naming `label`; `onTimeout` lets the
36
- * caller cancel what it started.
36
+ * caller cancel what it started. The hook owns the `expect.poll` calls it
37
+ * starts: one still running when it returns fails it with
38
+ * `STEP_NOT_AWAITED` and is cancelled, and one left by a hook that failed is
39
+ * cancelled beside that failure.
37
40
  */
38
41
  export declare function runHook(label: string, run: () => void | Promise<void>, timeoutMs: number, onTimeout?: () => void): Promise<void>;
39
42
  export interface RealmManagerOptions {
package/dist/run/realm.js CHANGED
@@ -1,5 +1,6 @@
1
1
  /** Suite realm lifecycle: module re-import, beforeAll/afterAll scope tracking. */
2
2
  import { importModule } from '../config/load.js';
3
+ import { PollScope, runInPollScope } from '../expect/poll-scope.js';
3
4
  import { classifyError, E2EError, serializeError, TestTimeoutError, } from '../internal/errors.js';
4
5
  import { DebugTrace } from '../internal/debug.js';
5
6
  import { titlePathKey } from '../internal/ids.js';
@@ -12,13 +13,26 @@ export function findRegistered(realm, test) {
12
13
  /**
13
14
  * Runs one hook, or a fixture teardown, within its budget. One that overruns
14
15
  * fails with the runner's timeout error naming `label`; `onTimeout` lets the
15
- * caller cancel what it started.
16
+ * caller cancel what it started. The hook owns the `expect.poll` calls it
17
+ * starts: one still running when it returns fails it with
18
+ * `STEP_NOT_AWAITED` and is cancelled, and one left by a hook that failed is
19
+ * cancelled beside that failure.
16
20
  */
17
- export function runHook(label, run, timeoutMs, onTimeout) {
18
- return withTimeout(Promise.resolve().then(run), timeoutMs, () => {
19
- onTimeout?.();
20
- return new TestTimeoutError(`${label} timed out`);
21
- });
21
+ export async function runHook(label, run, timeoutMs, onTimeout) {
22
+ const polls = new PollScope(`the ${label}`);
23
+ try {
24
+ await withTimeout(runInPollScope(polls, () => Promise.resolve().then(run)), timeoutMs, () => {
25
+ onTimeout?.();
26
+ return new TestTimeoutError(`${label} timed out`);
27
+ });
28
+ }
29
+ catch (cause) {
30
+ polls.close();
31
+ throw cause;
32
+ }
33
+ const notAwaited = polls.close();
34
+ if (notAwaited !== undefined)
35
+ throw notAwaited;
22
36
  }
23
37
  /** Creates realms and runs suite-scope hooks with per-realm entry tracking. */
24
38
  export class RealmManager {
@@ -30,13 +30,13 @@ export interface ArtifactRecord {
30
30
  /**
31
31
  * Mirrors report-1: how much of the file the runner masked. A screenshot is
32
32
  * `complete`; a trace is `complete` once every registered secret value was
33
- * rewritten out of its text, and `not-required` when no secret reached its
34
- * session (filled, or held by the engine); a video is `incomplete`, since a recording masks nothing
33
+ * rewritten out of its text, and `not-required` when the run has no
34
+ * secret value to rewrite; a video is `incomplete`, since a recording masks nothing
35
35
  * (a secure field renders its own dots, but anything else the screen
36
36
  * showed is in the frames), and is kept as it is; a download is
37
37
  * `incomplete` too, bytes the app served and the runner did not rewrite,
38
- * unless a secret reached the session and the file is text the
39
- * ledger was run over, which makes it `complete`. report-1 also admits an
38
+ * unless the file is text the session's ledger was run over, which makes
39
+ * it `complete`. report-1 also admits an
40
40
  * `incomplete` artifact without a `path`: a video a hosted service keeps,
41
41
  * recorded by `url`, or one its producer withheld, which this runner never
42
42
  * writes.
@@ -148,6 +148,22 @@ export interface ResultRecord {
148
148
  * every other field is plain data.
149
149
  */
150
150
  export type WireResultRecord = Omit<ResultRecord, 'target'>;
151
+ /** The failure a runtime skip followed, with the evidence its attempt captured. */
152
+ export interface FailureBeforeSkip {
153
+ readonly error: SerializedError;
154
+ readonly failure: FailureEvidence | undefined;
155
+ /** The failing attempt's artifacts; for a serial member, the group attempt's. */
156
+ readonly artifacts: readonly ArtifactRecord[];
157
+ }
158
+ /**
159
+ * The failure a test's `test.skip(...)` followed: the last attempt that
160
+ * failed before a skipped retry, or a soft failure the skipping attempt kept.
161
+ * Engine cleanup diagnostics after the skip are not one. A serial member's
162
+ * attempts live on its group, which `groupOf` looks up only for such a member.
163
+ */
164
+ export declare function failureBeforeSkip(result: Pick<ResultRecord, 'status' | 'skip' | 'attempts' | 'serialGroupId' | 'test'>, groupOf: (id: string) => SerialGroupRecord | undefined): FailureBeforeSkip | undefined;
165
+ /** Whether any skipped result followed a failure: what `failOnSkippedFailure` fails a run for. */
166
+ export declare function someSkippedAfterFailure(results: readonly ResultRecord[], serialGroups: readonly SerialGroupRecord[]): boolean;
151
167
  /** Strips the live target from a result for transport. */
152
168
  export declare function encodeResult(record: ResultRecord): WireResultRecord;
153
169
  export interface RunError {
@@ -7,6 +7,32 @@
7
7
  export function isFailedStatus(status) {
8
8
  return status !== 'passed' && status !== 'skipped' && status !== 'flaky';
9
9
  }
10
+ /**
11
+ * The failure a test's `test.skip(...)` followed: the last attempt that
12
+ * failed before a skipped retry, or a soft failure the skipping attempt kept.
13
+ * Engine cleanup diagnostics after the skip are not one. A serial member's
14
+ * attempts live on its group, which `groupOf` looks up only for such a member.
15
+ */
16
+ export function failureBeforeSkip(result, groupOf) {
17
+ if (result.status !== 'skipped' || result.skip?.cause !== 'explicit')
18
+ return undefined;
19
+ const runs = result.serialGroupId === undefined
20
+ ? result.attempts
21
+ : (groupOf(result.serialGroupId)?.attempts ?? []).flatMap((attempt) => attempt.members
22
+ .filter((member) => member.testId === result.test.id)
23
+ .map((member) => ({ ...member, artifacts: attempt.artifacts })));
24
+ for (const run of runs.toReversed()) {
25
+ const error = run.error ?? run.secondaryErrors.find((secondary) => secondary.phase !== 'cleanup');
26
+ if (error !== undefined)
27
+ return { error, failure: run.failure, artifacts: run.artifacts };
28
+ }
29
+ return undefined;
30
+ }
31
+ /** Whether any skipped result followed a failure: what `failOnSkippedFailure` fails a run for. */
32
+ export function someSkippedAfterFailure(results, serialGroups) {
33
+ const groups = new Map(serialGroups.map((group) => [group.id, group]));
34
+ return results.some((result) => failureBeforeSkip(result, (id) => groups.get(id)) !== undefined);
35
+ }
10
36
  /** Strips the live target from a result for transport. */
11
37
  export function encodeResult(record) {
12
38
  const { target: _target, ...rest } = record;
@@ -12,6 +12,15 @@ export declare function isRetryEligible(attempt: RetryAttempt): boolean;
12
12
  * Runs up to `maxAttempts` attempts, stopping on pass, interrupt, or a
13
13
  * failure that does not consume retry budget. `runOnce` returns the
14
14
  * finished attempt, or undefined when the attempt could not start (the
15
- * loop stops and the last observed status stands).
15
+ * loop stops and the last observed status stands). An interrupt during a
16
+ * retry keeps the verdict the failed attempt before it reached: the test
17
+ * did fail, and only its second chance was cut short.
16
18
  */
17
19
  export declare function runWithRetries(maxAttempts: number, interruptSignal: AbortSignal, runOnce: (attemptIndex: number) => Promise<RetryAttempt | undefined>): Promise<ResultStatus>;
20
+ /**
21
+ * The verdict a sequence of finished attempts reaches: a pass is `flaky`
22
+ * after a failure, an interrupt during a retry keeps the verdict before it,
23
+ * and a self-skip decides. Also the verdict of attempts a worker finished
24
+ * before the run's interrupt stopped it, so both read alike.
25
+ */
26
+ export declare function retryVerdict(attempts: readonly RetryAttempt[]): ResultStatus;