/p/2026-07-09 · explainer
Paper explainer · 2607.06636 · Haeri & Ghelichi, TD Bank

Tests don’t work because they exist.
They work because they know — not guess.

In an LLM self-test-and-repair loop, changing one line of the test-writer’s prompt — pasting the task specification as an enumerated rule checklist instead of saying “include edge cases” — raises final code correctness by +38 percentage points on every Claude tier, while cutting false alarms on correct code from 33% to 0%.

01 · The problem

LLM code drops the edges nobody stated — and LLM tests guess at them

Models write code that nails the happy path and silently drops unstated edge cases: missing input checks, unhandled boundaries — e.g. Opus 4.8’s parse_range crashes on "1-2-3", "" and "-3-5" with opaque int() errors. The standard fix (CodeT, AlphaCodium, LEVER) is “generate tests, repair until green.” But why does that work? Two hypotheses with opposite advice: quantity — any plausible test sometimes trips a bug, so generate more — or grounding — tests derived from an external specification aim at the behaviours that matter.

The paper isolates the question to a single line. Both arms use the identical tester system prompt, the same tester model (Sonnet 4.6), the same budget (K tests, K = number of spec rules), the same repair loop, and a hand-written gold oracle no model ever sees. Flip the line yourself:

Interactive · the single-line diff8 core tasks · 48 instances · FA: trickier-specs slice
# tester user prompt (system prompt, model, budget K all identical) [ticket text] The specification is these K rules: [R1..RK]. Produce exactly K test cases, one focused case per rule, in order. Compute expected values from the specification, never from code.
30/30bugs detected
100%final correctness
0%false alarms on correct code

The ungrounded arm is not a strawman: it is explicitly told to “include cases for invalid inputs, error conditions, and boundary/edge values.” That one line still moves final correctness 60% → 100%.

02 · The mechanism

A rule checklist tells the tester what “correct” means at each edge

To write a test, the author must know the intended behaviour at each edge. An ungrounded tester must guess — twice. If it doesn’t think to probe an edge, the bug escapes (false negative). If it probes but guesses the wrong intended value, it rejects correct code (false positive). The enumerated spec supplies the missing information, so detection and precision improve together instead of trading off.

Interactive · rules name the non-obvious edgesper-task detection, 6 buggy draws each
free+
spec

The edge-prompted tester catches the obvious edge but not the non-obvious one; one test per rule catches both 6/6, because a rule names each edge. On clamp, invalid-input tests written: free 0 · free+ 2 · spec 6.

Interactive · one root cause, two error types30 buggy core one-shots · FA from trickier behavioural specs

Missed bugs (false negatives)

False alarms (false positives)

Grounding empties both cells at once: detection 18/30 → 30/30 and false alarms 33% → 0%. There is no sensitivity/precision dial being turned — the tester simply stops guessing.

Is it the spec’s content, or the checklist structure?

“spec” bundles three things: the spec’s content, its decomposition into rules, and a one-test-per-rule plan. Four conditions at fixed budget and tester pull them apart:

Interactive · content vs structure ablationdetection on 30 buggy one-shots
spec

Which rules carry the signal?

Interactive · specification dose-responsedetection on 60 buggy one-shots
full
detection

03 · The controls

Quantity, ensembles, and peeking at the code are not the lever

If the quantity hypothesis were right, doubling the test budget or unioning many independent suites should close the gap. Step through the arms:

Interactive · arms explorer8 core tasks × 2 models × 3 seeds
final correct
detection

Interactive · best-of-N ungrounded suitesreal-world slice · 29 bugs
k=1
union detection, % of 29 real-world bugs
spec, single draw: 100%
union

Never show the tester the code

In every arm, expected values come from the ticket or rules — never the candidate code — because a tester shown buggy code treats the bug as intended. The paper verified this directly by regenerating tests for 24 buggy one-shots with the candidate code pasted into the tester prompt:

Interactive · code-in-prompt control24 buggy one-shots
free+
spec

Catching is not fixing

Repair is only as good as the failing test’s expected value. On the six scale-up tasks detection ties (40/42 both arms) — yet final correctness is 96% vs 78%, because the ungrounded arm repairs toward its own wrong expectation. Step through the paper’s format_duration case:

Interactive · repair step-throughformat_duration · scale-up slice

