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

5.6 KiB

description
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

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:

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