DEV Community

Finley Zhou
Finley Zhou

Posted on

When a Flake Freeze Expires: Three Ledger Checks Before the Test Comes Back

A quarantine list only grows. Someone adds a test on Monday, someone else adds two more on Thursday, and by the end of the quarter nobody remembers which entries are still justified.

The decision that actually matters is not "is this test flaky." You already settled that argument when you froze it. The hard decision is the exit interview: what evidence do you need before that test runs in the merge gate again?

My answer is deliberately boring. Three small YAML ledgers and one replay log, validated by a script that fails the build when they disagree with reality. No dashboard. No weekly triage meeting. If a freeze is stale or a fixture has drifted, CI says so.

One caveat before the code: the rows below are schema illustrations, not measurements from a real repository. Treat them as fixtures for the checker, not as results.

1. Split the quarantine file into three ledgers

A single flaky.txt is unqueryable. It cannot tell you who owns a freeze, what input the test depended on, or which behavioral property you were hoping to preserve. Split it.

ledgers/freezes.yaml — one row per quarantined test. It carries an owner, a frozen date, a hard expiry, and the property ID that the test was supposed to protect.

# ledgers/freezes.yaml  (illustrative rows)
- test: tests/test_orders.py::test_partial_refund
  owner: @finley
  frozen_on: 2026-10-20
  expires: 2026-11-19
  reason: "assertion depends on wall-clock ordering"
  property_id: PROP-014
  replay_env: pinned-py311
Enter fullscreen mode Exit fullscreen mode

ledgers/fixtures.yaml — content hash for every fixture file a frozen test or a property touches. This is the part teams skip, and it is the part that catches silent drift.

# ledgers/fixtures.yaml
- id: FX-refunds
  path: tests/fixtures/refund_events.json
  sha256: "0b1c...e9"
- id: FX-clock
  path: tests/fixtures/clock_sequence.json
  sha256: "7d4a...12"
Enter fullscreen mode Exit fullscreen mode

ledgers/properties.yaml — the properties you actually care about, bound to both tests and fixtures. A property with no fixture binding is a wish.

# ledgers/properties.yaml
- id: PROP-014
  statement: "sum(refunds) never exceeds original charge"
  fixtures: [FX-refunds, FX-clock]
  tests: [tests/test_orders.py::test_partial_refund]
Enter fullscreen mode Exit fullscreen mode

ledgers/replays.jsonl — append-only. One line per replay run, with the seed list and the pinned environment label.

{"test":"tests/test_orders.py::test_partial_refund","run_at":"2026-11-04T02:10:00Z","env":"pinned-py311","seeds":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20],"passed":20,"total":20}
Enter fullscreen mode Exit fullscreen mode

2. Write the checker before you write the policy

Policies in prose get ignored. A checker that exits non-zero gets fixed. This script is the whole enforcement layer, and it runs in under a second on a ledger of a few hundred rows.

#!/usr/bin/env python3
# tools/check_ledger.py
"""Fail the build when a flake freeze is stale, unowned, or unevidenced."""
import hashlib, json, sys
from datetime import date
from pathlib import Path

import yaml

ROOT = Path(__file__).resolve().parent.parent
L = ROOT / "ledgers"
MAX_WINDOW_DAYS = 30
MAX_REPLAY_AGE_DAYS = 14
MIN_SEEDS = 20


def yaml_rows(name):
    return yaml.safe_load((L / name).read_text()) or []


def sha256(path: Path) -> str:
    return hashlib.sha256(path.read_bytes()).hexdigest()


def check_freezes(freezes, today):
    errs = []
    for f in freezes:
        t = f["test"]
        if not f.get("owner"):
            errs.append(f"{t}: freeze has no owner")
        frozen = date.fromisoformat(str(f["frozen_on"]))
        expires = date.fromisoformat(str(f["expires"]))
        if expires < today:
            errs.append(f"{t}: freeze expired on {expires}, unfreeze or re-justify")
        if (expires - frozen).days > MAX_WINDOW_DAYS:
            errs.append(f"{t}: freeze window exceeds {MAX_WINDOW_DAYS} days")
        if not f.get("property_id"):
            errs.append(f"{t}: no property_id, nothing protects the behavior")
    return errs


def check_fixtures(fixtures):
    errs = []
    for fx in fixtures:
        p = ROOT / fx["path"]
        if not p.exists():
            errs.append(f"{fx['id']}: fixture file missing at {p}")
            continue
        if sha256(p) != fx["sha256"]:
            errs.append(f"{fx['id']}: content hash drifted, lock is stale")
    return errs


def check_properties(props, freezes, fixtures):
    errs = []
    frozen = {f["test"] for f in freezes}
    known_fx = {fx["id"] for fx in fixtures}
    for pr in props:
        pid = pr["id"]
        if not pr.get("tests"):
            errs.append(f"{pid}: property has no test mapping")
        for t in pr.get("tests") or []:
            if t in frozen:
                errs.append(f"{pid}: only protected by frozen test {t}")
        for fxid in pr.get("fixtures") or []:
            if fxid not in known_fx:
                errs.append(f"{pid}: references unknown fixture {fxid}")
        if not pr.get("fixtures"):
            errs.append(f"{pid}: property has no fixture binding")
    return errs


