DEV Community

Finley Zhou
Finley Zhou

Posted on

Pin Time, Maps, and IDs Before an Agent Patch Can Pass

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.

  1. Clock reads: time.time, time.monotonic, datetime.now, datetime.utcnow, perf_counter.
  2. Generators: uuid.uuid4, os.urandom, random.*, secrets.*.
  3. Iteration that depends on hash seed: bare dict/set equality against a literal, json.dumps without sort_keys.
  4. Environment: TZ, LANG, LC_*, working directory, process id, hostname.
  5. 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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>"}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)