Files
foxhunt/TEST_ISOLATION_FRAMEWORK_DESIGN.md
jgrusewski 83629f9ca8 feat(deployment): Complete Runpod GPU deployment infrastructure
Implement comprehensive Runpod deployment with S3 volume mount architecture for
FP32 ML model training on Tesla V100 GPUs.

## Infrastructure Components

### Deployment Scripts (scripts/)
- runpod_deploy.sh: Master deployment orchestrator (8-step workflow)
- runpod_upload.sh: S3 upload for binaries and test data
- upload_env_to_runpod.sh: Secure .env credentials upload
- runpod_deploy_test.sh: Prerequisites validation

### Docker Configuration
- Dockerfile.runpod: Multi-stage CUDA 12.1 runtime (~2GB, no binaries)
- entrypoint.sh: Volume verification and training execution
- Architecture: Volume mount (NO S3 downloads in pods)

### S3 Configuration
- Bucket: se3zdnb5o4 (Iceland region: eur-is-1)
- Endpoint: https://s3api-eur-is-1.runpod.io
- Structure: binaries/, test_data/, models/, .env

### OpenTofu Infrastructure (terraform/runpod/)
- main.tf: Pod and volume resources
- variables.tf: Configuration variables
- outputs.tf: Pod connection info
- Security: NO credentials in state (uses volume .env)

## Deployment Assets Uploaded

### Training Binaries (77MB)
- train_tft_parquet (23M) - TFT-225 features
- train_mamba2_parquet (22M) - MAMBA-2 state space
- train_dqn (22M) - Deep Q-Network
- train_ppo (13M) - Proximal Policy Optimization

### Test Data (13.8 MB)
- 9 Parquet files: ES.FUT, NQ.FUT, 6E.FUT, ZN.FUT (180-day datasets)

### Credentials
- .env file (1.5 KB, private access, chmod 600)

## Documentation

### Deployment Guides
- RUNPOD_DEPLOYMENT_READY_SUMMARY.md: Complete deployment status
- RUNPOD_VOLUME_DEPLOYMENT_GUIDE.md: Step-by-step guide (42KB)
- RUNPOD_DEPLOYMENT_QUICK_START.md: Quick reference
- RUNPOD_UPLOAD_GUIDE.md: S3 upload instructions
- RUNPOD_VOLUME_CONFIGURATION_COMPLETE.md: S3 setup report
- RUNPOD_S3_PARQUET_UPLOAD_REPORT.md: Data upload verification

### Architecture Documentation
- RUNPOD_VOLUME_MOUNT_ARCHITECTURE.md: Volume mount design
- RUNPOD_S3_ARCHITECTURE_DIAGRAM.txt: S3 API vs filesystem access
- DOCKERFILE_RUNPOD_FINAL_SUMMARY.md: Docker image specification

### Decision Documentation
- RUNPOD_DEPLOYMENT_CHECKLIST.md: Go/no-go decision matrix (27KB)
- RUNPOD_DEPLOYMENT_DECISION_TREE.md: Decision workflow
- FP32_RUNPOD_DEPLOYMENT_READY.md: FP32 deployment readiness

## QAT Enhancements

### Core QAT Infrastructure
- ml/src/memory_optimization/qat.rs: Enhanced QAT observer (+226 lines)
- ml/src/memory_optimization/auto_batch_size.rs: OOM recovery (+84 lines)
- ml/src/tft/qat_tft.rs: QAT TFT wrapper (+154 lines)
- ml/src/trainers/tft.rs: QAT training integration (+433 lines)
- ml/src/qat_metrics_exporter.rs: NEW - QAT metrics export

### QAT Testing
- ml/tests/qat_integration_tests.rs: NEW - Integration test suite
- ml/tests/qat_gradient_clipping_test.rs: NEW - Gradient clipping tests
- ml/tests/qat_device_consistency_test.rs: Device mismatch tests (+205 lines)
- ml/tests/qat_accuracy_validation_test.rs: Accuracy validation
- ml/tests/qat_tft_integration_test.rs: TFT QAT integration

### QAT Documentation
- ml/docs/QAT_GUIDE.md: Comprehensive QAT guide (+616 lines)
- ml/docs/QAT_GRADIENT_CHECKPOINTING_WORKAROUND.md: NEW - Workaround guide
- QAT_BLOCKERS_ROOT_CAUSE_ANALYSIS.md: P0 blocker analysis (44KB)
- QAT_ACCURACY_VALIDATION_REPORT.md: Accuracy comparison
- QAT_GRADIENT_CLIPPING_VALIDATION_REPORT.md: Clipping validation

### QAT Monitoring
- config/grafana/dashboards/qat-training-metrics.json: NEW - Grafana dashboard

## AWS CLI Configuration

### Credentials Setup
- ~/.aws/credentials: Runpod profile configured
  - Access Key: user_2xxA3XcIFj16yfL3aBon9niiSpr
  - Secret Key: (from RUNPOD_S3_SECRET)
- ~/.aws/config: Iceland region (eur-is-1)

## Production Readiness

### FP32 Models:  READY FOR DEPLOYMENT
- DQN: 15-20s training, ~6MB GPU memory
- PPO: 7-10s training, ~145MB GPU memory
- MAMBA-2: 2-3 min training, ~164MB GPU memory
- TFT-225: 3-5 min training, ~500MB GPU memory
- Total GPU Budget: 815MB (fits on 4GB+ Tesla V100)

### QAT Models: 🔴 BLOCKED
- 24 tests implemented but DO NOT COMPILE (11 errors)
- 3 P0 blockers: device mismatch, gradient checkpointing, OOM recovery
- Timeline: 1-2 weeks to fix (13h P0 fixes + validation)

### Wave D Features:  OPERATIONAL
- 225 features fully integrated
- Feature extraction: 5.10μs/bar (196x faster than target)
- Wave D backtest: Sharpe 2.00, Win Rate 60%, Drawdown 15%
- Database migration 045: Applied cleanly, zero conflicts

## Cost Analysis

### One-Time Setup
- Network Volume: $4/month (50GB SSD)
- Upload costs: FREE (S3 API included)

### Per Training Run (TFT-225)
- GPU: Tesla V100-PCIE-16GB @ $0.29/hr
- Training Time: ~4 hours
- Cost per run: $1.16

### Monthly (20 Training Runs)
- Storage: $4.00/month
- Training: $23.20/month (20 runs × $1.16)
- Total: $27.20/month

## Security

### Credentials Management
-  NO credentials in Docker image
-  NO credentials in Terraform state
-  .env gitignored and not committed
-  .env file private on S3 (HTTP 401 on public access)
-  Docker Hub repository PRIVATE (jgrusewski/foxhunt)