def check_replays(freezes, replays, today):
    errs = []
    for f in freezes:
        t = f["test"]
        rows = [r for r in replays if r.get("test") == t]
        if not rows:
            errs.append(f"{t}: no replay record, freeze is unevidenced")
            continue
        last = max(rows, key=lambda r: r["run_at"])
        age = (today - date.fromisoformat(last["run_at"][:10])).days
        if age > MAX_REPLAY_AGE_DAYS:
            errs.append(f"{t}: last replay is {age} days old")
        if len(last.get("seeds") or []) < MIN_SEEDS:
            errs.append(f"{t}: replay used fewer than {MIN_SEEDS} seeds")
        if last.get("env") != f.get("replay_env"):
            errs.append(f"{t}: replay env mismatch ({last.get('env')} != {f.get('replay_env')})")
    return errs


def main() -> int:
    today = date.today()
    freezes = yaml_rows("freezes.yaml")
    fixtures = yaml_rows("fixtures.yaml")
    props = yaml_rows("properties.yaml")
    replays = [json.loads(l) for l in (L / "replays.jsonl").read_text().splitlines() if l.strip()]

    errs = (
        check_freezes(freezes, today)
        + check_fixtures(fixtures)
        + check_properties(props, freezes, fixtures)
        + check_replays(freezes, replays, today)
    )
    for e in errs:
        print(f"LEDGER: {e}")
    print(f"{len(freezes)} freezes, {len(fixtures)} fixtures, {len(props)} properties, {len(errs)} violations")
    return 1 if errs else 0


if __name__ == "__main__":
    sys.exit(main())
Enter fullscreen mode Exit fullscreen mode

Wire it into two places: a pre-commit hook so authors see violations before pushing, and the merge gate so nobody can route around it.

python tools/check_ledger.py || exit 1
Enter fullscreen mode Exit fullscreen mode

3. Replay on a pinned host, not on a laptop

A replay record is worthless if the environment floats between runs. Fix the interpreter, the timezone, and the random seed set, then append the result.

pytest "tests/test_orders.py::test_partial_refund" \
  -p no:randomly \
  --tb=short \
  $(python -c 'print(" ".join(f"--seed={i}" for i in range(1,21)))')
Enter fullscreen mode Exit fullscreen mode

Most projects will not accept that invocation as-is; the seed flag comes from a plugin. Adapt the flag, keep the twenty-seed floor. The floor is the point: one green run proves nothing about a test that failed one time in forty.

This is where I use the free server option in MonkeyCode: a small always-on box with a fixed image, so replay environment labels are stable across weeks and I am not burning a laptop for a nightly job. The free model access in the same workspace is useful for one narrow task — drafting candidate properties from a frozen test's assertions. I hand-edit every draft, because a model that proposes the property will also happily propose one the code cannot violate.

Disclosure: This article was prepared as part of MonkeyCode's product outreach.

4. Turn the ledger state into a decision

The checker reports violations. It does not decide what to do. That mapping stays human and explicit, which is why it fits in a table.

Ledger state Action Why
Freeze expired, replay shows 20/20 green on the pinned env Unfreeze, delete the row Exit evidence exists
Freeze expired, replay flaky or absent Delete the test, keep the property A flaky test cannot protect a property
Fixture hash drift, test still frozen Re-lock after reviewing the diff The old evidence is void
Property bound only to frozen tests Write a new test before touching the freeze Otherwise coverage silently drops
Freeze window over 30 days Re-justify or delete Long freezes are deletions with paperwork
Replay env label changed Re-run before unfreezing Mixed environments are not comparable

Row two is the unpopular one. Deleting a test feels like losing coverage, but a test that fails one run in forty has already stopped providing a signal — it provides noise plus a habit of clicking retry. The property survives the deletion. That is the whole reason the property ledger exists.

5. What this scheme cannot see

  • Semantic drift. A hash lock catches bytes, not meaning. If someone rewrites the fixture generator to produce equivalent-but-different data, the hash changes and you re-lock it, and you have learned nothing about whether the new data still exercises the boundary.
  • Ordering and shared state. A twenty-seed replay on one host under-samples cross-test pollution. If your failures cluster when the full suite runs, replay the suite, not the test.
  • Network and clock dependencies. The checker will happily unfreeze a test that is green on the pinned box and red on the CI runner. Vendoring the dependency is a prerequisite, not an optimization.
  • True model behavior. Property drafting with a hosted model is a suggestion engine. It cannot know which invariants your domain treats as sacred.

6. Who should not adopt this

Skip the ledger if you quarantine fewer than a handful of tests a quarter — the bookkeeping will cost more than the flakes. Skip it if you cannot pin fixture data, for example because fixtures are generated from live PII-bearing records; hashing those files creates a compliance question you do not want. And skip it if your flakes come from a shared integration environment you do not control, because a replay record from your own pinned host tells you nothing about the environment that actually fails.

Everyone else: the script is about 90 lines, the ledgers are three files, and the first run will probably surface a freeze from eight months ago that nobody owns. Delete that one first.

Top comments (0)