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,15 +1,17 @@
1
1
  ---
2
2
  title: Browser
3
3
  description: The browser engine, its options, and the browser fixture it contributes.
4
+ icon: "globe"
4
5
  ---
5
6
 
6
7
  `@e2e-dev/web` drives Chromium, Firefox, and WebKit through Playwright.
7
8
  A web target names it as its engine, and it contributes the `browser` fixture for
8
9
  browser-only work. Import `test` from the package to have `browser` typed.
9
10
 
10
- The package declares `playwright` as a peer dependency (`>=1.63.0 <2`) and
11
- does not install it. An app that already depends on Playwright keeps its
12
- version; `e2e init` adds `playwright` only to a project that has none.
11
+ The package depends on `playwright-core` pinned to an exact version, so the
12
+ engine always runs the Playwright it was tested against. An app that depends
13
+ on Playwright itself keeps its own copy; the two share a browser cache only
14
+ when their versions match.
13
15
 
14
16
  ```ts title="e2e.config.ts"
15
17
  import type { E2EConfig } from 'e2e';
@@ -20,6 +22,31 @@ export default {
20
22
  } satisfies E2EConfig;
21
23
  ```
22
24
 
25
+ ## Installing browsers
26
+
27
+ A missing browser downloads once per run, before the run's clock starts. To
28
+ install browsers as their own step, in CI or a container image, run the
29
+ package's command. It installs the browsers for the Playwright version the
30
+ engine pins:
31
+
32
+ <CodeGroup>
33
+ ```bash npm
34
+ npx @e2e-dev/web install chromium --with-deps
35
+ ```
36
+
37
+ ```bash pnpm
38
+ pnpm exec e2e-web install chromium --with-deps
39
+ ```
40
+
41
+ ```bash bun
42
+ bunx @e2e-dev/web install chromium --with-deps
43
+ ```
44
+ </CodeGroup>
45
+
46
+ Name any of `chromium`, `firefox`, and `webkit`; with none named it installs
47
+ `chromium`. `--with-deps` also installs the system libraries the browsers need
48
+ on Linux. The command exits with the installer's exit code.
49
+
23
50
  ## Options
24
51
 
