e2e 0.21.1 → 0.21.2

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 (133) hide show
  1. package/README.md +2 -0
  2. package/dist/.build.json +1 -1
  3. package/dist/agent/act-validation.js +1 -0
  4. package/dist/agent/act.js +7 -3
  5. package/dist/agent/action-dispatcher.js +30 -8
  6. package/dist/agent/actions.d.ts +2 -0
  7. package/dist/agent/actions.js +5 -2
  8. package/dist/agent/error.js +1 -0
  9. package/dist/agent/executor.d.ts +14 -6
  10. package/dist/agent/index.js +5 -1
  11. package/dist/agent/invocation.d.ts +3 -0
  12. package/dist/agent/model/sdk.d.ts +5 -0
  13. package/dist/agent/model/sdk.js +18 -1
  14. package/dist/agent/observation.js +1 -0
  15. package/dist/agent/primitives.js +14 -3
  16. package/dist/agent/replay.js +9 -3
  17. package/dist/agent/step-cache.d.ts +3 -2
  18. package/dist/agent/step-cache.js +7 -4
  19. package/dist/agent/tool-loop.js +4 -1
  20. package/dist/cache/context.js +1 -1
  21. package/dist/cache/recorder.d.ts +2 -1
  22. package/dist/cache/recorder.js +11 -2
  23. package/dist/cache/trace-fields.d.ts +10 -0
  24. package/dist/cache/trace-fields.js +17 -0
  25. package/dist/cache/trace.d.ts +9 -1
  26. package/dist/cache/trace.js +26 -7
  27. package/dist/cli/feedback.js +2 -1
  28. package/dist/cli/init/sibling-versions.json +1 -1
  29. package/dist/collect/select.js +12 -2
  30. package/dist/config/agent.d.ts +1 -1
  31. package/dist/config/diagnose.js +9 -3
  32. package/dist/config/esm-hooks.js +3 -2
  33. package/dist/config/resolve.js +3 -2
  34. package/dist/config/typescript.js +52 -2
  35. package/dist/engine/contract.d.ts +3 -2
  36. package/dist/engine/manifest.js +2 -2
  37. package/dist/internal/errors.js +5 -2
  38. package/dist/internal/source.js +2 -0
  39. package/dist/internal/unserved-model.d.ts +5 -0
  40. package/dist/internal/unserved-model.js +22 -0
  41. package/dist/locator/screen.js +1 -2
  42. package/dist/oauth/copilot.js +23 -5
  43. package/dist/oauth/device-code.js +35 -19
  44. package/dist/oauth/errors.d.ts +3 -0
  45. package/dist/oauth/errors.js +26 -11
  46. package/dist/oauth/providers/github-copilot.d.ts +4 -0
  47. package/dist/oauth/providers/github-copilot.js +64 -16
  48. package/dist/oauth/providers/openai.d.ts +2 -2
  49. package/dist/oauth/providers/openai.js +47 -19
  50. package/dist/oauth/providers/opencode-console.js +36 -15
  51. package/dist/oauth/providers/xai.js +18 -9
  52. package/dist/oauth/token-endpoint.d.ts +15 -6
  53. package/dist/oauth/token-endpoint.js +18 -6
  54. package/dist/report/code-frame.js +9 -3
  55. package/dist/report/failure-text.d.ts +10 -2
  56. package/dist/report/failure-text.js +24 -8
  57. package/dist/report/list-model.d.ts +13 -0
  58. package/dist/report/list-steps.d.ts +6 -1
  59. package/dist/report/list-steps.js +29 -0
  60. package/dist/report/list.d.ts +1 -0
  61. package/dist/report/list.js +16 -7
  62. package/dist/report/live-window.js +2 -1
  63. package/dist/report/markdown.js +1 -1
  64. package/dist/run/fixtures.js +2 -0
  65. package/dist/run/host-trace.d.ts +3 -2
  66. package/dist/run/host-trace.js +2 -1
  67. package/dist/run/last-run.d.ts +9 -0
  68. package/dist/run/last-run.js +28 -0
  69. package/dist/run/open-session.d.ts +3 -1
  70. package/dist/run/open-session.js +1 -1
  71. package/dist/run/runner.js +6 -2
  72. package/dist/run/sessions.js +3 -3
  73. package/dist/run/standalone-attempt.d.ts +2 -1
  74. package/dist/run/standalone-attempt.js +3 -1
  75. package/dist/run/steps.d.ts +3 -1
  76. package/dist/run/units.d.ts +8 -0
  77. package/dist/run/units.js +14 -0
  78. package/dist/run/worker-models.d.ts +2 -0
  79. package/dist/run/worker-models.js +7 -0
  80. package/dist/types.d.ts +2 -2
  81. package/docs/authentication.mdx +26 -23
  82. package/docs/browser.mdx +7 -4
  83. package/docs/bug-bash.mdx +2 -2
  84. package/docs/cache.mdx +6 -1
  85. package/docs/ci/bitrise.mdx +1 -1
  86. package/docs/ci/codemagic.mdx +2 -2
  87. package/docs/ci/eas-workflows.mdx +1 -1
  88. package/docs/coding-agents.mdx +7 -35
  89. package/docs/debugging.mdx +24 -36
  90. package/docs/executors.mdx +1 -1
  91. package/docs/github.mdx +2 -1
  92. package/docs/integrations/eas.mdx +1 -1
  93. package/docs/integrations/index.mdx +6 -0
  94. package/docs/integrations/lightpanda.mdx +192 -0
  95. package/docs/integrations/testmu.mdx +152 -0
  96. package/docs/mcp.mdx +123 -0
  97. package/docs/migrate/detox.mdx +1 -1
  98. package/docs/migrate/maestro.mdx +101 -105
  99. package/docs/migrate/playwright.mdx +65 -70
  100. package/docs/mobile.mdx +18 -3
  101. package/docs/models.mdx +4 -2
  102. package/docs/quickstart.mdx +7 -0
  103. package/docs/reference/agent.mdx +369 -277
  104. package/docs/reference/app.mdx +141 -32
  105. package/docs/reference/cli.mdx +674 -558
  106. package/docs/reference/config.mdx +1406 -752
  107. package/docs/reference/engine.mdx +1062 -256
  108. package/docs/reference/environment.mdx +156 -84
  109. package/docs/reference/errors.mdx +106 -72
  110. package/docs/reference/expect.mdx +752 -372
  111. package/docs/reference/mcp.mdx +366 -141
  112. package/docs/reference/mobile.mdx +844 -403
  113. package/docs/reference/reporters.mdx +312 -106
  114. package/docs/reference/runner.mdx +373 -174
  115. package/docs/reference/screen.mdx +931 -398
  116. package/docs/reference/security.mdx +106 -40
  117. package/docs/reference/test.mdx +626 -273
  118. package/docs/reference/web.mdx +956 -533
  119. package/docs/security.mdx +6 -5
  120. package/docs/snippets/model-provider.jsx +763 -0
  121. package/docs/snippets/setup-prompt-card.jsx +22 -0
  122. package/docs/snippets/show-more.jsx +22 -0
  123. package/docs/snippets/video-card.jsx +34 -0
  124. package/docs/snippets/video.jsx +25 -0
  125. package/docs/subscriptions.mdx +10 -3
  126. package/docs/telemetry.mdx +39 -39
  127. package/package.json +3 -2
  128. package/schema/report-v1.schema.json +6 -0
  129. package/skills/e2e/references/agent.md +8 -3
  130. package/skills/e2e/references/debugging.md +1 -1
  131. package/skills/e2e/references/mcp.md +1 -1
  132. package/skills/e2e/references/setup.md +12 -3
  133. package/skills/e2e/references/writing-tests.md +1 -1
@@ -10,7 +10,15 @@ These variables configure the runner and first-party packages. In the tables,
10
10
  `set` means the variable exists, `value` means it is not blank, and `flag`
11
11
  means it is not blank, `0`, or `false`. Flag values are case-insensitive.
12
12
 
13
- ## Model
13
+ ```bash
14
+ export E2E_USER_ADMIN_PASSWORD="$ADMIN_PASSWORD"
15
+ export E2E_CACHE_DIR=~/.cache/e2e
16
+ export E2E_TELEMETRY_DISABLED=1
17
+ ```
18
+
19
+ ## Reference
20
+
21
+ ### Model
14
22
 
15
23
  Configure a model in `e2e.config.ts`. Its provider reads the API key:
16
24
  `gateway()` uses `AI_GATEWAY_API_KEY`, `openrouter()` uses
@@ -21,109 +29,173 @@ credential fails on the first model call. Subscription models use a stored
21
29
  login instead.
22
30
  See [Models](/models) and [Subscriptions](/subscriptions).
23
31
 
