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,18 +1,22 @@
1
1
  /** Child-process `UnitRunner`: spawn, messaging, exit tracking. */
2
2
  import { fork } from 'node:child_process';
3
3
  import { existsSync } from 'node:fs';
4
- import { createRequire } from 'node:module';
5
4
  import { fileURLToPath } from 'node:url';
5
+ import { WARNED_TSCONFIGS_ENV, warnedTsconfigs } from '../../config/tsconfig.js';
6
6
  import { InfrastructureError } from '../../internal/errors.js';
7
- /** Resolves the worker entry for both src (vitest, .ts) and dist (.js) layouts. */
7
+ /**
8
+ * Resolves the worker entry for both src (vitest, .ts) and dist (.js)
9
+ * layouts. From source, e2e's own loader is registered before the entry
10
+ * loads (`config/register.ts`): Node.js strips types but does not compile
11
+ * the rest of TypeScript.
12
+ */
8
13
  function resolveEntry() {
9
14
  const js = fileURLToPath(new URL('./entry.js', import.meta.url));
10
15
  if (existsSync(js))
11
16
  return { path: js, execArgv: [] };
12
17
  const ts = fileURLToPath(new URL('./entry.ts', import.meta.url));
13
18
  if (existsSync(ts)) {
14
- const require = createRequire(import.meta.url);
15
- return { path: ts, execArgv: ['--import', require.resolve('tsx')] };
19
+ return { path: ts, execArgv: ['--import', new URL('../../config/register.ts', import.meta.url).href] };
16
20
  }
17
21
  throw new InfrastructureError('WORKER_ENTRY_MISSING', 'e2e worker entry module not found');
18
22
  }
@@ -32,7 +36,8 @@ class ChildProcessRunner {
32
36
  cwd: spawn.projectRoot,
33
37
  execArgv: entry.execArgv,
34
38
  stdio: ['ignore', 'inherit', 'inherit', 'ipc'],
35
- env: spawn.env,
39
+ // Collecting read the project's tsconfig.json files first; the worker warns only about ones it reaches on its own.
40
+ env: { ...spawn.env, [WARNED_TSCONFIGS_ENV]: warnedTsconfigs() },
36
41
  });
37
42
  this.child.on('message', (message) => events.onMessage(message));
