Files
foxhunt/services/trading_service/tests/rollback_automation_tests.rs
jgrusewski 35feadf55e 🚀 Wave 160 Phase 6: CUDA Mandatory + TDD Testing + TFT Complete (21 Agents)
## Major Achievements

### 1. CUDA Made Default & Mandatory (Agent 143)
- CUDA now default feature in ml/Cargo.toml
- All training requires GPU (no silent CPU fallback)
- Added get_training_device() helper with fail-fast errors
- Removed --use-gpu flags (GPU mandatory)
- **Impact**: No more wasting time on accidental CPU training

### 2. TFT Training COMPLETE (Agent 144)
-  Training completed successfully in 7.6 minutes
-  Early stopping at epoch 100/200 (best val loss: 0.097318)
-  11 checkpoints saved to ml/trained_models/production/tft/
-  GPU Performance: 99% utilization, 367MB VRAM, 4.4s/epoch
-  10x speedup vs CPU (4.4s vs 43-55s per epoch)
- **Status**: PRODUCTION READY

### 3. TFT CUDA Tensor Contiguity Fix (Agent 142)
- Fixed "matmul not supported for non-contiguous tensors" error
- Added .contiguous() call after narrow() operation in QuantileLayer
- Enabled CUDA-accelerated TFT training
- **Files**: ml/src/tft/quantile_outputs.rs

### 4. MAMBA-2 CUDA Layer Normalization (Agent 145)
- Created CudaLayerNorm wrapper for missing CUDA kernel
- Implemented manual layer norm: γ * (x - μ) / sqrt(σ² + ε) + β
- MAMBA-2 now runs on CUDA (no more "no cuda implementation" error)
- **Files**: ml/src/mamba/mod.rs

### 5. TDD E2E Test Suite (Agent 146) 
- Created comprehensive MAMBA-2 test suite (297 lines)
- 7 tests: shapes, batches, CUDA, gradients, configs
- **16x faster debugging**: 5s per iteration vs 80s
- Already caught dtype mismatch bug (F32 vs F64)
- **Files**: ml/tests/e2e_mamba2_training.rs

## Agent Summary (Agents 126-146)

### Code Fixes (Parallel - Agents 137-141)
- **Agent 137**: MAMBA-2 batch dimension fix (streaming + batch loaders)
- **Agent 138**: Liquid NN API fix (mutable loader, iterator fix)
- **Agent 139**: PPO CheckpointMetadata fix (signature fields)
- **Agent 140**: Paper trading executor (498 lines, 100ms polling)
- **Agent 141**: Real model loading (RealDQNModel, RealPPOModel)

### Infrastructure (Agents 143-146)
- **Agent 143**: CUDA mandatory (Cargo.toml, device helpers)
- **Agent 144**: TFT verification (completion monitoring)
- **Agent 145**: MAMBA-2 CUDA layer norm wrapper
- **Agent 146**: TDD E2E test suite (16x faster debugging)

## Files Modified

### Core ML Infrastructure
- ml/Cargo.toml: Added default = ["minimal-inference", "cuda"]
- ml/src/lib.rs: Added get_training_device() helper (+109 lines)
- ml/src/tft/quantile_outputs.rs: Fixed tensor contiguity
- ml/src/mamba/mod.rs: Added CudaLayerNorm wrapper (+41 lines)

### Training Scripts
- ml/examples/train_tft_dbn.rs: Removed --use-gpu flag
- ml/examples/train_ppo.rs: Removed --use-gpu flag
- ml/examples/train_mamba2_dbn.rs: Forced CUDA-only mode
- ml/examples/train_liquid_dbn.rs: Fixed API usage

### Data Loaders
- ml/src/data_loaders/dbn_sequence_loader.rs: Fixed batch dimensions
- ml/src/data_loaders/streaming_dbn_loader.rs: Fixed batch dimensions

