Skip to content

Lesson 3.4 — Oracles for the un-testable

When you can't write expect(), give the agent eyes: render, screenshot, compare, fix.

TL;DR: UI work has no clean assertion for "looks right." The oracle becomes an image the agent looks at — screenshot vs. baseline (regression) or screenshot vs. intent (build loop). Same closed loop as TDD 1.

ELI5: a passing suite can sit on a broken page

"Center the button, make errors red" has no meaningful unit test.

   testable logic            un-testable surface (UI)
   ┌──────────────┐          ┌──────────────────────────┐
   │ run assert   │          │ render ─► screenshot ─►   │
   │ pass / fail  │          │ compare to baseline/spec  │
   └──────────────┘          │ → "looks wrong" = fail    │
                             └──────────────────────────┘

You can assert color === '#ff0000', but not "it looks right." A green suite can sit on a page with overlapping text, an off-screen button, a modal behind the backdrop. The oracle becomes an image the agent can look at — Anthropic lists "a browser screenshot compared against a design" as a first-class check, right next to tests and build exit codes 1.

The two flavors of visual oracle

One guards against regressions; one drives the build toward intent.

Flavor Baseline? Loop Catches
Screenshot-diff (regression) yes — known-good image render → diff pixels → over threshold = fail "changed the header, broke the footer"
Visual feedback (matches intent?) no — you're building it render → look → "centered?" → fix "plausible CSS that renders wrong"
flowchart LR
    C["agent writes CSS"] --> S["screenshot"]
    S --> Q{"centered?"}
    Q -->|no| F["fix"]
    F --> C
    Q -->|yes| D["done"]

Same self-correcting loop as TDD — render-look-fix instead of run-fail-fix. Anthropic's prescribed prompt is literally: "implement this design. take a screenshot of the result and compare it to the original. list differences and fix them" 1.

🧠 Test Yourself: Your unit tests are green but the dashboard looks broken in the browser. What does that tell you?

AnswerLogic tests pass over a visually broken surface — "equal height" is a render-time property assertions can't capture. This work needs a screenshot/visual oracle, not more unit tests 1.

Worked example

"Make pricing cards equal height, align CTA buttons to the bottom."

Logic test can't express "equal height" — flexbox computes it at render time
Visual oracle agent renders /pricing, screenshots, looks: middle card taller, button floats mid-card → adds align-items: stretch + margin-top: auto → screenshots again → three equal cards. Proved by looking, not guessing 1
Regression guard that final screenshot becomes the baseline; next week's padding change is diffed against it

Per-agent note (capability differs — degrade gracefully)

Where the agent can't see, route the image to you — the loop is the same; the eyes move.

Capability Claude Code Codex Cursor
Agent views screenshots ✅ native (paste / browser MCP) partial ✅ native
Drive a real browser via browser tool / Playwright MCP via tools via MCP

Visual feedback is strongest on Claude/Cursor, which ingest the image directly 12. Where an agent can't see, the oracle downgrades: still render and screenshot, but route the image to you for the look-fix step — you become rung ① of the gradient for the visual layer only. The trap is having no visual check and trusting CSS because it parsed.

🧠 Test Yourself: An agent can't view images. Does that mean you skip the visual oracle?

AnswerNo — you downgrade it. Still render and screenshot, but you do the look-fix step. The loop survives; the eyes are temporarily yours. No visual check at all is the real failure 1.

Your turn (exercise)

On your next UI tweak, force the loop: have the agent screenshot before and after and put the two side by side. Find one thing the change broke that you weren't thinking about — a wrapped label, a shifted neighbor. That "I wouldn't have caught that by reading the diff" moment is exactly why visual work needs a visual oracle — code review can't see pixels 2.


Lesson 3.3 · next → Lesson 3.5 — What the scaffolder automates