Skip to content
allsrc.dev
Go back

"I've Fixed The Failing Test" Is A Claim, Not A Completion Signal

Agent: I’ve fixed the failing test.

pytest: FAILED tests/test_invoice.py::test_total — assert 4200 == 4300

Nothing went wrong in any way your monitoring can see. The agent made a plausible change, wrote a plausible summary, and returned success. Anything downstream that trusts the summary — a ticket transition, a CI step, a metric, a human skimming the thread — now believes the work is done.

This is not a hallucination problem you can prompt away. A confident summary of work that did not happen is exactly as plausible as one of work that did, because plausibility is what the model optimises. The only fix is to stop asking it.

TL;DR

the agent says done -> an INDEPENDENT verifier checks -> pass,
                       or the actual failure goes back as the next instruction

Two properties make a verifier worth having:

  1. Independence. Running pytest is independent. Asking the same model “did you do a good job?” is theatre — it shares the failure mode you are trying to catch, and it will confidently agree with itself. You will have built a very expensive return True.
  2. Determinism, where you can get it. Tests, compilers, linters, schema validators either pass or fail. Prefer them absolutely. An LLM judge is the fallback for what you cannot automate, and it belongs behind a label so nobody mistakes it for a fact.

OWASP: ASI09 — the benign, everyday form of human-agent trust exploitation. Runnable code: patterns/harness/verification_loops/

The loop, as program output

uv run python -m patterns.harness.verification_loops.demo
=== WITHOUT the pattern ===
  agent reports: I've fixed the test.
  tests actually pass: False
  ^ recorded as a success by anything that trusts the agent's summary

=== WITH the pattern ===
  attempt 1: REJECTED — I've fixed the test.
    completion-claim: FAILED tests/test_invoice.py::test_total
  attempt 2: accepted — Fixed — the discount is now applied before tax

  verified: True after 2 attempts
  tests actually pass: True

  what attempt 2 was actually told:
    You reported the work as complete, but verification disagrees:
    FAILED tests/test_invoice.py::test_total
      AssertionError: assert 4200 == 4300
      (tax is applied before the discount)
    Do not report completion again until the check passes.

=== deterministic verifiers and judges are not the same thing ===
  pytest         ok=True  deterministic
  customer-tone  ok=False NON-deterministic (evidence)

The pattern

loop = VerificationLoop(
    verifiers=[
        CommandVerifier("pytest", run_pytest),
        CommandVerifier("ruff", run_ruff),
        ClaimVerifier(evidence=run_pytest),   # catches "it's fixed" when it isn't
    ],
    max_attempts=3,
)
outcome = loop.run(harness_factory, "Fix the failing invoice total test", ctx_factory)

1. This is an outer loop, not a hook. Verification runs after the run completes; failure starts a new run. A hook that rewrote the final answer could hide the failure — re-running makes the retry visible, budgeted, and auditable.

2. The feedback is the failure, not a scolding. Attempt two receives the actual assertion, the file, and the line, plus “do not report completion again until the check passes.” An error message is a prompt; “please try harder” is not one.

3. Attempts are budgeted, and exhaustion is honest. After max_attempts the outcome is verified=False with every attempt’s failures retained. An unverified result must never be silently returned as success — that is the original bug with extra steps.

4. All verifiers must agree, and failures are collected together so one attempt can fix everything rather than discovering the linter after the tests.

When to use it

When NOT to use it

Trade-offs and failure modes

Frequently asked questions

Is this just the evaluator-optimizer pattern?

It is that pattern with a specific, opinionated constraint: the evaluator must be independent and preferably deterministic. The generic formulation allows an LLM evaluator, which is where most implementations quietly lose the property that makes the loop worth running.

Can the same model be the verifier if I use a different prompt?

No. A different prompt does not create independence — the same weights, the same training, and often the same context produce correlated errors. If the only available verifier is a model, use a different model, accept it is evidence rather than proof, and label it as such.

How do I verify something that has no test?

Write the smallest deterministic check that would have caught the last failure of this kind. A schema validation, an invariant, a reconciliation, an exit code. If you truly cannot express one, that is a useful finding: it means “done” is undefined for this task, and the agent was never going to be reliably right.

Where does this run — inside CI, or in the agent?

Both, for different jobs. Inside the agent’s loop it converts a failure into a retry. Inside CI it converts a failure into a blocked deploy, which is the eval gate.

References


Part of the agent harness and governance series. Next: tool design — the mistake that costs 17× more tokens than it needs to.



Previous Post
The Retry That Paid The Invoice Twice
Next Post
Your Tool Definitions Are Prompts, And Most Are Bad Ones