Halmurat T.
Halmurat T.

Senior SDET

Home Blog Books ask About

The Dispatch

Weekly QA notes from the trenches.

Welcome aboard!

You're on the list. Expect real-world QA insights — no fluff, no spam.

© 2026 Halmurat T.

Automation 28
  • Selenium
  • Playwright
  • Appium
  • Cypress
AI Testing 67
CI/CD 12
  • GitHub Actions
  • Slack Reporting
QA Strategy 10
Case Studies 8
Blog/AI Testing
AI TestingHalmurat T./July 16, 2026/13 min

Stop Asserting Clicks: Trajectory Tests for Browser Agents

Filed underai-testing/ai-agents/playwright/llm/framework-design
Stop Asserting Clicks: Trajectory Tests for Browser Agents

Table of Contents
  • Why do exact-step assertions fail on agents that work?
  • What is a trajectory test, concretely?
  • Which invariants actually earn their place?
  • How do you make this deterministic and cheap in CI?
  • Recording a fixture you can trust
  • Where trajectory tests stop, and something else takes over
  • The mindset shift

On this page

  • Why do exact-step assertions fail on agents that work?
  • What is a trajectory test, concretely?
  • Which invariants actually earn their place?
  • How do you make this deterministic and cheap in CI?
  • Recording a fixture you can trust
  • Where trajectory tests stop, and something else takes over
  • The mindset shift

Your product just shipped a browser agent — the kind that reads the page, decides what to click, and drives a checkout or a booking on the user’s behalf. You write the obvious end-to-end test: launch the agent, then expect it to click #tab-2, then #confirm-btn, then land on /success. It’s green today. Tomorrow the model reorders two steps, clicks a differently-labelled button that does the same thing, and your test is red — even though the agent did its job perfectly. Assert on the exact click sequence and you’ve built a test that fails every time the agent succeeds differently.

The fix is to stop testing the path and start testing the trajectory: the sequence of decisions, judged against invariants instead of exact steps. Did the agent reach the goal state? Did it avoid the actions it must never take? Did it get there inside a step budget? That’s what you can actually assert on a non-deterministic system without your CI turning into a coin flip.

Why do exact-step assertions fail on agents that work?

Because the agent’s output is non-deterministic at the token level, and every click is downstream of a token. You cannot pin the path even if you want to. This isn’t a temperature knob you forgot to turn to zero — it’s structural.

The Thinking Machines Lab team generated 1,000 completions from the same prompt at temperature 0 and got 80 unique outputs; the completions were identical for the first 102 tokens, then diverged at the 103rd. The broader literature on “deterministic” LLM settings traces this to batch-dependent floating-point reductions, GPU kernel non-associativity, and mixture-of-experts routing — none of which you control from a test file. A browser agent inherits all of it and adds environment noise on top: a banner that loads late, an A/B bucket, a slightly different DOM order.

[ WARNING ]

If your assertion names a specific selector or a specific step index, you are encoding one particular sample of a probability distribution as if it were the spec. The next sample invalidates your test — and you’ll spend the afternoon “fixing” a test for an agent that never broke.

So the flake isn’t in your infrastructure; it’s in the shape of what you’re asserting. The path is a random variable. The outcome and the safety envelope are the properties you actually care about, and those are far more stable than any single trajectory through them.

What is a trajectory test, concretely?

A trajectory test records the agent’s decision stream — each { step, observation, action, target } tuple — and asserts on properties of that stream rather than on any exact member of it. Think of it as the difference between asserting “the agent typed these 14 keystrokes” and asserting “the agent ended up logged in, never touched the delete button, and did it in under 12 steps.”

This is exactly the move the research benchmarks made when they hit the same wall. WebArena scores its 812 tasks by success rate on the generated trajectory — outcome, not path. And the COLM 2025 “Illusion of Progress” study showed why loose path-matching is dangerous in the other direction: static benchmarks with sloppy scoring overestimated live-web agent competence badly, and their fix, WebJudge, reaches ~85% agreement with human annotators — 8 to 18 points higher than prior methods — precisely by checking that key intermediate steps were retained rather than accumulating screenshots. The lesson cuts both ways: assert too tightly and you get flake; assert too loosely and you pass broken agents. Trajectory invariants are the middle.

§ Why the path is the wrong assertion target

80

Unique outputs

From 1,000 runs at temperature 0

812

WebArena tasks

Scored on trajectory outcome, not exact path

~85%

WebJudge–human agreement

By checking key steps, not screenshots

