Skip to main content

trustformers_debug/
error_recovery.rs

1//! Comprehensive error recovery mechanisms for debugging sessions
2
3use anyhow::Result;
4use serde::{Deserialize, Serialize};
5use std::collections::{HashMap, VecDeque};
6use std::time::{Duration, Instant};
7use uuid::Uuid;
8
9/// Comprehensive error recovery system
10#[derive(Debug)]
11pub struct ErrorRecoverySystem {
12    config: ErrorRecoveryConfig,
13    recovery_strategies: HashMap<ErrorType, Vec<RecoveryStrategy>>,
14    error_history: VecDeque<ErrorEvent>,
15    recovery_history: VecDeque<RecoveryEvent>,
16    circuit_breaker: CircuitBreaker,
17    health_monitor: SystemHealthMonitor,
18    failsafe_manager: FailsafeManager,
19}
20
21/// Configuration for error recovery
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct ErrorRecoveryConfig {
24    pub enabled: bool,
25    pub max_retry_attempts: usize,
26    pub retry_delay_ms: u64,
27    pub circuit_breaker_threshold: usize,
28    pub health_check_interval_ms: u64,
29    pub auto_failsafe_enabled: bool,
30    pub error_history_limit: usize,
31    pub recovery_timeout_ms: u64,
32}
33
34impl Default for ErrorRecoveryConfig {
35    fn default() -> Self {
36        Self {
37            enabled: true,
38            max_retry_attempts: 3,
39            retry_delay_ms: 100,
40            circuit_breaker_threshold: 5,
41            health_check_interval_ms: 5000,
42            auto_failsafe_enabled: true,
43            error_history_limit: 1000,
44            recovery_timeout_ms: 30000,
45        }
46    }
47}
48
49/// Types of errors that can occur during debugging
50#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
51pub enum ErrorType {
52    TensorInspectionError,
53    GradientDebuggingError,
54    ModelDiagnosticsError,
55    VisualizationError,
56    MemoryProfilingError,
57    IOError,
58    NetworkError,
59    ResourceExhaustion,
60    ConfigurationError,
61    DataCorruption,
62    SystemFailure,
63    UserError,
64}
65
66/// Recovery strategies for different error types
67#[derive(Debug, Clone, Serialize, Deserialize)]
68pub enum RecoveryStrategy {
69    Retry { max_attempts: usize, delay_ms: u64 },
70    Fallback { alternative_method: String },
71    GracefulDegradation { reduced_functionality: String },
72    ResourceCleanup { cleanup_type: String },
73    SystemReset { component: String },
74    EmergencyShutdown,
75    UserNotification { message: String },
76    AutomaticRepair { repair_action: String },
77}
78
79/// Error event record
80#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct ErrorEvent {
82    pub id: Uuid,
83    pub error_type: ErrorType,
84    pub error_message: String,
85    pub component: String,
86    pub severity: ErrorSeverity,
87    pub timestamp: chrono::DateTime<chrono::Utc>,
88    pub context: ErrorContext,
89    pub stack_trace: Option<String>,
90}
91
92/// Error severity levels
93#[derive(Debug, Clone, Serialize, Deserialize)]
94pub enum ErrorSeverity {
95    Low,
96    Medium,
97    High,
98    Critical,
99    Fatal,
100}
101
102/// Context information for errors
103#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct ErrorContext {
105    pub session_id: Uuid,
106    pub operation: String,
107    pub parameters: HashMap<String, String>,
108    pub system_state: SystemState,
109}
110
111/// System state at time of error
112#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct SystemState {
114    pub memory_usage_mb: u64,
115    pub cpu_usage_percent: f64,
116    pub active_tensors: usize,
117    pub active_sessions: usize,
118    pub uptime_seconds: u64,
119}
120
121/// Recovery event record
122#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct RecoveryEvent {
124    pub id: Uuid,
125    pub error_id: Uuid,
126    pub strategy: RecoveryStrategy,
127    pub start_time: chrono::DateTime<chrono::Utc>,
128    pub end_time: Option<chrono::DateTime<chrono::Utc>>,
129    pub success: Option<bool>,
130    pub result_message: String,
131    pub attempts: usize,
132}
133
134/// Circuit breaker for preventing cascading failures
135#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct CircuitBreaker {
137    pub state: CircuitState,
138    pub failure_count: usize,
139    pub last_failure_time: Option<chrono::DateTime<chrono::Utc>>,
140    pub threshold: usize,
141    pub timeout_duration: Duration,
142}
143
144/// Circuit breaker states
145#[derive(Debug, Clone, Serialize, Deserialize)]
146pub enum CircuitState {
147    Closed,
148    Open,
149    HalfOpen,
150}
151
152/// System health monitoring
153#[derive(Debug, Clone, Serialize, Deserialize)]
154pub struct SystemHealthMonitor {
155    pub overall_health: HealthStatus,
156    pub component_health: HashMap<String, HealthStatus>,
157    pub last_health_check: chrono::DateTime<chrono::Utc>,
158    pub health_metrics: HealthMetrics,
159}
160
161/// Health status
162#[derive(Debug, Clone, Serialize, Deserialize)]
163pub enum HealthStatus {
164    Healthy,
165    Degraded,
166    Unhealthy,
167    Critical,
168}
169
170/// Health metrics
171#[derive(Debug, Clone, Serialize, Deserialize)]
172pub struct HealthMetrics {
173    /// Errors recorded in the last five minutes, normalised by 100.
174    /// Genuinely recomputed by [`ErrorRecoverySystem::check_system_health`].
175    pub error_rate: f64,
176    /// Fraction of the recovery attempts that finished in the last five
177    /// minutes and reported success, or `None` when none finished in that
178    /// window.
179    ///
180    /// This used to be seeded to `1.0` in
181    /// [`ErrorRecoverySystem::new`] -- a published 100% recovery rate before a
182    /// single error had been handled -- and only ever overwritten once a
183    /// recovery actually completed.
184    pub recovery_success_rate: Option<f64>,
185    /// Mean wall-clock duration of those same completed recovery attempts, in
186    /// milliseconds; `None` when none finished in the window.
187    ///
188    /// Renamed from `average_response_time_ms`, which nothing ever wrote to:
189    /// it stayed at its `0.0` seed for the lifetime of the system. It is
190    /// recovery time, not response time.
191    pub average_recovery_time_ms: Option<f64>,
192    /// Always `None`: nothing in this crate measures memory health, and there
193    /// is no defined scale for it. Previously seeded to `1.0` and never
194    /// updated, i.e. a permanent "perfectly healthy" verdict.
195    pub memory_health_score: Option<f64>,
196    /// Always `None`, for the same reason (previously a permanent `1.0`).
197    pub stability_score: Option<f64>,
198}
199
200/// Failsafe manager for critical situations
201#[derive(Debug, Clone, Serialize, Deserialize)]
202pub struct FailsafeManager {
203    pub enabled: bool,
204    pub emergency_protocols: Vec<EmergencyProtocol>,
205    pub safe_mode_enabled: bool,
206    pub data_backup_enabled: bool,
207    pub last_backup: Option<chrono::DateTime<chrono::Utc>>,
208}
209
210/// Emergency protocols
211#[derive(Debug, Clone, Serialize, Deserialize)]
212pub struct EmergencyProtocol {
213    pub name: String,
214    pub trigger_conditions: Vec<String>,
215    pub actions: Vec<String>,
216    pub priority: u8,
217}
218
219impl ErrorRecoverySystem {
220    /// Create a new error recovery system
221    pub fn new(config: ErrorRecoveryConfig) -> Self {
222        let mut system = Self {
223            config,
224            recovery_strategies: HashMap::new(),
225            error_history: VecDeque::new(),
226            recovery_history: VecDeque::new(),
227            circuit_breaker: CircuitBreaker {
228                state: CircuitState::Closed,
229                failure_count: 0,
230                last_failure_time: None,
231                threshold: 5,
232                timeout_duration: Duration::from_secs(60),
233            },
234            health_monitor: SystemHealthMonitor {
235                overall_health: HealthStatus::Healthy,
236                component_health: HashMap::new(),
237                last_health_check: chrono::Utc::now(),
238                health_metrics: HealthMetrics {
239                    // Nothing has happened yet: zero errors really is a
240                    // measured zero, everything else is honestly absent.
241                    error_rate: 0.0,
242                    recovery_success_rate: None,
243                    average_recovery_time_ms: None,
244                    memory_health_score: None,
245                    stability_score: None,
246                },
247            },
248            failsafe_manager: FailsafeManager {
249                enabled: true,
250                emergency_protocols: Vec::new(),
251                safe_mode_enabled: false,
252                data_backup_enabled: true,
253                last_backup: None,
254            },
255        };
256
257        system.initialize_default_strategies();
258        system.initialize_emergency_protocols();
259        system
260    }
261
262    /// Handle an error event and attempt recovery
263    pub async fn handle_error(&mut self, error: ErrorEvent) -> Result<RecoveryResult> {
264        // Check circuit breaker
265        if matches!(self.circuit_breaker.state, CircuitState::Open) {
266            return Ok(RecoveryResult {
267                success: false,
268                strategy_used: None,
269                message: "Circuit breaker is open - recovery attempts suspended".to_string(),
270                recovery_time: Duration::from_millis(0),
271            });
272        }
273
274        // Record error
275        self.record_error(error.clone());
276
277        // Check if emergency protocols should be triggered
278        if self.should_trigger_emergency_protocol(&error) {
279            return self.execute_emergency_protocol(&error).await;
280        }
281
282        // Attempt recovery
283        let recovery_result = self.attempt_recovery(&error).await?;
284
285        // Update circuit breaker and health monitor
286        self.update_circuit_breaker(&recovery_result);
287        self.update_health_metrics(&error, &recovery_result);
288
289        Ok(recovery_result)
290    }
291
292    /// Record an error event
293    pub fn record_error(&mut self, error: ErrorEvent) {
294        self.error_history.push_back(error);
295
296        // Maintain history limit
297        while self.error_history.len() > self.config.error_history_limit {
298            self.error_history.pop_front();
299        }
300    }
301
302    /// Attempt recovery using appropriate strategies
303    pub async fn attempt_recovery(&mut self, error: &ErrorEvent) -> Result<RecoveryResult> {
304        let strategies = self.get_recovery_strategies(&error.error_type);
305
306        for (attempt, strategy) in strategies.iter().enumerate() {
307            if attempt >= self.config.max_retry_attempts {
308                break;
309            }
310
311            let recovery_event = RecoveryEvent {
312                id: Uuid::new_v4(),
313                error_id: error.id,
314                strategy: strategy.clone(),
315                start_time: chrono::Utc::now(),
316                end_time: None,
317                success: None,
318                result_message: String::new(),
319                attempts: attempt + 1,
320            };
321
322            let result = self.execute_recovery_strategy(strategy, error).await?;
323
324            let mut updated_event = recovery_event;
325            updated_event.end_time = Some(chrono::Utc::now());
326            updated_event.success = Some(result.success);
327            updated_event.result_message = result.message.clone();
328
329            self.recovery_history.push_back(updated_event);
330
331            if result.success {
332                return Ok(result);
333            }
334
335            // Wait before next attempt
336            if attempt < strategies.len() - 1 {
337                tokio::time::sleep(Duration::from_millis(self.config.retry_delay_ms)).await;
338            }
339        }
340
341        Ok(RecoveryResult {
342            success: false,
343            strategy_used: None,
344            message: "All recovery strategies failed".to_string(),
345            recovery_time: Duration::from_millis(0),
346        })
347    }
348
349    /// Execute a specific recovery strategy
350    pub async fn execute_recovery_strategy(
351        &self,
352        strategy: &RecoveryStrategy,
353        error: &ErrorEvent,
354    ) -> Result<RecoveryResult> {
355        let start_time = Instant::now();
356
357        let result = match strategy {
358            RecoveryStrategy::Retry {
359                max_attempts,
360                delay_ms,
361            } => self.execute_retry_strategy(*max_attempts, *delay_ms, error).await,
362            RecoveryStrategy::Fallback { alternative_method } => {
363                self.execute_fallback_strategy(alternative_method, error).await
364            },
365            RecoveryStrategy::GracefulDegradation {
366                reduced_functionality,
367            } => self.execute_degradation_strategy(reduced_functionality, error).await,
368            RecoveryStrategy::ResourceCleanup { cleanup_type } => {
369                self.execute_cleanup_strategy(cleanup_type, error).await
370            },
371            RecoveryStrategy::SystemReset { component } => {
372                self.execute_reset_strategy(component, error).await
373            },
374            RecoveryStrategy::EmergencyShutdown => self.execute_shutdown_strategy(error).await,
375            RecoveryStrategy::UserNotification { message } => {
376                self.execute_notification_strategy(message, error).await
377            },
378            RecoveryStrategy::AutomaticRepair { repair_action } => {
379                self.execute_repair_strategy(repair_action, error).await
380            },
381        };
382
383        let recovery_time = start_time.elapsed();
384
385        match result {
386            Ok(mut recovery_result) => {
387                recovery_result.recovery_time = recovery_time;
388                recovery_result.strategy_used = Some(strategy.clone());
389                Ok(recovery_result)
390            },
391            Err(e) => Ok(RecoveryResult {
392                success: false,
393                strategy_used: Some(strategy.clone()),
394                message: format!("Recovery strategy failed: {}", e),
395                recovery_time,
396            }),
397        }
398    }
399
400    /// Get recovery strategies for an error type
401    pub fn get_recovery_strategies(&self, error_type: &ErrorType) -> Vec<RecoveryStrategy> {
402        self.recovery_strategies.get(error_type).cloned().unwrap_or_default()
403    }
404
405    /// Check system health
406    pub async fn check_system_health(&mut self) -> HealthStatus {
407        // Update health metrics
408        self.health_monitor.last_health_check = chrono::Utc::now();
409
410        // Calculate error rate from recent history
411        let recent_errors = self
412            .error_history
413            .iter()
414            .filter(|e| {
415                let age = chrono::Utc::now() - e.timestamp;
416                age < chrono::Duration::minutes(5)
417            })
418            .count();
419
420        self.health_monitor.health_metrics.error_rate = recent_errors as f64 / 100.0; // Normalized
421
422        // Calculate recovery success rate
423        let recent_recoveries = self
424            .recovery_history
425            .iter()
426            .filter(|r| {
427                if let Some(end_time) = r.end_time {
428                    let age = chrono::Utc::now() - end_time;
429                    age < chrono::Duration::minutes(5)
430                } else {
431                    false
432                }
433            })
434            .collect::<Vec<_>>();
435
436        if recent_recoveries.is_empty() {
437            // No completed recovery in the window: the previous figures are
438            // stale, and there is nothing to replace them with.
439            self.health_monitor.health_metrics.recovery_success_rate = None;
440            self.health_monitor.health_metrics.average_recovery_time_ms = None;
441        } else {
442            let successful_recoveries =
443                recent_recoveries.iter().filter(|r| r.success.unwrap_or(false)).count();
444            self.health_monitor.health_metrics.recovery_success_rate =
445                Some(successful_recoveries as f64 / recent_recoveries.len() as f64);
446            // Real mean wall-clock recovery duration over the same window.
447            let total_ms: f64 = recent_recoveries
448                .iter()
449                .filter_map(|r| {
450                    r.end_time.map(|end| (end - r.start_time).num_milliseconds() as f64)
451                })
452                .sum();
453            self.health_monitor.health_metrics.average_recovery_time_ms =
454                Some(total_ms / recent_recoveries.len() as f64);
455        }
456
457        // Determine overall health
458        self.health_monitor.overall_health = if self.health_monitor.health_metrics.error_rate > 0.5
459        {
460            HealthStatus::Critical
461        } else if self.health_monitor.health_metrics.error_rate > 0.2 {
462            HealthStatus::Unhealthy
463        } else if self.health_monitor.health_metrics.error_rate > 0.1 {
464            HealthStatus::Degraded
465        } else {
466            HealthStatus::Healthy
467        };
468
469        self.health_monitor.overall_health.clone()
470    }
471
472    /// Enable safe mode
473    pub fn enable_safe_mode(&mut self) {
474        self.failsafe_manager.safe_mode_enabled = true;
475        tracing::warn!("Safe mode enabled - operating with reduced functionality");
476    }
477
478    /// Disable safe mode
479    pub fn disable_safe_mode(&mut self) {
480        self.failsafe_manager.safe_mode_enabled = false;
481        tracing::info!("Safe mode disabled - full functionality restored");
482    }
483
484    /// Get error statistics
485    pub fn get_error_statistics(&self) -> ErrorStatistics {
486        let total_errors = self.error_history.len();
487        let error_type_counts = self.error_history.iter().fold(HashMap::new(), |mut acc, error| {
488            *acc.entry(error.error_type.clone()).or_insert(0) += 1;
489            acc
490        });
491
492        let severity_counts = self.error_history.iter().fold(HashMap::new(), |mut acc, error| {
493            *acc.entry(format!("{:?}", error.severity)).or_insert(0) += 1;
494            acc
495        });
496
497        ErrorStatistics {
498            total_errors,
499            error_type_counts,
500            severity_counts,
501            recovery_success_rate: self.health_monitor.health_metrics.recovery_success_rate,
502            circuit_breaker_state: self.circuit_breaker.state.clone(),
503            system_health: self.health_monitor.overall_health.clone(),
504        }
505    }
506
507    // Private helper methods
508
509    fn initialize_default_strategies(&mut self) {
510        // Initialize default recovery strategies for each error type
511        self.recovery_strategies.insert(
512            ErrorType::TensorInspectionError,
513            vec![
514                RecoveryStrategy::Retry {
515                    max_attempts: 3,
516                    delay_ms: 100,
517                },
518                RecoveryStrategy::ResourceCleanup {
519                    cleanup_type: "tensor_cache".to_string(),
520                },
521                RecoveryStrategy::Fallback {
522                    alternative_method: "simplified_inspection".to_string(),
523                },
524            ],
525        );
526
527        self.recovery_strategies.insert(
528            ErrorType::GradientDebuggingError,
529            vec![
530                RecoveryStrategy::Retry {
531                    max_attempts: 2,
532                    delay_ms: 200,
533                },
534                RecoveryStrategy::GracefulDegradation {
535                    reduced_functionality: "basic_gradient_info".to_string(),
536                },
537            ],
538        );
539
540        self.recovery_strategies.insert(
541            ErrorType::MemoryProfilingError,
542            vec![
543                RecoveryStrategy::ResourceCleanup {
544                    cleanup_type: "memory_profiler".to_string(),
545                },
546                RecoveryStrategy::SystemReset {
547                    component: "memory_tracker".to_string(),
548                },
549            ],
550        );
551
552        self.recovery_strategies.insert(
553            ErrorType::ResourceExhaustion,
554            vec![
555                RecoveryStrategy::ResourceCleanup {
556                    cleanup_type: "all_caches".to_string(),
557                },
558                RecoveryStrategy::GracefulDegradation {
559                    reduced_functionality: "essential_only".to_string(),
560                },
561                RecoveryStrategy::EmergencyShutdown,
562            ],
563        );
564
565        // Add more strategies for other error types...
566    }
567
568    fn initialize_emergency_protocols(&mut self) {
569        self.failsafe_manager.emergency_protocols = vec![
570            EmergencyProtocol {
571                name: "Memory Exhaustion Protocol".to_string(),
572                trigger_conditions: vec!["memory_usage > 90%".to_string()],
573                actions: vec![
574                    "clear_all_caches".to_string(),
575                    "reduce_tracking".to_string(),
576                ],
577                priority: 1,
578            },
579            EmergencyProtocol {
580                name: "Critical Error Protocol".to_string(),
581                trigger_conditions: vec!["error_severity == Fatal".to_string()],
582                actions: vec!["emergency_backup".to_string(), "safe_shutdown".to_string()],
583                priority: 0,
584            },
585        ];
586    }
587
588    fn should_trigger_emergency_protocol(&self, error: &ErrorEvent) -> bool {
589        matches!(error.severity, ErrorSeverity::Fatal)
590            || error.context.system_state.memory_usage_mb > 8192 // > 8GB
591    }
592
593    async fn execute_emergency_protocol(&mut self, error: &ErrorEvent) -> Result<RecoveryResult> {
594        tracing::error!(
595            "Executing emergency protocol for error: {}",
596            error.error_message
597        );
598
599        // Enable safe mode
600        self.enable_safe_mode();
601
602        // Execute emergency backup if enabled
603        if self.failsafe_manager.data_backup_enabled {
604            self.create_emergency_backup().await?;
605        }
606
607        Ok(RecoveryResult {
608            success: true,
609            strategy_used: Some(RecoveryStrategy::EmergencyShutdown),
610            message: "Emergency protocol executed successfully".to_string(),
611            recovery_time: Duration::from_millis(0),
612        })
613    }
614
615    async fn create_emergency_backup(&mut self) -> Result<()> {
616        tracing::info!("Creating emergency backup");
617        self.failsafe_manager.last_backup = Some(chrono::Utc::now());
618        // Implement backup logic here
619        Ok(())
620    }
621
622    fn update_circuit_breaker(&mut self, result: &RecoveryResult) {
623        if result.success {
624            self.circuit_breaker.failure_count = 0;
625            self.circuit_breaker.state = CircuitState::Closed;
626        } else {
627            self.circuit_breaker.failure_count += 1;
628            self.circuit_breaker.last_failure_time = Some(chrono::Utc::now());
629
630            if self.circuit_breaker.failure_count >= self.circuit_breaker.threshold {
631                self.circuit_breaker.state = CircuitState::Open;
632            }
633        }
634    }
635
636    fn update_health_metrics(&mut self, _error: &ErrorEvent, _result: &RecoveryResult) {
637        // Update health metrics based on error and recovery result
638        // This would include more sophisticated health scoring logic
639    }
640
641    // Recovery strategy execution.
642    //
643    // `ErrorRecoveryManager` records and classifies errors; it holds no handle
644    // to the subsystems a recovery would have to act on (no retryable operation
645    // closure, no allocator, no component registry, no process supervisor). The
646    // strategies below therefore report `success: false` with a message naming
647    // exactly what is not wired, instead of the `success: true` +
648    // "Retry successful" / "Resource cleanup completed" they used to return
649    // having done nothing at all -- which made `record_error` mark every error
650    // recovered and inflated `recovery_success_rate` to 100%.
651    //
652    // `execute_notification_strategy` is the one that really acts (it emits a
653    // `tracing::warn!`), and it is the one that still reports success.
654
655    /// Build an "unwired" result naming the missing capability.
656    fn unwired(capability: &str) -> RecoveryResult {
657        RecoveryResult {
658            success: false,
659            strategy_used: None,
660            message: format!(
661                "recovery not performed: {capability} -- ErrorRecoveryManager has no handle to \
662                 act on"
663            ),
664            recovery_time: Duration::from_millis(0),
665        }
666    }
667
668    async fn execute_retry_strategy(
669        &self,
670        _max_attempts: usize,
671        _delay_ms: u64,
672        _error: &ErrorEvent,
673    ) -> Result<RecoveryResult> {
674        Ok(Self::unwired(
675            "no retryable operation was supplied with the error",
676        ))
677    }
678
679    async fn execute_fallback_strategy(
680        &self,
681        alternative: &str,
682        _error: &ErrorEvent,
683    ) -> Result<RecoveryResult> {
684        Ok(Self::unwired(&format!(
685            "no dispatcher exists for the alternative method {alternative:?}"
686        )))
687    }
688
689    async fn execute_degradation_strategy(
690        &self,
691        functionality: &str,
692        _error: &ErrorEvent,
693    ) -> Result<RecoveryResult> {
694        Ok(Self::unwired(&format!(
695            "no feature switch exists for {functionality:?}"
696        )))
697    }
698
699    async fn execute_cleanup_strategy(
700        &self,
701        cleanup_type: &str,
702        _error: &ErrorEvent,
703    ) -> Result<RecoveryResult> {
704        Ok(Self::unwired(&format!(
705            "no allocator or cache handle for {cleanup_type:?}"
706        )))
707    }
708
709    async fn execute_reset_strategy(
710        &self,
711        component: &str,
712        _error: &ErrorEvent,
713    ) -> Result<RecoveryResult> {
714        Ok(Self::unwired(&format!(
715            "no component registry entry for {component:?}"
716        )))
717    }
718
719    async fn execute_shutdown_strategy(&self, _error: &ErrorEvent) -> Result<RecoveryResult> {
720        Ok(Self::unwired(
721            "no process supervisor handle to initiate shutdown",
722        ))
723    }
724
725    /// The one strategy that really performs its action: it emits the
726    /// notification through the `tracing` facade.
727    async fn execute_notification_strategy(
728        &self,
729        message: &str,
730        _error: &ErrorEvent,
731    ) -> Result<RecoveryResult> {
732        tracing::warn!("User notification: {}", message);
733        Ok(RecoveryResult {
734            success: true,
735            strategy_used: None,
736            message: "User notified via the tracing facade".to_string(),
737            recovery_time: Duration::from_millis(0),
738        })
739    }
740
741    async fn execute_repair_strategy(
742        &self,
743        repair_action: &str,
744        _error: &ErrorEvent,
745    ) -> Result<RecoveryResult> {
746        Ok(Self::unwired(&format!(
747            "no repair executor for the action {repair_action:?}"
748        )))
749    }
750}
751
752/// Result of a recovery attempt
753#[derive(Debug, Clone, Serialize, Deserialize)]
754pub struct RecoveryResult {
755    pub success: bool,
756    pub strategy_used: Option<RecoveryStrategy>,
757    pub message: String,
758    pub recovery_time: Duration,
759}
760
761/// Error statistics
762#[derive(Debug, Clone, Serialize, Deserialize)]
763pub struct ErrorStatistics {
764    pub total_errors: usize,
765    pub error_type_counts: HashMap<ErrorType, usize>,
766    pub severity_counts: HashMap<String, usize>,
767    /// See [`HealthMetrics::recovery_success_rate`]: `None` when no recovery
768    /// attempt has completed recently, never a fabricated `1.0`.
769    pub recovery_success_rate: Option<f64>,
770    pub circuit_breaker_state: CircuitState,
771    pub system_health: HealthStatus,
772}
773
774// ─────────────────────────────────────────────────────────────────────────────
775// Tests
776// ─────────────────────────────────────────────────────────────────────────────
777
778#[cfg(test)]
779mod tests {
780    use super::*;
781
782    fn sample_error() -> ErrorEvent {
783        ErrorEvent {
784            id: Uuid::new_v4(),
785            error_type: ErrorType::TensorInspectionError,
786            error_message: "boom".to_string(),
787            component: "tensor_inspector".to_string(),
788            severity: ErrorSeverity::Medium,
789            timestamp: chrono::Utc::now(),
790            context: ErrorContext {
791                session_id: Uuid::new_v4(),
792                operation: "inspect".to_string(),
793                parameters: HashMap::new(),
794                system_state: SystemState {
795                    memory_usage_mb: 0,
796                    cpu_usage_percent: 0.0,
797                    active_tensors: 0,
798                    active_sessions: 0,
799                    uptime_seconds: 0,
800                },
801            },
802            stack_trace: None,
803        }
804    }
805
806    // ---- Wave 6c debug-sweep2: recovery strategies stop claiming success ---
807
808    #[tokio::test]
809    async fn unwired_recovery_strategies_report_failure_not_success() {
810        let manager = ErrorRecoverySystem::new(ErrorRecoveryConfig::default());
811        let error = sample_error();
812
813        for result in [
814            manager.execute_retry_strategy(3, 10, &error).await.expect("call"),
815            manager.execute_fallback_strategy("other", &error).await.expect("call"),
816            manager.execute_degradation_strategy("feature", &error).await.expect("call"),
817            manager.execute_cleanup_strategy("cache", &error).await.expect("call"),
818            manager.execute_reset_strategy("component", &error).await.expect("call"),
819            manager.execute_shutdown_strategy(&error).await.expect("call"),
820            manager.execute_repair_strategy("fix", &error).await.expect("call"),
821        ] {
822            assert!(
823                !result.success,
824                "an unperformed recovery must not report success: {}",
825                result.message
826            );
827            assert!(
828                result.message.contains("recovery not performed"),
829                "the message must say what did not happen: {}",
830                result.message
831            );
832        }
833    }
834
835    #[tokio::test]
836    async fn the_notification_strategy_really_acts_and_reports_success() {
837        let manager = ErrorRecoverySystem::new(ErrorRecoveryConfig::default());
838        let error = sample_error();
839        let result = manager
840            .execute_notification_strategy("check the logs", &error)
841            .await
842            .expect("call");
843        assert!(result.success, "the notification is really emitted");
844        assert!(result.message.contains("tracing"));
845    }
846
847    fn make_error_event(error_type: ErrorType) -> ErrorEvent {
848        ErrorEvent {
849            id: Uuid::new_v4(),
850            error_type,
851            error_message: "test error".to_string(),
852            component: "test_component".to_string(),
853            severity: ErrorSeverity::Medium,
854            timestamp: chrono::Utc::now(),
855            context: ErrorContext {
856                session_id: Uuid::new_v4(),
857                operation: "test_op".to_string(),
858                parameters: HashMap::new(),
859                system_state: SystemState {
860                    memory_usage_mb: 1024,
861                    cpu_usage_percent: 50.0,
862                    active_tensors: 4,
863                    active_sessions: 1,
864                    uptime_seconds: 100,
865                },
866            },
867            stack_trace: None,
868        }
869    }
870
871    // ── ErrorRecoveryConfig ──────────────────────────────────────────────
872
873    #[test]
874    fn test_config_default_fields() {
875        let cfg = ErrorRecoveryConfig::default();
876        assert!(cfg.enabled);
877        assert!(cfg.max_retry_attempts > 0);
878        assert!(cfg.circuit_breaker_threshold > 0);
879        assert!(cfg.error_history_limit > 0);
880    }
881
882    // ── ErrorRecoverySystem creation ──────────────────────────────────────
883
884    #[test]
885    fn test_system_new_initializes_strategies() {
886        let system = ErrorRecoverySystem::new(ErrorRecoveryConfig::default());
887        // Should have strategies initialized for at least TensorInspectionError
888        let strategies = system.get_recovery_strategies(&ErrorType::TensorInspectionError);
889        assert!(!strategies.is_empty());
890    }
891
892    #[test]
893    fn test_system_new_circuit_breaker_closed() {
894        let system = ErrorRecoverySystem::new(ErrorRecoveryConfig::default());
895        assert!(matches!(system.circuit_breaker.state, CircuitState::Closed));
896    }
897
898    // ── record_error ─────────────────────────────────────────────────────
899
900    #[test]
901    fn test_record_error_adds_to_history() {
902        let mut system = ErrorRecoverySystem::new(ErrorRecoveryConfig::default());
903        let event = make_error_event(ErrorType::IOError);
904        system.record_error(event);
905        assert_eq!(system.error_history.len(), 1);
906    }
907
908    #[test]
909    fn test_record_error_respects_history_limit() {
910        let mut cfg = ErrorRecoveryConfig::default();
911        cfg.error_history_limit = 3;
912        let mut system = ErrorRecoverySystem::new(cfg);
913        for _ in 0..5 {
914            system.record_error(make_error_event(ErrorType::NetworkError));
915        }
916        assert_eq!(system.error_history.len(), 3);
917    }
918
919    // ── safe_mode ────────────────────────────────────────────────────────
920
921    #[test]
922    fn test_enable_disable_safe_mode() {
923        let mut system = ErrorRecoverySystem::new(ErrorRecoveryConfig::default());
924        assert!(!system.failsafe_manager.safe_mode_enabled);
925        system.enable_safe_mode();
926        assert!(system.failsafe_manager.safe_mode_enabled);
927        system.disable_safe_mode();
928        assert!(!system.failsafe_manager.safe_mode_enabled);
929    }
930
931    // ── get_error_statistics ──────────────────────────────────────────────
932
933    #[test]
934    fn test_error_statistics_empty() {
935        let system = ErrorRecoverySystem::new(ErrorRecoveryConfig::default());
936        let stats = system.get_error_statistics();
937        assert_eq!(stats.total_errors, 0);
938        assert!(matches!(stats.circuit_breaker_state, CircuitState::Closed));
939        assert!(matches!(stats.system_health, HealthStatus::Healthy));
940    }
941
942    #[test]
943    fn test_error_statistics_with_errors() {
944        let mut system = ErrorRecoverySystem::new(ErrorRecoveryConfig::default());
945        system.record_error(make_error_event(ErrorType::IOError));
946        system.record_error(make_error_event(ErrorType::NetworkError));
947        let stats = system.get_error_statistics();
948        assert_eq!(stats.total_errors, 2);
949        assert_eq!(
950            stats.error_type_counts.get(&ErrorType::IOError).copied().unwrap_or(0),
951            1
952        );
953    }
954
955    // ── ErrorType variants ────────────────────────────────────────────────
956
957    #[test]
958    fn test_error_type_variants() {
959        let types = [
960            ErrorType::TensorInspectionError,
961            ErrorType::GradientDebuggingError,
962            ErrorType::ModelDiagnosticsError,
963            ErrorType::VisualizationError,
964            ErrorType::MemoryProfilingError,
965            ErrorType::IOError,
966            ErrorType::NetworkError,
967            ErrorType::ResourceExhaustion,
968            ErrorType::ConfigurationError,
969            ErrorType::DataCorruption,
970            ErrorType::SystemFailure,
971            ErrorType::UserError,
972        ];
973        for t in &types {
974            assert!(!format!("{:?}", t).is_empty());
975        }
976    }
977
978    // ── ErrorSeverity variants ────────────────────────────────────────────
979
980    #[test]
981    fn test_error_severity_variants() {
982        let severities = [
983            ErrorSeverity::Low,
984            ErrorSeverity::Medium,
985            ErrorSeverity::High,
986            ErrorSeverity::Critical,
987            ErrorSeverity::Fatal,
988        ];
989        for s in &severities {
990            assert!(!format!("{:?}", s).is_empty());
991        }
992    }
993
994    // ── RecoveryStrategy variants ─────────────────────────────────────────
995
996    #[test]
997    fn test_recovery_strategy_variants() {
998        let strats = [
999            RecoveryStrategy::Retry {
1000                max_attempts: 3,
1001                delay_ms: 100,
1002            },
1003            RecoveryStrategy::Fallback {
1004                alternative_method: "alt".to_string(),
1005            },
1006            RecoveryStrategy::GracefulDegradation {
1007                reduced_functionality: "basic".to_string(),
1008            },
1009            RecoveryStrategy::ResourceCleanup {
1010                cleanup_type: "cache".to_string(),
1011            },
1012            RecoveryStrategy::SystemReset {
1013                component: "comp".to_string(),
1014            },
1015            RecoveryStrategy::EmergencyShutdown,
1016            RecoveryStrategy::UserNotification {
1017                message: "msg".to_string(),
1018            },
1019            RecoveryStrategy::AutomaticRepair {
1020                repair_action: "repair".to_string(),
1021            },
1022        ];
1023        for s in &strats {
1024            assert!(!format!("{:?}", s).is_empty());
1025        }
1026    }
1027
1028    // ── CircuitState variants ─────────────────────────────────────────────
1029
1030    #[test]
1031    fn test_circuit_state_variants() {
1032        let states = [
1033            CircuitState::Closed,
1034            CircuitState::Open,
1035            CircuitState::HalfOpen,
1036        ];
1037        for s in &states {
1038            assert!(!format!("{:?}", s).is_empty());
1039        }
1040    }
1041
1042    // ── HealthStatus variants ─────────────────────────────────────────────
1043
1044    #[test]
1045    fn test_health_status_variants() {
1046        let statuses = [
1047            HealthStatus::Healthy,
1048            HealthStatus::Degraded,
1049            HealthStatus::Unhealthy,
1050            HealthStatus::Critical,
1051        ];
1052        for s in &statuses {
1053            assert!(!format!("{:?}", s).is_empty());
1054        }
1055    }
1056
1057    // ── CircuitBreaker ────────────────────────────────────────────────────
1058
1059    #[test]
1060    fn test_circuit_breaker_initial_state() {
1061        let system = ErrorRecoverySystem::new(ErrorRecoveryConfig::default());
1062        assert_eq!(system.circuit_breaker.failure_count, 0);
1063        assert!(system.circuit_breaker.last_failure_time.is_none());
1064        assert_eq!(system.circuit_breaker.threshold, 5);
1065    }
1066
1067    // ── SystemState struct ────────────────────────────────────────────────
1068
1069    #[test]
1070    fn test_system_state_construction() {
1071        let state = SystemState {
1072            memory_usage_mb: 2048,
1073            cpu_usage_percent: 75.5,
1074            active_tensors: 10,
1075            active_sessions: 2,
1076            uptime_seconds: 3600,
1077        };
1078        assert_eq!(state.memory_usage_mb, 2048);
1079        assert!((state.cpu_usage_percent - 75.5).abs() < 1e-6);
1080    }
1081
1082    // ── HealthMetrics ──────────────────────────────────────────────────────
1083
1084    #[test]
1085    fn test_health_metrics_initial_values() {
1086        let system = ErrorRecoverySystem::new(ErrorRecoveryConfig::default());
1087        let m = &system.health_monitor.health_metrics;
1088        // A brand-new system has handled nothing. Zero errors is a real
1089        // measurement; a 100% recovery rate is not -- this used to assert
1090        // `recovery_success_rate == 1.0`, locking in a seeded fabrication.
1091        assert_eq!(m.error_rate, 0.0);
1092        assert_eq!(m.recovery_success_rate, None);
1093        assert_eq!(m.average_recovery_time_ms, None);
1094        assert_eq!(m.memory_health_score, None);
1095        assert_eq!(m.stability_score, None);
1096    }
1097
1098    /// The rate must be a real quotient over the recovery attempts that
1099    /// actually completed, and the mean recovery time a real mean -- not the
1100    /// `1.0` / `0.0` the system used to be born with.
1101    #[tokio::test]
1102    async fn test_health_metrics_are_computed_from_real_recovery_outcomes() {
1103        let mut system = ErrorRecoverySystem::new(ErrorRecoveryConfig::default());
1104        let now = chrono::Utc::now();
1105        for (success, millis) in [(true, 40i64), (true, 60), (false, 200)] {
1106            system.recovery_history.push_back(RecoveryEvent {
1107                id: Uuid::new_v4(),
1108                error_id: Uuid::new_v4(),
1109                strategy: RecoveryStrategy::Retry {
1110                    max_attempts: 1,
1111                    delay_ms: 0,
1112                },
1113                start_time: now - chrono::Duration::milliseconds(millis),
1114                end_time: Some(now),
1115                success: Some(success),
1116                result_message: "test".to_string(),
1117                attempts: 1,
1118            });
1119        }
1120
1121        system.check_system_health().await;
1122        let m = &system.health_monitor.health_metrics;
1123        let rate = m.recovery_success_rate.expect("three attempts completed in the window");
1124        assert!(
1125            (rate - 2.0 / 3.0).abs() < 1e-9,
1126            "two of three succeeded, got {rate}"
1127        );
1128        let mean_ms = m.average_recovery_time_ms.expect("three attempts completed in the window");
1129        assert!(
1130            (mean_ms - 100.0).abs() < 5.0,
1131            "(40+60+200)/3 = 100ms, got {mean_ms}"
1132        );
1133
1134        // Unmeasurable scores stay absent even after a health check.
1135        assert_eq!(m.memory_health_score, None);
1136        assert_eq!(m.stability_score, None);
1137    }
1138
1139    /// A health check with nothing in the recent window must clear the
1140    /// figures rather than leave a stale rate standing as current evidence.
1141    #[tokio::test]
1142    async fn test_health_check_reports_absence_when_nothing_recovered_recently() {
1143        let mut system = ErrorRecoverySystem::new(ErrorRecoveryConfig::default());
1144        system.check_system_health().await;
1145        assert_eq!(
1146            system.health_monitor.health_metrics.recovery_success_rate,
1147            None
1148        );
1149        assert_eq!(system.get_error_statistics().recovery_success_rate, None);
1150    }
1151
1152    // ── async handle_error with open circuit breaker ──────────────────────
1153
1154    #[tokio::test]
1155    async fn test_handle_error_with_open_circuit_breaker() {
1156        let mut system = ErrorRecoverySystem::new(ErrorRecoveryConfig::default());
1157        system.circuit_breaker.state = CircuitState::Open;
1158        let event = make_error_event(ErrorType::IOError);
1159        let result = system.handle_error(event).await.expect("should succeed");
1160        assert!(!result.success);
1161        assert!(result.message.contains("Circuit breaker"));
1162    }
1163}