DEV Community

Finley Zhou
Finley Zhou

Posted on

Lease Your Fixtures: Deterministic Agent-Patch Gates Without a Global Lock

A green checkmark means nothing if the seed file the test read was overwritten mid-run by a parallel patch. I kept seeing that exact situation on agent-patch gates: two patches touch the same fixture, both suites finish, and neither result is trustworthy.

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

The fix is not a global lock. The fix is a lease with a base snapshot and a TTL — small, boring, and reproducible.

The failure I kept seeing

Patch A starts a targeted pytest suite against seeds/bookings.json. Patch B starts the same suite a minute later. B rewrites the seed file while A is still reading it.

A's tests might read one version, B's tests read another. Both gates print green. The race window is small, so the failure looks flaky. Nobody suspects the fixture.

A global lock fixes the race but makes unrelated tests wait on fixtures they never touch. On the first run I tried that, CI time tripled because a shared seed file blocked every suite in the repository.

Leases solve the real problem: ownership with a time limit and proof that the data did not change under you.

The lease model

Three rules keep the gate deterministic:

  1. Only the owner may touch the fixture while the lease is held.
  2. Every lease expires, so a dead agent cannot hold a fixture forever.
  3. Release compares the current file hash with the base hash taken at acquire time.

Rule three is the important one. If another process modifies the file without respecting the lease, the release fails loudly instead of publishing a fake green.

Minimal artifact: fixture_lease.py

This is a cross-process JSON-backed version. It is deliberately small so the idea is visible. Python 3.10+.

# fixture_lease.py
from __future__ import annotations

import contextlib
import hashlib
import json
from pathlib import Path
import time


class LeaseConflict(RuntimeError):
    pass


class FixtureLease:
    def __init__(self, path: Path):
        self.path = path
        self.records: dict = self._read()

    def _read(self) -> dict:
        if not self.path.exists():
            return {}
        return json.loads(self.path.read_text())

    def _write(self) -> None:
        self.path.write_text(json.dumps(self.records, sort_keys=True, indent=2))

    def _hash(self, fixture: Path) -> str:
        if not fixture.exists():
            raise LeaseConflict(f"{fixture} is missing")
        return hashlib.sha256(fixture.read_bytes()).hexdigest()

    @contextlib.contextmanager
    def lease(self, fixture: str, owner: str, ttl_seconds: int = 900):
        now = time.time()
        current = self.records.get(fixture)
        if current and current["owner"] != owner and current["expires"] > now:
            raise LeaseConflict(
                f"{fixture} is leased by {current['owner']} "
                f"until {current['expires']}"
            )
        self.records[fixture] = {
            "owner": owner,
            "expires": now + ttl_seconds,
            "base": self._hash(Path(fixture)),
        }
        self._write()
        try:
            yield
        finally:
            record = self.records.get(fixture)
            if record is None or record["owner"] != owner:
                raise LeaseConflict(f"{fixture} was lost by {owner}")
            if self._hash(Path(fixture)) != record["base"]:
                raise LeaseConflict(f"{fixture} changed while leased by {owner}")
            self.records.pop(fixture)
            self._write()
Enter fullscreen mode Exit fullscreen mode

Usage in a patch gate looks like this:

import subprocess
from pathlib import Path

from fixture_lease import FixtureLease, LeaseConflict

lease_store = FixtureLease(Path("/tmp/gate/leases.json"))


def run_patch(patch_id: str, target: str) -> None:
    fixture = "seeds/bookings.json"
    try:
        with lease_store.lease(fixture, owner=patch_id, ttl_seconds=900):
            subprocess.run(["pytest", "-q", target], check=True)
    except LeaseConflict as exc:
        print(f"GATE REJECTED: {exc}")
Enter fullscreen mode Exit fullscreen mode

If Patch B tries to lease bookings.json while Patch A still holds it, B fails immediately with fixture is leased by patch-A until .... No retry. Retrying would hide the interference.

When Patch A finishes, the finally block hashes the file again. If B changed it without waiting for the lease, A fails at release time. That turns silent messy data into a visible gate failure.

Property checks fit inside the same context. The lease must wrap the property generation, not just the final assertion, because generation can mutate shared state too.

Flaky freezes as fixed-term leases

A separate problem remains: a single non-deterministic test can still pass by luck. I treat a flaky freeze as a lease on the test name.

The freeze record says: this test is quarantined for 24 hours. Expiry does not automatically re-enable it. The test only unlocks when the next patch's diff touches either the test itself or the fixture the freezed test reads.

That rule matters because a flaky test is often flaky due to a fixture interaction, not its own logic. Re-enabling it without a fixture change just imports the same instability.

When not to use this approach

Leases are not free. Do not add them to every fixture in the repository.

  • If your test suites use disjoint seed files, a lease registry is overhead. Measure first.
  • If you run multithreaded pytest in one process, this JSON-backed store is not safe. Threads inside the same process can interleave on self.records before _write finishes.
  • If you generate fixtures at runtime, hashing every generated file becomes noise. Lease a directory prefix instead of a single file path.
  • If you need distributed coordination across machines, do not build it with a JSON file. Use a small database table with optimistic locking.

The JSON version is for small agent-patch gates that run on one server. That is the scenario I test it in.

I run the same gate pattern on MonkeyCode's free model endpoint and free server tier because both fit the cost model: a gate that runs continuously cannot charge per minute. I have not measured quotas, latency, or uptime, so verify current availability yourself. The lease code itself has no vendor dependency — it works on any machine that can run Python and pytest.

A deterministic gate is a quiet gate

Once the lease is in place, the gate stops producing those once-in-a-while failures that consume a whole afternoon. A failure now means one of two things: a real bug in the patch, or a fixture conflict that needs a decision.

Both are actionable. Neither is a ghost.

Top comments (0)