Green tests after an agent patch are a weak signal when the suite still reads the clock, iterates a hash map, or compares freshly minted IDs. Those three seams create flakes, false passes, and “fixes” that stub the world instead of the defect. Pin entropy first. Then attach properties to a canonical document. Freeze only what remains, and give that freeze an expiry.
This is a merge rule, not a style preference. An agent optimizes for a green job. A green job that still observes wall time is not a proof.
The failure mode is quiet, not loud
Agent patches rarely delete the module under test. They insert time.sleep, patch datetime.now at the wrong layer, or loosen assert payload == expected into assert payload is not None. The suite gets larger. The constraint set does not.
Nondeterminism is the cheap way to look finished. Hash iteration order differs across process starts. UUID4 values never repeat. RFC 3339 timestamps include subsecond noise that no product rule required. If the test observes those values, the next agent will “repair” the observation instead of the behavior.
The method below is a worked example. It is labeled as a proposal where no run was recorded for this article. Do not treat the snippets as production metrics.
1. Inventory the entropy the patch can see
Work from the diff, not from memory. List every call that can change across identical inputs.
- Clock reads:
time.time,time.monotonic,datetime.now,datetime.utcnow,perf_counter. - Generators:
uuid.uuid4,os.urandom,random.*,secrets.*. - Iteration that depends on hash seed: bare
dict/setequality against a literal,json.dumpswithoutsort_keys. - Environment:
TZ,LANG,LC_*, working directory, process id, hostname. - Network and clock cousins: DNS order, HTTP date headers, retry jitter.
A small scanner is enough to start. It will over-flag. That is cheaper than a silent flake.
# proposal: entropy_scan.py — unexecuted in this article
import ast, pathlib, sys
HOOKS = {
"time": {"time", "monotonic", "sleep"},
"datetime": {"now", "utcnow", "today"},
"uuid": {"uuid4", "uuid1"},
"random": {"random", "randint", "choice", "shuffle"},
"os": {"urandom"},
"json": {"dumps"}, # inspect sort_keys in a later pass
}
def scan(path: pathlib.Path) -> list[str]:
tree = ast.parse(path.read_text(encoding="utf-8"))
hits = []
for node in ast.walk(tree):
if isinstance(node, ast.Attribute) and isinstance(node.value, ast.Name):
mods = HOOKS.get(node.value.id, set())
if node.attr in mods:
hits.append(f"{path}:{node.lineno}:{node.value.id}.{node.attr}")
return hits
if __name__ == "__main__":
root = pathlib.Path(sys.argv[1])
for p in root.rglob("*.py"):
for hit in scan(p):
print(hit)
Run it on the patch files and on the tests they touch. A hook in production code is a seam. A hook in a test is often the agent papering over that seam.
2. Replace observations with a clock and an ID factory
Do not freeze the entire datetime module in pytest if the product code has no clock argument. That hides the dependency. Inject it.
# proposal: seams.py
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Callable
import itertools
@dataclass(frozen=True)
class Seams:
now: Callable[[], datetime]
new_id: Callable[[], str]
def live_seams() -> Seams:
return Seams(
now=lambda: datetime.now(timezone.utc),
new_id=lambda: __import__("uuid").uuid4().hex,
)
def pinned_seams(start: datetime) -> Seams:
ticks = itertools.count()
ids = itertools.count(1)
def now() -> datetime:
# 1s steps keep order without claiming subsecond product rules
return start.replace(microsecond=0).replace(second=start.second) # noqa: placeholder
# clearer version:
counter = itertools.count()
def now2() -> datetime:
return start.replace(microsecond=0) # tests pass an explicit start
def new_id() -> str:
return f"id-{next(ids):04d}"
return Seams(now=now2, new_id=new_id)
The live seams belong in main. The pinned seams belong in tests and in any replay job. If the agent cannot construct the module without datetime.now, the patch is incomplete. Reject it for that reason alone.
time.sleep in a unit test is a failed pin. Replace it with an explicit scheduler or a fake clock advance. Sleep converts a race into a slower race.
3. Canonicalize before any equality check
Equality on raw dicts is an entropy leak. Canonicalize first. Then compare.
# proposal: canon.py
from decimal import Decimal
from typing import Any
import json
import re
ID_RE = re.compile(r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", re.I)
ISO_RE = re.compile(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}")
def canon(value: Any) -> Any:
if isinstance(value, dict):
return {str(k): canon(value[k]) for k in sorted(value, key=str)}
if isinstance(value, (list, tuple)):
return [canon(v) for v in value]
if isinstance(value, set):
return [canon(v) for v in sorted(value, key=lambda x: json.dumps(canon(x), sort_keys=True))]
if isinstance(value, Decimal):
return format(value, "f")
if isinstance(value, str) and ID_RE.match(value):
return "<id>"
if isinstance(value, str) and ISO_RE.match(value):
return "<ts>"
return value
def dumps(value: Any) -> str:
return json.dumps(canon(value), sort_keys=True, separators=(",", ":"), ensure_ascii=False)
Two warnings. If key order is part of the public contract, do not sort those keys; pin them as a list of pairs instead. If a timestamp is the product (billing cutover, token expiry), do not replace it with <ts>; pin a fake clock and assert the exact computed instant.
Canonicalization is not a license to ignore fields. Dropping a field because it flakes is the same class of error as assert result is not None.
4. Attach properties to the canonical document
Example tests lock one trace. Property checks lock a family of traces. The agent that wrote the patch must not be the only author of those properties.
# proposal: test_properties.py — unexecuted in this article
from datetime import datetime, timezone
from hypothesis import given, settings, strategies as st
from canon import dumps
from seams import pinned_seams
# from app import apply_patch # the module under test
@settings(max_examples=80, deadline=200) # wall clock per example, milliseconds
@given(
amount=st.integers(min_value=0, max_value=10_000),
currency=st.sampled_from(["USD", "EUR", "JPY"]),
)
def test_ledger_shape_is_stable(amount, currency):
seams = pinned_seams(datetime(2026, 9, 8, tzinfo=timezone.utc))
# out = apply_patch({"amount": amount, "currency": currency}, seams=seams)
out = {"amount": amount, "currency": currency, "id": seams.new_id(), "ts": seams.now().isoformat()}
doc = dumps(out)
assert '"amount":' in doc
assert '"<id>"' in doc or '"id-0001"' in doc
assert doc == dumps(out) # idempotent canonicalize
Keep the deadline. Unbounded property tests are how a free or shared runner becomes a hang. A hang is not a flake. It is an unbounded generator.
Properties that only restate the example (amount >= 0 when the strategy already sampled min_value=0) add noise. Require at least one independent invariant: round-trip, idempotence, monotonic totals, or forbidden fields.
5. Store fixtures in canonical form only
Raw recorded payloads bake in IDs and timestamps. The next agent will rewrite the fixture to match a new ID. That is a tautology.
Store the canonical document. Reload it through the same canon() path used in tests. If a field cannot be canonicalized without lying, it does not belong in the fixture; it belongs in a pinned seam.
# fixtures/ledger_min.json (canonical, sorted keys)
{"amount":100,"currency":"USD","id":"id-0001","ts":"<ts>"}
Blind, hidden fixtures are a different control. This article does not use them. The rule here is narrower: nothing that changes with process start may sit in the expected file.
6. Freeze remaining flakes with an expiry, not a skip
After pins and canonicalization, a remainder can still be environmental. Freeze that remainder in a register, not with @pytest.mark.skip.
| Field | Rule |
|---|---|
| test id | stable node id, not a file line |
| last green rev | git SHA where it passed three times |
| suspected seam | clock / map / id / network / unknown |
| freeze owner | a person, not "agent" |
| expires | calendar date, default 7 days |
| allowed action | quarantine job only; cannot gate merge after expiry |
A freeze that cannot expire is a deleted assertion. A freeze whose suspected seam is still unknown is not ready; keep investigating. Agents may propose a freeze row. They may not approve it.
# proposal: conftest.py fragment
import datetime as dt
import json
import pathlib
import pytest
FREEZE = json.loads(pathlib.Path("flake_freeze.json").read_text())
def pytest_collection_modifyitems(items):
today = dt.date.fromisoformat("2026-09-08") # CI should inject the real date
for item in items:
row = FREEZE.get(item.nodeid)
if not row:
continue
exp = dt.date.fromisoformat(row["expires"])
if today > exp:
item.add_marker(pytest.mark.xfail(strict=True, reason="freeze expired"))
else:
item.add_marker(pytest.mark.flaky_freeze)
Expired means fail-closed. The suite must go red until a human re-pins the seam or deletes the test with a reason.
Where a second session and a budgeted server fit
The patch author is a biased generator. Properties, mutant lists, and entropy inventories need a separate session that never sees the agent’s “just make it pass” prompt.
If you use MonkeyCode for that second session, the relevant parts are free model access and the free server option: draft the entropy list and the invariants from the diff in one place, then run the harness under a hard timeout in the other. Disclosure: This article was prepared as part of MonkeyCode's product outreach. Do not send the patch-authoring transcript into the property session. Do not let the same model both write apply_patch and declare the invariants complete.
A budgeted command keeps the free server from becoming an infinite loop:
# proposal: CI step — unexecuted here; tune the timeout to your runner
timeout 120s python -m pytest -q test_properties.py test_canon.py --maxfail=1
If the job hits timeout, treat it as a failed pin, not as infrastructure noise. Widen examples only after the deadline per case is stable.
Decision table
| Observation in the test | First action | Merge allowed? |
|---|---|---|
datetime.now in unit test |
inject Seams.now
|
no, until gone |
uuid4 compared to a literal |
new_id factory + canon |
no |
json.dumps without sort_keys
|
dumps(canon(...)) |
no |
time.sleep to “wait for” a task |
fake clock or explicit join | no |
assertion is is not None on a structured payload |
restore field invariants | no |
| remaining flake with known seam and expiry | freeze register | yes, until expiry |
freeze with unknown seam |
keep red | no |
Limits
Pinned entropy does not catch deterministic logic bugs. A wrong tax rate that is stable will pass every canonical fixture. Independent properties and a mutation battery the agent did not write still matter; they are out of scope for this checklist.
Canonicalization can destroy a real contract. Some APIs guarantee insertion order. Some audit logs require the original UUID. Classify those as product fields before you replace them with <id>.
A free server run is not a load test. It will not show GC jitter, NUMA effects, or cross-region clock skew. Do not advertise a 120s pytest job as production evidence.
Hypothesis-style properties follow the generator. A narrow strategy produces a narrow proof. If the generator never emits a zero amount, you have not tested zero.
Who should skip this
Skip the method if the product is allowed to be observationally nondeterministic and that fact is the spec: games with true RNG, eventually consistent dashboards, or physical sensors without a fake. Skip it if the team will not expire freezes. Skip it if the goal is to avoid reading the diff. Pins without review just move the tautology into canon().
If the checklist already matches how you review agent diffs, draft the entropy inventory in a separate free-model session and use a free server only when local CI minutes are the constraint. The merge question stays the same: did the patch pin time, maps, and IDs, or did it teach the suite to look away?
Top comments (0)