Files
dance-lessons-coach/adr/0025-bdd-scenario-isolation-strategies.md
Gabriel Radureau a24b4fdb3b 📝 docs(adr): homogenize 23 ADRs + rewrite README (Tâche 7 migration) (#18)
## Summary

Homogenize all 23 ADRs to a single canonical header format, and rewrite `adr/README.md` to match the actual state of the corpus.

This is **Tâche 7** of the ARCODANGE Phase 1 migration (Claude Code → Mistral Vibe). Independent from PR #17 (Tâche 6 — restructure AGENTS.md) — both can merge in any order. No code changes; only documentation.

## Changes

### 1. Homogenize 21 ADR headers (commit `db09d0a`)

The audit (Tâche 6 Phase A, Mistral intent-router agent, 2026-05-02) had identified **3 inconsistent header formats** :

- **F1** — list bullets (`* Status:` / `* Date:` / `* Deciders:`) : 11 ADRs (0001-0008, 0011, 0014, 0023)
- **F2** — bold fields (`**Status:**` / `**Date:**` / `**Authors:**`) : 9 ADRs (0009, 0010, 0012, 0013, 0015, 0016, 0017, 0018, 0019)
- **F3** — dedicated section (`## Status\n**Value** `) : 5 ADRs (0020, 0021, 0022, 0024, 0025)

Plus mixed metadata names (Authors / Deciders / Decision Date / Implementation Date / Implementation Status / Last Updated) and decorative emojis on status values made the corpus hard to scan or template against.

**Canonical format adopted** (see `adr/README.md` for full template) :

```markdown
# NN. Title

**Status:** <Proposed | Accepted | Implemented | Partially Implemented | Approved | Rejected | Deferred | Deprecated | Superseded by ADR-NNNN>
**Date:** YYYY-MM-DD
**Authors:** Name(s)

[optional **Field:** ... lines]

## Context...
```

**Transformations applied** (via `/tmp/homogenize-adrs.py` script, 23 files scanned, 21 modified — 0010 and 0012 were already conform) :

- F1 list bullets → bold fields
- F2 cleanup : `**Deciders:**` → `**Authors:**`, strip status emojis
- F3 sections : `## Status\n**Value** ` → `**Status:** Value` (single line)
- Strip decorative emojis from `**Status:**` and `**Implementation Status:**`
- Convert `* Last Updated:` / `* Implementation Status:` / `* Decision Drivers:` / `* Decision Date:` to bold
- Date typo fix : `2024-04-XX` → `2026-04-XX` for ADRs 0018, 0019 (off-by-2-years in original)
- Normalize multiple blank lines after header (max 1)

**ADR body content is preserved unchanged.** Only headers transformed.

### 2. Rewrite `adr/README.md` (commit `d64ab02`)

Previous README had multiple inconsistencies :

- Index table listed wrong titles for ADRs 0010-0021 (looked like an aspirational forecast that never matched reality — e.g. "0011 = Trunk-Based Development" but real 0011 is absent and Trunk-Based Development is actually 0017)
- Listed entries for ADRs 0011 (validation library) and 0014 (gRPC) but **these files do not exist** in the repo
- 0024 (BDD Test Organization) was missing from the detail list
- Template still showed the obsolete F1 format (`* Status:`)
- Decorative emojis on every status entry

Rewrite :

- Index table **regenerated from actual file contents** (title from H1, status from `**Status:**` line) — emoji-free, accurate
- Notes that 0011 / 0014 are not currently in use (reserved)
- Updated template block matches the canonical format
- Status Legend extended with `Approved`, `Partially Implemented`, `Deferred`
- Added note that 0026 is the next free number for new ADRs

## Test plan

- [x] All 23 ADRs follow `**Status:**` / `**Date:**` / `**Authors:**` (verified via grep)
- [x] No more occurrences of `* Status:` (F1) or `## Status` (F3) in any ADR header
- [x] No more emojis on `**Status:**` lines
- [x] `adr/README.md` index links resolve to existing files (no more 0011 / 0014 dead links)
- [x] Pre-commit hooks pass (`go mod tidy`, `go fmt`, `swag fmt`)

## Migration context

Part of Phase 1 of the ARCODANGE migration from Claude Code to Mistral Vibe. Tâche 7 of the curriculum.

Independent from PR #17 (which restructures `AGENTS.md`). The two PRs touch disjoint files — no merge conflict expected when both are merged.

🤖 Generated with [Claude Code](https://claude.com/claude-code) (Opus 4.7, 1M context). Mistral Vibe (intent-router agent / mistral-medium-3.5) did the original audit identifying the 3 formats during Tâche 6 Phase A.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Mistral Vibe (devstral-2 / mistral-medium-3.5)
Reviewed-on: #18
Co-authored-by: Gabriel Radureau <arcodange@gmail.com>
Co-committed-by: Gabriel Radureau <arcodange@gmail.com>
2026-05-03 11:01:13 +02:00

12 KiB

ADR 0025: BDD Scenario Isolation Strategies

Status: Proposed

Context

As our BDD test suite grows, we're encountering test pollution issues where scenarios interfere with each other through shared state. This is particularly problematic for:

  1. Database state: Scenarios create users, JWT secrets, config entries that persist across scenarios
  2. JWT secret rotation: Multiple secrets accumulate, affecting subsequent scenario authentication
  3. Config file modifications: Feature flag changes persist between tests
  4. Gherkin Background steps: Data set up in Background is visible to all scenarios in the feature

Our current approach clears database tables after each scenario, but this has race condition vulnerabilities with concurrent scenario execution.

Gherkin Background Consideration

Crucially, Gherkin's Background section runs before each scenario in a feature, not once before all scenarios. This means:

Feature: User registration
  Background:
    Given the database is empty
    And a default admin user exists
  
  Scenario: Register new user
    When I register user "alice"
    Then user "alice" should exist
  
  Scenario: Register duplicate user
    When I register user "alice"
    Then I should see error "user already exists"

The second scenario fails because Background creates data that persists, and the first scenario's data isn't cleaned up. Background steps are re-executed before each scenario.

Decision Drivers

  • Isolation: Each scenario must start with a clean slate
  • Performance: Cleanup must be fast enough for CI/CD pipelines
  • Concurrency: Must work with parallel scenario execution
  • Compatibility: Must work with Gherkin Background steps
  • Maintainability: Solution should be simple to understand and debug

Considered Options

Option 1: Transaction Rollback (Rejected )

Wrap each scenario in a database transaction, rollback at the end.

BeforeScenario: BEGIN;
AfterScenario: ROLLBACK;

Pros:

  • Simple implementation
  • Fast - transaction rollback is nearly instant
  • No data cleanup needed

Cons:

  • Fails if scenario commits: Nested transaction problem - COMMIT inside scenario releases the transaction, parent ROLLBACK has no effect
  • Cannot handle non-database state (JWT secrets in memory, config files)
  • Doesn't solve JWT secret pollution

Verdict: Not viable - Too many scenarios use database transactions internally.


Option 2: Clear Tables in Public Schema (Current /⚠️)

Delete all rows from all tables after each scenario.

AfterScenario: DELETE FROM table1; DELETE FROM table2; ...

Pros:

  • Currently implemented
  • Works with any scenario code
  • Handles database state

Cons:

  • ⚠️ Race conditions: Concurrent scenarios can interleave - Scenario A deletes data while Scenario B is still using it
  • ⚠️ Slow: Must delete from all tables, reset sequences
  • Misses in-memory state: JWT secrets, config changes persist
  • Doesn't handle Background: Background data is shared across scenarios

Verdict: Partially adequate - Works for sequential execution but has parallel execution issues.


Create a unique PostgreSQL schema for each scenario, drop it after.

BeforeScenario:
  schema := "test_" + sha256(scenario.Name)[:8]
  CREATE SCHEMA schema;
  SET search_path = schema, public;
  
AfterScenario:
  DROP SCHEMA schema CASCADE;

Pros:

  • True isolation: Each scenario has its own database namespace
  • Works with transactions: Scenario can commit freely - entire schema is dropped
  • Works with Background: Background runs in scenario's schema, data is isolated
  • Fast: Schema drop is instant (just metadata deletion)
  • Handles concurrent scenarios: Different schemas = no conflicts

Cons:

  • Requires CREATE/DROP SCHEMA database privileges in test environment
  • Some ORMs may hardcode public schema - need to use SET search_path carefully
  • Test DB must allow many schemas (typically fine for PostgreSQL)
  • We need to handle search_path in connection pooling (each scenario needs its own connection)

Implementation notes:

  • Use Luego (PostgreSQL schema prefix) approach: test_{hash}
  • Hash: sha256(feature_name + scenario_name)[:8] for consistency across runs
  • Execute Background steps in the scenario's schema context
  • Set search_path at the connection level, not globally

Option 4: Database-per-Feature ⚠️

Create a separate database for each feature file.

BeforeFeature: CREATE DATABASE feature_auth;
AfterFeature: DROP DATABASE feature_auth;

Pros:

  • Strong isolation between features
  • Simple implementation

Cons:

  • Doesn't isolate scenarios within a feature - Background data shared across scenarios
  • Database creation is slower than schema creation
  • Harder to manage in CI (more databases to create/cleanup)
  • Still need table clearing between scenarios within a feature

Verdict: Insufficient - Doesn't solve intra-feature pollution.


Option 5: Schema-per-Feature + Table Clearing per Scenario ⚠️

Create one schema per feature, clear tables between scenarios.

BeforeFeature: CREATE SCHEMA feature_auth;
AfterFeature: DROP SCHEMA feature_auth;
AfterScenario: DELETE FROM all_tables;

Pros:

  • Isolates features from each other
  • Simpler than per-scenario schemas

Cons:

  • Scenarios within a feature share state - Background data persists
  • Still has race conditions with concurrent scenarios in same feature
  • Requires table clearing overhead

Verdict: Better than current but still has issues.


Decision Outcome

Chosen option: Schema-per-Scenario + In-Memory State Reset + Per-Scenario Step State (Option 3 Enhanced)

We will implement schema-per-scenario because it:

  1. Provides true isolation for all database state
  2. Works with Gherkin Background - Background runs in each scenario's schema
  3. Handles concurrent execution - No race conditions
  4. Works with scenario transactions - Scenarios can commit freely
  5. Is fast - Schema operations are cheap

However, we discovered a critical limitation: PostgreSQL schemas only isolate database tables. In-memory state (application-level caches, user stores, JWT secret managers) persists across scenarios because they're stored in the shared sharedServer Go instance. Schema isolation does NOT solve this.

Enhanced Strategy: Multi-Layer Isolation

To achieve complete scenario isolation, we need a 3-layer approach:

Layer Component Strategy Status
DB PostgreSQL tables Schema-per-scenario Implemented
Memory Server-level state (JWT secrets) Reset to initial state Implemented
Memory Step-level state (tokens, user IDs) Per-scenario state map Implemented
Memory User store Reset/clear between scenarios ⚠️ TODO
Memory Auth cache Reset/clear between scenarios ⚠️ TODO
Cache Redis/Memcached Key prefix with schema hash ⚠️ TODO

Layer 3: Per-Scenario Step State Isolation

New insight from test failures: Step definition structs (AuthSteps, GreetSteps, etc.) maintain state in their fields:

  • lastToken, firstToken in AuthSteps
  • lastUserID in AuthSteps

This state spills across scenarios even with schema isolation, because struct fields are shared across all scenarios in a test process.

Solution: Create a ScenarioState manager with per-scenario isolation:

type ScenarioState struct {
    LastToken  string
    FirstToken string
    LastUserID uint
}

type scenarioStateManager struct {
    mu      sync.RWMutex
    states  map[string]*ScenarioState  // keyed by scenario hash
}

// Usage in step definitions:
func (s *AuthSteps) iShouldReceiveAValidJWTToken() error {
    state := steps.GetScenarioState(s.scenarioName)
    state.LastToken = extractedToken
    // ...
}

Benefits:

  • Zero code changes to step definitions (with helper functions)
  • Thread-safe (sync.RWMutex)
  • Consistent state per scenario
  • Automatic cleanup via BeforeScenario/AfterScenario hooks
  • Works with random test order

Status: Implemented in pkg/bdd/steps/scenario_state.go

Key Insight: Cache and In-Memory Store Isolation

For caches (Redis, Memcached, in-process):

  • Use schema hash as key prefix/suffix: cache_key_{schema_hash} or {schema_hash}_cache_key
  • This ensures each scenario gets isolated cache namespace
  • Works even with external cache services
  • Consistent with schema isolation philosophy

For in-memory stores (user repository, etc.):

  • Add Reset() methods that clear all state
  • Call in AfterScenario alongside schema teardown
  • Or use schema-prefix approach for shared stores

Alternative Approach: Background Explicit State Setup

Considered but rejected: Adding explicit "Given no user X exists" steps or heavy Background sections.

Pros: More readable, explicit about state Cons:

  • Error-prone (must remember for every entity)
  • Verbose (many Given steps)
  • Doesn't scale with many entities
  • Still has race conditions with concurrent scenarios

Verdict: Automated cleanup (schema drop + memory reset) is more reliable than manual Background setup.

Implementation Plan

Phase 1: Foundation ( Complete)

  • Add scenario-aware schema management to test server
  • Implement schema creation/drop in BeforeScenario/AfterScenario hooks
  • Handle search_path configuration for each scenario's database connection

Phase 2: In-Memory State Reset (🟡 TODO)

  • Add ResetUsers() method to clear in-memory user store
  • Add ResetCache() method for auth/rateLimiting caches
  • Call these in AfterScenario alongside JWT secret reset
  • Cache key strategy: key_{schema_hash} for all cache operations

Phase 3: Connection Pooling

  • Configure connection pool to respect per-scenario search_path
  • Each scenario gets isolated connections

Phase 4: Validation

  • Run full test suite to verify complete isolation
  • Fix any hardcoded public schema references

Schema Naming Convention

Schema name: test_{sha256(feature:scenario)[:8]}
Cache key prefix: {sha256(feature:scenario)[:8]}_

Example:

  • Feature: auth, Scenario: Successful user authentication
  • Hash: sha256("auth:Successful user authentication")[:8] = a3f7b2c1
  • Schema: test_a3f7b2c1
  • Cache key: a3f7b2c1_user:newuser instead of just user:newuser

Benefits:

  • Unique per scenario
  • Consistent across test runs (same scenario = same hash)
  • Short (8 chars) - efficient for cache keys
  • Identifiable for debugging

Schema Naming Convention

Schema name: test_{sha256(feature + scenario)[:8]}

Example:

  • Feature: auth, Scenario: Successful user authentication
  • Hash: sha256("auth_Successful user authentication")[:8] = a3f7b2c1
  • Schema: test_a3f7b2c1

Benefits:

  • Unique per scenario
  • Consistent across test runs (same scenario = same schema)
  • Short (8 chars + prefix = 14 chars max)
  • Identifiable for debugging

Pros and Cons Summary

Aspect Schema-per-Scenario Current (Clear Tables) Transaction Rollback
Isolation Strong ⚠️ Medium Weak
Works with Background Yes ⚠️ Partial No
Concurrency safe Yes No No
Works with TX Yes Yes No
Speed Fast ⚠️ Slow Fast
DB privileges ⚠️ Needs CREATE None None
Complexity ⚠️ Medium Low Low