### Access Control
- S3 API: Local client uploads only
- Volume mount: Pod filesystem access only
- Authentication: AWS CLI with Runpod profile required

## Next Steps

1.  COMPLETE: Build Docker image
2.  PENDING: Push to Docker Hub
3.  PENDING: Deploy pod via Runpod console
4.  PENDING: Validate training on Tesla V100

## Performance Targets

- Build time: 5-10 min
- Upload time: ~20 sec (90MB total)
- Pod startup: ~30 sec
- Training time: 3-5 min (TFT-225)
- Total deployment: ~40 min from start to first training run

## Test Status

- FP32 tests: 597/608 passing (98.2%)
- QAT tests: 0/24 passing (compilation errors)
- Overall: 2,062/2,086 passing (98.8% excluding QAT)

🤖 Generated with Claude Code (https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-24 01:11:43 +02:00

47 KiB

Test Isolation Framework Design

Version: 1.0
Date: 2025-10-23
Author: Claude (Agent 9)
Status: Design Complete - Ready for Implementation


Executive Summary

Problem: Database race conditions in parallel test execution prevent reliable CI/CD and cause intermittent test failures.

Solution: Hybrid test isolation framework with three strategies:

  • Transaction rollback for unit tests (fast path)
  • Schema-per-test for integration tests (balanced)
  • Testcontainers for E2E tests (full isolation)

Benefits:

  • Zero race conditions in parallel test execution
  • <10% performance overhead vs. current baseline
  • Scalable to 10,000+ tests without conflicts
  • Minimal test code changes (single attribute macro)

Investment: 3-week implementation timeline across 8 phases

ROI: 10-month payback period


Table of Contents

  1. Requirements & Goals
  2. Architectural Design
  3. Implementation Details
  4. Migration Strategy
  5. Implementation Plan
  6. Risk Management
  7. CI/CD Integration
  8. Success Criteria
  9. Long-Term Maintenance
  10. ROI Analysis
  11. Appendices

1. Requirements & Goals

1.1 Functional Requirements

ID Requirement Description
R1 Isolated database state Each test has independent database state (no cross-test pollution)
R2 Parallel execution support Tests can run concurrently without coordination
R3 Automatic cleanup Database state is cleaned up after test completion (success or failure)
R4 Migration state management Each test starts with full schema (all 45 migrations applied)
R5 Minimal test code changes Existing tests require <50 LOC changes per file

1.2 Non-Functional Requirements

ID Requirement Target Measurement
NFR1 Performance overhead <10% Total test suite time: 208s → <229s
NFR2 Zero false positives 100% No isolation-related test failures
NFR3 Test type coverage All Unit + integration + E2E tests supported
NFR4 CI/CD compatible 100% Works in GitHub Actions, local Docker Compose
NFR5 Error messages Clear Developer-friendly isolation failure diagnostics

1.3 Constraints

  • Database: PostgreSQL 14+ with TimescaleDB extension
  • Test count: 2,086 existing tests to migrate
  • Tooling: Rust ecosystem only (SQLX, testcontainers-rs, tokio)
  • Environment: Docker Compose for local development
  • Migrations: 45 applied migrations (045_regime_detection.sql latest)

2. Architectural Design

2.1 Pattern Evaluation Matrix

Four isolation patterns were evaluated:

Pattern Overhead Isolation Multi-Connection DDL Support Verdict
Testcontainers 2-5s Perfect Yes Yes E2E only
Transaction Rollback <1ms Good No No Unit tests
Schema-per-Test 10-50ms Perfect Yes Yes Integration tests
Hybrid (Recommended) <10% avg Perfect Yes Yes All test types

Decision: Use hybrid approach to optimize for common case (unit tests) while supporting complex scenarios (integration/E2E).

2.2 Hybrid Strategy Breakdown

Test Distribution & Overhead Analysis
======================================

┌─────────────────┬──────────┬──────────────┬──────────┬──────────┐
│ Test Type       │ Count    │ Strategy     │ Overhead │ Total    │
├─────────────────┼──────────┼──────────────┼──────────┼──────────┤
│ Unit Tests      │ ~1,500   │ Transaction  │ <1ms     │ 1.5s     │
│ Integration     │ ~500     │ Schema       │ ~30ms    │ 15s      │
│ E2E Tests       │ ~50      │ Container    │ ~3s      │ 150s     │
│ No DB Access    │ ~36      │ None         │ 0ms      │ 0s       │
├─────────────────┼──────────┼──────────────┼──────────┼──────────┤
│ TOTAL           │ 2,086    │ Hybrid       │ N/A      │ 166.5s   │
└─────────────────┴──────────┴──────────────┴──────────┴──────────┘

Baseline test suite time: 208s
With isolation overhead: 208s + 166.5s = 374.5s (80% increase)
With schema pooling: 208s + 18.5s = 226.5s (9% increase) ✓ MEETS TARGET

2.3 Component Architecture

foxhunt/
├── common/
│   └── test_utils/                    # NEW: Test isolation framework
│       ├── lib.rs                     # Public API exports
│       ├── isolation.rs               # IsolationStrategy trait
│       ├── transaction.rs             # TransactionStrategy impl
│       ├── schema.rs                  # SchemaStrategy + SchemaAwareMigrator
│       ├── container.rs               # ContainerStrategy impl
│       ├── schema_pool.rs             # Schema pooling (85% faster)
│       ├── isolated_pool.rs           # Multi-connection support
│       ├── cleanup.rs                 # Orphaned schema cleanup
│       └── metrics.rs                 # Performance monitoring
├── common_macros/                     # NEW: Proc macro crate
│   ├── Cargo.toml                     # proc-macro = true
│   └── src/
│       └── lib.rs                     # #[foxhunt_test] attribute macro
└── tests/
    └── test_isolation_framework/      # Framework integration tests

2.4 Core Trait Design

// common/test_utils/isolation.rs

use async_trait::async_trait;
use sqlx::{PgPool, Postgres, Transaction};

/// Represents an isolated database environment for testing
#[async_trait]
pub trait IsolationStrategy: Send + Sync {
    /// Setup isolation before test runs
    async fn setup(&self, pool: &PgPool) -> Result<IsolationContext, IsolationError>;
    
    /// Cleanup isolation after test completes
    async fn cleanup(&self, ctx: IsolationContext) -> Result<(), IsolationError>;
    
    /// Get executor for running queries in isolated environment
    fn executor<'a>(&'a self, ctx: &'a IsolationContext) -> Box<dyn Executor + 'a>;
}

/// Context containing isolation state
pub struct IsolationContext {
    pub strategy_type: StrategyType,
    pub schema_name: Option<String>,
    pub transaction: Option<Transaction<'static, Postgres>>,
    pub container_port: Option<u16>,
    pub pool: PgPool,
}

/// Strategy type enum
#[derive(Debug, Clone, Copy)]
pub enum StrategyType {
    Transaction,
    Schema,
    Container,
}

/// Unified error type for isolation failures
#[derive(Debug, thiserror::Error)]
pub enum IsolationError {
    #[error("Database error: {0}")]
    Database(#[from] sqlx::Error),
    
    #[error("Schema creation failed: {0}")]
    SchemaCreation(String),
    
    #[error("Container startup failed: {0}")]
    ContainerStartup(String),
    
    #[error("Migration failed: {0}")]
    Migration(String),
}

3. Implementation Details

3.1 Transaction Strategy (Unit Tests)

Use Case: Single-connection unit tests with no DDL statements

Implementation:

// common/test_utils/transaction.rs

pub struct TransactionStrategy;

#[async_trait]
impl IsolationStrategy for TransactionStrategy {
    async fn setup(&self, pool: &PgPool) -> Result<IsolationContext, IsolationError> {
        let tx = pool.begin().await?;
        
        Ok(IsolationContext {
            strategy_type: StrategyType::Transaction,
            transaction: Some(tx),
            schema_name: None,
            container_port: None,
            pool: pool.clone(),
        })
    }
    
    async fn cleanup(&self, mut ctx: IsolationContext) -> Result<(), IsolationError> {
        if let Some(tx) = ctx.transaction.take() {
            tx.rollback().await?; // Explicit rollback
        }
        Ok(())
    }
    
    fn executor<'a>(&'a self, ctx: &'a IsolationContext) -> Box<dyn Executor + 'a> {
        Box::new(TransactionExecutor {
            tx: ctx.transaction.as_ref().unwrap(),
        })
    }
}

Test Migration Example:

// BEFORE
#[tokio::test]
async fn test_order_validation() {
    let pool = get_pool().await;
    let order = Order { quantity: -10, ... };
    assert!(validate_order(&order, &pool).await.is_err());
}

// AFTER
#[foxhunt_test(isolation = "transaction")]
async fn test_order_validation() {
    let pool = get_test_pool().await.unwrap();
    let order = Order { quantity: -10, ... };
    assert!(validate_order(&order, &pool).await.is_err());
}

3.2 Schema Strategy (Integration Tests)

Use Case: Multi-connection integration tests, DDL statements, service-to-service tests

Implementation:

// common/test_utils/schema.rs

use uuid::Uuid;

pub struct SchemaStrategy {
    pub run_migrations: bool,
    pub migration_path: String,
}

impl Default for SchemaStrategy {
    fn default() -> Self {
        Self {
            run_migrations: true,
            migration_path: "./migrations".to_string(),
        }
    }
}

#[async_trait]
impl IsolationStrategy for SchemaStrategy {
    async fn setup(&self, pool: &PgPool) -> Result<IsolationContext, IsolationError> {
        // Generate unique schema name (UUID prevents collisions)
        let schema_name = format!("test_{}", Uuid::new_v4().simple());
        
        // Create schema
        sqlx::query(&format!("CREATE SCHEMA IF NOT EXISTS {}", schema_name))
            .execute(pool)
            .await
            .map_err(|e| IsolationError::SchemaCreation(e.to_string()))?;
        
        // Set search_path for this connection
        sqlx::query(&format!("SET search_path TO {}, public", schema_name))
            .execute(pool)
            .await?;
        
        // Run migrations in isolated schema
        if self.run_migrations {
            let migrator = SchemaAwareMigrator::new(&self.migration_path, &schema_name);
            migrator.run(pool).await
                .map_err(|e| IsolationError::Migration(e.to_string()))?;
        }
        
        Ok(IsolationContext {
            strategy_type: StrategyType::Schema,
            schema_name: Some(schema_name),
            transaction: None,
            container_port: None,
            pool: pool.clone(),
        })
    }
    
    async fn cleanup(&self, ctx: IsolationContext) -> Result<(), IsolationError> {
        if let Some(schema_name) = ctx.schema_name {
            // Drop schema with CASCADE (removes all objects)
            sqlx::query(&format!("DROP SCHEMA IF EXISTS {} CASCADE", schema_name))
                .execute(&ctx.pool)
                .await?;
        }
        Ok(())
    }
    
    fn executor<'a>(&'a self, ctx: &'a IsolationContext) -> Box<dyn Executor + 'a> {
        Box::new(PoolExecutor { pool: &ctx.pool })
    }
}

/// Schema-aware migration runner (wraps migrations with SET search_path)
struct SchemaAwareMigrator {
    path: String,
    schema_name: String,
}

impl SchemaAwareMigrator {
    fn new(path: &str, schema_name: &str) -> Self {
        Self {
            path: path.to_string(),
            schema_name: schema_name.to_string(),
        }
    }
    
    async fn run(&self, pool: &PgPool) -> Result<(), sqlx::Error> {
        let migrations = self.read_migrations()?;
        
        for migration in migrations {
            // Wrap each migration with search_path setting
            let wrapped_sql = format!(
                "SET search_path TO {}, public;\n{}",
                self.schema_name,
                migration.sql
            );
            
            sqlx::query(&wrapped_sql).execute(pool).await?;
        }
        
        Ok(())
    }
    
    fn read_migrations(&self) -> Result<Vec<Migration>, sqlx::Error> {
        // Parse migration files from disk (similar to sqlx::migrate!())
        // Implementation reads SQL files from ./migrations directory
        todo!("Parse migration files - see Phase 3 implementation")
    }
}

Test Migration Example:

// BEFORE
#[tokio::test]
async fn test_trading_flow() {
    let pool_a = get_pool().await;
    insert_order(&pool_a).await;
    
    let pool_b = get_pool().await;
    let order = fetch_order(&pool_b).await;
    assert!(order.is_some());
}

// AFTER
#[foxhunt_test(isolation = "schema")]
async fn test_trading_flow() {
    let pool = get_test_pool().await.unwrap();
    insert_order(&pool).await;
    
    let order = fetch_order(&pool).await;
    assert!(order.is_some());
}

3.3 Container Strategy (E2E Tests)

Use Case: Full system validation, external dependencies, performance testing

Implementation:

// common/test_utils/container.rs

use testcontainers::{clients::Cli, images::postgres::Postgres, RunnableImage};
use std::sync::Arc;

pub struct ContainerStrategy {
    pub postgres_version: String,
    pub run_migrations: bool,
}

impl Default for ContainerStrategy {
    fn default() -> Self {
        Self {
            postgres_version: "14-alpine".to_string(),
            run_migrations: true,
        }
    }
}

#[async_trait]
impl IsolationStrategy for ContainerStrategy {
    async fn setup(&self, _pool: &PgPool) -> Result<IsolationContext, IsolationError> {
        // Start Docker container
        let docker = Cli::default();
        let image = RunnableImage::from(Postgres::default())
            .with_tag(&self.postgres_version);
        
        let container = docker.run(image);
        let port = container.get_host_port_ipv4(5432);
        
        // Create connection pool to container
        let db_url = format!("postgres://postgres:postgres@localhost:{}/test", port);
        let pool = PgPool::connect(&db_url).await
            .map_err(|e| IsolationError::ContainerStartup(e.to_string()))?;
        
        // Run migrations
        if self.run_migrations {
            sqlx::migrate!("./migrations").run(&pool).await
                .map_err(|e| IsolationError::Migration(e.to_string()))?;
        }
        
        Ok(IsolationContext {
            strategy_type: StrategyType::Container,
            schema_name: None,
            transaction: None,
            container_port: Some(port),
            pool,
            _container: Some(Arc::new(container)), // Dropped on cleanup
        })
    }
    
    async fn cleanup(&self, ctx: IsolationContext) -> Result<(), IsolationError> {
        ctx.pool.close().await;
        // Container auto-drops when Arc<Container> is dropped
        Ok(())
    }
    
    fn executor<'a>(&'a self, ctx: &'a IsolationContext) -> Box<dyn Executor + 'a> {
        Box::new(PoolExecutor { pool: &ctx.pool })
    }
}

Test Migration Example:

// BEFORE
#[tokio::test]
async fn test_end_to_end() {
    let env = start_test_environment().await;
    let result = execute_trade_via_tli(&env).await;
    assert!(result.is_success());
}

// AFTER
#[foxhunt_test(isolation = "container")]
async fn test_end_to_end() {
    let env = start_test_environment().await;
    let result = execute_trade_via_tli(&env).await;
    assert!(result.is_success());
}

3.4 Performance Optimizations

3.4.1 Schema Pooling (85% Faster)

Problem: Creating and dropping schemas adds 30-40ms per test.

Solution: Pre-warm schema pool, reuse schemas via TRUNCATE instead of DROP.

Performance Impact:

Without pooling:
- Schema creation: 30ms
- Schema destruction: 10ms
- Total: 40ms per test
- 500 tests: 20 seconds overhead

With pooling:
- Schema acquisition: <1ms (pop from pool)
- Schema cleanup: 5ms (TRUNCATE vs DROP)
- Schema return: <1ms (push to pool)
- Total: 6ms per test
- 500 tests: 3 seconds overhead
- Improvement: 85% faster

Implementation:

// common/test_utils/schema_pool.rs

use std::collections::VecDeque;
use tokio::sync::Mutex;

pub struct SchemaPool {
    pool: Arc<Mutex<VecDeque<PrewarmedSchema>>>,
    db_pool: PgPool,
    config: SchemaPoolConfig,
}

pub struct SchemaPoolConfig {
    pub min_size: usize,        // Default: 4
    pub max_size: usize,        // Default: 16
    pub warmup_on_init: bool,   // Default: true
}

impl SchemaPool {
    pub async fn acquire(&self) -> Result<String, IsolationError> {
        let mut pool = self.pool.lock().await;
        
        // Try to get from pool
        if let Some(mut schema) = pool.pop_front() {
            schema.reuse_count += 1;
            return Ok(schema.name);
        }
        
        // Pool empty, create new schema
        drop(pool);
        self.create_schema().await
    }
    
    pub async fn release(&self, schema_name: String) -> Result<(), IsolationError> {
        let mut pool = self.pool.lock().await;
        
        // Clean schema data (faster than DROP + CREATE)
        self.truncate_schema(&schema_name).await?;
        
        // Add to pool if under max size
        if pool.len() < self.config.max_size {
            pool.push_back(PrewarmedSchema {
                name: schema_name,
                created_at: std::time::Instant::now(),
                reuse_count: 0,
            });
        } else {
            // Pool full, drop schema
            sqlx::query(&format!("DROP SCHEMA {} CASCADE", schema_name))
                .execute(&self.db_pool).await?;
        }
        
        Ok(())
    }
    
    async fn truncate_schema(&self, schema_name: &str) -> Result<(), IsolationError> {
        // Get all tables in schema
        let tables: Vec<String> = sqlx::query_scalar(&format!(
            "SELECT tablename FROM pg_tables WHERE schemaname = '{}'",
            schema_name
        ))
        .fetch_all(&self.db_pool)
        .await?;
        
        // Truncate all tables with CASCADE (handles foreign keys)
        for table in tables {
            sqlx::query(&format!("TRUNCATE TABLE {}.{} CASCADE", schema_name, table))
                .execute(&self.db_pool).await?;
        }
        
        Ok(())
    }
}

3.4.2 Multi-Connection Schema Propagation

Problem: Only setup connection has SET search_path. Other connections use public schema.

Solution: Wrapper around PgPool that auto-sets search_path on connection acquisition.

Implementation:

// common/test_utils/isolated_pool.rs

pub struct IsolatedPool {
    inner: PgPool,
    schema_name: String,
}

impl IsolatedPool {
    pub async fn acquire(&self) -> Result<PoolConnection<Postgres>, sqlx::Error> {
        let mut conn = self.inner.acquire().await?;
        
        // Set search_path for this connection
        sqlx::query(&format!("SET search_path TO {}, public", self.schema_name))
            .execute(&mut conn).await?;
        
        Ok(conn)
    }
}

pub trait PgPoolExt {
    fn isolate(self, schema_name: String) -> IsolatedPool;
}

impl PgPoolExt for PgPool {
    fn isolate(self, schema_name: String) -> IsolatedPool {
        IsolatedPool::new(self, schema_name)
    }
}

3.4.3 Orphaned Schema Cleanup

Problem: Test panics or cleanup failures leave orphaned schemas.

Solution: Periodic background task to drop schemas older than 1 hour.

Implementation:

// common/test_utils/cleanup.rs

pub struct SchemaCleanupTask {
    db_pool: PgPool,
    max_age: Duration,    // Default: 1 hour
    interval: Duration,   // Default: 10 minutes
}

impl SchemaCleanupTask {
    pub fn spawn(self) -> tokio::task::JoinHandle<()> {
        tokio::spawn(async move {
            loop {
                if let Err(e) = self.cleanup_orphaned_schemas().await {
                    eprintln!("Schema cleanup error: {}", e);
                }
                tokio::time::sleep(self.interval.to_std().unwrap()).await;
            }
        })
    }
    
    async fn cleanup_orphaned_schemas(&self) -> Result<(), sqlx::Error> {
        let test_schemas: Vec<String> = sqlx::query_scalar(
            "SELECT schema_name FROM information_schema.schemata 
             WHERE schema_name LIKE 'test_%'"
        )
        .fetch_all(&self.db_pool)
        .await?;
        
        for schema in test_schemas {
            // Check schema age (heuristic: no activity for max_age = orphaned)
            let last_modified: chrono::DateTime<Utc> = sqlx::query_scalar(&format!(
                "SELECT MAX(last_analyzed) FROM pg_stat_user_tables 
                 WHERE schemaname = '{}'",
                schema
            ))
            .fetch_one(&self.db_pool)
            .await
            .unwrap_or_else(|_| Utc::now());
            
            if Utc::now().signed_duration_since(last_modified) > self.max_age {
                println!("Dropping orphaned schema: {}", schema);
                sqlx::query(&format!("DROP SCHEMA {} CASCADE", schema))
                    .execute(&self.db_pool).await?;
            }
        }
        
        Ok(())
    }
}

4. Migration Strategy

4.1 Test Classification

Tests are categorized by isolation requirements:

Classification Decision Tree
============================

                    [Test]
                      |
          ┌───────────┴──────────┐
          v                      v
    [Uses Database?]       [No DB Access]
          |                      |
    ┌─────┴─────┐               v
    v           v          NO MIGRATION
[Single     [Multiple      (~36 tests)
 Conn?]      Conns?]
    |           |
    v           v
[DDL?]    [Schema-per-test]
    |      (~500 tests)
┌───┴───┐
v       v
[E2E?] [Txn]
   |    (~1,500)
   v
[Container]
(~50 tests)

Category 1: Unit Tests (Transaction Rollback) - ~1,500 tests

Characteristics:

  • Single connection, single service scope
  • No DDL statements (no CREATE/ALTER/DROP)
  • No multi-connection race conditions
  • Fast iteration required (<1ms overhead)

Example:

// services/trading_service/tests/order_validation_test.rs
#[foxhunt_test(isolation = "transaction")]
async fn test_validate_order_quantity() {
    let pool = get_test_pool().await.unwrap();
    let order = Order { quantity: -10, ... };
    assert!(validate_order(&order, &pool).await.is_err());
}

Category 2: Integration Tests (Schema-per-Test) - ~500 tests

Characteristics:

  • Multi-connection or multi-service coordination
  • May include DDL (temporary tables, indexes)
  • Service-to-service gRPC calls requiring DB state
  • Moderate performance overhead acceptable (10-50ms)

Example:

// services/trading_service/tests/integration/trading_flow_test.rs
#[foxhunt_test(isolation = "schema")]
async fn test_full_trading_flow() {
    let pool = get_test_pool().await.unwrap();
    
    // Service A writes order
    insert_order(&pool).await;
    
    // Service B reads order (different connection)
    let order = fetch_order(&pool).await;
    assert!(order.is_some());
}

Category 3: E2E Tests (Testcontainers) - ~50 tests

Characteristics:

  • Full system validation (all 5 microservices)
  • External dependencies (Redis, Vault, etc.)
  • Performance testing with realistic load
  • Extension-specific tests (TimescaleDB functions)

Example:

// tests/e2e/full_system_test.rs
#[foxhunt_test(isolation = "container")]
async fn test_end_to_end_trading() {
    let env = start_test_environment().await;
    let result = execute_trade_via_tli(&env).await;
    assert!(result.is_success());
}

Category 4: No Migration Needed - ~36 tests

Characteristics:

  • Pure in-memory tests (no database access)
  • Mock-only tests (no real DB queries)
  • Algorithm tests (feature extraction, indicator calculation)

Example (no changes):

// ml/tests/feature_extraction_test.rs
#[test]
fn test_rsi_calculation() {
    let prices = vec![100.0, 102.0, 101.0, 103.0];
    let rsi = calculate_rsi(&prices, 14);
    assert!((rsi - 56.7).abs() < 0.1);
}

4.2 Automated Migration

4.2.1 Classification Script

#!/bin/bash
# Script: classify_tests.sh

for test_file in $(find . -name "*test*.rs" -o -name "*tests.rs"); do
    # Check for multi-connection patterns
    if grep -q "get_pool().await.*get_pool().await" "$test_file"; then
        echo "$test_file: INTEGRATION (schema isolation)"
    # Check for DDL statements
    elif grep -q "CREATE TABLE\|ALTER TABLE\|DROP TABLE" "$test_file"; then
        echo "$test_file: INTEGRATION (schema isolation)"
    # Check for E2E patterns
    elif grep -q "start_.*_service\|docker\|testcontainers" "$test_file"; then
        echo "$test_file: E2E (container isolation)"
    # Check for DB access
    elif grep -q "sqlx::\|get_pool\|PgPool" "$test_file"; then
        echo "$test_file: UNIT (transaction isolation)"
    else
        echo "$test_file: NO_MIGRATION (no DB access)"
    fi
done

4.2.2 Codemod Tool

Automated refactoring patterns:

  1. Add #[foxhunt_test(isolation = "...")] attribute
  2. Remove manual pool setup/cleanup code
  3. Replace setup_test_pool() with get_test_pool()

Example transformation:

// BEFORE
#[tokio::test]
async fn test_foo() {
    let pool = setup_test_pool().await;
    // test logic
    cleanup_pool(pool).await;
}

// AFTER
#[foxhunt_test(isolation = "transaction")]
async fn test_foo() {
    let pool = get_test_pool().await.unwrap();
    // test logic
    // cleanup automatic
}

4.3 Manual Migration Guidelines

Handling Custom Setup/Teardown:

  • Move custom setup into test body (after isolation setup)
  • Remove cleanup code (isolation handles it)
  • Document non-standard patterns in comments

Savepoint Test Classification:

  • Tests using SAVEPOINT or nested transactions → "schema" isolation
  • Transaction isolation cannot test transaction logic itself

TimescaleDB Hypertable Considerations:

  • Test hypertables in isolated schemas early (Phase 3)
  • If incompatible, use container isolation for hypertable tests
  • Document limitation in TESTING_GUIDE.md

5. Implementation Plan

5.1 Phased Rollout (3 Weeks)

Timeline Overview
=================

Week 1: Infrastructure + Core Strategies
├── Day 1-2: Proc macro + trait definitions
├── Day 3:   Transaction strategy + 10 test PoC
└── Day 4-5: Schema strategy + 5 test PoC

Week 2: Optimizations + Container + Automation
├── Day 1-2: Schema pooling + isolated pool wrapper
├── Day 3:   Container strategy + 2 test PoC
└── Day 4-5: Classification script + codemod tool

Week 3: Full Migration + Validation
├── Day 1-2: Migrate 1,500 unit tests (automated)
├── Day 3-4: Migrate 500 integration + 50 E2E tests
└── Day 5:   100x CI/CD validation + documentation

Phase 1: Core Infrastructure (Week 1, Days 1-2)

  • Create common_macros crate
    • Setup Cargo.toml with proc-macro = true
    • Implement #[foxhunt_test] attribute macro
    • Add parsing for isolation parameter
    • Test macro expansion with cargo expand
  • Create common/test_utils module
    • Define IsolationStrategy trait
    • Define IsolationContext struct
    • Define IsolationError enum
    • Implement get_test_pool() helper
  • Write framework integration tests
    • Test all 3 isolation strategies compile
    • Test error handling (database down, invalid config)
    • Test cleanup on panic

Phase 2: Transaction Strategy (Week 1, Day 3)

  • Implement TransactionStrategy
    • setup() method with pool.begin()
    • cleanup() method with tx.rollback()
    • Executor wrapper for transaction queries
  • Migrate 10 simple unit tests as proof-of-concept
    • services/trading_service/tests/order_validation_test.rs
    • Verify tests pass with #[foxhunt_test(isolation = "transaction")]
    • Benchmark overhead (<1ms target)
  • Add metrics collection for transaction tests

Phase 3: Schema Strategy (Week 1, Days 4-5)

  • Implement SchemaStrategy
    • UUID-based schema naming
    • CREATE SCHEMA logic
    • SET search_path logic
    • DROP SCHEMA CASCADE cleanup
  • Implement SchemaAwareMigrator
    • Parse migration files from disk
    • Wrap each migration with SET search_path
    • Apply migrations in order
    • Handle migration errors gracefully
  • Migrate 5 integration tests as proof-of-concept
    • services/trading_service/tests/integration/trading_flow_test.rs
    • Verify multi-connection tests work
    • Benchmark overhead (10-50ms target)

Phase 4: Performance Optimizations (Week 2, Days 1-2)

  • Implement SchemaPool
    • Schema acquisition/release logic
    • TRUNCATE-based cleanup (vs DROP)
    • Pre-warming on initialization
    • Pool size tuning (min: 4, max: 16)
  • Implement IsolatedPool wrapper
    • Auto SET search_path on connection acquisition
    • Wrapper methods for common queries
    • Extension trait for PgPool
  • Benchmark schema pooling
    • Measure hit rate (target: >80%)
    • Measure overhead reduction (target: <10ms per test)

Phase 5: Container Strategy (Week 2, Day 3)

  • Implement ContainerStrategy
    • Testcontainers integration
    • Port mapping and connection setup
    • Migration execution in container
    • Cleanup via container drop
  • Migrate 2 E2E tests as proof-of-concept
    • tests/e2e/full_system_test.rs
    • Verify full isolation works
    • Accept 2-5s overhead (expected)

Phase 6: Automated Migration (Week 2, Days 4-5)

  • Write classify_tests.sh script
    • Detect multi-connection patterns
    • Detect DDL statements
    • Detect E2E patterns
    • Output classification CSV
  • Write codemod tool
    • Pattern match #[tokio::test]
    • Insert #[foxhunt_test(isolation = "...")]
    • Remove manual pool setup/cleanup
    • Preserve existing test logic
  • Run on low-risk categories first
    • Unit tests (transaction isolation)
    • Validate with cargo test
    • Fix failures iteratively

Phase 7: Full Migration (Week 3, Days 1-4)

  • Migrate integration tests (schema isolation)
    • Run codemod on integration test directories
    • Manual review of complex tests
    • Run test suite 10x to catch flakiness
  • Migrate E2E tests (container isolation)
    • Manual migration (low count, high complexity)
    • Update test documentation
    • Verify Docker availability in CI/CD
  • Cleanup orphaned code
    • Remove old pool setup/cleanup helpers
    • Update test README files
    • Add migration guide for future tests

Phase 8: Validation & Documentation (Week 3, Day 5)

  • Run full test suite 100x in CI/CD
    • Matrix of parallelism levels (1, 2, 4, 8, 16)
    • Verify 100% pass rate (zero race conditions)
    • Collect performance metrics
  • Write final documentation
    • TEST_ISOLATION_FRAMEWORK.md (architecture)
    • TESTING_GUIDE.md (when to use each strategy)
    • MIGRATION_GUIDE.md (for existing tests)
    • TROUBLESHOOTING.md (common issues)
  • Update CLAUDE.md
    • Add test isolation to development workflow
    • Document #[foxhunt_test] usage
    • Add performance benchmarks

5.2 Phase-Gate Reviews

Validation checkpoints to ensure quality before proceeding:

Phase Validation Duration Go/No-Go Criteria
Phase 2 Transaction validation 1 hour Overhead <1ms, 10/10 tests passing
Phase 3 Schema validation 1 hour Overhead <100ms, 5/5 tests passing
Phase 4 Pooling benchmark 30 min Hit rate >70%, overhead <10ms
Phase 5 Container validation 1 hour 2/2 tests passing, overhead <5s
Phase 7 Full suite validation 4 hours Pass rate >95%, zero race conditions

5.3 Go/No-Go Decision Points

After Phase 2: If transaction overhead >5ms → investigate before proceeding
After Phase 3: If schema overhead >100ms → enable pooling before proceeding
After Phase 6: If automated migration <50% → invest more in tooling
After Phase 7: If test pass rate <95% → pause, analyze failures


6. Risk Management

6.1 Risk Matrix

Risk Impact Probability Mitigation
Migration file parsing complexity High Medium Use runtime SQL file reading. Fall back to pre-generated schema templates.
Schema pool contention Medium Medium Implement adaptive pool sizing (auto-grow when hit rate <80%).
TimescaleDB hypertable incompatibility High Low Test hypertables in schemas early (Phase 3). Document limitations.
SQLX query!() macro breaks Low High Expected behavior. Migrate query!()query(). Document in guide.
Orphaned schema accumulation Medium Medium Implement cleanup task (runs every 10 min). Add monitoring.
Transaction rollback breaks savepoint tests Medium Low Classify savepoint tests as "schema" isolation.
Testcontainers Docker dependency Medium Medium Document Docker-in-Docker requirements. Provide schema fallback.
Performance regression >10% High Low Benchmark after each phase. Enable pooling earlier if needed.
False positive test failures High Medium Migrate incrementally (10 → 50 → 500 → all). Roll back if pass rate <95%.
Developer adoption resistance Medium High Provide examples, scripts, pair programming. Emphasize time savings.

6.2 Contingency Plans

If schema-per-test performance is too slow (>50ms overhead):

  1. Enable schema pooling immediately (move Phase 4 to Phase 3)
  2. Increase pool size to 32 schemas (from 16)
  3. Use TRUNCATE instead of DROP SCHEMA in all cases
  4. Pre-warm pool with 16 schemas on test suite startup

If migration file parsing fails:

  1. Fall back to sqlx::migrate!() macro with schema override
  2. Fork SQLX migration runner to add schema awareness
  3. Generate schema templates pre-filled with migrations
  4. Document manual migration steps for complex cases

If TimescaleDB hypertables break in schemas:

  1. Create hypertables in public schema, reference from test schemas
  2. Use separate "hypertable test" strategy with dedicated database
  3. Document limitation: hypertable tests must use container isolation
  4. Add test helper for hypertable-aware schema creation

If test migration causes >5% failure rate:

  1. Pause migration, analyze failures
  2. Categorize failures: isolation bugs vs. pre-existing flakiness
  3. Fix isolation bugs in framework
  4. Document pre-existing flaky tests separately
  5. Resume migration with fixed framework

If Docker unavailable in CI/CD:

  1. Fall back E2E tests to schema isolation (compromise)
  2. Document Docker setup instructions for CI/CD
  3. Add optional SKIP_CONTAINER_TESTS=1 env var
  4. Require manual E2E testing before production deployment

6.3 Rollback Plan

If migration fails or causes instability:

  1. Keep framework code in common/test_utils (opt-in, not forced)
  2. Migrate tests incrementally (can revert individual tests)
  3. Use feature flag ENABLE_TEST_ISOLATION=1 during migration
  4. Document rollback process:
    • git revert <commit>
    • Remove #[foxhunt_test] annotations
    • Restore manual pool setup/cleanup code

7. CI/CD Integration

7.1 GitHub Actions Workflow

# .github/workflows/test-isolation.yml

name: Test Isolation Framework

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  test-parallel:
    name: Parallel Test Execution (Race Condition Detection)
    runs-on: ubuntu-latest
    
    services:
      postgres:
        image: timescale/timescaledb:latest-pg14
        env:
          POSTGRES_USER: foxhunt
          POSTGRES_PASSWORD: foxhunt_dev_password
          POSTGRES_DB: foxhunt
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 5432:5432
    
    strategy:
      matrix:
        parallel: [1, 2, 4, 8, 16]
        iteration: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    
    steps:
      - uses: actions/checkout@v3
      
      - name: Install Rust
        uses: actions-rs/toolchain@v1
        with:
          profile: minimal
          toolchain: stable
      
      - name: Setup database
        run: cargo sqlx migrate run
        env:
          DATABASE_URL: postgresql://foxhunt:foxhunt_dev_password@localhost:5432/foxhunt
      
      - name: Run tests (parallel=${{ matrix.parallel }})
        run: cargo test --workspace -- --test-threads ${{ matrix.parallel }}
        env:
          DATABASE_URL: postgresql://foxhunt:foxhunt_dev_password@localhost:5432/foxhunt
          RUST_LOG: debug
      
      - name: Verify no orphaned schemas
        if: always()
        run: |
          psql -h localhost -U foxhunt -d foxhunt -c \
            "SELECT COUNT(*) FROM information_schema.schemata WHERE schema_name LIKE 'test_%';" \
            | grep -q "0" || exit 1
        env:
          PGPASSWORD: foxhunt_dev_password
  
  test-categories:
    name: Test Category Validation
    runs-on: ubuntu-latest
    
    services:
      postgres:
        image: timescale/timescaledb:latest-pg14
        env:
          POSTGRES_USER: foxhunt
          POSTGRES_PASSWORD: foxhunt_dev_password
          POSTGRES_DB: foxhunt
        ports:
          - 5432:5432
    
    steps:
      - uses: actions/checkout@v3
      
      - name: Validate transaction tests
        run: cargo test -p trading_service --lib -- --test-threads 16
        env:
          DATABASE_URL: postgresql://foxhunt:foxhunt_dev_password@localhost:5432/foxhunt
      
      - name: Validate integration tests
        run: cargo test -p trading_service --test integration -- --test-threads 8
        env:
          DATABASE_URL: postgresql://foxhunt:foxhunt_dev_password@localhost:5432/foxhunt
      
      - name: Validate E2E tests
        run: cargo test --test e2e -- --test-threads 1
        env:
          DATABASE_URL: postgresql://foxhunt:foxhunt_dev_password@localhost:5432/foxhunt

7.2 Performance Monitoring

// common/test_utils/metrics.rs

use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};

pub struct IsolationMetrics {
    pub transaction_tests: AtomicUsize,
    pub schema_tests: AtomicUsize,
    pub container_tests: AtomicUsize,
    
    pub transaction_duration_ns: AtomicU64,
    pub schema_setup_duration_ns: AtomicU64,
    pub schema_cleanup_duration_ns: AtomicU64,
    
    pub schema_pool_hits: AtomicUsize,
    pub schema_pool_misses: AtomicUsize,
    pub orphaned_schemas_cleaned: AtomicUsize,
}

impl IsolationMetrics {
    pub fn report(&self) {
        println!("\n=== Test Isolation Metrics ===");
        println!("Transaction tests: {}", self.transaction_tests.load(Ordering::Relaxed));
        println!("Schema tests: {}", self.schema_tests.load(Ordering::Relaxed));
        println!("Container tests: {}", self.container_tests.load(Ordering::Relaxed));
        println!("\nPerformance:");
        println!("  Transaction avg: {}μs", 
            self.transaction_duration_ns.load(Ordering::Relaxed) / 1000);
        println!("  Schema setup avg: {}ms", 
            self.schema_setup_duration_ns.load(Ordering::Relaxed) / 1_000_000);
        println!("\nSchema Pool:");
        println!("  Hits: {}", self.schema_pool_hits.load(Ordering::Relaxed));
        println!("  Hit rate: {:.1}%", 
            self.schema_pool_hits.load(Ordering::Relaxed) as f64 
            / (self.schema_pool_hits.load(Ordering::Relaxed) 
               + self.schema_pool_misses.load(Ordering::Relaxed)) as f64 
            * 100.0);
    }
}

#[cfg(test)]
#[ctor::dtor]
fn report_metrics() {
    METRICS.report();
}

7.3 Alerting Rules

Critical Alerts:

  • Orphaned schema count >100 (memory leak indicator)
  • Test suite time >250s (10% regression threshold exceeded)
  • Zero race conditions violated (parallel test failures)

Warning Alerts:

  • Schema pool hit rate <70% (inefficient pooling)
  • Cleanup task failures >5 in 1 hour (infrastructure issue)
  • Test pass rate <98% (potential isolation bugs)

8. Success Criteria

8.1 Must-Have Requirements

ID Requirement Target Validation Method
M1 Zero race conditions 100% 100 CI/CD runs with different parallelism levels
M2 Performance overhead <10% Total test suite time: 208s → <229s
M3 Test pass rate 100% 2,086/2,086 tests passing
M4 All strategies functional 100% Transaction + schema + container all work
M5 Automatic cleanup 100% Zero orphaned schemas after test run

8.2 Performance Targets

Metric Current Target Acceptable Range
Unit test overhead ~0ms <1ms 0-2ms
Integration test overhead ~0ms <10ms 0-20ms
E2E test overhead ~0ms <3s 0-5s
Total test suite time 208s <229s 208-250s
Schema pool hit rate N/A >80% 70-100%
Orphaned schema count Unknown 0 0-5

8.3 Validation Protocol

Phase-Specific Validation:

  • Phase 2: 10 transaction tests, 100 runs each, <1ms overhead
  • Phase 3: 5 schema tests, 100 runs each, <100ms overhead
  • Phase 4: Schema pool hit rate measurement (target: >80%)
  • Phase 5: 2 container tests, 10 runs each, <5s overhead
  • Phase 7: Full suite 100x with parallelism matrix (1, 2, 4, 8, 16 threads)

Final Validation Checklist:

  • 100 CI/CD runs with zero race conditions
  • Total test suite time <229s (10% overhead budget)
  • All 2,086 tests passing (100% pass rate)
  • Schema pool hit rate >70% (efficient pooling)
  • Zero orphaned schemas after test completion
  • All 3 isolation strategies functional
  • Documentation complete (4 docs delivered)
  • Developer satisfaction survey >4/5

9. Long-Term Maintenance

9.1 Ownership Model

Component Owner Responsibilities
Framework code Platform team Bug fixes, new features, performance tuning
Test migration Feature teams Migrate service-specific tests, fix failures
Documentation Tech writing Maintain guides, update examples
Monitoring DevOps team Grafana dashboards, alerting rules

9.2 Ongoing Tasks

Weekly:

  • Review orphaned schema metrics (should be 0)
  • Check schema pool hit rate (should be >70%)
  • Monitor test suite duration (should be <250s)

Monthly:

  • Performance benchmarking (detect regressions)
  • Review isolation failure patterns
  • Update documentation based on feedback

Quarterly:

  • Developer satisfaction surveys
  • Framework improvements planning
  • Training sessions for new developers

Annually:

  • Major version upgrades (SQLX, testcontainers-rs)
  • New isolation strategy evaluation
  • Architecture review and optimization

10. ROI Analysis

10.1 Costs

Item Cost
Development $26,000 (130 hours at $200/hour fully loaded)
Risk 2-3 weeks of potential test instability during migration
Ongoing maintenance $800/month (~4 hours/month)

10.2 Benefits

Benefit Annual Value
Debugging time saved $20,000/year (100+ hours debugging flaky tests)
CI/CD efficiency $10,000/year (faster feedback, fewer reruns)
Scalability Supports 10,000+ tests without conflicts
Developer productivity Confidence in test suite, faster iteration

10.3 Payback Period

Break-even calculation:
- Initial investment: $26,000
- Annual savings: $30,000
- Payback period: 10 months

Year 1 ROI: ($30,000 - $26,000 - $9,600) / $26,000 = 17% return
Year 2+ ROI: ($30,000 - $9,600) / $26,000 = 78% return per year

11. Appendices

Appendix A: Code Examples

Proc Macro Implementation:

// common_macros/src/lib.rs

use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, AttributeArgs, ItemFn};

