Phase 1 of mathias/skills extraction (infra#62 Track D — homelab next-step plan addendum). Imports ~/dev/.skills/ verbatim (19 skill dirs + SKILLS_INDEX.md) and adds the installation surface: - Taskfile.yml — install / update / list / release / check targets - install.sh — bootstrap installer for hosts without Task. Idempotent symlink wirer; default checkout at ~/.local/share/skills/ on every host; SKILLS_REF env var pins a tag (default: main). - .gitea/workflows/release.yml — auto-tag every push to main by Bump-Type footer (major/minor/patch, default patch). Skipped when commit contains [skip-release]. - README — usage, versioning, contribution flow, secret-hygiene rule. Phase 1 wires Claude Code only (~/.claude/skills/<name> global + <repo>/.claude/skills/<name> per-repo). Phase 2 adds Crush, opencode, antigravity, and gitea-resident agents (cobalt-dingo, agentsquad) once their skill conventions are researched. Public repo, markdown-only — no secrets, no client names. Verified via pre-push grep before initial push. [skip-release]
8.4 KiB
name, description
| name | description |
|---|---|
| atdd | Implement user stories using the ATDD workflow — Red (failing acceptance test) → Green (minimal implementation) → Refactor. One story at a time. |
Acceptance Test-Driven Development (ATDD)
Overview
ATDD extends TDD to the acceptance level. You write tests that capture the behavior described in a user story's acceptance criteria — then implement just enough to make them pass.
Input required: User stories with clear acceptance criteria. Load user-stories skill first.
Core Methodology: Red-Green-Refactor at Acceptance Level
RED → Write failing acceptance test (one story at a time)
GREEN → Write minimal code to make it pass
REFACTOR → Clean up code (load clean-code skill)
COMMIT → Commit with reference to the story
Key principle: Never mix phases. RED is only writing tests. GREEN is only making tests pass. REFACTOR is only cleaning up.
Stop and confirm before advancing from RED to GREEN, and from GREEN to REFACTOR.
Working on One Story at a Time
Pick the highest-priority story from the backlog. Do not start a second story until the first is done (committed, clean, all tests green).
Phase 1: RED — Write Failing Acceptance Test
Translate the story's acceptance criteria into executable tests.
For each acceptance criterion:
Given [context]
When [action]
Then [observable outcome]
Becomes a test case.
Go ATDD Example
Story: "Users can register with email and password"
Acceptance criteria:
- Given valid email and password, registration succeeds and returns the new user
- Given an email already in use, registration returns ErrDuplicateEmail
- Given an invalid email format, registration returns ErrInvalidEmail
- Given a password shorter than 8 chars, registration returns ErrWeakPassword
Acceptance tests (write these BEFORE implementation):
func TestUserRegistration_Acceptance(t *testing.T) {
tests := []struct {
name string
email string
pass string
wantErr error
}{
{
name: "valid registration creates user",
email: "alice@example.com",
pass: "securePass1!",
wantErr: nil,
},
{
name: "duplicate email returns ErrDuplicateEmail",
email: "existing@example.com",
pass: "securePass1!",
wantErr: ErrDuplicateEmail,
},
{
name: "invalid email format returns ErrInvalidEmail",
email: "not-an-email",
pass: "securePass1!",
wantErr: ErrInvalidEmail,
},
{
name: "short password returns ErrWeakPassword",
email: "alice@example.com",
pass: "short",
wantErr: ErrWeakPassword,
},
}
store := NewInMemoryUserStore()
// Pre-seed duplicate email
store.Seed(User{Email: "existing@example.com"})
svc := NewUserService(store)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := svc.Register(context.Background(), tt.email, tt.pass)
if tt.wantErr != nil {
assert.ErrorIs(t, err, tt.wantErr)
} else {
assert.NoError(t, err)
}
})
}
}
Run and verify they fail:
go test -run TestUserRegistration_Acceptance ./...
# FAIL: UserService.Register undefined
The test must fail because the implementation doesn't exist yet. If it passes, you're testing existing behavior. Check your test.
STOP here. Confirm before moving to GREEN.
Phase 2: GREEN — Minimal Implementation
Implement the absolute minimum code necessary to make the acceptance tests pass. Ugly code is acceptable at this stage. The goal is green, not clean.
// Minimal implementation — prioritize passing tests over elegance
func (s *UserService) Register(ctx context.Context, email, password string) (User, error) {
if !strings.Contains(email, "@") {
return User{}, ErrInvalidEmail
}
if len(password) < 8 {
return User{}, ErrWeakPassword
}
if s.store.Exists(ctx, email) {
return User{}, ErrDuplicateEmail
}
u := User{ID: newID(), Email: email}
if err := s.store.Save(ctx, u); err != nil {
return User{}, fmt.Errorf("save user: %w", err)
}
return u, nil
}
Verify GREEN:
go test -run TestUserRegistration_Acceptance ./...
# ok
go test ./... # All other tests still pass
go test -race ./...
STOP here. Confirm before moving to REFACTOR.
Phase 3: REFACTOR — Clean Up
With tests green, clean the code. Do not change behavior.
During REFACTOR, load and follow the clean-code skill.
Apply:
- Extract functions for complex logic
- Better naming
- Remove duplication
- Apply SOLID principles
- For design decisions, load
solidskill
After every change: go test ./... must stay green.
Do NOT refactor the acceptance tests themselves unless they have a bug. The tests are your specification — changing them changes what you've committed to.
STOP here. Confirm before committing.
Phase 4: COMMIT
Commit the completed story. Reference the story in the commit message.
git commit -m "feat: user registration with email validation
Implements acceptance criteria for US-001 (User Registration).
- Valid email/password creates a user
- Duplicate email returns ErrDuplicateEmail
- Invalid email format returns ErrInvalidEmail
- Short password returns ErrWeakPassword"
For Test Design Questions
When writing acceptance tests, load the test-design skill to ensure:
- Tests are understandable (Farley: Understandable)
- Tests don't couple to implementation details (Farley: Maintainable)
- Tests are repeatable across environments (Farley: Repeatable)
For Code Review Before Commit
Load the code-review skill before committing to check:
- Error handling follows
fmt.Errorf("op: %w", err)pattern - Context propagation is correct
- No race conditions in concurrent code
- Exported API is minimal
For Refactoring Guidance
Load the refactoring skill for systematic techniques to apply during REFACTOR phase.
For Clean Code Principles
Load the clean-code skill during REFACTOR phase to apply:
- Naming conventions
- Function size and responsibility
- SOLID principles
Go-Specific ATDD Notes
Acceptance Test Location
Place acceptance tests in _test.go files with the _acceptance_test.go suffix or //go:build acceptance build tag to separate from fast unit tests:
//go:build acceptance
package service_test
go test ./... # Unit tests only (fast)
go test -tags=acceptance ./... # Include acceptance tests
In-Memory Implementations for Acceptance Tests
Use in-memory implementations of repositories for acceptance tests:
type InMemoryUserStore struct {
mu sync.Mutex
users map[string]User
}
func (s *InMemoryUserStore) Save(ctx context.Context, u User) error {
s.mu.Lock()
defer s.mu.Unlock()
if _, exists := s.users[u.Email]; exists {
return ErrDuplicateEmail
}
s.users[u.Email] = u
return nil
}
func (s *InMemoryUserStore) Exists(ctx context.Context, email string) bool {
s.mu.Lock()
defer s.mu.Unlock()
_, exists := s.users[email]
return exists
}
This keeps acceptance tests fast and deterministic.
Given-When-Then in Go
The t.Run structure maps naturally to Given-When-Then:
t.Run("given valid credentials, when logging in, then returns token", func(t *testing.T) {
// Given: valid user exists
store := InMemoryUserStore{...}
// When: login is called
token, err := svc.Login(ctx, email, pass)
// Then: token is returned
require.NoError(t, err)
assert.NotEmpty(t, token)
})
Verification Checklist
Before committing:
- All acceptance tests pass
- All existing tests still pass
- Race detector clean:
go test -race ./... - REFACTOR phase complete — code is clean
- Commit message references the user story
- No behavior added beyond the acceptance criteria
Cross-References
- Requires:
user-storiesskill output (stories with acceptance criteria) - During REFACTOR: load
clean-codeskill - For test quality: load
test-designskill - For code review: load
code-reviewskill - For refactoring guidance: load
refactoringskill - Foundation: load
tddskill for core TDD methodology