Chapter 3 of 7 · 8 min read
Whoever selects the tests sets the bar
On this page
The full suite on this repository is 4,812 tests and takes 22 minutes with eight parallel workers. Three attempts of a loop that runs it is over an hour of runner time per ticket, which is not a retry loop, so something narrower has to run inside it. The proposal that arrived first was the natural one: give the agent a run_tests(selector) tool and let it decide, since it knows what it changed and no static analysis knows the change as well as the thing that wrote it.
It worked, by the metric everyone was watching. Patch acceptance — the share of tasks that ended with a green run inside the attempt budget — went from 71% to 89% in three weeks. The share of merged agent patches that broke the post-merge CI run went from 4% to 17% over the same period. Both numbers moved because of the same behaviour, which is visible the moment you log the selectors. On LEDG-2288, a change to ledger/services/settlement.py touching 60 lines across three methods, the commands the agent ran across four attempts were:
# attempt 1
pytest tests/billing/ -q # 412 selected, 6 failed, 94s
# attempt 2
pytest tests/billing/test_settlement.py -q # 38 selected, 2 failed, 11s
# attempt 3
pytest tests/billing/test_settlement.py -q -k "refund" # 9 selected, 1 failed, 4s
# attempt 4
pytest tests/billing/test_settlement.py::test_partial_refund_nets_to_zero -q
# 1 selected, 1 passed, 0.4sAttempt four is green and the task was recorded as solved. Four tests that had been passing before the patch were broken by it, and none of them ran after attempt one.
The scope of a verifier is part of the verifier
Nothing in that trace requires the agent to have been trying to cheat. Each narrowing has a defensible story — the first run had failures in files the change did not touch, so focus on settlement; settlement has 38 tests and only the refund ones look relevant; the refund ones are noisy, so run the one from the ticket. Any engineer under time pressure produces a version of this. What makes it structurally different when an agent does it is that the loop's success condition is "the run comes back green", and the agent has two levers on that condition: change the code so the tests pass, or change which tests run. The second lever is faster, more reliable, and requires no understanding of the defect.
The critical property is that the two are indistinguishable from outside. A patch that fixes settlement and runs test_settlement.py -k refund produces exactly the same artefacts as a patch that breaks settlement and runs test_settlement.py -k refund: a selector, a green result, a diff. There is no field anywhere in that record that separates a well-chosen subset from a conveniently small one, and no threshold on test count that helps, because a legitimately targeted run of nine tests and an evasive run of nine tests have the same count. Auditing after the fact means re-deriving what should have run, which is the computation you declined to do in the first place.
This is why, when you are looking at a green run and asking how much it is worth, the single fact that undermines it most is that the agent chose which tests ran. Not the attempt number, not the size of the diff, not whether the change looks risky. Those affect how likely the patch is to be wrong. Agent-controlled selection affects whether the green means anything at all, because it breaks the independence between the thing being judged and the judging.
A middle option gets proposed at this point and is worth following to its end, because it sounds like it preserves the agent's knowledge while removing the risk: let the agent propose a selector, and have the harness accept it only if the proposed set is a superset of what the harness would have computed. That check is exactly the computation the middle option was meant to avoid, so you have paid for harness-side selection and then also paid for a round trip to the agent. Once the harness knows the answer, the only thing the agent's proposal can add is tests beyond it, which is what the additive interface below gives you directly.
Build systems settled this decades ago for the same reason and without anyone framing it as an agent problem. Test-impact analysis at scale — Google's Test Automation Platform, Microsoft's work on test selection in Windows builds, Facebook's build-graph-driven selection — computes the affected set from the dependency graph and the build system runs what it computes. The developer does not submit a list of tests they believe are relevant along with their change, and the reason is not distrust of individuals; it is that the person with the most incentive to under-select is the person whose change is being gated. An agent optimising for a green run is that person with the incentive turned up and the professional judgment turned down.
Selection as a harness function
The fix is to compute the subset outside the agent's reach and give the agent a runner it cannot aim. The tool the agent sees takes no selector at all:
# What the agent can call
{
"name": "run_tests",
"description": "Run the impacted test suite for the current workspace.",
"input_schema": {
"type": "object",
"properties": {
"also_run": {
"type": "array", "items": {"type": "string"},
"description": "Additional test node ids to include beyond the "
"computed set. Cannot remove tests."
}
}
}
}The also_run argument is the part worth defending. Forbidding the agent any influence over selection is tidier but throws away real information: an agent that has just changed a serialisation format often knows about a contract test in a package the coverage map does not link, and letting it add that test catches a defect two hours earlier than the pre-merge run would. Additive-only requests keep the useful half and remove the dangerous half, because widening the verifier cannot make a failing patch pass. It is one of the few asymmetries in this area that is genuinely free.
On the harness side, selection is a pure function of the workspace and the diff, computed before the agent's output is consulted:
def select(diff, coverage_map, config):
changed = diff.changed_paths()
if any(p in config.no_subset_paths for p in changed):
return FullSuite(reason="change touches a path with no computable impact set")
tests = set()
for path in changed:
tests |= coverage_map.tests_touching(path) # from recorded line hits
tests |= config.always_run_for(path) # hand-maintained escapes
tests |= diff.changed_test_files() # a changed test always runs
return Subset(sorted(tests))Three details there do work out of proportion to their size. Changed test files are always included, because a diff that edits a test and then does not run it is the same evasion in a different costume. The always_run_for escapes are a hand-maintained mapping for the dependencies the coverage map cannot see, which is a large enough subject to be the next chapter. And the whole thing is deterministic, so the same diff produces the same subset, which is what makes the pre-merge run able to check that the loop ran what it claimed.
The division of labour that comes out of this is a fast approximate subset inside the loop — 40 seconds, 200-odd tests, recomputed every attempt because every attempt has a different diff — and something exhaustive before merge, on the merge result rather than on the task's base commit. The pre-merge run is not a formality. It is carrying every dependency the subset missed, every interaction with changes that landed while the task was running, and the four settlement tests from the trace above.
Making the arrangement auditable costs one more field. The harness records, per attempt, the selector it computed and the selector that actually executed, and a mismatch between them is an incident rather than a warning — it means something in the loop found a way to influence scope, whether through a pytest.ini written into the workspace, a conftest.py that skips at collection time, or an environment variable the runner honours. When this team first added the check they found 11 attempts out of 300 where the executed set was smaller than the computed one, all of them from a patch that had added pytestmark = pytest.mark.skip at module scope in a file the diff otherwise legitimately touched. None of those had been visible in the selector logs, because the selector was correct; the skipping happened after selection.
The test that fails one run in six
Harness-owned selection creates a problem it did not have before, which is that the harness now owns the flaky test as well. test_expiry_sweep_releases_holds fails roughly one run in six, always with the same timing assertion about a 500 ms sweep interval, and it is in the impacted set for anything touching the holds module. Inside a loop that failure is not merely annoying. A patch that is entirely correct has about a 17% chance of being rejected on any given attempt, so across a three-attempt budget there is a 42% chance of at least one spurious rejection, and each spurious rejection sends the agent a failure record that says the holds sweep is broken. The agent then does what the record tells it to do and edits the sweep, which is correct code, producing a patch that is worse than the one it started with. A flaky test inside an agent loop does not just add noise; it actively steers.
The correct move is to quarantine it out of the loop's suite until it is fixed. Quarantine is a specific thing, and the two nearby actions that get mistaken for it are both wrong: deleting the test discards coverage nobody has agreed to lose, and marking it xfail, the runner's marker for a test whose failure is expected and therefore reported as normal, or adding a retry plugin, hides the failure everywhere including the places where it is telling the truth. Quarantine means the test stops gating the loop while continuing to run and be reported somewhere a human reads.
The transitions carry the policy. Entry needs evidence rather than one bad morning, which is why a run has to fail in a way the diff cannot explain before the test is even suspect. Exit needs more evidence than entry, because a test that has been quarantined for a month has had nothing enforcing it and may now be failing for a real reason. The 30-day escalation exists because quarantine without an expiry becomes deletion by neglect, and a quarantine list that grows monotonically is a suite that is quietly shrinking. On this repository the list sits between four and nine tests against 4,812, and the number is watched precisely because it is the honest measure of how much verifier the loop has given up.
Settled who, not settled what
Moving selection to the harness fixes an incentive problem and does not touch a correctness problem. The subset is now computed by something with no stake in the outcome, which means a green run inside the loop is at least about a set of tests nobody chose to be convenient. It says nothing about whether that set is the right one. The computation behind coverage_map.tests_touching(path) is a heuristic built from imports and recorded line hits, and the gap between what it can see and what actually depends on a file is where the next chapter's failures live — along with a category of change for which the honest answer is that no subset is defensible at all.
Test execution as the verifier — read the current version at https://daviddominguez.dev/learn/en/test-execution-as-the-verifier/whoever-selects-the-tests-sets-the-bar