#[proc_macro_attribute]
pub fn foxhunt_test(args: TokenStream, input: TokenStream) -> TokenStream {
    let args = parse_macro_input!(args as AttributeArgs);
    let input_fn = parse_macro_input!(input as ItemFn);
    
    let isolation_strategy = parse_isolation_strategy(&args);
    let fn_name = &input_fn.sig.ident;
    let fn_body = &input_fn.block;
    let fn_attrs = &input_fn.attrs;
    
    let expanded = quote! {
        #(#fn_attrs)*
        #[tokio::test]
        async fn #fn_name() {
            use common::test_utils::*;
            
            let pool = get_test_pool().await.expect("Failed to get test pool");
            let strategy = #isolation_strategy;
            let ctx = strategy.setup(&pool).await.expect("Failed to setup isolation");
            
            let result = async { #fn_body }.await;
            
            strategy.cleanup(ctx).await.expect("Failed to cleanup isolation");
            result
        }
    };
    
    TokenStream::from(expanded)
}

Appendix B: Troubleshooting Guide

Error Cause Solution
"Schema creation failed" PostgreSQL permissions Grant CREATE permission: GRANT CREATE ON DATABASE foxhunt TO foxhunt;
"Migration parser error" Invalid SQL syntax in migration file Validate migration file with psql -f migrations/XXX.sql
"Pool exhausted" High parallelism, low pool size Increase SchemaPoolConfig.max_size from 16 to 32
"Orphaned schemas" Test panics or cleanup failures Run cleanup task manually: SchemaCleanupTask::cleanup_orphaned_schemas()
"SQLX query!() compile error" Runtime queries incompatible with compile-time checks Replace query!() with query() (lose compile-time validation)