38
43
  this.exit = new Promise((resolve) => {
@@ -12,7 +12,8 @@ import type { CliOverrides, PortAssignments, ResolvedTarget } from '../../config
12
12
  import type { AiTraceSnapshot } from '../../internal/ai-trace.ts';
13
13
  import type { DebugSnapshot } from '../../internal/debug.ts';
14
14
  import type { SerializedError } from '../../internal/errors.ts';
15
- import type { ResultRecord, RunError, SerialGroupRecord, WireResultRecord } from '../records.ts';
15
+ import type { AttemptRecord, ResultRecord, RunError, SerialGroupRecord, SerialMemberRecord, WireResultRecord } from '../records.ts';
16
+ import type { SerialAttemptRun, SerialAttemptStart } from '../serial.ts';
16
17
  import type { StepProgress } from '../steps.ts';
17
18
  /** One runnable pair on the wire; the worker resolves the test function. */
18
19
  export interface WirePair {
@@ -56,6 +57,8 @@ export interface WorkerBootstrap {
56
57
  readonly workerSlot: number;
57
58
  readonly runId: string;
58
59
  readonly artifactsRoot: string;
60
+ /** See `TargetExecutorOptions.rerunDir`. */
61
+ readonly rerunDir: string | undefined;
59
62
  readonly headed: boolean;
60
63
  readonly sessionsRoot: string;
61
64
  /** Per-run AES key; transferred only over this channel, never disk or env. */
@@ -105,7 +108,15 @@ export interface ShutdownMessage {
105
108
  export interface TerminateMessage {
106
109
  readonly type: 'terminate';
107
110
  }
108
- export type MainToWorker = RunUnitMessage | InterruptMessage | ShutdownMessage | TerminateMessage;
111
+ /**
112
+ * Asks the worker to answer with `pong` from its event loop. The scheduler
113
+ * sends it only once an attempt is past its test timeout, to tell a worker
114
+ * still tearing the attempt down from one whose event loop is blocked.
115
+ */
116
+ export interface PingMessage {
117
+ readonly type: 'ping';
118
+ }
119
+ export type MainToWorker = RunUnitMessage | InterruptMessage | ShutdownMessage | TerminateMessage | PingMessage;
109
120
  export interface ReadyMessage {
110
121
  readonly type: 'ready';
111
122
  }
@@ -164,6 +175,35 @@ export interface NoticeMessage {
164
175
  readonly type: 'notice';
165
176
  readonly message: string;
166
177
  }
178
+ /**
179
+ * An attempt's test timeout started: its `beforeEach` hooks and body run
180
+ * now. The worker enforces the timeout with its own timers, which a body
181
+ * that blocks the event loop never lets fire, so the scheduler keeps the
182
+ * deadline too; see `SchedulerWorker.watch`.
183
+ */
184
+ export interface AttemptDeadlineMessage {
185
+ readonly type: 'attempt-deadline';
186
+ readonly testId: string;
187
+ readonly agent: string;
188
+ readonly repeat: number;
189
+ /** The attempt's index among the pair's attempts. */
190
+ readonly attempt: number;
191
+ /** The attempt's record id and start, which a watchdog kill reports it with. */
192
+ readonly attemptId: string;
193
+ readonly startedAt: string;
194
+ /** The test's resolved `timeout`. */
195
+ readonly timeoutMs: number;
196
+ /** How long past the timeout the worker may go without answering a `ping`: the cleanup budget. */
197
+ readonly graceMs: number;
198
+ }
199
+ /** The attempt the last `attempt-deadline` announced has ended, verdict and cleanup included. */
200
+ export interface AttemptEndMessage {
201
+ readonly type: 'attempt-end';
202
+ }
203
+ /** The answer to `ping`. */
204
+ export interface PongMessage {
205
+ readonly type: 'pong';
206
+ }
167
207
  export interface ResultMessage {
168
208
  readonly type: 'result';
169
209
  readonly result: WireResultRecord;
@@ -172,6 +212,48 @@ export interface SerialGroupMessage {
172
212
  readonly type: 'serial-group';
173
213
  readonly group: SerialGroupRecord;
174
214
  }
215
+ /**
216
+ * An attempt of an ordinary or setup pair begins, its realm and `beforeAll`
217
+ * hooks included. With `attempt`, it tells a crash during an attempt from
218
+ * one between attempts (an `afterAll` after the last one): only the first
219
+ * is charged a new attempt, with this index.
220
+ */
221
+ export interface AttemptStartMessage {
222
+ readonly type: 'attempt-start';
223
+ readonly testId: string;
224
+ readonly agent: string;
225
+ readonly repeat: number;
226
+ readonly index: number;
227
+ }
228
+ /**
229
+ * One finished attempt of an ordinary or setup pair. A pair's result waits
230
+ * for its last attempt, so each attempt also goes out as it ends: a worker
231
+ * that dies during a retry must not take the attempts before it along. The
232
+ * result still carries every attempt; the scheduler keeps these only until
233
+ * it arrives.
234
+ */
235
+ export interface AttemptMessage {
236
+ readonly type: 'attempt';
237
+ readonly testId: string;
238
+ readonly agent: string;
239
+ readonly repeat: number;
240
+ readonly attempt: AttemptRecord;
241
+ }
242
+ /** One member that finished in the serial group attempt running now; see `AttemptMessage`. */
243
+ export interface SerialMemberMessage {
244
+ readonly type: 'serial-member';
245
+ /** The group's report id (`SerialGroupRecord.id`). */
246
+ readonly groupId: string;
247
+ readonly attempt: SerialAttemptStart;
248
+ readonly member: SerialMemberRecord;
249
+ }
250
+ /** One finished serial group attempt; see `AttemptMessage`. */
251
+ export interface SerialAttemptMessage {
252
+ readonly type: 'serial-attempt';
253
+ /** The group's report id (`SerialGroupRecord.id`). */
254
+ readonly groupId: string;
255
+ readonly run: SerialAttemptRun;
256
+ }
175
257
  export interface UnitDoneMessage {
176
258
  readonly type: 'unit-done';
177
259
  readonly unitId: string;
@@ -206,6 +288,6 @@ export interface RunAbortMessage {
206
288
  readonly type: 'run-abort';
207
289
  readonly error: SerializedError;
208
290
  }
209
- export type WorkerToMain = ReadyMessage | PairStartMessage | ProgressMessage | OutputMessage | NoticeMessage | ResultMessage | SerialGroupMessage | UnitDoneMessage | ShutdownDoneMessage | FatalMessage | RunAbortMessage;
291
+ export type WorkerToMain = ReadyMessage | PairStartMessage | ProgressMessage | AttemptDeadlineMessage | AttemptEndMessage | PongMessage | OutputMessage | NoticeMessage | ResultMessage | SerialGroupMessage | AttemptStartMessage | AttemptMessage | SerialMemberMessage | SerialAttemptMessage | UnitDoneMessage | ShutdownDoneMessage | FatalMessage | RunAbortMessage;
210
292
  /** Reattaches the scheduler's resolved target to a wire result. */
211
293
  export declare function decodeResult(wire: WireResultRecord, target: ResolvedTarget): ResultRecord;
@@ -35,6 +35,8 @@ export interface TargetWorkerDeps {
35
35
  readonly sessionStore: SessionStore;
36
36
  readonly runId: string;
37
37
  readonly artifactsRoot: string;
38
+ /** See `TargetExecutorOptions.rerunDir`. */
39
+ readonly rerunDir: string | undefined;
38
40
  readonly headed: boolean;
39
41
  /** See `TargetExecutorOptions.workerSlot`. */
40
42
  readonly workerSlot: number;
@@ -4,6 +4,7 @@
4
4
  * in-process) drive this class, so unit execution, interrupt handling, and
5
5
  * run-error forwarding exist exactly once.
6
6
  */
7
+ import inspector from 'node:inspector';
7
8
  import { DebugTrace } from '../../internal/debug.js';
8
9
  import { TargetExecutor } from '../execute.js';
9
10
  import { encodeResult } from '../records.js';
@@ -46,6 +47,7 @@ export class TargetWorker {
46
47
  target: deps.target,
47
48
  runId: deps.runId,
48
49
  artifactsRoot: deps.artifactsRoot,
50
+ rerunDir: deps.rerunDir,
49
51
  sessionStore: deps.sessionStore,
50
52
  headed: deps.headed,
51
53
  workerSlot: deps.workerSlot,
@@ -65,6 +67,10 @@ export class TargetWorker {
65
67
  this.countFailure();
66
68
  },
67
69
  onSerialGroup: (group) => this.host.emit({ type: 'serial-group', group }),
70
+ onAttemptStart: (pair, index) => this.host.emit({ type: 'attempt-start', testId: pair.test.id, agent: pair.agent, repeat: pair.repeat, index }),
71
+ onAttempt: (pair, attempt) => this.host.emit({ type: 'attempt', testId: pair.test.id, agent: pair.agent, repeat: pair.repeat, attempt }),
72
+ onSerialMember: (groupId, attempt, member) => this.host.emit({ type: 'serial-member', groupId, attempt, member }),
73
+ onSerialAttempt: (groupId, run) => this.host.emit({ type: 'serial-attempt', groupId, run }),
68
74
  onPairStart: (pair) => {
69
75
  this.inFlight = { testId: pair.test.id, agent: pair.agent, repeat: pair.repeat };
70
76
  this.host.emit({
@@ -77,6 +83,23 @@ export class TargetWorker {
77
83
  serialId: pair.test.serialId,
78
84
  });
79
85
  },
86
+ onAttemptDeadline: (pair, attempt) => {
87
+ // A worker paused at a breakpoint looks blocked; with a debugger attached nothing watches its attempts.
88
+ if (inspector.url() !== undefined)
89
+ return;
90
+ this.host.emit({
91
+ type: 'attempt-deadline',
92
+ testId: pair.test.id,
93
+ agent: pair.agent,
94
+ repeat: pair.repeat,
95
+ attempt: attempt.index,
96
+ attemptId: attempt.id,
97
+ startedAt: attempt.startedAt,
98
+ timeoutMs: pair.options.timeout,
99
+ graceMs: deps.config.cleanupTimeout,
100
+ });
101
+ },
102
+ onAttemptEnd: () => this.host.emit({ type: 'attempt-end' }),
80
103
  onProgress: (pair, progress) => this.host.emit({ type: 'progress', testId: pair.test.id, agent: pair.agent, repeat: pair.repeat, progress }),
81
104
  onNotice: (message) => this.host.emit({ type: 'notice', message }),
82
105
  onRunAbort: (runError) => {
@@ -112,6 +135,12 @@ export class TargetWorker {
112
135
  case 'terminate':
113
136
  this.terminate();
114
137
  return;
138
+ case 'ping':
139
+ // Answered now, not queued behind the running unit: the answer is the proof the event loop turns.
140
+ this.host.emit({ type: 'pong' });
141
+ return;
142
+ default:
143
+ message;
115
144
  }
116
145
  }
117
146
  /**
@@ -42,7 +42,8 @@ export declare function fleetName(env: NodeJS.ProcessEnv): string | null;
42
42
  * The id the environment states for itself, standing in for a machine that
43
43
  * has no preferences file: a fleet by its name, else the CI vendor when `CI`
44
44
  * is set. Undefined on a person's machine, where the random per-machine id
45
- * is the unit; a vendor marker alone, without `CI`, does not make one a runner.
45
+ * is the unit: a vendor marker without `CI` is a shell on a runner, and `CI`
46
+ * with a coding agent but no vendor is the agent's shell on a laptop.
46
47
  */
47
48
  export declare function statedIdentity(env: NodeJS.ProcessEnv): string | undefined;
48
49
  export declare function collectEnvironment(options: {
@@ -30,6 +30,10 @@ const CI_VENDORS = [
30
30
  ['SEMAPHORE', 'semaphore'],
31
31
  ['APPVEYOR', 'appveyor'],
32
32
  ['WOODPECKER', 'woodpecker'],
33
+ ['EAS_BUILD', 'eas'],
34
+ ['BITRISE_IO', 'bitrise'],
35
+ ['CM_BUILD_ID', 'codemagic'],
36
+ ['CI_XCODE_CLOUD', 'xcode-cloud'],
33
37
  ];
34
38
  /**
35
39
  * Environment variables coding agents set in the shells they spawn. The
@@ -99,13 +103,19 @@ export function fleetName(env) {
99
103
  * The id the environment states for itself, standing in for a machine that
100
104
  * has no preferences file: a fleet by its name, else the CI vendor when `CI`
101
105
  * is set. Undefined on a person's machine, where the random per-machine id
102
- * is the unit; a vendor marker alone, without `CI`, does not make one a runner.
106
+ * is the unit: a vendor marker without `CI` is a shell on a runner, and `CI`
107
+ * with a coding agent but no vendor is the agent's shell on a laptop.
103
108
  */
104
109
  export function statedIdentity(env) {
105
110
  const fleet = fleetName(env);
106
111
  if (fleet !== null)
107
112
  return `fleet:${fleet}`;
108
- return isCiMode(env) ? `ci:${ciName(env) ?? 'unknown'}` : undefined;
113
+ if (!isCiMode(env))
114
+ return undefined;
115
+ const vendor = firstMarker(env, CI_VENDORS);
116
+ if (vendor === undefined && firstMarker(env, CODING_AGENTS) !== undefined)
117
+ return undefined;
118
+ return `ci:${vendor ?? 'unknown'}`;
109
119
  }
110
120
  /** The runtime executing this process: Bun and Deno each announce themselves in `process.versions`. */
111
121
  function runtime(versions) {
@@ -205,6 +205,7 @@ export function runCompletedEvent(report, context) {
205
205
  tests_executed: run.summary.executed,
206
206
  tests_passed: run.summary.passed,
207
207
  tests_failed: run.summary.failed,
208
+ tests_interrupted: run.summary.interrupted,
208
209
  tests_flaky: run.summary.flaky,
209
210
  tests_skipped: run.summary.skipped,
210
211
  attempts_total: attempts.reduce((total, count) => total + count, 0),
@@ -8,10 +8,9 @@
8
8
  * commit, which every clone shares and no one can produce without the
9
9
  * repository itself; a commit hash carries no name, path, or remote. A
10
10
  * shallow clone has no root to offer: its boundary commits pose as roots and
11
- * move with every fetch, so it is treated like a directory outside git.
12
- * There, the project root path is hashed together with the machine's local
13
- * salt, so the id is stable on that machine and unrecoverable anywhere else.
14
- * In CI there is no salt, so a runner without a full history has no id: its
15
- * working directory is not a project.
11
+ * move with every fetch, so it has no id, like a directory outside git: a
12
+ * path is the same project on one machine only, so it cannot be told apart
13
+ * from a scratch directory nobody returns to.
16
14
  */
17
- export declare function anonymousProjectId(projectRoot: string, salt: string | undefined): Promise<string | undefined>;
15
+ /** The hashed root commit of the repository at `projectRoot`; undefined outside git and in a shallow clone. */
16
+ export declare function anonymousProjectId(projectRoot: string): Promise<string | undefined>;
@@ -8,11 +8,9 @@
8
8
  * commit, which every clone shares and no one can produce without the
9
9
  * repository itself; a commit hash carries no name, path, or remote. A
10
10
  * shallow clone has no root to offer: its boundary commits pose as roots and
11
- * move with every fetch, so it is treated like a directory outside git.
12
- * There, the project root path is hashed together with the machine's local
13
- * salt, so the id is stable on that machine and unrecoverable anywhere else.
14
- * In CI there is no salt, so a runner without a full history has no id: its
15
- * working directory is not a project.
11
+ * move with every fetch, so it has no id, like a directory outside git: a
12
+ * path is the same project on one machine only, so it cannot be told apart
13
+ * from a scratch directory nobody returns to.
16
14
  */
17
15
  import { COMMIT_HASH, git } from '../internal/git.js';
18
16
  import { sha256Hex } from '../internal/ids.js';
@@ -31,11 +29,8 @@ async function rootCommit(cwd) {
31
29
  .filter((line) => COMMIT_HASH.test(line))
32
30
  .toSorted()[0];
33
31
  }
34
- export async function anonymousProjectId(projectRoot, salt) {
32
+ /** The hashed root commit of the repository at `projectRoot`; undefined outside git and in a shallow clone. */
33
+ export async function anonymousProjectId(projectRoot) {
35
34
  const commit = await rootCommit(projectRoot);
36
- if (commit !== undefined)
37
- return sha256Hex(`git\n${commit}`);
38
- if (salt === undefined)
39
- return undefined;
40
- return sha256Hex(`path\n${salt}\n${projectRoot}`);
35
+ return commit === undefined ? undefined : sha256Hex(`git\n${commit}`);
41
36
  }
@@ -2,9 +2,11 @@
2
2
  * The user-level telemetry preferences: `~/.config/e2e/telemetry.json`
3
3
  * (`$XDG_CONFIG_HOME/e2e` when set, `%APPDATA%\e2e` on Windows). The file
4
4
  * holds the opt-out, the random id every event from this machine is
5
- * attributed to, the salt that keeps a hashed path unrecoverable, and which
6
- * notice was shown when. Nothing in it names the person or the machine: the
7
- * id and the salt are random bytes, generated here and never derived.
5
+ * attributed to, and which notice was shown when. Nothing in it names the
6
+ * person or the machine: the id is random bytes, generated here and never
7
+ * derived. A complete file also holds a random salt that nothing is hashed
8
+ * with anymore: older versions hashed paths with it and replace the id of a
9
+ * file without one, and a machine often has several versions installed.
8
10
  *
9
11
  * The store fails closed. `open` completes the file in one write when the id
10
12
  * or the salt is missing, and when that write cannot happen there is no
@@ -38,8 +40,6 @@ export declare class TelemetryStore {
38
40
  get enabled(): boolean;
39
41
  /** The random per-machine id every event from this machine is attributed to. */
40
42
  get anonymousId(): string;
41
- /** The local salt that keeps a hashed project path unrecoverable; never leaves the machine. */
42
- get pathSalt(): string;
43
43
  /** Whole days since the ids were generated; undefined for a file from before the time was recorded. */
44
44
  ageDays(now?: number): number | undefined;
45
45
  /** Saves the choice; false when the file could not be written and the choice stayed in memory. */
@@ -2,9 +2,11 @@
2
2
  * The user-level telemetry preferences: `~/.config/e2e/telemetry.json`
3
3
  * (`$XDG_CONFIG_HOME/e2e` when set, `%APPDATA%\e2e` on Windows). The file
4
4
  * holds the opt-out, the random id every event from this machine is
5
- * attributed to, the salt that keeps a hashed path unrecoverable, and which
6
- * notice was shown when. Nothing in it names the person or the machine: the
7
- * id and the salt are random bytes, generated here and never derived.
5
+ * attributed to, and which notice was shown when. Nothing in it names the
6
+ * person or the machine: the id is random bytes, generated here and never
7
+ * derived. A complete file also holds a random salt that nothing is hashed
8
+ * with anymore: older versions hashed paths with it and replace the id of a
9
+ * file without one, and a machine often has several versions installed.
8
10
  *
9
11
  * The store fails closed. `open` completes the file in one write when the id
10
12
  * or the salt is missing, and when that write cannot happen there is no
@@ -113,10 +115,6 @@ export class TelemetryStore {
113
115
  get anonymousId() {
114
116
  return this.preferences.anonymousId;
115
117
  }
116
- /** The local salt that keeps a hashed project path unrecoverable; never leaves the machine. */
117
- get pathSalt() {
118
- return this.preferences.salt;
119
- }
120
118
  /** Whole days since the ids were generated; undefined for a file from before the time was recorded. */
121
119
  ageDays(now = Date.now()) {
122
120
  const created = this.preferences.createdAt;
@@ -43,7 +43,7 @@ export interface TelemetryOptions {
43
43
  readonly fetch?: typeof fetch;
44
44
  /** Where the notice and the debug output go; stderr when absent. */
45
45
  readonly write?: (text: string) => void;
46
- /** Resolves the anonymous project id; git and the salted path when absent. */
46
+ /** Resolves the anonymous project id; the hashed root commit when absent. */
47
47
  readonly projectId?: typeof anonymousProjectId;
48
48
  }
49
49
  export declare class Telemetry {
@@ -114,8 +114,7 @@ export declare class Telemetry {
114
114
  record(event: TelemetryEvent): void;
115
115
  /**
116
116
  * Git is asked for the project id now, while the command runs, so the
117
- * flush at the end waits on the network alone. A fleet or CI has no
118
- * store, so a path outside git has nothing to salt it and yields no id.
117
+ * flush at the end waits on the network alone.
119
118
  */
120
119
  private lookUpProject;
121
120
  /** The session event, first in the batch; absent for an invocation that never reached a command. */
@@ -187,12 +187,10 @@ export class Telemetry {
187
187
  }
188
188
  /**
189
189
  * Git is asked for the project id now, while the command runs, so the
190
- * flush at the end waits on the network alone. A fleet or CI has no
191
- * store, so a path outside git has nothing to salt it and yields no id.
190
+ * flush at the end waits on the network alone.
192
191
  */
193
192
  lookUpProject() {
194
- const store = this.statedId === undefined ? this.store() : undefined;
195
- this.project ??= this.projectId(this.cwd, store?.pathSalt);
193
+ this.project ??= this.projectId(this.cwd);
196
194
  }
197
195
  /** The session event, first in the batch; absent for an invocation that never reached a command. */
198
196
  sessionEvent() {
package/dist/types.d.ts CHANGED
@@ -198,7 +198,7 @@ export interface TextMatchOptions {
198
198
  /**
199
199
  * Role query options. A role query never matches a node hidden from the
200
200
  * accessibility tree, on every engine; `visible` (inherited) is the one knob
201
- * that narrows the other query kinds the same way.
201
+ * that narrows the other query kinds, to the nodes `toBeVisible()` accepts.
202
202
  */
203
203
  export interface RoleOptions extends TextMatchOptions {
204
204
  /** Accessible name filter. */
@@ -265,6 +265,7 @@ export interface SwipeOptions {
265
265
  momentum?: Momentum;
266
266
  from?: never;
267
267
  to?: never;
268
+ duration?: never;
268
269
  }
269
270
  /** A swipe along a path between two viewport points: a touch swipe on a device, a pointer drag on a document platform. */
270
271
  export interface SwipePathOptions {
@@ -272,6 +273,8 @@ export interface SwipePathOptions {
272
273
  from: Point;
273
274
  /** Where it lifts. */
274
275
  to: Point;
276
+ /** Gesture duration in milliseconds; omitted = the engine's default. */
277
+ duration?: number;
275
278
  direction?: never;
276
279
  momentum?: never;
277
280
  }
@@ -377,9 +380,13 @@ export interface Locator extends Screen {
377
380
  all(): Promise<Locator[]>;
378
381
  /** Reads the normalized text of every current match, without auto-waiting; empty when nothing matches. */
379
382
  allTextContents(): Promise<string[]>;
380
- /** Waits for the requested locator state. */
383
+ /**
384
+ * Waits for the requested locator state, `visible` by default: `attached`
385
+ * for one match, visible or not; `detached` for none; `hidden` for none or
386
+ * a hidden one.
387
+ */
381
388
  waitFor(options?: {
382
- state?: 'visible' | 'hidden';
389
+ state?: 'attached' | 'detached' | 'visible' | 'hidden';
383
390
  timeout?: number;
384
391
  }): Promise<void>;
385
392
  /** Adds deterministic locator filters. */
@@ -1024,9 +1031,9 @@ export interface StoredArtifact {
1024
1031
  readonly path: string;
1025
1032
  /**
1026
1033
  * How much of the file the runner masked, as the report records it. A
1027
- * `download` is `incomplete` unless the runner rewrote it as text after a
1028
- * secret fill; a store that exports only what the runner vouches for reads
1029
- * this rather than the kind.
1034
+ * `download` is `incomplete` unless the runner ran it as text through the
1035
+ * session's secret values; a store that exports only what the runner
1036
+ * vouches for reads this rather than the kind.
1030
1037
  */
1031
1038
  readonly redaction: 'complete' | 'not-required' | 'incomplete';
1032
1039
  readonly runId: string;
@@ -1250,6 +1257,8 @@ export interface E2EConfig {
1250
1257
  cleanupTimeout?: number;
1251
1258
  /** Retries per test, 0 through 10; default 1 in CI, else 0. */
1252
1259
  retries?: number;
1260
+ /** Fail the run when a test skips itself after a soft failure or an earlier failed attempt; default false. */
1261
+ failOnSkippedFailure?: boolean;
1253
1262
  /** Parallel workers, 1 through 1024; default 1 in CI, else half the cores. An engine may cap it lower. */
1254
1263
  workers?: number;
1255
1264
  /** `{ store }` hands every artifact to a host store as it is produced. */
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  title: How agent steps work
3
3
  description: What the model sees, what it may do, how a step ends, and what it costs.
4
+ icon: "list-check"
4
5
  ---
5
6
 
6
7
  An agent step is one `agent.act`, `agent.assert`, `agent.waitFor`, or
package/docs/agents.mdx CHANGED
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  title: Agents and personas
3
3
  description: Configure several agents in one suite, pin tests to them, and run one flow as each persona.
4
+ icon: "users"
4
5
  ---
5
6
 
6
7
  Define named agents in `agents` to give tests different models, instructions,
@@ -109,9 +110,12 @@ agent if the problem affects several tests:
109
110
  5. Compare models or `providerOptions` by running the same tests with
110
111
  `--agent <name>`.
111
112
 
112
- Named agents can use the [replay cache](/cache). Custom tools and executors
113
- have their own replay constraints, described under [project tools](/tools)
114
- and [custom executors](/executors#replay-cache).
113
+ Named agents can use the [replay cache](/cache). Each agent records its
114
+ own entries, keyed by its name and its context, so a test that runs as
115
+ `buyer` and as `admin` never replays one persona's steps for the other, and
116
+ changing `agentContext` records again. Custom tools and executors have their
117
+ own replay constraints, described under [project tools](/tools) and
118
+ [custom executors](/executors#replay-cache).
115
119
 
116
120
  ## Pick an agent per run
117
121
 
@@ -1,6 +1,7 @@
1
1
  ---
2
- title: Testing APIs
2
+ title: API
3
3
  description: Check your API with fetch and value matchers, in the same files and run as UI tests.
4
+ icon: "server"
4
5
  ---
5
6
 
6
7
  API tests run in the same files and the same run as UI tests. A test that
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  title: Signing in
3
3
  description: Sign in once, reuse the session across tests, and pass secrets without exposing them to the model.
4
+ icon: "key"
4
5
  ---
5
6
 
6
7
  Sign in once in a setup test and save a named session. Other tests restore
@@ -164,7 +165,7 @@ runner keeps it away from test code and the model altogether.
164
165
 
165
166
  A `Secret` has no `.value`. Three sinks accept one: the two below in a
166
167
  test, and an engine option that declares it, as
167
- [`basicAuth`](/starting-your-app#protected-previews) does:
168
+ [`basicAuth`](/web#protected-previews) does:
168
169
 
169
170
  <Tabs>
170
171
  <Tab title="Deterministic">
@@ -211,7 +212,9 @@ export default {
211
212
  ```
212
213
 
213
214
  `E2E_SECRET_<NAME>` overrides an entry per run, with the same uppercasing rule
214
- as credentials, so a CI job can supply the value without touching the config:
215
+ as credentials, so a CI job can supply the value without touching the config.
216
+ Two entries that map to one variable, like `stripe-key` and `stripe_key`, fail
217
+ the config load with `INVALID_CONFIG`, as two such credentials do:
215
218
 
216
219
  <CodeGroup>
217
220
  ```bash npm
@@ -277,7 +280,7 @@ account, a username with the secrets that sign it in, while a secret is one
277
280
  value, so `credentials.admin` and `secrets.admin` can both exist.
278
281
 
279
282
  In `e2e.config.ts`, `secrets.get()` hands an engine option a secret it needs
280
- before any test runs, such as a [basic-auth password](/starting-your-app#protected-previews).
283
+ before any test runs, such as a [basic-auth password](/web#protected-previews).
281
284
  The config load checks the name, and the value is redacted once the engine
282
285
  resolves it, together with the base64 `Authorization` credential it becomes.
283
286
  The protection is text only: screenshots and the model's pixels are kept, so