You do not understand the messy function. That is fine. The snapshot does not care about your understanding. Golden files turn "I think this is safe" into "the diff says so." This loop has four commands: record, verify, move, commit. Each move is one semantic change. The snapshot judges every move.
A 600-line function hides its contract. Callers see the return value. They also see database writes, emails, and exceptions. Human reviewers guess about those side effects. AI reviewers guess with more confidence. Neither can prove the behavior is identical. A golden file can.
The golden file is a recorded behavior. It stores the return value and side effects for a fixed input. It is not a unit test. It encodes no intent. It only says: this is what the code did on this input. That is enough for a refactor.
Choose one entry point. This walkthrough uses import_orders(raw_rows)
from a legacy module. The function parses rows, writes to a database, sends emails, and returns a list. That is the boundary contract.
def import_orders(raw_rows):
orders = []
for row in raw_rows:
if not row.get("sku"):
continue
qty = int(row.get("qty") or 1) # legacy default: None -> 1
if qty < 0:
qty = 1
order = {"sku": row["sku"], "qty": qty}
orders.append(order)
return orders
Record four things per input: the return value, the DB writes, the emails, and the exception type. The recorder replaces the module's side effects with logging stubs.
import json
import sys
from pathlib import Path
import legacy # the messy module
def run_case(payload):
events = []
legacy.db.insert = lambda row: events.append(("db", row))
legacy.mailer.send = lambda mail: events.append(("mail", mail))
try:
result = legacy.import_orders(payload)
return {"return": result, "events": events, "error": None}
except Exception as exc:
return {"return": None, "events": events, "error": type(exc).__name__}
def main(action, snapshot_dir, fixture_dir):
snap = Path(snapshot_dir)
for case in sorted(Path(fixture_dir).glob("*.json")):
record = run_case(json.loads(case.read_text()))
golden = snap / f"{case.stem}.golden.json"
if action == "record":
golden.write_text(json.dumps(record, sort_keys=True, indent=2))
continue
if golden.read_text() != json.dumps(record, sort_keys=True, indent=2):
print(f"BEHAVIOR CHANGED: {case.stem}", file=sys.stderr)
sys.exit(1)
print("snapshot verified")
if __name__ == "__main__":
main(sys.argv[1], sys.argv[2], sys.argv[3])
The stubs assume the function references module attributes. If it imports helpers directly, patch those helpers instead. The principle stays the same: record, do not trust.
The fixtures determine the value of the whole loop. Replay logged production payloads if you have them. One week of real rows beats ten hand-written cases. Weak fixtures create fake confidence. Make sure the corpus hits every branch, every error path, and the None
quantity default. The snapshot can only judge what the corpus exercises.
python tools/characterize.py record snapshots fixtures
python tools/characterize.py verify snapshots fixtures
Why not write unit tests instead? Unit tests encode expected behavior. Golden files record actual behavior. For a messy function, intent is exactly what you lack. Recording is faster and more honest than guessing the contract in advance.
Now the current behavior exists as files on disk. From this point, the snapshot is the specification.
"Smallest" needs a measurable definition. Use a diff budget. The budget allows one file and a bounded number of changed lines per move. This shell gate enforces it:
#!/usr/bin/env bash
set -euo pipefail
MAX_CHURN=80
verify() { python tools/characterize.py verify snapshots fixtures; }
budget() {
local churn files
churn=$(git diff --numstat -- '*.py' | awk '{a+=$1; d+=$2} END {print a+d}')
files=$(git diff --name-only -- '*.py' | wc -l)
if (( churn > MAX_CHURN )); then
echo "budget exceeded: $churn lines" >&2; exit 1
fi
if (( files > 1 )); then
echo "one file per move" >&2; exit 1
fi
}
"$@"
The loop becomes mechanical:
git checkout -b refactor/validate-first
./tools/refactor.sh verify
./tools/refactor.sh budget
git add legacy.py
git commit -m "extract row validation into a pure function"
Each commit message names exactly one move. "Extract validation." "Rename order
to parsed
." "Split the import loop." One verb per commit. Never "cleanup." Never "refactor import_orders."
Every move starts as a narrow prompt. One prompt asks for exactly one change: "Extract the validation block into a pure function. Change nothing else." This loop runs dozens of prompts per refactor. Free model access keeps that prompt cost at zero. MonkeyCode's free model access covers these small proposals. The model proposes; the snapshot disposes.
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
If verify turns red, the patch is wrong for this codebase. Model confidence does not matter. The golden file does. If you want the experiment fully isolated, MonkeyCode's free server option offers a clean environment for the verify step. Apply the candidate patch there, run the check, inspect the diff. Promote the accepted move to your branch only after it passes. Keep the prompt narrow. One move per prompt. If the model returns a multi-move patch, split it or reject it. The budget gate will reject it anyway.
Here is a failure mode this loop catches reliably. The legacy code treats qty=None
as 1. The model's extraction "fixed" it to raise ValueError
. Return-value tests stayed green. The golden file caught the new exception immediately. The bug was in the patch, not in the code. This is the whole point of the loop.
| Move | Snapshot sensitivity | Risk |
|---|---|---|
| Extract a pure block | low | low |
| Rename a local variable | none | negligible |
| Reorder two DB writes | high | medium |
| Change an exception path | high | high |
| Inline a cached value | medium | medium |
Pure extraction and renames are cheap. Reordering side effects is not. Exception paths are the most dangerous. The snapshot records all of them.
Golden files freeze bugs too. If the refactor must fix a bug, update the golden file deliberately. Write the reason in the commit message. Do not let the snapshot bless the bug and the fix at the same time.
Flaky behavior breaks the loop. Timestamps, random IDs, and network calls poison golden files. Seed randomness and stub time before recording. If the boundary is genuinely nondeterministic, this loop is the wrong tool.
The diff budget is a proxy, not a proof. An 80-line change can still break behavior. A 400-line move can be perfectly safe. The budget enforces discipline, not correctness. Run the verify command in CI. Add it to the pre-commit hook. The snapshot becomes a regression net for the whole branch.
Who should not use this? Greenfield code. Tiny functions with two callers. Urgent behavior changes. In those cases, ordinary tests beat golden files. Snapshot-first is for code bases where fear is the dominant emotion.
git checkout -b refactor/one-move
./tools/refactor.sh verify # red before you start? fix the harness first
./tools/refactor.sh verify # green? the move is safe
./tools/refactor.sh budget # within budget? commit it
git commit -m "one move"
Pick the function you avoid opening. Run this loop once. The golden files will argue with you. Let them win.