### Trading Service
- services/trading_service/src/paper_trading_executor.rs: New executor (+498 lines)
- services/trading_service/src/services/enhanced_ml.rs: Real model loading
- services/trading_service/src/ensemble_coordinator.rs: Integration

### Tests
- ml/tests/e2e_mamba2_training.rs: New TDD test suite (+297 lines)

### Trainers
- ml/src/trainers/tft.rs: Fixed CheckpointMetadata signature fields

## Performance Metrics

### TFT Training
- Duration: 7.6 minutes (100 epochs with early stopping)
- GPU Utilization: 99%
- GPU Memory: 367MB / 4GB (9%)
- Epoch Time: 4.4 seconds (vs 43-55s on CPU)
- Speedup: 10x vs CPU
- Status:  PRODUCTION READY

### TDD Testing
- Test Execution: 5-10 seconds per test
- Debugging Iteration: 5 seconds (vs 80 seconds before)
- Speedup: 16x faster debugging
- First Bug Found: <1 minute (dtype mismatch)

## Documentation
- 21 comprehensive agent reports
- TDD quick start guide
- CUDA troubleshooting guide
- Training verification procedures

## Next Steps
1. Fix MAMBA-2 dtype mismatch (F32→F64) - 2 minutes
2. Run MAMBA-2 tests until passing - 5-10 minutes
3. Launch full MAMBA-2 training - 200 epochs
4. Launch Liquid NN training

## System Status
- TFT:  COMPLETE (production ready)
- MAMBA-2: 🧪 IN TESTING (TDD suite ready)
- CUDA:  DEFAULT (mandatory for training)
- Tests:  16x faster debugging

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-14 23:13:34 +02:00

688 lines
23 KiB
Rust

