Files
thamanyah/.claude/skills/atdd/SKILL.md
T

122 lines
5.6 KiB
Markdown

---
description: The ATDD loop this repo develops features with — write the Gherkin scenario first, watch it fail, write just enough code in cms/ to pass it, re-run, move to the next scenario. Use when adding or changing an endpoint, a validation rule, an error response, or any user-visible behaviour in cms/.
---
## The loop
One scenario at a time. Never write two scenarios' worth of code in one pass.
1. **Write the scenario** in `tests/features/*.feature`, in the language of the
feature rather than of HTTP where you can. Name it for the behaviour
(`Refusing to register a video whose file was never uploaded`), not the
mechanism.
2. **Run it and watch it fail.** `cd tests && go test -count=1 -v ./...`
Confirm it fails for the reason you expect. A scenario that passes before
you have written any code is testing nothing — go back to step 1.
3. **Write just enough code in `cms/`** to turn that one scenario green.
Not the next scenario's code. Not a generalisation you have no test for.
4. **Re-run the suite** — the whole file, not just the new scenario. Green
means done; a scenario that used to pass and now does not means the new
code changed published behaviour, which is a decision, not an accident.
5. **Next scenario.** Repeat until the feature is described.
Only then tidy: extract helpers, rename, split files. The suite is what makes
that safe, so do it with the tests green and re-run after.
## Where things go
Tests are **black box**. `tests/` is its own Go module (`thamanyah/tests`) that
imports nothing from `cms/` and speaks only HTTP. Do not add `_test.go` files
inside `cms/` — if a behaviour cannot be reached through the API, that is a
finding about the design, not a reason to reach past it.
| file | what goes in it |
|---|---|
| `tests/features/*.feature` | the scenarios |
| `tests/suite_test.go` | step registration, the per-scenario `world` |
| `tests/steps_test.go` | what each step does |
| `tests/client_test.go` | HTTP plumbing and wire types |
Production code keeps to the seams in `CLAUDE.md`: wire structs in
`internal/api`, domain types in `internal/models`, HTTP in `internal/handlers`,
SQL in `internal/db/repositories`, AWS in `internal/services`. "Just enough
code" still lands on the right seam — a handler that runs its own SQL is not
minimal, it is misplaced.
## Running it
```bash
docker compose up -d # localstack + infra + cms (needs LOCALSTACK_AUTH_TOKEN)
docker exec cms ./cms migrate # see Migrations below
cd tests && go test -count=1 -v ./...
```
`-count=1` defeats Go's test caching, which otherwise hides a re-run against a
changed service. `-v` prints the Gherkin: gobdd nests a subtest per feature,
scenario and step, so the step text appears in the output.
`CMS_BASE_URL` (default `http://localhost:8081`) points the suite elsewhere. If
nothing is serving, the suite skips rather than fails.
**Migrations.** Compose starts `cms` in server mode only — the migrate
container exists in the ECS task definition, not in `docker-compose.yml`. A new
migration is **not** applied by `docker compose watch` rebuilding the image.
Run `docker exec cms ./cms migrate` after adding one, or every scenario fails
on `relation "…" does not exist`.
**The spec.** Changing a handler, its annotations, or an `internal/api` struct
means regenerating the OpenAPI spec, or the build drifts from the published
contract:
```bash
$(go env GOPATH)/bin/swag init --generalInfo main.go --dir ./ --parseInternal --output ./docs
```
## Reading a red run
gobdd does **not** stop a scenario at the first failing step. A step that fails
leaves the later steps running against stale state, so one real failure
produces a cascade of noisy ones. **Read the first failure in a scenario and
ignore the rest** until it is fixed.
`cannot find step definition for step: …` is a legitimate red: the scenario is
written and the step is not. Add the function to `steps_test.go`, register it
in `TestVideoUpload`, and run again.
## Writing steps
- **Anchor every pattern** with `^…$`. gobdd matches unanchored and the
first registered pattern that matches wins, so an unanchored pattern can
silently shadow another step.
- State moves between steps through the `*world` in the gobdd context, never
package variables. It is held by **pointer** because gobdd clones the context
between the `Background` and the scenario steps.
- A `Scenario Outline` runs every example row in **one** world. Steps must
overwrite state, never accumulate it.
- Reuse a step before writing a new one. Two steps that differ only in wording
are two ways for the feature file to drift.
## Asserting errors
Errors are RFC 9457 Problem Details on `application/problem+json`. `title` is
held identical across every occurrence of a given problem so clients can branch
on it — so **assert the exact title**, not just the status. A new error
condition means a new stable title, and a new `@Failure` annotation on the
handler.
When a status alone would be ambiguous, distinguish the cases in code rather
than in the assertion: a client's mistake and a server fault reaching the same
`writeProblem` call is the bug, and a sentinel error in
`internal/handlers/errors.go` plus `errors.Is` at the call site is how this repo
separates them.
## `@known-gap`
Tag a scenario `@known-gap` when it pins what the service does **today** and
that differs from what it is documented to do. It still passes — it is a record,
not a failure — and it carries a comment saying what the contract promises and
what changing it would take. Fixing the code means changing the scenario in the
same commit and dropping the tag.
Do not use the tag to park a scenario you could not make pass.