24
- | Variable | Reading | Effect |
25
- | --- | --- | --- |
26
- | `E2E_OAUTH_CREDENTIALS` | value | The stored subscription logins as JSON, the contents of `oauth.json`, for a machine nobody can sign in on. The `e2e/oauth/*` models and `e2e models` read it in place of `$XDG_CONFIG_HOME/e2e/oauth.json`; `e2e login` and `e2e logout` refuse to change it, and refreshed tokens are not written back |
27
- | `OPENCODE_API_KEY` | value | An OpenCode Console service account API key or an OpenCode Go subscription API key; `opencodeConsole()` and `e2e models opencode-console` use it in place of the stored `opencode-console` login. OpenCode reads the same variable |
28
- | `XDG_CONFIG_HOME` | value | Directory of the stored logins, `$XDG_CONFIG_HOME/e2e/oauth.json`; default `~/.config/e2e/oauth.json` |
29
-
30
- ## Credentials and secrets
31
-
32
- | Variable | Reading | Effect |
33
- | --- | --- | --- |
34
- | `E2E_USER_<NAME>_USERNAME` | set | Replaces `credentials.<name>.username` |
35
- | `E2E_USER_<NAME>_PASSWORD` | set | Replaces `credentials.<name>.password` |
36
- | `E2E_SECRET_<NAME>` | set | Replaces the value of `secrets.<name>` |
37
-
38
- `<NAME>` is the name uppercased with every character outside `A-Z0-9`
39
- replaced by `_`, so `power-user` becomes `E2E_USER_POWER_USER_USERNAME`. Two
40
- credentials, or two secrets, whose names map to the same `<NAME>` (`api-key`
41
- and `api_key` both read `E2E_SECRET_API_KEY`; `svc-a` and `svc_a` both read
42
- `E2E_USER_SVC_A_USERNAME` and `_PASSWORD`) are `INVALID_CONFIG` at config
43
- load, whether or not a variable is set, so one rotation never replaces two
44
- entries; the message names the entries and the variables. A credential and a
45
- secret may share a name, since their prefixes differ. A
46
- variable for a name absent from the config is ignored. An empty value is
47
- `INVALID_CONFIG`, and so is a password or secret shorter than 6 characters
48
- (code points): redaction rewrites every occurrence of a value, and one that
49
- short would take ordinary text with it.
50
-
51
- ## Run mode
52
-
53
- | Variable | Reading | Effect |
54
- | --- | --- | --- |
55
- | `CI` | flag | CI mode: `retries` defaults to 1 and `workers` to 1, `cache` defaults to `read-only`, a focused test is `ONLY_IN_CI`, and `reuseExisting` on an app command is ignored |
56
-
57
- ## Replay cache
58
-
59
- | Variable | Reading | Effect |
60
- | --- | --- | --- |
61
- | `E2E_CACHE_DIR` | value | Replaces [`cache.dir`](/reference/config#cache) for `e2e run` and `e2e cache`, resolved from the project root; `~` or a leading `~/` is the home directory. `--cache-dir` wins over it, a custom `cache.store` ignores it, the cache mode stays as configured, and `--no-cache` still turns the cache off |
62
-
63
- ## Diagnostics
64
-
65
- | Variable | Reading | Effect |
66
- | --- | --- | --- |
67
- | `NO_COLOR` | set | Plain CLI output. Colors are otherwise on for a TTY or in CI and off when piped |
68
- | `FORCE_COLOR` | set | Colored CLI output even when piped |
69
- | `TERM` | value | `dumb` draws the `e2e init` wordmark at rest, without animation, as in CI |
70
- | `GITHUB_SHA`, `GITHUB_HEAD_REF`, `GITHUB_REF_NAME` | value | The commit and branch the report records under `run.vcs` when `git` cannot read the project root. `GITHUB_HEAD_REF` wins over `GITHUB_REF_NAME` |
32
+ | Variable | Example | Reading | Effect |
33
+ | --- | --- | --- | --- |
34
+ | [`E2E_OAUTH_CREDENTIALS`](#e2e_oauth_credentials) | `E2E_OAUTH_CREDENTIALS="$(cat oauth.json)"` | value | The stored subscription logins as JSON |
35
+ | [`OPENCODE_API_KEY`](#opencode_api_key) | `OPENCODE_API_KEY=<key>` | value | Replaces the stored `opencode-console` login |
36
+ | `XDG_CONFIG_HOME` | `XDG_CONFIG_HOME=~/.config` | value | Directory of the stored logins, `$XDG_CONFIG_HOME/e2e/oauth.json`; default `~/.config/e2e/oauth.json` |
37
+
38
+ #### `E2E_OAUTH_CREDENTIALS`
39
+
40
+ The stored subscription logins as JSON, the contents of `oauth.json`, for a
41
+ machine nobody can sign in on.
42
+
43
+ - The `e2e/oauth/*` models and `e2e models` read it in place of
44
+ `$XDG_CONFIG_HOME/e2e/oauth.json`.
45
+ - `e2e login` and `e2e logout` refuse to change it.
46
+ - Refreshed tokens are not written back.
47
+
48
+ #### `OPENCODE_API_KEY`
49
+
50
+ An OpenCode Console service account API key or an OpenCode Go subscription
51
+ API key. `opencodeConsole()` and `e2e models opencode-console` use it in place
52
+ of the stored `opencode-console` login. OpenCode reads the same variable.
53
+
54
+ ### Credentials and secrets
55
+
56
+ | Variable | Example | Reading | Effect |
57
+ | --- | --- | --- | --- |
58
+ | `E2E_USER_<NAME>_USERNAME` | `E2E_USER_ADMIN_USERNAME=admin@example.test` | set | Replaces `credentials.<name>.username` |
59
+ | `E2E_USER_<NAME>_PASSWORD` | `E2E_USER_ADMIN_PASSWORD="$ADMIN_PASSWORD"` | set | Replaces `credentials.<name>.password` |
60
+ | `E2E_SECRET_<NAME>` | `E2E_SECRET_API_KEY="$API_KEY"` | set | Replaces the value of `secrets.<name>` |
61
+
62
+ - `<NAME>` is the name uppercased with every character outside `A-Z0-9`
63
+ replaced by `_`, so `power-user` becomes `E2E_USER_POWER_USER_USERNAME`.
64
+ - Two credentials, or two secrets, whose names map to one `<NAME>` are
65
+ `INVALID_CONFIG` at config load, set or not. So one rotation never
66
+ replaces two entries. The message names the entries and the variables.
67
+ For example, `api-key` and `api_key` both read `E2E_SECRET_API_KEY`;
68
+ `svc-a` and `svc_a` both read `E2E_USER_SVC_A_USERNAME` and `_PASSWORD`.
69
+ - A credential and a secret may share a name, since their prefixes differ.
70
+ - A variable for a name absent from the config is ignored.
71
+ - An empty password or secret is `INVALID_CONFIG`, and so is one shorter
72
+ than 6 characters (code points). Redaction rewrites every occurrence of a
73
+ value, and one that short would take ordinary text with it. An empty
74
+ username is accepted.
75
+
76
+ ### Run mode
77
+
78
+ | Variable | Example | Reading | Effect |
79
+ | --- | --- | --- | --- |
80
+ | [`CI`](#ci) | `CI=1` | flag | CI mode |
81
+
82
+ #### `CI`
83
+
84
+ Turns on CI mode:
85
+
86
+ - `retries` defaults to 1 and `workers` to 1.
87
+ - `cache` defaults to `read-only`.
88
+ - A focused test is `ONLY_IN_CI`.
89
+ - `reuseExisting` on an app command is ignored.
90
+
91
+ ### Replay cache
92
+
93
+ | Variable | Example | Reading | Effect |
94
+ | --- | --- | --- | --- |
95
+ | [`E2E_CACHE_DIR`](#e2e_cache_dir) | `E2E_CACHE_DIR=~/.cache/e2e` | value | Replaces [`cache.dir`](/reference/config#cache) |
96
+
97
+ #### `E2E_CACHE_DIR`
98
+
99
+ Replaces [`cache.dir`](/reference/config#cache) for `e2e run` and
100
+ `e2e cache`, resolved from the project root.
101
+
102
+ - `~` or a leading `~/` is the home directory.
103
+ - `--cache-dir` wins over it.
104
+ - A custom `cache.store` ignores it.
105
+ - The cache mode stays as configured, and `--no-cache` still turns the cache
106
+ off.
107
+
108
+ ### Diagnostics
109
+
110
+ | Variable | Example | Reading | Effect |
111
+ | --- | --- | --- | --- |
112
+ | `NO_COLOR` | `NO_COLOR=1` | set | Plain CLI output. Without it, colors are on for a TTY or in CI, off when piped |
113
+ | `FORCE_COLOR` | `FORCE_COLOR=1` | set | Colored CLI output even when piped |
114
+ | `TERM` | `TERM=dumb` | value | `dumb` draws the `e2e init` wordmark at rest, without animation, as in CI |
115
+ | `GITHUB_SHA`, `GITHUB_HEAD_REF`, `GITHUB_REF_NAME` | `GITHUB_REF_NAME=main` | value | The commit and branch the report records under `run.vcs` when `git` cannot read the project root. `GITHUB_HEAD_REF` wins over `GITHUB_REF_NAME` |
71
116
 
72
117
  `--debug` and `--ai-trace` are flags, not variables; see
73
118
  [CLI](/reference/cli#e2e-run).
74
119
 
75
- ## Telemetry
120
+ ### Telemetry
121
+
122
+ | Variable | Example | Reading | Effect |
123
+ | --- | --- | --- | --- |
124
+ | `E2E_TELEMETRY_DISABLED` | `E2E_TELEMETRY_DISABLED=1` | flag | Turns anonymous usage telemetry off. `e2e telemetry disable` saves the same choice in the preferences file |
125
+ | `DO_NOT_TRACK` | `DO_NOT_TRACK=1` | flag | Same as `E2E_TELEMETRY_DISABLED` |
126
+ | `E2E_TELEMETRY_DEBUG` | `E2E_TELEMETRY_DEBUG=1` | flag | Prints every telemetry event to stderr instead of sending it |
127
+ | [`E2E_TELEMETRY_FLEET`](#e2e_telemetry_fleet) | `E2E_TELEMETRY_FLEET=acme` | value | Names the platform running e2e for its users |
128
+ | `XDG_CONFIG_HOME` | `XDG_CONFIG_HOME=~/.config` | value | Directory of the preferences file, `$XDG_CONFIG_HOME/e2e`; default `~/.config/e2e` |
129
+ | `APPDATA` | `APPDATA=C:\Users\ada\AppData\Roaming` | value | The same on Windows, `%APPDATA%\e2e`; default `~/AppData/Roaming/e2e` |
130
+
131
+ #### `E2E_TELEMETRY_FLEET`
76
132
 
77
- | Variable | Reading | Effect |
78
- | --- | --- | --- |
79
- | `E2E_TELEMETRY_DISABLED` | flag | Turns anonymous usage telemetry off. `e2e telemetry disable` saves the same choice in the preferences file |
80
- | `DO_NOT_TRACK` | flag | Same as `E2E_TELEMETRY_DISABLED` |
81
- | `E2E_TELEMETRY_DEBUG` | flag | Prints every telemetry event to stderr instead of sending it |
82
- | `E2E_TELEMETRY_FLEET` | value | Names the platform running e2e for its users; events are attributed to `fleet:<name>` instead of a per-machine id, no preferences file is written, and no notice is printed. A value that is not a plain token is sent as `other` |
83
- | `XDG_CONFIG_HOME` | value | Directory of the preferences file, `$XDG_CONFIG_HOME/e2e`; default `~/.config/e2e` |
84
- | `APPDATA` | value | The same on Windows, `%APPDATA%\e2e`; default `~/AppData/Roaming/e2e` |
133
+ Names the platform running e2e for its users.
134
+
135
+ - Events are attributed to `fleet:<name>` instead of a per-machine id.
136
+ - No preferences file is written, and no notice is printed.
137
+ - A value that is not a plain token is sent as `other`.
138
+
139
+ #### Detected environments
85
140
 
86
141
  Telemetry also checks for the markers CI vendors and coding agents set. Only
87
142
  the derived name is sent.
88
143
 
89
144
  | Markers | Reported as |
90
145
  | --- | --- |
91
- | `GITHUB_ACTIONS`, `GITLAB_CI`, `CIRCLECI`, `BUILDKITE`, `JENKINS_URL`, `TRAVIS`, `BITBUCKET_BUILD_NUMBER`, `TF_BUILD`, `TEAMCITY_VERSION`, `CODEBUILD_BUILD_ID`, `VERCEL`, `NETLIFY`, `DRONE`, `SEMAPHORE`, `APPVEYOR`, `WOODPECKER` | `ci_name`: `github-actions`, `gitlab`, `circleci`, `buildkite`, `jenkins`, `travis`, `bitbucket`, `azure-pipelines`, `teamcity`, `aws-codebuild`, `vercel`, `netlify`, `drone`, `semaphore`, `appveyor`, `woodpecker`; `unknown` when only `CI` is set |
146
+ | `GITHUB_ACTIONS`, `GITLAB_CI`, `CIRCLECI`, `BUILDKITE`, `JENKINS_URL`, `TRAVIS`, `BITBUCKET_BUILD_NUMBER`, `TF_BUILD`, `TEAMCITY_VERSION`, `CODEBUILD_BUILD_ID`, `VERCEL`, `NETLIFY`, `DRONE`, `SEMAPHORE`, `APPVEYOR`, `WOODPECKER`, `EAS_BUILD`, `BITRISE_IO`, `CM_BUILD_ID`, `CI_XCODE_CLOUD` | `ci_name`: `github-actions`, `gitlab`, `circleci`, `buildkite`, `jenkins`, `travis`, `bitbucket`, `azure-pipelines`, `teamcity`, `aws-codebuild`, `vercel`, `netlify`, `drone`, `semaphore`, `appveyor`, `woodpecker`, `eas`, `bitrise`, `codemagic`, `xcode-cloud`; `unknown` when only `CI` is set |
92
147
  | `CLAUDECODE`, `CLAUDE_CODE`, `CURSOR_AGENT`, `CURSOR_TRACE_ID`, `CODEX_SANDBOX`, `CODEX_THREAD_ID`, `CODEX_CI`, `GEMINI_CLI`, `COPILOT_GITHUB_TOKEN`, `COPILOT_MODEL`, `COPILOT_ALLOW_ALL`, `OPENCODE_CLIENT`, `AUGMENT_AGENT`, `ANTIGRAVITY_AGENT`, `REPL_ID`, `AI_AGENT` | `coding_agent`: `claude-code`, `cursor`, `codex`, `gemini-cli`, `copilot`, `opencode`, `augment`, `antigravity`, `replit`, `other` |
93
148
  | `WSL_DISTRO_NAME` | `wsl: true` |
94
149
  | `npm_config_user_agent` | `package_manager`: the invoking package manager. `e2e init` reads the same variable to pick the install command |
95
150
 
96
151
  What is sent and how to read it is on the [Telemetry](/telemetry) page.
97
152
 
98
- ## Reporters
153
+ ### Reporters
99
154
 
100
155
  `@e2e-dev/github` reads the variables GitHub Actions sets, when a run finishes:
101
156
 
102
- | Variable | Reading | Effect |
103
- | --- | --- | --- |
104
- | `GITHUB_ACTIONS` | value | Without it the reporter posts nothing and says so |
105
- | `GITHUB_REPOSITORY`, `GITHUB_RUN_ID`, `GITHUB_EVENT_NAME`, `GITHUB_WORKFLOW`, `GITHUB_JOB` | value | Always set by the runner; a job missing one fails the reporter with the variable named. The repository and run id build the artifacts link, the event name is in the "not a pull request" row, and the workflow and job join the marker that lets a rerun find its own comment |
106
- | `GITHUB_TOKEN`, `GH_TOKEN` | value | The token the comment is posted with; the first one set wins. Unset, the job summary is still written |
107
- | `GITHUB_EVENT_PATH`, `GITHUB_REF` | value | The pull request number and head commit from the event payload, or the number from `refs/pull/N/merge` |
108
- | `GITHUB_SHA` | value | The commit source links point at when the event is not about a pull request |
109
- | `GITHUB_WORKSPACE` | value | The checkout root; source links add the project's path inside it, so a suite in a subdirectory links to the right file |
110
- | `GITHUB_SERVER_URL`, `GITHUB_API_URL` | value | The GitHub host, for Enterprise Server; `https://github.com` and `https://api.github.com` by default |
111
- | `GITHUB_STEP_SUMMARY` | value | The job summary file the comment text is appended to |
112
-
113
- ## Engines
114
-
115
- | Variable | Reading | Effect |
116
- | --- | --- | --- |
117
- | `PLAYWRIGHT_BROWSERS_PATH` | value | Playwright's browser cache, honored by the engine's `prepare` hook |
118
- | `PLAYWRIGHT_SKIP_BROWSER_GC` | value | Defaults to `1` for the engine's browser install, so it keeps other browsers in the cache. Set `0` to let the install remove them |
119
- | `KERNEL_API_KEY` | value | The API key `kernel()` from `@e2e-dev/kernel` creates browsers with; unset, the browser lease fails. See [Kernel](/integrations/kernel) |
120
- | `EXPO_TOKEN` | value | The Expo access token `easSimulators()` from `@e2e-dev/eas` starts and stops simulator sessions with; unset, it uses the eas-cli login in `.expo/state.json` under `HOME` (`USERPROFILE` on Windows), and with neither the device lease fails. See [EAS Simulators](/integrations/eas) |
121
- | `HOME`, `USERPROFILE` | value | The home `easSimulators()` finds the eas-cli login under when `EXPO_TOKEN` is unset: `USERPROFILE` on Windows, `HOME` elsewhere, the OS user's home when unset |
122
- | `E2E_AGENT_DEVICE_POOL_<TARGET>_<digest>` | value | Written by `@e2e-dev/mobile` in `prepare` and read by each worker to find its booted device. Not meant to be set by hand |
157
+ | Variable | Example | Reading | Effect |
158
+ | --- | --- | --- | --- |
159
+ | `GITHUB_ACTIONS` | `GITHUB_ACTIONS=true` | value | Without it the reporter posts nothing and says so |
160
+ | `GITHUB_REPOSITORY`, `GITHUB_RUN_ID`, `GITHUB_EVENT_NAME`, `GITHUB_WORKFLOW`, `GITHUB_JOB` | `GITHUB_REPOSITORY=acme/shop` | value | Always set by the runner. A missing one fails the reporter, naming the variable. The repository and run id build the artifacts link. The event name shows in the "not a pull request" row. The workflow and job join the marker a rerun finds its own comment by |
161
+ | `GITHUB_TOKEN`, `GH_TOKEN` | `GITHUB_TOKEN=<token>` | value | Posts the comment; the first one set wins. Unset, the job summary is still written. A fork's `pull_request` token is read-only and cannot post; see [When nothing is posted](/github#when-nothing-is-posted) |
162
+ | `GITHUB_EVENT_PATH`, `GITHUB_REF` | `GITHUB_REF=refs/pull/42/merge` | value | The pull request number and head commit from the event payload, or the number from `refs/pull/N/merge` |
163
+ | `GITHUB_SHA` | `GITHUB_SHA=<commit>` | value | The commit source links point at when the event is not about a pull request |
164
+ | `GITHUB_WORKSPACE` | `GITHUB_WORKSPACE=/home/runner/work/shop/shop` | value | The checkout root. Source links add the project's path inside it, so a suite in a subdirectory links to the right file |
165
+ | `GITHUB_SERVER_URL`, `GITHUB_API_URL` | `GITHUB_SERVER_URL=https://github.example.com` | value | The GitHub host, for Enterprise Server. Default `https://github.com` and `https://api.github.com` |
166
+ | `GITHUB_STEP_SUMMARY` | `GITHUB_STEP_SUMMARY=<file>` | value | The job summary file the comment text is appended to |
167
+
168
+ ### Engines
169
+
170
+ | Variable | Example | Reading | Effect |
171
+ | --- | --- | --- | --- |
172
+ | `PLAYWRIGHT_BROWSERS_PATH` | `PLAYWRIGHT_BROWSERS_PATH=~/.cache/ms-playwright` | value | Playwright's browser cache, honored by the engine's `prepare` hook |
173
+ | `PLAYWRIGHT_SKIP_BROWSER_GC` | `PLAYWRIGHT_SKIP_BROWSER_GC=0` | value | Defaults to `1` for the engine's browser install, which keeps other browsers in the cache. `0` lets the install remove them |
174
+ | `KERNEL_API_KEY` | `KERNEL_API_KEY=<key>` | value | The API key `kernel()` from `@e2e-dev/kernel` creates browsers with. Unset, the browser lease fails. See [Kernel](/integrations/kernel) |
175
+ | `LIGHTPANDA_PATH` | `LIGHTPANDA_PATH=/opt/lightpanda` | value | The binary `lightpanda()` from `@e2e-dev/lightpanda` starts a CDP server with. Unset, `lightpanda` on `PATH`, `~/.lightpanda`, or `~/.local/bin`. See [Lightpanda](/integrations/lightpanda) |
176
+ | [`EXPO_TOKEN`](#expo_token) | `EXPO_TOKEN=<token>` | value | The Expo access token `easSimulators()` from `@e2e-dev/eas` uses |
177
+ | `HOME`, `USERPROFILE` | `HOME=/home/ada` | value | Where `easSimulators()` finds the eas-cli login without `EXPO_TOKEN`: `USERPROFILE` on Windows, `HOME` elsewhere, the OS user's home when unset |
178
+ | `LIMRUN_API_KEY` | `LIMRUN_API_KEY=<key>` | value | The API key `limrun()` from `@e2e-dev/limrun` leases devices with. Unset, the lease fails with `INVALID_CONFIG`. See [Limrun](/integrations/limrun) |
179
+ | `LIMRUN_BASE_URL` | `LIMRUN_BASE_URL=<url>` | value | The Limrun API host. Default `https://api.limrun.com` |
180
+ | `LT_USERNAME`, `LT_ACCESS_KEY` | `LT_USERNAME=acme` | value | The TestMu AI username and access key `testmu()` from `@e2e-dev/testmu` leases devices with. With either unset, the device lease fails. See [TestMu AI](/integrations/testmu) |
181
+ | `TESTMU_API_ENDPOINT` | `TESTMU_API_ENDPOINT=<url>` | value | The TestMu AI sessions API base URL `testmu()` looks up session recordings at. Default `https://mobile-api.lambdatest.com/mobile-automation/api/v1`. A value that is not an http(s) URL, or carries a username or password, fails the lookup with `INVALID_CONFIG`. agent-device's TestMu AI plugin reads it too |
182
+ | `E2E_WEB_BROWSERS_<TARGET>_<digest>` | - | value | `@e2e-dev/web` writes it in `prepare`; each worker reads it to find its leased browser. Don't set it by hand |
183
+ | `E2E_AGENT_DEVICE_POOL_<TARGET>_<digest>` | - | value | `@e2e-dev/mobile` writes it in `prepare`; each worker reads it to find its booted device. Don't set it by hand |
184
+
185
+ #### `EXPO_TOKEN`
186
+
187
+ The Expo access token `easSimulators()` from `@e2e-dev/eas` starts and stops
188
+ simulator sessions with. See [EAS Simulators](/integrations/eas).
189
+
190
+ - Unset, it uses the eas-cli login in `.expo/state.json` under the home
191
+ directory ([`HOME`, `USERPROFILE`](#engines)).
192
+ - With neither, the device lease fails.
123
193
 
124
194
  ## What app processes inherit
125
195
 
126
- An app `command` receives `PATH`, `HOME`, `TMPDIR`, `TMP`,
127
- `TEMP`, `SystemRoot`, and `COMSPEC`, plus whatever `command.env` sets.
128
- `APP_URL` is read by the scaffolded config module, not the runner; pass it
129
- through `command.env` if the dev server needs it too.
196
+ An app `command` receives `PATH`, `HOME`, `TMPDIR`, `TMP`, `TEMP`,
197
+ `SystemRoot`, and `COMSPEC`, plus the variables `command.inheritEnv` matches
198
+ and whatever `command.env` sets.
199
+
200
+ The scaffolded config module reads `APP_URL`, not the runner. If the dev
201
+ server needs it too, pass it through `command.env`.
@@ -6,8 +6,30 @@ icon: "triangle-exclamation"
6
6
  "twitter:image": "https://e2e.tester.army/docs/images/og/reference/errors.png?v=b184354c61"
7
7
  ---
8
8
 
9
- Every error carries a class. The class determines the exit code; the code
10
- identifies the cause. A model never selects either.
9
+ Every runner error carries a class and a code. The class sets the exit code,
10
+ and the code names the cause. A model never selects either. `e2e login`,
11
+ `e2e logout`, and `e2e models` are the exception, under
12
+ [Exit codes](#exit-codes).
13
+
14
+ ```ts title="tests/upgrade.e2e.ts"
15
+ import { isAgentError, test } from 'e2e';
16
+
17
+ test('upgrade to Pro', async ({ agent }) => {
18
+ try {
19
+ await agent.act('upgrade the workspace to Pro');
20
+ } catch (error) {
21
+ if (isAgentError(error) && error.code === 'STEP_TIMEOUT') {
22
+ // the step ran out of time; the message carries the last judgment
23
+ console.error(error.category, error.details);
24
+ }
25
+ throw error;
26
+ }
27
+ });
28
+ ```
29
+
30
+ ## Reference
31
+
32
+ ### Exit codes
11
33
 
12
34
  | Class | Exit code |
13
35
  | --- | ---: |
@@ -17,13 +39,36 @@ identifies the cause. A model never selects either.
17
39
  | internal | 4 |
18
40
  | interrupted | 130 |
19
41
 
20
- ## AgentError
42
+ - In a mixed run the highest-precedence code present wins,
43
+ `130 > 4 > 3 > 2 > 1 > 0`. The report keeps every individual result.
44
+ - A setup assertion failure is a test failure and skips its dependents.
45
+ Missing setup infrastructure keeps its configuration or infrastructure
46
+ class.
47
+ - The `internal` class and exit code 4 are reserved for runner invariant
48
+ violations. No first-party code raises a code in that class today.
49
+ - `e2e login`, `e2e logout`, and `e2e models` print a failure's message with
50
+ no code and exit 1, or 130 when a sign-in or picker is cancelled.
51
+
52
+ | Codes | Description |
53
+ | --- | --- |
54
+ | [Agent codes](#agent-codes) | The `code` of an `AgentError` |
55
+ | [Runner codes](#runner-codes) | Plain strings in output and report |
56
+ | [Test code errors](#test-code-errors) | Thrown from test code |
57
+
58
+ ### AgentError
59
+
60
+ `AgentError` is the runner-classified agent failure, exported from `e2e`.
21
61
 
22
62
  ```ts
23
63
  /** Runner-classified agent failure. */
24
64
  export class AgentError extends Error {
25
65
  /** Closed union, assigned by the runner, never taken from model text. */
26
66
  readonly code: AgentErrorCode;
67
+ /** The error class in the table above; it sets the exit code. */
68
+ readonly category: 'test' | 'configuration' | 'infrastructure' | 'internal' | 'interrupted';
69
+ readonly retryable: boolean;
70
+ /** Structured facts: locator, expected, observed, match count, wait time. */
71
+ readonly details: ErrorDetails | undefined;
27
72
  /** Sanitized, bounded prose; the same value as `message`. */
28
73
  readonly explanation: string;
29
74
  /** Artifact path, present only when evidence was captured and permitted. */
@@ -33,52 +78,47 @@ export class AgentError extends Error {
33
78
  }
34
79
  ```
35
80
 
81
+ <Note>
36
82
  Test files load in their own module realm, so `instanceof AgentError` can be
37
- false. Use `isAgentError` from `e2e` and branch on `code`:
83
+ false. Use `isAgentError` from `e2e` and branch on `code`, as in the example
84
+ at the top of this page.
85
+ </Note>
38
86
 
39
- ```ts
40
- import { isAgentError } from 'e2e';
87
+ #### Agent codes
41
88
 
42
- try {
43
- await agent.act('upgrade the workspace to Pro');
44
- } catch (error) {
45
- if (isAgentError(error) && error.code === 'STEP_TIMEOUT') {
46
- // the step ran out of time; the message carries the last judgment
47
- }
48
- throw error;
49
- }
50
- ```
89
+ The members of the closed `AgentErrorCode` union. `blocked:` names the
90
+ category a blocked verdict reports as the obstacle.
51
91
 
52
92
  | Code | When it happens | Class | Exit |
53
93
  | --- | --- | --- | ---: |
54
- | `AUTH_CREDENTIAL_UNAVAILABLE` | A `Secret` or `credentials.user()` names a credential absent from `config.credentials`, the call happened outside an active runner, or the password's provider returned something other than a string of at least 6 characters (code points) | configuration | 2 |
55
- | `AUTH_CREDENTIAL_INVALID` | A configured credential was rejected by the application (blocked: credentials) | configuration | 2 |
56
- | `SECRET_UNAVAILABLE` | A `Secret` or `secrets.get()` during a run names a secret absent from `config.secrets`, the secret's provider returned something other than a string of at least 6 characters (code points), or an engine resolved a secret it did not declare in its `secrets` | configuration | 2 |
94
+ | `AUTH_CREDENTIAL_UNAVAILABLE` | A `Secret` or `credentials.user()` names a credential missing from `config.credentials`, the call ran outside an active runner, or the password's provider returned no string of at least 6 characters (code points) | configuration | 2 |
95
+ | `AUTH_CREDENTIAL_INVALID` | The application rejected a configured credential (blocked: credentials) | configuration | 2 |
96
+ | `SECRET_UNAVAILABLE` | A `Secret` or `secrets.get()` during a run names a secret missing from `config.secrets`, the secret's provider returned no string of at least 6 characters (code points), or an engine resolved a secret it did not declare in its `secrets` | configuration | 2 |
57
97
  | `ENVIRONMENT_UNAVAILABLE` | The environment under test is down or unreachable beyond the app itself (blocked: environment) | infrastructure | 3 |
58
98
  | `SEED_DATA_MISSING` | Data the step depends on does not exist in the environment (blocked: seed_data) | configuration | 2 |
59
- | `TEST_SETUP_FAILED` | Test prerequisites outside the product failed to arrange (blocked: test_setup). Also a `test.extend()` fixture that broke its contract: returned without calling `use()`, called it twice, or redefined a fixture the target's engine contributes | configuration | 2 |
60
- | `AUTOMATION_UNSUPPORTED` | The step needs an interaction the available toolset cannot perform, e.g. drag-and-drop (blocked: automation) | test | 1 |
61
- | `MODEL_UNAVAILABLE` | No model configured: the built-in agent's `agents.<name>.model` holds no AI SDK model instance; the message names the agent that was selected. Checked once per run when the `agent` fixture is first acquired; recorded as one run-level error that stops the run. `e2e explore` checks before anything starts, a custom executor's entry included, since explore runs the built-in agent. A custom executor without a model is not checked: its `act` and `assert` steps run, and `waitFor` or `extract` fails with this code on its first call. Evicts no [recording](/cache). | configuration | 2 |
62
- | `POLICY_DENIED` | A denied destination (a navigation scheme other than `http:` or `https:`, `about:blank` aside, a malformed URL), a forbidden fill (an undeclared secret, a non-editable sink, a password into a field that is not a password field), or pixel evidence after a secret fill (`vision: 'only'` with pixels withheld, or an `act` or `vision: true` step whose semantic capture timed out before the step did). Also reading a value, text, or attribute from a secure field, the getters and the `toHaveValue`, `toHaveText`, `toContainText`, and `toHaveAttribute` matchers alike. An unknown tool or an undeclared field is refused to the model as the call's result and is not this code | configuration | 2 |
63
- | `APP_UNREACHABLE` | The app URL could not be reached, or an app command exited early or exceeded `startupTimeout`. The startup error includes recent log output with `command.env` values redacted, and suggests adding a `log` if none is configured. Under the agent, also any engine fault with no more specific code: on a device, an iOS automation runner that is busy or wedged reports here with a message that names the runner, the session and device, and the recovery; see [Mobile](/mobile#troubleshooting) | infrastructure | 3 |
64
- | `APP_ALREADY_RUNNING` | `readyUrl` already answered before the target's `app.command` was spawned and `reuseExisting` was not in effect; stop that process or set `reuseExisting: true` locally | infrastructure | 3 |
65
- | `MODEL_PROVIDER_FAILED` | Provider transport failure (network error, 5xx, rate limit, or a request with no response within 120 s) after the adapter's transport retries (up to 5 per model call, honoring `retry-after`, bounded by the remaining step timeout). A request that stalls is aborted at 120 s and sent again, and an `agent.act` step's `--debug` transcript notes it. The bound covers the whole request, so a model that needs longer than 120 s for one answer fails here too. An error body the provider package cannot read shows as the HTTP status and the body, secrets redacted, cut at 1 KB. Evicts no [recording](/cache) | infrastructure | 3 |
66
- | `CANCELLED` | The runner cancelled the operation, without a process signal: an `e2e mcp` `open_session` whose request the client cancelled or whose connection closed, or that the server's shutdown cut short, after stopping what it had started | infrastructure | 3 |
67
- | `MODEL_OUTPUT_INVALID` | Structured output still failed validation after the repair budget was spent. Under a [decision model](/decision-models), an answer without a valid choice distribution, a choice the request never offered, or a question the provider refused; decisions are not repaired | test | 1 |
68
- | `APP_NOT_OPEN` | An operation needed an open app; `app.open()` had not run, or the engine reported invalid state | test | 1 |
99
+ | `TEST_SETUP_FAILED` | Test prerequisites outside the product failed to arrange (blocked: test_setup). Also a `test.extend()` fixture that broke its contract: it returned without calling `use()`, called it twice, or redefined a fixture the target's engine contributes | configuration | 2 |
100
+ | `AUTOMATION_UNSUPPORTED` | The step needs an interaction the available toolset cannot perform, such as drag-and-drop (blocked: automation) | test | 1 |
101
+ | `MODEL_UNAVAILABLE` | No model configured: the built-in agent's `agents.<name>.model` holds no AI SDK model instance (the message names the selected agent), or GitHub Copilot does not serve the configured model to this account (`model_not_supported`; the message names the model). Recorded once as a run-level error, and the run stops. In a test run, a missing model is found when the `agent` fixture is first acquired, and an unserved model at its first call. `e2e explore` runs the built-in agent, so it checks for a missing model before anything starts, a custom executor's entry included. A custom executor without a model is not checked: its `act` and `assert` steps run, and `waitFor` or `extract` fails with this code on its first call. Evicts no [recording](/cache). | configuration | 2 |
102
+ | `POLICY_DENIED` | The security policy refused an operation: a denied destination, a forbidden fill, pixels after a secret fill, or a read from a secure field. [Policy denials](/reference/security#policy-denials) lists every case. An unknown tool or an undeclared field goes back to the model as the call's result, never as this code | configuration | 2 |
103
+ | `APP_UNREACHABLE` | The app URL could not be reached, or an app command exited early or exceeded `startupTimeout`. With a `log`, the startup error quotes the log's output since the command started, `command.env` values redacted, or says there was none. Without one, it says to set a `log`. A command that fails to spawn reports the spawn error. Under the agent, also any engine fault with no more specific code. On a device, a busy or wedged iOS automation runner reports here; the message names the runner, the session and device, and the recovery. See [Mobile](/mobile#troubleshooting) | infrastructure | 3 |
104
+ | `APP_ALREADY_RUNNING` | `readyUrl` answered before the target's `app.command` was spawned, and `reuseExisting` was not in effect. Stop that process, or set `reuseExisting: true` locally | infrastructure | 3 |
105
+ | `MODEL_PROVIDER_FAILED` | Provider transport failure (network error, 5xx, rate limit, or no response within 120 s) after the adapter's transport retries: up to 5 per model call, honoring `retry-after`, within the remaining step timeout. A stalled request is aborted at 120 s and sent again; an `agent.act` step's `--debug` transcript notes it. The 120 s covers the whole request, so a model that needs longer for one answer fails here too. An error body the provider package cannot read shows as the HTTP status and the body, secrets redacted, cut at 1 KB. Evicts no [recording](/cache) | infrastructure | 3 |
106
+ | `CANCELLED` | The runner cancelled the operation without a process signal: an `e2e mcp` `open_session` cut short by a client cancel, a closed connection, or the server's shutdown, after it stopped what it had started | infrastructure | 3 |
107
+ | `MODEL_OUTPUT_INVALID` | Structured output still failed validation after the repair budget was spent. Under a [decision model](/decision-models): an answer without a valid choice distribution, a choice the request never offered, or a question the provider refused. Decisions are not repaired | test | 1 |
108
+ | `APP_NOT_OPEN` | An operation needed an open app: `app.open()` had not run, or the engine reported invalid state | test | 1 |
69
109
  | `LOCATOR_NOT_FOUND` | No matching node within the deadline, or an agent action refers to a node that is no longer available | test | 1 |
70
- | `LOCATOR_AMBIGUOUS` | More than one match for a single-node operation. Fails immediately, no polling. | test | 1 |
110
+ | `LOCATOR_AMBIGUOUS` | More than one match for a single-node operation. Fails at once, with no polling. | test | 1 |
71
111
  | `ACTION_FAILED` | Actionability failure, operation timeout, or an action that may already have committed | test | 1 |
72
112
  | `STEP_BUDGET_EXHAUSTED` | The model-call or action-step budget ran out before a conclusion | test | 1 |
73
- | `STEP_TIMEOUT` | The invocation deadline expired, an operation timing out within a poll tick of it included. Carries the last judgment where one exists, with the `ASSERTION_INCONCLUSIVE` vision hint when that judgment was inconclusive | test | 1 |
74
- | `CONTEXT_OVERFLOW` | A request exceeded the model's context window. `act` shortens its history and retries once if that reduces the request; judgments fail directly. Lower `agents.<name>.maxObservationBytes` or split the instruction into shorter steps. Reported as blocked: automation | test | 1 |
113
+ | `STEP_TIMEOUT` | The invocation deadline expired, including an operation that timed out within a poll tick of it. Carries the last judgment where one exists, with the [vision hint](/reference/agent#agent-assert) when that judgment was inconclusive | test | 1 |
114
+ | `CONTEXT_OVERFLOW` | A request exceeded the model's context window. `act` shortens its history and retries once if that makes the request smaller; judgments fail at once. Lower `agents.<name>.maxObservationBytes`, or split the instruction into shorter steps. Reported as blocked: automation | test | 1 |
75
115
  | `ASSERTION_FAILED` | `agent.assert` judged false, or a locator, `browser`, or value matcher failed | test | 1 |
76
- | `ASSERTION_INCONCLUSIVE` | `agent.assert` could not judge: the screen did not show enough to decide either way (the relevant part is not on screen, still loading, or unreadable). `agent.extract` raises it when the model answers that the screen does not show the requested data, instead of returning a placeholder. A failure, never a pass; the explanation says what was missing. Judged from the tree alone with `vision` off, the message adds `pass vision: true when the answer is in pixels`, as a condition (`if the engine captures pixels, ...`) until a step of the attempt has received pixels; no hint when the engine declares no screenshot capture, a secret was filled, or an earlier step's pixel request was degraded | test | 1 |
77
- | `AUTHENTICATION_FAILED` | The executor concluded that signing in failed inside the app: a product verdict. `AUTH_CREDENTIAL_INVALID` is the blocked counterpart for a configured credential the app rejects | test | 1 |
78
- | `STEP_NO_CONCLUSION` | The executor used every turn without calling `complete_step`; the step fails with the agent's last summary | test | 1 |
79
- | `REPLAY_STALE` | Under `cache.strict` or `--strict-cache`, the step's recording exists but no longer replays (`step.cache.reason` is one of `invalid-entry`, `wrong-context`, `target-not-found`, `target-ambiguous`, `viewport-changed`, `action-failed`, `action-uncertain`, `end-mismatch`, or `no-entry` while the cache directory, not a custom `cache.store`, holds a recording of the step under another key, which the message names), so the step fails with no model call instead of handing off to the agent. Not retried: every attempt replays the same recording. Re-record with a read-write run without the flag, and with `cache.strict` off when the config set it, then commit the changed entry; the message names the knob that turned strict on and the configured cache directory (or `cache.store`) | configuration | 2 |
116
+ | `ASSERTION_INCONCLUSIVE` | `agent.assert` could not judge: the screen did not show enough to decide either way (the relevant part is not on screen, still loading, or unreadable). `agent.extract` raises it when the model answers that the screen does not show the requested data, instead of returning a placeholder. A failure, never a pass; the explanation says what was missing. Judged from the tree alone with `vision` off, the message can add a [vision hint](/reference/agent#agent-assert) | test | 1 |
117
+ | `AUTHENTICATION_FAILED` | The executor concluded that signing in failed inside the app: a product verdict. `AUTH_CREDENTIAL_INVALID` is the blocked counterpart, for a configured credential the app rejects | test | 1 |
118
+ | `STEP_NO_CONCLUSION` | The executor used every turn without calling `complete_step`. The step fails with the agent's last summary | test | 1 |
119
+ | `REPLAY_STALE` | Under `cache.strict` or `--strict-cache`, the step's recording exists but no longer replays, so the step fails with no model call instead of handing off to the agent. `step.cache.reason` is one of `invalid-entry`, `wrong-context`, `target-not-found`, `target-ambiguous`, `viewport-changed`, `action-failed`, `action-uncertain`, `end-mismatch`, or `no-entry` while the cache directory (not a custom `cache.store`) holds a recording of the step under another key, which the message names. Not retried: every attempt replays the same recording. To fix it, re-record with a read-write run without the flag, with `cache.strict` off if the config set it, then commit the changed entry. The message names the knob that turned strict on and the configured cache directory (or `cache.store`) | configuration | 2 |
80
120
 
81
- ## Runner codes
121
+ ### Runner codes
82
122
 
83
123
  These appear in terminal output and in the report as plain string codes.
84
124
 
@@ -86,64 +126,58 @@ These appear in terminal output and in the report as plain string codes.
86
126
  | --- | --- | --- | ---: |
87
127
  | `CONFIG_NOT_FOUND` | `--config <path>` names a file that does not exist, or no `e2e.config.ts` or `e2e.config.mts` exists from the working directory up to the repository root | configuration | 2 |
88
128
  | `CONFIG_AMBIGUOUS` | Both `e2e.config.ts` and `e2e.config.mts` in one directory | configuration | 2 |
89
- | `CONFIG_LOAD_FAILED` | The config module failed to import: a package that is declared but not installed, an undeclared package, a wrong subpath, a removed export, or any other throw while it evaluates. The message names the fix. A config error raised while it evaluates keeps its own code: an option a factory the config calls (`web()`, `mobile()`, `defineEngine()`, `decisionExecutor()`) refuses, or a `secrets.get()` handle turned into a string, is `INVALID_CONFIG` | configuration | 2 |
90
- | `INVALID_CONFIG` | A factory the config module calls (`web()`, `mobile()`, `defineEngine()`, `decisionExecutor()`) refused its options, with the factory's own message; or an unknown key, out-of-range value, invalid enum, missing `targets`, a target key outside `{ name, platform, engine, app, trace, video }`, a target `app` key that is unknown or malformed, `app` on a target without an engine, a `web()` target without `app.url` or with a device field, a `mobile()` target without `app.bundleId` or `app.appPath`, with `app.url` (not supported yet), a link in `bundleId`, or an unknown permission name or state (the message names the nearest name), an unknown key in a `web()` or `mobile()` option, including `web({ connect })` and `web({ basicAuth })` (the message names the nearest one), a `trace` or `video` that is not a recording mode, the removed artifact kinds list (`artifacts: [...]` or `artifacts.kinds`) and `artifacts.trace` block (the message names the `trace` mode they meant), the removed `artifacts.video` block or `video` artifact kind (the message names the `video` option), the removed `specVersion` key, a `secrets.get()` in an engine option that names no `secrets` entry (a credential's password is not one), a `secrets.get()` reference turned into a string while the config evaluates (a template literal, `String()`) or given where a string is needed (an `app.command` `args` entry or `env` value; only an engine option that declares secrets takes one), an `app.command` `args` entry or `env` value that is not a string (an `undefined` `env` value is dropped instead), an unknown key in `app.command` (the message names the nearest one), an `engine` that is not a `defineEngine` handle, `json` reporter combined with `list`, duplicate or malformed target name, a secret or credential password shorter than 6 characters (code points), a credential that is not `{ username, password }`, whose `username` is not a string (only `password` may be a function), or that has no username in the config or `E2E_USER_<NAME>_USERNAME`, two credentials or two secrets whose names map to one `E2E_USER_*` or `E2E_SECRET_*` override variable (`api-key` and `api_key`), oversized `agents.<name>.context`, an `expect.poll` `timeout` or `interval` that is not a finite number of milliseconds, a `tests` value that is not a glob or a list of globs, a `tests` list of only `!` exclusions, the removed top-level `limits` key, an agents entry that is a bare `StepExecutor` (the message names `{ executor }`) or a model instance (the message names `{ model }`), an unknown agent key (the message names the nearest one), the removed agent keys `timeout` and `maxTurns` (the message names `judgmentTimeout` and `maxModelCalls`), `system` or `tools` beside `executor`, a `tools` entry `defineTool` did not build or that takes a built-in tool name (a grammar verb, `complete_step`, `locate`, `start_recording`, `stop_recording`, or `report_finding`), `createToolLoopExecutor({ maxTurns })`, `reuseExisting` on an app whose `app.url` asks for a free port, or a port-0 `app.url` with no `app.command` to serve it | configuration | 2 |
129
+ | `CONFIG_LOAD_FAILED` | The config module failed to import: a package that is declared but not installed, an undeclared package, a wrong subpath, a removed export, or any other throw while it evaluates. The message names the fix. A config error raised while it evaluates keeps its own code: options a factory the config calls refuses, or a `secrets.get()` handle turned into a string, is `INVALID_CONFIG` | configuration | 2 |
130
+ | `INVALID_CONFIG` | A factory the config module calls (`web()`, `mobile()`, `defineEngine()`, `decisionExecutor()`) refused its options, with the factory's own message. Or: an unknown key, out-of-range value, invalid enum, or missing `targets`; a target key outside `{ name, platform, engine, app, trace, video }`; a target `app` key that is unknown or malformed; `app` on a target without an engine; a `web()` target without `app.url` or with a device field; a `mobile()` target without `app.bundleId` or `app.appPath`, with `app.url` (not supported yet), with a link in `bundleId`, or with an unknown permission name or state (the message names the nearest name); an unknown key in a `web()` or `mobile()` option, `web({ connect })` and `web({ basicAuth })` included (the message names the nearest one); a `trace` or `video` that is not a recording mode; the removed artifact kinds list (`artifacts: [...]` or `artifacts.kinds`) or `artifacts.trace` block (the message names the `trace` mode they meant); the removed `artifacts.video` block or `video` artifact kind (the message names the `video` option); the removed `specVersion` key; a `secrets.get()` in an engine option that names no `secrets` entry (a credential's password is not one); a `secrets.get()` reference turned into a string while the config evaluates (a template literal, `String()`), or given where a string is needed (an `app.command` `args` entry or `env` value; only an engine option that declares secrets takes one); an `app.command` `args` entry or `env` value that is not a string (an `undefined` `env` value is dropped instead); an unknown key in `app.command` (the message names the nearest one); an `engine` that is not a `defineEngine` handle; the `json` reporter combined with `list`; a duplicate or malformed target name; a secret or credential password shorter than 6 characters (code points); a credential that is not `{ username, password }`, whose `username` is not a string (only `password` may be a function), or that has no username in the config or `E2E_USER_<NAME>_USERNAME`; two credentials or two secrets whose names map to one `E2E_USER_*` or `E2E_SECRET_*` override variable (`api-key` and `api_key`); an oversized `agents.<name>.context`; an `expect.poll` `timeout` or `interval` that is not a finite number of milliseconds; a `tests` value that is not a glob or a list of globs, or a `tests` list of only `!` exclusions; the removed top-level `limits` key; an agents entry that is a bare `StepExecutor` (the message names `{ executor }`) or a model instance (the message names `{ model }`); an unknown agent key (the message names the nearest one); the removed agent keys `timeout` and `maxTurns` (the message names `judgmentTimeout` and `maxModelCalls`); `system` or `tools` beside `executor`; a `tools` entry that `defineTool` did not build, or that takes a built-in tool name (a grammar verb, `complete_step`, `locate`, `start_recording`, `stop_recording`, or `report_finding`); `createToolLoopExecutor({ maxTurns })`; `reuseExisting` on an app whose `app.url` asks for a free port; or a port-0 `app.url` with no `app.command` to serve it | configuration | 2 |
91
131
  | `CONFIG_NOT_DETERMINISTIC` | A worker re-resolved the config module and got a different digest than the runner | configuration | 2 |
92
132
  | `APP_URL_REQUIRED` | A navigation or relative URL on a target that declares no `app.url`, or an `app.command` with no `app.url` or `app.readyUrl` to poll | configuration | 2 |
93
- | `INVALID_APP_URL` | Base URL with userinfo, a query, a fragment, a non-HTTP scheme, plain HTTP on a non-loopback host, port 0 on a host other than a literal loopback address (`127.0.0.1`, `[::1]`), a loopback address the run could not bind a free port on, or unparseable | configuration | 2 |
94
- | `INVALID_GLOB` | Empty pattern, a first segment that starts with `!` beyond the one leading `!` of a `tests` exclusion (`!!tests/**`, `./!tests/**`), a bare `!`, braces, character classes, extglobs, a backslash separator, an absolute path, a `..` segment, a trailing `/` or `.`, a `tests` entry with no wildcard that names a directory (the message names the glob to write), a pattern that names only the project root, or a `**` that is not a whole segment | configuration | 2 |
133
+ | `INVALID_APP_URL` | A base URL that is unparseable or has userinfo, a query, a fragment, a non-HTTP scheme, or plain HTTP on a non-loopback host; port 0 on a host other than a literal loopback address (`127.0.0.1`, `[::1]`); or a loopback address the run could not bind a free port on | configuration | 2 |
134
+ | `INVALID_GLOB` | An empty pattern; a first segment that starts with `!` beyond the one leading `!` of a `tests` exclusion (`!!tests/**`, `./!tests/**`); a bare `!`; braces, character classes, or extglobs; a backslash separator; an absolute path or a `..` segment; a trailing `/` or `.`; a `tests` entry with no wildcard that names a directory (the message names the glob to write); a pattern that names only the project root; or a `**` that is not a whole segment | configuration | 2 |
95
135
  | `UNKNOWN_TARGET` | `--target`, `open_session.target`, or `openHost` (`e2e/runner`) named a target that is not configured | configuration | 2 |
96
136
  | `TARGET_REQUIRED` | `e2e mcp` or `openHost` (`e2e/runner`): the config declares several targets and none was named | configuration | 2 |
97
137
  | `NO_SESSION` | `e2e mcp`: `call` or `tools` before `open_session` or after the session ended (the message names why), or `call`, `tools`, or `close_session` naming a `session` id that is not open or is closing | configuration | 2 |
98
- | `SESSION_REQUIRED` | `e2e mcp`: `call`, `tools`, or `close_session` without a `session` while several sessions are open; the message lists them | configuration | 2 |
99
- | `ENGINE_IN_USE` | `e2e mcp`: `open_session` on a target whose engine instance another open session drives, because the config gets it from a package every session shares; create the engine in the config or a file it imports by path | configuration | 2 |
100
- | `CONFIG_IN_USE` | `e2e mcp`: `open_session` on a config other than the one the open sessions use; credentials and secrets resolve process-wide, so open on that config or close those sessions first | configuration | 2 |
101
- | `SESSION_OPEN` | `e2e mcp`: `open_session` while every session slot (`--max-sessions`, default 4) holds a session that is open, opening, or closing, or while the server shuts down; close one first | configuration | 2 |
102
- | `UNKNOWN_TOPIC` | `e2e mcp`: an `e2e://guide/{topic}` resource named a topic the skill does not have; the message lists them | configuration | 2 |
103
- | `UNKNOWN_TOOL` | `e2e mcp`: `call` or `tools` named a tool the session's catalog does not have; the message lists the catalog | configuration | 2 |
104
- | `UNSUPPORTED_ARTIFACT` | A [`video` mode](/reference/config#recording) set on a target, or on a test that will run there, records on some attempt, and the target's engine cannot record video. The message names the target and, for a test's mode, the test. A mode from the config root or `--video` never raises it: it applies to the targets that can record, with a notice naming the rest | configuration | 2 |
138
+ | `SESSION_REQUIRED` | `e2e mcp`: `call`, `tools`, or `close_session` without a `session` while several sessions are open. The message lists them | configuration | 2 |
139
+ | `ENGINE_IN_USE` | `e2e mcp`: `open_session` on a target whose engine instance another open session drives, because the config gets it from a package every session shares. Create the engine in the config, or in a file it imports by path | configuration | 2 |
140
+ | `CONFIG_IN_USE` | `e2e mcp`: `open_session` on a config other than the one the open sessions use. Credentials and secrets resolve process-wide, so open on that config or close those sessions first | configuration | 2 |
141
+ | `SESSION_OPEN` | `e2e mcp`: `open_session` while every session slot (`--max-sessions`, default 4) holds an open, opening, or closing session, or while the server shuts down. Close one first | configuration | 2 |
142
+ | `UNKNOWN_TOPIC` | `e2e mcp`: an `e2e://guide/{topic}` resource named a topic the skill does not have. The message lists them | configuration | 2 |
143
+ | `UNKNOWN_TOOL` | `e2e mcp`: `call` or `tools` named a tool the session's catalog does not have. The message lists the catalog. An action verb the engine does not declare is `UNSUPPORTED_CAPABILITY` instead | configuration | 2 |
144
+ | `UNSUPPORTED_ARTIFACT` | A [`video` mode](/reference/config#recording) set on a target, or on a test that will run there, records on some attempt, and the target's engine cannot record video. The message names the target, and the test for a test's mode. A mode from the config root or `--video` never raises it: it applies to the targets that can record, with a notice naming the rest | configuration | 2 |
105
145
  | `UNSUPPORTED_CAPABILITY` | A fixture, action, or tier the target's engine does not declare: a contributed fixture it does not contribute, `screen` without `locate`, state restore without `state`, screenshots without `artifacts`, `app.clearState()` on a persistent browser context (the message names the mode), or `agent.assert` `vision` with an executor that does not declare `vision: true` | configuration | 2 |
106
- | `NO_TESTS` | Zero runnable ordinary test-target pairs. The message names the cause: no file matched the `tests` globs (with look-alike files), a positional matched nothing (naming the `!` entry that excludes a file it names), a file registered no tests, or filters excluded everything. Suppress with `--pass-with-no-tests`. | configuration | 2 |
107
- | `RUNNER_BUSY` | A suite overlapped another suite, an opening or open host, or a live attempt; or a host opened during a suite. Await the suite and close existing hosts before retrying. | configuration | 2 |
146
+ | `NO_TESTS` | Zero runnable ordinary test-target pairs. The message names the cause: no file matched the `tests` globs (it lists look-alike files), a positional matched nothing (it names the `!` entry that excludes a file the positional names), a file registered no tests, or filters excluded everything. Suppress with `--pass-with-no-tests`. | configuration | 2 |
147
+ | `RUNNER_BUSY` | A suite overlapped another suite, an opening or open host, or a live attempt; or a host opened during a suite. Await the suite and close existing hosts before you retry. | configuration | 2 |
108
148
  | `NO_LAST_RUN` | `--last-failed` found no `report.json` where the run writes it, or a file there that is not a `report-1` document. Run once without the flag first. | configuration | 2 |
109
149
  | `ONLY_IN_CI` | A focused test reached a CI run | configuration | 2 |
110
- | `COLLECTION_ERROR` | Registration outside collection, an invalid title, a non-function body, an async `describe` body, nested serial groups, an option forbidden inside a serial group, a setup test inside `describe`, an invalid session name, a malformed test option such as `tags` that is not a list of distinct names, a test or `describe` option key the call does not take (the message names the nearest one), a test file that fails to import (in a run with no positionals, or a file the positionals selected), a session that a selected test consumes or `e2e explore --session` names but no collected setup test produces (the message lists the declared sessions), a consumed session on a target whose engine has no `state` capability to save it, or a positional path outside the project root | configuration | 2 |
111
- | `POLICY_DENIED` | A denied destination (a navigation scheme other than `http:` or `https:`, `about:blank` aside, a malformed URL), a forbidden fill (an undeclared secret, a non-editable sink, a mismatched purpose, a `Secret` reaching a JSON sink), or tainted pixels (`app.screenshot()` after a secret fill, `vision: 'only'` with pixels withheld, an `act` or `vision: true` step whose semantic capture timed out before the step did); see the [security rules](/reference/security#policy-denials). An unknown tool or an undeclared field goes back to the model as the call's result, never this code | configuration | 2 |
150
+ | `COLLECTION_ERROR` | Registration outside collection; an invalid title; a non-function body; an async `describe` body; nested serial groups; an option forbidden inside a serial group; a setup test inside `describe`; an invalid session name; a malformed test option, such as `tags` that is not a list of distinct names; a test or `describe` option key the call does not take (the message names the nearest one); a test file that fails to import, in a run with no positionals or when the positionals selected it; a session that a selected test consumes, or `e2e explore --session` names, but no collected setup test produces (the message lists the declared sessions); a consumed session on a target whose engine has no `state` capability to save it; or a positional path outside the project root | configuration | 2 |
112
151
  | `SESSION_MISMATCH` | A stored session does not match the current run, target, engine, platform, or app identity | configuration | 2 |
113
152
  | `SESSION_EXPIRED` | A stored session is past its expiry at restore time (configuration, exit 2), or a setup test saved state whose expiry is not after its creation (test, exit 1) | configuration or test | 2 or 1 |
114
153
  | `SESSION_INVALID` | A stored session failed authentication. Tampered or corrupt. | configuration | 2 |
115
154
  | `SESSION_UNAVAILABLE` | A test requires a session no selected setup test produced | configuration | 2 |
116
155
  | `SESSION_CONTRACT` | A setup test saved a session twice, saved an undeclared name, or finished without saving a declared one | test | 1 |
117
- | `TEST_TIMEOUT` | The attempt exceeded the resolved test `timeout`. The body is not stopped: the harness operation it is in is cancelled and its fixtures die with the attempt, so it cannot reach the next test. A test that blocks the event loop gets its worker killed once `cleanupTimeout` (at least 5 s) passes after the timeout; see [Hooks](/reference/test#hooks) | test | 1 |
118
- | `STEP_NOT_AWAITED` | The test body settled while a step it started was still running: a step call without `await`. The step is failed at the line of the call and cancelled; the attempt fails with this code when the body returned, or keeps the body's own error and records this one under `secondaryErrors` when the body threw. An `expect.poll` matcher left running when the body, an `afterEach` hook, or a fixture teardown returned fails that phase the same way at the line of the call, and is cancelled; one left by a `beforeAll` or `afterAll` hook fails the hook with `HOOK_FAILED` | test | 1 |
119
- | `UNHANDLED_REJECTION` | A promise rejected with no test running. An unhandled rejection is charged to the test running when it surfaces, since Node cannot say which test created the promise; between tests it is this run error, naming the last test that finished in the worker as the likely source. Before any test has finished in the worker it ends the worker instead: the rejection is the run error and the test on its way fails with `WORKER_CRASH`. The body that left it keeps running like one past `TEST_TIMEOUT` | test | 1 |
156
+ | `TEST_TIMEOUT` | The attempt exceeded the resolved test `timeout`. The body is not stopped: the harness operation it is in is cancelled, and its fixtures die with the attempt, so it cannot reach the next test. A test that blocks the event loop gets its worker killed once `cleanupTimeout` (at least 5 s) passes after the timeout. See [Hooks](/reference/test#hooks) | test | 1 |
157
+ | `STEP_NOT_AWAITED` | The test body settled while a step it started was still running: a step call without `await`. The step fails at the line of the call and is cancelled. The attempt fails with this code when the body returned. When the body threw, the attempt keeps the body's error and records this one under `secondaryErrors`. An `expect.poll` matcher still running when the body, an `afterEach` hook, or a fixture teardown returned fails that phase the same way, at the line of the call, and is cancelled. One left by a `beforeAll` or `afterAll` hook fails the hook with `HOOK_FAILED` | test | 1 |
158
+ | `UNHANDLED_REJECTION` | A promise rejected with no test running. Node cannot say which test created a promise, so an unhandled rejection is charged to the test running when it surfaces. Between tests it is this run error, naming the last test that finished in the worker as the likely source. Before any test has finished in the worker, it ends the worker instead: the rejection is the run error, and the test on its way fails with `WORKER_CRASH`. The body that left it keeps running like one past `TEST_TIMEOUT` | test | 1 |
120
159
  | `HOOK_FAILED` | `beforeAll` or `afterAll` threw. Skips the suite's tests. | test | 1 |
121
- | `RUN_TIMEOUT` | The run spent its [`maxDuration`](/reference/config#maxduration) or `--max-duration`: the running tests end as `interrupted`, the rest are skipped with cause `time-limit` | test | 1 |
160
+ | `RUN_TIMEOUT` | The run spent its [`maxDuration`](/reference/config#maxduration) or `--max-duration`. The running tests end as `interrupted`, and the rest are skipped with cause `time-limit` | test | 1 |
122
161
  | `LAUNCH_TIMEOUT` | Engine init or attempt start exceeded `config.launchTimeout` | infrastructure | 3 |
123
162
  | `CLEANUP_TIMEOUT` | Engine attempt end exceeded `config.cleanupTimeout`, or a route or dialog handler the engine ran for the test was still running when the attempt settled | infrastructure | 3 |
124
163
  | `BROWSER_INSTALL_FAILED` | Browser provisioning failed | infrastructure | 3 |
125
164
  | `WORKER_INIT_FAILED` | A worker process never became ready | infrastructure | 3 |
126
- | `WORKER_EXIT` | A worker exited unexpectedly while running a file: the test in flight fails with `WORKER_CRASH` and the file's other tests are skipped. Attempts that finished before the exit stay in the report, a retry's earlier attempts and a serial group's finished members included, and the crash is recorded as the attempt after them, or on the last attempt when it came after it (an `afterAll`). An unhandled rejection is charged to the test running when it surfaces, or recorded as `UNHANDLED_REJECTION` between tests; before any test has finished there it ends the worker, and the rejection itself is the run error, not this one | infrastructure | 3 |
165
+ | `WORKER_EXIT` | A worker exited unexpectedly while running a file. The test in flight fails with `WORKER_CRASH`, and the file's other tests are skipped. Attempts that finished before the exit stay in the report, including a retry's earlier attempts and a serial group's finished members. The crash is recorded as the attempt after them, or on the last attempt when it came after it (an `afterAll`). When an unhandled rejection ends the worker (see `UNHANDLED_REJECTION`), the rejection is the run error, not this one | infrastructure | 3 |
127
166
  | `WORKER_CRASH` | A worker exited during a specific test | infrastructure | 3 |
128
167
  | `WORKER_ENTRY_MISSING` | The worker entry module could not be resolved. Packaging fault. | infrastructure | 3 |
129
- | `NODE_UNSUPPORTED` | The runtime cannot run e2e's TypeScript loader: a Node.js outside `^22.22.3 \|\| >=24.8.0`, Bun (no `module.registerHooks`), or Deno. The CLI checks this before it starts and exits 2 with the same sentence; the loader checks it again where it registers its hooks, for a host that runs e2e's runner without the CLI | infrastructure | 3 |
130
- | `TYPESCRIPT_COMPILER_UNAVAILABLE` | oxc, which compiles e2e's TypeScript, could not be loaded; the message gives the cause. Most often its native bindings for this platform (`@oxc-transform/binding-<platform>-<arch>`, `@oxc-parser/binding-<platform>-<arch>`) are not installed. npm leaves it out with `--omit=optional`, or when `package-lock.json` was written on another platform (npm/cli#4828): install again without `--omit=optional`, or delete `package-lock.json` and `node_modules` and run `npm install` | infrastructure | 3 |
168
+ | `NODE_UNSUPPORTED` | The runtime cannot run e2e's TypeScript loader: a Node.js outside `^22.22.3 \|\| >=24.8.0`, Bun (no `module.registerHooks`), or Deno. The CLI checks this before it starts and exits 2 with the same sentence. The loader checks again where it registers its hooks, for a host that runs e2e's runner without the CLI | infrastructure | 3 |
169
+ | `TYPESCRIPT_COMPILER_UNAVAILABLE` | oxc, which compiles e2e's TypeScript, could not be loaded; the message gives the cause. Most often its native bindings for this platform (`@oxc-transform/binding-<platform>-<arch>`, `@oxc-parser/binding-<platform>-<arch>`) are not installed. npm leaves them out with `--omit=optional`, or when `package-lock.json` was written on another platform (npm/cli#4828). Install again without `--omit=optional`, or delete `package-lock.json` and `node_modules` and run `npm install` | infrastructure | 3 |
131
170
  | `ENGINE_FAILURE` | Any engine fault with no more specific mapping | infrastructure | 3 |
132
- | `WORKER_PROTOCOL` | A worker reported the wrong unit, an attempt overlapped a live attempt, `openHost` tried to acquire an engine instance already owned by another host, or `host.openSession` tried to reuse a closed host or overlap an opening, live, or closing session | infrastructure | 3 |
171
+ | `WORKER_PROTOCOL` | A worker reported the wrong unit, an attempt overlapped a live attempt, `openHost` tried to acquire an engine instance another host owns, or `host.openSession` tried to reuse a closed host or overlap an opening, live, or closing session | infrastructure | 3 |
133
172
  | `SESSION_KEY_INVALID` | A worker received a session key that is not 32 bytes. Packaging or IPC fault. | infrastructure | 3 |
134
- | `REPORT_WRITE_FAILED` | `report.json` or `ai-trace.json` could not be written. Recorded as a run error; the in-memory outcome is still complete. A `junit.xml` that could not be written is the `junit` reporter's stderr line instead | infrastructure | 3 |
173
+ | `REPORT_WRITE_FAILED` | `report.json` or `ai-trace.json` could not be written. Recorded as a run error; the in-memory outcome is still complete. A `junit.xml` that could not be written is a line on the `junit` reporter's stderr instead | infrastructure | 3 |
135
174
  | `EVALUATE_FAILED` | `browser.evaluate` threw inside the page (`@e2e-dev/web`) | test | 1 |
136
- | `INTERRUPTED` | A user or CI signal aborted the run. A test cut short by `--max-failures` or the time limit also ends with this code, its message naming the limit; that run exits 1, not 130 | interrupted | 130 |
137
-
138
- Errors raised by argument validation in test code use `INVALID_ARGUMENT` and
139
- `INVALID_LOCATOR`, both test failures with exit code 1. An unclassified thrown
140
- value from test code is reported as `ERROR`. The `internal` class and exit
141
- code 4 are reserved for runner invariant violations; no first-party code raises
142
- a code in that class today.
175
+ | `INTERRUPTED` | A user or CI signal aborted the run. A test cut short by `--max-failures` or the time limit also ends with this code, and its message names the limit; that run exits 1, not 130 | interrupted | 130 |
143
176
 
144
- ## Exit codes
177
+ #### Test code errors
145
178
 
146
- For a mixed run the highest-precedence code present wins, `130 > 4 > 3 > 2 >
147
- 1 > 0`. The report keeps every individual result. A setup assertion failure
148
- is a test failure and skips its dependents. Missing setup infrastructure keeps
149
- its configuration or infrastructure class.
179
+ | Code | When it happens | Class | Exit |
180
+ | --- | --- | --- | ---: |
181
+ | `INVALID_ARGUMENT` | Argument validation in test code refused an argument | test | 1 |
182
+ | `INVALID_LOCATOR` | Argument validation in test code refused a locator's arguments | test | 1 |
183
+ | `ERROR` | A value thrown from test code that no other code classifies | test | 1 |