text
| 1 | # Test Writer Loop — Harness |
| 2 | |
| 3 | A disciplined red/green loop for improving test coverage: pick an untested (or |
| 4 | under-tested) unit of behavior, write a test that fails for the right reason, implement |
| 5 | just enough to pass it, run the suite, and repeat — tracking coverage delta each cycle |
| 6 | so the loop has an objective stop condition instead of running forever. |
| 7 | |
| 8 | This is a **harness**: it wraps a control loop around the agent's normal edit/run |
| 9 | cycle. See `loop.md` for the per-iteration procedure and `stop-conditions.md` for when |
| 10 | to end the session. |
| 11 | |
| 12 | Inputs: `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 | |
| 17 | Writing tests against code you're also about to change tests your assumptions about |
| 18 | the code, not the code's actual behavior. The loop instead: |
| 19 | 1. 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. |
| 23 | 2. Only then implements/fixes the behavior to make it pass. |
| 24 | 3. Confirms the full suite is still green (no regressions from the change) before |
| 25 | moving to the next unit. |
| 26 | |
| 27 | This is the same discipline as classic TDD, applied opportunistically to existing |
| 28 | under-tested code rather than only to new code. |
| 29 | |
| 30 | ## Setup |
| 31 | |
| 32 | 1. 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. |
| 34 | 2. Run the existing suite once to get a coverage baseline. Record: overall %, and |
| 35 | per-file/per-module % where the tool reports it. |
| 36 | 3. 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 | |
| 40 | See `loop.md` for the iteration procedure and `stop-conditions.md` for when to stop. |
| 41 | |
| 42 | ## Selecting a target when none is given |
| 43 | |
| 44 | When `target_path` is omitted, rank candidate modules by a combination of: |
| 45 | 1. **Coverage %** — lower is more urgent, but see point 3 before picking the absolute |
| 46 | lowest. |
| 47 | 2. **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. |
| 50 | 3. **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. |
| 54 | 4. **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 | |
| 58 | State 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 |
| 60 | the tax-calculation logic other modules depend on") so the choice is auditable rather |
| 61 | than opaque. |
| 62 | |
| 63 | ## Example session shape |
| 64 | |
| 65 | ``` |
| 66 | Baseline: src/billing/invoice.ts — 22% line coverage, 0% branch coverage on error paths |
| 67 | |
| 68 | Iteration 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 | |
| 74 | Iteration 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 | |
| 82 | Stopped after iteration 6: two consecutive iterations added < 1% coverage; |
| 83 | remaining uncovered lines are defensive branches unreachable via the public API. |
| 84 | |
| 85 | Final: 22% → 61% line coverage, 0% → 74% branch coverage on error paths. |
| 86 | 6 tests added across 6 iterations. 1 gap deferred: currency-conversion path |
| 87 | requires a live exchange-rate API with no test double available here. |
| 88 | ``` |
| 89 | |
| 90 | ## Reporting |
| 91 | |
| 92 | At the end of the session (whether stopped by a stop condition or `max_iterations`), |
| 93 | report: |
| 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 |