Stochastic Property-Based Testing for Agent Swarm Behaviors Beyond Unit Tests

Why deterministic unit tests fail for emergent behavior, and how property-based testing catches regressions in autonomous agent swarms.

by
Stochastic Property-Based Testing for Agent Swarm Behaviors Beyond Unit Tests

Stochastic Property-Based Testing for Agent Swarm Behaviors Beyond Unit Tests

You've built an agent swarm. Each agent passes its unit tests with flying colors. The orchestrator routes messages correctly. The vector store returns top-k hits. But when you deploy to staging, agents start hoarding context windows, forgetting tasks, or entering deadlock loops that only show up after 1000 iterations. Your unit tests didn't catch it because the failure is emergent — it depends on the order of events, the timing of messages, and the stochastic nature of LLM outputs.

Deterministic unit tests are necessary but not sufficient for agent swarms. You need property-based testing (PBT) with stochastic state exploration. This article shows you how to implement it using Hypothesis, stateful testing, and a dash of chaos engineering — all on your own infrastructure.

Why Unit Tests Fail for Swarms

A unit test checks that a function returns the expected output for a given input. In an agent swarm, the "function" is the entire system, and the "input" is a sequence of events over time. The output is a trace of agent states, messages, and side effects. The space of possible traces is astronomical — even with 3 agents and 10 message types, the number of interleavings explodes combinatorially.

Example: you have an agent that can delegate tasks to other agents. A unit test verifies that when it receives a task, it emits a delegation message. But what if the agent receives a cancellation while the delegation is in flight? What if two agents try to delegate to each other simultaneously? These scenarios are not covered by isolated unit tests.

Emergent bugs often arise from:

  • Race conditions in message delivery
  • Resource exhaustion (context window, token budget, retry limits)
  • Feedback loops (agent A asks B, who asks A)
  • State corruption from concurrent mutations

Unit tests are too narrow to catch these. Property-based tests, on the other hand, generate random sequences of operations and check that invariants hold for all of them.

Property-Based Testing 101

Property-based testing (PBT) is a technique where you define properties that should always hold true for your system, and a library generates random inputs to try to falsify them. The canonical tool is QuickCheck (Haskell), but we'll use Hypothesis (Python) because it's the most practical for AI infrastructure.

A property looks like:

from hypothesis import given, strategies as st

@given(st.lists(st.integers()))
def test_sort_invariant(lst):
    sorted_lst = sorted(lst)
    assert len(sorted_lst) == len(lst)
    assert all(sorted_lst[i] <= sorted_lst[i+1] for i in range(len(lst)-1))

This test generates random lists and checks that sorted() preserves length and ordering. If it finds a counterexample, it shrinks it to the minimal failing case.

For agent swarms, we need stateful PBT. Instead of generating isolated inputs, we generate sequences of actions that mutate the system state. Hypothesis provides RuleBasedStateMachine for this.

Stateful Property-Based Testing for Swarms

Let's model a simple agent swarm. Each agent has a message queue, a context window (limited tokens), and a set of tasks. Agents can receive messages, process them, and send messages to other agents. We want to test that no agent's context window ever exceeds its limit, and that every task eventually completes or is explicitly cancelled.

First, define the state machine:

from hypothesis.stateful import RuleBasedStateMachine, rule, precondition, invariant
from hypothesis import strategies as st

class SwarmStateMachine(RuleBasedStateMachine):
    def __init__(self):
        super().__init__()
        self.agents = {
            "A": Agent(agent_id="A", context_limit=4096),
            "B": Agent(agent_id="B", context_limit=4096),
            "C": Agent(agent_id="C", context_limit=4096),
        }
        self.message_log = []
        self.task_store = TaskStore()

Define actions as rules:

    @rule(
        sender=st.sampled_from(["A", "B", "C"]),
        receiver=st.sampled_from(["A", "B", "C"]),
        payload=st.text(min_size=1, max_size=200),
    )
    def send_message(self, sender, receiver, payload):
        # Precondition: sender and receiver are distinct? Not necessarily, self-messages allowed
        msg = Message(sender=sender, receiver=receiver, payload=payload)
        self.agents[receiver].inbox.put(msg)
        self.message_log.append(msg)

    @rule(
        agent_id=st.sampled_from(["A", "B", "C"]),
    )
    def process_message(self, agent_id):
        agent = self.agents[agent_id]
        if not agent.inbox.empty():
            msg = agent.inbox.get()
            # Process: might generate new messages or tasks
            responses = agent.process(msg)
            for resp in responses:
                self.agents[resp.receiver].inbox.put(resp)
                self.message_log.append(resp)