Appendix C: Performance Benchmarks

Baseline (No Isolation):

  • Unit test: ~0.1ms average
  • Integration test: ~1ms average
  • E2E test: ~50ms average
  • Total suite: 208s (2,086 tests)

With Isolation (No Pooling):

  • Unit test: ~0.1ms + <1ms = ~1.1ms (10x slower)
  • Integration test: ~1ms + 40ms = ~41ms (40x slower)
  • E2E test: ~50ms + 3s = ~3.05s (60x slower)
  • Total suite: 374.5s (80% overhead)

With Isolation (With Pooling):

  • Unit test: ~0.1ms + <1ms = ~1.1ms (10x slower)
  • Integration test: ~1ms + 6ms = ~7ms (7x slower)
  • E2E test: ~50ms + 3s = ~3.05s (60x slower)
  • Total suite: 226.5s (9% overhead) ✓ MEETS TARGET

Appendix D: References


Conclusion

This test isolation framework provides a comprehensive solution for eliminating database race conditions in Foxhunt's test suite. The hybrid approach (transaction + schema + container) balances performance, isolation, and pragmatism, delivering zero race conditions with <10% overhead.

Key Strengths:

  • Optimizes for common case (1,500 unit tests with <1ms overhead)
  • Robust isolation for complex tests (schema pooling = 85% faster)
  • Incremental migration path (low risk, high control)
  • Future-proof (scales to 10,000+ tests)

Next Steps:

  1. Review this design document with team
  2. Approve budget and timeline
  3. Begin Phase 1 implementation (proc macro + infrastructure)
  4. Execute 3-week phased rollout
  5. Validate success criteria after migration

Approval Required: Platform team lead, Tech lead, DevOps lead


Document Version History:

  • v1.0 (2025-10-23): Initial design complete