25
52
  ```ts
@@ -82,8 +109,27 @@ export interface WebOptions {
82
109
  * (`connect.reconnectEndpoint`, or a provider with `scope: 'attempt'`).
83
110
  */
84
111
  readonly userAgent?: string;
112
+ /**
113
+ * The locale every attempt's context runs in, a BCP 47 tag such as
114
+ * `de-DE`: what `navigator.language`, `Intl` formatting, and the
115
+ * `Accept-Language` header report. Defaults to the browser's own. An
116
+ * `accept-language` entry in `headers` beside it is `INVALID_CONFIG`, and
117
+ * so is a persistent context.
118
+ */
119
+ readonly locale?: string;
120
+ /**
121
+ * The IANA time zone every attempt's context runs in, such as
122
+ * `Europe/Berlin`: what `Date` and `Intl` resolve local time against.
123
+ * Defaults to the machine's. A persistent context is `INVALID_CONFIG`.
124
+ */
125
+ readonly timezoneId?: string;
126
+ /** Scripts every document runs before the page's own. See [Init scripts](#init-scripts). */
127
+ readonly initScripts?: readonly WebInitScript[];
85
128
  }
86
129
 
130
+ /** JavaScript source, a file relative to the project root, or a function. */
131
+ export type WebInitScript = string | { readonly path: string } | (() => unknown);
132
+
87
133
  /** The engine's screencast of the page: the frame size and the JPEG quality of the frames it encodes. */
88
134
  export interface WebScreencastOptions {
89
135
  /** Frame size in pixels; default the viewport's, or the window's under `viewport: null`. A smaller size makes a smaller file. */
@@ -117,7 +163,7 @@ That protection is text only: unlike a fill, it withholds no screenshot or
117
163
  model pixels, so a page that renders the password on screen is not masked in
118
164
  them ([Security](/security#secrets)).
119
165
 
120
- `headers` and `basicAuth` are how a test reaches a [protected preview](/starting-your-app#protected-previews).
166
+ `headers` and `basicAuth` are how a test reaches a [protected preview](/web#protected-previews).
121
167
 
122
168
  `screencast` shapes the recording, not which attempts record: that is the
123
169
  config's [`video`](/reference/config#video) mode. `size` takes whole pixels,
@@ -180,7 +226,7 @@ recording resume, but a segment lost during the disconnect cannot be recovered.
180
226
  Code using `surfaceOf(engine)` must read its accessors again after recovery;
181
227
  previously retained Playwright objects belong to the disconnected transport.
182
228
 
183
- Persistent recovery does not support `headers`, `basicAuth`, `userAgent`, `app.clearState`,
229
+ Persistent recovery does not support `headers`, `basicAuth`, `userAgent`, `locale`, `timezoneId`, `app.clearState`,
184
230
  or session state capture and restore. `app.restart` remains available. Omit
185
231
  `reconnectEndpoint` to keep ordinary isolated contexts and the existing behavior.
186
232
  A `browser` provider with `scope: 'attempt'` rides the same persistent
@@ -202,7 +248,7 @@ a page acts on that tab: `goto`, `reload`, `back`, `forward`, `url`, `title`,
202
248
  `waitForURL`, `locator`, `frameLocator`, `evaluate`, `waitForResponse`,
203
249
  `waitForDownload`, `keyboard`, `mouse`, the `expect(browser)` matchers, and
204
250
  every `screen` query. What belongs to the browser covers every tab:
205
- `cookies`, `setCookies`, `route`, `unroute`, and `onDialog`. `setViewport`
251
+ `cookies`, `setCookies`, `route`, `unroute`, `onDialog`, and `addInitScript`. `setViewport`
206
252
  sizes the active tab and every tab opened after it.
207
253
 
208
254
  A tab the app opens itself, from a `target="_blank"` link or `window.open`,
@@ -246,13 +292,17 @@ export interface Browser extends Expectable<BrowserExpectation> {
246
292
  locator(selector: string): Locator;
247
293
  /** Creates a screen query scope inside one iframe. */
248
294
  frameLocator(selector: string): FrameScreen;
249
- /** Evaluates trusted test code in the page. */
295
+ /** Evaluates trusted test code in the page: a function, or a string expression whose value is returned, called first if it is a function. */
250
296
  evaluate<T extends JsonValue>(fn: string | (() => T | Promise<T>)): Promise<T>;
251
297
  /** Evaluates trusted test code with one required JSON-safe argument. */
252
298
  evaluate<T extends JsonValue, Arg extends JsonValue>(
253
299
  fn: string | ((arg: Arg) => T | Promise<T>),
254
300
  arg: Arg,
255
301
  ): Promise<T>;
302
+ /** Adds a script every document runs before the page's own, from the next navigation on. */
303
+ addInitScript(script: string | { path: string } | (() => unknown)): Promise<void>;
304
+ /** Adds an init script function called with one JSON argument. */
305
+ addInitScript<Arg extends JsonValue>(script: (arg: Arg) => unknown, arg: Arg): Promise<void>;
256
306
  /** Adds an attempt-scoped network route. */
257
307
  route(
258
308
  pattern: string | RegExp,
@@ -308,7 +358,7 @@ export interface Browser extends Expectable<BrowserExpectation> {
308
358
  | `goto`, `reload`, `back`, `forward` | `config.timeout`, 120000 ms, the same budget `app.open()` uses; `{ timeout }` overrides, capped by the test deadline |
309
359
  | `waitForResponse`, `waitForDownload` | `config.actionTimeout`, 30000 ms; `{ timeout }` overrides, capped by the test deadline |
310
360
  | `waitForURL` | `config.assertionTimeout`, 5000 ms |
311
- | `url`, `title`, `evaluate`, `route`, `unroute`, `cookies`, `setCookies`, `setViewport`, `onDialog`, `keyboard.*`, `mouse.*` | `config.actionTimeout`, 30000 ms, not overridable |
361
+ | `url`, `title`, `evaluate`, `addInitScript`, `route`, `unroute`, `cookies`, `setCookies`, `setViewport`, `onDialog`, `keyboard.*`, `mouse.*` | `config.actionTimeout`, 30000 ms, not overridable |
312
362
 
313
363
  Every `browser` call is a recorded `browser.<method>` step, except `locator` and
314
364
  `frameLocator`: they build a locator without touching the page, and the
@@ -319,7 +369,7 @@ in a suite that also runs on other platforms.
319
369
  ### Navigation
320
370
 
321
371
  `goto` defaults `waitUntil` to `'load'` and throws `POLICY_DENIED` for a
322
- `file:`, `data:`, or `javascript:` URL. `waitForURL` polls every 100 ms; a
372
+ URL whose scheme is not `http:` or `https:` (`about:blank` is admitted). `waitForURL` polls every 100 ms; a
323
373
  relative string resolves against the base URL and matches exactly after
324
374
  normalization, a `RegExp` tests the full URL, anything else (a predicate
325
375
  function, say) is `INVALID_ARGUMENT`, and a timeout is `ASSERTION_FAILED`. It
@@ -379,6 +429,10 @@ close over anything outside its own body; helpers declared inside the body
379
429
  work. Pass outside values through `arg`. Both `arg` and the return value
380
430
  must be `JsonValue`, otherwise the call fails with `INVALID_ARGUMENT`.
381
431
 
432
+ A string is evaluated as a single expression, not as statements. If it
433
+ evaluates to a function, that function is called with `arg`; otherwise its
434
+ value is the result. A promise, from either, is awaited.
435
+
382
436
  An exception in the evaluated code is `EVALUATE_FAILED` with its message. A
383
437
  Playwright timeout is `ACTION_FAILED`; a closed page or lost context is
384
438
  `ENGINE_FAILURE`.
@@ -389,18 +443,52 @@ const flag = await browser.evaluate(
389
443
  (name: string) => window.localStorage.getItem(name),
390
444
  'featureFlags',
391
445
  );
446
+ const title = await browser.evaluate<string>('document.title');
447
+ ```
448
+
449
+ ### Init scripts
450
+
451
+ An init script runs in every document before the page's own scripts, in every
452
+ tab and frame. `web({ initScripts })` applies to every test;
453
+ `browser.addInitScript` adds one for the rest of the test.
454
+
455
+ A script is JavaScript source, a `{ path }` relative to the project root, or a
456
+ function. A function cannot close over test variables; pass a JSON `arg`
457
+ instead.
458
+
459
+ ```ts
460
+ await browser.addInitScript((value) => { Math.random = () => value; }, 0.5);
461
+ await app.open('/raffle');
392
462
  ```