//! Comprehensive Integration Tests for Rollback Automation
//!
//! Tests all 4 failure scenarios with automatic recovery:
//! 1. Daily loss exceeds $2K
//! 2. Model disagreement >70% for 1 hour
//! 3. Single model >3 consecutive errors
//! 4. Cascade failure (2+ models fail)
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
use tokio::time::sleep;
use trading_service::ensemble_coordinator::EnsembleCoordinator;
use trading_service::ensemble_risk_manager::{EnsembleRiskConfig, EnsembleRiskManager};
use trading_service::rollback_automation::{
RollbackAutomation, RollbackConfig, RollbackReport, RollbackScenario, RollbackAction,
};
/// Test helper to create automation with default config
fn create_automation() -> RollbackAutomation {
let config = RollbackConfig {
daily_loss_threshold_usd: 2000.0,
high_disagreement_threshold: 0.70,
disagreement_duration_secs: 10, // 10 seconds for testing
max_consecutive_errors: 3,
cascade_failure_threshold: 2,
position_reduction_factor: 0.50,
monitoring_interval_secs: 1, // 1 second for testing
recovery_timeout_secs: 300,
enable_automatic_rollback: true,
};
RollbackAutomation::new(config)
}
/// Test helper to create ensemble risk manager
async fn create_risk_manager() -> Arc<EnsembleRiskManager> {
let config = EnsembleRiskConfig {
max_consecutive_errors: 3,
cascade_failure_threshold: 2,
cascade_detection_window_secs: 60,
..Default::default()
};
let manager = EnsembleRiskManager::new(config);
// Register test models
manager.register_model("DQN".to_string()).await.unwrap();
manager.register_model("PPO".to_string()).await.unwrap();
manager.register_model("TFT".to_string()).await.unwrap();
Arc::new(manager)
}
/// Test helper to create ensemble coordinator
async fn create_coordinator() -> Arc<EnsembleCoordinator> {
let coordinator = EnsembleCoordinator::new();
coordinator.register_model("DQN".to_string(), 0.33).await.unwrap();
coordinator.register_model("PPO".to_string(), 0.33).await.unwrap();
coordinator.register_model("TFT".to_string(), 0.34).await.unwrap();
Arc::new(coordinator)
}
// ============================================================================
// SCENARIO 1: Daily Loss Exceeds $2K
// ============================================================================
#[tokio::test]
async fn test_scenario_1_daily_loss_exceeded_basic() {
let automation = create_automation();
// Simulate daily loss exceeding threshold
automation.update_daily_pnl(-2500.0).await.unwrap();
// Trigger scenario manually
automation.trigger_scenario_manual(RollbackScenario::DailyLossExceeded).await.unwrap();
let state = automation.get_state().await;
assert!(state.active_scenarios.contains_key(&RollbackScenario::DailyLossExceeded));
assert_eq!(state.daily_pnl_usd, -2500.0);
}
#[tokio::test]
async fn test_scenario_1_emergency_halt_executed() {
let automation = create_automation();
// Trigger scenario
automation.trigger_scenario_manual(RollbackScenario::DailyLossExceeded).await.unwrap();
// Execute recovery
let config = automation.config.clone();
RollbackAutomation::execute_recovery_actions(&config, &automation.state).await.unwrap();
// Verify emergency halt
assert!(automation.is_trading_halted().await);
let state = automation.get_state().await;
assert!(state.executed_actions.iter().any(|(a, _)| *a == RollbackAction::EmergencyHalt));
}
#[tokio::test]
async fn test_scenario_1_position_reduction_executed() {
let automation = create_automation();
// Trigger scenario
automation.trigger_scenario_manual(RollbackScenario::DailyLossExceeded).await.unwrap();
// Execute recovery
let config = automation.config.clone();
RollbackAutomation::execute_recovery_actions(&config, &automation.state).await.unwrap();
// Verify position reduction
assert!(automation.are_positions_reduced().await);
let state = automation.get_state().await;
assert!(state.executed_actions.iter().any(|(a, _)| *a == RollbackAction::ReducePositions));
}
#[tokio::test]
async fn test_scenario_1_recovery_time_under_5_minutes() {
let automation = create_automation();
// Trigger and recover
automation.trigger_scenario_manual(RollbackScenario::DailyLossExceeded).await.unwrap();
let config = automation.config.clone();
RollbackAutomation::execute_recovery_actions(&config, &automation.state).await.unwrap();
// Check recovery time
let duration = automation.get_recovery_duration().await;
assert!(duration.is_some());
assert!(duration.unwrap().as_secs() < 300); // Less than 5 minutes
}
#[tokio::test]
async fn test_scenario_1_full_recovery_sequence() {
let automation = create_automation();
// Simulate loss
automation.update_daily_pnl(-2500.0).await.unwrap();
// Trigger
automation.trigger_scenario_manual(RollbackScenario::DailyLossExceeded).await.unwrap();
// Execute recovery
let config = automation.config.clone();
RollbackAutomation::execute_recovery_actions(&config, &automation.state).await.unwrap();
// Verify complete recovery
let state = automation.get_state().await;
let report = RollbackReport::from_state(&state);
assert!(report.trading_halted);
assert!(report.positions_reduced);
assert!(!report.actions_executed.is_empty());
}
// ============================================================================
// SCENARIO 2: Model Disagreement >70% for 1 Hour
// ============================================================================
#[tokio::test]
async fn test_scenario_2_high_disagreement_detection() {
let automation = create_automation();
// Record sustained high disagreement
for _ in 0..15 {
automation.record_disagreement(0.75).await.unwrap();
sleep(Duration::from_millis(100)).await;
}
// Trigger scenario
automation.trigger_scenario_manual(RollbackScenario::HighDisagreement).await.unwrap();
let state = automation.get_state().await;
assert!(state.active_scenarios.contains_key(&RollbackScenario::HighDisagreement));
}
#[tokio::test]
async fn test_scenario_2_baseline_revert_executed() {
let automation = create_automation();
// Trigger scenario
automation.trigger_scenario_manual(RollbackScenario::HighDisagreement).await.unwrap();
// Execute recovery
let config = automation.config.clone();
RollbackAutomation::execute_recovery_actions(&config, &automation.state).await.unwrap();
// Verify baseline mode
assert!(automation.is_baseline_mode_active().await);
let state = automation.get_state().await;
assert!(state.executed_actions.iter().any(|(a, _)| *a == RollbackAction::RevertToBaseline));
}
#[tokio::test]
async fn test_scenario_2_position_reduction() {
let automation = create_automation();
// Trigger scenario
automation.trigger_scenario_manual(RollbackScenario::HighDisagreement).await.unwrap();
// Execute recovery
let config = automation.config.clone();
RollbackAutomation::execute_recovery_actions(&config, &automation.state).await.unwrap();
// Verify position reduction
assert!(automation.are_positions_reduced().await);
}
#[tokio::test]
async fn test_scenario_2_disagreement_windowing() {
let automation = create_automation();
// Record disagreement within window
for _ in 0..10 {
automation.record_disagreement(0.75).await.unwrap();
}
let state = automation.get_state().await;
assert!(!state.disagreement_history.is_empty());
assert!(state.disagreement_history.len() <= 10);
}
#[tokio::test]
async fn test_scenario_2_recovery_time() {
let automation = create_automation();
// Trigger and recover
automation.trigger_scenario_manual(RollbackScenario::HighDisagreement).await.unwrap();
let config = automation.config.clone();
RollbackAutomation::execute_recovery_actions(&config, &automation.state).await.unwrap();
// Check recovery time
let duration = automation.get_recovery_duration().await;
assert!(duration.is_some());
assert!(duration.unwrap().as_secs() < 300);
}
// ============================================================================
// SCENARIO 3: Single Model >3 Consecutive Errors
// ============================================================================
#[tokio::test]
async fn test_scenario_3_model_failure_detection() {
let risk_manager = create_risk_manager().await;
// Simulate 3 consecutive errors for DQN
for _ in 0..3 {
risk_manager.record_prediction_result("DQN", false).await.unwrap();
}
// Check model health
let health = risk_manager.get_model_health("DQN").await.unwrap();
assert!(!health.enabled);
assert_eq!(health.consecutive_errors, 3);
}
#[tokio::test]
async fn test_scenario_3_model_disabled() {
let risk_manager = create_risk_manager().await;
let automation = create_automation()
.with_ensemble_risk_manager(Arc::clone(&risk_manager));
// Fail DQN model
for _ in 0..3 {
risk_manager.record_prediction_result("DQN", false).await.unwrap();
}
// Trigger scenario
automation.trigger_scenario_manual(RollbackScenario::ModelFailure).await.unwrap();
// Execute recovery
let config = automation.config.clone();
RollbackAutomation::execute_recovery_actions(&config, &automation.state).await.unwrap();
let state = automation.get_state().await;
assert!(state.executed_actions.iter().any(|(a, _)| *a == RollbackAction::DisableModels));
}
#[tokio::test]
async fn test_scenario_3_baseline_mode_activated() {
let risk_manager = create_risk_manager().await;
let automation = create_automation()
.with_ensemble_risk_manager(Arc::clone(&risk_manager));
// Trigger model failure
automation.trigger_scenario_manual(RollbackScenario::ModelFailure).await.unwrap();
// Execute recovery
let config = automation.config.clone();
RollbackAutomation::execute_recovery_actions(&config, &automation.state).await.unwrap();
// Verify baseline mode
assert!(automation.is_baseline_mode_active().await);
}
#[tokio::test]
async fn test_scenario_3_successful_prediction_resets_errors() {
let risk_manager = create_risk_manager().await;
// Record 2 errors
risk_manager.record_prediction_result("DQN", false).await.unwrap();
risk_manager.record_prediction_result("DQN", false).await.unwrap();
// Record success (should reset counter)
risk_manager.record_prediction_result("DQN", true).await.unwrap();
let health = risk_manager.get_model_health("DQN").await.unwrap();
assert!(health.enabled);
assert_eq!(health.consecutive_errors, 0);
}
#[tokio::test]
async fn test_scenario_3_recovery_time() {
let risk_manager = create_risk_manager().await;
let automation = create_automation()
.with_ensemble_risk_manager(Arc::clone(&risk_manager));
// Trigger and recover
automation.trigger_scenario_manual(RollbackScenario::ModelFailure).await.unwrap();
let config = automation.config.clone();
RollbackAutomation::execute_recovery_actions(&config, &automation.state).await.unwrap();
let duration = automation.get_recovery_duration().await;
assert!(duration.is_some());
assert!(duration.unwrap().as_secs() < 300);
}
// ============================================================================
// SCENARIO 4: Cascade Failure (2+ Models Fail)
// ============================================================================
#[tokio::test]
async fn test_scenario_4_cascade_failure_detection() {
let risk_manager = create_risk_manager().await;
// Fail DQN
for _ in 0..3 {
risk_manager.record_prediction_result("DQN", false).await.unwrap();
}
// Fail PPO
for _ in 0..3 {
risk_manager.record_prediction_result("PPO", false).await.unwrap();
}
// Check cascade state
let cascade_state = risk_manager.get_cascade_state().await;
assert!(cascade_state.is_cascading);
assert_eq!(cascade_state.failed_models.len(), 2);
}
#[tokio::test]
async fn test_scenario_4_emergency_halt() {
let risk_manager = create_risk_manager().await;
let automation = create_automation()
.with_ensemble_risk_manager(Arc::clone(&risk_manager));
// Trigger cascade failure
automation.trigger_scenario_manual(RollbackScenario::CascadeFailure).await.unwrap();
// Execute recovery
let config = automation.config.clone();
RollbackAutomation::execute_recovery_actions(&config, &automation.state).await.unwrap();
// Verify emergency halt
assert!(automation.is_trading_halted().await);
}
#[tokio::test]
async fn test_scenario_4_baseline_revert() {
let risk_manager = create_risk_manager().await;
let automation = create_automation()
.with_ensemble_risk_manager(Arc::clone(&risk_manager));
// Trigger cascade failure
automation.trigger_scenario_manual(RollbackScenario::CascadeFailure).await.unwrap();
// Execute recovery
let config = automation.config.clone();
RollbackAutomation::execute_recovery_actions(&config, &automation.state).await.unwrap();
// Verify baseline mode
assert!(automation.is_baseline_mode_active().await);
}
#[tokio::test]
async fn test_scenario_4_cascade_within_window() {
let risk_manager = create_risk_manager().await;
// Fail models within detection window
for _ in 0..3 {
risk_manager.record_prediction_result("DQN", false).await.unwrap();
}
sleep(Duration::from_millis(100)).await;
for _ in 0..3 {
risk_manager.record_prediction_result("PPO", false).await.unwrap();
}
let cascade_state = risk_manager.get_cascade_state().await;
assert!(cascade_state.is_cascading);
}
#[tokio::test]
async fn test_scenario_4_recovery_time() {
let risk_manager = create_risk_manager().await;
let automation = create_automation()
.with_ensemble_risk_manager(Arc::clone(&risk_manager));
// Trigger and recover
automation.trigger_scenario_manual(RollbackScenario::CascadeFailure).await.unwrap();
let config = automation.config.clone();
RollbackAutomation::execute_recovery_actions(&config, &automation.state).await.unwrap();
let duration = automation.get_recovery_duration().await;
assert!(duration.is_some());
assert!(duration.unwrap().as_secs() < 300);
}
// ============================================================================
// COMPREHENSIVE INTEGRATION TESTS
// ============================================================================
#[tokio::test]
async fn test_all_scenarios_sequential() {
let risk_manager = create_risk_manager().await;
let automation = create_automation()
.with_ensemble_risk_manager(Arc::clone(&risk_manager));
// Scenario 1: Daily loss
automation.update_daily_pnl(-2500.0).await.unwrap();
automation.trigger_scenario_manual(RollbackScenario::DailyLossExceeded).await.unwrap();
// Scenario 2: High disagreement
for _ in 0..10 {
automation.record_disagreement(0.75).await.unwrap();
}
automation.trigger_scenario_manual(RollbackScenario::HighDisagreement).await.unwrap();
// Scenario 3: Model failure
automation.trigger_scenario_manual(RollbackScenario::ModelFailure).await.unwrap();
// Scenario 4: Cascade failure
automation.trigger_scenario_manual(RollbackScenario::CascadeFailure).await.unwrap();
// Execute recovery
let config = automation.config.clone();
RollbackAutomation::execute_recovery_actions(&config, &automation.state).await.unwrap();
// Verify all actions executed
let state = automation.get_state().await;
assert!(state.trading_halted);
assert!(state.positions_reduced);
assert!(state.baseline_mode_active);
assert_eq!(state.active_scenarios.len(), 4);
}
#[tokio::test]
async fn test_recovery_report_generation() {
let automation = create_automation();
// Trigger scenarios
automation.trigger_scenario_manual(RollbackScenario::DailyLossExceeded).await.unwrap();
automation.trigger_scenario_manual(RollbackScenario::HighDisagreement).await.unwrap();
// Execute recovery
let config = automation.config.clone();
RollbackAutomation::execute_recovery_actions(&config, &automation.state).await.unwrap();
// Generate report
let state = automation.get_state().await;
let report = RollbackReport::from_state(&state);
assert_eq!(report.scenarios_triggered.len(), 2);
assert!(!report.actions_executed.is_empty());
assert!(report.recovery_duration.is_some());
}
#[tokio::test]
async fn test_success_criteria_validation() {
let automation = create_automation();
// Trigger and recover quickly
automation.trigger_scenario_manual(RollbackScenario::DailyLossExceeded).await.unwrap();
let config = automation.config.clone();
RollbackAutomation::execute_recovery_actions(&config, &automation.state).await.unwrap();
// Wait briefly
sleep(Duration::from_millis(100)).await;
// Complete recovery
let state = automation.get_state().await;
let report = RollbackReport::from_state(&state);
// Verify meets success criteria if recovery completed
if report.recovery_completed {
assert!(report.meets_success_criteria());
}
}
#[tokio::test]
async fn test_action_priority_execution() {
let automation = create_automation();
// Trigger cascade (should execute EmergencyHalt first)
automation.trigger_scenario_manual(RollbackScenario::CascadeFailure).await.unwrap();
let config = automation.config.clone();
RollbackAutomation::execute_recovery_actions(&config, &automation.state).await.unwrap();
let state = automation.get_state().await;
// Find EmergencyHalt action
let halt_idx = state.executed_actions.iter().position(|(a, _)| *a == RollbackAction::EmergencyHalt);
// Find other actions
let baseline_idx = state.executed_actions.iter().position(|(a, _)| *a == RollbackAction::RevertToBaseline);
// EmergencyHalt should execute before or at same time as baseline
if let (Some(halt), Some(baseline)) = (halt_idx, baseline_idx) {
assert!(halt <= baseline);
}
}
#[tokio::test]
async fn test_idempotent_action_execution() {
let automation = create_automation();
// Trigger scenario
automation.trigger_scenario_manual(RollbackScenario::DailyLossExceeded).await.unwrap();
// Execute recovery multiple times
let config = automation.config.clone();
for _ in 0..3 {
RollbackAutomation::execute_recovery_actions(&config, &automation.state).await.unwrap();
}
let state = automation.get_state().await;
// Each action should only be executed once
let halt_count = state.executed_actions.iter().filter(|(a, _)| *a == RollbackAction::EmergencyHalt).count();
assert_eq!(halt_count, 1);
}
#[tokio::test]
async fn test_reset_functionality() {
let automation = create_automation();
// Trigger scenarios and recover
automation.trigger_scenario_manual(RollbackScenario::DailyLossExceeded).await.unwrap();
automation.update_daily_pnl(-3000.0).await.unwrap();
let config = automation.config.clone();
RollbackAutomation::execute_recovery_actions(&config, &automation.state).await.unwrap();
// Reset
automation.reset_all().await.unwrap();
// Verify clean state
let state = automation.get_state().await;
assert_eq!(state.daily_pnl_usd, 0.0);
assert!(state.active_scenarios.is_empty());
assert!(state.executed_actions.is_empty());
assert!(!state.trading_halted);
assert!(!state.positions_reduced);
assert!(!state.baseline_mode_active);
}
#[tokio::test]
async fn test_disabled_automatic_rollback() {
let config = RollbackConfig {
enable_automatic_rollback: false,
..Default::default()
};
let automation = RollbackAutomation::new(config.clone());
// Trigger scenario
automation.trigger_scenario_manual(RollbackScenario::DailyLossExceeded).await.unwrap();
// Execute recovery (should do nothing)
RollbackAutomation::execute_recovery_actions(&config, &automation.state).await.unwrap();
let state = automation.get_state().await;
assert!(state.executed_actions.is_empty());
assert!(!state.trading_halted);
}
#[tokio::test]
async fn test_daily_reset() {
let automation = create_automation();
// Set P&L
automation.update_daily_pnl(-1500.0).await.unwrap();
// Reset daily
automation.reset_daily().await.unwrap();
let state = automation.get_state().await;
assert_eq!(state.daily_pnl_usd, 0.0);
}
#[tokio::test]
async fn test_recovery_timeout_detection() {
let config = RollbackConfig {
recovery_timeout_secs: 1, // 1 second timeout
..Default::default()
};
let automation = RollbackAutomation::new(config.clone());
// Trigger scenario (starts recovery)
automation.trigger_scenario_manual(RollbackScenario::DailyLossExceeded).await.unwrap();
RollbackAutomation::execute_recovery_actions(&config, &automation.state).await.unwrap();
// Wait for timeout
sleep(Duration::from_secs(2)).await;
// Check timeout (would be logged as error)
let duration = automation.get_recovery_duration().await;
assert!(duration.is_some());
assert!(duration.unwrap().as_secs() >= 1);
}
// ============================================================================
// STRESS TESTS
// ============================================================================
#[tokio::test]
async fn test_rapid_scenario_triggers() {
let automation = create_automation();
// Rapidly trigger scenarios
for _ in 0..10 {
automation.trigger_scenario_manual(RollbackScenario::DailyLossExceeded).await.unwrap();
sleep(Duration::from_millis(10)).await;
}
let state = automation.get_state().await;
assert!(state.active_scenarios.contains_key(&RollbackScenario::DailyLossExceeded));
}
#[tokio::test]
async fn test_concurrent_disagreement_recording() {
let automation = Arc::new(create_automation());
// Spawn concurrent tasks recording disagreement
let mut handles = vec![];
for i in 0..10 {
let auto = Arc::clone(&automation);
let handle = tokio::spawn(async move {
auto.record_disagreement(0.75 + (i as f64 * 0.01)).await.unwrap();
});
handles.push(handle);
}
// Wait for all tasks
for handle in handles {
handle.await.unwrap();
}
let state = automation.get_state().await;
assert!(!state.disagreement_history.is_empty());
}
#[tokio::test]
async fn test_high_frequency_pnl_updates() {
let automation = create_automation();
// Rapidly update P&L
for i in 0..100 {
let pnl = -1000.0 - (i as f64 * 10.0);
automation.update_daily_pnl(pnl).await.unwrap();
}
let state = automation.get_state().await;
assert!(state.daily_pnl_usd < -2000.0);
}