DEV Community

Finley Zhou
Finley Zhou

Posted on

Replay a Temporary Test Ignore Only When Three Ledger Keys Still Match

A temporary test ignore stays replayable only when three ledger fields still describe the patch under review. Those fields are the property identifier, the fixture contract hash, and an unexpired window for the same runner class. One green log does not create the match. A rewritten formula under the old identifier does not create it either.

Agent diffs break this quietly. The test, the fixture, and a copied ignore note arrive together. The note still looks specific. The bytes it names are no longer the bytes in the tree. Treat that as an invalid record, not as proof the check is flaky.

What each key is allowed to prove

A property identifier names one invariant. order_total_equals_sum_of_lines is an identifier. A path is not. Change the invariant, and you allocate a new identifier. The old row stays on the old behavior.

A fixture contract hash binds the inputs that invariant may read. Include schema version, field names, and fixed seeds. Exclude timestamps, hostnames, and process IDs. Those identify a run, not a contract.

The expiry field limits how long the ignore can be cited. It must name the runner class it was recorded for. A missing expiry is not open-ended approval. The decision is a reject.

These keys do not show the patch is correct. They show that a temporary ignore still points at the same invariant, the same input contract, and the same runner class. That narrower claim is the only one this workflow makes.

Canonical hashing before the digest is trusted

Hash the parsed contract, not the raw file. Load JSON, dump it with sorted keys and tight separators, then take SHA-256 of the UTF-8 bytes. A trailing newline must not move the digest. Reordered keys must not move it either.

Reject contracts that use floats for values the property treats as exact. Store money as integers in minor units, or as strings. Two parses of 0.1 are not a stable contract.

Object key order is normalized by the dump. Array order is not. Array order is data. If a list is semantically a set, sort it in the producer and record that rule by bumping schema. The bump changes the digest on purpose.

The digest is an equality check against the contract file in this patch. It is not a review of whether the contract is adequate. A clean hash can still describe the wrong inputs.

Use the same json.dumps settings on every side. The proposal leaves ensure_ascii at its default. Do not flip that flag in a one-off command, or non-ASCII field names will disagree for a reason that is not a fixture edit.

Decision table

Read left to right. The first failed key wins. Do not average a near-miss into a partial allow.

Property ID Fixture hash Window Action
Equal to the cited ID Equal to this contract Unexpired, same runner class, failure class environment allow_cite. Do not extend the window in this patch.
Equal Different Any reject:fixture_hash. Review the contract edit as a behavior change.
Unequal Any Any reject:property_id. Leave the old row on the old identifier.
Equal Equal Missing, expired, or runner class differs reject:window. Do not copy the old timestamp forward.
Absent from the test module Any Any reject:unnamed. The search command covers this row.

An expiry equal to --now is already closed. Matching keys with a failure class other than environment still reject. The checker returns reject:not_environment for that case. allow_cite is not a merge approval. The named assertion still has to run.

Five steps

  1. Put the identifier in the test module. The ledger string must be searchable in that file. A pull-request comment is not an identifier.

  2. Write a contract file from inputs the assertion actually reads. Keep schema, field names, and the seed. Omit clocks and host identity.

  3. Classify the miss before the row is eligible. Use invariant when the named property fails on the hashed contract. Use environment for timeouts, connection resets, and missing optional services. Only environment may later return allow_cite.

  4. Compare the row at a timestamp you pass in. --now is an argument, not a clock read, and not a timestamp copied from a remote log.

  5. Keep the decision string with the diff. allow_cite means the existing row may stay referenced. Any reject:* value means the reference comes out. This checker does not mint a replacement row.

Proposed test and proposed checker

Both listings are unexecuted proposals. They are not a production run, and they are not measurements.

def test_order_total_equals_sum_of_lines(contract):
    """property_id: order_total_equals_sum_of_lines"""
    lines = contract["lines"]
    assert order_total(lines) == sum(row["qty"] * row["price"] for row in lines)
Enter fullscreen mode Exit fullscreen mode

The assertion is the invariant. The docstring is the identifier. If the formula changes, the identifier should change with it. Keeping the name while editing the formula is how an ignore survives a behavior change.

#!/usr/bin/env python3
"""Proposed checker for one ignore record. Unexecuted example."""

import argparse
import hashlib
import json
from datetime import datetime, timezone

def contract_hash(path: str) -> str:
    with open(path, "r", encoding="utf-8") as handle:
        payload = json.load(handle)
    canonical = json.dumps(payload, sort_keys=True, separators=(",", ":"))
    return hashlib.sha256(canonical.encode("utf-8")).hexdigest()

def parse_zulu(value: str) -> datetime:
    stamp = datetime.fromisoformat(value.replace("Z", "+00:00"))
    return stamp.astimezone(timezone.utc)