393
463
 
464
+ A script added after the page opened runs from the next navigation or reload.
465
+
394
466
  ### Routing
395
467
 
396
468
  `route`, `unroute`, and `waitForResponse` take a string pattern (`*` within a
397
469
  path segment, `**` across `/`) or a `RegExp`, both tested against the full
398
470
  URL; a predicate function is `INVALID_ARGUMENT`. A route survives
399
471
  `app.restart()`, `app.clearState()`, and session restore.
400
- The handler must decide exactly once with `fulfill`, `continue`, or `abort`;
401
- deciding twice or not at all is `ACTION_FAILED`, as is a `waitForResponse`
402
- that nothing matches inside the deadline. The response resolves once its body
403
- is known: `url`, `status`, and `headers` are always readable, and a body the
472
+ The handler must decide exactly once with `fulfill`, `continue`, `fallback`,
473
+ or `abort`; deciding twice or not at all is `ACTION_FAILED`. Routes run newest
474
+ first. `continue` sends the request to the network and no other route sees
475
+ it; its `url` resolves against the base URL and goes through the same rule as
476
+ `goto`, so a forbidden scheme is `POLICY_DENIED`, and it must keep the
477
+ request's scheme. `headers` replaces every request header. The configured
478
+ `headers` still ride a continued request bound for the app's site, and only
479
+ that site. `fallback` hands the request to the route registered before this
480
+ one, or the network when none matches. `fulfill` takes one of `json`, `body`,
481
+ or `path`, a file relative to the project root whose extension sets the
482
+ `Content-Type` unless `contentType` or a `content-type` header does. An option
483
+ a decision does not take (Playwright's `abort(errorCode)`, a misspelled key) is
484
+ `INVALID_ARGUMENT`: the request is aborted and the next step fails.
485
+
486
+ A `waitForResponse` that nothing matches inside the deadline is
487
+ `ACTION_FAILED`. The deadline bounds the match: the response resolves as soon
488
+ as its headers arrive, and `text()` and `json()` wait for the body on a budget
489
+ of their own, `config.actionTimeout` capped by the test deadline; a body still
490
+ streaming when it runs out is `ACTION_FAILED`. `url`, `status`, and `headers`
491
+ are always readable, and a body the
404
492
  browser could not read (a connection reset short of the declared
405
493
  `Content-Length`, a request the page aborted, or a redirect, whose body the
406
494
  browser never keeps) rejects `text()` and `json()` with `ACTION_FAILED`
@@ -421,16 +509,35 @@ expect(response.status).toBe(201);
421
509
  ```
422
510
 
423
511
  ```ts
424
- /** `json` or `body`, never both; neither fulfills with an empty body. */
512
+ /**
513
+ * One of `json`, `body`, or `path`, or none for an empty body. `path` names a
514
+ * file relative to the project root; its `Content-Type` follows its
515
+ * extension unless `contentType` or a `content-type` header sets one.
516
+ */
425
517
  export type RouteFulfillResponse = {
426
518
  status?: number;
427
519
  headers?: Record<string, string>;
520
+ /** Sets the `Content-Type` response header. */
521
+ contentType?: string;
428
522
  } & (
429
- | { json: JsonValue; body?: never }
430
- | { body: string; json?: never }
431
- | { body?: never; json?: never }
523
+ | { json: JsonValue; body?: never; path?: never }
524
+ | { body: string; json?: never; path?: never }
525
+ | { path: string; json?: never; body?: never }
526
+ | { body?: never; json?: never; path?: never }
432
527
  );
433
528
 
529
+ /** What `continue` changes about the request before it goes to the network. */
530
+ export interface RouteContinueOverrides {
531
+ /** Resolved against the base URL like `goto`; must keep the request's scheme. */
532
+ url?: string;
533
+ /** Request method. */
534
+ method?: string;
535
+ /** Replaces every request header; the configured `headers` for the app's site are added on top. */
536
+ headers?: Record<string, string>;
537
+ /** Request body. */
538
+ postData?: string;
539
+ }
540
+
434
541
  export interface WebRoute {
435
542
  /** The intercepted request. */
436
543
  readonly request: {
@@ -441,8 +548,10 @@ export interface WebRoute {
441
548
  };
442
549
  /** Fulfills the intercepted request once. */
443
550
  fulfill(response: RouteFulfillResponse): Promise<void>;
444
- /** Continues the intercepted request once. */
445
- continue(): Promise<void>;
551
+ /** Sends the intercepted request to the network once, skipping every other route. */
552
+ continue(overrides?: RouteContinueOverrides): Promise<void>;
553
+ /** Hands the intercepted request to the route registered before this one, or the network when none matches. */
554
+ fallback(): Promise<void>;
446
555
  /** Aborts the intercepted request once. */
447
556
  abort(): Promise<void>;
448
557
  }
@@ -454,9 +563,9 @@ export interface WebResponse {
454
563
  readonly status: number;
455
564
  /** Response headers, lower-cased names. */
456
565
  readonly headers: Readonly<Record<string, string>>;
457
- /** Parses the response body as JSON. */
566
+ /** Waits for the body and parses it as JSON. */
458
567
  json<T = unknown>(): Promise<T>;
459
- /** Reads the response body as text. */
568
+ /** Waits for the body and reads it as text. */
460
569
  text(): Promise<string>;
461
570
  }
462
571
  ```
@@ -464,7 +573,8 @@ export interface WebResponse {
464
573
  ### Cookies and viewport
465
574
 
466
575
  `setCookies` validates every cookie before setting any; an unparseable
467
- target or a forbidden scheme is `POLICY_DENIED` and nothing is set. For the
576
+ target or a forbidden scheme is `POLICY_DENIED` and nothing is set. A
577
+ relative `url` resolves against the base URL, as `goto` resolves it. For the
468
578
  `domain` form the scheme follows the base URL and a leading `.` is stripped.
469
579
  `setViewport` records the new size on the report. It works before the first
470
580
  navigation too, as Playwright's `setViewportSize` does: the next `app.open`
@@ -586,7 +696,7 @@ cannot reuse node IDs or publish handles over a newer observation.
586
696
  `BrowserProvider`, `BrowserProviderScope`, `BrowserRequest`, `BrowserLease`,
587
697
  `BrowserReleaseContext`, `BrowserProviderDownloads`, `BrowserDownloadContext`,
588
698
  `PlaywrightLiveSurface`, `Browser`, `FrameScreen`,
589
- `BrowserExpectation`, `WebRoute`, `RouteFulfillResponse`, `WebResponse`,
699
+ `BrowserExpectation`, `WebRoute`, `RouteFulfillResponse`, `RouteContinueOverrides`, `WebResponse`,
590
700
  `Cookie`, `CookieFields`, `Dialog`, and `DialogHandler` are exported from the
591
701
  package for typing config helpers and fixtures. The runtime exports are
592
702
  `web`, [`surfaceOf`](#surfaceof), and `test`, `describe`, `beforeEach`,
package/docs/security.mdx CHANGED
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  title: Security model
3
3
  description: What the model can access, how the runner handles secrets, and the limits of its protections.
4
+ icon: "shield-halved"
4
5
  ---
5
6
 
6
7
  Test code and configuration run with your operating-system permissions.
@@ -48,7 +49,7 @@ A tool call that breaks a rule fails with `POLICY_DENIED`:
48
49
 
49
50
  | Denied | Why |
50
51
  | --- | --- |
51
- | Navigation to `file:`, `data:`, or `javascript:` | These URL schemes are forbidden. |
52
+ | Navigation to any scheme but `http:` and `https:`, other than `about:blank` | `file:`, `data:`, `javascript:`, and wrappers such as `view-source:file:` would load local or inline content. |
52
53
  | A malformed URL | Nothing is repaired. |
53
54
  | A `Secret` reaching a JSON sink | Handles carry no value, and no value may be serialized. |
54
55
  | Reading a value, text, or attribute from a secure field, with a getter or a `toHaveValue`, `toHaveText`, `toContainText`, or `toHaveAttribute` matcher | Assert the outcome, not the secret. A withheld value is redacted, not empty or absent. |
@@ -77,10 +78,12 @@ list. Shared hosting domains such as `vercel.app` count as one site, so
77
78
  headers can reach other hosts under that domain. `basicAuth` is broader:
78
79
  it answers a `401` challenge from any origin.
79
80
 
80
- The runner checks forbidden schemes on explicit navigation: `app.open`, the
81
- agent's `navigate` verb, and `device.openLink`. `device.openApp` and the
82
- `open_app` tool take an app id and refuse a link, since the device would
83
- open one as a URL. The runner does not re-check redirects or attach to popup
81
+ The runner checks the scheme on explicit navigation. `app.open`,
82
+ `browser.goto`, and the agent's `navigate` verb admit `http:`, `https:`,
83
+ and the exact `about:blank`. A device link may use an app's custom scheme, so `device.openLink`
84
+ refuses a list instead: `file:`, `data:`, `javascript:`, `view-source:`,
85
+ `blob:`, and `filesystem:`. `device.openApp` and the `open_app` tool take an
86
+ app id and refuse a link, since the device would open one as a URL. The runner does not re-check redirects or attach to popup
84
87
  pages. Register `browser.onDialog` to handle browser dialogs.
85
88
 
86
89
  ## Secrets
@@ -104,7 +107,9 @@ redaction and disables pixels for the rest of the attempt.
104
107
  After any secret fill, for the rest of the attempt, screenshot and pixel
105
108
  tools are unavailable, `app.screenshot()` is denied, and the runner omits
106
109
  assertion and failure screenshots. The app could display the value anywhere,
107
- beyond the field's masked rectangle.
110
+ beyond the field's masked rectangle. An `act` or `vision: true` step whose
111
+ semantic capture times out before the step does fails with `POLICY_DENIED`
112
+ instead of reading a fallback screenshot.
108
113
 
109
114
  A test that restores a [session](#sessions) whose setup filled a secret keeps
110
115
  this protection. A setup that signs in without filling a secret, for example
@@ -128,13 +133,14 @@ password on screen is not masked in pixels.
128
133
  ### Redaction
129
134
 
130
135
  The runner replaces every occurrence of a registered secret value with
131
- `<secret:name>` in model input, reports, a worker process's console output,
132
- and the trace text of a session a secret was filled on or an engine option
133
- holds. That includes a value the test passes as a plain string rather than
134
- a `secrets.get()` handle: test and `describe` titles, step labels (an
135
- `app.open` URL, a locator's text), an `agent.act` or `agent.assert`
136
- instruction, the strings in `agent.act` params, `agentContext`, and an
137
- `e2e explore` goal. Titles are redacted as the test file is collected, so
136
+ `<secret:name>` in model input, reports, what your code prints during
137
+ `e2e run` (tests, the config's and test files' top-level code, reporters),
138
+ trace text, and text downloads. That includes a value the test passes as a
139
+ plain string rather than a `secrets.get()` handle: test and `describe`
140
+ titles, step labels (an `app.open` URL, a locator's text), a `fill`, an
141
+ `agent.act` or `agent.assert` instruction, the strings in `agent.act`
142
+ params, `agentContext`, and an `e2e explore` goal. A plain string is not a
143
+ secret fill: it is protected as text only, and screenshots stay available. Titles are redacted as the test file is collected, so
138
144
  the test id, `--grep`, the reporters, and the artifact and failure-page file
139
145
  names all see `<secret:name>`, and two titles in one file that differ only
140
146
  in a secret value are a duplicate title path. A custom executor's
@@ -145,9 +151,16 @@ Matching covers the value as written, JSON-escaped, HTML-escaped, and
145
151
  percent-encoded, in any letter case (so a CSS `text-transform` does not hide
146
152
  it), and with its whitespace collapsed. Text cut at a length limit is masked
147
153
  too when it ends with the first 8 or more characters of a value (half of a
148
- value shorter than 16). Redaction rewrites only what the runner reports;
149
- assertions still compare the text the page shows. Secret values never enter
150
- cache entries.
154
+ value shorter than 16), as written or with its whitespace collapsed, the way
155
+ an engine collapses text before it cuts it. Every string of the observed
156
+ screen, names, text, values, test ids, attributes, selectors, and frame
157
+ paths alike, is redacted once, before the model, an executor's tree, a
158
+ replay cache descriptor, or an end anchor reads it. Redaction rewrites only
159
+ what the runner reports; assertions still compare the text the page shows.
160
+ Secret values and their cut parts never enter cache entries.
161
+
162
+ Output printed while the config loads appears once it has loaded. If the
163
+ load fails, that output is withheld, since the config's secrets are unknown.
151
164
 
152
165
  A static value shorter than 6 characters is `INVALID_CONFIG`, and a provider
153
166
  returning one fails the fill: every occurrence of so short a value would take
@@ -161,40 +174,39 @@ ordinary text with it.
161
174
  a hash, is a different string and passes through.
162
175
  - A value the app renders in another Unicode normalization form, decomposed
163
176
  accents where the secret has composed ones, passes through.
164
- - Text that widens the whitespace inside the value, or drops it, passes
165
- through.
177
+ - Text that drops the whitespace inside the value passes through. Text that
178
+ widens it passes through too, except on the observed screen: a field whose
179
+ collapsed form holds the value is kept collapsed and masked.
166
180
  - A value spread over several nodes, one character per cell as a PIN pad
167
181
  shows it, is not one string in the tree and passes through.
168
- - Text cut at a length limit is matched by its prefix only in the value's
169
- own form: a case mapping that changes length (`ß` shown as `SS`) or
170
- whitespace collapsed to one space escapes the prefix rule.
182
+ - A case mapping that changes length (`ß` shown as `SS`) escapes the prefix
183
+ rule for text cut at a length limit.
171
184
  - A title is redacted when its file is collected, before any provider
172
185
  runs, so a value a secret provider returns is not redacted from titles.
173
- - A Playwright trace from a session no secret reached is kept as recorded,
174
- so a value the test only spelled into a URL or a locator stays in its
175
- text.
176
186
  - When tests run in the runner's own process, with the config or tests
177
187
  passed in memory by an embedding host, their console output is not
178
188
  redacted.
179
- - A Playwright trace from a session a secret reached, filled or held by the
180
- engine, is rewritten before it is kept: any run of 8 or more characters of
181
- a value becomes `<secret:name>`, so unrelated text that shares 8 characters
182
- with a value is masked too. A base64 or base64url run that decodes to a
183
- value or such a fragment (a basic-auth header, a cookie, a token segment)
184
- is replaced whole. After a fill, screencast frames are dropped; with only
185
- an engine-held secret they are kept. An image the app served that
186
- draws the secret survives. A trace the runner cannot rewrite is deleted and
189
+ - A Playwright trace is rewritten before it is kept whenever its session
190
+ knows a secret value, filled or not: every static one, and a provider's
191
+ once the session resolved it. Any run of 8 or more characters of a value,
192
+ its whitespace collapsed or not, becomes `<secret:name>`, so unrelated
193
+ text that shares 8 characters with a value is masked too. A base64 or
194
+ base64url run that decodes to a value or such a fragment (a basic-auth
195
+ header, a cookie, a token segment) is replaced whole. After a fill,
196
+ screencast frames are dropped; otherwise they are kept. An image the app
197
+ served that draws the secret survives. A trace the runner cannot rewrite is deleted and
187
198
  the attempt records `TRACE_WITHHELD`.
188
199
  - A video masks nothing and is labeled `redaction: "incomplete"`. It is
189
200
  never recorded unless you ask. A recording a hosted browser or
190
201
  device service keeps stays with that service; the report holds only its
191
202
  URL.
192
- - A download is what the app served. Once a secret reached the session,
193
- filled or held by the engine, a text download (`text/*` or JSON by file
194
- extension) is rewritten the way trace text is and labeled
195
- `redaction: "complete"`. Any other download, a binary or one from a
196
- session no secret reached, is kept as served and labeled
197
- `redaction: "incomplete"`.
203
+ - A download is what the app served. When the session knows a secret
204
+ value, every whole occurrence of it in a text download (`text/*` or JSON
205
+ by file extension) is replaced, as in reports, and the download is labeled
206
+ `redaction: "complete"`. Fragments and base64 runs are matched in traces
207
+ only. Any other download, a binary, one that is not valid UTF-8 or cannot
208
+ be rewritten, or one from a session that knows no secret value, is kept
209
+ as served and labeled `redaction: "incomplete"`.
198
210
  - The `headers` a protected preview needs are recorded in the trace as
199
211
  request headers. Share that trace as you would the bypass secret.
200
212
  - `command.log` is captured as the process writes it.
@@ -230,8 +242,12 @@ actions without consulting a model and are not signed.
230
242
 
231
243
  The runner validates entries before use. Invalid entries become misses.
232
244
  Files are named by a hash of the expected key and refused if larger than
233
- 1 MiB. Entries store actions and a redacted summary; replay executes only
234
- the actions. Secret fills store a name and authorize the fill again.
245
+ 1 MiB. The key holds the agent's context only as a hash of its redacted
246
+ text, so no secret value enters it. Entries store actions and a redacted
247
+ summary; replay executes only the actions. Secret fills store a name and
248
+ authorize the fill again. An entry recorded by one agent never replays for
249
+ another, and one recorded on the app's origin never replays on a page from
250
+ another origin.
235
251
 
236
252
  `e2e init` ignores `.e2e/cache/` in Git. If you choose to
237
253
  [commit recordings](/cache#commit-the-replay-cache), review their actions and typed
@@ -248,9 +264,9 @@ project-relative path you configure.
248
264
  | --- | --- | --- |
249
265
  | `report.json` | The run record. Errors, reasoning excerpts, and action summaries pass the redactor. | complete |
250
266
  | `artifacts/.../*.png` | Screenshots. Secure fields are masked before screenshots are returned or saved as artifacts. | complete |
251
- | `artifacts/.../trace.zip` | Playwright trace, rewritten after a secret fill or when an engine option holds a secret; screencast frames are dropped after a fill only. | not-required, complete, or deleted |
267
+ | `artifacts/.../trace.zip` | Playwright trace, rewritten whenever its session knows a secret value; screencast frames are dropped after a fill only. | not-required (no secret value), complete, or deleted |
252
268
  | `artifacts/.../*.webm`, `*.mp4` | Video, opt-in through `video` or `--video`; `retain-on-failure` keeps only the attempts that did not pass. | incomplete |
253
- | `artifacts/.../downloads/*` | Files the app served through a download. Text ones are rewritten after a secret fill or when an engine option holds a secret; the rest are kept as served. | complete or incomplete |
269
+ | `artifacts/.../downloads/*` | Files the app served through a download. Text ones are rewritten whenever the session knows a secret value; the rest are kept as served. | complete or incomplete |
254
270
  | `artifacts/.../failure/screen.txt` | The redacted tree at the moment of failure. | complete |
255
271
  | `ai-trace.json` | Every model call, opt-in via `--ai-trace`. Prompts are the redacted ones the model saw; inline image bytes are replaced by their size. | as the prompt |
256
272
  | `sessions/<runId>/` | Encrypted sessions, deleted at cleanup. | encrypted |
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  title: Subscriptions
3
- description: Use your ChatGPT, GitHub Copilot, or SuperGrok subscription for agent steps.
3
+ description: Use your ChatGPT, GitHub Copilot, OpenCode Console, or SuperGrok subscription for agent steps.
4
+ icon: "id-card"
4
5
  ---
5
6
 
6
7
  Use an existing subscription without a separate API key. Choose your plan
@@ -10,6 +11,7 @@ in `e2e init`, install dependencies, then sign in:
10
11
  | --- | --- |
11
12
  | ChatGPT Plus or Pro | `npx e2e login openai` |
12
13
  | GitHub Copilot | `npx e2e login github-copilot` |
14
+ | OpenCode Console (OpenCode Zen and OpenCode Go) | `npx e2e login opencode-console` |
13
15
  | SuperGrok or X Premium+ | `npx e2e login spacexai` |
14
16
 
15
17
  Follow the terminal prompts, then run `npx e2e run`. Agent calls use your
@@ -52,17 +54,17 @@ const agent = { model: chatgpt('gpt-6-luna') };
52
54
  <Tab title="GitHub Copilot">
53
55
  <CodeGroup>
54
56
  ```bash npm
55
- npm install -D ai @ai-sdk/openai-compatible
57
+ npm install -D ai @ai-sdk/openai-compatible @ai-sdk/openai
56
58
  npx e2e login github-copilot
57
59
  ```
58
60
 
59
61
  ```bash pnpm
60
- pnpm add -D ai @ai-sdk/openai-compatible
62
+ pnpm add -D ai @ai-sdk/openai-compatible @ai-sdk/openai
61
63
  pnpm exec e2e login github-copilot
62
64
  ```
63
65
 
64
66
  ```bash bun
65
- bun add -d ai @ai-sdk/openai-compatible
67
+ bun add -d ai @ai-sdk/openai-compatible @ai-sdk/openai
66
68
  bunx e2e login github-copilot
67
69
  ```
68
70
  </CodeGroup>
@@ -71,6 +73,33 @@ bunx e2e login github-copilot
71
73
  import { copilot } from 'e2e/oauth/copilot';
72
74
 
73
75
  const agent = { model: copilot('claude-sonnet-5') };
76
+ ```
77
+ </Tab>
78
+ <Tab title="OpenCode Console">
79
+ <CodeGroup>
80
+ ```bash npm
81
+ npm install -D ai @ai-sdk/openai-compatible @ai-sdk/openai @ai-sdk/anthropic @ai-sdk/google
82
+ npx e2e login opencode-console
83
+ ```
84
+
85
+ ```bash pnpm
86
+ pnpm add -D ai @ai-sdk/openai-compatible @ai-sdk/openai @ai-sdk/anthropic @ai-sdk/google
87
+ pnpm exec e2e login opencode-console
88
+ ```
89
+
90
+ ```bash bun
91
+ bun add -d ai @ai-sdk/openai-compatible @ai-sdk/openai @ai-sdk/anthropic @ai-sdk/google
92
+ bunx e2e login opencode-console
93
+ ```
94
+ </CodeGroup>
95
+
96
+ ```ts title="Model setup"
97
+ import { opencodeConsole } from 'e2e/oauth/opencode-console';
98
+
99
+ // OpenCode Zen, billed to the workspace's credits
100
+ const agent = { model: opencodeConsole('deepseek-v4.1-flash') };
101
+ // OpenCode Go, billed to your Go subscription
102
+ const goAgent = { model: opencodeConsole('go/deepseek-v4.1-flash') };
74
103
  ```
75
104
  </Tab>
76
105
  <Tab title="SuperGrok">
@@ -124,20 +153,49 @@ bunx e2e models openai # one provider
124
153
  ```
125
154
  </CodeGroup>
126
155
 
127
- Each line is the id to pass to `chatgpt()`, `copilot()`, or `grok()`, the
128
- vendor's display name, and what it says about the model: reasoning levels,
129
- vision, preview, or hidden. ChatGPT lists what the Codex backend serves your
130
- plan; Copilot lists the chat models of your plan; SuperGrok lists the
131
- language models the token reaches.
156
+ Each line is the id to pass to `chatgpt()`, `copilot()`, `opencodeConsole()`,
157
+ or `grok()`, the vendor's display name, and what it says about the model:
158
+ reasoning levels, vision, preview, or hidden. ChatGPT lists what the Codex
159
+ backend serves your plan; Copilot lists the chat models of your plan;
160
+ OpenCode Console lists the Zen and Go models your workspace has enabled,
161
+ each tagged with the plan that pays for it, and leaves out custom providers; SuperGrok lists the language models the
162
+ token reaches.
132
163
 
133
164
  Copilot also lists models that `copilot()` cannot call, and marks them:
134
165
 
135
166
  - `not enabled`: the model is off for your plan. Turn it on in your Copilot
136
167
  settings on GitHub, or ask your organization admin if they manage your
137
168
  seat.
138
- - `no chat completions`: Copilot serves the model only over other APIs,
139
- mostly its Responses API. `copilot()` uses chat completions, so pick
140
- another model.
169
+ - `no chat or responses`: Copilot serves the model only over APIs `copilot()`
170
+ does not speak, such as Anthropic's Messages API, so pick another model.
171
+
172
+ On its first call `copilot()` picks the endpoint from the model's entry in
173
+ this same listing: chat completions for most models, Copilot's Responses API
174
+ for the ones served only there. When the listing cannot be read, that call
175
+ goes over chat completions and the next call reads it again. The Responses
176
+ call runs through `@ai-sdk/openai`, so install it beside
177
+ `@ai-sdk/openai-compatible` to use those models; `e2e init` adds both.
178
+
179
+ ### OpenCode Console
180
+
181
+ One OpenCode Console sign-in serves both plans billed through it:
182
+
183
+ - A bare id, such as `deepseek-v4.1-flash`, is an OpenCode Zen model, paid from
184
+ the workspace's credits.
185
+ - A `go/` id, such as `go/deepseek-v4.1-flash`, is an OpenCode Go model, paid
186
+ from the Go subscription. Go models are listed only when you hold the
187
+ workspace's Go subscription.
188
+
189
+ The list leaves out models your workspace has turned off, and Zen's free
190
+ models, which OpenCode keeps for its own clients.
191
+
192
+ On its first call `opencodeConsole()` reads the workspace config to pick the
193
+ model's API: chat completions, OpenAI Responses (`@ai-sdk/openai`), Anthropic
194
+ Messages (`@ai-sdk/anthropic`), or Google (`@ai-sdk/google`). Install those
195
+ beside `@ai-sdk/openai-compatible`; `e2e init` adds all four. Set
196
+ `providerOptions` under the key of that API: `opencode` for chat completions
197
+ models, on Zen and Go alike, and `openai`, `anthropic`, or `google` for the
198
+ others.
141
199
 
142
200
  ## Sign-in help
143
201
 
@@ -146,6 +204,9 @@ Copilot also lists models that `copilot()` cannot call, and marks them:
146
204
  - Copilot reuses your GitHub CLI login. If you do not use `gh`, pass
147
205
  `--client-id` for your own GitHub OAuth app with device flow enabled.
148
206
  Use `--enterprise-url` for GitHub Enterprise.
207
+ - OpenCode Console prints a device code and opens the page where you approve
208
+ it and pick the workspace; the page shows e2e as an unknown client. To
209
+ switch workspaces, run `npx e2e logout opencode-console` and sign in again.
149
210
  - SuperGrok prints a device code and the URL where you enter it.
150
211
 
151
212
  Run `npx e2e login` to choose a provider and see which accounts are signed
@@ -164,3 +225,22 @@ instead of the local file, but refreshed tokens are not saved back to that
164
225
  variable. A value containing a rotating refresh token cannot be reused
165
226
  indefinitely. See [CLI reference](/reference/cli#e2e-login) for all sign-in
166
227
  options.
228
+
229
+ For OpenCode Console in CI, create a service account API key in the Console
230
+ (Service Accounts, then API Keys) and set it as `OPENCODE_API_KEY`;
231
+ `opencodeConsole()` uses it instead of the stored login. `e2e models` still
232
+ reads the stored login.
233
+
234
+ <CodeGroup>
235
+ ```bash npm
236
+ OPENCODE_API_KEY=oc_sk_... npx e2e run
237
+ ```
238
+
239
+ ```bash pnpm
240
+ OPENCODE_API_KEY=oc_sk_... pnpm exec e2e run
241
+ ```
242
+
243
+ ```bash bun
244
+ OPENCODE_API_KEY=oc_sk_... bunx e2e run
245
+ ```
246
+ </CodeGroup>