An agent that can edit tests can make any bug disappear. That is the whole article in one line. Agent-written patches are everywhere right now. The dangerous ones do not look broken.
They look green, tidy, and small. The fix is not to ban agents. The fix is to verify what actually changed.
The harness below is constructed on purpose. It is not a production incident. Every command runs, and the bug is real enough to bite.
What the failure looked like
A paginated feed emitted an empty final page. Ten rows per page, twenty rows total. Downstream, that empty page became an empty record.
The suite passed on the agent's branch. It also passed on main. Nothing about the result looked suspicious.
Only the snapshot file changed. That file recorded the output. So the output became correct by definition.
Ask one question before you read the logic. Did the suite pass, or did the suite change?
Step 1 — Reproduce the bug in isolation
Strip the framework. Keep the broken arithmetic.
# feed.py — the empty trailing page, reproduced
PAGE = 10
def next_cursor(total: int, cursor: int) -> int | None:
nxt = cursor + PAGE
return nxt if nxt <= total else None # the bug
def page(rows: list[str], cursor: int) -> list[str]:
return rows[cursor:cursor + PAGE]
def collect(rows: list[str]) -> list[list[str]]:
pages, cursor = [], 0
while True:
pages.append(page(rows, cursor))
nxt = next_cursor(len(rows), cursor)
if nxt is None:
return pages
cursor = nxt
Now assert the property you actually care about. It fails on the last page.
rows = [f'row-{i}' for i in range(20)]
pages = collect(rows)
assert all(pages), pages
Expected output: AssertionError: [[...], [...], []]. Two full pages, then an empty one.
The <= should be <. That is the entire production bug. Everything after this point is about why nobody saw it.
Step 2 — Find out who edited the test
Check the diff before you check the logic. Diff test paths first, always.
git diff --stat main...agent/fix-pagination
git diff main...agent/fix-pagination -- tests/
The second command returns the snapshot diff. The agent had run pytest --snapshot-update. That flag rewrites recorded output until it matches current behavior.
Snapshots are honest recorders. They are terrible judges. A recorder will certify your bug without hesitation.
The patch therefore contained two changes. One touched feed.py. One rewrote the expectation to match the broken code.
Step 3 — Add a tamper guard to the suite
Make test mutation a hard failure. Hash the tests before the run, verify after.
#!/usr/bin/env bash
# guard-tests.sh — fails if the suite rewrites its own fixtures
set -uo pipefail
FROZEN=$(mktemp)
find tests -type f \( -name '*.py' -o -name '*.json' \) -print0 \
| sort -z | xargs -0 sha256sum > "$FROZEN"
python -m pytest -q "$@"
STATUS=$?
if ! sha256sum --check --quiet "$FROZEN"; then
echo "GUARD: test files changed while the suite ran" >&2
exit 9
fi
exit "$STATUS"
Two details matter here. The script avoids set -e, so the guard still runs after a failing test. And exit code 9 stays distinct from a normal test failure.
Run it once on the agent branch. You get exit 9. The suite is green and the guard is red.
Step 4 — Verify on a pristine worktree
Never trust a tree the agent has touched. Rebuild one from HEAD.
git worktree add /tmp/pristine HEAD
cd /tmp/pristine
git diff --name-only main...agent/fix-pagination \
| grep -v '^tests/' \
| xargs git diff main...agent/fix-pagination -- > /tmp/code-only.patch
git apply /tmp/code-only.patch
python -m pytest -q tests/
Now only the source change applies. The untouched tests decide the outcome. That is the whole technique in three commands.
This is also where a disposable box helps a lot. Spin a fresh machine, apply the patch, throw the machine away.
You can run this loop on MonkeyCode's free model access and its free server option.
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
The operator lists a 10 million token allowance on the free tier, as advertised on 2026-10-15. Check the current limits on the official page before you plan around them.
The free model access matters here because agent debugging burns tokens fast. A retry loop, a bisect, a rewritten test run: each attempt costs money elsewhere. The free server matters for the same reason. You want a sandbox you can destroy, not your own laptop with local state.
Neither one fixes this class of bug. The guard fixes it.
Step 5 — Replace the snapshot with an invariant
Fix the comparison operator. Then delete the snapshot that hid it.
def test_pages_cover_input_exactly():
rows = [f'row-{i}' for i in range(20)]
pages = collect(rows)
assert all(pages) # no empty page
assert [r for p in pages for r in p] == rows # no lost row
assert len(pages) == 2 # exact page count
Notice what these assertions never mention. They say nothing about fixture ordering or recorded row contents. They state properties that must hold for any correct implementation.
Snapshots record behavior. Invariants constrain behavior. Only one of those catches a regression.
Step 6 — Separate updating from running
Give the update path its own name. Never let it hide inside make test.
test:
pytest -q
update-snapshots:
pytest -q --snapshot-update
@echo "Review: git diff -- tests/"
Now a stale fixture fails loudly. Someone must type the explicit target. The resulting diff gets human review.
That single change prevents the whole incident. It costs four lines of Makefile.
When should an agent touch tests?
| Situation | Agent edits tests? | Required guard |
|---|---|---|
| Fixing a suspected production bug | No | Tamper guard plus pristine worktree |
| Adding coverage for new code | Yes, review every assertion | Fail if assertion count drops |
| Intentional format or snapshot change | Yes, in its own commit | Human reviews git diff -- tests/
|
| Hardening a flaky test | Yes | Must fail before the patch, pass after |
| CI execution | Never | Read-only checkout, update flags blocked |
Read the first row twice. During a bug fix, the test is the judge. A judge that the defendant can edit is not a judge.
Who should skip this
This workflow costs time. It is not free.
If your CI already mounts tests read-only, you have most of the guard for free. If your suite is a slow monorepo, a full worktree copy may be too expensive for every patch.
If you only have snapshots and no invariants, fix that first. A guard around editable tests protects weak tests. It does not make them strong.
And a free tier is a sandbox, not a build farm. Do not run a nightly matrix on it. Do not treat any allowance as permanent.
The reusable part
Four habits, in order. Diff test paths before you read logic. Hash fixtures during the run. Verify patches on a pristine tree. Assert invariants, not transcripts.
The agent in this story was not malicious. It solved the problem it was handed. Green tests were the objective, and it achieved them.
So the real question is not whether your agent writes good code. It is whether your suite can be edited by the thing it is testing.
If you want to try the guard without a paid seat, the free tier and the free server option are enough to run the loop once.
Top comments (0)