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
package/dist/run/retry.js CHANGED
@@ -8,28 +8,46 @@ export function isRetryEligible(attempt) {
8
8
  * Runs up to `maxAttempts` attempts, stopping on pass, interrupt, or a
9
9
  * failure that does not consume retry budget. `runOnce` returns the
10
10
  * finished attempt, or undefined when the attempt could not start (the
11
- * loop stops and the last observed status stands).
11
+ * loop stops and the last observed status stands). An interrupt during a
12
+ * retry keeps the verdict the failed attempt before it reached: the test
13
+ * did fail, and only its second chance was cut short.
12
14
  */
13
15
  export async function runWithRetries(maxAttempts, interruptSignal, runOnce) {
14
- let finalStatus = 'failed';
15
- let attemptCount = 0;
16
+ const attempts = [];
16
17
  for (let attemptIndex = 0; attemptIndex < maxAttempts; attemptIndex += 1) {
17
18
  if (interruptSignal.aborted)
18
19
  break;
19
20
  const attempt = await runOnce(attemptIndex);
20
21
  if (attempt === undefined)
21
22
  break;
22
- attemptCount += 1;
23
- if (attempt.status === 'passed')
24
- return attemptCount > 1 ? 'flaky' : 'passed';
25
- if (attempt.status === 'interrupted')
26
- return 'interrupted';
23
+ attempts.push(attempt);
27
24
  // A body that skipped itself has decided; a retry would only ask again.
28
- if (attempt.status === 'skipped')
29
- return 'skipped';
30
- finalStatus = attempt.status;
25
+ if (attempt.status === 'passed' || attempt.status === 'interrupted' || attempt.status === 'skipped')
26
+ break;
31
27
  if (!isRetryEligible(attempt))
32
28
  break;
33
29
  }
34
- return finalStatus;
30
+ return retryVerdict(attempts);
31
+ }
32
+ /**
33
+ * The verdict a sequence of finished attempts reaches: a pass is `flaky`
34
+ * after a failure, an interrupt during a retry keeps the verdict before it,
35
+ * and a self-skip decides. Also the verdict of attempts a worker finished
36
+ * before the run's interrupt stopped it, so both read alike.
37
+ */
38
+ export function retryVerdict(attempts) {
39
+ let verdict = 'failed';
40
+ for (const [index, attempt] of attempts.entries()) {
41
+ switch (attempt.status) {
42
+ case 'passed':
43
+ return index > 0 ? 'flaky' : 'passed';
44
+ case 'interrupted':
45
+ return index > 0 ? verdict : 'interrupted';
46
+ case 'skipped':
47
+ return 'skipped';
48
+ default:
49
+ verdict = attempt.status;
50
+ }
51
+ }
52
+ return verdict;
35
53
  }
@@ -5,7 +5,8 @@ import { type Shard, type TagMode } from '../collect/select.ts';
5
5
  import type { ExploreProgress } from '../explore/progress.ts';
6
6
  import { type Report1Document, type ReportExplore } from '../report/build.ts';
7
7
  import { type RunEventSink, type RunExitCode, type RunStatus } from './events.ts';
8
- import type { ResultRecord } from './records.ts';
8
+ import { type ResultRecord } from './records.ts';
9
+ import type { RunnerOutput } from './process-output.ts';
9
10
  import type { BuiltinReporter, E2EConfig, RecordingMode } from '../types.ts';
