chore: bootstrap skills library — 19 skills + installer + CI auto-tag
Some checks failed
release / tag (push) Has been cancelled
Some checks failed
release / tag (push) Has been cancelled
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]
This commit is contained in:
294
atdd/SKILL.md
Normal file
294
atdd/SKILL.md
Normal file
@@ -0,0 +1,294 @@
|
||||
---
|
||||
name: atdd
|
||||
description: 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):**
|
||||
```go
|
||||
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:**
|
||||
```bash
|
||||
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.
|
||||
|
||||
```go
|
||||
// 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:**
|
||||
```bash
|
||||
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 `solid` skill
|
||||
|
||||
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.
|
||||
|
||||
```bash
|
||||
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
|
||||
//go:build acceptance
|
||||
|
||||
package service_test
|
||||
```
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```go
|
||||
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:
|
||||
|
||||
```go
|
||||
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-stories` skill output (stories with acceptance criteria)
|
||||
- During REFACTOR: load `clean-code` skill
|
||||
- For test quality: load `test-design` skill
|
||||
- For code review: load `code-review` skill
|
||||
- For refactoring guidance: load `refactoring` skill
|
||||
- Foundation: load `tdd` skill for core TDD methodology
|
||||
Reference in New Issue
Block a user