Test Discipline
Three gates agents reliably skip, plus a proportional explicit test-only path, enforced by the
real test runner instead of self-audit: the
failing test before a bug fix, the behavior pin before a refactor of untested code, and the
can-this-test-actually-fail check after a new test goes green. Each gate runs BEFORE the edit
lands (or, for validate, immediately after a test is written), so the safety artifact exists
when it still has power.
Triage first — silent no-op on trivial work
Before engaging any mode, read references/right-sizing.md and references/anti-triggers.md.
If the change matches an anti-trigger (typo fix, dependency bump, rename, formatting,
new-feature implementation without an explicit validate request, generated files,
already-covered code), do the requested work without invoking or surfacing this skill. The
structural check below IS the right-size gate; there is no ceremony on trivial edits.
Mode routing — one question
What concrete action is about to happen?
| About to… |
Mode |
Polarity |
| Fix a reported bug with a stated symptom (error, wrong output, failing case) |
repro |
red → green |
| Edit or refactor code with no behavioral test coverage |
characterize |
green → green invariant |
| Write only a test for named existing behavior |
test-only |
run and report; no production edit |
| Explicitly validate a fresh green test, or validate a repro/characterization test |
validate |
mutate → observe → restore |
Anything matching references/anti-triggers.md |
none |
silent no-op |
validate is not a competing implicit trigger: it chains after repro and characterize, and
fires standalone only when the user explicitly asks to validate a freshly-green test. New-feature
implementation stays out of scope; an explicit post-test mutation-check is allowed because it
validates the test rather than taking over the feature workflow.
Mode: repro — reproduce before you fix
- Restate the symptom as a concrete assertion: input → expected output vs reported wrong
output. If the report has no observable symptom, ask for one before writing anything.
- Find the project's test conventions and existing coverage. If an existing test already
asserts the reported behavior and fails for the stated reason, use it as the red oracle;
do not add a duplicate. Otherwise follow the runner, layout, and naming conventions.
- When no adequate test exists, write one asserting the CORRECT behavior — the behavior
the fix should produce.
- Run it and confirm it fails for the stated reason. The failure output must reflect the
reported symptom (the wrong value, the error), not an import or setup problem. Fails for a
different reason → fix the test, not the code.
If it passes, stop. The bug may be elsewhere, already fixed, or misreported — report
"cannot reproduce" with the evidence and go no further; never patch speculatively.
- Preserve the red test in the working tree. Do not stage, commit, push, or alter unrelated
index/worktree state. The red output is the pre-edit evidence; capture it in the response or
eval record rather than repository history.
- Fix the production code. Rerun the test → green. Rerun any suite the project treats as
the pre-commit bar.
- Chain into
validate on the repro oracle after it turns green. This includes a
pre-existing committed test that was red before the fix; “newly green” describes the observed
transition, not whether the test file itself was newly written.
- For user-visible behavior, offer a
/verify-style run-the-app confirmation as the
post-fix mirror — that step is downstream of this gate, not part of it.
Mode: characterize — pin behavior before you refactor
- Verify the no-coverage precondition. Search the test suite for the symbol under edit.
Coverage means tests that assert observable return values, state changes, emitted events,
errors, or other externally visible effects; mock-only or import-only references don't
count. If behavioral coverage exists, this mode does not fire — the suite is the pin.
- Enumerate the observable behaviors worth pinning: main paths, edge cases, error paths.
- Write tests capturing CURRENT behavior exactly — including ugly or suspicious outputs,
annotated as pinned-not-endorsed (a comment like
// pins current behavior; change deliberately, not accidentally). Never "fix" behavior while pinning it.
- Run them against the pre-edit code — all green. Keep the pins in the working tree and
record the pre-edit output. Do not stage or commit them.
- Chain each new pin through
validate (below) so a vacuous pin can't fake safety.
- Perform the edit, keeping the pins green. A red pin means behavior changed: stop, and
either revert the change or get explicit confirmation that the behavior change is intended,
then update the pin deliberately without staging or committing it.
Mode: test-only — honor the narrow request
When the user asks only to add a unit or regression test for named existing behavior:
- Inspect the project's conventions and existing behavioral coverage; extend an appropriate test
instead of duplicating it.
- Establish the expected behavior from the user's request, an authoritative specification, or
current behavior. Ask one focused question if the intended assertion is genuinely ambiguous.
- Write the smallest behavioral test and run only the relevant test target.
- Do not edit production code. If the test is red, report the observed mismatch; a test-only
request does not authorize fixing it. If green, report it as passing.
- Do not mutation-check automatically. Run
validate only if the user explicitly asked to assess
the test's sensitivity. Never stage, commit, or push the test.
Mode: validate — prove the green test can fail
A green test that cannot go red is worse than no test. Right after a repro/characterization oracle
becomes green—including a pre-existing repro test that failed before the fix—prove it observes real
behavior. A test-only request does not opt into mutation. Read
references/mutation-guide.md for mutation selection; the contract:
Pick one mutation in the code under test — the regression the test claims to catch
(invert the fixed condition, wrong constant, skip the guard). One file only.
Run one transactional trial. Prefer:
First resolve the current host's installed test-discipline directory and authorized project
root to absolute paths. Substitute those actual absolute paths, rather than assuming a
provider-specific environment variable:
python3 "/absolute/installed/test-discipline/scripts/mutation_trial.py" src/module.py \
--root "/absolute/project/root" --old 'EXACT ORIGINAL' --new 'MUTATION' \
-- TARGET_TEST_COMMAND...
The wrapper publishes one integrity-checked adjacent backup atomically, applies one exact
replacement, runs only the supplied target test, and attempts restoration in finally. It
restores only when the target still has the captured mutant digest; if a test, user, or other
process changed it, the wrapper preserves that current file and the backup, then stops with a
recovery conflict instead of clobbering the edit. Otherwise it verifies the original digest and
reruns the target test. It refuses linked, hard-linked, special, or out-of-project targets. If
exact replacement cannot represent the mutation, use the lower-level helper with its captured
current-digest restore contract; never use cp, mv, a HEAD snapshot, or hand-rolled backup.
Interpret only this mutation. The wrapper reports nonzero or survived, never
“detected.” A nonzero exit counts as detection only after inspecting the target-test output and
confirming the intended assertion failed because of the selected behavior mutation. Import,
syntax, setup, timeout, signal, and unrelated assertion failures are wrong-red/inconclusive.
A surviving mutation means this test did not detect it; inspect whether the mutant is
behaviorally equivalent or poorly chosen before concluding the test is weak. Call the test
vacuous only with independent evidence, such as discovering that it never imports production
code.
If a meaningful mutant survives, strengthen the test and repeat. If no meaningful safe
mutation can be formed, report the validation gap rather than forcing one.
Never silently leave a mutation in the tree. A leftover .mutbak normally means cleanup did not
complete. If the target digest changed concurrently, do not overwrite it: stop, report both paths
and digests, and let the user reconcile the preserved current file with the verified backup.
Boundaries
/verify and /run are post-action mirrors: they confirm behavior after a change. These
gates run before the edit lands; repro and characterize may hand off to them after green.
/code-review and /simplify are post-diff. A gate never reviews or simplifies; it makes
the safety artifact exist, then the diff flows onward.
- New feature work belongs to feature-dev end-to-end. These gates protect existing behavior.
- Plan mode produces a plan; these gates produce narrow working-tree test artifacts. They never
stage or commit, and may run after planning.
Reference files
references/anti-triggers.md — test-discipline's should-NOT-fire surface. Read during triage.
references/right-sizing.md — test-discipline's one-question triage rule. Read during triage.
references/mutation-guide.md — mutation selection and restore mechanics. Read when
running validate mode.
1---2name: test-discipline3description: Mandatory pre-edit test gate for changes to existing code. Use before fixing a reported regression, crash (including an HTTP 500), invalid-input failure, error, or wrong output—even when the user only says 'fix it' and never requests a test. For a refactor or cleanup, use only when the named behavior lacks behavioral tests; any adequate behavioral coverage is already the pin. Also use proportionately when asked only to write a unit/regression test for named existing behavior, or explicitly asked to validate whether a freshly-green test detects its claimed regression. Reproduce fixes red-before-green, characterize untested behavior before refactoring, and mutation-check only when the workflow or user requests it. Never stage or commit artifacts. Do NOT use for diagnosis-only requests, typos/copy, config/version/dependency bumps, behavior-preserving renames or control-flow rewrites, formatting, generated/vendored/lockfile changes, or ordinary new-feature implementation.4---56# Test Discipline78Three gates agents reliably skip, plus a proportional explicit test-only path, enforced by the9real test runner instead of self-audit: the10failing test before a bug fix, the behavior pin before a refactor of untested code, and the11can-this-test-actually-fail check after a new test goes green. Each gate runs BEFORE the edit12lands (or, for validate, immediately after a test is written), so the safety artifact exists13when it still has power.1415## Triage first — silent no-op on trivial work1617Before engaging any mode, read `references/right-sizing.md` and `references/anti-triggers.md`.18If the change matches an anti-trigger (typo fix, dependency bump, rename, formatting,19new-feature implementation without an explicit validate request, generated files,20already-covered code), do the requested work without invoking or surfacing this skill. The21structural check below IS the right-size gate; there is no ceremony on trivial edits.2223## Mode routing — one question2425What concrete action is about to happen?2627| About to… | Mode | Polarity |28|---|---|---|29| Fix a reported bug with a stated symptom (error, wrong output, failing case) | `repro` | red → green |30| Edit or refactor code with no behavioral test coverage | `characterize` | green → green invariant |31| Write only a test for named existing behavior | `test-only` | run and report; no production edit |32| Explicitly validate a fresh green test, or validate a repro/characterization test | `validate` | mutate → observe → restore |33| Anything matching `references/anti-triggers.md` | none | silent no-op |3435`validate` is not a competing implicit trigger: it chains after `repro` and `characterize`, and36fires standalone only when the user explicitly asks to validate a freshly-green test. New-feature37implementation stays out of scope; an explicit post-test mutation-check is allowed because it38validates the test rather than taking over the feature workflow.3940## Mode: repro — reproduce before you fix41421. **Restate the symptom as a concrete assertion**: input → expected output vs reported wrong43 output. If the report has no observable symptom, ask for one before writing anything.442. **Find the project's test conventions and existing coverage.** If an existing test already45 asserts the reported behavior and fails for the stated reason, use it as the red oracle;46 do not add a duplicate. Otherwise follow the runner, layout, and naming conventions.473. **When no adequate test exists, write one asserting the CORRECT behavior** — the behavior48 the fix should produce.494. **Run it and confirm it fails for the stated reason.** The failure output must reflect the50 reported symptom (the wrong value, the error), not an import or setup problem. Fails for a51 different reason → fix the test, not the code.52 **If it passes, stop.** The bug may be elsewhere, already fixed, or misreported — report53 "cannot reproduce" with the evidence and go no further; never patch speculatively.545. **Preserve the red test in the working tree.** Do not stage, commit, push, or alter unrelated55 index/worktree state. The red output is the pre-edit evidence; capture it in the response or56 eval record rather than repository history.576. **Fix the production code.** Rerun the test → green. Rerun any suite the project treats as58 the pre-commit bar.597. **Chain into `validate`** on the repro oracle after it turns green. This includes a60 pre-existing committed test that was red before the fix; “newly green” describes the observed61 transition, not whether the test file itself was newly written.628. For user-visible behavior, offer a `/verify`-style run-the-app confirmation as the63 post-fix mirror — that step is downstream of this gate, not part of it.6465## Mode: characterize — pin behavior before you refactor66671. **Verify the no-coverage precondition.** Search the test suite for the symbol under edit.68 Coverage means tests that assert observable return values, state changes, emitted events,69 errors, or other externally visible effects; mock-only or import-only references don't70 count. If behavioral coverage exists, this mode does not fire — the suite is the pin.712. **Enumerate the observable behaviors** worth pinning: main paths, edge cases, error paths.723. **Write tests capturing CURRENT behavior exactly** — including ugly or suspicious outputs,73 annotated as pinned-not-endorsed (a comment like `// pins current behavior; change74 deliberately, not accidentally`). Never "fix" behavior while pinning it.754. **Run them against the pre-edit code — all green.** Keep the pins in the working tree and76 record the pre-edit output. Do not stage or commit them.775. **Chain each new pin through `validate`** (below) so a vacuous pin can't fake safety.786. **Perform the edit, keeping the pins green.** A red pin means behavior changed: stop, and79 either revert the change or get explicit confirmation that the behavior change is intended,80 then update the pin deliberately without staging or committing it.8182## Mode: test-only — honor the narrow request8384When the user asks only to add a unit or regression test for named existing behavior:85861. Inspect the project's conventions and existing behavioral coverage; extend an appropriate test87 instead of duplicating it.882. Establish the expected behavior from the user's request, an authoritative specification, or89 current behavior. Ask one focused question if the intended assertion is genuinely ambiguous.903. Write the smallest behavioral test and run only the relevant test target.914. Do not edit production code. If the test is red, report the observed mismatch; a test-only92 request does not authorize fixing it. If green, report it as passing.935. Do not mutation-check automatically. Run `validate` only if the user explicitly asked to assess94 the test's sensitivity. Never stage, commit, or push the test.9596## Mode: validate — prove the green test can fail9798A green test that cannot go red is worse than no test. Right after a repro/characterization oracle99becomes green—including a pre-existing repro test that failed before the fix—prove it observes real100behavior. A test-only request does not opt into mutation. Read101`references/mutation-guide.md` for mutation selection; the contract:1021031. **Pick one mutation** in the code under test — the regression the test claims to catch104 (invert the fixed condition, wrong constant, skip the guard). One file only.1052. **Run one transactional trial.** Prefer:106107 First resolve the current host's installed `test-discipline` directory and authorized project108 root to absolute paths. Substitute those actual absolute paths, rather than assuming a109 provider-specific environment variable:110111 ```bash112 python3 "/absolute/installed/test-discipline/scripts/mutation_trial.py" src/module.py \113 --root "/absolute/project/root" --old 'EXACT ORIGINAL' --new 'MUTATION' \114 -- TARGET_TEST_COMMAND...115 ```116117 The wrapper publishes one integrity-checked adjacent backup atomically, applies one exact118 replacement, runs only the supplied target test, and attempts restoration in `finally`. It119 restores only when the target still has the captured mutant digest; if a test, user, or other120 process changed it, the wrapper preserves that current file and the backup, then stops with a121 recovery conflict instead of clobbering the edit. Otherwise it verifies the original digest and122 reruns the target test. It refuses linked, hard-linked, special, or out-of-project targets. If123 exact replacement cannot represent the mutation, use the lower-level helper with its captured124 current-digest restore contract; never use `cp`, `mv`, a HEAD snapshot, or hand-rolled backup.1253. **Interpret only this mutation.** The wrapper reports `nonzero` or `survived`, never126 “detected.” A nonzero exit counts as detection only after inspecting the target-test output and127 confirming the intended assertion failed because of the selected behavior mutation. Import,128 syntax, setup, timeout, signal, and unrelated assertion failures are wrong-red/inconclusive.129 A surviving mutation means this test did not detect it; inspect whether the mutant is130 behaviorally equivalent or poorly chosen before concluding the test is weak. Call the test131 vacuous only with independent evidence, such as discovering that it never imports production132 code.1334. If a meaningful mutant survives, strengthen the test and repeat. If no meaningful safe134 mutation can be formed, report the validation gap rather than forcing one.135136Never silently leave a mutation in the tree. A leftover `.mutbak` normally means cleanup did not137complete. If the target digest changed concurrently, do not overwrite it: stop, report both paths138and digests, and let the user reconcile the preserved current file with the verified backup.139140## Boundaries141142- `/verify` and `/run` are post-action mirrors: they confirm behavior after a change. These143 gates run before the edit lands; `repro` and `characterize` may hand off to them after green.144- `/code-review` and `/simplify` are post-diff. A gate never reviews or simplifies; it makes145 the safety artifact exist, then the diff flows onward.146- New feature work belongs to feature-dev end-to-end. These gates protect existing behavior.147- Plan mode produces a plan; these gates produce narrow working-tree test artifacts. They never148 stage or commit, and may run after planning.149150## Reference files151152- `references/anti-triggers.md` — test-discipline's should-NOT-fire surface. Read during triage.153- `references/right-sizing.md` — test-discipline's one-question triage rule. Read during triage.154- `references/mutation-guide.md` — mutation selection and restore mechanics. Read when155 running validate mode.