--- 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.