Skip to content
arun mv
Back to blog
AI & Engineering

Your Evals Have No Tests

A green eval suite isn't evidence your agent works — it's the absence of evidence. Mutation testing shows you exactly how blind your evals really are.

· 10 min read

Your Evals Have No Tests
TL;DR the 30-second version

A green eval suite is not evidence your system works — it’s the absence of evidence, until you’ve watched it go red for a reason you planted. Mutation testing answers that: deliberately break the prompt, retrieval, model, and agent layers, rerun your evals, and count what gets through. Most production suites let 60%+ of those breaks through, and the ones that escape cluster in the exact failures evals were supposed to catch — wrong-year context that reads fluent, skipped auth checks that still refund correctly. A second number, the noise floor, tells you whether an escape is a real blind spot or just below what your suite can ever resolve. Report both, or neither is meaningful.

In how we measure RAG quality, the fix for “vibes don’t survive a second engineer” was to wire an eval set to a hard CI threshold — NDCG@10 has to clear 0.75 or the change doesn’t merge. That’s the right fix for the problem it solves: no more shipping on “looks better to me.”

But it opens a question nobody asks next: how do you know the gate itself works?

You know your unit tests work because you’ve watched them fail. Break the function, the test goes red, you fix it, the test goes green. That loop is what earns a test suite its authority — not that it’s green, but that you’ve seen it go red for the right reason. An eval suite almost never gets that loop. It goes green on day one, it stays green, and teams slowly start treating “green” as evidence of quality rather than what it actually is: the absence of a signal that anything’s wrong yet.

The question mutation testing asks

Normal software solved this decades ago. DeMillo, Lipton, and Sayward proposed the idea in 1978, and modern tools like PIT (Java), Stryker (JS/.NET), and mutmut (Python) run it in CI today: make small, deliberate breaks in your source — flip a > to >=, invert a boolean, delete an assignment — then rerun the test suite against each mutated version.

  • If the tests fail, the mutant is killed: your tests noticed.
  • If the tests pass, the mutant escaped: your tests are blind to that entire class of bug.

The mutation score — mutants killed ÷ mutants generated — is a stricter adequacy bar than line or branch coverage, because coverage only asks “did this line execute,” and mutation testing asks “would a test actually notice if this line were wrong.” Mature Java codebases running PIT in CI typically land in the 60–90% mutation-score range even after years of investment; greenfield suites frequently start below 40%. The gap between “we have tests” and “our tests would catch a bug” is usually that large, even in a discipline forty years older than LLM evals.

The insight that should worry you about evals is the same one that motivated mutation testing in the first place: you can delete every assertion in a test file and still report 100% line coverage. Coverage measures execution, not correctness. Just et al. (ISSTA 2014) went further and showed mutants are a statistically valid proxy for real faults — suites that kill more mutants also catch more real, human-introduced bugs. That’s the load-bearing claim: mutation score isn’t a toy metric, it correlates with the thing you actually care about.

Now translate that to evals. What’s the eval-suite equivalent of “100% coverage but zero assertions”? It’s “we have 120 examples and four metrics.” That number tells you the suite ran. It tells you nothing about whether it would notice if your system got worse.

Nineteen ways to break your own system

The translation from code mutants to eval mutants is direct, and the catalogue writes itself out of every production incident you’ve ever had:

SurfaceMutationWhat it’s supposed to catch
PromptDelete a constraint word (only, never, must)Guardrail language quietly dropped in a refactor
PromptRemove the grounding instructionModel reverting to parametric knowledge over context
PromptDrop the output-format specDownstream parsers silently failing on malformed output
PromptDelete a few-shot exampleFormat/style drift nobody notices until support tickets
PromptSoften must to shouldPolicy language losing its teeth
RetrievalTruncate context to top-1Whether your metrics actually need the full context window
RetrievalShuffle rank order”Lost in the middle” sensitivity
RetrievalInject one document from the wrong policy year / wrong tenantGrounding checks that only verify a source exists, not the right one
RetrievalHalve the chunk overlapAnswers that straddle a chunk boundary
ModelDowngrade a tierThe easiest mutant to kill — and the one everyone over-indexes on
ModelPush temperature 0 → 1Consistency checks that assume determinism
ModelCut max_tokens by 40%Silent truncation mid-answer
AgentSkip an authorisation tool callOutcome-only evals scoring a correct refund as a pass regardless of how it got there
AgentSwap the order of two tool callsEvals that check final state but not the causal path
AgentMake one tool return an errorWhether the agent has a real fallback or just retries blindly
AgentRemove a tool from the available setGraceful degradation vs. silent capability loss

Apply each mutation, rerun your existing eval suite unchanged, and count what got through. That number — escape rate — is the first honest measurement of eval quality most teams will ever compute.

