A patch lands on a small utility library. It adds the missing test for a paging helper, fixes a ceiling-division bug, and, in the same commit, rewrites two older assertions into looser >= comparisons. CI is green, the diff reads cleanly, and the review takes ninety seconds. Six weeks later, a pagination regression slips through the exact code path those softened assertions used to guard.
That scenario is a composite, not a specific incident, and it can be rebuilt locally in about ten minutes. Everything below runs against a four-file fixture repository that any reader can construct with the commands shown. The goal is a mechanical check that a patch's tests actually observe the patch's behavior, instead of merely coexisting with it.
Why a green suite proves less than it appears
A passing suite answers one narrow question: the assertions that exist did not fail. It says nothing about whether those assertions are strict enough, or whether the new test would also pass on the unfixed code. Reviewers usually compensate with attention, which is exactly the resource that runs out on a Friday afternoon.
Three cheap signals restore most of the missing information. The first is whether the new test fails on the base commit. The second is whether the suite fails when the fix is removed but the tests are kept. The third is how many assertion lines the patch deletes or relaxes.
The three states every patch should visit
Each state is a detached Git worktree, so no branch juggling and no stash risk is involved:
- Base state — upstream tip plus the contributor's test changes only. The new test must fail here, and it must fail for the intended reason.
- Patched state — the contributor's full diff applied. The suite must pass here.
-
Mutant state — the patched tree with
src/reverted to base and the tests left alone. The suite must fail here, or the tests do not observe the fix at all.
State three is the one most review workflows skip, and it is the only state that detects a test written to describe the fix rather than to constrain it.
Step 1: build the fixture repository
The fixture is deliberately small: one module, one test file, one bug. The bug is a missing ceiling division that returns zero pages for any total smaller than the page size.
mkdir tamper-demo && cd tamper-demo
git init -q
mkdir -p src tests bin
src/paging.py on the base commit:
def pages(total, size):
return total // size
tests/test_paging.py on the base commit:
from src.paging import pages
def test_exact_multiple():
assert pages(10, 5) == 2
def test_large_total():
assert pages(11, 5) == 2
def test_zero_total():
assert pages(0, 5) == 0
Commit that as main, then create a patch branch that fixes the division, adds a fourth test, and weakens two existing assertions. The added test uses pages(10, 5) == 2, which passes on the buggy code, so it captures nothing.
Step 2: the three-state script
The script below assumes pytest is installed and that src/ is the only directory carrying the fix. Both assumptions are adjustable, and the limitations section returns to them.
#!/usr/bin/env bash
# bin/three-state-check.sh <base-ref> <patch-ref>
set -euo pipefail
BASE_REF="${1:-origin/main}"
PATCH_REF="${2:-HEAD}"
PY="${PY:-python3}"
WORK="$(mktemp -d)"
LOG="$WORK/report.txt"
status=0
cleanup() {
for d in base patched mutant; do
git worktree remove --force "$WORK/$d" 2>/dev/null || true
done
}
trap cleanup EXIT
note() { printf '%s\n' "$*" | tee -a "$LOG"; }
suite() { ( cd "$1" && "$PY" -m pytest -q 2>&1 | tail -n 25 ); }
git worktree add -q --detach "$WORK/base" "$BASE_REF"
git worktree add -q --detach "$WORK/patched" "$PATCH_REF"
git worktree add -q --detach "$WORK/mutant" "$PATCH_REF"
git -C "$WORK/mutant" checkout -q "$BASE_REF" -- src/
note "== state 1: base ($BASE_REF) =="
base_out="$(suite "$WORK/base")"
printf '%s\n' "$base_out" | tee -a "$LOG"
if printf '%s' "$base_out" | grep -qE '[0-9]+ passed' && \
! printf '%s' "$base_out" | grep -qE '[0-9]+ failed'; then
note "WARN base is green; the failing reproduction is missing"
fi
note "== state 2: patched ($PATCH_REF) =="
patched_out="$(suite "$WORK/patched")"
printf '%s\n' "$patched_out" | tee -a "$LOG"
if printf '%s' "$patched_out" | grep -qE '[0-9]+ failed'; then
note "FAIL the patch does not make the suite pass"
status=1
fi
note "== state 3: mutant (fix reverted, tests kept) =="
mutant_out="$(suite "$WORK/mutant")"
printf '%s\n' "$mutant_out" | tee -a "$LOG"
if ! printf '%s' "$mutant_out" | grep -qE '[0-9]+ failed'; then
note "FAIL the suite passes without the fix; the tests observe nothing"
status=1
fi
note "== assertion churn =="
added="$(git diff -U0 "$BASE_REF" "$PATCH_REF" -- 'tests/' \
| grep -E '^\+[^+]' | grep -c 'assert' || true)"
removed="$(git diff -U0 "$BASE_REF" "$PATCH_REF" -- 'tests/' \
| grep -E '^-[^-]' | grep -c 'assert' || true)"
note "assertions added=$added removed=$removed"
if [ "${removed:-0}" -gt 0 ]; then
note "REVIEW the test diff first: $removed assertion line(s) removed"
fi
exit "$status"
Step 3: read the output like a reviewer
Running bin/three-state-check.sh main patch on the fixture produces a report close to the following. The exact pytest counts vary with the fixture, and the verdict lines do not.
== state 1: base (main) ==
1 failed, 3 passed in 0.11s
== state 2: patched (patch) ==
4 passed in 0.09s
== state 3: mutant (fix reverted, tests kept) ==
4 passed in 0.09s
FAIL the suite passes without the fix; the tests observe nothing
== assertion churn ==
assertions added=1 removed=2
REVIEW the test diff first: 2 assertion line(s) removed
Two independent signals point at the same weakness. The mutant run proves the new test cannot distinguish fixed from broken code, and the churn count shows that two earlier guarantees were softened. Neither signal is conclusive on its own, but together they justify a request for changes instead of an approval.
The failure message matters as much as the failure itself. A mutant run that fails on an import error has proved nothing about the assertion, so the log should be checked for AssertionError or the named test before the result counts.
Where free models genuinely help, and where they do not
A free model is useful for the parts of this loop that are text-shaped and low-risk, and it is a poor substitute for the maintainer's judgment. The division of labor that has held up in practice looks like this:
- Model-assisted: rewriting a failing mutant log into a two-sentence PR comment, drafting the missing test from the diff, naming the observable behavior a patch changes, and proposing which existing assertions a refactor legitimately touches.
- Maintainer-only: deciding whether a weakened assertion is justified, judging whether a suite is flaky rather than wrong, and approving the merge.
A prompt skeleton that keeps the model inside those bounds:
You are given: a unified diff, the base test log, the patched test log,
and the mutant test log. Do not propose a merge decision.
1. List the observable behaviors the diff changes.
2. For each new test, state whether it would fail without the diff.
3. Propose one additional test that would fail in the mutant state.
4. Mark anything you cannot tell from the logs as unknown.
Free model access through MonkeyCode covers the drafting steps above without a paid key, and the free server option gives the three worktrees a disposable host instead of the reviewer's laptop. Disclosure: This article was prepared as part of MonkeyCode's product outreach. Capacity and terms for both the free model access and the free server option are operator-supplied and can change, so check the currently published terms before depending on them in a team workflow.
The disposable host is a real convenience rather than a marketing point. Three worktrees plus a virtualenv plus a test cache accumulate quickly, and a host that can be destroyed after the report is written makes "start from a clean state" the default rather than a chore. Running the script there and copying only the report log back keeps the review artifact small and auditable.
git clone --quiet https://example.invalid/your-fork.git verify
cd verify && git fetch --quiet origin pull/1234/head:pr-1234
PY=python3 bin/three-state-check.sh origin/main pr-1234 > report.txt 2>&1 || true
A decision table for patch triage
The table converts the script output into a repeatable first response, which matters more than any single verdict.
| Signal | Merge | Ask the author | Block |
|---|---|---|---|
| New test fails at base, passes patched, fails mutant | yes | — | — |
| Mutant state passes | — | — | yes |
| Assertion lines removed | — | yes, with a written reason | — |
| Test files exceed 30% of the diff | — | yes | — |
| Base state already green | — | yes, reproduction missing | — |
Limitations and failure modes
The mutant run is a heuristic, and it degrades in predictable ways that reviewers should anticipate:
- Reverting
src/can break imports, so the mutant failure may originate outside the assertion under test. - Suites that assert on mocks or log strings often stay green in the mutant state even when behavior changed.
- Flaky tests make state three noisy, and a retry policy is needed before the signal is trusted.
- Large monorepos need affected-test selection, which reduces the mutant run's coverage to the selected subset.
- Patches that legitimately rename or restructure tests will trip the churn counter and require manual reading.
Who should skip this workflow
Repositories without a working test command should not start here, because the script only amplifies a suite that already exists. Teams that treat tests as documentation rather than constraints will find state three permanently red and will disable it within a week. Maintainers reviewing one small patch a month will get more value from reading the test diff carefully than from maintaining three worktrees and a shell script.
The workflow earns its keep on repositories where patch volume outpaces reviewer attention, and where a weakened assertion is expensive to discover late. Three worktrees, one script, and a log file are enough to make the check routine, and readers who want a throwaway host for the run can trial MonkeyCode's free server option on a scratch fork and compare the report against their local output.
chmod +x bin/three-state-check.sh
./bin/three-state-check.sh main patch; echo "exit=$?"
Top comments (1)
The mutant state is such a clean idea, basically one-shot mutation testing aimed at exactly the lines the patch says it fixes. A small gotcha if you script it: if the patch adds new files under src/,
git checkout base -- src/leaves them in place, so the mutant can quietly keep part of the fix. Deleting src/ before checking it out from base avoids that. Does your relaxed-assertion count catch swapped matchers too (like toEqual becoming toContain), or just comparison operators?