10
11
  export interface RunOptions {
11
12
  cwd?: string | undefined;
@@ -109,6 +110,13 @@ export interface RunOptions {
109
110
  onEvent?: RunEventSink | undefined;
110
111
  /** Budget for each reporter's `onRunFinished`, in ms. Only the test harness sets it; there is no flag. */
111
112
  reporterTimeout?: number | undefined;
113
+ /**
114
+ * The process's stdout and stderr, when the caller claimed them (the CLI):
115
+ * what user code in this process prints is held while the config loads
116
+ * and shown through the list reporter while the run reports, and the list
117
+ * reporter writes to the terminal past it.
118
+ */
119
+ processOutput?: RunnerOutput | undefined;
112
120
  }
113
121
  /**
114
122
  * A registration supplied in memory, under the virtual file name the report
@@ -18,11 +18,14 @@ import { writeJsonReport } from '../report/write.js';
18
18
  import { createRunEventEmitter, toEventResult } from './events.js';
19
19
  import { allocateAppPorts } from './app-ports.js';
20
20
  import { inProcessSpawner } from './in-process.js';
21
+ import { someSkippedAfterFailure } from './records.js';
21
22
  import { runUnits } from './scheduler.js';
22
23
  import { buildWorkPlans, plannedSlots } from './units.js';
23
24
  import { SessionStore } from './sessions.js';
24
25
  import { outputLayout } from './output.js';
25
- import { lastFailedIds, readLastRun } from './last-run.js';
26
+ import { registerStaticSecrets } from './secrecy.js';
27
+ import { claimRerunDir, pruneArtifacts } from './artifacts.js';
28
+ import { carryForward, lastFailedIds, readLastRun, reportArtifactPaths } from './last-run.js';
26
29
  import { childProcessSpawner } from './worker/handle.js';
27
30
  import { setSecretRegistry } from '../secrets.js';
28
31
  import { withAbort } from '../internal/time.js';
@@ -129,14 +132,27 @@ export async function run(options = {}) {
129
132
  //
130
133
  // Free ports for URLs declared with port 0 are chosen here, once: workers
131
134
  // re-resolve the config and get the assignments in their bootstrap.
135
+ //
136
+ // The config's secrets are known to the process before what its top-level
137
+ // code printed is released, so that output is redacted like any other.
138
+ const loadConfig = async () => {
139
+ const config = await loadRunConfig(options, cwd, env, cli);
140
+ registerStaticSecrets(config.allSecrets);
141
+ return allocateAppPorts(config);
142
+ };
143
+ const processOutput = options.processOutput;
132
144
  const loaded = await debug
133
- .time('config.load', () => loadRunConfig(options, cwd, env, cli).then(allocateAppPorts))
145
+ .time('config.load', () => (processOutput === undefined ? loadConfig() : processOutput.withholdDuring(loadConfig)))
134
146
  .then((config) => ({ config, error: undefined }), (cause) => ({ config: undefined, error: classifyError(cause) }));
135
147
  // Every reporter, built-in or configured, is one `Reporter` on one spine:
136
148
  // the ids name the shipped ones, and a config failure still leaves the ids
137
149
  // the CLI asked for, so the failure renders through them.
138
150
  const reporterIds = loaded.config?.reporters ?? options.reporters ?? ['list'];
139
- const listReporter = options.quiet === true || !reporterIds.includes('list') ? undefined : new ListReporter();
151
+ const listReporter = options.quiet === true || !reporterIds.includes('list') ? undefined : new ListReporter(processOutput?.listOutput);
152
+ // Until the run is over, what user code prints lands above the live window.
153
+ processOutput?.showThrough(listReporter === undefined
154
+ ? undefined
155
+ : { show: (stream, text) => listReporter.processOutput(stream, text), end: () => listReporter.endProcessOutput() });
140
156
  const activeReporters = [
141
157
  ...(listReporter === undefined ? [] : [listReporter]),
142
158
  ...reporterIds.filter((id) => id !== 'list').map((id) => STATELESS_REPORTERS[id]),
@@ -180,32 +196,45 @@ export async function run(options = {}) {
180
196
  emit({ type: 'run-stopped', failures, limit });
181
197
  };
182
198
  /**
183
- * The exit code is a fold over run state — every result, every run error,
199
+ * The exit code is a fold over run state — every result and serial group, every run error,
184
200
  * the interrupt — never threaded through by hand. A run error recorded
185
201
  * anywhere, including during teardown or the report write, reaches the exit
186
202
  * code the same way.
187
203
  */
188
204
  const currentExitCode = () => combineExitCodes([
189
- ...resultExitCodes(results),
205
+ ...verdictExitCodes(results, serialGroups),
206
+ ...(loaded.config?.failOnSkippedFailure === true && someSkippedAfterFailure(results, serialGroups) ? [1] : []),
190
207
  ...runErrors.map((runError) => exitCodeForCategory(runError.error.category)),
191
208
  ...(interruptController.signal.aborted ? [130] : []),
192
209
  ].filter((code) => code !== 130 || !(runAborted || stoppedEarly)));
193
210
  // Detected once the config names the project root; a report written before
194
211
  // that (a config failure) has no checkout to describe.
195
212
  let vcs;
196
- const buildRunReport = (exitCode) => buildReport({
197
- runId,
198
- config: loaded.config,
199
- vcs,
200
- startedAt,
201
- status: statusOf(exitCode),
202
- exitCode,
203
- results,
204
- serialGroups,
205
- runErrors,
206
- targetProvenance,
207
- explore: options.tests?.explore?.snapshot(),
208
- });
213
+ // The report `--last-failed` selected from, once collection has read it;
214
+ // reporters get it beside this run's report to fold the rerun into it, and
215
+ // this run's report carries what it owed that this run left out.
216
+ let lastRun;
217
+ // The ids of every test collection found, a setup no selected test needed
218
+ // among them, and the files it could not collect: what a rerun can still
219
+ // carry when the report lists no row.
220
+ let rerunCollection = { testIds: new Set(), uncollectedFiles: new Set() };
221
+ const buildRunReport = (exitCode) => {
222
+ const document = buildReport({
223
+ runId,
224
+ config: loaded.config,
225
+ vcs,
226
+ startedAt,
227
+ status: statusOf(exitCode),
228
+ exitCode,
229
+ results,
230
+ serialGroups,
231
+ runErrors,
232
+ targetProvenance,
233
+ explore: options.tests?.explore?.snapshot(),
234
+ });
235
+ const carried = lastRun === undefined ? undefined : carryForward(lastRun, document, rerunCollection);
236
+ return carried === undefined ? document : { ...document, run: { ...document.run, carried } };
237
+ };
209
238
  /**
210
239
  * Writes the canonical report and returns its path only once the file
211
240
  * exists: a host must never be handed a path to a report that was not
@@ -249,9 +278,6 @@ export async function run(options = {}) {
249
278
  return undefined;
250
279
  }
251
280
  };
252
- // The report `--last-failed` selected from, once collection has read it;
253
- // reporters get it beside this run's report to fold the rerun into it.
254
- let lastRun;
255
281
  /**
256
282
  * Whether the run got as far as its tests. Only such a run writes into the
257
283
  * output directory: one that stopped before leaves the previous run's
@@ -281,6 +307,9 @@ export async function run(options = {}) {
281
307
  ...(reportPath === undefined ? {} : { reportPath }),
282
308
  ...(aiTracePath === undefined ? {} : { aiTracePath }),
283
309
  });
310
+ // The list reporter has printed its summary and stopped its window; a
311
+ // line another reporter left unfinished on `run-finished` prints too.
312
+ processOutput?.showThrough(undefined);
284
313
  if (debug.enabled) {
285
314
  process.stderr.write(debug.summary());
286
315
  process.stderr.write(agentStepTable(results, serialGroups));
@@ -364,6 +393,10 @@ export async function run(options = {}) {
364
393
  : collectInMemory(config.projectRoot, options.tests.file, options.tests.registration, consumesSession(options.tests.registration) ? await collectSetups(config) : undefined);
365
394
  const inputs = await selectionInputs(options, config);
366
395
  lastRun = inputs.lastRun;
396
+ rerunCollection = {
397
+ testIds: new Set(collection.tests.map((test) => test.id)),
398
+ uncollectedFiles: new Set(collection.uncollected.map((skipped) => skipped.file)),
399
+ };
367
400
  const selection = repeatEach(select(collection, config, inputs.filters, options.passWithNoTests !== undefined ? { passWithNoTests: options.passWithNoTests } : {}), options.repeatEach ?? 1);
368
401
  return { collection, selection };
369
402
  }));
@@ -409,7 +442,7 @@ export async function run(options = {}) {
409
442
  // stream as `notice` events under it, so the reporter prints them instead
410
443
  // of a worker's stderr fighting the live status block.
411
444
  try {
412
- plans = await debug.time('engine.prepare', () => prepareEngines(plans, runWorkers, engines, { runId, projectRoot: config.projectRoot, env, signal: interrupted, notice }, emit));
445
+ plans = await debug.time('engine.prepare', () => prepareEngines(plans, runWorkers, engines, { runId, projectRoot: config.projectRoot, env, signal: interrupted, headed: options.headed ?? false, notice }, emit));
413
446
  }
414
447
  catch (cause) {
415
448
  if (!interrupted.aborted)
@@ -450,15 +483,26 @@ export async function run(options = {}) {
450
483
  // Tests are about to start, and only now is the previous run's evidence
451
484
  // given up: the artifact tree is emptied, so what is there once this run
452
485
  // ends is its own and nothing a report no longer names, and this run's
453
- // report replaces the last. A run that stopped before here (no test
454
- // selected, a collection error, a target that cannot record what it asks,
455
- // an app that failed to start, an interrupt) leaves both, and
486
+ // report replaces the last. A `--last-failed` rerun keeps the evidence
487
+ // the report it reruns names instead, since that report's results fold
488
+ // into the rerun's and its carried tests do not run again, and files its
489
+ // own attempts in a fresh `rerun-<n>` directory beside it, so no attempt
490
+ // overwrites an earlier one's files. A run that stopped before here (no
491
+ // test selected, a collection error, a target that cannot record what it
492
+ // asks, an app that failed to start, an interrupt) leaves both, and
456
493
  // `--last-failed` still reads the run that executed. A wipe that fails
457
494
  // part way has already given up the old evidence, so this run's report
458
495
  // records the failure.
459
496
  testsStarted = true;
497
+ let rerunDir;
460
498
  try {
461
- await rm(layout.artifacts, { recursive: true, force: true });
499
+ if (lastRun === undefined) {
500
+ await rm(layout.artifacts, { recursive: true, force: true });
501
+ }
502
+ else {
503
+ await pruneArtifacts(layout.artifacts, reportArtifactPaths(lastRun));
504
+ rerunDir = await claimRerunDir(layout.artifacts);
505
+ }
462
506
  }
463
507
  catch (cause) {
464
508
  recordFailure(cause, 'launch');
@@ -482,6 +526,7 @@ export async function run(options = {}) {
482
526
  inMemory: options.tests,
483
527
  runId,
484
528
  artifactsRoot,
529
+ rerunDir,
485
530
  sessionStore: store,
486
531
  headed: options.headed ?? false,
487
532
  debug,
@@ -495,6 +540,7 @@ export async function run(options = {}) {
495
540
  ports: config.ports,
496
541
  runId,
497
542
  artifactsRoot,
543
+ rerunDir,
498
544
  headed: options.headed ?? false,
499
545
  sessionsRoot: layout.sessions,
500
546
  sessionKeyBase64: store.exportKeyForWorker(),
@@ -723,14 +769,20 @@ function plannedFiles(selection) {
723
769
  function statusOf(exitCode) {
724
770
  return exitCode === 0 ? 'passed' : exitCode === 1 ? 'failed' : exitCode === 130 ? 'interrupted' : 'error';
725
771
  }
726
- function resultExitCodes(results) {
772
+ /**
773
+ * The exit codes the verdicts imply: a failed test or serial group exits 1,
774
+ * or its attempts' error category when that is worse. A serial group answers
775
+ * for its members, whose results carry no attempts, and for a group attempt
776
+ * that failed before any member ran (a launch), whose members are skipped.
777
+ */
778
+ function verdictExitCodes(results, serialGroups) {
727
779
  const codes = [0];
728
- for (const result of results) {
729
- switch (result.status) {
780
+ for (const verdict of [...results, ...serialGroups]) {
781
+ switch (verdict.status) {
730
782
  case 'failed':
731
783
  case 'timed-out':
732
784
  codes.push(1);
733
- for (const attempt of result.attempts) {
785
+ for (const attempt of verdict.attempts) {
734
786
  if (attempt.error !== undefined)
735
787
  codes.push(exitCodeForCategory(attempt.error.category));
736
788
  }
@@ -747,13 +799,21 @@ function resultExitCodes(results) {
747
799
  /** Why a reporter's `onRunFinished` was given up on: the message is the stderr line. */
748
800
  class ReporterAbandoned extends Error {
749
801
  }
802
+ /**
803
+ * The built-in reporters that write files beside `report.json`. A forced
804
+ * interrupt never abandons them: they are local writes, and giving one up
805
+ * would leave the previous run's `summary.md` or `junit.xml` beside this
806
+ * run's report, where CI reads it as this run's.
807
+ */
808
+ const FILE_REPORTERS = new Set([STATELESS_REPORTERS.junit, STATELESS_REPORTERS.markdown]);
750
809
  /**
751
810
  * Awaits every reporter's `onRunFinished` at once, each within `timeoutMs`
752
- * and until a forced interrupt, and collects the summary rows they resolve
753
- * with. Each reporter gets a signal that aborts on either, so a well-behaved
754
- * one cancels its own work and leaves no handle holding the process. A
755
- * reporter that throws, runs out of time, or returns something other than
756
- * rows is one line on stderr; it can never change the run's outcome.
811
+ * and, except the built-in file reporters, until a forced interrupt, and
812
+ * collects the summary rows they resolve with. Each reporter gets a signal
813
+ * that aborts on either, so a well-behaved one cancels its own work and
814
+ * leaves no handle holding the process. A reporter that throws, runs out of
815
+ * time, or returns something other than rows is one line on stderr; it can
816
+ * never change the run's outcome.
757
817
  */
758
818
  async function runReporters(reporters, finished, timeoutMs, force) {
759
819
  const warn = (reporter, detail) => {
@@ -767,10 +827,12 @@ async function runReporters(reporters, finished, timeoutMs, force) {
767
827
  const abandon = new AbortController();
768
828
  const timer = setTimeout(() => abandon.abort(new ReporterAbandoned(`did not finish within ${budget}`)), timeoutMs);
769
829
  const onForce = () => abandon.abort(new ReporterAbandoned('abandoned: the run was forced to stop'));
770
- if (force.aborted)
771
- onForce();
772
- else
773
- force.addEventListener('abort', onForce, { once: true });
830
+ if (!FILE_REPORTERS.has(reporter)) {
831
+ if (force.aborted)
832
+ onForce();
833
+ else
834
+ force.addEventListener('abort', onForce, { once: true });
835
+ }
774
836
  try {
775
837
  const result = await withAbort(() => Promise.resolve(onRunFinished.call(reporter, finished, abandon.signal)), abandon.signal, () => abandon.signal.reason);
776
838
  if (result === undefined)