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)
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)
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
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]}"
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)
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
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)