//! Chaos Tests for Coherence Engine //! //! Tests system behavior under adversarial and random conditions: //! - Random energy spikes //! - Throttling behavior under load //! - Recovery from extreme states //! - Concurrent modifications //! - Edge case handling use rand::prelude::*; use rand_chacha::ChaCha8Rng; use std::collections::HashMap; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::{Arc, Mutex}; use std::thread; use std::time::{Duration, Instant}; // ============================================================================ // TEST INFRASTRUCTURE // ============================================================================ /// Coherence gate with throttling #[derive(Clone)] struct ThrottledGate { green_threshold: f32, amber_threshold: f32, red_threshold: f32, current_throttle: f32, // 0.0 = no throttle, 1.0 = max throttle blocked_count: u64, throttled_count: u64, allowed_count: u64, } impl ThrottledGate { fn new(green: f32, amber: f32, red: f32) -> Self { Self { green_threshold: green, amber_threshold: amber, red_threshold: red, current_throttle: 0.0, blocked_count: 0, throttled_count: 0, allowed_count: 0, } } fn decide(&mut self, energy: f32) -> Decision { if energy < self.green_threshold { self.current_throttle = (self.current_throttle - 0.1).max(0.0); self.allowed_count += 1; Decision::Allow } else if energy < self.amber_threshold { let throttle_factor = (energy - self.green_threshold) / (self.amber_threshold - self.green_threshold); self.current_throttle = (self.current_throttle + throttle_factor * 0.1).min(1.0); self.throttled_count += 1; Decision::Throttle { factor: throttle_factor, } } else { self.current_throttle = 1.0; self.blocked_count += 1; Decision::Block } } fn should_process(&self, rng: &mut impl Rng) -> bool { if self.current_throttle <= 0.0 { true } else { rng.gen::() > self.current_throttle } } fn stats(&self) -> GateStats { let total = self.allowed_count + self.throttled_count + self.blocked_count; GateStats { total_decisions: total, allowed: self.allowed_count, throttled: self.throttled_count, blocked: self.blocked_count, allow_rate: if total > 0 { self.allowed_count as f64 / total as f64 } else { 1.0 }, block_rate: if total > 0 { self.blocked_count as f64 / total as f64 } else { 0.0 }, } } } #[derive(Debug, Clone, Copy, PartialEq)] enum Decision { Allow, Throttle { factor: f32 }, Block, } #[derive(Debug)] struct GateStats { total_decisions: u64, allowed: u64, throttled: u64, blocked: u64, allow_rate: f64, block_rate: f64, } /// Simple coherence state for chaos testing struct ChaosState { nodes: HashMap>, edges: HashMap<(u64, u64), f32>, operation_count: AtomicU64, } impl ChaosState { fn new() -> Self { Self { nodes: HashMap::new(), edges: HashMap::new(), operation_count: AtomicU64::new(0), } } fn add_node(&mut self, id: u64, state: Vec) { self.nodes.insert(id, state); self.operation_count.fetch_add(1, Ordering::Relaxed); } fn add_edge(&mut self, src: u64, tgt: u64, weight: f32) { if self.nodes.contains_key(&src) && self.nodes.contains_key(&tgt) { self.edges.insert((src, tgt), weight); self.operation_count.fetch_add(1, Ordering::Relaxed); } } fn compute_energy(&self) -> f32 { let mut total = 0.0; for ((src, tgt), weight) in &self.edges { if let (Some(s), Some(t)) = (self.nodes.get(src), self.nodes.get(tgt)) { let dim = s.len().min(t.len()); let residual: f32 = s .iter() .take(dim) .zip(t.iter().take(dim)) .map(|(a, b)| (a - b).powi(2)) .sum(); total += weight * residual; } } total } fn perturb_node(&mut self, id: u64, rng: &mut impl Rng) { if let Some(state) = self.nodes.get_mut(&id) { for val in state.iter_mut() { *val += rng.gen_range(-0.1..0.1); } self.operation_count.fetch_add(1, Ordering::Relaxed); } } } // ============================================================================ // CHAOS: RANDOM ENERGY SPIKES // ============================================================================ #[test] fn test_random_energy_spikes() { let mut rng = ChaCha8Rng::seed_from_u64(42); let mut gate = ThrottledGate::new(0.1, 0.5, 1.0); let mut energies = Vec::new(); let mut decisions = Vec::new(); // Generate random energy values with occasional spikes for _ in 0..1000 { let base = rng.gen_range(0.0..0.2); let spike = if rng.gen_bool(0.1) { rng.gen_range(0.0..2.0) // 10% chance of spike } else { 0.0 }; let energy = base + spike; energies.push(energy); decisions.push(gate.decide(energy)); } let stats = gate.stats(); // Verify system handled spikes appropriately // With 10% spike rate and spikes going up to 2.0 (well above amber threshold), // we expect a mix of decisions assert!(stats.blocked > 0, "Should have blocked some spikes"); assert!( stats.allowed > 0, "Should have allowed low-energy operations" ); // Allow rate depends on threshold settings - with spikes going to amber/red zone, // we expect at least some operations to be allowed (the 90% non-spike operations) assert!( stats.allow_rate > 0.3, "Should have allowed at least 30% of operations (got {})", stats.allow_rate ); } #[test] fn test_sustained_spike_triggers_persistent_block() { let mut rng = ChaCha8Rng::seed_from_u64(123); let mut gate = ThrottledGate::new(0.1, 0.5, 1.0); // Normal operations for _ in 0..50 { let energy = rng.gen_range(0.0..0.1); gate.decide(energy); } assert!( gate.current_throttle < 0.1, "Should have low throttle initially" ); // Sustained high energy for _ in 0..20 { gate.decide(0.8); } assert!( gate.current_throttle > 0.5, "Should have high throttle after sustained spikes" ); // Verify recovery is gradual let throttle_before = gate.current_throttle; for _ in 0..10 { gate.decide(0.05); } assert!( gate.current_throttle < throttle_before, "Throttle should decrease after normal operations" ); } #[test] fn test_spike_patterns() { let mut rng = ChaCha8Rng::seed_from_u64(456); let mut gate = ThrottledGate::new(0.1, 0.5, 1.0); // Pattern 1: Regular low-high oscillation for i in 0..100 { let energy = if i % 2 == 0 { 0.05 } else { 0.8 }; gate.decide(energy); } let stats1 = gate.stats(); // Reset gate = ThrottledGate::new(0.1, 0.5, 1.0); // Pattern 2: Bursts for burst in 0..10 { // Low energy burst for _ in 0..8 { gate.decide(0.05); } // High energy burst for _ in 0..2 { gate.decide(0.9); } } let stats2 = gate.stats(); // Both patterns have the same 20% high-energy ratio but different distributions. // Pattern 1: random distribution across iterations // Pattern 2: burst pattern (8 low, 2 high per burst) // The key invariant is that both should have some blocks from the high-energy operations assert!(stats1.blocked > 0, "Pattern 1 should have blocks"); assert!(stats2.blocked > 0, "Pattern 2 should have blocks"); // Both should process 100 operations assert_eq!(stats1.total_decisions, 100); assert_eq!(stats2.total_decisions, 100); } // ============================================================================ // CHAOS: THROTTLING UNDER LOAD // ============================================================================ #[test] fn test_throttling_fairness() { let mut rng = ChaCha8Rng::seed_from_u64(789); let mut gate = ThrottledGate::new(0.1, 0.5, 1.0); // Put gate into throttled state for _ in 0..10 { gate.decide(0.3); } // Count how many requests get through let mut processed = 0; let mut total = 0; for _ in 0..1000 { total += 1; if gate.should_process(&mut rng) { processed += 1; } } let process_rate = processed as f64 / total as f64; // Should be roughly inverse of throttle let expected_rate = 1.0 - gate.current_throttle as f64; assert!( (process_rate - expected_rate).abs() < 0.1, "Process rate {} should be close to expected {}", process_rate, expected_rate ); } #[test] fn test_throttling_response_time() { let mut gate = ThrottledGate::new(0.1, 0.5, 1.0); // Measure response time under different throttle states let measure_response = |gate: &mut ThrottledGate, energy: f32| { let start = Instant::now(); for _ in 0..100 { gate.decide(energy); } start.elapsed() }; let low_energy_time = measure_response(&mut gate, 0.05); gate = ThrottledGate::new(0.1, 0.5, 1.0); let high_energy_time = measure_response(&mut gate, 0.8); // Decision time should be similar regardless of energy level let ratio = high_energy_time.as_nanos() as f64 / low_energy_time.as_nanos() as f64; assert!( ratio < 10.0, "High energy decisions shouldn't be much slower (ratio: {})", ratio ); } #[test] fn test_progressive_throttling() { let mut gate = ThrottledGate::new(0.1, 0.5, 1.0); let mut throttle_history = Vec::new(); // Gradually increase energy for i in 0..100 { let energy = i as f32 / 100.0; // 0.0 to 1.0 gate.decide(energy); throttle_history.push(gate.current_throttle); } // Throttle should generally increase let increasing_segments = throttle_history .windows(10) .filter(|w| w.last() > w.first()) .count(); assert!( increasing_segments > 5, "Throttle should generally increase with energy" ); } // ============================================================================ // CHAOS: CONCURRENT MODIFICATIONS // ============================================================================ #[test] fn test_concurrent_state_modifications() { let state = Arc::new(Mutex::new(ChaosState::new())); // Initialize some nodes { let mut s = state.lock().unwrap(); for i in 0..100 { s.add_node(i, vec![i as f32 / 100.0; 4]); } for i in 0..99 { s.add_edge(i, i + 1, 1.0); } } // Spawn threads that concurrently modify state let handles: Vec<_> = (0..4) .map(|thread_id| { let state = Arc::clone(&state); thread::spawn(move || { let mut rng = ChaCha8Rng::seed_from_u64(thread_id); for _ in 0..100 { let mut s = state.lock().unwrap(); let node_id = rng.gen_range(0..100); s.perturb_node(node_id, &mut rng); } }) }) .collect(); for h in handles { h.join().unwrap(); } // State should still be valid let s = state.lock().unwrap(); assert_eq!(s.nodes.len(), 100); assert_eq!(s.edges.len(), 99); // Energy should be computable let energy = s.compute_energy(); assert!(energy.is_finite(), "Energy should be finite"); } #[test] fn test_concurrent_energy_computation() { let state = Arc::new(Mutex::new(ChaosState::new())); // Initialize { let mut s = state.lock().unwrap(); for i in 0..50 { s.add_node(i, vec![i as f32 / 50.0; 4]); } for i in 0..49 { s.add_edge(i, i + 1, 1.0); } } // Concurrent energy computations let handles: Vec<_> = (0..8) .map(|_| { let state = Arc::clone(&state); thread::spawn(move || { let s = state.lock().unwrap(); s.compute_energy() }) }) .collect(); let energies: Vec = handles.into_iter().map(|h| h.join().unwrap()).collect(); // All computations should give the same result let first = energies[0]; for e in &energies { assert!( (e - first).abs() < 1e-6, "Concurrent computations should give same result" ); } } // ============================================================================ // CHAOS: EXTREME VALUES // ============================================================================ #[test] fn test_extreme_energy_values() { let mut gate = ThrottledGate::new(0.1, 0.5, 1.0); // Very small energy let decision = gate.decide(1e-10); assert_eq!(decision, Decision::Allow); // Very large energy let decision = gate.decide(1e10); assert_eq!(decision, Decision::Block); // Zero let decision = gate.decide(0.0); assert_eq!(decision, Decision::Allow); // Negative (should still work, though unusual) let decision = gate.decide(-0.1); assert_eq!(decision, Decision::Allow); // Less than green threshold } #[test] fn test_extreme_state_values() { let mut state = ChaosState::new(); // Very large state values state.add_node(1, vec![1e10, -1e10, 1e10, -1e10]); state.add_node(2, vec![-1e10, 1e10, -1e10, 1e10]); state.add_edge(1, 2, 1.0); let energy = state.compute_energy(); assert!(energy.is_finite(), "Energy should handle large values"); assert!(energy > 0.0, "Energy should be positive"); } #[test] fn test_many_small_perturbations() { let mut rng = ChaCha8Rng::seed_from_u64(999); let mut state = ChaosState::new(); // Create a stable baseline state.add_node(1, vec![0.5, 0.5, 0.5, 0.5]); state.add_node(2, vec![0.5, 0.5, 0.5, 0.5]); state.add_edge(1, 2, 1.0); let initial_energy = state.compute_energy(); // Many small perturbations for _ in 0..1000 { state.perturb_node(1, &mut rng); state.perturb_node(2, &mut rng); } let final_energy = state.compute_energy(); // Energy should still be reasonable (not exploded) assert!(final_energy.is_finite()); // Random walk should increase variance assert!(final_energy > initial_energy * 0.1 || final_energy < initial_energy * 10.0); } // ============================================================================ // CHAOS: RECOVERY SCENARIOS // ============================================================================ #[test] fn test_recovery_from_blocked_state() { let mut rng = ChaCha8Rng::seed_from_u64(111); let mut gate = ThrottledGate::new(0.1, 0.5, 1.0); // Drive into blocked state for _ in 0..20 { gate.decide(0.9); } assert!( gate.current_throttle > 0.9, "Should be in high throttle state" ); // Recover with low energy let mut recovery_steps = 0; while gate.current_throttle > 0.1 && recovery_steps < 200 { gate.decide(0.05); recovery_steps += 1; } assert!( recovery_steps < 200, "Should recover within reasonable time" ); assert!( gate.current_throttle < 0.2, "Should have low throttle after recovery" ); } #[test] fn test_oscillation_dampening() { let mut gate = ThrottledGate::new(0.1, 0.5, 1.0); // Oscillate between extremes let mut throttle_variance = Vec::new(); for cycle in 0..10 { // High phase for _ in 0..5 { gate.decide(0.8); } // Low phase for _ in 0..5 { gate.decide(0.05); } throttle_variance.push(gate.current_throttle); } // Throttle should not oscillate wildly let max = throttle_variance .iter() .cloned() .fold(f32::NEG_INFINITY, f32::max); let min = throttle_variance .iter() .cloned() .fold(f32::INFINITY, f32::min); // Should settle to some stable-ish range // (This is a soft check - exact behavior depends on parameters) assert!(max - min < 1.0, "Throttle oscillation should be bounded"); } // ============================================================================ // CHAOS: RANDOM GRAPH MODIFICATIONS // ============================================================================ #[test] fn test_random_graph_operations() { let mut rng = ChaCha8Rng::seed_from_u64(222); let mut state = ChaosState::new(); // Random operations for _ in 0..1000 { let op = rng.gen_range(0..3); match op { 0 => { // Add node let id = rng.gen_range(0..100); let dim = rng.gen_range(2..8); let values: Vec = (0..dim).map(|_| rng.gen_range(-1.0..1.0)).collect(); state.add_node(id, values); } 1 => { // Add edge let src = rng.gen_range(0..100); let tgt = rng.gen_range(0..100); if src != tgt { let weight = rng.gen_range(0.1..2.0); state.add_edge(src, tgt, weight); } } 2 => { // Perturb existing node let id = rng.gen_range(0..100); state.perturb_node(id, &mut rng); } _ => {} } } // State should be valid assert!(state.nodes.len() <= 100); let energy = state.compute_energy(); assert!(energy.is_finite()); } // ============================================================================ // CHAOS: STRESS TESTS // ============================================================================ #[test] fn test_rapid_fire_decisions() { let mut rng = ChaCha8Rng::seed_from_u64(333); let mut gate = ThrottledGate::new(0.1, 0.5, 1.0); let start = Instant::now(); let mut count = 0; while start.elapsed() < Duration::from_millis(100) { let energy = rng.gen_range(0.0..0.6); gate.decide(energy); count += 1; } assert!(count > 1000, "Should process many decisions quickly"); let stats = gate.stats(); assert_eq!(stats.total_decisions, count); } #[test] fn test_memory_stability() { let mut rng = ChaCha8Rng::seed_from_u64(444); let mut state = ChaosState::new(); // Many cycles of add/modify for cycle in 0..100 { // Add phase for i in 0..10 { let id = cycle * 10 + i; state.add_node(id, vec![rng.gen::(); 4]); } // Modify phase for _ in 0..50 { let id = rng.gen_range(0..(cycle + 1) * 10); state.perturb_node(id, &mut rng); } // Energy check let energy = state.compute_energy(); assert!( energy.is_finite(), "Energy should be finite at cycle {}", cycle ); } assert!(state.nodes.len() > 0); } // ============================================================================ // CHAOS: DETERMINISTIC CHAOS (SEEDED RANDOM) // ============================================================================ #[test] fn test_seeded_chaos_reproducible() { fn run_chaos(seed: u64) -> (f32, u64, u64, u64) { let mut rng = ChaCha8Rng::seed_from_u64(seed); let mut gate = ThrottledGate::new(0.1, 0.5, 1.0); let mut state = ChaosState::new(); // Add nodes with random states for i in 0..100 { state.add_node(i, vec![rng.gen::(); 4]); } // Add edges to create energy (without edges, compute_energy is always 0) for i in 0..50 { let src = rng.gen_range(0..100); let tgt = rng.gen_range(0..100); if src != tgt { state.add_edge(src, tgt, rng.gen_range(0.1..1.0)); } } for _ in 0..500 { let energy = state.compute_energy(); gate.decide(energy / 100.0); let node_id = rng.gen_range(0..100); state.perturb_node(node_id, &mut rng); } let stats = gate.stats(); ( state.compute_energy(), stats.allowed, stats.throttled, stats.blocked, ) } let result1 = run_chaos(12345); let result2 = run_chaos(12345); // Same seed should produce same results (using approximate comparison for floats // due to potential floating point ordering differences) assert!( (result1.0 - result2.0).abs() < 0.01, "Same seed should produce same energy: {} vs {}", result1.0, result2.0 ); assert_eq!( result1.1, result2.1, "Same seed should produce same allowed count" ); assert_eq!( result1.2, result2.2, "Same seed should produce same throttled count" ); assert_eq!( result1.3, result2.3, "Same seed should produce same blocked count" ); // Use very different seeds to ensure different random sequences let result3 = run_chaos(99999); // At minimum, the final energy should differ between different seeds assert!( (result1.0 - result3.0).abs() > 0.001 || result1.1 != result3.1 || result1.2 != result3.2, "Different seeds should produce different results: seed1={:?}, seed2={:?}", result1, result3 ); }