What if the spec itself is wrong?

Interactive · corrupting the spec5 buggy core tasks
Detection
30/30
completeness governs detection
Tester accuracy
100%
correctness governs precision

04 · The evidence — and where it stops

Replicated across vendors, stacks and real oracles; null where specs are already complete

+38 pp
final correctness gain, every Claude tier
Haiku 4.5 / Sonnet 4.6 / Opus 4.8 as coder; free+ plateaus at 62%, spec reaches 100% (8 core tasks). Smallest model + grounding beats largest model one-shot.
+28 / +19 pp
cross-vendor replication
GPT-5.3-codex (72→100%) and Gemini 3.5 Flash (72→92%) as coder+tester+repairer, 12 tasks × 3 seeds.
30/30 vs 18/30
bug detection, spec vs edge-prompted free+
On the 30 buggy core one-shots; plain free catches 0/30, free@2K catches 3/30.
0% vs 33%
false alarms on correct code
Trickier behavioural specs. Against the CPython stdlib oracle free+ hits 68% false alarms; against PyPA packaging, 100% — it rejects everything.
+68 pp
real-world external-contract slice
Tasks whose edges are fixed by RFC 791, CSS hex grammar, ISBN-10 etc.: spec 100% (87/87) vs free+ 32% (28/87) — the largest gap in the paper.
p = 0.002
task-level sign test
18 specification-completeness tasks: spec strictly better on all 9 tasks showing any difference, mean +31 pp; pooled over 36 independent-stack tasks, p≈6×10⁻⁵.
72% = free+
AlphaCodium-style flow adds nothing
Full agentic flow on GPT-5.3-codex (reflect, 2K self-tests, 3 dual-fix rounds) lands exactly at the ungrounded level; spec on the same base: 100%.
~$6–7 / 1k tasks
cost of grounding ≈ cost of baseline
Three model calls either way. Matching the gain with model scale alone costs ~2.2× tokens and tops out at 54%.

The honest null — and the fine print

On a HumanEval+ port (24 tasks, 5 model families) the gap vanishes: spec detects 13/22 vs free+ 14/22. Well-specified algorithmic benchmarks barely produce the bug class (pooled one-shot bug rate 6%), and 8 pure-logic tasks yielded 0 natural bugs in 45 valid submissions. The claim is bounded to specification-completeness defects — mostly small pure Python functions plus a stateful slice. The tester is deliberately strong (weak testers keep a 44–67% false-alarm floor even with rules), the repair loop is minimal, auto-derived specs recover only 47% of detection (edge-forcing recovers it but adds 33% false alarms — the binding cost is human knowledge of correct edge semantics), and the absolute 100% scores are artifacts of easy tasks: the stable quantity is the gap and its direction. No public code repository is released; the study is described as reproducible from task specs, reference implementations, gold suites and raw outputs.

05 · Why you care

Your codegen repair loop, with and without the one-line change

If your product has a “write tests, then fix until green” step, this is a one-prompt-change experiment you can run this week. Walk the loop below and flip grounding on the test-writer step. Illustrative walkthrough — the code and tests are simulated; the per-task counts quoted are the paper’s.

Interactive · ticket → code → tests → repairworked example: clamp, lo > hi

The copy-ready technique

Enumerate the spec as R1..RK checkable rules; one focused test per rule, in order; the tester never sees the code; every expected value computed from the rules. Rules about invalid input become error-expecting tests.

SYSTEM You write tests for a function you will NEVER see implemented. Do not ask for, read, or reason from any candidate code. USER Task ticket: <paste the ticket> Specification — K enumerated, checkable rules: R1: <rule> R2: <rule> ... RK: <rule> Produce exactly K test cases, one focused case per rule, in order. For each test output: {"rule": "Rn", "args_json": ..., "expect_error": true|false, "expected_json": ...} Compute every expected value from the rules alone. Any rule about invalid input becomes an error-expecting test.

Practical notes from the paper’s sweeps: keep the spec version-controlled next to the ticket (dropping validation rules degrades detection visibly, never silently); don’t fully trust auto-derived rules (detection automates, precision doesn’t); and don’t score suites by mutation kills — mutation score actually favoured the ungrounded arm (95.9% vs 93.9%) because it rewards sensitivity regardless of whether the expected values are right.