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:
- Only the owner may touch the fixture while the lease is held.
- Every lease expires, so a dead agent cannot hold a fixture forever.
- 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()
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}")
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.recordsbefore_writefinishes. - 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)