Now define invariants that must hold after any sequence of actions:

    @invariant()
    def context_window_never_exceeded(self):
        for agent in self.agents.values():
            assert agent.context_used <= agent.context_limit, \
                f"Agent {agent.agent_id} exceeded context limit"

    @invariant()
    def no_orphaned_tasks(self):
        # Every task in task_store has an owner that is still alive or has completed
        for task in self.task_store.all_tasks():
            assert task.status != "orphaned"

    @invariant()
    def message_never_loops_more_than_max(self):
        # Detect cycles: a message chain that revisits the same agent > max_depth
        # This is expensive to check exhaustively, but we can sample
        pass

Run the state machine:

from hypothesis import settings

SwarmTest = SwarmStateMachine.TestCase
SwarmTest.settings = settings(max_examples=1000, stateful_step_count=50)

Hypothesis will generate random sequences of up to 50 actions, run them, and check invariants after each step. If it finds a violation, it shrinks the sequence to the minimal set of steps that triggers the bug.

Stochasticity: Injecting Non-Determinism

Agent swarms are non-deterministic because LLM outputs are stochastic. Your tests need to account for that. One approach: mock the LLM with a controlled random generator. But that defeats the purpose — you want to test the real behavior.

Instead, use stochastic property-based testing: run the same sequence multiple times and check that invariants hold with high probability. Hypothesis supports this via @given with random seeds, but for state machines, you can wrap the test in a loop with different seeds.

Better: use a chaos engineering approach. Introduce random delays, message drops, or reordering in the test harness. This exposes race conditions and timing bugs.

Example: add a rule that randomly drops a message:

    @rule(
        agent_id=st.sampled_from(["A", "B", "C"]),
    )
    def maybe_drop_message(self, agent_id):
        agent = self.agents[agent_id]
        if not agent.inbox.empty() and random.random() < 0.1:
            agent.inbox.get()  # drop it

Now your invariants must hold even under message loss. If they don't, you've found a real issue.

Real-World Example: Context Window Leak

In our own swarm (used for document analysis), we had an agent that would accumulate context from every message it processed, even irrelevant ones. Unit tests didn't catch it because they only sent one message at a time. Property-based testing with 50-step sequences quickly found that context_used would exceed the limit after ~20 messages.

We fixed it by adding a context window eviction policy. The property test now passes with the same random sequences.

Tooling and Infrastructure

  • Hypothesis (Python): stateful testing with RuleBasedStateMachine, built-in shrinking. Use @settings to control number of examples and steps.
  • Postgres as the state store: for persistent agents, you can run PBT against a real database using psycopg2 and rollback after each example.
  • pgvector: if your agents use embeddings, you can test that nearest neighbor retrieval is consistent under concurrent updates.
  • Systemd: run your PBT suite as a periodic service (e.g., systemd.timer) to catch regressions overnight.
  • CI: integrate into your pipeline with pytest and hypothesis. Use --hypothesis-seed to reproduce failures.

Limitations and Trade-offs

Property-based testing is not a silver bullet. It's slow: 1000 examples with 50 steps each means 50,000 state transitions. You'll want to run it in CI but not on every commit. Use nightly builds or a separate pipeline.

Also, defining good invariants is hard. If your invariants are too weak, the test passes but the system is still buggy. If they're too strong, you get false positives. Start with obvious invariants (no context overflow, no deadlock) and add more as you discover real bugs.

Finally, stochastic property-based tests can flake if the system is genuinely non-deterministic. You can mitigate by setting a random seed and re-running with the same seed on failure.

Conclusion

Unit tests are the foundation, but for agent swarms, you need stochastic property-based testing to catch emergent regressions. Use Hypothesis state machines to model your swarm, define invariants that must always hold, and inject chaos to simulate real-world conditions. This methodology has saved us from at least three production bugs that would have been impossible to find with unit tests alone.

Start small: model one invariant (e.g., context window never exceeds limit) and one action (send message). Then expand. Your future self — and your users — will thank you.

#agent-swarms#forensic#property-based-testing#testing#verification
Share — X / Twitter · LinkedIn · HN · Email
Damir Radulić
Founder of RiNET. On the Croatian internet since 1996 (Kvarner Net). In Amsterdam now, building autonomous AI infrastructure that runs on Monday morning when nobody's watching — sovereign stacks, agent swarms, LoRA fine-tuning, civic-intelligence platforms.