---
name: tdd
description: Use when implementing any feature or bugfix, before writing implementation code, and when deciding what to test, where a seam goes, or whether a mock is warranted.
---

# Test-Driven Development

Write the test first. Watch it fail. Write the minimal code that passes it.

**If you did not watch the test fail, you do not know that it tests the right thing.** A test written
after the code passes immediately, which proves nothing: it may be testing the implementation, or the
wrong behaviour, or nothing at all.

**Violating the letter of the rule is violating its spirit.**

## The Iron Law

```
NO PRODUCTION CODE WITHOUT A FAILING TEST FIRST
```

Wrote the code first? Delete it and start over. Not kept as reference, not adapted while you write
the test, not consulted. Delete means delete, and implement fresh from the test.

Exceptions worth asking about: throwaway prototypes, generated code, config files. Thinking "skip it
just this once"? That thought is the rationalization, not the exception.

## Agree the seams before writing anything

A **seam** is the public boundary you test at: the interface where behaviour is observable without
reaching inside. Tests live at seams, never against internals.

**Test only at pre-agreed seams.** Before writing any test, name the seams under test and confirm
them:

> "The public interface here is `X`. I plan to test at `X` and `Y`, and not at `Z` because it is
> internal. Does that match what you expect?"

No test gets written at an unconfirmed seam. This is the whole mechanism that keeps a suite focused:
you cannot test everything, so agreeing the seams up front is what puts the effort on the critical
paths and the complex logic instead of spreading it thin across every edge case.

When the shape of that interface is itself the open question, how deep the module should be, where
the seam belongs, what the interface exposes, call the Skill tool with "codebase-design" and use that
vocabulary. It is a reference to consult, not a session to run.

## The loop

Run this once per behaviour. One seam, one test, one minimal implementation per cycle.

1. **RED: write one failing test.** One behaviour, a name that describes it, real code rather than
   mocks. See [TESTS.md](TESTS.md) for what a good one looks like.
2. **Verify RED. Mandatory, never skipped.** Run the project's test command for that single file.
   Confirm it *fails* rather than errors, that the message is the one you expected, and that it fails
   because the behaviour is missing rather than because of a typo. A test that passes immediately is
   testing behaviour that already exists: fix the test. A test that errors is broken: fix it and
   re-run until it fails correctly.
3. **GREEN: write the simplest code that passes.** Nothing speculative, no options objects for
   futures nobody asked for, no improving adjacent code.
4. **Verify GREEN. Mandatory.** The test passes, the other tests still pass, and the output is
   pristine with no stray errors or warnings. Test still failing? Fix the code, never the test.
5. **Tidy, then stop.** Remove duplication you just introduced and improve names, keeping every test
   green. **Design refactoring is not part of this loop**: reshaping a module, moving a seam, or
   extracting an abstraction belongs to review, not to the red-green cycle. Confusing the two is how
   a cycle turns into an afternoon.
6. **Repeat** with the next failing test.

Look up the test command rather than assuming one: `package.json` scripts are the source of truth,
and these repos are usually `bun test <path>` or `bunx vitest run <path>`.

## Work in vertical slices

**Horizontal slicing is the anti-pattern**: writing all the tests first, then all the implementation.
Bulk tests verify *imagined* behaviour. You end up testing the shape of things rather than what a
caller experiences, the tests go insensitive to real changes, and you commit to a test structure
before you understand the implementation.

Work vertically instead: one test, one implementation, repeat. Each test is a **tracer bullet** that
responds to what the last cycle taught you.

## Mocking

Mock at system boundaries only, never your own modules. See [MOCKING.md](MOCKING.md) for the boundary
list and how to design an interface that is easy to substitute at.

## Rationalizations

| Excuse | Reality |
|---|---|
| "Too simple to test" | Simple code breaks. The test takes 30 seconds. |
| "I'll test after" | Tests written after pass immediately, which proves nothing. You never watched it fail, so you never proved it can catch anything. |
| "Tests after achieve the same thing, spirit not ritual" | Tests-after answer "what does this do?"; tests-first answer "what should this do?" Writing them after biases you toward the cases you already remembered. |
| "I already tested it manually" | Manual testing leaves no record, cannot be re-run, and is the first thing dropped under pressure. |
| "Deleting hours of work is wasteful" | Sunk cost. That time is spent either way. Keeping code you cannot trust is the waste. |
| "I'll keep it as reference and write tests first" | You will adapt it, which is testing after. Delete means delete. |
| "I need to explore first" | Fine. Throw the exploration away and start with TDD. |
| "This is hard to test" | Listen to the test. Hard to test means hard to use: the design is talking to you. |
| "TDD will slow me down" | The shortcut means debugging in production, which is slower. |
| "The existing code has no tests" | You are improving it. Add them. |

## Red flags: stop and start over

- Code written before the test
- The test passed the first time you ran it
- You cannot explain why the test failed
- "I'll add tests later"
- "It's about the spirit, not the ritual"
- "Keep it as reference"
- "This one is different because..."

All of these mean the same thing: delete the code, start again with the test.

## When stuck

| Problem | What it means |
|---|---|
| Do not know how to test it | Write the API you wish existed, then the assertion. Ask if still stuck. |
| The test is too complicated | The design is too complicated. Simplify the interface. |
| You have to mock everything | The code is too coupled. Inject dependencies instead. |
| The setup is enormous | Extract helpers. Still enormous? The design needs work. |

## Fixing a bug

Never fix a bug without a test. Write the failing test that reproduces it, watch it fail, then fix.
The test proves the fix and prevents the regression. Where the bug is hard to reproduce at all, the
loop-building discipline is its own skill: call the Skill tool with "diagnosing-bugs" first, then come
back here once you have a red-capable repro.

## Done when

- Every new behaviour has a test that was watched failing, for the expected reason, before the code
  existed.
- Every test sits at a seam that was agreed before it was written.
- The full suite passes and the output is pristine.
- No test asserts on a mock of something you own, and no expected value was computed the way the
  code computes it.
