npx skills add ...
npx skills add pandoscope/skills --skill tdd
Test-driven development via red-green-refactor with an auditable commit protocol. Use when implementing any feature, bugfix, refactor, or behavior change before writing implementation code, or when the user mentions TDD, red-green-refactor, or test-first development.
npx skills add pandoscope/skills --skill tdd
Write test first. Commit it red. Write minimal code to pass. Commit green. Refactor.
Core principle: tests verify behavior through public interfaces, not implementation. Code can change entirely; tests shouldn't. A test that breaks when you rename an internal function — with no behavior change — was testing implementation. Delete it.
No watched failure = no proof the test tests the right thing. Commit history is the evidence; hooks check its structure.
See tests.md for examples and mocking.md for mocking guidelines.
NO PRODUCTION CODE WITHOUT A FAILING TEST COMMITTED FIRST.
Wrote code before the test? Delete it. Implement fresh from tests. Delete means delete. Exceptions (throwaway prototypes, generated code, config) need human sign-off. Thinking "skip TDD just this once"? That is rationalization.
Use the project's domain glossary so test names and interface vocabulary match the codebase; respect ADRs in the area you touch.
DO NOT write all tests, then all implementation. That is horizontal slicing and it produces crap tests: written in bulk they test imagined behavior and the shape of things (signatures, data structures), go insensitive to real changes, and commit you to test structure before you understand the code.
Work in vertical slices — one test → one implementation → repeat. Each test responds to what the last cycle taught you.
The first slice is a tracer bullet: it proves the path works end to end.
One behavior = one RED commit + one GREEN commit. Multiple cycles per branch is fine; prefer one cycle in flight in Go/Rust repos (see markers).
Every commit — RED included — leaves the tree lint-clean. After each
commit, run the repo's full hook suite over the whole tree (e.g.
prek run --all-files): exit code 0 and no files modified. An auto-fixer
changing files IS a failure — apply the fix and amend it into the commit that
introduced the problem before starting the next cycle. CI evaluating only the
PR head never excuses a dirty intermediate commit; per-commit hygiene is
verified here, at commit time, or not at all.
A RED commit is red on tests only: lint, format, and type checks still
pass on it. When the new test calls a function, method, or parameter that does
not exist yet, type checking would fail — so first commit a minimal
signature-only stub as its own commit, prefix chore(stub):. Signature +
docstring, zero behavior: a new callable raises NotImplementedError; a new
parameter is accepted and ignored. This is the one sanctioned exception to the
Iron Law: a stub defines interface, and must contain no implementation —
any logic in a stub commit is production code without a failing test. Skip
this step entirely when the surface already exists.
NotImplementedError — never a typo, import error, or missing symbol). Passes immediately? It tests existing behavior — fix the test.test(red):.| Language | Marker | Strict? |
|---|---|---|
| Python (pytest) | @pytest.mark.xfail(strict=True) | yes — XPASS fails suite |
| TS/JS (vitest) | test.fails(...) / it.fails(...) | yes |
| TS/JS (jest) | it.failing(...) | yes |
| Go | //go:build red + TestRed prefix + red-tests job | aggregate only |
| Rust | #[ignore = "red"] + red_ prefix + red-tests job | aggregate only |
| Other | find a strict expected-failure mechanism; none exists → commit unmarked, tell the human the repo lacks red enforcement | n/a |
Go/Rust build-tag/ignore markers exclude tests from the normal suite, so a red-tests CI job must run only the marked tests and expect failure (no-op when none exist). Its exit code is aggregate: two red tests in flight, one wrongly passing → the job still passes. Strict markers catch this per-test; the job doesn't.
feat:/fix:.Remove duplication, improve names, extract helpers, deepen modules. Tests stay green, no new behavior, separate commit. Never refactor while red. Then start the next cycle.
After all tests pass, look for refactor candidates:
lint-red.sh (ships with skill; wire into prek + CI): staged/commit modes check that test(red): commits touch only test files and add a marker, others add none; merge mode rejects any markers in tree; if Go/Rust markers are present, a red-tests job must exist.
Hooks verify commit structure and marker hygiene. They do not verify you ran the unmarked test and watched it fail for the right reason — that stays on you. Same for per-commit lint: an installed pre-commit hook blocks dirty commits at commit time, but where no hook is installed (fresh remote environments), the after-every-commit --all-files run is manual discipline — do it anyway. Strict markers partially compensate (XPASS catches tests of already-existing behavior). "Lint passed" ≠ "TDD verified."
| Quality | Rule |
|---|---|
| Behavioral | Exercises a real path through the public API; survives refactors |
| Minimal | One thing. "and" in the name? Split it. |
| Clear | Name states the behavior, not test1 |
| Honest | Tests the code, never the mock |
Unmarked test passes before implementation exists · can't explain why the test failed · weakening an assertion in GREEN to make it pass · testing the mock · "just this once" · "I'm being pragmatic, TDD is dogmatic."
Read testing-anti-patterns.md to avoid common pitfalls.
| Excuse | Reality |
|---|---|
| "Too simple to test" | Simple code breaks. The test takes 30s. |
| "I'll test after" | Tests-after are biased by the implementation: "what does this do?" not "what should this?" |
| "Deleting hours of code is wasteful" | Sunk cost. Unverified code is debt. |
| "Test is hard to write" | Hard to test = hard to use. Listen to it; simplify the interface. |
| "Must mock everything" | Too coupled. Inject dependencies. |
| "Lint passed, TDD done" | Hooks check structure, not that you watched the failure. |
Don't know how to test → write the wished-for API and assertion first. Found a bug → write a failing test reproducing it, then run the full protocol. Never fix a bug without a test.