diff options
| -rw-r--r-- | .ai/workflows/code-quality.org | 7 | ||||
| -rw-r--r-- | claude-rules/testing.md | 37 | ||||
| -rw-r--r-- | claude-templates/.ai/workflows/code-quality.org | 7 |
3 files changed, 51 insertions, 0 deletions
diff --git a/.ai/workflows/code-quality.org b/.ai/workflows/code-quality.org index 3ac3e9d..0481166 100644 --- a/.ai/workflows/code-quality.org +++ b/.ai/workflows/code-quality.org @@ -9,6 +9,13 @@ One trigger that runs every behavior-preserving quality pass over a scope of orchestrator — each pass keeps its own discipline and its own confirm gate; this workflow only sequences them and collects the residue. +*Behavior-preserving rests on a test net.* The passes below claim to preserve +behavior, but a refactor on untested code is a guess, not a preservation. Where +the scope has no tests, bring it under a characterization net first +(Normal/Boundary/Error per unit, per =testing.md='s "Adding Tests to Existing +Untested Code") — that net is what turns "behavior-preserving" from an assertion +into something the green suite actually verifies across each pass. + The passes it chains: 1. =/refactor= — structural and logic cleanup on measurable metrics (complexity, diff --git a/claude-rules/testing.md b/claude-rules/testing.md index b3fa5bf..81bd391 100644 --- a/claude-rules/testing.md +++ b/claude-rules/testing.md @@ -32,6 +32,31 @@ When working in a codebase without tests: 2. Use the characterization test as a safety net while refactoring 3. Then follow normal TDD for the new change +A characterization test asserts what the code *actually does* right now, not +what it *should* do. Write it by running the code against a fixed input, +reading the exact value or effect it currently produces, and asserting that +value — Feathers' recipe is to assert something you know is wrong, run it, and +paste the real value out of the failure. You don't need to know the correct +answer to write one; you record the observed one. That's what makes it +mechanical enough to bring a large untested surface under test without +re-deriving each unit's spec. + +**Characterize with the same Normal/Boundary/Error set as any unit** (the three +categories below), not one happy-path capture per function. On a characterization +test the negative and boundary cases are the ones that find bugs: untested legacy +code is weakest exactly at the empty input, the malformed value, the missing +upstream, and pinning what it *currently* does there writes the wrong behavior +down in black and white, where it becomes a bug you can see and decide on. When a +pinned case turns out to be a bug rather than behavior worth preserving, that one +test graduates from "record current" to "assert correct" and you fix the code. +The happy-path case is the regression net; the negative and boundary cases are +the audit. + +Bugs that live *inside* a unit are caught by this three-category set; bugs in how +units compose — ordering, shared state handed between them — are invisible to any +per-unit test and need a functional/integration test over the composed path (see +Integration Tests below and the pyramid). + ## Test Categories (Required for All Code) Every unit under test requires coverage across three categories: @@ -287,6 +312,18 @@ Fix: extract focused helpers (one responsibility each), test each in isolation with real inputs, compose them in a thin outer function. Several small unit tests plus one composition test beats one monster test behind a wall of mocks. +When the untestable function is legacy code you're hardening, this extraction +**is** the hardening — not a detour around it. A function whose boundary or +error case can't be exercised without mocking the world (a shell function that +calls `tmux`/`git` directly, a handler that reaches straight into I/O) can't be +characterized, so you can't refactor it safely and you can't pin its edge +behavior. Extracting the pure decision logic into a helper that takes plain +inputs and returns a plain result makes that logic characterizable with the full +Normal/Boundary/Error set; the I/O calls become a thin wrapper you cover once +with a single composition test. "It needs too much mocking to test" is therefore +never a reason to skip the boundary and error cases — it's the signal to reshape +the function so those cases are writable. + ## Coverage Targets - Business logic and domain services: **90%+** diff --git a/claude-templates/.ai/workflows/code-quality.org b/claude-templates/.ai/workflows/code-quality.org index 3ac3e9d..0481166 100644 --- a/claude-templates/.ai/workflows/code-quality.org +++ b/claude-templates/.ai/workflows/code-quality.org @@ -9,6 +9,13 @@ One trigger that runs every behavior-preserving quality pass over a scope of orchestrator — each pass keeps its own discipline and its own confirm gate; this workflow only sequences them and collects the residue. +*Behavior-preserving rests on a test net.* The passes below claim to preserve +behavior, but a refactor on untested code is a guess, not a preservation. Where +the scope has no tests, bring it under a characterization net first +(Normal/Boundary/Error per unit, per =testing.md='s "Adding Tests to Existing +Untested Code") — that net is what turns "behavior-preserving" from an assertion +into something the green suite actually verifies across each pass. + The passes it chains: 1. =/refactor= — structural and logic cleanup on measurable metrics (complexity, |