I learned this the expensive way on a logistics client’s shipment-booking agent. Our first suite asserted the click path. Over three weeks it went red 11 times; every single one was a false alarm — the agent had found an equivalent route through the UI. We were the flakiest suite in the org and we’d caught zero real bugs. The rewrite asserted four invariants instead. It caught a genuine regression in week one: after a prompt tweak, the agent started calling the address-autocomplete widget twice, blowing the step budget on 6% of runs. A path assertion would never have seen it; a step-budget invariant caught it immediately.

Which invariants actually earn their place?

Four categories cover the vast majority of what you want to guarantee. Each is stable under non-determinism because none of them names a specific step.

Goal-state reached

Assert on the observable end state, not the route to it — a URL, a confirmation record in the DB, a toast with an order ID. This is your success-rate check, and it’s the one assertion that maps directly to “did the feature work.”

Forbidden actions never taken

The safety envelope. The agent must never click delete account, never call issue_refund outside a bounded amount, never submit a form on a /admin route. These are negative invariants over the whole trajectory, and they’re where the scary bugs hide.

Step budget respected

Cap the number of actions. A task that should take 6 steps taking 25 means the agent is thrashing — looping, re-reading, second-guessing. Budget overruns are an early-warning signal for reasoning regressions long before they become outright failures.

Key waypoints hit (order-free)

For multi-stage tasks, assert that certain states were visited at all — reached the payment step, saw the review screen — without pinning their order or their neighbours. This is WebJudge’s “key point” idea applied as a test assertion.

Notice what’s missing: any assertion about which selector, in which order, with which exact arguments. That granularity belongs one layer down, at the single tool call — which is a different and complementary technique I covered in testing what your AI agent does, not what it says. Trajectory tests sit above it, guarding the shape of the whole run.

How do you make this deterministic and cheap in CI?

Record once, replay forever. The reason agent E2E tests are slow and expensive is that they call a live model on every run — and a non-deterministic one, so you’re paying for flake. Capture the decision stream on a real run, freeze it as a fixture, and replay the frozen trajectory against your invariant assertions in the PR gate. No model call, no network, no flake, sub-second.

tests/agent/trajectory.spec.ts
import { test, expect } from '@playwright/test';
import { replayTrajectory } from './harness';
import booking from './fixtures/book-shipment.trajectory.json';
test('booking agent stays inside its envelope', async ({ page }) => {
// Replays a recorded decision stream — no live LLM call in the PR gate
const run = await replayTrajectory(page, booking);
// 1. Goal state
await expect(page).toHaveURL(/\/shipments\/[A-Z0-9]{8}\/confirmed/);
// 2. Forbidden actions — the safety envelope
const forbidden = ['delete_shipment', 'issue_refund', 'submit_admin_form'];
const usedTools = run.actions.map(a => a.tool);
expect(forbidden.filter(f => usedTools.includes(f))).toEqual([]);
// 3. Step budget
expect(run.actions.length).toBeLessThanOrEqual(12);
// 4. Key waypoints, order-free
expect(run.states).toEqual(expect.arrayContaining(['rate_selected', 'address_confirmed']));
});

The recorded fixture is the same artifact your production observability stack already emits. In LangChain’s State of Agent Engineering survey, 89% of teams run agent observability and 62% capture detailed tool-call traces — so the decision stream you need to record is data you’re very likely already collecting in prod. You’re not building new plumbing; you’re pointing a test at an existing trace format.

Recording a fixture you can trust

A replay is only as good as the trajectory you froze, so record against a pinned environment — seeded database, fixed feature flags, a specific model version — and capture two or three runs, not one. If the runs disagree on their path that’s fine and expected; if they disagree on an invariant (one reaches the goal, another doesn’t), you’ve found a reliability bug before you’ve written a single assertion. Strip PII from the observations before you commit the fixture, the same way you would redact Playwright traces before they reach an LLM.

[ TIP ]

Run the frozen-replay suite on every PR for a deterministic gate, and run a live trajectory suite on a nightly schedule to catch drift the fixture can’t see. It’s the same two-lane split I use for mocking LLM responses in CI and evaluating live nightly — deterministic gate, non-deterministic eval, separated on purpose.

§ Delta · What the test asserts One fragile path → four stable invariants

Before

agent clicks #tab-2 → #confirm-btn → /success

→ ↓

After

goal reached · no forbidden action · ≤12 steps · waypoints hit

Where trajectory tests stop, and something else takes over

