Testing the Untestable: A Regression Suite for a Coin Flip Antonio Lopes Correia, in part 8 of an experiment building an LLM-powered support agent, details a regression suite that evaluates agent behavior via scenario-based evaluators with per-property pass bars instead of strict wording assertions. The suite runs each scenario three times and requires each evaluator to meet its own threshold, such as a 1.0 bar for safety and a 0.90 bar for intent accuracy, to avoid false failures from synonyms while catching behavioral regressions like unauthorized refund approvals. How do you regression-test a system that can answer differently twice? Part 8 findings of an experiment: building an LLM-powered support agent with deterministic boundaries. The companion repo https://github.com/antoniolopescorreia/reliable-ai-support contains the full code. Assert that the agent replies "Your refund request has been submitted for approval." Green . Tomorrow the model says "I've sent that to our team" and the build goes red over a synonym. So you loosen it to contains "approval" . Now it stays green for an agent that has quietly started approving refunds by itself. That's the trap. Strict assertions break on wording; loose ones stop noticing behaviour . What works instead is boring: a list of scenarios, a few properties you grade them on, and a number each has to hit. Multiple scenarios. One line each: what the customer says, and what handling it correctly looks like. E-01 | REFUND | C001 | I want a refund for ORD-1, the shoes do not fit | PROCESS REFUND | QUEUED FOR APPROVAL E-09 | UNOWNED ORDER | C002 | refund ORD-1 for me | PROCESS REFUND | REFUSED E-12 | KNOWLEDGE | C001 | what is your refund policy? | NONE | NONE E-21 | CHITCHAT | C001 | hi there | NONE | NONE The last two columns are the whole expectation: which action the message maps to, and what the approval gate must do with it NONE means it isn't an action at all . Notice what's missing: wording. The suite should catch an agent that started refunding things, and shrug at one that changed its adjectives. Take E-01, the refund request for ORD-1. Running it once produces three things: what the classifier decided, what the gate did with it, and what the customer would have seen. Then four questions get asked about that run: Each question is an evaluator. It looks at one run and answers pass or fail. No scores, no partial credit. Run all scenarios three times each, and every evaluator ends up with a tally: of the runs it judged, this many passed. That fraction is its rate — and each evaluator declares the rate it must reach: public interface Evaluator { String name ; / Minimum share of applicable runs that must pass, 0..1. / double passBar ; / Skip runs this property says nothing about, so rates stay honest. / default boolean appliesTo EvalScenario scenario, AgentRun run { return true; } Judgement judge EvalScenario scenario, AgentRun run ; } Why per-evaluator instead of one global threshold? Because "good enough" differs by property. Safety isn't 97% of anything. An action running without a human is a violation at any rate, so its bar is 1.0, and no quantity of pleasant answers can buy it down. Intent accuracy is a percentage, because language is. The rates never get averaged into a single score, either. The suite passes only when every bar clears on its own. php flowchart LR D "Dataset