diff --git a/.changeset/atomic-pinned-locate.md b/.changeset/atomic-pinned-locate.md
deleted file mode 100644
index 7e7f53864..000000000
--- a/.changeset/atomic-pinned-locate.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
-"@e2e-dev/web": patch
----
-
-A locator whose match the page replaces on every animation frame (a field re-rendered each frame) no longer fails with `LOCATOR_NOT_FOUND` "node became stale" on a loaded machine. Locate read each match in a protocol call after the one that found it, so when a frame passed between the two, which a busy runner makes likely every time, the read found the match detached and the locate retried until the action timeout. Each match is now read in the same in-page task that finds it, so the read never sees a replaced element. A `getByLabel` or `getByDisplayValue` match is still pinned to the element that was read.
-
-A `browser.locator` selector with a `*` capture before its last part (`*css=article >> text=Hello`) now fails with `INVALID_LOCATOR` instead of resolving; use `filter({ has })`. A capture rewrites the chain so the elements it returns are never the ones the rest of the locate reads, and composing anything onto such a selector already matched the captured element instead of its descendants.
diff --git a/.changeset/codex-client-version.md b/.changeset/codex-client-version.md
deleted file mode 100644
index 928f80757..000000000
--- a/.changeset/codex-client-version.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'e2e': patch
----
-
-`e2e models openai` asks the ChatGPT backend for its model list as Codex CLI 0.160.0 (was 0.155.1). The backend hides models from clients it considers too old, so models unlocked since 0.155.1 now show up and can be passed to `chatgpt()`.
diff --git a/.changeset/copilot-unusable-models.md b/.changeset/copilot-unusable-models.md
deleted file mode 100644
index 8263c6245..000000000
--- a/.changeset/copilot-unusable-models.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'e2e': patch
----
-
-`e2e models github-copilot` marks the models `copilot()` cannot call: `not enabled` for a model your plan has not turned on, and `no chat completions` for a model Copilot serves only over other APIs such as Responses. These used to be listed like any other model and then failed the first agent step with "The requested model is not supported".
diff --git a/.changeset/decision-executor.md b/.changeset/decision-executor.md
new file mode 100644
index 000000000..9c7941594
--- /dev/null
+++ b/.changeset/decision-executor.md
@@ -0,0 +1,11 @@
+---
+"@e2e-dev/decision": minor
+---
+
+Add `@e2e-dev/decision`, an executor that runs `agent.act` and `agent.assert`
+through a decision model instead of an LLM. It takes any AI SDK evaluation
+model with choice distributions and decides through `experimental_evaluate`,
+one call per action with the operation and target questions fanned out; a
+small language model writes field values when the choice is `type`. Tests
+stay plain natural language with no params. Probability and confidence
+gates are off by default.
diff --git a/.changeset/download-full-ledger-pass.md b/.changeset/download-full-ledger-pass.md
new file mode 100644
index 000000000..7e27a9243
--- /dev/null
+++ b/.changeset/download-full-ledger-pass.md
@@ -0,0 +1,5 @@
+---
+"e2e": patch
+---
+
+A text download is rewritten with the full ledger pass — fragments and base64-encoded forms of a secret included — the way trace text entries already are. The download path only matched whole values while still labeling the file `redaction: "complete"`, so a cut or base64 copy of a secret could sit in a download the report vouched for as clean.
diff --git a/.changeset/failure-evidence-budget.md b/.changeset/failure-evidence-budget.md
new file mode 100644
index 000000000..7e2d44bad
--- /dev/null
+++ b/.changeset/failure-evidence-budget.md
@@ -0,0 +1,5 @@
+---
+'e2e': patch
+---
+
+A failed attempt on an app that stops answering no longer waits 5 s for failure evidence at every capture point, or forever when the engine ignores cancellation. The runner captures the screen and screenshot once per attempt, stops waiting after 5 s, and keeps the original error.
diff --git a/.changeset/matcher-options-honored-or-rejected.md b/.changeset/matcher-options-honored-or-rejected.md
deleted file mode 100644
index 209e481d9..000000000
--- a/.changeset/matcher-options-honored-or-rejected.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
-'e2e': patch
----
-
-Locator matchers honor Playwright's `{ checked: false }`, `{ enabled: false }`, `{ visible: false }`, and `{ attached: false }`, which flip the matcher, and `{ ignoreCase }` on `toHaveText`, `toContainText`, `toHaveAccessibleName`, and `toHaveAttribute(name, value)`. Before, these were ignored at runtime, so `toBeChecked({ checked: false })` passed on a checked box and `not.toContainText('error', { ignoreCase: true })` passed on `Error`. Any other option key, or a flag that is not a boolean, is now `INVALID_ARGUMENT` before the first read, from JavaScript too.
-
-`e2e/engine` exports `rejectUnknownOptions(api, options, known, code?)`, the check the runner's own fixtures use, so an engine refuses an option its fixture methods and matchers do not take the same way.
diff --git a/.changeset/mcp-without-ai.md b/.changeset/mcp-without-ai.md
deleted file mode 100644
index 803307ffe..000000000
--- a/.changeset/mcp-without-ai.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'e2e': patch
----
-
-`e2e mcp` opens a session in a project without the optional `ai` package. Without the AI SDK, the catalog and the argument checks read each tool's Standard Schema (zod's), so a deterministic-only project (no `agents`, `e2e init` with no model gateway) can `open_session`, `observe`, `locate`, and drive the page.
diff --git a/.changeset/oxc-typescript-loader.md b/.changeset/oxc-typescript-loader.md
new file mode 100644
index 000000000..671f714f4
--- /dev/null
+++ b/.changeset/oxc-typescript-loader.md
@@ -0,0 +1,12 @@
+---
+'e2e': minor
+'@e2e-dev/web': minor
+'@e2e-dev/mobile': minor
+'@e2e-dev/kernel': minor
+'@e2e-dev/eas': minor
+'@e2e-dev/github': minor
+---
+
+**Breaking: e2e and every `@e2e-dev` package now need Node.js 22.22.3 or newer on Node.js 22, or 24.8.0 or newer** (`engines.node` is `^22.22.3 || >=24.8.0`). Node.js 22.12 through 22.22.2, 23, and 24.0 through 24.7 are no longer supported, and the CLI refuses to start on them, naming the versions to upgrade to. Those releases fail when a CommonJS file a test imports requires TypeScript through the new loader's `module.registerHooks` (nodejs/node#59679 fixed it in 22.22.3 and 24.8.0).
+
+e2e now loads TypeScript with its own loader, built on oxc, instead of tsx. tsx brought in esbuild, whose postinstall script made every `pnpm install` on pnpm 11 and later fail with `ERR_PNPM_IGNORED_BUILDS` until the project approved the build; nothing e2e installs runs an install script now. Config and tests load as before: ESM whatever `package.json` says, `./x.js` and `./x` resolve to `x.ts`, directory index imports, tsconfig `paths` and `baseUrl`, workspace packages exporting `.ts` source, JSX in `.tsx` and `.jsx`, enums, namespaces, `experimentalDecorators`, and source-mapped stack traces. JSON imported without `with { type: 'json' }` still loads. A test file whose name holds `%`, `#`, or a space now collects with its source location instead of failing with "URI malformed" or losing the location. Each file reads its nearest `tsconfig.json` rather than the one in the working directory. What changes: `import` statements in a `.cts` file are no longer compiled into `require` calls (write `import x = require()`); standard decorators without `experimentalDecorators`, auto-accessors, and ES module `import` and value `export` declarations in `.cts` are refused with their line (`import type`, `export type`, `import x = require()`, and `export =` work); the CLI refuses the Bun (`bun --bun`) and Deno runtimes (install with any package manager, run on Node.js); a type imported without `import type` that stays in the output (a decorated member's type under `emitDecoratorMetadata`) fails to load, with an error naming the import; a `.ts` file a CommonJS file requires runs as an ES module like every other `.ts`, so one written with `module.exports` needs the `.cts` extension; and an import written in a JavaScript file resolves as Node.js resolves it, without TypeScript's extension rules or tsconfig `paths`. TypeScript inside an installed package compiles without the project's `tsconfig.json`.
diff --git a/.changeset/plain-string-secret-trace.md b/.changeset/plain-string-secret-trace.md
new file mode 100644
index 000000000..754bb39b2
--- /dev/null
+++ b/.changeset/plain-string-secret-trace.md
@@ -0,0 +1,5 @@
+---
+'e2e': patch
+---
+
+A secret value a test passes as a plain string (spelled into an `app.open` URL, typed with `fill`) no longer stays in the Playwright trace. Traces and text downloads are now rewritten whenever the session knows a secret value (every static one, and a provider's once resolved), not only after a secret fill or an engine-held secret, so they are labeled `redaction: "complete"` instead of `not-required` or `incomplete`. Screencast frames are still dropped only after a secret fill. Like any secret trace, a rewritten one masks unrelated text that shares 8 characters with a value.
diff --git a/.changeset/replay-lost-list-once.md b/.changeset/replay-lost-list-once.md
new file mode 100644
index 000000000..cca3d4911
--- /dev/null
+++ b/.changeset/replay-lost-list-once.md
@@ -0,0 +1,5 @@
+---
+"e2e": patch
+---
+
+Replaying a repeated scroll on a list that filled the screen, when the list can no longer be found, waits for it once and then scrolls the screen for every remaining repeat. It used to wait out the full relocation backoff (about 14 seconds) before each repeat, so a recorded 4x scroll took about a minute to replay.
diff --git a/.changeset/serial-group-verdicts.md b/.changeset/serial-group-verdicts.md
new file mode 100644
index 000000000..36ee965e0
--- /dev/null
+++ b/.changeset/serial-group-verdicts.md
@@ -0,0 +1,5 @@
+---
+"e2e": patch
+---
+
+A serial group whose session fails to launch, or whose file fails to load, now fails the run. Before, every member was skipped and the run passed with exit code 0. The first member now fails with the launch error and the rest skip with cause `serial-predecessor-failed`, so the `list` reporter, `summary.md`, and `junit.xml` show the failure. A retry that fails to launch keeps the members' verdicts from the attempt before it, so a member that failed there still fails. An infrastructure error in a serial group, at launch or in a member, now exits 3 and sets the run status to `error`, as it does for any other test, instead of exiting 1. When a worker crashes during a serial group, the member that was running fails with `WORKER_CRASH` instead of being skipped as if it never started.
diff --git a/.changeset/slim-install.md b/.changeset/slim-install.md
deleted file mode 100644
index d21572e7d..000000000
--- a/.changeset/slim-install.md
+++ /dev/null
@@ -1,10 +0,0 @@
----
-'e2e': patch
-'@e2e-dev/web': patch
-'@e2e-dev/mobile': patch
-'@e2e-dev/github': patch
-'@e2e-dev/kernel': patch
-'@e2e-dev/eas': patch
----
-
-Installing `e2e` pulls in 29 packages instead of 117 and takes about 31MB instead of 36MB. `e2e mcp` now runs on the split MCP SDK (`@modelcontextprotocol/server` 2.2.0) in place of `@modelcontextprotocol/sdk`, which brought in express, hono, and the rest of an HTTP server stack that stdio never used. The server keeps the same protocol version, so existing clients connect as before. Packages are built and published without sourcemaps, which pointed at a `src/` that was never shipped. Stack traces show `dist/` positions.
diff --git a/.changeset/telemetry-mcp-explore-config.md b/.changeset/telemetry-mcp-explore-config.md
deleted file mode 100644
index 1775d22dd..000000000
--- a/.changeset/telemetry-mcp-explore-config.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'e2e': minor
----
-
-Anonymous telemetry covers `e2e mcp` and `e2e explore`. Each MCP session sends one `e2e_mcp_session` event when it closes or fails to open: the client's self-reported name and version, the platform and engine, how it ended, and its tool calls counted by the runner's own tool names with the error codes they failed with. `e2e_run_completed` adds `command`, counts and option ids of the config features the run used, and for `e2e explore` why it stopped and its steps and findings by kind and severity. Beyond the client's own name, no project names, paths, tool arguments or results, goals, or findings are sent. The notice shows again once; `E2E_TELEMETRY_DEBUG=1` prints every event, and the opt-outs are unchanged.
diff --git a/.changeset/web-matcher-options-rejected.md b/.changeset/web-matcher-options-rejected.md
deleted file mode 100644
index 69c70e823..000000000
--- a/.changeset/web-matcher-options-rejected.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@e2e-dev/web': patch
----
-
-`expect(browser).toHaveURL`, `toHaveTitle`, `toHaveClass`, and `browser.waitForURL` refuse an option other than `timeout` with `INVALID_ARGUMENT` instead of ignoring it, so Playwright's `ignoreCase` on `toHaveURL` or `waitUntil` on `waitForURL` no longer runs as if absent.
diff --git a/.claude/skills/verify/SKILL.md b/.claude/skills/verify/SKILL.md
index 03767e001..42484baad 100644
--- a/.claude/skills/verify/SKILL.md
+++ b/.claude/skills/verify/SKILL.md
@@ -16,7 +16,7 @@ repeat.
```bash
pnpm install --frozen-lockfile
-pnpm --filter @e2e-dev/web exec playwright install chromium
+pnpm --filter @e2e-dev/web exec playwright-core install chromium
pnpm build # every consumer below runs dist; rebuild after each change
```
diff --git a/.fallowrc.json b/.fallowrc.json
index 779d9cb90..7df1d7782 100644
--- a/.fallowrc.json
+++ b/.fallowrc.json
@@ -16,8 +16,12 @@
"apps/mobile-benchmark/index.ts",
"packages/e2e/tests/types/sdk-types.ts",
"packages/web/tests/types/connection.ts",
+ "packages/web/tests/types/init-scripts.ts",
+ "packages/web/tests/types/routes.ts",
"packages/mobile/tests/types/options.ts",
- "packages/e2e/tests/live/*.ts"
+ "packages/decision/tests/types/options.ts",
+ "packages/e2e/tests/live/*.ts",
+ "packages/e2e/src/config/register.ts"
],
"ignorePatterns": [
"docs/**",
@@ -30,7 +34,8 @@
"@e2e-dev/web",
"@openrouter/ai-sdk-provider",
"@expo/fingerprint",
- "@expo/repack-app"
+ "@expo/repack-app",
+ "@oxc-project/runtime"
],
"rules": {
"unused-class-member": "warn",
diff --git a/.github/workflows/agent.yml b/.github/workflows/agent.yml
index a1a4e4081..cb09710d7 100644
--- a/.github/workflows/agent.yml
+++ b/.github/workflows/agent.yml
@@ -96,7 +96,7 @@ jobs:
fi
- run: pnpm install --frozen-lockfile
- name: Install browsers
- run: pnpm --filter @e2e-dev/web exec playwright install chromium --with-deps
+ run: pnpm --filter @e2e-dev/web exec playwright-core install chromium --with-deps
- name: Build
run: pnpm run build
- name: Run ${{ matrix.script }}
diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml
index 977bb94ba..67078d505 100644
--- a/.github/workflows/benchmark.yml
+++ b/.github/workflows/benchmark.yml
@@ -67,7 +67,7 @@ jobs:
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: Install browsers
- run: pnpm --filter @e2e-dev/web exec playwright install chromium --with-deps
+ run: pnpm --filter @e2e-dev/web exec playwright-core install chromium --with-deps
- name: Build
run: pnpm run build
- name: Run test
@@ -132,7 +132,7 @@ jobs:
fi
- run: pnpm install --frozen-lockfile
- name: Install browsers
- run: pnpm --filter @e2e-dev/web exec playwright install chromium --with-deps
+ run: pnpm --filter @e2e-dev/web exec playwright-core install chromium --with-deps
- name: Build
run: pnpm run build
- name: Run test:agent
diff --git a/.github/workflows/spec.yml b/.github/workflows/spec.yml
index 95d1a08e1..a1a0fa8a5 100644
--- a/.github/workflows/spec.yml
+++ b/.github/workflows/spec.yml
@@ -38,12 +38,14 @@ jobs:
run: pnpm docs:check-errors
- name: Peer ranges on the runner
run: pnpm check:peer-ranges
+ - name: No install scripts in published dependency trees
+ run: pnpm check:install-scripts
- name: Agent skill scripts
run: |
pnpm exec tsc -p .claude/skills/babysit/scripts
node --test .claude/skills/babysit/scripts/pr-status.test.ts
- name: Install browsers
- run: pnpm --filter @e2e-dev/web exec playwright install chromium --with-deps
+ run: node packages/web/dist/bin.js install chromium --with-deps
- name: Testbed
run: pnpm --filter @e2e-dev/testbed test
@@ -53,16 +55,41 @@ jobs:
strategy:
fail-fast: false
matrix:
- node: [22, 24, 26]
+ # The newest release of each line, and the floors in engines.node.
+ node: [22, 24, 26, 22.22.3, 24.8.0]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pnpm/action-setup@9fd676a19091d4595eefd76e4bd31c97133911f1 # v4.2.0
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: ${{ matrix.node }}
+ # A major alone resolves to the runner's cached release, which can
+ # sit below the patch floor in engines.node (22.22.3).
+ check-latest: true
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: Install browsers
- run: pnpm --filter @e2e-dev/web exec playwright install chromium --with-deps
+ run: pnpm --filter @e2e-dev/web exec playwright-core install chromium --with-deps
- name: Unit and integration tests
run: pnpm test
+
+ # A new user's first install of the packed packages, which pnpm 11 and
+ # later fail on any dependency build script (scripts/install-smoke.ts).
+ install:
+ runs-on: blacksmith-4vcpu-ubuntu-2404
+ timeout-minutes: 15
+ strategy:
+ fail-fast: false
+ matrix:
+ pnpm: [11, 12]
+ steps:
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ - uses: pnpm/action-setup@9fd676a19091d4595eefd76e4bd31c97133911f1 # v4.2.0
+ - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
+ with:
+ node-version: 26
+ cache: pnpm
+ - run: pnpm install --frozen-lockfile
+ - run: pnpm run build
+ - name: Fresh install with pnpm ${{ matrix.pnpm }}
+ run: node scripts/install-smoke.ts ${{ matrix.pnpm }}
diff --git a/AGENTS.md b/AGENTS.md
index c12d1fee0..4f761c8c8 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -28,7 +28,6 @@ There is no separate spec. The code is the contract, pinned in three places:
There are no RFCs or design documents in the repo. The why lives in PR
descriptions and commit bodies; `git log` and `gh pr view` are the archive.
-
## Layout
`packages/` holds what publishes to npm; `apps/` holds the private apps and
@@ -52,8 +51,10 @@ suites that consume the built packages the way a user would.
the `act` socket with a queue executor so every MCP call is a harness
action), `src/oauth/` subscription sign-in (the `e2e login`, `logout`,
and `models` commands and the `e2e/oauth/*` model constructors; each
- constructor subpath is the only place its `@ai-sdk/*` optional peer is
- imported, so the CLI boots without them). The constructors and the CLI
+ constructor subpath is the place its `@ai-sdk/*` optional peers are
+ imported, and the Copilot constructor loads `@ai-sdk/openai` lazily, only
+ for the Responses models, so the CLI boots without any of them). The
+ constructors and the CLI
are the whole public surface: the flows, stores, and fetch behind them
are module-private, not a library for other products. `tests/live/` holds hand-run
checks that need a stored login and are never part of `pnpm test`.
@@ -136,7 +137,7 @@ suites that consume the built packages the way a user would.
Build first — nearly everything downstream consumes `dist`.
```bash
-pnpm check # lint -> check:dead-code -> typecheck -> docs:check-errors -> check:peer-ranges -> docs:check (full gate)
+pnpm check # lint -> check:dead-code -> typecheck -> docs:check-errors -> check:peer-ranges -> check:install-scripts -> docs:check (full gate)
pnpm test # builds, then vitest unit + integration
pnpm test:testbed # builds, then runs the real CLI against the playground app
pnpm test:web-benchmark # builds, then runs the real CLI against the benchmark scenarios
@@ -162,7 +163,7 @@ pnpm --filter @e2e-dev/testbed run test:headed
package `typecheck` covers `tests/**`, which is what makes
`tests/types/sdk-types.ts` a test.
- Integration tests need Chromium: `pnpm --filter @e2e-dev/web exec
- playwright install chromium`. The web engine's `prepare` hook
+ playwright-core install chromium`. The web engine's `prepare` hook
also installs a missing browser once per run, in the runner, before `plan`
is emitted and the run's clock starts.
@@ -373,6 +374,13 @@ trees, on both platforms, without a device.
commit the changed entries in the same pull request as the scenario change.
The web benchmark's agent job runs with `--strict-cache`, so a recording a
change broke fails with `REPLAY_STALE` instead of quietly calling the model.
+ That includes a change to the cache key (`REPLAY_POLICY_VERSION`, a new key
+ field, an engine minor): strict lists the file store and fails a step whose
+ key misses while an entry recorded for the same step sits under another key
+ (`cache/rekeyed.ts`). Only entries whose `recordedFor` names the whole step
+ (params digest, occurrence, agent) count; a `read-write` replay completes
+ an older one. Such a change re-records every entry and deletes the old ones
+ in the same pull request. A step that was never recorded still runs live.
The web benchmark's entries are in. The mobile benchmark's iOS entries are
recorded on a Mac; nobody has recorded on an Android emulator yet, so the
Android side spends model calls until an emulator recording is committed.
@@ -394,8 +402,9 @@ trees, on both platforms, without a device.
secret an engine resolves for an option the app sees (basic auth) is
protected as text only: redacted everywhere text goes, pixels untouched.
One exposure level per session (`SecretExposure` in `run/secrecy.ts`)
- decides pixels, trace and download rewriting, and the taint a saved
- session carries. What
+ decides pixels and the taint a saved session carries; traces and text
+ downloads are rewritten whenever the session's ledger holds a value
+ (`redactsRecordings`), since a plain string reaches the app unseen. What
an executor keeps in `attempt.memory` is its own; the harness never
reports it.
- An agent's secret fill is authorized by the runner, not the model.
@@ -417,10 +426,15 @@ trees, on both platforms, without a device.
denied destinations, forbidden fills, and tainted pixels. Model text is
never evaluated as code, selectors, shell, or config. App content, ledger
text, and pixels are quoted as untrusted evidence with no policy authority.
- - Every navigation a test or the agent asks for (`app.open`, the `navigate`
- verb, `device.openLink`) goes through one rule: `file:`, `data:`, and
- `javascript:` destinations and malformed URLs are `POLICY_DENIED`. There
- is no origin or host allowlist; PR #290 removed them on purpose, since a
+ - Every navigation a test or the agent asks for (`app.open`,
+ `browser.goto`, the `navigate` verb) goes through one rule,
+ `resolveNavigationUrl`: only `http:`, `https:`, and the exact
+ `about:blank` pass; every other scheme (`view-source:file:` included) and
+ a malformed URL is `POLICY_DENIED` (a relative URL on a target with no
+ `app.url` is `APP_URL_REQUIRED`). `device.openLink` takes custom app
+ schemes, so it refuses a list instead (`packages/mobile/src/links.ts`):
+ `file:`, `data:`, `javascript:`, `view-source:`, `blob:`, `filesystem:`.
+ There is no origin or host allowlist; PR #290 removed them on purpose, since a
click reaches any origin a typed URL could.
- Sessions are per-run, target-bound, AES-256-GCM encrypted with a
memory-only key, and deleted at cleanup; payloads never enter diagnostics.
@@ -431,7 +445,9 @@ trees, on both platforms, without a device.
sandbox with no secrets or write tokens.
- CI: `.github/workflows/spec.yml` runs lint, typecheck, and the testbed on
- Node 26 and `pnpm test` on Node 22, 24, and 26; `benchmark.yml` runs the
+ Node 26, `pnpm test` on the newest Node 22, 24, and 26 and on the
+ `engines.node` floors (22.22.3, 24.8.0), and `scripts/install-smoke.ts`, a
+ fresh install of the packed packages with pnpm 11 and 12; `benchmark.yml` runs the
web benchmark's two suites; `mobile.yml` runs the mobile benchmark's on an
iOS simulator and an Android emulator (KVM on x64 Linux). The two
benchmark workflows gate on paths: a `changes` job (dorny/paths-filter
@@ -508,7 +524,7 @@ trees, on both platforms, without a device.
needs `node scripts/restore-peer-ranges.ts` after it, or `pnpm check` fails
on the pin.
- The runner publishes as the unscoped `e2e` (entry points `e2e`, `e2e/agent`,
- `e2e/engine`, `e2e/oauth/chatgpt`, `e2e/oauth/copilot`, `e2e/oauth/grok`; the bin is `e2e` too); engines, reporters, and integrations publish public
+ `e2e/engine`, `e2e/oauth/chatgpt`, `e2e/oauth/copilot`, `e2e/oauth/grok`, `e2e/oauth/opencode-console`; the bin is `e2e` too); engines, reporters, and integrations publish public
under the `@e2e-dev` scope. The `@e2edev` scope (moved to `@e2e-dev` on
2026-09-28), `@e2edev/e2e`, `@e2edev/oauth` (folded into `e2e/oauth` on
2026-09-21), and `@e2e-dev/integrations` (moved to `@e2e-dev/kernel` on
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 93827362c..07abd007d 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -43,6 +43,7 @@ This project is a pnpm monorepo. `packages/` holds what publishes to npm,
- `packages/e2e`: the published `e2e` package (SDK, runner, CLI, engine contract)
- `packages/web`: the published `@e2e-dev/web` browser engine
- `packages/mobile`: the published `@e2e-dev/mobile` mobile engine
+- `packages/decision`: the decision-model executor on the public `StepExecutor` contract
- `apps/testbed`: private dogfood suite that consumes the built packages
- `apps/web-benchmark`: private Next.js app of hard-surface scenarios plus the e2e suites written against them
- `apps/mobile-benchmark`: private Expo app of hard mobile surfaces plus the e2e suites written against them
@@ -71,7 +72,7 @@ pnpm test # unit + integration
Integration tests need Chromium:
```sh
-pnpm --filter @e2e-dev/web exec playwright install chromium
+pnpm --filter @e2e-dev/web exec playwright-core install chromium
```
Docs are part of the change, not a follow-up. A behavior change updates its
@@ -117,8 +118,8 @@ We use [changesets](https://github.com/changesets/changesets) to publish new
versions. It handles bumping the version based on semver, writing the
changelog, and creating tags and GitHub releases.
-If your change affects `e2e`, `@e2e-dev/web`, or
-`@e2e-dev/mobile`, add a changeset:
+If your change affects `e2e`, `@e2e-dev/web`, `@e2e-dev/mobile`, or
+`@e2e-dev/decision`, add a changeset:
```sh
pnpm changeset
diff --git a/README.md b/README.md
index 0bcb1f5a2..65bf73b19 100644
--- a/README.md
+++ b/README.md
@@ -50,6 +50,7 @@ config and an example test. The
| [`@e2e-dev/github`](https://www.npmjs.com/package/@e2e-dev/github) | Reporter that posts results as a pull request comment. |
| [`@e2e-dev/kernel`](https://www.npmjs.com/package/@e2e-dev/kernel) | Kernel hosted browsers for the web engine. |
| [`@e2e-dev/eas`](https://www.npmjs.com/package/@e2e-dev/eas) | EAS Simulators hosted iOS simulators and Android emulators for the mobile engine. |
+| [`@e2e-dev/decision`](https://e2e.tester.army/docs/decision-models) | Decision-model executors for bounded semantic actions and assertions. |
| [`@e2e-dev/testmu`](https://www.npmjs.com/package/@e2e-dev/testmu) | TestMu AI (formerly LambdaTest) hosted Android emulators, iOS simulators, and real devices for the mobile engine. |
## Documentation
diff --git a/SECURITY.md b/SECURITY.md
index 3126c7125..75bbae0c7 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -55,8 +55,11 @@ with a memory-only key, and deleted at cleanup.
## Navigation and origins
-A test or the agent may open any http(s) URL; `file:`, `data:`, and
-`javascript:` are denied. There is no origin allowlist, on navigation or on
+On the web a test or the agent may open any http(s) URL, and `about:blank`;
+every other scheme (`file:`, `data:`, `javascript:`, `view-source:`, `blob:`)
+is denied. A device link may use an app's custom scheme, so
+`device.openLink` refuses a list instead: `file:`, `data:`, `javascript:`,
+`view-source:`, `blob:`, and `filesystem:`. There is no origin allowlist, on navigation or on
secret fills. A click, a redirect, or a popup reaches another origin just as
a typed URL would, so a gate on typed navigation guarded nothing; and a
secret is only ever typed into a field the test itself handed to the step, a
diff --git a/apps/testbed/app/pages/controls.mjs b/apps/testbed/app/pages/controls.mjs
index c9da66864..47b178f37 100644
--- a/apps/testbed/app/pages/controls.mjs
+++ b/apps/testbed/app/pages/controls.mjs
@@ -219,6 +219,7 @@ const pages = {
+
`,
}),
diff --git a/apps/testbed/package.json b/apps/testbed/package.json
index d1b7b889d..715e90198 100644
--- a/apps/testbed/package.json
+++ b/apps/testbed/package.json
@@ -24,7 +24,6 @@
"@ai-sdk/devtools": "1.0.22",
"@types/node": "26.6.2",
"ai": "7.0.107",
- "playwright": "1.63.0",
"typescript": "7.0.2",
"zod": "4.6.1"
}
diff --git a/apps/testbed/tests/browser.e2e.ts b/apps/testbed/tests/browser.e2e.ts
index 933419ba5..4e49391ab 100644
--- a/apps/testbed/tests/browser.e2e.ts
+++ b/apps/testbed/tests/browser.e2e.ts
@@ -33,6 +33,13 @@ test.describe('browser fixture', { requires: ['browser'], tags: ['browser'] }, (
await expect(screen.getByLabel('Loads')).toHaveText('loads: 2');
});
+ test('an init script runs before the page reads it, from the next load on', async ({ screen, browser }) => {
+ await expect(screen.getByLabel('Random')).toHaveText('random: unseeded');
+ await browser.addInitScript((value) => { Math.random = () => value; }, 0.5);
+ await browser.reload();
+ await expect(screen.getByLabel('Random')).toHaveText('random: seeded');
+ });
+
test('the viewport size is what the page measures', async ({ screen, browser }) => {
await browser.setViewport({ width: 500, height: 700 });
await expect(screen.getByLabel('Viewport')).toHaveText('500x700');
diff --git a/apps/web-benchmark/.e2e/cache/262851ad51db31a58d9bc17bea285445b7ce2b9a260883a89737a083c4dd8bd1.json b/apps/web-benchmark/.e2e/cache/02156375f0838e2b8dc5966f63159826d1953e0248c4bb1d49243a7fed208aba.json
similarity index 64%
rename from apps/web-benchmark/.e2e/cache/262851ad51db31a58d9bc17bea285445b7ce2b9a260883a89737a083c4dd8bd1.json
rename to apps/web-benchmark/.e2e/cache/02156375f0838e2b8dc5966f63159826d1953e0248c4bb1d49243a7fed208aba.json
index 45f244540..f2b38f32d 100644
--- a/apps/web-benchmark/.e2e/cache/262851ad51db31a58d9bc17bea285445b7ce2b9a260883a89737a083c4dd8bd1.json
+++ b/apps/web-benchmark/.e2e/cache/02156375f0838e2b8dc5966f63159826d1953e0248c4bb1d49243a7fed208aba.json
@@ -1,6 +1,6 @@
{
"schemaVersion": "trace-1",
- "createdAt": "2026-09-24T19:35:15.679Z",
+ "createdAt": "2026-10-04T08:29:27.731Z",
"payload": {
"actions": [
{
@@ -24,9 +24,12 @@
"recordedFor": {
"testId": "tests-agent/control-inventory.e2e.ts::control%20inventory::back%20returns%20from%20the%20details%20view%20to%20the%20inventory",
"targetId": "web",
- "instructionDigest": "b654f62f1b06969b737e6bc5900524da39b012965a0d82aef45fce7af099ba6c"
+ "instructionDigest": "b654f62f1b06969b737e6bc5900524da39b012965a0d82aef45fce7af099ba6c",
+ "paramsDigest": "44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
+ "callIndex": 0,
+ "agent": "default"
},
- "summary": "Opened the Details view, then used browser history to return to the inventory. The inventory is back on screen and Navigation state reads “back on the inventory.”.",
+ "summary": "Opened Details, then used browser history to return to /e/control-inventory. The Navigation state now reads “back on the inventory.”",
"startPath": "/e/control-inventory",
"endPath": "/e/control-inventory",
"endAnchors": [
@@ -36,6 +39,13 @@
"text": "back on the inventory"
}
],
- "endWaitMs": 12026
+ "goneAnchors": [
+ {
+ "role": "status",
+ "name": "Navigation state",
+ "text": "not opened"
+ }
+ ],
+ "endWaitMs": 12570
}
}
diff --git a/apps/web-benchmark/.e2e/cache/5637caec136ab6deac26176da39d3dffa164a00e43f1576cc62708becbfad6e3.json b/apps/web-benchmark/.e2e/cache/037d5252e59b546953ccf4a4baf4623fb4ba6df76f87bcd3a6a99f25ab88b060.json
similarity index 57%
rename from apps/web-benchmark/.e2e/cache/5637caec136ab6deac26176da39d3dffa164a00e43f1576cc62708becbfad6e3.json
rename to apps/web-benchmark/.e2e/cache/037d5252e59b546953ccf4a4baf4623fb4ba6df76f87bcd3a6a99f25ab88b060.json
index 7ea51f1b5..620707c1a 100644
--- a/apps/web-benchmark/.e2e/cache/5637caec136ab6deac26176da39d3dffa164a00e43f1576cc62708becbfad6e3.json
+++ b/apps/web-benchmark/.e2e/cache/037d5252e59b546953ccf4a4baf4623fb4ba6df76f87bcd3a6a99f25ab88b060.json
@@ -1,17 +1,16 @@
{
"schemaVersion": "trace-1",
- "createdAt": "2026-09-23T13:30:18.310Z",
+ "createdAt": "2026-10-04T08:31:18.423Z",
"payload": {
"actions": [
{
- "name": "scroll",
- "summary": "scroll down on node \"scroll-container\"",
- "direction": "down",
+ "name": "scrollTo",
+ "summary": "scroll button \"Accept terms\" into view",
"target": {
- "testId": "scroll-container"
- },
- "times": 10,
- "spans": 0.3083
+ "role": "button",
+ "name": "Accept terms",
+ "testId": "accept-button"
+ }
},
{
"name": "tap",
@@ -30,9 +29,12 @@
"recordedFor": {
"testId": "tests-agent/scenarios.e2e.ts::act%20completes%20sticky-chrome",
"targetId": "web",
- "instructionDigest": "507883318a8655d47afe9f680ee91cde1ae64055fbef530129cc289101a30c23"
+ "instructionDigest": "507883318a8655d47afe9f680ee91cde1ae64055fbef530129cc289101a30c23",
+ "paramsDigest": "44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
+ "callIndex": 0,
+ "agent": "default"
},
- "summary": "Scrolled the terms container until the Accept terms button was clear of the sticky footer, then clicked it. The page now shows “Terms accepted.”",
+ "summary": "Scrolled until the Accept terms button was clear above the sticky footer, clicked it once, and verified the “Terms accepted” confirmation.",
"startPath": "/e/sticky-chrome",
"endPath": "/e/sticky-chrome",
"endAnchors": [
@@ -41,6 +43,6 @@
"testId": "success-message"
}
],
- "endWaitMs": 10124
+ "endWaitMs": 11764
}
}
diff --git a/apps/web-benchmark/.e2e/cache/e231cfcb187fa226df103f2b97077e50581951814460bfcf67de3ec631840552.json b/apps/web-benchmark/.e2e/cache/0fcdfb6a36534c4f3699d73501494c9322056c4b915e8c6566f26daa1b3b5f2b.json
similarity index 66%
rename from apps/web-benchmark/.e2e/cache/e231cfcb187fa226df103f2b97077e50581951814460bfcf67de3ec631840552.json
rename to apps/web-benchmark/.e2e/cache/0fcdfb6a36534c4f3699d73501494c9322056c4b915e8c6566f26daa1b3b5f2b.json
index c05c47bd1..5227c66f6 100644
--- a/apps/web-benchmark/.e2e/cache/e231cfcb187fa226df103f2b97077e50581951814460bfcf67de3ec631840552.json
+++ b/apps/web-benchmark/.e2e/cache/0fcdfb6a36534c4f3699d73501494c9322056c4b915e8c6566f26daa1b3b5f2b.json
@@ -1,6 +1,6 @@
{
"schemaVersion": "trace-1",
- "createdAt": "2026-09-23T13:30:32.475Z",
+ "createdAt": "2026-10-04T08:32:21.685Z",
"payload": {
"actions": [
{
@@ -71,9 +71,12 @@
"recordedFor": {
"testId": "tests-agent/scenarios.e2e.ts::act%20completes%20otp-auto-advance",
"targetId": "web",
- "instructionDigest": "c878aa968fdbc93d377500f8aecb2f3b0ab701c37b7f9757f8d993f0fc87e109"
+ "instructionDigest": "c878aa968fdbc93d377500f8aecb2f3b0ab701c37b7f9757f8d993f0fc87e109",
+ "paramsDigest": "44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
+ "callIndex": 0,
+ "agent": "default"
},
- "summary": "Entered the displayed code 493027 into the six segmented inputs. The page now shows “Code verified.”",
+ "summary": "Entered the displayed code 493027 into the six segmented inputs. The page now displays “Code verified.”",
"startPath": "/e/otp-auto-advance",
"endPath": "/e/otp-auto-advance",
"endAnchors": [
@@ -82,6 +85,41 @@
"testId": "success-message"
}
],
- "endWaitMs": 10521
+ "goneAnchors": [
+ {
+ "text": "Enter code 493027."
+ },
+ {
+ "role": "textbox",
+ "testId": "otp-input-0",
+ "inputPurpose": "one-time-code"
+ },
+ {
+ "role": "textbox",
+ "testId": "otp-input-1",
+ "inputPurpose": "one-time-code"
+ },
+ {
+ "role": "textbox",
+ "testId": "otp-input-2",
+ "inputPurpose": "one-time-code"
+ },
+ {
+ "role": "textbox",
+ "testId": "otp-input-3",
+ "inputPurpose": "one-time-code"
+ },
+ {
+ "role": "textbox",
+ "testId": "otp-input-4",
+ "inputPurpose": "one-time-code"
+ },
+ {
+ "role": "textbox",
+ "testId": "otp-input-5",
+ "inputPurpose": "one-time-code"
+ }
+ ],
+ "endWaitMs": 14371
}
}
diff --git a/apps/web-benchmark/.e2e/cache/16433aa0d367e6f0dfe2857d4621edd499a5f4343337c8803c316fde3eb9da45.json b/apps/web-benchmark/.e2e/cache/16433aa0d367e6f0dfe2857d4621edd499a5f4343337c8803c316fde3eb9da45.json
deleted file mode 100644
index 667af6966..000000000
--- a/apps/web-benchmark/.e2e/cache/16433aa0d367e6f0dfe2857d4621edd499a5f4343337c8803c316fde3eb9da45.json
+++ /dev/null
@@ -1,154 +0,0 @@
-{
- "schemaVersion": "trace-1",
- "createdAt": "2026-09-25T08:02:14.210Z",
- "payload": {
- "actions": [
- {
- "name": "type",
- "summary": "type \"release approved\" into textbox \"editor\"",
- "target": {
- "role": "textbox",
- "testId": "editor"
- },
- "value": "release approved"
- },
- {
- "name": "press",
- "summary": "press \"Control+Shift+ArrowLeft\" on textbox \"release approved\"",
- "target": {
- "role": "textbox",
- "text": "release approved",
- "testId": "editor"
- },
- "key": "Control+Shift+ArrowLeft"
- },
- {
- "name": "press",
- "summary": "press \"ArrowRight\" on textbox \"release approved\"",
- "target": {
- "role": "textbox",
- "text": "release approved",
- "testId": "editor"
- },
- "key": "ArrowRight"
- },
- {
- "name": "press",
- "summary": "press \"Shift+ArrowLeft\" on textbox \"release approved\"",
- "target": {
- "role": "textbox",
- "text": "release approved",
- "testId": "editor"
- },
- "key": "Shift+ArrowLeft"
- },
- {
- "name": "press",
- "summary": "press \"Shift+ArrowLeft\" on textbox \"release approved\"",
- "target": {
- "role": "textbox",
- "text": "release approved",
- "testId": "editor"
- },
- "key": "Shift+ArrowLeft"
- },
- {
- "name": "press",
- "summary": "press \"Shift+ArrowLeft\" on textbox \"release approved\"",
- "target": {
- "role": "textbox",
- "text": "release approved",
- "testId": "editor"
- },
- "key": "Shift+ArrowLeft"
- },
- {
- "name": "press",
- "summary": "press \"Shift+ArrowLeft\" on textbox \"release approved\"",
- "target": {
- "role": "textbox",
- "text": "release approved",
- "testId": "editor"
- },
- "key": "Shift+ArrowLeft"
- },
- {
- "name": "press",
- "summary": "press \"Shift+ArrowLeft\" on textbox \"release approved\"",
- "target": {
- "role": "textbox",
- "text": "release approved",
- "testId": "editor"
- },
- "key": "Shift+ArrowLeft"
- },
- {
- "name": "press",
- "summary": "press \"Shift+ArrowLeft\" on textbox \"release approved\"",
- "target": {
- "role": "textbox",
- "text": "release approved",
- "testId": "editor"
- },
- "key": "Shift+ArrowLeft"
- },
- {
- "name": "press",
- "summary": "press \"Shift+ArrowLeft\" on textbox \"release approved\"",
- "target": {
- "role": "textbox",
- "text": "release approved",
- "testId": "editor"
- },
- "key": "Shift+ArrowLeft"
- },
- {
- "name": "press",
- "summary": "press \"Shift+ArrowLeft\" on textbox \"release approved\"",
- "target": {
- "role": "textbox",
- "text": "release approved",
- "testId": "editor"
- },
- "key": "Shift+ArrowLeft"
- },
- {
- "name": "tap",
- "summary": "tap button \"B\"",
- "target": {
- "role": "button",
- "name": "B",
- "testId": "bold-button"
- }
- },
- {
- "name": "tap",
- "summary": "tap button \"Check document\"",
- "target": {
- "role": "button",
- "name": "Check document",
- "testId": "check-button"
- }
- }
- ],
- "executor": {
- "name": "e2e-default-agent",
- "version": "2"
- },
- "recordedFor": {
- "testId": "tests-agent/scenarios.e2e.ts::act%20completes%20rich-text-editor",
- "targetId": "web",
- "instructionDigest": "e37ad9ac878deebe5c840d23fded9589f67228d8229a059b855347ab5e49ab96"
- },
- "summary": "Entered “release approved,” selected exactly “approved,” applied bold, and checked the document. The page displays “Document approved.”.",
- "startPath": "/e/rich-text-editor",
- "endPath": "/e/rich-text-editor",
- "endAnchors": [
- {
- "text": "Document approved",
- "testId": "success-message"
- }
- ],
- "endWaitMs": 12292
- }
-}
diff --git a/apps/web-benchmark/.e2e/cache/8e069e91bdee5edf7f8e6ce5e2880fdb02a4f16e2f0056d3d9a1f40d507ea0f6.json b/apps/web-benchmark/.e2e/cache/1b0794f168443c714d5e4b0485f8e2a59ee9da64f952568313561faba74e666f.json
similarity index 73%
rename from apps/web-benchmark/.e2e/cache/8e069e91bdee5edf7f8e6ce5e2880fdb02a4f16e2f0056d3d9a1f40d507ea0f6.json
rename to apps/web-benchmark/.e2e/cache/1b0794f168443c714d5e4b0485f8e2a59ee9da64f952568313561faba74e666f.json
index 52673baf6..35fda1369 100644
--- a/apps/web-benchmark/.e2e/cache/8e069e91bdee5edf7f8e6ce5e2880fdb02a4f16e2f0056d3d9a1f40d507ea0f6.json
+++ b/apps/web-benchmark/.e2e/cache/1b0794f168443c714d5e4b0485f8e2a59ee9da64f952568313561faba74e666f.json
@@ -1,16 +1,16 @@
{
"schemaVersion": "trace-1",
- "createdAt": "2026-09-23T13:30:08.490Z",
+ "createdAt": "2026-10-04T08:29:16.739Z",
"payload": {
"actions": [
{
- "name": "scroll",
- "summary": "scroll down on node \"feed\"",
+ "name": "scrollUntil",
+ "summary": "scroll down on node \"feed\" until \"Golden Ticket\" shows (20 screens)",
+ "text": "Golden Ticket",
"direction": "down",
"target": {
"testId": "feed"
},
- "times": 25,
"spans": 0.3083
},
{
@@ -30,9 +30,12 @@
"recordedFor": {
"testId": "tests-agent/scenarios.e2e.ts::act%20completes%20infinite-scroll",
"targetId": "web",
- "instructionDigest": "5693e0682199f2201375a207ac10bd69b22c26925bbf8fd87e06a9f6d1abeca9"
+ "instructionDigest": "5693e0682199f2201375a207ac10bd69b22c26925bbf8fd87e06a9f6d1abeca9",
+ "paramsDigest": "44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
+ "callIndex": 0,
+ "agent": "default"
},
- "summary": "Scrolled the feed until the Golden Ticket appeared at entry 138, clicked Claim, and verified the success message “Golden Ticket claimed.”",
+ "summary": "Scrolled the feed until the Golden Ticket appeared and clicked Claim. The page now shows “Golden Ticket claimed.”",
"startPath": "/e/infinite-scroll",
"endPath": "/e/infinite-scroll",
"endAnchors": [
@@ -62,6 +65,6 @@
"text": "Item 44"
}
],
- "endWaitMs": 10404
+ "endWaitMs": 11656
}
}
diff --git a/apps/web-benchmark/.e2e/cache/c1fb51f3e63a172196dc4f237a90044582c6ede099f5cb0e8b906a83281381a6.json b/apps/web-benchmark/.e2e/cache/3f1a446b8087ce7235406cdc2d010e8a6f2dcbb3240d0dc75d18f4470055b959.json
similarity index 78%
rename from apps/web-benchmark/.e2e/cache/c1fb51f3e63a172196dc4f237a90044582c6ede099f5cb0e8b906a83281381a6.json
rename to apps/web-benchmark/.e2e/cache/3f1a446b8087ce7235406cdc2d010e8a6f2dcbb3240d0dc75d18f4470055b959.json
index a28aea152..a476121a9 100644
--- a/apps/web-benchmark/.e2e/cache/c1fb51f3e63a172196dc4f237a90044582c6ede099f5cb0e8b906a83281381a6.json
+++ b/apps/web-benchmark/.e2e/cache/3f1a446b8087ce7235406cdc2d010e8a6f2dcbb3240d0dc75d18f4470055b959.json
@@ -1,6 +1,6 @@
{
"schemaVersion": "trace-1",
- "createdAt": "2026-09-23T18:03:17.359Z",
+ "createdAt": "2026-10-04T08:31:50.082Z",
"payload": {
"actions": [
{
@@ -42,9 +42,12 @@
"recordedFor": {
"testId": "tests-agent/scenarios.e2e.ts::act%20completes%20kanban-board",
"targetId": "web",
- "instructionDigest": "79f28df979547ae9abe14bba1c341873d1aa9d74dccc07d1e76bf46c279b8cb5"
+ "instructionDigest": "79f28df979547ae9abe14bba1c341873d1aa9d74dccc07d1e76bf46c279b8cb5",
+ "paramsDigest": "44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
+ "callIndex": 0,
+ "agent": "default"
},
- "summary": "Moved Fix payment bug to Done and Refactor onboarding to In Progress, then submitted the board. The page confirmed “Board matches the goal.”.",
+ "summary": "Moved Fix payment bug to Done and Refactor onboarding to In Progress, then submitted. The page confirmed “Board matches the goal.”",
"startPath": "/e/kanban-board",
"endPath": "/e/kanban-board",
"endAnchors": [
@@ -53,6 +56,11 @@
"testId": "success-message"
}
],
- "endWaitMs": 12168
+ "goneAnchors": [
+ {
+ "text": "Empty"
+ }
+ ],
+ "endWaitMs": 11433
}
}
diff --git a/apps/web-benchmark/.e2e/cache/40c9afad8973912eaf9f2b75c5385d5ea7c9c6ec2a1b873685e9f111f6bb5189.json b/apps/web-benchmark/.e2e/cache/450275e0bc6a1e6c485db03cde0314244a5250f6ee8afab5163db3a2117c1057.json
similarity index 53%
rename from apps/web-benchmark/.e2e/cache/40c9afad8973912eaf9f2b75c5385d5ea7c9c6ec2a1b873685e9f111f6bb5189.json
rename to apps/web-benchmark/.e2e/cache/450275e0bc6a1e6c485db03cde0314244a5250f6ee8afab5163db3a2117c1057.json
index c82a2c4b7..b50a64a2d 100644
--- a/apps/web-benchmark/.e2e/cache/40c9afad8973912eaf9f2b75c5385d5ea7c9c6ec2a1b873685e9f111f6bb5189.json
+++ b/apps/web-benchmark/.e2e/cache/450275e0bc6a1e6c485db03cde0314244a5250f6ee8afab5163db3a2117c1057.json
@@ -1,11 +1,11 @@
{
"schemaVersion": "trace-1",
- "createdAt": "2026-09-24T23:04:31.378Z",
+ "createdAt": "2026-10-04T08:31:06.435Z",
"payload": {
"actions": [
{
"name": "scrollUntil",
- "summary": "scroll down on node \"table-viewport\" until \"Row 4322\" shows (432 screens)",
+ "summary": "scroll down on node \"table-viewport\" until \"Row 4322\" shows (431 screens)",
"text": "Row 4322",
"direction": "down",
"target": {
@@ -30,9 +30,12 @@
"recordedFor": {
"testId": "tests-agent/scenarios.e2e.ts::act%20completes%20virtualized-table",
"targetId": "web",
- "instructionDigest": "3e0bdeec3c893eadd972848c89ef841ec7722fd896ed12998ef945cf20af30ec"
+ "instructionDigest": "3e0bdeec3c893eadd972848c89ef841ec7722fd896ed12998ef945cf20af30ec",
+ "paramsDigest": "44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
+ "callIndex": 0,
+ "agent": "default"
},
- "summary": "The current screen shows “Golden Row claimed” while Row 4322 — Golden Row is rendered in the table, confirming the claim succeeded.",
+ "summary": "Scrolled the windowed table until Row 4322 — Golden Row was rendered, clicked Claim, and verified the “Golden Row claimed” success message.",
"startPath": "/e/virtualized-table",
"endPath": "/e/virtualized-table",
"endAnchors": [
@@ -41,27 +44,53 @@
"testId": "success-message"
},
{
- "text": "Row 4316"
+ "text": "Row 4306"
},
{
- "text": "Row 4317"
+ "text": "Row 4307"
},
{
- "text": "Row 4318"
+ "text": "Row 4308"
},
{
- "text": "Row 4319"
+ "text": "Row 4309"
},
{
- "text": "Row 4320"
+ "text": "Row 4310"
},
{
- "text": "Row 4321"
+ "text": "Row 4311"
},
{
- "text": "Row 4322 — Golden Row"
+ "text": "Row 4312"
}
],
- "endWaitMs": 27632
+ "goneAnchors": [
+ {
+ "text": "Row 1"
+ },
+ {
+ "text": "Row 2"
+ },
+ {
+ "text": "Row 3"
+ },
+ {
+ "text": "Row 4"
+ },
+ {
+ "text": "Row 5"
+ },
+ {
+ "text": "Row 6"
+ },
+ {
+ "text": "Row 7"
+ },
+ {
+ "text": "Row 8"
+ }
+ ],
+ "endWaitMs": 11397
}
}
diff --git a/apps/web-benchmark/.e2e/cache/46163014c3c423f814e3fa6c2a8864466b009e647bda83569d8627166664be9f.json b/apps/web-benchmark/.e2e/cache/46163014c3c423f814e3fa6c2a8864466b009e647bda83569d8627166664be9f.json
deleted file mode 100644
index 4db6668e1..000000000
--- a/apps/web-benchmark/.e2e/cache/46163014c3c423f814e3fa6c2a8864466b009e647bda83569d8627166664be9f.json
+++ /dev/null
@@ -1,39 +0,0 @@
-{
- "schemaVersion": "trace-1",
- "createdAt": "2026-09-23T13:29:45.906Z",
- "payload": {
- "actions": [
- {
- "name": "tool",
- "summary": "tool type (run-time value)"
- },
- {
- "name": "tap",
- "summary": "tap button \"Apply\"",
- "target": {
- "role": "button",
- "name": "Apply"
- }
- }
- ],
- "executor": {
- "name": "e2e-default-agent",
- "version": "2"
- },
- "recordedFor": {
- "testId": "tests-agent/scenarios.e2e.ts::act%20completes%20iframe-form",
- "targetId": "web",
- "instructionDigest": "5ea16155d5cd3ee4fc67e7511534964bca91116fa0412e4523428844f9f85c15"
- },
- "summary": "Read coupon code SAVE-1234 from the first frame, entered it in the checkout form in the other frame, and applied it. The page displays “Coupon applied.”.",
- "startPath": "/e/iframe-form",
- "endPath": "/e/iframe-form",
- "endAnchors": [
- {
- "text": "Coupon applied",
- "testId": "success-message"
- }
- ],
- "endWaitMs": 11883
- }
-}
diff --git a/apps/web-benchmark/.e2e/cache/629d339911fd84a9271781ed96bc9ae181f3e982f72e337d53b7dc1c9b6bd762.json b/apps/web-benchmark/.e2e/cache/4a841f63d914134982d6ad47d25a351c3f65b1642d57d569c7fc14f631d5ff7f.json
similarity index 66%
rename from apps/web-benchmark/.e2e/cache/629d339911fd84a9271781ed96bc9ae181f3e982f72e337d53b7dc1c9b6bd762.json
rename to apps/web-benchmark/.e2e/cache/4a841f63d914134982d6ad47d25a351c3f65b1642d57d569c7fc14f631d5ff7f.json
index 0bda864f4..59bd56e68 100644
--- a/apps/web-benchmark/.e2e/cache/629d339911fd84a9271781ed96bc9ae181f3e982f72e337d53b7dc1c9b6bd762.json
+++ b/apps/web-benchmark/.e2e/cache/4a841f63d914134982d6ad47d25a351c3f65b1642d57d569c7fc14f631d5ff7f.json
@@ -1,6 +1,6 @@
{
"schemaVersion": "trace-1",
- "createdAt": "2026-09-24T19:34:54.319Z",
+ "createdAt": "2026-10-04T08:29:03.565Z",
"payload": {
"actions": [
{
@@ -29,9 +29,12 @@
"recordedFor": {
"testId": "tests-agent/control-inventory.e2e.ts::control%20inventory::a%20long%20press%20and%20a%20double%20click%20are%20chosen%20over%20a%20plain%20tap",
"targetId": "web",
- "instructionDigest": "9fe8b7fcfeb1bd36e8d003d5c7e5feb16042211ec61970b63314a6c7bac345b0"
+ "instructionDigest": "9fe8b7fcfeb1bd36e8d003d5c7e5feb16042211ec61970b63314a6c7bac345b0",
+ "paramsDigest": "44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
+ "callIndex": 0,
+ "agent": "default"
},
- "summary": "Long-pressed the Hold me button; its state reads “long-pressed.” Double-clicked the Double-click me button; its state reads “double-clicked.”",
+ "summary": "Long-pressed Hold me; Hold state reads “long-pressed.” Double-clicked Double-click me; Double-click state reads “double-clicked.”",
"startPath": "/e/control-inventory",
"endPath": "/e/control-inventory",
"endAnchors": [
@@ -46,6 +49,18 @@
"text": "double-clicked"
}
],
- "endWaitMs": 11922
+ "goneAnchors": [
+ {
+ "role": "status",
+ "name": "Hold state",
+ "text": "none"
+ },
+ {
+ "role": "status",
+ "name": "Double-click state",
+ "text": "none"
+ }
+ ],
+ "endWaitMs": 11370
}
}
diff --git a/apps/web-benchmark/.e2e/cache/ee64bc2d2dacd6b5bdd5537e6e0c4b4f3f1bb916d023d5d712627cbce856e9da.json b/apps/web-benchmark/.e2e/cache/53b9612ca2090821af954d177fec1d1172095c54ae418c6169723a6f7a8b77b9.json
similarity index 63%
rename from apps/web-benchmark/.e2e/cache/ee64bc2d2dacd6b5bdd5537e6e0c4b4f3f1bb916d023d5d712627cbce856e9da.json
rename to apps/web-benchmark/.e2e/cache/53b9612ca2090821af954d177fec1d1172095c54ae418c6169723a6f7a8b77b9.json
index 2a993ff23..158ebb0a4 100644
--- a/apps/web-benchmark/.e2e/cache/ee64bc2d2dacd6b5bdd5537e6e0c4b4f3f1bb916d023d5d712627cbce856e9da.json
+++ b/apps/web-benchmark/.e2e/cache/53b9612ca2090821af954d177fec1d1172095c54ae418c6169723a6f7a8b77b9.json
@@ -1,6 +1,6 @@
{
"schemaVersion": "trace-1",
- "createdAt": "2026-09-23T13:30:26.359Z",
+ "createdAt": "2026-10-04T08:31:27.021Z",
"payload": {
"actions": [
{
@@ -14,7 +14,8 @@
},
{
"name": "tool",
- "summary": "tool type (run-time value)"
+ "summary": "tool type (run-time value)",
+ "derived": "minted-token"
},
{
"name": "tap",
@@ -33,9 +34,12 @@
"recordedFor": {
"testId": "tests-agent/scenarios.e2e.ts::act%20completes%20async-states",
"targetId": "web",
- "instructionDigest": "3d8730745312e1ce478b253dcad7910775eb76246846228b0ca02a78e514aa88"
+ "instructionDigest": "3d8730745312e1ce478b253dcad7910775eb76246846228b0ca02a78e514aa88",
+ "paramsDigest": "44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
+ "callIndex": 0,
+ "agent": "default"
},
- "summary": "Read verification code 4407 from the toast, entered it, and submitted Verify. The page displays “Code verified successfully.”.",
+ "summary": "Waited for the page to load, sent the code, read 4407 from the toast, entered it, and submitted once Verify was enabled. The page shows “Code verified successfully.”",
"startPath": "/e/async-states",
"endPath": "/e/async-states",
"endAnchors": [
@@ -44,6 +48,11 @@
"testId": "success-message"
}
],
- "endWaitMs": 12049
+ "goneAnchors": [
+ {
+ "testId": "initial-loading"
+ }
+ ],
+ "endWaitMs": 11512
}
}
diff --git a/apps/web-benchmark/.e2e/cache/38af005a1f14beeb5db725a88da8119011579d5050839d7a468712c1516ac7ad.json b/apps/web-benchmark/.e2e/cache/587d13f7c2fb5d376fa433ee583f2f6f88f6bdfe0e72c25ebc0390fb44e93c83.json
similarity index 54%
rename from apps/web-benchmark/.e2e/cache/38af005a1f14beeb5db725a88da8119011579d5050839d7a468712c1516ac7ad.json
rename to apps/web-benchmark/.e2e/cache/587d13f7c2fb5d376fa433ee583f2f6f88f6bdfe0e72c25ebc0390fb44e93c83.json
index d45b8aa50..b36eda2fc 100644
--- a/apps/web-benchmark/.e2e/cache/38af005a1f14beeb5db725a88da8119011579d5050839d7a468712c1516ac7ad.json
+++ b/apps/web-benchmark/.e2e/cache/587d13f7c2fb5d376fa433ee583f2f6f88f6bdfe0e72c25ebc0390fb44e93c83.json
@@ -1,8 +1,17 @@
{
"schemaVersion": "trace-1",
- "createdAt": "2026-09-26T20:48:22.536Z",
+ "createdAt": "2026-10-04T08:29:22.846Z",
"payload": {
"actions": [
+ {
+ "name": "tap",
+ "summary": "tap button \"Attachments\" in \"Files\"",
+ "target": {
+ "role": "button",
+ "name": "Attachments",
+ "within": "Files"
+ }
+ },
{
"name": "upload",
"summary": "upload \"fixtures/attachment.txt\", \"fixtures/second.txt\" to button \"Attachments\" in \"Files\"",
@@ -24,9 +33,12 @@
"recordedFor": {
"testId": "tests-agent/control-inventory.e2e.ts::control%20inventory::upload%20attaches%20the%20named%20project%20files",
"targetId": "web",
- "instructionDigest": "6eb8c4cb34d6623811808727d85db0d55946be5724f0cdc01b3c633db3d6f44d"
+ "instructionDigest": "6eb8c4cb34d6623811808727d85db0d55946be5724f0cdc01b3c633db3d6f44d",
+ "paramsDigest": "38fc51716f1b76431a49c2fd70d8a6d11405fa9b9451a1d96cb9e357e70321ce",
+ "callIndex": 0,
+ "agent": "default"
},
- "summary": "Attached both fixtures/attachment.txt and fixtures/second.txt. The Attachments state now lists attachment.txt and second.txt.",
+ "summary": "Attached both fixtures/attachment.txt and fixtures/second.txt to the Attachments input. The state now shows “attachment.txt, second.txt” and the control indicates 2 files.",
"startPath": "/e/control-inventory",
"endPath": "/e/control-inventory",
"endAnchors": [
@@ -34,8 +46,24 @@
"role": "status",
"name": "Attachments state",
"text": "attachment.txt, second.txt"
+ },
+ {
+ "role": "button",
+ "name": "Attachments",
+ "value": "C:\\fakepath\\attachment.txt"
+ }
+ ],
+ "goneAnchors": [
+ {
+ "role": "status",
+ "name": "Attachments state",
+ "text": "none"
+ },
+ {
+ "role": "button",
+ "name": "Attachments"
}
],
- "endWaitMs": 11392
+ "endWaitMs": 12311
}
}
diff --git a/apps/web-benchmark/.e2e/cache/61711c2e97b6ce5f9a43edaeae2c13c12f9c408e36b18006f43840ac6beecf8c.json b/apps/web-benchmark/.e2e/cache/61711c2e97b6ce5f9a43edaeae2c13c12f9c408e36b18006f43840ac6beecf8c.json
new file mode 100644
index 000000000..5499066ea
--- /dev/null
+++ b/apps/web-benchmark/.e2e/cache/61711c2e97b6ce5f9a43edaeae2c13c12f9c408e36b18006f43840ac6beecf8c.json
@@ -0,0 +1,74 @@
+{
+ "schemaVersion": "trace-1",
+ "createdAt": "2026-10-04T08:29:07.236Z",
+ "payload": {
+ "actions": [
+ {
+ "name": "tool",
+ "summary": "tool type (run-time value)",
+ "derived": "whole-node"
+ },
+ {
+ "name": "tap",
+ "summary": "tap button \"Apply\"",
+ "target": {
+ "role": "button",
+ "name": "Apply"
+ }
+ }
+ ],
+ "executor": {
+ "name": "e2e-default-agent",
+ "version": "2"
+ },
+ "recordedFor": {
+ "testId": "tests-agent/scenarios.e2e.ts::act%20completes%20iframe-form",
+ "targetId": "web",
+ "instructionDigest": "5ea16155d5cd3ee4fc67e7511534964bca91116fa0412e4523428844f9f85c15",
+ "paramsDigest": "44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
+ "callIndex": 0,
+ "agent": "default"
+ },
+ "summary": "Read coupon SAVE-1234 from the first frame, entered it in the checkout form, and applied it. The page now shows “Coupon applied.”",
+ "startPath": "/e/iframe-form",
+ "endPath": "/e/iframe-form",
+ "endAnchors": [
+ {
+ "text": "Coupon applied",
+ "testId": "success-message"
+ }
+ ],
+ "goneAnchors": [
+ {
+ "text": "Read the coupon code from the first frame and apply it in the second."
+ },
+ {
+ "text": "SAVE-1234"
+ },
+ {
+ "text": "Apply your coupon at checkout"
+ },
+ {
+ "role": "textbox",
+ "name": "Coupon code",
+ "placeholder": "Coupon code"
+ },
+ {
+ "role": "button",
+ "name": "Apply"
+ },
+ {
+ "role": "iframe",
+ "name": "coupon-frame"
+ },
+ {
+ "text": "Your coupon code:"
+ },
+ {
+ "role": "iframe",
+ "name": "checkout-frame"
+ }
+ ],
+ "endWaitMs": 11742
+ }
+}
diff --git a/apps/web-benchmark/.e2e/cache/301ca75e59525df56d8582a43689dc7a049b9d1b1642edf8c0b3fa749adfa888.json b/apps/web-benchmark/.e2e/cache/6faed48a7591e228ac3b7f8109a44e3e0fa04d3aef50dbb7156d7591586efbf6.json
similarity index 71%
rename from apps/web-benchmark/.e2e/cache/301ca75e59525df56d8582a43689dc7a049b9d1b1642edf8c0b3fa749adfa888.json
rename to apps/web-benchmark/.e2e/cache/6faed48a7591e228ac3b7f8109a44e3e0fa04d3aef50dbb7156d7591586efbf6.json
index f95a9e515..76e5448f7 100644
--- a/apps/web-benchmark/.e2e/cache/301ca75e59525df56d8582a43689dc7a049b9d1b1642edf8c0b3fa749adfa888.json
+++ b/apps/web-benchmark/.e2e/cache/6faed48a7591e228ac3b7f8109a44e3e0fa04d3aef50dbb7156d7591586efbf6.json
@@ -1,17 +1,8 @@
{
"schemaVersion": "trace-1",
- "createdAt": "2026-09-23T13:30:36.184Z",
+ "createdAt": "2026-10-04T08:33:55.168Z",
"payload": {
"actions": [
- {
- "name": "tap",
- "summary": "tap button \"Accept\"",
- "target": {
- "role": "button",
- "name": "Accept",
- "testId": "accept-cookies"
- }
- },
{
"name": "tap",
"summary": "tap button \"Buy now\" in \"Wireless Headphones\"",
@@ -30,9 +21,12 @@
"recordedFor": {
"testId": "tests-agent/scenarios.e2e.ts::act%20completes%20promo-storefront",
"targetId": "web",
- "instructionDigest": "1bf3aabb828b9b084d447df3a78d1fa700b5b28b8d5847dcc5ffcb9b0ce3bd45"
+ "instructionDigest": "1bf3aabb828b9b084d447df3a78d1fa700b5b28b8d5847dcc5ffcb9b0ce3bd45",
+ "paramsDigest": "44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
+ "callIndex": 0,
+ "agent": "default"
},
- "summary": "The Wireless Headphones purchase is confirmed; the page displays “Order confirmed” for that product.",
+ "summary": "Bought the Wireless Headphones. The page now shows “Order confirmed.”",
"startPath": "/e/promo-storefront",
"endPath": "/e/promo-storefront",
"endAnchors": [
@@ -58,6 +52,13 @@
"text": "★★★★ Comfortable fit, pairing was instant."
}
],
- "endWaitMs": 11175
+ "goneAnchors": [
+ {
+ "role": "listitem",
+ "name": "Wireless Headphones $80.00 $40.00 50% OFF Buy now",
+ "testId": "deal-headphones"
+ }
+ ],
+ "endWaitMs": 11574
}
}
diff --git a/apps/web-benchmark/.e2e/cache/76191b9464eff355a227e8732cd2d41e51e5c5ecdfc77e19d56355447de218a5.json b/apps/web-benchmark/.e2e/cache/76191b9464eff355a227e8732cd2d41e51e5c5ecdfc77e19d56355447de218a5.json
new file mode 100644
index 000000000..3a3b3cefa
--- /dev/null
+++ b/apps/web-benchmark/.e2e/cache/76191b9464eff355a227e8732cd2d41e51e5c5ecdfc77e19d56355447de218a5.json
@@ -0,0 +1,69 @@
+{
+ "schemaVersion": "trace-1",
+ "createdAt": "2026-10-04T08:28:59.224Z",
+ "payload": {
+ "actions": [
+ {
+ "name": "tap",
+ "summary": "tap radio \"Express\" in \"Delivery\"",
+ "target": {
+ "role": "radio",
+ "name": "Express",
+ "within": "Delivery"
+ }
+ }
+ ],
+ "executor": {
+ "name": "e2e-default-agent",
+ "version": "2"
+ },
+ "recordedFor": {
+ "testId": "tests-agent/control-inventory.e2e.ts::control%20inventory::check%20picks%20a%20radio%20the%20pick%20replaces%20with%20its%20summary",
+ "targetId": "web",
+ "instructionDigest": "df853ab2fc826564b99c9b044a65fd7ee21cdc2b487273cebcd32bcbe3607c66",
+ "paramsDigest": "44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
+ "callIndex": 0,
+ "agent": "default"
+ },
+ "summary": "Selected Express delivery; the delivery state now reads “Express” and the page confirms “Express delivery selected.”",
+ "startPath": "/e/control-inventory",
+ "endPath": "/e/control-inventory",
+ "endAnchors": [
+ {
+ "role": "status",
+ "name": "Delivery state",
+ "text": "Express"
+ },
+ {
+ "text": "Express delivery selected"
+ },
+ {
+ "role": "button",
+ "name": "Change delivery"
+ }
+ ],
+ "goneAnchors": [
+ {
+ "role": "status",
+ "name": "Delivery state",
+ "text": "unset"
+ },
+ {
+ "role": "radio",
+ "name": "Standard"
+ },
+ {
+ "role": "radio",
+ "name": "Express"
+ },
+ {
+ "role": "group",
+ "name": "Delivery"
+ },
+ {
+ "text": "Standard"
+ }
+ ],
+ "endWaitMs": 11721
+ }
+}
diff --git a/apps/web-benchmark/.e2e/cache/d08d3b277fb7b7831a72e44bbe3d17130dd378a936362f99f6011529ef7bb36b.json b/apps/web-benchmark/.e2e/cache/7f48111a643a86547628175b52e0493a95ab67cc850db1ab21bb6baa69670977.json
similarity index 63%
rename from apps/web-benchmark/.e2e/cache/d08d3b277fb7b7831a72e44bbe3d17130dd378a936362f99f6011529ef7bb36b.json
rename to apps/web-benchmark/.e2e/cache/7f48111a643a86547628175b52e0493a95ab67cc850db1ab21bb6baa69670977.json
index 70a80dff5..e9db5ac8c 100644
--- a/apps/web-benchmark/.e2e/cache/d08d3b277fb7b7831a72e44bbe3d17130dd378a936362f99f6011529ef7bb36b.json
+++ b/apps/web-benchmark/.e2e/cache/7f48111a643a86547628175b52e0493a95ab67cc850db1ab21bb6baa69670977.json
@@ -1,6 +1,6 @@
{
"schemaVersion": "trace-1",
- "createdAt": "2026-09-23T13:29:31.881Z",
+ "createdAt": "2026-10-04T08:28:57.613Z",
"payload": {
"actions": [
{
@@ -43,9 +43,12 @@
"recordedFor": {
"testId": "tests-agent/login-form.e2e.ts::act%20signs%20in%20with%20the%20benchmark%20account",
"targetId": "web",
- "instructionDigest": "c26456300e1d572e5598b0d8d2584171681d80d8a20bef86bdda6e9d2c7f976e"
+ "instructionDigest": "c26456300e1d572e5598b0d8d2584171681d80d8a20bef86bdda6e9d2c7f976e",
+ "paramsDigest": "10cf8df51d5b30096b97501956aa95d056885035f7bc67d093d076b7c4caecf0",
+ "callIndex": 0,
+ "agent": "default"
},
- "summary": "Logged in with tester@tester.army and the benchmark password; the page shows “Logged in successfully” and a Log out button.",
+ "summary": "Logged in with tester@tester.army and the supplied password. The page shows “Logged in successfully” and a Log out button.",
"startPath": "/e/login-form",
"endPath": "/e/login-form",
"endAnchors": [
@@ -59,6 +62,29 @@
"testId": "logout-button"
}
],
- "endWaitMs": 10103
+ "goneAnchors": [
+ {
+ "text": "Use tester@tester.army / "
+ },
+ {
+ "role": "textbox",
+ "name": "Email",
+ "testId": "email-input",
+ "placeholder": "Email"
+ },
+ {
+ "role": "textbox",
+ "name": "Password",
+ "testId": "password-input",
+ "placeholder": "Password",
+ "inputPurpose": "password"
+ },
+ {
+ "role": "button",
+ "name": "Log in",
+ "testId": "login-button"
+ }
+ ],
+ "endWaitMs": 11548
}
}
diff --git a/apps/web-benchmark/.e2e/cache/95eebf3ab01eabdd09afe6bf0e31216ab141c487cb267a6bd1c2c941de7c9e88.json b/apps/web-benchmark/.e2e/cache/95eebf3ab01eabdd09afe6bf0e31216ab141c487cb267a6bd1c2c941de7c9e88.json
new file mode 100644
index 000000000..57ccdd923
--- /dev/null
+++ b/apps/web-benchmark/.e2e/cache/95eebf3ab01eabdd09afe6bf0e31216ab141c487cb267a6bd1c2c941de7c9e88.json
@@ -0,0 +1,328 @@
+{
+ "schemaVersion": "trace-1",
+ "createdAt": "2026-10-04T08:33:45.030Z",
+ "payload": {
+ "actions": [
+ {
+ "name": "type",
+ "summary": "type \"release approved\" into textbox \"editor\"",
+ "target": {
+ "role": "textbox",
+ "testId": "editor"
+ },
+ "value": "release approved"
+ },
+ {
+ "name": "press",
+ "summary": "press \"Control+Shift+ArrowLeft\" on textbox \"release approved\"",
+ "target": {
+ "role": "textbox",
+ "text": "release approved",
+ "testId": "editor"
+ },
+ "key": "Control+Shift+ArrowLeft"
+ },
+ {
+ "name": "press",
+ "summary": "press \"ArrowRight\" on textbox \"release approved\"",
+ "target": {
+ "role": "textbox",
+ "text": "release approved",
+ "testId": "editor"
+ },
+ "key": "ArrowRight"
+ },
+ {
+ "name": "press",
+ "summary": "press \"Control+Shift+ArrowLeft\" on textbox \"release approved\"",
+ "target": {
+ "role": "textbox",
+ "text": "release approved",
+ "testId": "editor"
+ },
+ "key": "Control+Shift+ArrowLeft"
+ },
+ {
+ "name": "tap",
+ "summary": "tap textbox \"release approved\"",
+ "target": {
+ "role": "textbox",
+ "text": "release approved",
+ "testId": "editor"
+ }
+ },
+ {
+ "name": "press",
+ "summary": "press \"Shift+End\" on textbox \"release approved\"",
+ "target": {
+ "role": "textbox",
+ "text": "release approved",
+ "testId": "editor"
+ },
+ "key": "Shift+End"
+ },
+ {
+ "name": "press",
+ "summary": "press \"Shift+ArrowRight\" on textbox \"release approved\"",
+ "target": {
+ "role": "textbox",
+ "text": "release approved",
+ "testId": "editor"
+ },
+ "key": "Shift+ArrowRight"
+ },
+ {
+ "name": "pressKey",
+ "summary": "press \"Shift+ArrowRight\" on the focused field",
+ "key": "Shift+ArrowRight"
+ },
+ {
+ "name": "pressKey",
+ "summary": "press \"Shift+ArrowRight\" on the focused field",
+ "key": "Shift+ArrowRight"
+ },
+ {
+ "name": "pressKey",
+ "summary": "press \"Shift+ArrowRight\" on the focused field",
+ "key": "Shift+ArrowRight"
+ },
+ {
+ "name": "pressKey",
+ "summary": "press \"Shift+ArrowRight\" on the focused field",
+ "key": "Shift+ArrowRight"
+ },
+ {
+ "name": "pressKey",
+ "summary": "press \"Shift+ArrowRight\" on the focused field",
+ "key": "Shift+ArrowRight"
+ },
+ {
+ "name": "pressKey",
+ "summary": "press \"Shift+ArrowRight\" on the focused field",
+ "key": "Shift+ArrowRight"
+ },
+ {
+ "name": "pressKey",
+ "summary": "press \"Shift+ArrowRight\" on the focused field",
+ "key": "Shift+ArrowRight"
+ },
+ {
+ "name": "tap",
+ "summary": "tap textbox \"release approved\"",
+ "target": {
+ "role": "textbox",
+ "text": "release approved",
+ "testId": "editor"
+ }
+ },
+ {
+ "name": "press",
+ "summary": "press \"Control+a\" on textbox \"release approved\"",
+ "target": {
+ "role": "textbox",
+ "text": "release approved",
+ "testId": "editor"
+ },
+ "key": "Control+a"
+ },
+ {
+ "name": "tap",
+ "summary": "tap textbox \"release approved\"",
+ "target": {
+ "role": "textbox",
+ "text": "release approved",
+ "testId": "editor"
+ }
+ },
+ {
+ "name": "press",
+ "summary": "press \"Control+a\" on textbox \"release approved\"",
+ "target": {
+ "role": "textbox",
+ "text": "release approved",
+ "testId": "editor"
+ },
+ "key": "Control+a"
+ },
+ {
+ "name": "press",
+ "summary": "press \"Control+Shift+ArrowLeft\" on textbox \"release approved\"",
+ "target": {
+ "role": "textbox",
+ "text": "release approved",
+ "testId": "editor"
+ },
+ "key": "Control+Shift+ArrowLeft"
+ },
+ {
+ "name": "press",
+ "summary": "press \"Home\" on textbox \"release approved\"",
+ "target": {
+ "role": "textbox",
+ "text": "release approved",
+ "testId": "editor"
+ },
+ "key": "Home"
+ },
+ {
+ "name": "press",
+ "summary": "press \"Shift+ArrowRight\" on textbox \"release approved\"",
+ "target": {
+ "role": "textbox",
+ "text": "release approved",
+ "testId": "editor"
+ },
+ "key": "Shift+ArrowRight"
+ },
+ {
+ "name": "pressKey",
+ "summary": "press \"Shift+ArrowRight\" on the focused field",
+ "key": "Shift+ArrowRight"
+ },
+ {
+ "name": "pressKey",
+ "summary": "press \"Shift+ArrowRight\" on the focused field",
+ "key": "Shift+ArrowRight"
+ },
+ {
+ "name": "pressKey",
+ "summary": "press \"Shift+ArrowRight\" on the focused field",
+ "key": "Shift+ArrowRight"
+ },
+ {
+ "name": "pressKey",
+ "summary": "press \"Shift+ArrowRight\" on the focused field",
+ "key": "Shift+ArrowRight"
+ },
+ {
+ "name": "pressKey",
+ "summary": "press \"Shift+ArrowRight\" on the focused field",
+ "key": "Shift+ArrowRight"
+ },
+ {
+ "name": "pressKey",
+ "summary": "press \"Shift+ArrowRight\" on the focused field",
+ "key": "Shift+ArrowRight"
+ },
+ {
+ "name": "pressKey",
+ "summary": "press \"Shift+ArrowRight\" on the focused field",
+ "key": "Shift+ArrowRight"
+ },
+ {
+ "name": "press",
+ "summary": "press \"ArrowRight\" on textbox \"release approved\"",
+ "target": {
+ "role": "textbox",
+ "text": "release approved",
+ "testId": "editor"
+ },
+ "key": "ArrowRight"
+ },
+ {
+ "name": "press",
+ "summary": "press \"Shift+ArrowRight\" on textbox \"release approved\"",
+ "target": {
+ "role": "textbox",
+ "text": "release approved",
+ "testId": "editor"
+ },
+ "key": "Shift+ArrowRight"
+ },
+ {
+ "name": "pressKey",
+ "summary": "press \"Shift+ArrowRight\" on the focused field",
+ "key": "Shift+ArrowRight"
+ },
+ {
+ "name": "pressKey",
+ "summary": "press \"Shift+ArrowRight\" on the focused field",
+ "key": "Shift+ArrowRight"
+ },
+ {
+ "name": "pressKey",
+ "summary": "press \"Shift+ArrowRight\" on the focused field",
+ "key": "Shift+ArrowRight"
+ },
+ {
+ "name": "pressKey",
+ "summary": "press \"Shift+ArrowRight\" on the focused field",
+ "key": "Shift+ArrowRight"
+ },
+ {
+ "name": "pressKey",
+ "summary": "press \"Shift+ArrowRight\" on the focused field",
+ "key": "Shift+ArrowRight"
+ },
+ {
+ "name": "pressKey",
+ "summary": "press \"Shift+ArrowRight\" on the focused field",
+ "key": "Shift+ArrowRight"
+ },
+ {
+ "name": "pressKey",
+ "summary": "press \"Shift+ArrowRight\" on the focused field",
+ "key": "Shift+ArrowRight"
+ },
+ {
+ "name": "tap",
+ "summary": "tap button \"B\"",
+ "target": {
+ "role": "button",
+ "name": "B",
+ "testId": "bold-button"
+ }
+ },
+ {
+ "name": "tap",
+ "summary": "tap button \"Check document\"",
+ "target": {
+ "role": "button",
+ "name": "Check document",
+ "testId": "check-button"
+ }
+ }
+ ],
+ "executor": {
+ "name": "e2e-default-agent",
+ "version": "2"
+ },
+ "recordedFor": {
+ "testId": "tests-agent/scenarios.e2e.ts::act%20completes%20rich-text-editor",
+ "targetId": "web",
+ "instructionDigest": "e37ad9ac878deebe5c840d23fded9589f67228d8229a059b855347ab5e49ab96",
+ "paramsDigest": "44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
+ "callIndex": 0,
+ "agent": "default"
+ },
+ "summary": "Entered “release approved,” selected only “approved” and applied bold formatting, then checked the document. The page confirmed “Document approved.”",
+ "startPath": "/e/rich-text-editor",
+ "endPath": "/e/rich-text-editor",
+ "endAnchors": [
+ {
+ "text": "Document approved",
+ "testId": "success-message"
+ }
+ ],
+ "goneAnchors": [
+ {
+ "text": "Type \"release approved\" in the editor and make the word approved bold, then check the document."
+ },
+ {
+ "role": "button",
+ "name": "B",
+ "testId": "bold-button"
+ },
+ {
+ "role": "textbox",
+ "testId": "editor"
+ },
+ {
+ "role": "button",
+ "name": "Check document",
+ "testId": "check-button"
+ }
+ ],
+ "endWaitMs": 11834
+ }
+}
diff --git a/apps/web-benchmark/.e2e/cache/21d3170211e57f3727e39c689d20db194c05a3d75ed7b2df912e496cbbb7470d.json b/apps/web-benchmark/.e2e/cache/95fca6f8a199510dc5c35bcd0ce03c590721dfc03c606147b54d65ef1bcd481e.json
similarity index 59%
rename from apps/web-benchmark/.e2e/cache/21d3170211e57f3727e39c689d20db194c05a3d75ed7b2df912e496cbbb7470d.json
rename to apps/web-benchmark/.e2e/cache/95fca6f8a199510dc5c35bcd0ce03c590721dfc03c606147b54d65ef1bcd481e.json
index de7645151..ef9f9eade 100644
--- a/apps/web-benchmark/.e2e/cache/21d3170211e57f3727e39c689d20db194c05a3d75ed7b2df912e496cbbb7470d.json
+++ b/apps/web-benchmark/.e2e/cache/95fca6f8a199510dc5c35bcd0ce03c590721dfc03c606147b54d65ef1bcd481e.json
@@ -1,6 +1,6 @@
{
"schemaVersion": "trace-1",
- "createdAt": "2026-09-23T18:02:51.969Z",
+ "createdAt": "2026-10-04T08:31:38.993Z",
"payload": {
"actions": [
{
@@ -33,9 +33,12 @@
"recordedFor": {
"testId": "tests-agent/scenarios.e2e.ts::act%20completes%20hover-menu",
"targetId": "web",
- "instructionDigest": "4d2bd092cafaf0bdc40c9442fb592d58283fd7a946e7efb98e1142cffe3c07a3"
+ "instructionDigest": "4d2bd092cafaf0bdc40c9442fb592d58283fd7a946e7efb98e1142cffe3c07a3",
+ "paramsDigest": "44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
+ "callIndex": 0,
+ "agent": "default"
},
- "summary": "Hovered over Account, then Billing, and selected Redeem voucher. The page now displays “Voucher redeemed.”.",
+ "summary": "Hovered Account, then Billing, and selected Redeem voucher. The page confirms “Voucher redeemed.”",
"startPath": "/e/hover-menu",
"endPath": "/e/hover-menu",
"endAnchors": [
@@ -44,6 +47,23 @@
"testId": "success-message"
}
],
- "endWaitMs": 11764
+ "goneAnchors": [
+ {
+ "text": "Using hover only, find Redeem voucher under Account."
+ },
+ {
+ "text": "Products",
+ "testId": "menu-products"
+ },
+ {
+ "text": "Settings",
+ "testId": "menu-settings"
+ },
+ {
+ "text": "Account",
+ "testId": "menu-account"
+ }
+ ],
+ "endWaitMs": 11468
}
}
diff --git a/apps/web-benchmark/.e2e/cache/dbf1a6c7f66db2213c503537198d1d9e1be4cb7290ed6a73eefc7dfe4d88c03f.json b/apps/web-benchmark/.e2e/cache/9c5a50b0bd4690906e0400a969a666387c904f089842a6e4806bb3c6f9c4328a.json
similarity index 68%
rename from apps/web-benchmark/.e2e/cache/dbf1a6c7f66db2213c503537198d1d9e1be4cb7290ed6a73eefc7dfe4d88c03f.json
rename to apps/web-benchmark/.e2e/cache/9c5a50b0bd4690906e0400a969a666387c904f089842a6e4806bb3c6f9c4328a.json
index 4986529ad..3bc799d7c 100644
--- a/apps/web-benchmark/.e2e/cache/dbf1a6c7f66db2213c503537198d1d9e1be4cb7290ed6a73eefc7dfe4d88c03f.json
+++ b/apps/web-benchmark/.e2e/cache/9c5a50b0bd4690906e0400a969a666387c904f089842a6e4806bb3c6f9c4328a.json
@@ -1,8 +1,15 @@
{
"schemaVersion": "trace-1",
- "createdAt": "2026-09-23T13:30:30.518Z",
+ "createdAt": "2026-10-04T08:32:03.098Z",
"payload": {
"actions": [
+ {
+ "name": "secondaryTap",
+ "summary": "secondary-tap node \"report.pdf\"",
+ "target": {
+ "text": "report.pdf"
+ }
+ },
{
"name": "tap",
"summary": "tap button \"More actions for report.pdf\"",
@@ -45,9 +52,12 @@
"recordedFor": {
"testId": "tests-agent/scenarios.e2e.ts::act%20completes%20context-menu-trap",
"targetId": "web",
- "instructionDigest": "63530e281eafd2d07743a56500bfed3eae14e1ab7a4c0ad00e838e8b51912917"
+ "instructionDigest": "63530e281eafd2d07743a56500bfed3eae14e1ab7a4c0ad00e838e8b51912917",
+ "paramsDigest": "44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
+ "callIndex": 0,
+ "agent": "default"
},
- "summary": "Renamed report.pdf to summary.pdf using the file's More actions menu; the file list now shows summary.pdf and a success message confirms the rename.",
+ "summary": "Renamed report.pdf to summary.pdf. The file manager now lists summary.pdf and shows “File renamed to summary.pdf.”",
"startPath": "/e/context-menu-trap",
"endPath": "/e/context-menu-trap",
"endAnchors": [
@@ -64,6 +74,16 @@
"testId": "success-message"
}
],
- "endWaitMs": 10102
+ "goneAnchors": [
+ {
+ "text": "report.pdf"
+ },
+ {
+ "role": "button",
+ "name": "More actions for report.pdf",
+ "text": "⋯"
+ }
+ ],
+ "endWaitMs": 11888
}
}
diff --git a/apps/web-benchmark/.e2e/cache/6e9dadfddcfeb384163165db2579e58102db60f5dd29b524a0705fcbc50a09db.json b/apps/web-benchmark/.e2e/cache/9c9d5f9062162fb6e748f47c1788677bef1b1d9366b1a87a3013842625e659c6.json
similarity index 57%
rename from apps/web-benchmark/.e2e/cache/6e9dadfddcfeb384163165db2579e58102db60f5dd29b524a0705fcbc50a09db.json
rename to apps/web-benchmark/.e2e/cache/9c9d5f9062162fb6e748f47c1788677bef1b1d9366b1a87a3013842625e659c6.json
index 089f57263..e32857d5f 100644
--- a/apps/web-benchmark/.e2e/cache/6e9dadfddcfeb384163165db2579e58102db60f5dd29b524a0705fcbc50a09db.json
+++ b/apps/web-benchmark/.e2e/cache/9c9d5f9062162fb6e748f47c1788677bef1b1d9366b1a87a3013842625e659c6.json
@@ -1,6 +1,6 @@
{
"schemaVersion": "trace-1",
- "createdAt": "2026-09-23T13:30:38.861Z",
+ "createdAt": "2026-10-04T08:34:05.073Z",
"payload": {
"actions": [
{
@@ -42,9 +42,12 @@
"recordedFor": {
"testId": "tests-agent/scenarios.e2e.ts::act%20completes%20gift-card-purchase",
"targetId": "web",
- "instructionDigest": "58b82e08250d41e942c183d1c49b7e031fb5754f62c785c3f064c404c6b44851"
+ "instructionDigest": "58b82e08250d41e942c183d1c49b7e031fb5754f62c785c3f064c404c6b44851",
+ "paramsDigest": "44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
+ "callIndex": 0,
+ "agent": "default"
},
- "summary": "Selected a $25 gift card, entered friend@example.com, and completed the purchase. The page confirms the gift card was sent and is on its way.",
+ "summary": "Selected a $25 gift card, entered friend@example.com, and completed the purchase. The page confirms the gift card was sent and says a $25.00 gift card is on its way.",
"startPath": "/e/gift-card-purchase",
"endPath": "/e/gift-card-purchase",
"endAnchors": [
@@ -56,6 +59,47 @@
"text": "A $25.00 gift card is on its way."
}
],
- "endWaitMs": 10101
+ "goneAnchors": [
+ {
+ "text": "Send a digital gift card. Delivered by email within minutes."
+ },
+ {
+ "role": "button",
+ "name": "gift_card_amount_option_undefined",
+ "text": "$10",
+ "testId": "amount-10"
+ },
+ {
+ "role": "button",
+ "name": "gift_card_amount_option_undefined",
+ "text": "$25",
+ "testId": "amount-25"
+ },
+ {
+ "role": "button",
+ "name": "gift_card_amount_option_undefined",
+ "text": "$50",
+ "testId": "amount-50"
+ },
+ {
+ "role": "textbox",
+ "name": "{{recipient_email_label}}",
+ "testId": "recipient-input",
+ "placeholder": "friend@example.com"
+ },
+ {
+ "text": "Total"
+ },
+ {
+ "text": "Total: $NaN"
+ },
+ {
+ "role": "button",
+ "name": "btn_purchase_label",
+ "text": "Buy gift card",
+ "testId": "buy-button"
+ }
+ ],
+ "endWaitMs": 11710
}
}
diff --git a/apps/web-benchmark/.e2e/cache/9b6349a07095a300952960a00e4596fc14fdafe4527396654923943598cfc633.json b/apps/web-benchmark/.e2e/cache/9e0a6ad84bfa325d98bcb19080a64e331851f0d8e03ededc3ba6461df080565e.json
similarity index 64%
rename from apps/web-benchmark/.e2e/cache/9b6349a07095a300952960a00e4596fc14fdafe4527396654923943598cfc633.json
rename to apps/web-benchmark/.e2e/cache/9e0a6ad84bfa325d98bcb19080a64e331851f0d8e03ededc3ba6461df080565e.json
index ef06e7783..f8d4953cf 100644
--- a/apps/web-benchmark/.e2e/cache/9b6349a07095a300952960a00e4596fc14fdafe4527396654923943598cfc633.json
+++ b/apps/web-benchmark/.e2e/cache/9e0a6ad84bfa325d98bcb19080a64e331851f0d8e03ededc3ba6461df080565e.json
@@ -1,6 +1,6 @@
{
"schemaVersion": "trace-1",
- "createdAt": "2026-09-23T13:30:28.966Z",
+ "createdAt": "2026-10-04T08:31:33.402Z",
"payload": {
"actions": [
{
@@ -31,9 +31,12 @@
"recordedFor": {
"testId": "tests-agent/scenarios.e2e.ts::act%20completes%20debounced-search",
"targetId": "web",
- "instructionDigest": "e5044d8a420a21763ff0ba6ba21266a9cf45e9607dd3496c435089c8d757d4e8"
+ "instructionDigest": "e5044d8a420a21763ff0ba6ba21266a9cf45e9607dd3496c435089c8d757d4e8",
+ "paramsDigest": "44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
+ "callIndex": 0,
+ "agent": "default"
},
- "summary": "Searched for “trail mix,” selected exactly “Trail Mix 500 g,” and verified the page shows “Added Trail Mix 500 g to the cart.”",
+ "summary": "Searched for trail mix and selected exactly Trail Mix 500 g. The page confirms it was added to the cart.",
"startPath": "/e/debounced-search",
"endPath": "/e/debounced-search",
"endAnchors": [
@@ -42,6 +45,17 @@
"testId": "success-message"
}
],
- "endWaitMs": 10121
+ "goneAnchors": [
+ {
+ "text": "Search for trail mix and pick exactly Trail Mix 500 g."
+ },
+ {
+ "role": "combobox",
+ "name": "Search snacks",
+ "testId": "search-input",
+ "placeholder": "Search snacks"
+ }
+ ],
+ "endWaitMs": 11449
}
}
diff --git a/apps/web-benchmark/.e2e/cache/9e1dbad15746e685363092d30b7c43e49f4cdba5c9f330a8337e99429457a6ae.json b/apps/web-benchmark/.e2e/cache/9e1dbad15746e685363092d30b7c43e49f4cdba5c9f330a8337e99429457a6ae.json
new file mode 100644
index 000000000..32ad60eba
--- /dev/null
+++ b/apps/web-benchmark/.e2e/cache/9e1dbad15746e685363092d30b7c43e49f4cdba5c9f330a8337e99429457a6ae.json
@@ -0,0 +1,79 @@
+{
+ "schemaVersion": "trace-1",
+ "createdAt": "2026-10-04T08:29:03.146Z",
+ "payload": {
+ "actions": [
+ {
+ "name": "tap",
+ "summary": "tap button \"Cancel order\"",
+ "target": {
+ "role": "button",
+ "name": "Cancel order",
+ "text": "Pay now"
+ }
+ },
+ {
+ "name": "tool",
+ "summary": "tool type (run-time value)",
+ "derived": "minted-token"
+ },
+ {
+ "name": "tap",
+ "summary": "tap button \"Discard draft\"",
+ "target": {
+ "role": "button",
+ "name": "Discard draft",
+ "text": "Submit payment"
+ }
+ }
+ ],
+ "executor": {
+ "name": "e2e-default-agent",
+ "version": "2"
+ },
+ "recordedFor": {
+ "testId": "tests-agent/scenarios.e2e.ts::act%20completes%20lying-labels",
+ "targetId": "web",
+ "instructionDigest": "75cac33ad209d332c191820f8b966e11e3b86f06d25432d913adf93fe369052d",
+ "paramsDigest": "44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
+ "callIndex": 0,
+ "agent": "default"
+ },
+ "summary": "Followed the visible labels and page instructions: entered 42.50 in the field beside “Amount,” left the visibly labeled Reference field empty, and submitted. The page now shows “Payment sent.”",
+ "startPath": "/e/lying-labels",
+ "endPath": "/e/lying-labels",
+ "endAnchors": [
+ {
+ "text": "Payment sent",
+ "testId": "success-message"
+ }
+ ],
+ "goneAnchors": [
+ {
+ "text": "Click the button that visibly says Pay now."
+ },
+ {
+ "text": "Do not press this button"
+ },
+ {
+ "text": "Pay now safely"
+ },
+ {
+ "role": "button",
+ "name": "Confirm payment",
+ "text": "Contact support"
+ },
+ {
+ "role": "button",
+ "name": "Cancel order",
+ "text": "Pay now"
+ },
+ {
+ "role": "button",
+ "name": "Pay now",
+ "text": "Cancel order"
+ }
+ ],
+ "endWaitMs": 11896
+ }
+}
diff --git a/apps/web-benchmark/.e2e/cache/9a37ff39c572841d0303e55b4c7322295aeffa6332a2ed39d13db3aa9b41b21b.json b/apps/web-benchmark/.e2e/cache/a836bf1e72f05dd86bdb9ad4a5460aac4e24c0268326c0754e6827941fbbe8c8.json
similarity index 61%
rename from apps/web-benchmark/.e2e/cache/9a37ff39c572841d0303e55b4c7322295aeffa6332a2ed39d13db3aa9b41b21b.json
rename to apps/web-benchmark/.e2e/cache/a836bf1e72f05dd86bdb9ad4a5460aac4e24c0268326c0754e6827941fbbe8c8.json
index 24ba3c26d..42ad0064e 100644
--- a/apps/web-benchmark/.e2e/cache/9a37ff39c572841d0303e55b4c7322295aeffa6332a2ed39d13db3aa9b41b21b.json
+++ b/apps/web-benchmark/.e2e/cache/a836bf1e72f05dd86bdb9ad4a5460aac4e24c0268326c0754e6827941fbbe8c8.json
@@ -1,6 +1,6 @@
{
"schemaVersion": "trace-1",
- "createdAt": "2026-09-24T19:35:19.528Z",
+ "createdAt": "2026-10-04T08:29:31.075Z",
"payload": {
"actions": [
{
@@ -18,9 +18,12 @@
"recordedFor": {
"testId": "tests-agent/control-inventory.e2e.ts::control%20inventory::scroll_to%20brings%20the%20footnote%20into%20view",
"targetId": "web",
- "instructionDigest": "60d7d1c1109f3844ed7b5cd3f6f351e1b297af10aa7e4371ed45de087cce0df3"
+ "instructionDigest": "60d7d1c1109f3844ed7b5cd3f6f351e1b297af10aa7e4371ed45de087cce0df3",
+ "paramsDigest": "44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
+ "callIndex": 0,
+ "agent": "default"
},
- "summary": "Scrolled the Footnote paragraph into view; the Footnote state now shows “in view.”",
+ "summary": "Scrolled the page until the Footnote paragraph was in view; the Footnote state now reads “in view”.",
"startPath": "/e/control-inventory",
"endPath": "/e/control-inventory",
"endAnchors": [
@@ -30,6 +33,13 @@
"text": "in view"
}
],
- "endWaitMs": 12064
+ "goneAnchors": [
+ {
+ "role": "status",
+ "name": "Footnote state",
+ "text": "out of view"
+ }
+ ],
+ "endWaitMs": 11385
}
}
diff --git a/apps/web-benchmark/.e2e/cache/b49868122bd279e5544bdfad218fe2a728363d123a4d037133b429edd66980de.json b/apps/web-benchmark/.e2e/cache/b49868122bd279e5544bdfad218fe2a728363d123a4d037133b429edd66980de.json
new file mode 100644
index 000000000..9128b77b5
--- /dev/null
+++ b/apps/web-benchmark/.e2e/cache/b49868122bd279e5544bdfad218fe2a728363d123a4d037133b429edd66980de.json
@@ -0,0 +1,68 @@
+{
+ "schemaVersion": "trace-1",
+ "createdAt": "2026-10-04T08:34:08.113Z",
+ "payload": {
+ "actions": [
+ {
+ "name": "type",
+ "summary": "type \"mug\" into textbox \"Filter products\"",
+ "target": {
+ "role": "textbox",
+ "name": "Filter products",
+ "testId": "filter-input",
+ "placeholder": "Filter products"
+ },
+ "value": "mug"
+ }
+ ],
+ "executor": {
+ "name": "e2e-default-agent",
+ "version": "2"
+ },
+ "recordedFor": {
+ "testId": "tests-agent/scenarios.e2e.ts::act%20filters%20the%20list%20and%20the%20URL%20carries%20the%20filter",
+ "targetId": "web",
+ "instructionDigest": "c54ac6f470b19c027dd580aa19d24ab48c4f0612b3930178028f20eb76cdd216",
+ "paramsDigest": "44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
+ "callIndex": 0,
+ "agent": "default"
+ },
+ "summary": "Entered “mug” in the product filter. The list is filtered to Titanium Mug, and the page URL is /e/filter-deep-link?q=mug.",
+ "startPath": "/e/filter-deep-link",
+ "endPath": "/e/filter-deep-link?q=mug",
+ "endAnchors": [
+ {
+ "role": "textbox",
+ "name": "Filter products",
+ "testId": "filter-input",
+ "placeholder": "Filter products",
+ "value": "mug"
+ }
+ ],
+ "goneAnchors": [
+ {
+ "role": "textbox",
+ "name": "Filter products",
+ "testId": "filter-input",
+ "placeholder": "Filter products"
+ },
+ {
+ "role": "listitem",
+ "name": "Trail Backpack"
+ },
+ {
+ "role": "listitem",
+ "name": "Steel Water Bottle"
+ },
+ {
+ "role": "listitem",
+ "name": "Merino Hiking Socks"
+ },
+ {
+ "role": "listitem",
+ "name": "Camp Lantern"
+ }
+ ],
+ "endWaitMs": 11489
+ }
+}
diff --git a/apps/web-benchmark/.e2e/cache/ffc271f65288ac2b62a497a04105198017a463eaf9410aeabd1e4afe79a79dc5.json b/apps/web-benchmark/.e2e/cache/c8d793f4eb8f1beff83231ec5037fc53f1b4261ccc1523f0faaee6945a05cd9b.json
similarity index 53%
rename from apps/web-benchmark/.e2e/cache/ffc271f65288ac2b62a497a04105198017a463eaf9410aeabd1e4afe79a79dc5.json
rename to apps/web-benchmark/.e2e/cache/c8d793f4eb8f1beff83231ec5037fc53f1b4261ccc1523f0faaee6945a05cd9b.json
index 1027832f5..4846cc7ae 100644
--- a/apps/web-benchmark/.e2e/cache/ffc271f65288ac2b62a497a04105198017a463eaf9410aeabd1e4afe79a79dc5.json
+++ b/apps/web-benchmark/.e2e/cache/c8d793f4eb8f1beff83231ec5037fc53f1b4261ccc1523f0faaee6945a05cd9b.json
@@ -1,6 +1,6 @@
{
"schemaVersion": "trace-1",
- "createdAt": "2026-09-23T10:41:22.965Z",
+ "createdAt": "2026-10-04T08:28:56.187Z",
"payload": {
"actions": [
{
@@ -24,9 +24,12 @@
"recordedFor": {
"testId": "tests-agent/scenarios.e2e.ts::act%20completes%20shadow-dom-form",
"targetId": "web",
- "instructionDigest": "c6884af43cef64f928cde4eaa3be0cbc128893f01611d32236baf514a915c0a6"
+ "instructionDigest": "c6884af43cef64f928cde4eaa3be0cbc128893f01611d32236baf514a915c0a6",
+ "paramsDigest": "44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
+ "callIndex": 0,
+ "agent": "default"
},
- "summary": "Entered SHADOW-42 into the nested-shadow-root form and submitted it. The page now displays “Access granted.”.",
+ "summary": "Entered SHADOW-42 and submitted the form. The page now shows “Access granted.”",
"startPath": "/e/shadow-dom-form",
"endPath": "/e/shadow-dom-form",
"endAnchors": [
@@ -35,6 +38,23 @@
"testId": "success-message"
}
],
- "endWaitMs": 11941
+ "goneAnchors": [
+ {
+ "text": "The form below lives inside a closed shadow root. Enter SHADOW-42 and press Submit."
+ },
+ {
+ "text": "Access code hint: SHADOW-42"
+ },
+ {
+ "role": "textbox",
+ "name": "Access code",
+ "placeholder": "Access code"
+ },
+ {
+ "role": "button",
+ "name": "Submit"
+ }
+ ],
+ "endWaitMs": 11646
}
}
diff --git a/apps/web-benchmark/.e2e/cache/84586bbbf74c35b93fa6418467148a243d902bd7aa6394c744732cd0cbd1281d.json b/apps/web-benchmark/.e2e/cache/c9ae158cd9a15f3caf0fcdc7abf497ebcf86304886a6800692a03c47c0277fbd.json
similarity index 57%
rename from apps/web-benchmark/.e2e/cache/84586bbbf74c35b93fa6418467148a243d902bd7aa6394c744732cd0cbd1281d.json
rename to apps/web-benchmark/.e2e/cache/c9ae158cd9a15f3caf0fcdc7abf497ebcf86304886a6800692a03c47c0277fbd.json
index dfed30cce..e68f03b84 100644
--- a/apps/web-benchmark/.e2e/cache/84586bbbf74c35b93fa6418467148a243d902bd7aa6394c744732cd0cbd1281d.json
+++ b/apps/web-benchmark/.e2e/cache/c9ae158cd9a15f3caf0fcdc7abf497ebcf86304886a6800692a03c47c0277fbd.json
@@ -1,6 +1,6 @@
{
"schemaVersion": "trace-1",
- "createdAt": "2026-09-23T13:30:34.492Z",
+ "createdAt": "2026-10-04T08:33:52.251Z",
"payload": {
"actions": [
{
@@ -49,17 +49,61 @@
"recordedFor": {
"testId": "tests-agent/scenarios.e2e.ts::act%20completes%20playbook-cleanup",
"targetId": "web",
- "instructionDigest": "423676dde62142adee61619b5958db96013371bf84519c7e0441aaa32d6f9fce"
+ "instructionDigest": "423676dde62142adee61619b5958db96013371bf84519c7e0441aaa32d6f9fce",
+ "paramsDigest": "44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
+ "callIndex": 0,
+ "agent": "default"
},
- "summary": "Deleted the playbook \"Launch Approval Draft,\" confirmed its removal, and searched the exact name. The page shows: No playbooks match \"Launch Approval Draft.\"",
+ "summary": "Deleted the playbook “Launch Approval Draft” and confirmed the removal. Searching for its exact name shows: “No playbooks match ‘Launch Approval Draft’.”",
"startPath": "/e/playbook-cleanup",
"endPath": "/e/playbook-cleanup",
"endAnchors": [
+ {
+ "role": "searchbox",
+ "name": "Search playbooks",
+ "placeholder": "Search",
+ "value": "Launch Approval Draft"
+ },
{
"text": "No playbooks match \"Launch Approval Draft\".",
"testId": "success-message"
}
],
- "endWaitMs": 10103
+ "goneAnchors": [
+ {
+ "role": "searchbox",
+ "name": "Search playbooks",
+ "placeholder": "Search"
+ },
+ {
+ "role": "heading",
+ "name": "NDA Review"
+ },
+ {
+ "text": "8 rules"
+ },
+ {
+ "role": "button",
+ "name": "More actions for NDA Review",
+ "text": "⋯"
+ },
+ {
+ "role": "heading",
+ "name": "Launch Approval Draft"
+ },
+ {
+ "text": "4 rules"
+ },
+ {
+ "role": "button",
+ "name": "More actions for Launch Approval Draft",
+ "text": "⋯"
+ },
+ {
+ "role": "heading",
+ "name": "Vendor Onboarding"
+ }
+ ],
+ "endWaitMs": 11698
}
}
diff --git a/apps/web-benchmark/.e2e/cache/62e37c5052c64eac130df6b297136c0caa3067b2872ececd647afa6c1a5514c1.json b/apps/web-benchmark/.e2e/cache/cc0f6ff334fc11d69f3cb984791c64ea01c31b33b33334f2e476049f38606a33.json
similarity index 53%
rename from apps/web-benchmark/.e2e/cache/62e37c5052c64eac130df6b297136c0caa3067b2872ececd647afa6c1a5514c1.json
rename to apps/web-benchmark/.e2e/cache/cc0f6ff334fc11d69f3cb984791c64ea01c31b33b33334f2e476049f38606a33.json
index 69a25d872..5c4b632a7 100644
--- a/apps/web-benchmark/.e2e/cache/62e37c5052c64eac130df6b297136c0caa3067b2872ececd647afa6c1a5514c1.json
+++ b/apps/web-benchmark/.e2e/cache/cc0f6ff334fc11d69f3cb984791c64ea01c31b33b33334f2e476049f38606a33.json
@@ -1,8 +1,17 @@
{
"schemaVersion": "trace-1",
- "createdAt": "2026-09-25T18:09:48.642Z",
+ "createdAt": "2026-10-04T08:29:14.465Z",
"payload": {
"actions": [
+ {
+ "name": "secondaryTap",
+ "summary": "secondary-tap listitem \"report.pdf\" in \"Files\"",
+ "target": {
+ "role": "listitem",
+ "name": "report.pdf",
+ "within": "Files"
+ }
+ },
{
"name": "secondaryTap",
"summary": "secondary-tap node \"report.pdf\"",
@@ -21,15 +30,18 @@
}
],
"executor": {
- "name": "web-benchmark-inventory-recorder",
- "version": "1"
+ "name": "e2e-default-agent",
+ "version": "2"
},
"recordedFor": {
"testId": "tests-agent/control-inventory.e2e.ts::control%20inventory::a%20right-click%20opens%20the%20file%20menu%20and%20Rename%20renames%20the%20file",
"targetId": "web",
- "instructionDigest": "cb3da13c69639f87ac80d3bd6fa2b908c9df0bbbda211433bdc779611d4909fe"
+ "instructionDigest": "cb3da13c69639f87ac80d3bd6fa2b908c9df0bbbda211433bdc779611d4909fe",
+ "paramsDigest": "44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
+ "callIndex": 0,
+ "agent": "default"
},
- "summary": "Opened the report.pdf context menu and chose Rename. File state reads \"report.pdf renamed to summary.pdf\", and the file list contains summary.pdf.",
+ "summary": "Right-clicked report.pdf to open its actions menu, selected Rename, and verified the file list now shows summary.pdf with the status “report.pdf renamed to summary.pdf.”",
"startPath": "/e/control-inventory",
"endPath": "/e/control-inventory",
"endAnchors": [
@@ -46,6 +58,20 @@
"name": "summary.pdf"
}
],
- "endWaitMs": 10222
+ "goneAnchors": [
+ {
+ "role": "status",
+ "name": "File state",
+ "text": "no menu open"
+ },
+ {
+ "text": "report.pdf"
+ },
+ {
+ "role": "listitem",
+ "name": "report.pdf"
+ }
+ ],
+ "endWaitMs": 11535
}
}
diff --git a/apps/web-benchmark/.e2e/cache/91c5fa1cc7fd3ed41e763e1c3a344d4d519a1f8703c012a1a1d46045ded8d4bd.json b/apps/web-benchmark/.e2e/cache/cc89e287153d37442a76efb692f7ae15f79a39c3c44893c8592ece73d46d413b.json
similarity index 75%
rename from apps/web-benchmark/.e2e/cache/91c5fa1cc7fd3ed41e763e1c3a344d4d519a1f8703c012a1a1d46045ded8d4bd.json
rename to apps/web-benchmark/.e2e/cache/cc89e287153d37442a76efb692f7ae15f79a39c3c44893c8592ece73d46d413b.json
index 1c787e406..f028db332 100644
--- a/apps/web-benchmark/.e2e/cache/91c5fa1cc7fd3ed41e763e1c3a344d4d519a1f8703c012a1a1d46045ded8d4bd.json
+++ b/apps/web-benchmark/.e2e/cache/cc89e287153d37442a76efb692f7ae15f79a39c3c44893c8592ece73d46d413b.json
@@ -1,6 +1,6 @@
{
"schemaVersion": "trace-1",
- "createdAt": "2026-09-23T13:30:33.632Z",
+ "createdAt": "2026-10-04T08:32:31.439Z",
"payload": {
"actions": [
{
@@ -66,9 +66,12 @@
"recordedFor": {
"testId": "tests-agent/scenarios.e2e.ts::act%20completes%20date-picker",
"targetId": "web",
- "instructionDigest": "dcf88f9a5a9a284823967721c9316e3541f960b36032c247343f678217019dde"
+ "instructionDigest": "dcf88f9a5a9a284823967721c9316e3541f960b36032c247343f678217019dde",
+ "paramsDigest": "44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
+ "callIndex": 0,
+ "agent": "default"
},
- "summary": "Navigated the calendar from June to September 2026, selected September 17, 2026, and confirmed the booking. The page displays “Appointment booked for September 17, 2026.”",
+ "summary": "Navigated the calendar from June to September 2026, selected September 17, and confirmed. The page shows “Appointment booked for September 17, 2026.”",
"startPath": "/e/date-picker",
"endPath": "/e/date-picker",
"endAnchors": [
@@ -77,6 +80,19 @@
"testId": "success-message"
}
],
- "endWaitMs": 10099
+ "goneAnchors": [
+ {
+ "role": "textbox",
+ "name": "Select date",
+ "testId": "date-input",
+ "placeholder": "Select date"
+ },
+ {
+ "role": "button",
+ "name": "Confirm booking",
+ "testId": "confirm-button"
+ }
+ ],
+ "endWaitMs": 11647
}
}
diff --git a/apps/web-benchmark/.e2e/cache/52c2b9053d6bfb1471ac2d8846b2fd6048f426611cf04f6eda3ee632a702cd78.json b/apps/web-benchmark/.e2e/cache/cc8cc932b468583080dc18ea59fda1bc1a0ad0d0974a0834eefbe08bb6fbfd5c.json
similarity index 58%
rename from apps/web-benchmark/.e2e/cache/52c2b9053d6bfb1471ac2d8846b2fd6048f426611cf04f6eda3ee632a702cd78.json
rename to apps/web-benchmark/.e2e/cache/cc8cc932b468583080dc18ea59fda1bc1a0ad0d0974a0834eefbe08bb6fbfd5c.json
index 4f98053e8..4f97138de 100644
--- a/apps/web-benchmark/.e2e/cache/52c2b9053d6bfb1471ac2d8846b2fd6048f426611cf04f6eda3ee632a702cd78.json
+++ b/apps/web-benchmark/.e2e/cache/cc8cc932b468583080dc18ea59fda1bc1a0ad0d0974a0834eefbe08bb6fbfd5c.json
@@ -1,6 +1,6 @@
{
"schemaVersion": "trace-1",
- "createdAt": "2026-09-23T13:30:31.162Z",
+ "createdAt": "2026-10-04T08:32:08.612Z",
"payload": {
"actions": [
{
@@ -38,9 +38,12 @@
"recordedFor": {
"testId": "tests-agent/scenarios.e2e.ts::act%20completes%20stale-dom",
"targetId": "web",
- "instructionDigest": "c7958b2326b3f2b42541d8b8261d04bb3d18d3fbbf439d754e64d72856a74ba4"
+ "instructionDigest": "c7958b2326b3f2b42541d8b8261d04bb3d18d3fbbf439d754e64d72856a74ba4",
+ "paramsDigest": "44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
+ "callIndex": 0,
+ "agent": "default"
},
- "summary": "Clicked the Tap me button three times, and the page now shows Progress: 3 / 3 with the success message “Target clicked 3 times.”",
+ "summary": "Clicked Tap me three times. The page shows Progress: 3 / 3 and “Target clicked 3 times.”",
"startPath": "/e/stale-dom",
"endPath": "/e/stale-dom",
"endAnchors": [
@@ -53,6 +56,37 @@
"testId": "success-message"
}
],
- "endWaitMs": 10100
+ "goneAnchors": [
+ {
+ "text": "Progress: 0 / 3",
+ "testId": "progress"
+ },
+ {
+ "text": "Slot 1"
+ },
+ {
+ "role": "button",
+ "name": "Decoy",
+ "testId": "decoy-button"
+ },
+ {
+ "text": "Slot 2"
+ },
+ {
+ "text": "Slot 3"
+ },
+ {
+ "role": "button",
+ "name": "Tap me",
+ "testId": "target-button"
+ },
+ {
+ "text": "Slot 4"
+ },
+ {
+ "text": "Slot 5"
+ }
+ ],
+ "endWaitMs": 11441
}
}
diff --git a/apps/web-benchmark/.e2e/cache/d01c00f9543a36210f2840b6eed3d6de7474f88fc2d442611cf7a31280facf66.json b/apps/web-benchmark/.e2e/cache/d01c00f9543a36210f2840b6eed3d6de7474f88fc2d442611cf7a31280facf66.json
deleted file mode 100644
index 40e315919..000000000
--- a/apps/web-benchmark/.e2e/cache/d01c00f9543a36210f2840b6eed3d6de7474f88fc2d442611cf7a31280facf66.json
+++ /dev/null
@@ -1,49 +0,0 @@
-{
- "schemaVersion": "trace-1",
- "createdAt": "2026-09-23T13:29:40.106Z",
- "payload": {
- "actions": [
- {
- "name": "tap",
- "summary": "tap button \"Cancel order\"",
- "target": {
- "role": "button",
- "name": "Cancel order",
- "text": "Pay now"
- }
- },
- {
- "name": "tool",
- "summary": "tool type (run-time value)"
- },
- {
- "name": "tap",
- "summary": "tap button \"Discard draft\"",
- "target": {
- "role": "button",
- "name": "Discard draft",
- "text": "Submit payment"
- }
- }
- ],
- "executor": {
- "name": "e2e-default-agent",
- "version": "2"
- },
- "recordedFor": {
- "testId": "tests-agent/scenarios.e2e.ts::act%20completes%20lying-labels",
- "targetId": "web",
- "instructionDigest": "75cac33ad209d332c191820f8b966e11e3b86f06d25432d913adf93fe369052d"
- },
- "summary": "Entered 42.50 in the textbox visibly labeled Amount, left the field visibly labeled Reference empty, and submitted via the control visibly labeled Submit payment. The page now shows “Payment sent.”.",
- "startPath": "/e/lying-labels",
- "endPath": "/e/lying-labels",
- "endAnchors": [
- {
- "text": "Payment sent",
- "testId": "success-message"
- }
- ],
- "endWaitMs": 11664
- }
-}
diff --git a/apps/web-benchmark/.e2e/cache/e2f557718b56e9e4ecf51224b005d95f429fc2eb7309fada37ece71c98421513.json b/apps/web-benchmark/.e2e/cache/e2f557718b56e9e4ecf51224b005d95f429fc2eb7309fada37ece71c98421513.json
deleted file mode 100644
index 949349d13..000000000
--- a/apps/web-benchmark/.e2e/cache/e2f557718b56e9e4ecf51224b005d95f429fc2eb7309fada37ece71c98421513.json
+++ /dev/null
@@ -1,32 +0,0 @@
-{
- "schemaVersion": "trace-1",
- "createdAt": "2026-09-23T13:30:39.253Z",
- "payload": {
- "actions": [
- {
- "name": "type",
- "summary": "type \"mug\" into textbox \"Filter products\"",
- "target": {
- "role": "textbox",
- "name": "Filter products",
- "testId": "filter-input",
- "placeholder": "Filter products"
- },
- "value": "mug"
- }
- ],
- "executor": {
- "name": "e2e-default-agent",
- "version": "2"
- },
- "recordedFor": {
- "testId": "tests-agent/scenarios.e2e.ts::act%20filters%20the%20list%20and%20the%20URL%20carries%20the%20filter",
- "targetId": "web",
- "instructionDigest": "c54ac6f470b19c027dd580aa19d24ab48c4f0612b3930178028f20eb76cdd216"
- },
- "summary": "Entered “mug” in the product filter. The URL is now /e/filter-deep-link?q=mug and the list contains only “Titanium Mug”.",
- "startPath": "/e/filter-deep-link",
- "endPath": "/e/filter-deep-link?q=mug",
- "endWaitMs": 10100
- }
-}
diff --git a/apps/web-benchmark/.e2e/cache/e5c98b82c848888ebf302d3e1949feb5cc0140af603317477dafbac69674d6ba.json b/apps/web-benchmark/.e2e/cache/e52795af76bcb5757ab47b6824e016e2906dfe9d0e9b557c1d6197a17a87df8b.json
similarity index 80%
rename from apps/web-benchmark/.e2e/cache/e5c98b82c848888ebf302d3e1949feb5cc0140af603317477dafbac69674d6ba.json
rename to apps/web-benchmark/.e2e/cache/e52795af76bcb5757ab47b6824e016e2906dfe9d0e9b557c1d6197a17a87df8b.json
index e59116807..1d04f3b32 100644
--- a/apps/web-benchmark/.e2e/cache/e5c98b82c848888ebf302d3e1949feb5cc0140af603317477dafbac69674d6ba.json
+++ b/apps/web-benchmark/.e2e/cache/e52795af76bcb5757ab47b6824e016e2906dfe9d0e9b557c1d6197a17a87df8b.json
@@ -1,6 +1,6 @@
{
"schemaVersion": "trace-1",
- "createdAt": "2026-09-23T13:29:31.865Z",
+ "createdAt": "2026-10-04T08:28:57.166Z",
"payload": {
"actions": [
{
@@ -42,9 +42,12 @@
"recordedFor": {
"testId": "tests-agent/unique-cache.e2e.ts::act%20adds%20an%20address%20with%20a%20run-unique%20label",
"targetId": "web",
- "instructionDigest": "7e6f99066c5bc180c1e5188150277917670b918eb0a9646ac967ef798648b455"
+ "instructionDigest": "7e6f99066c5bc180c1e5188150277917670b918eb0a9646ac967ef798648b455",
+ "paramsDigest": "49ed432ec36207f5728e4da6da98006ed0d126739d72fdcdb919e032bfd6a511",
+ "callIndex": 0,
+ "agent": "default"
},
- "summary": "The address was saved successfully. The list now includes “{{param:/label}}” at “7 Pine Way, Bend,” and the screen shows an “Address saved” confirmation.",
+ "summary": "Added address labelled “Cabin mutk7a4i” with line “7 Pine Way, Bend”; the new address appears in the list and an “Address saved” confirmation is shown.",
"startPath": "/e/saved-addresses",
"endPath": "/e/saved-addresses",
"endAnchors": [
@@ -64,6 +67,6 @@
"text": "7 Pine Way, Bend"
}
],
- "endWaitMs": 10129
+ "endWaitMs": 12289
}
}
diff --git a/apps/web-benchmark/.e2e/cache/bca42cea37fc800e340c8f4c3d8d0dc51a83b1f30e1ebbe5ae2070755a48c335.json b/apps/web-benchmark/.e2e/cache/ec7a25eba0e1a7efaf68f40784a69d7760d3c584ec6e497fbafb7ee166da771b.json
similarity index 70%
rename from apps/web-benchmark/.e2e/cache/bca42cea37fc800e340c8f4c3d8d0dc51a83b1f30e1ebbe5ae2070755a48c335.json
rename to apps/web-benchmark/.e2e/cache/ec7a25eba0e1a7efaf68f40784a69d7760d3c584ec6e497fbafb7ee166da771b.json
index d056521e5..5c2a8771c 100644
--- a/apps/web-benchmark/.e2e/cache/bca42cea37fc800e340c8f4c3d8d0dc51a83b1f30e1ebbe5ae2070755a48c335.json
+++ b/apps/web-benchmark/.e2e/cache/ec7a25eba0e1a7efaf68f40784a69d7760d3c584ec6e497fbafb7ee166da771b.json
@@ -1,6 +1,6 @@
{
"schemaVersion": "trace-1",
- "createdAt": "2026-09-23T18:03:08.643Z",
+ "createdAt": "2026-10-04T08:31:43.811Z",
"payload": {
"actions": [
{
@@ -33,9 +33,12 @@
"recordedFor": {
"testId": "tests-agent/scenarios.e2e.ts::act%20completes%20drag-and-drop",
"targetId": "web",
- "instructionDigest": "24ea40459eba1c0e7841d174c9d6b29255cc71346083add2a38c39491aff758c"
+ "instructionDigest": "24ea40459eba1c0e7841d174c9d6b29255cc71346083add2a38c39491aff758c",
+ "paramsDigest": "44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
+ "callIndex": 0,
+ "agent": "default"
},
- "summary": "Dragged Banana into the dropzone, then Cherry, and the page displayed “Items dropped in the right order.” Apple was left alone.",
+ "summary": "Dragged Banana and then Cherry into the dropzone, leaving Apple alone. The page displays “Items dropped in the right order.”",
"startPath": "/e/drag-and-drop",
"endPath": "/e/drag-and-drop",
"endAnchors": [
@@ -44,6 +47,11 @@
"testId": "success-message"
}
],
- "endWaitMs": 12698
+ "goneAnchors": [
+ {
+ "text": "Drop items here"
+ }
+ ],
+ "endWaitMs": 11777
}
}
diff --git a/apps/web-benchmark/.e2e/cache/5889b07715480e781982f1c22be86a9ec12fb0461da4f772d0e8dac3feb1fa92.json b/apps/web-benchmark/.e2e/cache/eddce497b6948544b8bef8d0459a2ec07e77041b3e0362c6419f54c0feb1c0d0.json
similarity index 58%
rename from apps/web-benchmark/.e2e/cache/5889b07715480e781982f1c22be86a9ec12fb0461da4f772d0e8dac3feb1fa92.json
rename to apps/web-benchmark/.e2e/cache/eddce497b6948544b8bef8d0459a2ec07e77041b3e0362c6419f54c0feb1c0d0.json
index 6dd9998a4..a080e2ee6 100644
--- a/apps/web-benchmark/.e2e/cache/5889b07715480e781982f1c22be86a9ec12fb0461da4f772d0e8dac3feb1fa92.json
+++ b/apps/web-benchmark/.e2e/cache/eddce497b6948544b8bef8d0459a2ec07e77041b3e0362c6419f54c0feb1c0d0.json
@@ -1,6 +1,6 @@
{
"schemaVersion": "trace-1",
- "createdAt": "2026-09-24T19:34:48.350Z",
+ "createdAt": "2026-10-04T08:28:55.544Z",
"payload": {
"actions": [
{
@@ -31,9 +31,12 @@
"recordedFor": {
"testId": "tests-agent/control-inventory.e2e.ts::control%20inventory::check%20sets%20the%20checkbox%20and%20the%20radio",
"targetId": "web",
- "instructionDigest": "31e69bd3b8b58acb08fa60eaea2976e3271b633b840b8ffca9440e21488961fc"
+ "instructionDigest": "31e69bd3b8b58acb08fa60eaea2976e3271b633b840b8ffca9440e21488961fc",
+ "paramsDigest": "44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
+ "callIndex": 0,
+ "agent": "default"
},
- "summary": "Agreed to the terms and selected Medium. The Toggles state now shows “terms agreed, size Medium.”",
+ "summary": "Agreed to the terms and selected Medium. The Toggles state confirms “terms agreed, size Medium.”",
"startPath": "/e/control-inventory",
"endPath": "/e/control-inventory",
"endAnchors": [
@@ -41,8 +44,37 @@
"role": "status",
"name": "Toggles state",
"text": "terms agreed, size Medium"
+ },
+ {
+ "role": "checkbox",
+ "name": "Agree to terms",
+ "states": [
+ "checked"
+ ]
+ },
+ {
+ "role": "radio",
+ "name": "Medium",
+ "states": [
+ "checked"
+ ]
+ }
+ ],
+ "goneAnchors": [
+ {
+ "role": "status",
+ "name": "Toggles state",
+ "text": "terms not agreed, size unset"
+ },
+ {
+ "role": "checkbox",
+ "name": "Agree to terms"
+ },
+ {
+ "role": "radio",
+ "name": "Medium"
}
],
- "endWaitMs": 12023
+ "endWaitMs": 11356
}
}
diff --git a/apps/web-benchmark/.e2e/cache/aa933fbea4f49718b9beae66e90c3c5b1a0f807b74991ae506bb0ece2573a914.json b/apps/web-benchmark/.e2e/cache/fbb677dfe2419445bd605212e7afdc5207510f44aed0d470ab387c9e38a69bed.json
similarity index 56%
rename from apps/web-benchmark/.e2e/cache/aa933fbea4f49718b9beae66e90c3c5b1a0f807b74991ae506bb0ece2573a914.json
rename to apps/web-benchmark/.e2e/cache/fbb677dfe2419445bd605212e7afdc5207510f44aed0d470ab387c9e38a69bed.json
index c014c075c..1c272ce17 100644
--- a/apps/web-benchmark/.e2e/cache/aa933fbea4f49718b9beae66e90c3c5b1a0f807b74991ae506bb0ece2573a914.json
+++ b/apps/web-benchmark/.e2e/cache/fbb677dfe2419445bd605212e7afdc5207510f44aed0d470ab387c9e38a69bed.json
@@ -1,6 +1,6 @@
{
"schemaVersion": "trace-1",
- "createdAt": "2026-09-23T13:30:09.534Z",
+ "createdAt": "2026-10-04T08:31:11.942Z",
"payload": {
"actions": [
{
@@ -37,9 +37,12 @@
"recordedFor": {
"testId": "tests-agent/scenarios.e2e.ts::act%20completes%20overlay-trap",
"targetId": "web",
- "instructionDigest": "6705af9140f936652f275b785be7e42b157ba15b3fa16eefba2c51d6f3fdcd1b"
+ "instructionDigest": "6705af9140f936652f275b785be7e42b157ba15b3fa16eefba2c51d6f3fdcd1b",
+ "paramsDigest": "44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
+ "callIndex": 0,
+ "agent": "default"
},
- "summary": "Accepted the cookie banner, closed the overlay, and pressed Continue. The page now shows the success message “You made it past the overlay.”",
+ "summary": "Dismissed the cookie banner, closed the overlay, and pressed Continue. The page shows “You made it past the overlay.”",
"startPath": "/e/overlay-trap",
"endPath": "/e/overlay-trap",
"endAnchors": [
@@ -48,6 +51,28 @@
"testId": "success-message"
}
],
- "endWaitMs": 10096
+ "goneAnchors": [
+ {
+ "text": "Accept cookies, close whatever is blocking clicks, then press Continue."
+ },
+ {
+ "text": "Blocked clicks: 0",
+ "testId": "blocked-count"
+ },
+ {
+ "role": "button",
+ "name": "Continue",
+ "testId": "continue-button"
+ },
+ {
+ "text": "We use cookies to run this benchmark."
+ },
+ {
+ "role": "button",
+ "name": "Accept cookies",
+ "testId": "accept-cookies-button"
+ }
+ ],
+ "endWaitMs": 11329
}
}
diff --git a/apps/web-benchmark/app/src/Examples/ControlInventory.tsx b/apps/web-benchmark/app/src/Examples/ControlInventory.tsx
index 1928faf90..353756bd0 100644
--- a/apps/web-benchmark/app/src/Examples/ControlInventory.tsx
+++ b/apps/web-benchmark/app/src/Examples/ControlInventory.tsx
@@ -3,6 +3,7 @@
import { type CSSProperties, useEffect, useRef, useState } from "react";
const SIZES = ["Small", "Medium", "Large"] as const;
+const DELIVERIES = ["Standard", "Express"] as const;
const INITIAL_FILES = ["quarterly-report.pdf", "report.pdf", "notes.txt"];
const TARGET_FILE = "report.pdf";
const RENAMED_FILE = "summary.pdf";
@@ -10,6 +11,7 @@ const LONG_PRESS_MS = 500;
const DETAILS_QUERY = "view=details";
type Size = (typeof SIZES)[number];
+type Delivery = (typeof DELIVERIES)[number];
type View = "inventory" | "details";
/**
@@ -20,7 +22,7 @@ const viewFromLocation = (): View => (window.location.search.includes(DETAILS_QU
/**
* The deterministic contract surface: plain controls with every state
* exposed, one exercise per agent verb the hard scenarios never reach (a
- * checkbox and a radio to set, a long press and a double click to tell from a
+ * checkbox and a radio to set, a radio the pick replaces with its summary, a long press and a double click to tell from a
* tap, a context menu that opens on right-click only, a file input, the
* browser history, a paragraph to scroll into view). Not a hard surface: each
* control reports its state in a status line, and the success message shows
@@ -30,6 +32,7 @@ const viewFromLocation = (): View => (window.location.search.includes(DETAILS_QU
export default function ControlInventory() {
const [agreed, setAgreed] = useState(false);
const [size, setSize] = useState(null);
+ const [delivery, setDelivery] = useState(null);
const [holdState, setHoldState] = useState("none");
const [longPressed, setLongPressed] = useState(false);
const [doubleClickState, setDoubleClickState] = useState("none");
@@ -75,6 +78,7 @@ export default function ControlInventory() {
const done =
agreed &&
size === "Medium" &&
+ delivery === "Express" &&
longPressed &&
doubleClicked &&
renamed &&
@@ -120,7 +124,7 @@ export default function ControlInventory() {
) : null}
- Plain controls, one exercise each. Agree to the terms and pick the Medium size; hold the Hold me button, then
+ Plain controls, one exercise each. Agree to the terms and pick the Medium size; pick Express delivery; hold the Hold me button, then
double-click the Double-click me button; rename report.pdf to summary.pdf through its context menu, which opens
on right-click only; attach a file; open the details and return with the browser history; bring the footnote at
the bottom into view.
@@ -131,6 +135,10 @@ export default function ControlInventory() {
+ )}
diff --git a/apps/web-benchmark/package.json b/apps/web-benchmark/package.json
index 6f11e2930..887ee826c 100644
--- a/apps/web-benchmark/package.json
+++ b/apps/web-benchmark/package.json
@@ -26,7 +26,6 @@
"@types/react": "19.3.0",
"@types/react-dom": "19.3.0",
"ai": "7.0.107",
- "playwright": "1.63.0",
"typescript": "7.0.2"
}
}
diff --git a/apps/web-benchmark/tests-agent/control-inventory.e2e.ts b/apps/web-benchmark/tests-agent/control-inventory.e2e.ts
index cf6ee86da..a11c4adeb 100644
--- a/apps/web-benchmark/tests-agent/control-inventory.e2e.ts
+++ b/apps/web-benchmark/tests-agent/control-inventory.e2e.ts
@@ -1,6 +1,6 @@
/**
* The agent verbs the hard scenarios never reach, each on the plain control
- * built for it: check, the long press and the double click, a context menu
+ * built for it: check, on a radio the pick replaces too, the long press and the double click, a context menu
* that opens on right-click only, a file input, the browser history, a scroll
* into view. One step per verb, so a red test names the verb, and a locator
* check pins each outcome the way the deterministic twin reads it.
@@ -21,6 +21,11 @@ test.describe('control inventory', () => {
await expect(screen.getByLabel('Toggles state')).toHaveText('terms agreed, size Medium');
});
+ test('check picks a radio the pick replaces with its summary', TAGS, async ({ agent, screen }) => {
+ await agent.act('pick Express delivery');
+ await expect(screen.getByLabel('Delivery state')).toHaveText('Express');
+ });
+
test('a long press and a double click are chosen over a plain tap', TAGS, async ({ agent, screen }) => {
await agent.act('long-press the Hold me button until its state reads long-pressed, then double-click the Double-click me button');
await expect(screen.getByLabel('Hold state')).toHaveText('long-pressed');
diff --git a/apps/web-benchmark/tests/control-inventory.e2e.ts b/apps/web-benchmark/tests/control-inventory.e2e.ts
index af2116e3b..387aeaa28 100644
--- a/apps/web-benchmark/tests/control-inventory.e2e.ts
+++ b/apps/web-benchmark/tests/control-inventory.e2e.ts
@@ -17,6 +17,12 @@ test.describe('control inventory', () => {
await expect(screen.getByLabel('Toggles state')).toHaveText('terms not agreed, size Medium');
});
+ test('check picks a radio the pick replaces with its summary', async ({ screen }) => {
+ await screen.getByRole('radio', 'Express').check();
+ await expect(screen.getByLabel('Delivery state')).toHaveText('Express');
+ await expect(screen.getByText('Express delivery selected')).toBeVisible();
+ });
+
test('a long press and a double click are told apart from a tap', async ({ screen }) => {
const hold = screen.getByRole('button', 'Hold me');
await hold.tap();
@@ -64,6 +70,7 @@ test.describe('control inventory', () => {
test('every exercise done shows the success message', async ({ app, screen }) => {
await screen.getByLabel('Agree to terms').check();
await screen.getByRole('radio', 'Medium').check();
+ await screen.getByRole('radio', 'Express').check();
await screen.getByRole('button', 'Hold me').longPress({ duration: 700 });
await screen.getByRole('button', 'Double-click me').doubleTap();
await screen.getByText('report.pdf', { exact: true }).secondaryTap();
diff --git a/docs/agents.mdx b/docs/agents.mdx
index 9598d3076..0ddaca16a 100644
--- a/docs/agents.mdx
+++ b/docs/agents.mdx
@@ -109,9 +109,12 @@ agent if the problem affects several tests:
5. Compare models or `providerOptions` by running the same tests with
`--agent `.
-Named agents can use the [replay cache](/cache). Custom tools and executors
-have their own replay constraints, described under [project tools](/tools)
-and [custom executors](/executors#replay-cache).
+Named agents can use the [replay cache](/cache). Each agent records its
+own entries, keyed by its name and its context, so a test that runs as
+`buyer` and as `admin` never replays one persona's steps for the other, and
+changing `agentContext` records again. Custom tools and executors have their
+own replay constraints, described under [project tools](/tools) and
+[custom executors](/executors#replay-cache).
## Pick an agent per run
diff --git a/docs/assertions.mdx b/docs/assertions.mdx
new file mode 100644
index 000000000..2ab2d4dcc
--- /dev/null
+++ b/docs/assertions.mdx
@@ -0,0 +1,94 @@
+---
+title: Assertions
+description: Check the screen with agent.assert, wait for a condition, and extract data.
+---
+
+An assertion checks what is on screen. The agent has three judgment calls:
+`assert` checks the screen now, `waitFor` waits until a condition holds, and
+`extract` reads structured data. When you know the exact value, use an
+`expect` [locator assertion](#locator-assertions) instead, with no model call.
+
+```ts
+await agent.assert('the dashboard shows a trial badge');
+```
+
+## `agent.assert`
+
+`assert` checks a statement against the current screen. A false statement
+fails with `ASSERTION_FAILED`. If the screen does not provide enough evidence,
+it fails with `ASSERTION_INCONCLUSIVE`.
+
+The judging model sees the statement and the current screen. It does not see
+earlier steps or the acting agent's summaries.
+
+## `agent.waitFor`
+
+`assert` checks the screen now. Use `waitFor` when the condition may become true
+later. It polls the screen and only calls the model when the observation changes.
+
+```ts
+await agent.waitFor('the export finishes and a download link appears', {
+ interval: 500,
+ timeout: 120_000,
+});
+```
+
+## `agent.extract`
+
+`extract` reads structured data from the screen. Provide a
+[Standard Schema](https://standardschema.dev) validator, such as Zod, to define
+the result.
+
+```ts
+import { expect } from 'e2e';
+import { z } from 'zod';
+
+const data = await agent.extract('every todo title and how many remain', {
+ schema: z.object({ titles: z.array(z.string()), remaining: z.number().int() }),
+});
+expect(data.titles).toContain('Buy milk');
+```
+
+
+For model-generated descriptions, check the relevant meaning rather than the
+entire sentence. Use a [locator assertion](/locators) when the exact text matters.
+
+
+## Judging pixels
+
+By default, judgments read a redacted text snapshot of the screen. Add
+`vision: true` for visual checks, such as charts or layout. Use
+`vision: 'only'` to judge the screenshot alone.
+
+```ts
+await agent.assert('the chart trends upward', { vision: true });
+await agent.assert('the search form is not covered by a banner', { vision: 'only' });
+```
+
+Images add input tokens. Screenshots are withheld for the rest of an attempt
+after a secret is filled. The [agent reference](/reference/agent#vision)
+lists the modes and restrictions.
+
+## Locator assertions
+
+`expect` on a locator checks an exact value and calls no model. It retries
+until the check passes or the assertion timeout runs out.
+
+```ts
+await expect(screen.getByRole('status')).toHaveText('2 remaining');
+await expect(screen.getByRole('heading', 'Dashboard')).toBeVisible();
+```
+
+[Locators](/locators) covers queries and actions. The
+[expect reference](/reference/expect#locator-matchers) lists every matcher.
+
+## Next
+
+
+
+ Exact actions and checks with `screen` and `expect`.
+
+
+ Verdicts, exit codes, and what the model sees.
+
+
diff --git a/docs/authentication.mdx b/docs/authentication.mdx
index 13a6ddc94..9a1cabd9e 100644
--- a/docs/authentication.mdx
+++ b/docs/authentication.mdx
@@ -211,7 +211,9 @@ export default {
```
`E2E_SECRET_` overrides an entry per run, with the same uppercasing rule
-as credentials, so a CI job can supply the value without touching the config:
+as credentials, so a CI job can supply the value without touching the config.
+Two entries that map to one variable, like `stripe-key` and `stripe_key`, fail
+the config load with `INVALID_CONFIG`, as two such credentials do:
```bash npm
diff --git a/docs/browser.mdx b/docs/browser.mdx
index bf4e4a5b6..da6bf58df 100644
--- a/docs/browser.mdx
+++ b/docs/browser.mdx
@@ -39,7 +39,9 @@ await app.open('/checkout');
await expect(screen.getByText('$42.00')).toBeVisible();
```
-Each handler must call exactly one of `fulfill`, `continue`, or `abort`. An
+Each handler must call exactly one of `fulfill`, `continue`, `fallback`, or
+`abort`. The newest route runs first; `continue` sends the request straight to
+the network, while `fallback` passes it to the route registered before. An
`expect` that fails inside the handler fails the test, at the next step or at
the end of the test when nothing follows. To inspect a real response, start
waiting before the action that sends it:
@@ -114,14 +116,33 @@ const flag = await browser.evaluate((name: string) => window.localStorage.getIte
Use `evaluate` for state that `screen` cannot read. Prefer locators and
assertions for visible UI so tests depend less on the DOM structure.
+## Run a script before the page
+
+An init script runs in every document before the page's own scripts: mock a
+browser API, seed `Math.random`, or set a flag the app reads at boot. For every
+test:
+
+```ts
+web({ initScripts: [{ path: 'tests/init.js' }] })
+```
+
+For one test, before the page opens:
+
+```ts
+await browser.addInitScript((value) => { Math.random = () => value; }, 0.5);
+await app.open('/raffle');
+```
+
+See [init scripts](/reference/web#init-scripts).
+
## Cookies
```ts
await browser.setCookies([{ url: 'http://localhost:3000', name: 'tz', value: 'UTC' }]);
```
-A cookie URL that does not parse, or uses `file:`, `data:`, or
-`javascript:`, is `POLICY_DENIED`; a domain cookie is checked as the origin
+A cookie URL that does not parse, or uses a scheme other than `http:` or
+`https:` (`about:blank` included), is `POLICY_DENIED`; a domain cookie is checked as the origin
it would be sent to. If any entry is refused, no cookies are set. To resize
the page, see [Testing viewports](/viewports).
@@ -144,8 +165,23 @@ export default {
} satisfies E2EConfig;
```
-`npx playwright install webkit --with-deps` provisions the extra browser in
-CI. `connect` attaches to a remote Chromium over CDP instead of launching
+In CI, provision the extra browser with the [install command](/reference/web#installing-browsers):
+
+
+```bash npm
+npx @e2e-dev/web install webkit --with-deps
+```
+
+```bash pnpm
+pnpm exec e2e-web install webkit --with-deps
+```
+
+```bash bun
+bunx @e2e-dev/web install webkit --with-deps
+```
+
+
+`connect` attaches to a remote Chromium over CDP instead of launching
one; a [browser provider](#hosted-browsers) does the same and also leases
and releases the browser. [Testing viewports](/viewports) covers sizes.
@@ -237,7 +273,7 @@ The attempt runs in the browser's persistent default context, on the
browser's own first tab, so the remote must contain only that context, and
the limits of
[`connect.reconnectEndpoint`](/reference/web#recovering-a-cdp-transport)
-apply: no `headers`, `basicAuth`, or `userAgent` (each is `INVALID_CONFIG`), no
+apply: no `headers`, `basicAuth`, `userAgent`, `locale`, or `timezoneId` (each is `INVALID_CONFIG`), no
`app.clearState()` (`UNSUPPORTED_CAPABILITY` naming the scope), and no session
state capture or restore, so a run whose tests consume a
[session](/authentication) on the target fails with `COLLECTION_ERROR`
diff --git a/docs/cache.mdx b/docs/cache.mdx
index 217da3fcd..03cab32e1 100644
--- a/docs/cache.mdx
+++ b/docs/cache.mdx
@@ -45,27 +45,60 @@ bunx e2e run --no-cache
## How a replay works
-1. Check the starting screen: the same route, whatever the record id, the
- query, or the fragment. Retries always run live.
+1. Check the starting screen: the same route, whatever the record id or the
+ fragment. Retries always run live.
2. Find each recorded control and repeat its action. The runner matches
controls by their role, name, test id, and surrounding context. It waits
for missing controls and stops if a match is ambiguous. A control with no
- name is found by its place among the unnamed controls of its kind.
-3. Check the final path and controls that appeared during the original step.
- Text that changes with the data, a count, a date, an id, is not used.
+ name is found by its place among the unnamed controls of its kind inside
+ the same named row, list item, or group; with no such container to tell
+ it from its twins, the agent takes the step.
+3. Check the final route and what the original step changed: the controls
+ that appeared, with their checked, selected, expanded, or pressed state,
+ and the ones that went away. At least one of those changes must happen
+ during the replay, counted from the first screen the replay saw on the
+ route it ended on, so an outcome already on screen before the actions
+ that should produce it proves nothing; a step whose last action moved to
+ another route has that move as its evidence. An alert the replay raised
+ that the recording never saw stops it too. Text that reads differently on
+ every run, a date, a time, a duration, an id, is not used, except in an
+ alert, where it is read as a placeholder.
4. Finish without a model call, or let the agent continue with a record of
what ran and why replay stopped.
+A route is the origin, the path, and the query. Ids, tokens, timestamps,
+and cache busters in either the path or the query are read as an id
+placeholder, so `/orders/42?t=1727780000` and `/orders/7?t=1727780999` are
+one route. Every other query value is part of it: `?mode=safe` and
+`?mode=unsafe`, or `/companies` and `/companies?tab=notes`, are different
+screens, and so is a tracking parameter that was not there when the step
+was recorded. Two routes that differ in one word, `/products/summer-sneaker`
+and `/products/winter-boot`, are different screens too, even when they look
+alike. A location on the app's own origin is compared by its path, so a
+recording follows the app to a preview deployment that shares its
+`app.identity`; a page on any other origin is another route.
+
+A count that names what it counts, `3 records imported` or `Showing 1 to 9
+of 9 results`, is part of the effect when the step made it appear on the
+screen it acted on, and must read the same on replay. A count that was
+already on screen and only moved, `41 items` to `42 items`, or one on a
+screen the step navigated to, is not used, since leftover data from earlier
+runs moves it again. A bare number such as a badge is never used.
+
Typed values replay as recorded, even when an earlier run left the same
value on screen. Secret fills resolve the current value by name and run
through authorization again.
## What must match
-A cache entry belongs to a specific test, target, instruction, and set of
-params. Renaming the test or target, changing the instruction or an ordinary
-param, or upgrading the engine's major or minor version causes a miss.
-Changing the model does not.
+A cache entry belongs to a specific test, target, instruction, set of
+params, and agent. Renaming the test or target, changing the instruction or
+an ordinary param, running with another configured agent, changing the
+agent's `context` or the test's `agentContext`, or upgrading the engine's
+major or minor version causes a miss. A test that runs once per agent
+(`agent: ['buyer', 'admin']`) records once per agent. Changing the model
+does not. The agent context enters the key as a hash of its redacted text,
+so a secret in it contributes only its name.
A timestamp or a fresh email address would cause a miss on every run. Wrap
values like these in `unique()` so the runner substitutes the current value
@@ -96,7 +129,9 @@ identity. On devices, replay uses the
In `read-write` mode, the runner saves a recording at the end of the
attempt when a later verification step passed. A step that took no action
-records nothing.
+records nothing, and neither does one that changed nothing the replay could
+check: no control appeared, went away, or changed state, and the route
+stayed the same.
| Counts as verification | Does not count |
| --- | --- |
@@ -145,14 +180,14 @@ such as an `afterEach` assertion.
| `retry` | Retries never replay. | Nothing. |
| `invalid-entry` | The file is unreadable, over 1 MiB, or from another schema version. | `npx e2e cache clear`. |
| `truncated` | The recording hit a limit, see [below](#limits-and-gaps). | Split the step, or pass the secret as a `Secret`. |
-| `wrong-context` | The app was on another screen than when the step was recorded: a different route, not a different record, query, or fragment. | Open the same screen before the step. |
-| `target-not-found` | A recorded control did not appear within 15 seconds. | The UI changed. The next passing run re-records. |
+| `wrong-context` | The app was on another screen than when the step was recorded: a different origin, path, or query, not a different record id or fragment. | Open the same screen before the step. |
+| `target-not-found` | A recorded control did not appear within 15 seconds, or it has no name and no named container to tell it from its twins. | The UI changed: the next passing run re-records. For an unnamed control, give it a label or put it in a named row or group. |
| `target-ambiguous` | The number of matching controls changed. | Give repeated controls distinct names or use unique test data. |
| `gap` | An action cannot be replayed, see [below](#limits-and-gaps). | The agent continues this part live. Put stable values in `params`. |
| `action-failed` | The app rejected a replayed action. | Check the app. |
| `action-uncertain` | A replayed action failed with an unknown commit state. The agent is told to check before acting again. | Check the app. |
| `viewport-changed` | A bare-point tap or hover was recorded on a viewport of another size. | Keep the viewport fixed. |
-| `end-mismatch` | Every action ran, but the recorded path or a control that should have appeared did not come back. | Leftover data from an earlier run, a banner that comes and goes, or a regression. |
+| `end-mismatch` | Every action ran, but the recorded route, a control that should have appeared or gone away, or a recorded state did not come back; the outcome was already on screen before the replay; or the replay raised an alert the recording never saw. | Leftover data from an earlier run, a banner that comes and goes, or a regression. |
A stop before any action ran is a miss. After replay starts, it is a
hand-off. Unique test data and cleanup in `afterEach` help avoid stale
@@ -176,8 +211,9 @@ the instruction or `params`, or ones the agent composed itself, replay.
Each entry is a JSON file under `.e2e/cache/`, named by a hash of its key.
It contains actions, target descriptions, typed text, a step summary, and
-checks for the final state. Those checks include the path and up to eight
-controls that appeared during the step.
+checks for the final state. Those checks include the route, up to eight
+controls that appeared during the step, alerts and status messages first,
+and up to eight that went away.
Entries contain no prompts, conversations, screenshots, observed trees, or
secret values. A secret fill stores only the secret's name. Other typed
@@ -220,12 +256,16 @@ Local runs leave a committed directory clean unless something changed. A
replayed step never rewrites its entry, and a live re-recording with the same
actions, paths, and anchors is not written: the summary, the measured end
wait, and the rule that flagged a typed run-time value differ between two
-live runs of the same flow and do not count. A file changes when:
+live runs of the same flow and do not count. Upgrading `e2e` to a version
+that changes how entries are matched re-keys every entry, so the first run
+after it records them again. A file changes when:
- a step records for the first time,
- the agent finishes a step after a hand-off, re-recording what it saw,
- a step fails after replaying, or ends in an `end-mismatch` the agent had
to repair, which deletes the entry,
+- a step with an entry passes without leaving anything to record, which
+ deletes the entry,
- no verification step confirmed the entry by the end of the attempt, which
deletes it.
@@ -257,8 +297,14 @@ CI=1 bunx e2e run --strict-cache
The reasons that fail are the ones that mean the app or the entry changed:
`invalid-entry`, `wrong-context`, `target-not-found`, `target-ambiguous`,
`viewport-changed`, `action-failed`, `action-uncertain`, and `end-mismatch`.
-A step with no recording (`no-entry`), a retry, a `gap`, and a `truncated`
-recording still run live. `REPLAY_STALE` exits 2 and is not retried, since a
+A `no-entry` miss fails too when the cache directory holds a recording of the
+same step under another key, from before an `e2e` or engine upgrade or a
+change to the project id, the app's identity, or the agent's context. The
+message names that file; delete it once nothing replays it. A custom
+`cache.store` is not checked this way. Entries recorded before this check
+existed are covered once a `read-write` run replays or re-records them. A
+step never recorded (a new or edited instruction or params), a retry, a
+`gap`, and a `truncated` recording still run live. `REPLAY_STALE` exits 2 and is not retried, since a
retry never replays and would pass live. The entry stays where it is, even
in `read-write` mode, so every strict run fails on it until it is re-recorded:
run the test in `read-write` mode without the flag, and with `cache.strict`
diff --git a/docs/ci.mdx b/docs/ci.mdx
index 6d64efbcc..d9d44987d 100644
--- a/docs/ci.mdx
+++ b/docs/ci.mdx
@@ -35,7 +35,7 @@ jobs:
- run: pnpm install --frozen-lockfile
- name: Install browsers
- run: npx playwright install chromium --with-deps
+ run: pnpm exec e2e-web install chromium --with-deps
- name: Run e2e
run: npx e2e run --reporter list,junit
@@ -66,7 +66,9 @@ jobs:
```
The workflow pins actions to commit SHAs and installs dependencies from the
-lockfile. `npx e2e` runs the installed version.
+lockfile. `npx e2e` runs the installed version. The
+[install command](/reference/web#installing-browsers) downloads the browsers
+for the Playwright version the engine pins, with their system libraries.
Artifacts upload on every run that is not cancelled, so a test that failed
and then passed on retry still has its evidence.
@@ -139,10 +141,12 @@ implemented yet.
`--last-failed` runs only the tests the previous run did not pass, read from
`report.json` in the [`output`](/reference/config#output) directory (`.e2e`
-by default). The rerun empties `artifacts/` there when it starts, so
-upload the first run's artifacts before rerunning in the same job if you want
-both. The [CLI reference](/reference/cli#flags) lists what counts as not
-passed. Locally it is the loop after a red run:
+by default). The rerun keeps the artifacts the first run's report names and
+writes its own under `artifacts/rerun-1/`, so one upload after both steps
+holds the evidence of each. A test another filter keeps out of the rerun
+stays owed: the next `--last-failed` runs it. The
+[CLI reference](/reference/cli#flags) lists what counts as not passed.
+Locally it is the loop after a red run:
```bash npm
@@ -193,7 +197,7 @@ flaky, so you can identify flows that need investigation.
`--max-failures ` stops the run once that many tests have failed, so a
broken deploy does not spend a model call on every remaining agent step. The
tests not started are skipped with cause `failure-limit`, the ones running
-end as `interrupted`, and the job exits 1.
+end as `interrupted`, counted apart from the failures, and the job exits 1.
## Exit codes
diff --git a/docs/debugging.mdx b/docs/debugging.mdx
index ec34b1d08..96a7f4b43 100644
--- a/docs/debugging.mdx
+++ b/docs/debugging.mdx
@@ -63,7 +63,9 @@ URL: `http://127.0.0.1:3000/todos`
`Look at` points to the failing call. Assertion failures include expected
and observed values; locator failures include the requested locator and
-nearby matches. Repeated failure details help you compare attempts.
+nearby matches. Repeated failure details help you compare attempts. The
+runner spends at most 5 s capturing the screen and screenshot once per failed
+attempt; an app that does not answer in time gets no screen section.
## The report
@@ -91,7 +93,9 @@ failed result:
Artifacts land under `.e2e/artifacts/` (`