Mutants killed vs. escaped, by surface prompt retrieval model agent wrong-year ✗ skip-auth ✗ killed escaped
19 mutants across four surfaces, 4 killed and 15 escaped (79% escape rate). Escapes cluster where the metric was never looking: injected wrong-year context and a skipped auth check both scored identically to the unmutated baseline.

Most production suites land north of 60% escape rate the first time they run this, and the escapes cluster in predictable, embarrassing places:

  • Nearly everyone catches the model downgrade. Metrics are usually output-quality metrics, and a weaker model writes visibly worse text. This is the mutant that gives teams false confidence — “our evals work, look, they caught the GPT-3.5 downgrade” — while telling you nothing about the other three surfaces.
  • Nearly nobody catches inject-wrong-year-document. The answer stays fluent, stays grounded-looking, and is confidently wrong — precisely the failure mode the suite was supposedly built for. Faithfulness scorers check “is this claim supported by some retrieved passage,” not “is the retrieved passage the correct one.” A wrong-year policy document supports the claim just fine.
  • Essentially nobody catches skip-auth. Outcome-only evals score a refund as correct if the dollar amount matches, independent of whether the agent verified authorization before issuing it. This is the mutant that maps most directly to real incidents: the agent did the right thing by accident, and no metric in the suite was ever positioned to notice it skipped a step.

The second number: can your suite see anything at all?

There’s a failure mode that looks identical to a missing check on the page but needs the opposite fix, and conflating the two is how teams end up writing metrics that can never work.

Run your unchanged suite five times. Change nothing — same prompts, same model, same everything. Watch the numbers move anyway, because LLM output is stochastic even at temperature 0 once you account for batching nondeterminism, retrieval index drift, and judge-model variance. That spread is your noise floor, and it sets a hard ceiling on what the suite can ever detect, independent of how many metrics you bolt on.

Can the suite see anything at all? 0.70 0.95 noise band (σ=0.041) 5 re-runs, same suite 3pt threshold real 6.8pt drop — still hidden MDE 9.2pt — first drop this suite can prove
Five re-runs of the same unchanged suite (n=40) scatter within a σ=0.041 noise band — a minimum detectable effect of about 9.2 points. The 3-point CI threshold sits inside the noise: it fires on chance roughly half the time. The wrong-year escape (a real 6.8-point drop) is still inside the band and invisible; only widening the eval set narrows the band enough to see it.

The statistic that formalizes this is the minimum detectable effect (MDE): roughly, how large a real regression has to be before it’s reliably distinguishable from run-to-run noise, given your sample size and observed variance. A 40-example set with a standard deviation of 4 points has an MDE around 9 points — smaller regressions are, by construction, indistinguishable from chance. Configure a 3-point CI threshold on top of that and the gate fires on noise roughly half the time. Engineers learn the pattern fast: re-run it until it’s green. The check becomes decorative, and worse, it trains the team to distrust the next alert too, including the real one.

So an escaped mutant has two possible diagnoses, and mixing them up sends people in the wrong direction:

  • Blind spot — the drop was real and large, but no metric was pointed at it. Write a new check.
  • Below resolution — the drop was real, but smaller than the suite’s noise floor. Add examples until MDE clears the effect size you care about, or explicitly accept you can’t detect it yet.

Reporting escape rate without resolution sends people off writing metrics that mathematically cannot help — you can add a hundred new assertions to a 40-example set and the MDE barely moves, because it’s driven by sample size and variance, not metric count. Both numbers or neither.

What this looks like in practice

SABOT — 19 mutants, 4 killed, 15 escaped (79% escape rate)

KILLED    prompt/delete-only            groundedness 0.86 → 0.71  ✓
KILLED    retrieval/top-1               recall@5     0.91 → 0.62  ✓
ESCAPED   retrieval/inject-wrong-year   groundedness 0.86 → 0.85  ✗ CRITICAL
ESCAPED   agent/skip-auth               task_success 0.88 → 0.88  ✗ CRITICAL
ESCAPED   model/temp-1.0                all metrics within noise  ✗ major

Resolution: n=40, σ=0.041 → MDE 9.2pts. 11 escapes sit below your
suite's noise floor. Adding metrics will not help until n ≥ 310.

Two caveats, because this technique has a real, well-documented failure mode of its own.

The thing underneath

The measuring RAG quality post argued that a green dashboard is not evidence your agent works. This one argues the sharper version: a green eval is not evidence either, until you’ve watched it go red for a reason you planted — and until you know whether it’s even capable of going red for the regression you actually care about.

That’s the loop sabot is built to close. Break it on purpose. Count what got through. Measure whether the suite could have seen it at all.

What would escape your suite today?


Related reading