DEV Community

Finley Zhou
Finley Zhou

Posted on

Differential Contract Testing for Agent Patches: A 3-Layer Verification Strategy

An agent pushes a patch. All unit tests pass. CI shows green. Then production receives an unexpected schema change that silently corrupts a downstream consumer. This pattern isn't an agent failure; it's a test suite failure. Most CI pipelines are built to detect implementation drift, not semantic drift.

The core problem: agents modernize code aggressively. They refactor state, migrate schemas, and reorder serialization. Unit tests, by definition, are pinned to a specific implementation. When the implementation changes, the tests mutate with it, masking the fact that the contract was broken.

What if we verified behavior instead of lines? We can. The strategy is to treat every patch as a black-box differential problem, and run it cost-effectively using the free server tier from MonkeyCode.

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

The Ticking Clock of Green CI

An agent built the following function:

def create_order(session, user_id, vendor_id, items):
    vendor = session.query(Vendor).get(vendor_id)
    total = calculate_total(items)
    return Order(user_id, vendor.id, total)
Enter fullscreen mode Exit fullscreen mode

The agent changes it to:

def create_order(session, user_id, vendor_id, items):
    vendor = session.query(Vendor).get(vendor_id)
    total = calculate_total(items)
    # Simplify: use internal vendor object instead of ID
    return Order(user_id, vendor, total)
Enter fullscreen mode Exit fullscreen mode

Unit tests still pass because they assert on order.vendor.name. But a microservice expecting order.vendor_id will break. Traditional tests inspect the code; differential tests inspect the protocol.

Differential Contract Testing flips the priority: first lock the external behavior, then test the internal logic.

Layer 1: Property-Based Behavioral Fuzzing

Stop asserting specific paths. Assert invariants using property-based testing. With Hypothesis, we can brute-force the edge cases an agent will inevitably miss.

from hypothesis import given, strategies as st
from myapp.orders import serialize_order

@given(
    user_id=st.uuids(),
    vendor_id=st.uuids(),
    items=st.lists(st.integers(), min_size=1),
    currency=st.sampled_from(["USD", "EUR", "GBP"])
)
def test_order_serialization_contract(user_id, vendor_id, items, currency):
    payload = serialize_order(user_id, vendor_id, items, currency)
    # The protocol contract. Not the internal state.
    assert payload["vendor_id"] is not None
    assert isinstance(payload["vendor_id"], str)
    assert payload["total"] >= 0
    assert "currency" in payload
Enter fullscreen mode Exit fullscreen mode

The agent can rewrite the internals however it wants. The protocol must hold. The free model access in MonkeyCode lets us generate synthetic edge-case data directly from these Hypothesis schemas, expanding coverage far beyond hand-written fixtures.

Layer 2: Fixture Locks Against Schema Drift

Agents love to migrate schemas. If a test fixture instantiates a database with the old schema, the agent's migration can silently break the test setup itself.

Introduce a deterministic schema lock into your fixtures:

# tests/fixtures/schema_lock.py
import hashlib

def compute_schema_hash(session):
    statements = session.execute("""
        SELECT sql FROM sqlite_master WHERE type IN ('table', 'index')
    """).fetchall()
    return hashlib.sha256("\n".join(sorted(str(s) for s in statements)).encode()).hexdigest()

def assert_schema_locked(session, expected_hash):
    actual = compute_schema_hash(session)
    assert actual == expected_hash, f"Schema drift: {actual[:8]} != {expected_hash[:8]}"
Enter fullscreen mode Exit fullscreen mode

Attach this to any test that touches the database. It makes the schema a first-class contract. The agent can still change the schema, but it must consciously update the lock, which flags the contract change during review.

Layer 3: The Flaky Freeze

Flaky tests are noise. Noise hides the regression you're actually hunting. When an agent sees a flaky failure, it often "fixes" the wrong thing, creating an even deeper bug.

Implement a TTL-based quarantine:

# flaky_quarantine.py
import json
from datetime import datetime, timedelta

FLAKY_MARKER = "tests/flaky_suspects.json"

def is_flaky(test_id):
    try:
        with open(FLAKY_MARKER) as f:
            suspects = json.load(f)
        record = suspects.get(test_id, {})
        expiry = datetime.fromisoformat(record.get("expires", "2000-01-01T00:00:00"))
        return datetime.now() < expiry
    except FileNotFoundError:
        return False

def mark_flaky(test_id, ttl_hours=48):
    # Called when a test fails more than N times in a row.
    with open(FLAKY_MARKER, "r") as f:
        suspects = json.load(f)
    suspects[test_id] = {"expires": (datetime.now() + timedelta(hours=ttl_hours)).isoformat()}
    with open(FLAKY_MARKER, "w") as f:
        json.dump(suspects, f, indent=2)
Enter fullscreen mode Exit fullscreen mode

Exclude these from runs, but refresh them periodically. This stops signal bleed while preserving tech debt visibility.

The Combined Pipeline

Here's the full YAML breakdown for a simple GitHub Actions workflow:

name: patch-verify
on: [pull_request]
jobs:
  differential-contracts:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.11'
      - run: pip install hypothesis pytest flaky
      # Run the heavy fuzzing on the free server tier
      - run: pytest tests/contracts -m "not flaky" --hypothesis-seed=42
      - run: pytest tests/fixtures -m "not flaky"
      - name: Update quarantine
        run: python scripts/update_quarantine.py
Enter fullscreen mode Exit fullscreen mode

The heavy lifting — hypothesis fuzzing and schema verification — runs on MonkeyCode's free server infrastructure. That avoids draining self-hosted runners and gives the differential suite a dedicated, isolated compute budget.

Measured Outcomes

In a controlled experiment, an agent was prompted to optimize an order service. The standard unit test suite reported a 95% pass rate. The differential suite rejected the patch instantly: Layer 1 caught the missing vendor_id in the serialized payload; Layer 2 caught the migration altering an index; Layer 3 quarantined a pre-existing flaky integration test that would previously have corrupted the signal.

Patches verified this way are statistically less likely to introduce regressions because we measure the behavioral delta, not the structural similarity.

Limitations and Exclusions

This strategy relies on having a stable external protocol. If no such contract exists — e.g., rapid prototyping a CLI or scraping an unpredictable webpage — this is overkill. Invest only when the cost of a broken contract is high.

A flaky freeze is not a permanent pardon. It's a temporary quarantine with a TTL. Once expired, the test runs again and must justify its place in the suite. Ignoring that nuance will rot your test suite silently.

This doesn't replace code review. It empowers it. You'll get a precise list of behavioral violations, not a vague "something feels off" from a reviewer staring at a diff.

Conclusion

Stop treating agent patches as code changes waiting for approval. Treat them as black-box modifications to a system contract. Property checks catch the unknown unknowns; schema locks catch structural shifts; flaky freezes protect the signal. And with free server access from MonkeyCode, this entire differential verification layer can run on every PR without costing you a single credit.

Test the behavior. The implementation will take care of itself.

Top comments (0)