Trajectory invariants tell you the agent stayed in bounds and hit the goal. They deliberately say nothing about quality — whether the summary it wrote was accurate, whether the rate it picked was the cheapest. That’s a judgment call, and it belongs to an eval, not an assertion. When you need to grade the content of what the agent produced, reach for a rubric-scored evaluator, with all the calibration headaches that entails — which is its own discipline I dug into in why LLM-as-judge tests go flaky. Keep the two separate: invariants gate the PR, evals grade the quality.

The mindset shift

The hard part of testing agents isn’t tooling — Playwright already gives you everything you need to observe actions and states. The hard part is unlearning the deterministic-E2E instinct that says a good test pins exact behavior. With an agent, pinning exact behavior is the bug in your test. You’re testing a sampler, and the only honest assertions are the ones that hold across samples: the goal it must reach, the lines it must not cross, the budget it must respect.

Your concrete next step: take your flakiest agent test, look at what it asserts, and count how many of its assertions name a specific selector or step index. Every one of those is a future false alarm. Replace them with the four invariants above, record one clean trajectory as a fixture, and move the “is the answer good?” question into a separate nightly eval. You’ll trade a suite that cries wolf for one that actually guards the envelope.

[ NOTE ]

Want the patterns that keep AI-feature suites green for the right reasons — no flake, no theater? Get weekly QA automation insights drawn from 10+ years of shipping test infrastructure at enterprise scale.

§ Frequently Asked FAQ
+ Isn't a trajectory test just a weaker end-to-end test?

No — it’s a differently-scoped one. A traditional E2E test asserts an exact path, which is the wrong target for a non-deterministic agent because the path is a random variable. A trajectory test asserts invariants that hold across samples: goal state reached, forbidden actions avoided, step budget respected, key waypoints hit. It’s stricter where it matters (safety) and looser where the model is legitimately free (the exact route).

+ How do I record a trajectory to replay?

Capture the agent’s decision stream — each step’s observation, chosen action, and target — as it runs once against a real environment, and serialize it to JSON. This is the same trace format most teams already emit for agent observability, so you’re usually reusing an existing artifact rather than building new instrumentation.

+ Won't a frozen fixture miss real model regressions?

The PR gate will, by design — that’s the point of a fast deterministic gate. You catch drift by running a separate live trajectory suite on a nightly schedule, where the agent calls the real model against a real environment and the same invariants are asserted. Deterministic gate on every PR, non-deterministic eval nightly.

+ How do I set the step budget without making it flaky?

Record several real runs, take the maximum step count across them, and add roughly 50% headroom. The budget is a thrashing detector, not a tight bound — you want it loose enough that normal variation passes and tight enough that a reasoning regression (loops, repeated tool calls) trips it.

+ Does this replace asserting on individual tool calls?

No, they compose. Tool-call assertions guard a single decision — the right function with the right arguments. Trajectory invariants guard the whole run — the shape and safety of the full decision stream. Use tool-call assertions for correctness of a step and trajectory invariants for correctness of the journey.

§ Further Reading 03 of 03
01AI Testing

Your Agent's Confirm Dialog Is Safety Theater

Your Playwright test checks the confirm dialog renders — not that the irreversible action was blocked. Test the human-in-the-loop approval gate properly.

Read →
02AI Testing

Your Agent Acts on Half-Streamed Tool-Call JSON

Streamed tool-call arguments are invalid JSON until the final chunk — here's the Playwright pattern that stops your AI agent from acting on half-parsed args.

Read →
03AI Testing

Test What Your AI Agent Does, Not What It Says

Your AI agent test is green because you asserted on its reply. Assert on the tool calls it makes in Playwright instead — the pattern for agentic features.

Read →

Don't miss a thing

Subscribe to get updates straight to your inbox.

HT

No spam · Unsubscribe anytime

Welcome aboard!

You're on the list. Expect real-world QA insights — no fluff, no spam.

§ Colophon

Halmurat T. — Senior SDET writing about test automation, CI/CD, and QA strategy from 10+ years in the enterprise trenches.

Set in
IBM Plex Sans, Lora, and IBM Plex Mono.
Built with
Astro, MDX, Tailwind CSS & Expressive Code. Served by Vercel.
Privacy
No cookies. No tracking scripts on the main thread — analytics run sandboxed via Partytown.
Source
github.com/Halmurat-Uyghur
Terminal
Try /ask to query Halmurat's notes in a shell prompt.

© 2026 Halmurat T. · Written in plain text, shipped in plain time.

Search
Esc

Search is not available in dev mode.

Run npm run build then npm run preview:local to test search locally.