Skip to content
OpenAgentsbeta
text
1# Test Writer Loop — Harness
2
3A disciplined red/green loop for improving test coverage: pick an untested (or
4under-tested) unit of behavior, write a test that fails for the right reason, implement
5just enough to pass it, run the suite, and repeat — tracking coverage delta each cycle
6so the loop has an objective stop condition instead of running forever.
7
8This is a **harness**: it wraps a control loop around the agent's normal edit/run
9cycle. See `loop.md` for the per-iteration procedure and `stop-conditions.md` for when
10to end the session.
11
12Inputs: `target_path` (optional — auto-selects lowest-coverage module if omitted),
13`max_iterations` (default 10), `coverage_command` (auto-detected if omitted).
14
15## Why red/green, not "write tests for this file"
16
17Writing tests against code you're also about to change tests your assumptions about
18the code, not the code's actual behavior. The loop instead:
191. Writes a test for behavior that **should** exist (from the spec/requirements, a
20 docstring, an issue, or an inferred contract) and confirms it **fails** first —
21 proving the test actually exercises something, and isn't a false-positive that
22 would pass even against broken code.
232. Only then implements/fixes the behavior to make it pass.
243. Confirms the full suite is still green (no regressions from the change) before
25 moving to the next unit.
26
27This is the same discipline as classic TDD, applied opportunistically to existing
28under-tested code rather than only to new code.
29
30## Setup
31
321. Detect the test runner and coverage tool from the package manifest (`npm test`,
33 `pytest --cov`, `go test -cover`, etc.), or use `coverage_command` if provided.
342. Run the existing suite once to get a coverage baseline. Record: overall %, and
35 per-file/per-module % where the tool reports it.
363. If `target_path` is given, scope to it. Otherwise select the module with the lowest
37 coverage that also has non-trivial logic (skip pure re-exports, generated code,
38 trivial getters/setters, and config files — low coverage there isn't meaningful).
39
40See `loop.md` for the iteration procedure and `stop-conditions.md` for when to stop.
41
42## Selecting a target when none is given
43
44When `target_path` is omitted, rank candidate modules by a combination of:
451. **Coverage %** — lower is more urgent, but see point 3 before picking the absolute
46 lowest.
472. **Change frequency** — a module that shows up often in `git log` is more likely to
48 regress silently; prioritize it over an equally-low-coverage module that rarely
49 changes.
503. **Logic density** — skip modules that are low-coverage only because they're mostly
51 type definitions, constants, or trivial pass-through code. A 20%-covered file with
52 five branches of real logic is a better target than a 5%-covered file that's 200
53 lines of enum declarations.
544. **Blast radius** — prefer modules imported by many other modules (a shared utility,
55 a core data model) over leaf modules used in exactly one place, since a regression
56 there has wider impact.
57
58State the selection reasoning briefly before starting the loop (e.g. "targeting
59`src/billing/invoice.ts`: 22% coverage, changed in 8 of the last 20 commits, and holds
60the tax-calculation logic other modules depend on") so the choice is auditable rather
61than opaque.
62
63## Example session shape
64
65```
66Baseline: src/billing/invoice.ts — 22% line coverage, 0% branch coverage on error paths
67
68Iteration 1: "throws when quantity is negative"
69 red → test fails with "expected InvalidQuantityError, got undefined"
70 green → added validation + custom error class
71 suite → 142/142 passing
72 coverage → 31% (+9%)
73
74Iteration 2: "rounds unit price to 2 decimal places on total"
75 red → test fails: 19.999999999998 !== 20.00
76 green → applied Math.round in the total calculation
77 suite → 143/143 passing
78 coverage → 38% (+7%)
79
80...
81
82Stopped after iteration 6: two consecutive iterations added < 1% coverage;
83remaining uncovered lines are defensive branches unreachable via the public API.
84
85Final: 22% → 61% line coverage, 0% → 74% branch coverage on error paths.
866 tests added across 6 iterations. 1 gap deferred: currency-conversion path
87requires a live exchange-rate API with no test double available here.
88```
89
90## Reporting
91
92At the end of the session (whether stopped by a stop condition or `max_iterations`),
93report:
94- Coverage before → after (overall and for the targeted module(s)).
95- Number of tests added, number of iterations run.
96- Any behavior gaps found but *not* fixed (e.g. a bug the loop surfaced that's out of
97 scope for a test-writing pass — flag it, don't silently fix unrelated bugs).
98- Any test that was skipped/deferred and why (e.g. requires infrastructure not
99 available in this environment, like a real database or external API).
100
101## Guardrails
102
103- Never delete or weaken an existing passing test to make the suite green faster.
104- Never change production code's behavior to match a convenient test unless the
105 existing behavior is confirmed to be a bug (and if so, call it out explicitly as a
106 bug fix, not a routine coverage improvement).
107- If implementing the missing behavior would require a design decision the harness
108 can't infer (e.g. "what should happen on duplicate input" isn't specified anywhere),
109 stop and ask rather than guessing silently.
110- Keep each iteration's diff small and focused on one unit of behavior — this keeps
111 the red/green cycle fast and makes each commit reviewable on its own.
112

Keyboard shortcuts

Focus search
/
Go to Explore
ge
Go to Home
gh
Go to Tags
gt
Go to Collections
gc
Show this help
?
Close suggestions or this dialog
Esc