Test-driven development
The red → green loop. This is the reference that makes the loop produce tests worth keeping.
This repo describes itself in .agents/, at the repository root. Read the files that bind your step,
and treat them as authority over anything you would otherwise assume.
| File |
Answers |
.agents/lifecycle.md |
which phases run, which review lenses are in use, how many rounds each loop gets, how wide a wave may be |
.agents/gates.md |
the commands that must pass, in order, and what "green" means here |
.agents/tracker.md |
where specs and tickets live, and what may be written to the board |
.agents/forge.md |
the git host, and how a branch becomes a reviewed change |
.agents/docs.md |
where architecture truth lives, the glossary, and what "docs are part of done" costs |
.agents/naming.md |
branch, commit, directory and test-name grammar |
.agents/working-agreement.md |
working hours, what may run unattended, how to report, when to stop and ask |
Three rules:
- A missing file is a real answer: that thing is not configured here. Say so in one line and take
the safest reading — do not invent the repo's conventions, and do not fall back on another project's.
If the whole directory is absent, stop and tell the user to run
/onboard instead of guessing.
.agents/ beats your own judgement, and loses to the user. It was written deliberately, so a rule
you disagree with is reported once, not routed around.
- Read only what your step needs. Every line costs on every turn of a long run.
Ground yourself first
- The glossary named in
.agents/docs.md. Test names and interface vocabulary use the project's
words. A test named for a synonym the glossary rules out is a rename waiting to happen.
- The accepted decisions and documented seams for the area you touch. They are constraints, not
suggestions.
.agents/naming.md for the test-name grammar, and .agents/gates.md for the test levels, the
test command and the coverage floor.
Seams — where tests go
A seam is the public boundary you observe behaviour at without reaching inside. Tests live at seams,
never against internals.
Test only at agreed seams. They are named in plan.md before any code is
written, and the ticket carries them. Do not invent one mid-test. A seam chosen while writing the
test is chosen to make that test easy, which is how a suite ends up pinned to the implementation it was
meant to be independent of.
No seam in your brief, or the one you were given cannot observe the behaviour? Say so and stop — that
is architectural-ambiguity, and it goes back to the plan. It is not yours to settle quietly.
Which level
Take the cheapest test level that can faithfully reproduce the failure mode. The levels, their costs
and which ones this repo keeps rare are in .agents/gates.md. If stubbing at a cheap level would
trivialise the assertion, either promote the test or rename it to match what it actually proves.
Rules of the loop
- Red before green. Write the failing test, watch it fail for the right reason, then write only
enough code to pass it. A test you never saw fail is a test you have not verified.
- One slice at a time. One seam, one test, one minimal implementation per cycle. Never write all the
tests and then all the code — bulk tests verify imagined behaviour and pin the shape of things
rather than what a caller does.
- Refactoring is not part of the loop. It belongs to review.
- Do not anticipate. No speculative feature, no test for a case the ticket did not ask for.
What a good test is
It verifies behaviour through a public interface, reads like a specification, and survives a refactor
because it does not care about internal structure.
- Name it per
.agents/naming.md. Enforced in review.
- Comment it per
.agents/naming.md, and nothing else. A test explaining itself in prose is a test
whose name is wrong.
- One logical assertion. Several assertion calls proving one behaviour is fine; two behaviours is two
tests.
The three anti-patterns
Tautological — the expected value is computed the way the code computes it, so it passes by
construction and can never disagree with the implementation.
// BAD — recomputes the implementation
const expected = items.reduce((n, i) => n + i.price, 0)
expect(total(items)).toBe(expected)
// GOOD — an independent, known literal
expect(total([{ price: 10 }, { price: 5 }])).toBe(15)
Never pin a constant to its own literal. Cover the value through the behaviour that uses it.
Implementation-coupled — mocks an internal collaborator, reaches a private member, or verifies
through a side channel (querying the table instead of reading back through the API). The tell: it
breaks on a refactor while behaviour is unchanged.
// BAD — bypasses the interface to verify
await createUser({ name: 'Alice' })
const row = await db.users.findOne({ name: 'Alice' })
// GOOD — verifies through the interface
const created = await createUser({ name: 'Alice' })
expect((await getUser(created.id)).name).toBe('Alice')
Horizontal slicing — all the tests, then all the code. Work in vertical slices instead, each test a
tracer bullet that responds to what the last cycle taught you.
Mocking
Mock at system boundaries only: an external API, the clock, randomness, sometimes the file system.
Prefer a real dependency through the repo's test harness over mocking the database.
Never mock your own types. An internal collaborator you control is not a boundary, and mocking it is
how a test becomes implementation-coupled.
Two rules that make a boundary easy to substitute:
- Inject the dependency, never construct it inside the unit.
- Prefer a purpose-named method per operation over one generic fetcher.
getUser(id) can be
substituted with one shape; send(endpoint, options) forces conditional logic into the stub, which is
a second implementation nobody reviews.
Control time explicitly through whatever abstraction the repo uses. Never read the system clock
directly in code under test.
Cover what you add
The floor and the tool that measures it are in .agents/gates.md.
Working test-first covers the paths the ticket is about. Then walk your own diff for the code around
them — a guard clause, an error path, an extracted helper — and report any added line no test executes.
You never reach the floor by widening an exclusion or by writing a test that passes by construction.
TDD exemptions
Work a test cannot precede: a generated client, pure configuration, documentation, a mechanical rename.
Name every exemption you use in your report. An unnamed exemption is indistinguishable from a skipped
test.
Running the tests
Use the command in .agents/gates.md, not the framework's bare runner. Where the two differ, the
configured one usually adds coverage and the exclusions the quality gate uses — and it is the only
faithful preview of what CI will say.
1---2name: tdd3description: The red-green loop, and what makes a test worth keeping — one test, one implementation, repeat, at seams that were agreed before any test was written. Use when building a feature or fixing a bug test-first, when someone says "red-green-refactor" or "write the test first", and by `ticket-implementer` on every ticket. Keywords - tdd, test first, red green, write the test first, failing test, seam, tautological test, what should I test.4---56# Test-driven development78The red → green loop. This is the reference that makes the loop produce tests worth keeping.910<!-- shared:repo-config:start source=repo-config.md -->11**This repo describes itself in `.agents/`, at the repository root. Read the files that bind your step,12and treat them as authority over anything you would otherwise assume.**1314| File | Answers |15|---|---|16| `.agents/lifecycle.md` | which phases run, which review lenses are in use, how many rounds each loop gets, how wide a wave may be |17| `.agents/gates.md` | the commands that must pass, in order, and what "green" means here |18| `.agents/tracker.md` | where specs and tickets live, and what may be written to the board |19| `.agents/forge.md` | the git host, and how a branch becomes a reviewed change |20| `.agents/docs.md` | where architecture truth lives, the glossary, and what "docs are part of done" costs |21| `.agents/naming.md` | branch, commit, directory and test-name grammar |22| `.agents/working-agreement.md` | working hours, what may run unattended, how to report, when to stop and ask |2324Three rules:2526- **A missing file is a real answer: that thing is not configured here.** Say so in one line and take27 the safest reading — do not invent the repo's conventions, and do not fall back on another project's.28 If the whole directory is absent, stop and tell the user to run `/onboard` instead of guessing.29- **`.agents/` beats your own judgement, and loses to the user.** It was written deliberately, so a rule30 you disagree with is reported once, not routed around.31- **Read only what your step needs.** Every line costs on every turn of a long run.32<!-- shared:repo-config:end -->3334## Ground yourself first3536- **The glossary named in `.agents/docs.md`.** Test names and interface vocabulary use the project's37 words. A test named for a synonym the glossary rules out is a rename waiting to happen.38- **The accepted decisions and documented seams** for the area you touch. They are constraints, not39 suggestions.40- **`.agents/naming.md`** for the test-name grammar, and **`.agents/gates.md`** for the test levels, the41 test command and the coverage floor.4243## Seams — where tests go4445A **seam** is the public boundary you observe behaviour at without reaching inside. Tests live at seams,46never against internals.4748**Test only at agreed seams.** They are named in [`plan.md`](../plan/SKILL.md) before any code is49written, and the ticket carries them. **Do not invent one mid-test.** A seam chosen while writing the50test is chosen to make that test easy, which is how a suite ends up pinned to the implementation it was51meant to be independent of.5253No seam in your brief, or the one you were given cannot observe the behaviour? **Say so and stop** — that54is `architectural-ambiguity`, and it goes back to the plan. It is not yours to settle quietly.5556## Which level5758Take the **cheapest test level that can faithfully reproduce the failure mode.** The levels, their costs59and which ones this repo keeps rare are in `.agents/gates.md`. If stubbing at a cheap level would60trivialise the assertion, either promote the test or rename it to match what it actually proves.6162## Rules of the loop6364- **Red before green.** Write the failing test, watch it fail for the right reason, then write only65 enough code to pass it. **A test you never saw fail is a test you have not verified.**66- **One slice at a time.** One seam, one test, one minimal implementation per cycle. Never write all the67 tests and then all the code — bulk tests verify *imagined* behaviour and pin the shape of things68 rather than what a caller does.69- **Refactoring is not part of the loop.** It belongs to review.70- **Do not anticipate.** No speculative feature, no test for a case the ticket did not ask for.7172## What a good test is7374It verifies behaviour through a public interface, reads like a specification, and survives a refactor75because it does not care about internal structure.7677- **Name it per `.agents/naming.md`.** Enforced in review.78- **Comment it per `.agents/naming.md`**, and nothing else. A test explaining itself in prose is a test79 whose name is wrong.80- **One logical assertion.** Several assertion calls proving one behaviour is fine; two behaviours is two81 tests.8283## The three anti-patterns8485- **Tautological** — the expected value is computed the way the code computes it, so it passes by86 construction and can never disagree with the implementation.8788 ```ts89 // BAD — recomputes the implementation90 const expected = items.reduce((n, i) => n + i.price, 0)91 expect(total(items)).toBe(expected)9293 // GOOD — an independent, known literal94 expect(total([{ price: 10 }, { price: 5 }])).toBe(15)95 ```9697 **Never pin a constant to its own literal.** Cover the value through the behaviour that uses it.9899- **Implementation-coupled** — mocks an internal collaborator, reaches a private member, or verifies100 through a side channel (querying the table instead of reading back through the API). The tell: it101 breaks on a refactor while behaviour is unchanged.102103 ```ts104 // BAD — bypasses the interface to verify105 await createUser({ name: 'Alice' })106 const row = await db.users.findOne({ name: 'Alice' })107108 // GOOD — verifies through the interface109 const created = await createUser({ name: 'Alice' })110 expect((await getUser(created.id)).name).toBe('Alice')111 ```112113- **Horizontal slicing** — all the tests, then all the code. Work in vertical slices instead, each test a114 tracer bullet that responds to what the last cycle taught you.115116## Mocking117118**Mock at system boundaries only**: an external API, the clock, randomness, sometimes the file system.119Prefer a real dependency through the repo's test harness over mocking the database.120121**Never mock your own types.** An internal collaborator you control is not a boundary, and mocking it is122how a test becomes implementation-coupled.123124Two rules that make a boundary easy to substitute:125126- **Inject the dependency**, never construct it inside the unit.127- **Prefer a purpose-named method per operation over one generic fetcher.** `getUser(id)` can be128 substituted with one shape; `send(endpoint, options)` forces conditional logic into the stub, which is129 a second implementation nobody reviews.130131**Control time explicitly** through whatever abstraction the repo uses. Never read the system clock132directly in code under test.133134## Cover what you add135136The floor and the tool that measures it are in `.agents/gates.md`.137138Working test-first covers the paths the ticket is about. **Then walk your own diff** for the code around139them — a guard clause, an error path, an extracted helper — and report any added line no test executes.140**You never reach the floor by widening an exclusion or by writing a test that passes by construction.**141142## TDD exemptions143144Work a test cannot precede: a generated client, pure configuration, documentation, a mechanical rename.145**Name every exemption you use in your report.** An unnamed exemption is indistinguishable from a skipped146test.147148## Running the tests149150Use the command in `.agents/gates.md`, not the framework's bare runner. Where the two differ, the151configured one usually adds coverage and the exclusions the quality gate uses — and it is the only152faithful preview of what CI will say.