5.6 KiB
name, version, description
| name | version | description |
|---|---|---|
| tdd-criteria | 0.1.0 | TDD by default with four bright-line carve-outs. Applies before any code change the agent didn't author this session. Triggers: "TDD", "test-driven", "следуй TDD", "use TDD", "should I write tests", "skip tdd", "[skip-tdd: ...]", "[test-modify: ...]", "tdd-criteria". Cross-agent policy — no tool refs. Full rationale: .wiki/concepts/tdd-criteria-design.md |
tdd-criteria
TDD by default. Skip only with a bright-line marker. Tests defend code from silent deletion; rule 4 defends tests from silent rewriting.
When this runs
At session start — when CLAUDE.md contains the line follow tdd-criteria.
Before any code change the agent didn't author this session — touching a *.ts, *.js, *.py, or similar source file triggers the decision algorithm below.
On explicit reference — when the user says "TDD", "test-driven", "следуй TDD", "use TDD", "should I write tests", "skip tdd", "tdd-criteria", or includes [skip-tdd: ...] or [test-modify: ...] in a commit subject.
Default mode
TDD by default. Skip only if one of four bright-line carve-outs matches and is marked in the commit subject.
Decision algorithm (8 questions, top-down)
Walk through in order. First «yes» determines mode. All «no» → TDD by default.
- Bug fix? → TDD (red-test first)
- Consuming external contract (SDK, REST API, чужая schema)? → TDD (contract-test)
- Security / auth / money / identifiers? → TDD
- Pure logic — function (input → output) without I/O, global state, bounded inputs? → TDD
- Visual / config — CSS, layout, design tokens,
.env.example, prompts, wiki, README? → SKIP,[skip-tdd: visual] - Spike — explicit POC «throwaway» in commit/PR/task subject? → SKIP,
[skip-tdd: spike]+ spike-survivor task if code survives - One-shot — migration, ETL backfill, ad-hoc cleanup, runs once? → SKIP,
[skip-tdd: oneshot] - Transit wrapper ≤10 lines — no branching, re-export / glue? → SKIP,
[skip-tdd: wrapper]
(default) → TDD
Ironclad rules (TDD obligatory)
- Bug fix — if there's an issue / failure log / repro, write a red-test codifying the repro. Marginal cost: 5 min. Marginal benefit: regression test forever.
- Pure logic, bounded inputs — no fixtures, no mocks, no setup. Test = input/output pair. Skipping is gratuitous.
- Third-party contract — SDK bumps change signatures silently. Contract-test pins known input → known shape. Catches break at first install, not days later.
- Security / auth / money / IDs — asymmetric blast radius: false positive ≪ false negative. TDD = insurance.
In all four, recovery cost from silent deletion is high. The test is the only artefact that makes deletion visible.
Permissive carve-outs (skip + marker required)
| # | Category | Trigger | Marker |
|---|---|---|---|
| 5 | Visual / config | CSS, layout, tokens, .env.example, prompts, wiki |
[skip-tdd: visual] |
| 6 | Spike | «POC, throwaway» in commit/PR/task subject | [skip-tdd: spike] |
| 7 | One-shot | Migration, ETL, ad-hoc cleanup; runs once | [skip-tdd: oneshot] |
| 8 | Wrapper | ≤10 lines, no branching, re-export / glue | [skip-tdd: wrapper] |
These are accepted-risk zones — you accept that an agent can vandalise without immediate signal, because recovery is cheap (eyeball next render; throwaway by contract; runs once; reconstruct ≤ delete).
Anti-loophole
- Skip without category is invalid. One of four explicit categories required — not «other reasons». No marker = violation.
- Spike survivor rule. Merged spike → same merge-commit creates
[backfill-tests-<slug>]task. Otherwise «spike» becomes «skipped tests forever». - Friction is the point.
[skip-tdd: visual]50× in a design-system rework is irritating — that's the fence. Re-evaluate after ≥2 weeks, not before. - Tests are append-only by default. Modifying an assertion, deleting a test, or disabling it (
it.skip/xit/@pytest.mark.skip/@Disabled) requires:- Marker in commit subject:
[test-modify: <test-name>: was <X>; is <Y>; reason: <Z>]—<X>and<Y>are literal assertion expressions, not paraphrased. Example:[test-modify: validates email: was expect(isValid("a@b")).toBe(true); is expect(isValid("a@b.com")).toBe(true); reason: tightened spec to require TLD] - Separate commit from impl changes. A commit must not modify both
*.test.*andsrc/*files (or project-equivalents).git log --grep '\[test-modify'must show a clean test-only audit trail. - Why literal was/is: an agent forced to write the literal assertion publishes exactly what they're rewriting. «Updated to match new behaviour» hides everything — agents will use that whenever allowed.
- Marker in commit subject:
Cross-agent applicability
Pure policy — no agent-specific tool references in this body. Works on Claude, Gemini, Copilot, or any future agent. Hermes mapping: mode: auto, category: software-development, no replace-rules.
Out of scope
- Does not enforce via git hooks (separate optional task:
tdd-criteria-precommit-hook). - Does not modify project
CLAUDE.md(that'sproject-bootstrap's job). - Does not run tests.
- Does not apply rule 4 retroactively to tests written before the rule was adopted.
Why this exists
Tests make behaviour an invariant; without them, code is an artefact silent-deletable by agents. AND: the test itself must be defended too (rule 4) — otherwise the contract collapses back into an artefact when the agent rewrites the failing test. Full rationale: .wiki/concepts/tdd-criteria-design.md.