def decide(record: dict, digest: str, now: datetime) -> str:
    if not record.get("property_id"):
        return "reject:unnamed"
    if record.get("property_id") != record.get("cited_property_id"):
        return "reject:property_id"
    if record.get("fixture_hash") != digest:
        return "reject:fixture_hash"
    if record.get("runner_class") != record.get("cited_runner_class"):
        return "reject:window"
    if record.get("failure_class") != "environment":
        return "reject:not_environment"
    expires = record.get("expires_at")
    if not expires or parse_zulu(expires) <= now:
        return "reject:window"
    return "allow_cite"

def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--ledger", required=True)
    parser.add_argument("--contract", required=True)
    parser.add_argument("--now", required=True)
    args = parser.parse_args()
    with open(args.ledger, "r", encoding="utf-8") as handle:
        record = json.load(handle)
    print(decide(record, contract_hash(args.contract), parse_zulu(args.now)))

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

The ledger below illustrates field shape for an evaluation date of 2026-10-11. The expiry value is a format example only. It is not a recommended window, and it is not an observed flake interval.

{
  "property_id": "order_total_equals_sum_of_lines",
  "cited_property_id": "order_total_equals_sum_of_lines",
  "fixture_hash": "replace-with-digest",
  "runner_class": "local-fixture",
  "cited_runner_class": "local-fixture",
  "failure_class": "environment",
  "expires_at": "2026-10-18T00:00:00Z"
}
Enter fullscreen mode Exit fullscreen mode
{
  "schema": 1,
  "fields": ["qty", "price"],
  "seed": 17,
  "lines": [{"qty": 2, "price": 500}]
}
Enter fullscreen mode Exit fullscreen mode

The sample ledger fails closed until fixture_hash is replaced with the digest of that contract. That is intentional.

Commands

Print the digest with the same canonicalization, then replay the decision at a fixed time. After that, confirm the identifier is still in the test module.

python3 - <<'PY'
import hashlib, json
with open("fixture_contract.json", encoding="utf-8") as handle:
    payload = json.load(handle)
canonical = json.dumps(payload, sort_keys=True, separators=(",", ":"))
print(hashlib.sha256(canonical.encode("utf-8")).hexdigest())
PY
Enter fullscreen mode Exit fullscreen mode
python3 freeze_keys.py --ledger freeze_record.json --contract fixture_contract.json --now 2026-10-11T00:00:00Z
Enter fullscreen mode Exit fullscreen mode
rg -n "property_id: order_total_equals_sum_of_lines" tests/test_order_total.py
Enter fullscreen mode Exit fullscreen mode

The Python proposal checks the ledger against the contract file. The search checks the test module. Both must pass. If the digest mismatches, stop. Changing expires_at does not repair a hash. If the search fails, stop. The row points at a name the patch does not contain.

Redact before any draft

A draft is only as usable as the excerpt you paste. Strip hostnames, account identifiers, tokens, and customer payloads first. Keep the exception class, the property identifier, and the contract field names.

Do not paste fixture values when they are production records. The contract in the repo should already be synthetic. If it is not, stop this workflow. Replace the data, then hash the replacement. The new digest is a new contract.

The scratch note may store a drafted label. It may not store secrets that survived a weak redact. When redaction is uncertain, skip the draft and set failure_class by reading the assertion locally.

Where a draft and a server sample may enter

Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode's free model access fits step 3 as a drafting aid, not as a label the checker stores. Provide the identifier, the contract field names, and the redacted excerpt. Ask for one label, invariant or environment, plus one sentence of reason. Leave that text in the scratch note. decide never reads it.

MonkeyCode's free server option fits step 4 as one place to collect a sample. It is not a source of --now. If you use it, write the class string you actually ran, for example free-server, into cited_runner_class. A differing class string fails the window key. The log can sit beside a new, unapproved row. It cannot flip an existing row to allow_cite.

Neither availability claim states a quota, a hardware shape, a retention period, or a standing waiver of review. If either option is unreachable, leave the scratch note empty and run the local checker. The allow path does not call them.

When a redacted excerpt and a contract file are already on disk, free model access and the free server option are sufficient to draft the class label and to attach one labeled sample before anyone edits the expiry field.

Who should not use this

Skip the workflow when the suite cannot name a pure invariant. Full-page snapshot diffs, tests that pass by sleeping, and patches that replace the runner are out of scope. A hash would still compute. The action code would not mean what a reviewer expects.

Do not use allow_cite to retain a failing invariant. The environment class is a precondition. Do not source --now from an untrusted log line. Do not treat digest equality as proof the contract is the right contract.

This article reports no flake rate, no runtime, and no count of patched tests. Borrowing those figures from an older write-up would not make decide more correct.

What the patch should still contain

Ship the named assertion, the contract file, and the decision string for the timestamp you chose. Remove every ignore reference that returns reject:*. Drafts and server samples can remain in the discussion if a reviewer wants context. They stay off the allow path.

The gate is local and replayable. Three keys, one caller-supplied timestamp, and an environment class are the whole decision. Context that cannot change decide does not belong in the record.

Top comments (0)