Files
dance-lessons-coach/pkg/bdd/steps
Gabriel Radureau 4d2e0c1a42 feat(server): /api/info aggregator + frontend version footer
Sprint 2 of autonomous trainer day 2026-05-05. Mistral-implemented
through ICM workspace ship-info-aggregator (bootstrapped backend +
BDD before hitting price limit at stage 02), Claude-completed for
frontend + Playwright + verifier + PR.

Backend:
- GET /api/info aggregator returning version, commit_short, build_date,
  uptime_seconds, cache_enabled, healthz_status (single round trip)
- Optional cache via existing cache service (X-Cache: HIT/MISS)
- BDD scenario @critical covers happy path + version regex; cache
  scenario kept under @skip @bdd-deferred until BDD harness gains a
  cache-enabled mode

Frontend:
- AppFooterView (dumb) + AppFooter (smart wrapper, useFetch) following
  the HealthDashboard / HealthDashboardView pattern
- layouts/default.vue auto-applied via NuxtLayout in app.vue
- humaniseUptime helper in utils/
- Playwright tests use route.fulfill mocking (decoupled from dev-proxy
  infra), assert visible AND content (PR #32 lesson)

Docs:
- documentation/API.md /api/info entry with schema and rationale
- ADR-0026 documents composite endpoint vs separate calls choice

Verifier verdict (skill-driven, audit at stage 04): APPROVE_WITH_NITS.
Nits: handleInfo is 51 lines (could split into builder + emitter);
X-Cache: DISABLED could improve ops clarity.

Out-of-scope follow-up: existing tests/e2e/health.spec.ts happy path
hits the same dev-proxy infra issue as my footer happy path before
mocking. Same fix (server: false + route.fulfill) would apply.
2026-05-05 08:28:00 +02:00
..

BDD Steps Organization

This folder contains the step definitions for the BDD tests, organized by domain for better maintainability and scalability.

Structure

pkg/bdd/steps/
├── steps.go                  # Main registration file that ties everything together
├── scenario_state.go         # Per-scenario state isolation manager
├── common_steps.go           # Shared steps used across multiple domains
├── auth_steps.go             # Authentication and user management steps
├── config_steps.go           # Configuration and hot-reloading steps
├── greet_steps.go            # Greet-related steps (v1 and v2 API)
├── health_steps.go           # Health check and server status steps
├── jwt_retention_steps.go    # JWT secret retention policy steps
└── README.md                 # This file

Design Principles

  1. Domain Separation: Steps are grouped by functional domain
  2. Single Responsibility: Each file focuses on a specific area of functionality
  3. Reusability: Common steps are shared via common_steps.go
  4. Scalability: Easy to add new domains as the application grows
  5. State Isolation: Use per-scenario state to prevent pollution between test scenarios

Adding New Steps

  1. For new domains: Create a new *_steps.go file following the existing pattern
  2. For existing domains: Add to the appropriate domain file
  3. For shared functionality: Add to common_steps.go
  4. Register all steps: Update steps.go to include the new steps

Step Naming Convention

  • Use descriptive, action-oriented names
  • Follow the pattern: i[Action][Object] or the[Object][State]
  • Example: iRequestAGreetingFor, theAuthenticationShouldBeSuccessful
  • Use present tense for actions: "I authenticate", "the server reloads"

State Isolation Pattern

Problem: Step definition structs (AuthSteps, GreetSteps, etc.) maintain state in their fields (e.g., lastToken, lastUserID). This state persists across all scenarios in a test process, causing pollution even with database schema isolation.

Solution: Use the ScenarioState manager for per-scenario state isolation.

How It Works

The scenario_state.go provides a thread-safe mechanism to store and retrieve state that is isolated per scenario:

// Get scenario-specific state
state := steps.GetScenarioState(scenarioName)

// Store scenario-specific data
state.LastToken = token
state.LastUserID = userID

// Retrieve scenario-specific data
token := state.LastToken

Usage in Step Definitions

Instead of storing state in struct fields:

// ❌ NOT RECOMMENDED - state shared across all scenarios
type AuthSteps struct {
    client     *testserver.Client
    lastToken  string  // Shared across all scenarios!
    lastUserID uint    // Shared across all scenarios!
}

func (s *AuthSteps) iShouldReceiveAValidJWTToken() error {
    s.lastToken = extractedToken  // Pollutes other scenarios
    return nil
}

Use per-scenario state:

// ✅ RECOMMENDED - state isolated per scenario
type AuthSteps struct {
    client     *testserver.Client
    scenarioName string  // Track current scenario for state isolation
}

func (s *AuthSteps) iShouldReceiveAValidJWTToken() error {
    state := steps.GetScenarioState(s.scenarioName)
    state.LastToken = extractedToken  // Isolated to this scenario
    return nil
}

Integration with Suite Hooks

Clear state in AfterScenario to prevent memory growth:

sc.AfterScenario(func(s *godog.Scenario, err error) {
    scenarioKey := s.Name
    if s.Uri != "" {
        scenarioKey = fmt.Sprintf("%s:%s", s.Uri, s.Name)
    }
    steps.ClearScenarioState(scenarioKey)
})

ScenarioState Structure

The ScenarioState struct contains common fields needed across step definitions:

type ScenarioState struct {
    LastToken  string
    FirstToken string
    LastUserID uint
    // Add more fields as needed for other step types
}

If you need additional scenario-scoped fields, add them to the ScenarioState struct.

Testing the Steps

Run BDD tests with:

# Run all features
go test ./features/... -v

# Run specific feature
go test ./features/auth -v

# Run with state tracing enabled
BDD_TRACE_STATE=1 go test ./features/auth -v

# Validate full test suite
./scripts/validate-test-suite.sh 1

State Cleanup Strategy

Cleanup Level When What Implementation
Per-Scenario After each scenario Step struct fields ClearScenarioState()
Per-Scenario After each scenario Database state CleanupDatabase() (if no schema isolation)
Per-Scenario After each scenario Schema DROP SCHEMA (if schema isolation enabled)
Per-Process After each feature test Server-level state ResetJWTSecrets()
Per-Suite After all scenarios All state Server restart

Best Practices

1. Use Per-Scenario State for Shared Data

Any data that:

  • Is modified during scenario execution
  • Affects subsequent steps in the same scenario
  • Should NOT affect other scenarios

Use: GetScenarioState(scenarioName).Field

2. Keep Step Definitions Stateless Where Possible

If a step doesn't need to store intermediate state, don't store it:

// ✅ Good - stateless
func (s *GreetSteps) iRequestAGreetingFor(name string) error {
    return s.client.Request("GET", fmt.Sprintf("/api/v1/greet/%s", name), nil)
}

// ❌ Avoid - unnecessary state
func (s *GreetSteps) iRequestAGreetingFor(name string) error {
    s.lastGreetedName = name  // Unnecessary unless used later
    return s.client.Request("GET", fmt.Sprintf("/api/v1/greet/%s", name), nil)
}

3. Prefix Config Files Per-Scenario

If your scenario modifies config files, use scenario-specific paths:

configPath := fmt.Sprintf("features/%s/%s-scenario-%s.yaml", 
    feature, feature, scenarioKey)

4. Document Dependencies

If a step depends on state set by another step, document it:

// Step: The user should have a valid JWT token
// Requires: iAuthenticateWithUsernameAndPassword to have been called first
func (s *AuthSteps) theUserShouldHaveAValidJWTToken() error {
    state := steps.GetScenarioState(s.scenarioName)
    if state.LastToken == "" {
        return fmt.Errorf("no token found - did you authenticate first?")
    }
    // Verify token is valid...
}

Future Domains

As the application grows, consider adding:

  • payment_steps.go - Payment processing steps
  • notification_steps.go - Notification and email steps
  • admin_steps.go - Admin-specific functionality steps
  • api_steps.go - General API interaction patterns
  • user_steps.go - User profile and management steps (if auth gets complex)

Troubleshooting

State Pollution Between Scenarios

Symptom: Tests pass individually but fail when run together

Check:

  1. Are you using struct fields to store state? → Use ScenarioState instead
  2. Are database tables being cleaned up? → Verify CleanupDatabase() or schema isolation
  3. Are JWT secrets being reset? → Verify ResetJWTSecrets() is called

Debug: Enable state tracing:

BDD_TRACE_STATE=1 go test ./features/auth -v

Timeout or Delay Issues

Symptom: Config reloading tests fail intermittently

Cause: Server monitors config files every 1 second

Fix: Add delays >1100ms after config file changes:

time.Sleep(1100 * time.Millisecond)  // Wait for monitoring cycle

Missing Step Definitions

Symptom: undefined step error

Check:

  1. Step is defined in the appropriate *_steps.go file
  2. Step is registered in steps.go
  3. Step regex matches the feature file text exactly
  4. No typos in the step name

Tip: Run with -v to see which step is undefined