How we test
The bug was never broken code. It was a green check over a wrong artifact.
Type-checks pass, tests pass, every route returns 200 — and the page still ships wrong. That’s the failure we kept hitting, and it’s the one a test suite designed to check what you told it to check will never catch. So the disciplines below aren’t a methodology we adopted. Each one is a specific bug that got past us, and the rule we wrote so it couldn’t happen the same way twice.
Unit tests
jest, over deterministic cores
catches: parser / formatter / clamp / money regressions
CI/CD gates
lint + typecheck, RLS & migration audits, a prerender build
catches: type / lint errors, un-RLS’d tables, build-only landmines
Screenshot validation
curl the served bytes; a headless browser (or a careful eye) on the real render — not yet wired into CI
catches: green-but-wrong renders — copy present, reading order, contrast
Each layer catches what the one before it can’t. The last one isn’t internal tooling — AI Personas is a product we ship, run on our own surfaces first, so what a customer gets is the thing we already trust our own pages to.
The honest gap: the amber layer — the rendered-artifact check that has caught our most expensive bugs — is also the least automated, and it’s the same on the QuickSites half of this mesh. It’s the only stage that needs a deployed artifact to point at, so it can’t run the moment you write the code; today it runs by discipline, not on a cron. The highest-value check being the least automated isn’t an oversight to hide — it’s the honest shape of the problem, and naming it is the point.
Verify the artifact that shipped, not the code’s story about itself
The most expensive misses share a shape: the source is correct and the received output is wrong, so nothing that reads the source ever sees it. The only instrument that catches this is fetching the served bytes and reading them.
The incident
A page’s Schema.org structured data — the block that tells Google who a writer is — was rendered client-side, gated on data that arrived after JavaScript ran. Every human check passed: you open the page, the schema is in the DOM, the validator you paste the rendered HTML into agrees. Only the crawler saw the truth, and a crawler never tells you. It was invisible to type-checks, to tests, and to a 200 response. A single curl of the served HTML found it in seconds.
We ran that sweep across every public page. Five surfaces were fetching their own content in an effect and shipping an empty shell to first paint; we fixed them. And the pages that mattered most for search — the comparison and roundup content — were clean. That’s worth stating plainly: a sweep that finds nothing where it would hurt most is a result, not a wasted afternoon. Silence is only reassuring if you actually ran the check.
A machine’s finding is a claim until a human agrees
We run browsing personas — backstoried, cost-capped agents that visit a public page with a first-time-visitor’s goal (“order dinner near me tonight”) and report what they actually hit: dead ends, confusing copy, a page that renders nothing. They catch the class Playwright can’t, because Playwright asserts what you told it to; a persona notices what you didn’t.
The load-bearing part isn’t the agent — it’s the discipline around trusting it:
- Every finding files as
triage, neveropen— a machine’s observation is a claim until a human confirms it. The status is hard-coded on the receiving side, so a payload that tries to file itself as confirmed work still lands in triage. One bad run must not be able to flood the real queue; that’s how a tool earns being left on. - “I couldn’t find X” and “I hit X” are different evidence. An absence the agent searched for and didn’t find is weaker than friction it ran into directly — because “didn’t find” is indistinguishable from “didn’t look.” We tag them separately and cap the weak one, so a confident-sounding absence can’t outrank a thing that actually happened.
- The label travels with the finding. Every report carries a fixed note — “AI persona observation, behaves as a real person would, not a human tester” — rendered verbatim, never stripped. A finding sold as human testing is a lie the first bad session exposes.
And it isn’t a script we keep to ourselves — it’s a product, AI Personas, run on our own surfaces before anyone else’s. Each one is a real backstory the agent inhabits — the point of the backstory is the blind spot it removes. A few of the roster (representative examples):
Who they think they are
Hungry at 7pm, wants dinner he can actually get tonight. Low patience for a sign-up wall between him and a menu.
What they notice
Every dead end between landing and an order — and the moment he’d bounce to a delivery app instead.
Who they think they are
A restaurant owner who got sent a link about her own kitchen. Skeptical: who made this, is it real, what does it cost me?
What they notice
Whether an unclaimed page reads as a trap or an offer — and where the “claim it” path hides.
Who they think they are
First job search in nine years. Doesn’t know the current jargon and won’t pretend to.
What they notice
Every place a page assumes knowledge a newcomer doesn’t have — the word that quietly excludes.
And here is where their runs land — the operations side, for a human to weigh, at triage, never as confirmed work:
this run
6 sessions
cost
$0.71 / $2.00 cap
status
3 in triage
AI persona observation — behaves as a real person would, not a human tester. Findings are claims at triage until a human confirms them.
Dish search returns 0 results but the "201 dishes" header still renders — a hungry visitor sees an empty page that claims to be full.
Hero pitches "claim your page" to a diner who wants dinner — the one sentence up top is for the wrong audience.
Looked for a way to filter by cuisine; couldn’t find one. (Weak evidence — may not have looked hard enough.)
Generated output is checked by a second instrument, never trusted by one
When a model reads a screenshot or writes a draft, “the code is correct” tells you nothing about the output — it’s the one artifact where the two are unrelated. So generated output gets a human confirm before it counts as real, and format gets its own separate check from content.
The incident
A generated file was written to a .webp path but was actually a 2 MB PNG wearing the extension. It looked perfect to the eye and to the browser; only reading the magic bytes revealed it. Eyes catch garbled text and wrong faces; they do not catch a mislabeled 2 MB payload on the one page a lost visitor loads on a bad connection. Two instruments, because neither substitutes for the other.
Same rule, one product over: when a vision model parses a calendar screenshot into events, the parse is shown for review before anything saves. A calendar that silently invents a meeting is worse than one you confirm — so the confirm isn’t optional.
Check your own guarantee before someone else repeats it
A sibling product wanted to tell its users, in its own voice, that our engine stores nothing they paste. Before we let that sentence stand, we read our own system end to end — every route, every write. The only persistence in the whole path is a usage log, and it records metadata only: which feature, token counts, cost. No pasted text, no résumé, no answer. We could confirm the claim as a fact instead of a hope, and the contract between the two products states it that way — with the exact edit it must never receive noted in the code, because “never sent anywhere” is one careless pass from “never stored.”
Cost is a test constraint, not an afterthought
Agentic tests and generative steps spend real money, so the budget is part of the test design: a hard step cap and a per-run cost cap, a kill switch, and a cache that never re-synthesizes a line it has already produced. An unbounded test loop is a test you can’t afford to run often enough to trust — so bounding it is what makes it a gate rather than a demo.
The through-line
None of this is rigor for its own sake. Every rule here is the scar tissue of a specific green-but-wrong ship, and the cheapest instrument that would have caught it: a curl, a second reader with no prior commitment to what the page was supposed to be, a re-import with real data present, a file over the bytes. Test the output your users receive, not the story your code tells about itself — because the gap between those two is exactly where the expensive bugs live.
The other half
This is one product’s half of a four-product mesh. QuickSites tests the same shape from the sitebuilder side — and marks its own highest-value layer as the least-automated, for the same reason (it needs a deployed artifact to point at). That the gap is identical on both sides is the honest part. How QuickSites tests →
These are HiveJournal’s disciplines, drawn from real incidents across a small mesh of products. Written to be useful, not to impress — if a rule here reads as obvious, it’s only obvious after the bug that taught it.