agentic_warden/
error.rs

1//! Unified error handling for the agentic-warden project
2//!
3//! This module provides a comprehensive error handling system with
4//! proper error classification, context, and recovery mechanisms.
5
6#![allow(dead_code)] // 错误处理模块,部分辅助函数是公共API
7
8use anyhow::Error as AnyhowError;
9use std::collections::HashMap;
10use std::fmt;
11use std::io;
12use thiserror::Error;
13
14/// Main error type for the application
15#[derive(Error, Debug)]
16pub enum AgenticWardenError {
17    /// Configuration errors
18    #[error("Configuration error: {message}")]
19    Config {
20        message: String,
21        source: Option<Box<dyn std::error::Error + Send + Sync>>,
22    },
23
24    /// Provider service errors
25    #[error("Provider error: {provider} - {message}")]
26    Provider {
27        provider: String,
28        message: String,
29        error_code: Option<u32>,
30        source: Option<Box<dyn std::error::Error + Send + Sync>>,
31    },
32
33    /// Task execution errors
34    #[error("Task error (ID: {task_id}): {message}")]
35    Task {
36        task_id: u64,
37        message: String,
38        exit_code: Option<i32>,
39        source: Option<Box<dyn std::error::Error + Send + Sync>>,
40    },
41
42    /// Synchronization errors
43    #[error("Sync error ({operation}): {message}")]
44    Sync {
45        message: String,
46        operation: SyncOperation,
47        source: Option<Box<dyn std::error::Error + Send + Sync>>,
48    },
49
50    /// Authentication errors
51    #[error("Authentication error: {message}")]
52    Auth {
53        message: String,
54        provider: String,
55        source: Option<Box<dyn std::error::Error + Send + Sync>>,
56    },
57
58    /// Network errors
59    #[error("Network error: {message}")]
60    Network {
61        message: String,
62        url: Option<String>,
63        source: Option<Box<dyn std::error::Error + Send + Sync>>,
64    },
65
66    /// Filesystem errors
67    #[error("Filesystem error: {message} (path: {path})")]
68    Filesystem {
69        message: String,
70        path: String,
71        source: Option<Box<dyn std::error::Error + Send + Sync>>,
72    },
73
74    /// TUI errors
75    #[error("TUI error: {message}")]
76    Tui {
77        message: String,
78        component: String,
79        source: Option<Box<dyn std::error::Error + Send + Sync>>,
80    },
81
82    /// Process management errors
83    #[error("Process error: {message}")]
84    Process {
85        message: String,
86        command: String,
87        source: Option<Box<dyn std::error::Error + Send + Sync>>,
88    },
89
90    /// Validation errors
91    #[error("Validation error: {message}")]
92    Validation {
93        message: String,
94        field: Option<String>,
95        value: Option<String>,
96    },
97
98    /// Resource errors (memory, file handles, etc.)
99    #[error("Resource error: {message}")]
100    Resource {
101        message: String,
102        resource_type: String,
103        source: Option<Box<dyn std::error::Error + Send + Sync>>,
104    },
105
106    /// Dependency injection errors
107    #[error("Dependency injection error: {message}")]
108    Dependency {
109        message: String,
110        service: Option<String>,
111    },
112
113    /// Timeout errors
114    #[error("Timeout error: {message} (timeout: {timeout_ms}ms)")]
115    Timeout {
116        message: String,
117        timeout_ms: u64,
118        source: Option<Box<dyn std::error::Error + Send + Sync>>,
119    },
120
121    /// Concurrency errors
122    #[error("Concurrency error: {message}")]
123    Concurrency {
124        message: String,
125        operation: Option<String>,
126        source: Option<Box<dyn std::error::Error + Send + Sync>>,
127    },
128
129    /// Unknown errors
130    #[error("Unknown error: {message}")]
131    Unknown {
132        message: String,
133        source: Option<Box<dyn std::error::Error + Send + Sync>>,
134    },
135}
136
137/// High-level sync operation categories used for user messaging and recovery.
138#[derive(Debug, Clone, Copy, PartialEq, Eq)]
139pub enum SyncOperation {
140    DirectoryHashing,
141    ConfigPacking,
142    ConfigLoading,
143    ConfigSaving,
144    ArchiveExtraction,
145    Compression,
146    GoogleDriveAuth,
147    GoogleDriveRequest,
148    NetworkProbe,
149    Upload,
150    Download,
151    OAuthCallback,
152    StateVerification,
153    Discovery,
154    Unknown,
155}
156
157impl fmt::Display for SyncOperation {
158    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
159        f.write_str(self.as_str())
160    }
161}
162
163impl SyncOperation {
164    pub fn as_str(&self) -> &'static str {
165        match self {
166            SyncOperation::DirectoryHashing => "directory_hashing",
167            SyncOperation::ConfigPacking => "config_packing",
168            SyncOperation::ConfigLoading => "config_loading",
169            SyncOperation::ConfigSaving => "config_saving",
170            SyncOperation::ArchiveExtraction => "archive_extraction",
171            SyncOperation::Compression => "compression",
172            SyncOperation::GoogleDriveAuth => "google_drive_auth",
173            SyncOperation::GoogleDriveRequest => "google_drive_request",
174            SyncOperation::NetworkProbe => "network_probe",
175            SyncOperation::Upload => "upload",
176            SyncOperation::Download => "download",
177            SyncOperation::OAuthCallback => "oauth_callback",
178            SyncOperation::StateVerification => "state_verification",
179            SyncOperation::Discovery => "discovery",
180            SyncOperation::Unknown => "unknown",
181        }
182    }
183}
184
185impl AgenticWardenError {
186    /// Get error category
187    pub fn category(&self) -> ErrorCategory {
188        match self {
189            AgenticWardenError::Config { .. } => ErrorCategory::Config,
190            AgenticWardenError::Provider { .. } => ErrorCategory::Provider,
191            AgenticWardenError::Task { .. } => ErrorCategory::Task,
192            AgenticWardenError::Sync { .. } => ErrorCategory::Sync,
193            AgenticWardenError::Auth { .. } => ErrorCategory::Auth,
194            AgenticWardenError::Network { .. } => ErrorCategory::Network,
195            AgenticWardenError::Filesystem { .. } => ErrorCategory::Filesystem,
196            AgenticWardenError::Tui { .. } => ErrorCategory::Tui,
197            AgenticWardenError::Process { .. } => ErrorCategory::Process,
198            AgenticWardenError::Validation { .. } => ErrorCategory::Validation,
199            AgenticWardenError::Resource { .. } => ErrorCategory::Resource,
200            AgenticWardenError::Dependency { .. } => ErrorCategory::Dependency,
201            AgenticWardenError::Timeout { .. } => ErrorCategory::Timeout,
202            AgenticWardenError::Concurrency { .. } => ErrorCategory::Concurrency,
203            AgenticWardenError::Unknown { .. } => ErrorCategory::Unknown,
204        }
205    }
206
207    /// Get error severity
208    pub fn severity(&self) -> ErrorSeverity {
209        match self {
210            AgenticWardenError::Config { .. } => ErrorSeverity::High,
211            AgenticWardenError::Provider { .. } => ErrorSeverity::Medium,
212            AgenticWardenError::Task { .. } => ErrorSeverity::Medium,
213            AgenticWardenError::Sync { .. } => ErrorSeverity::Medium,
214            AgenticWardenError::Auth { .. } => ErrorSeverity::High,
215            AgenticWardenError::Network { .. } => ErrorSeverity::Medium,
216            AgenticWardenError::Filesystem { .. } => ErrorSeverity::Medium,
217            AgenticWardenError::Tui { .. } => ErrorSeverity::Low,
218            AgenticWardenError::Process { .. } => ErrorSeverity::Medium,
219            AgenticWardenError::Validation { .. } => ErrorSeverity::Low,
220            AgenticWardenError::Resource { .. } => ErrorSeverity::High,
221            AgenticWardenError::Dependency { .. } => ErrorSeverity::High,
222            AgenticWardenError::Timeout { .. } => ErrorSeverity::Medium,
223            AgenticWardenError::Concurrency { .. } => ErrorSeverity::High,
224            AgenticWardenError::Unknown { .. } => ErrorSeverity::Medium,
225        }
226    }
227
228    /// Check if error is recoverable
229    pub fn is_recoverable(&self) -> bool {
230        match self {
231            AgenticWardenError::Config { .. } => false,
232            AgenticWardenError::Provider { .. } => true,
233            AgenticWardenError::Task { .. } => true,
234            AgenticWardenError::Sync { .. } => true,
235            AgenticWardenError::Auth { .. } => true,
236            AgenticWardenError::Network { .. } => true,
237            AgenticWardenError::Filesystem { .. } => false,
238            AgenticWardenError::Tui { .. } => true,
239            AgenticWardenError::Process { .. } => true,
240            AgenticWardenError::Validation { .. } => true,
241            AgenticWardenError::Resource { .. } => false,
242            AgenticWardenError::Dependency { .. } => false,
243            AgenticWardenError::Timeout { .. } => true,
244            AgenticWardenError::Concurrency { .. } => false,
245            AgenticWardenError::Unknown { .. } => true,
246        }
247    }
248
249    /// Returns the sync operation associated with the error, if any.
250    pub fn sync_operation(&self) -> Option<SyncOperation> {
251        if let AgenticWardenError::Sync { operation, .. } = self {
252            Some(*operation)
253        } else {
254            None
255        }
256    }
257
258    /// Get user-friendly message
259    pub fn user_message(&self) -> String {
260        match self {
261            AgenticWardenError::Config { message, .. } => {
262                format!("Configuration problem: {}", message)
263            }
264            AgenticWardenError::Provider {
265                provider, message, ..
266            } => {
267                format!("Provider '{}' issue: {}", provider, message)
268            }
269            AgenticWardenError::Task {
270                task_id, message, ..
271            } => {
272                format!("Task #{} failed: {}", task_id, message)
273            }
274            AgenticWardenError::Sync {
275                message, operation, ..
276            } => match operation {
277                SyncOperation::DirectoryHashing => {
278                    format!("Unable to scan configuration directories: {}", message)
279                }
280                SyncOperation::ConfigPacking | SyncOperation::Compression => {
281                    format!("Failed to prepare configuration archive: {}", message)
282                }
283                SyncOperation::ConfigLoading | SyncOperation::ConfigSaving => {
284                    format!("Configuration file problem: {}", message)
285                }
286                SyncOperation::ArchiveExtraction => {
287                    format!("Unable to unpack configuration archive: {}", message)
288                }
289                SyncOperation::GoogleDriveAuth | SyncOperation::OAuthCallback => {
290                    format!("Google Drive authentication required: {}", message)
291                }
292                SyncOperation::GoogleDriveRequest | SyncOperation::NetworkProbe => {
293                    format!("Google Drive request failed: {}", message)
294                }
295                SyncOperation::Upload => format!("Upload failed: {}", message),
296                SyncOperation::Download => format!("Download failed: {}", message),
297                SyncOperation::StateVerification => {
298                    format!("Could not verify remote state: {}", message)
299                }
300                SyncOperation::Discovery => format!("Discovery issue: {}", message),
301                SyncOperation::Unknown => format!("Sync problem: {}", message),
302            },
303            AgenticWardenError::Auth {
304                message, provider, ..
305            } => {
306                format!("Authentication with {} failed: {}", provider, message)
307            }
308            AgenticWardenError::Network { message, .. } => {
309                format!("Network issue: {}", message)
310            }
311            AgenticWardenError::Filesystem { message, .. } => {
312                format!("File system problem: {}", message)
313            }
314            AgenticWardenError::Tui { message, .. } => {
315                format!("Interface issue: {}", message)
316            }
317            AgenticWardenError::Process { message, .. } => {
318                format!("Process problem: {}", message)
319            }
320            AgenticWardenError::Validation { message, .. } => {
321                format!("Input validation failed: {}", message)
322            }
323            AgenticWardenError::Resource { message, .. } => {
324                format!("Resource issue: {}", message)
325            }
326            AgenticWardenError::Dependency { message, .. } => {
327                format!("Service setup problem: {}", message)
328            }
329            AgenticWardenError::Timeout { message, .. } => {
330                format!("Operation timed out: {}", message)
331            }
332            AgenticWardenError::Concurrency { message, .. } => {
333                format!("Concurrency issue: {}", message)
334            }
335            AgenticWardenError::Unknown { message, .. } => {
336                format!("Unexpected error: {}", message)
337            }
338        }
339    }
340
341    /// Get technical details for logging
342    pub fn technical_details(&self) -> String {
343        match self {
344            AgenticWardenError::Config { source, .. } => {
345                if let Some(src) = source {
346                    format!("Config error - Source: {}", src)
347                } else {
348                    "Config error - No source".to_string()
349                }
350            }
351            AgenticWardenError::Provider {
352                provider,
353                error_code,
354                source,
355                ..
356            } => {
357                let mut details = format!("Provider error - Provider: {}", provider);
358                if let Some(code) = error_code {
359                    details.push_str(&format!(", Code: {}", code));
360                }
361                if let Some(src) = source {
362                    details.push_str(&format!(", Source: {}", src));
363                }
364                details
365            }
366            AgenticWardenError::Task {
367                task_id,
368                exit_code,
369                source,
370                ..
371            } => {
372                let mut details = format!("Task error - Task ID: {}", task_id);
373                if let Some(code) = exit_code {
374                    details.push_str(&format!(", Exit code: {}", code));
375                }
376                if let Some(src) = source {
377                    details.push_str(&format!(", Source: {}", src));
378                }
379                details
380            }
381            _ => format!("Error details: {}", self),
382        }
383    }
384}
385
386impl From<AnyhowError> for AgenticWardenError {
387    fn from(err: AnyhowError) -> Self {
388        let message = err.to_string();
389        AgenticWardenError::Unknown {
390            message,
391            source: None,
392        }
393    }
394}
395
396impl From<io::Error> for AgenticWardenError {
397    fn from(err: io::Error) -> Self {
398        AgenticWardenError::Filesystem {
399            message: format!("I/O error: {err}"),
400            path: "<io>".to_string(),
401            source: Some(Box::new(err)),
402        }
403    }
404}
405
406/// Error categories
407#[derive(Debug, Clone, Copy, PartialEq, Eq)]
408pub enum ErrorCategory {
409    Config,
410    Provider,
411    Task,
412    Sync,
413    Auth,
414    Network,
415    Filesystem,
416    Tui,
417    Process,
418    Validation,
419    Resource,
420    Dependency,
421    Timeout,
422    Concurrency,
423    Unknown,
424}
425
426impl ErrorCategory {
427    pub fn display_name(&self) -> &'static str {
428        match self {
429            ErrorCategory::Config => "Configuration",
430            ErrorCategory::Provider => "Provider",
431            ErrorCategory::Task => "Task",
432            ErrorCategory::Sync => "Synchronization",
433            ErrorCategory::Auth => "Authentication",
434            ErrorCategory::Network => "Network",
435            ErrorCategory::Filesystem => "Filesystem",
436            ErrorCategory::Tui => "Interface",
437            ErrorCategory::Process => "Process",
438            ErrorCategory::Validation => "Validation",
439            ErrorCategory::Resource => "Resource",
440            ErrorCategory::Dependency => "Dependency",
441            ErrorCategory::Timeout => "Timeout",
442            ErrorCategory::Concurrency => "Concurrency",
443            ErrorCategory::Unknown => "Unknown",
444        }
445    }
446}
447
448/// Error severity levels
449#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
450pub enum ErrorSeverity {
451    Low,
452    Medium,
453    High,
454    Critical,
455}
456
457/// Result type alias for convenience
458pub type AgenticResult<T> = Result<T, AgenticWardenError>;
459
460/// Registry-specific errors
461#[derive(Debug, Error)]
462pub enum RegistryError {
463    #[error("shared task map init failed: {0}")]
464    Shared(String),
465    #[error("shared hashmap operation failed: {0}")]
466    Map(String),
467    #[error("registry mutex poisoned")]
468    Poison,
469    #[error("record serialization failed: {0}")]
470    Serialize(#[from] serde_json::Error),
471    #[error("task not found: {0}")]
472    TaskNotFound(u32),
473    #[error("process tree error: {0}")]
474    ProcessTree(String),
475}
476
477impl From<shared_hashmap::Error> for RegistryError {
478    fn from(value: shared_hashmap::Error) -> Self {
479        RegistryError::Map(value.to_string())
480    }
481}
482
483impl From<crate::core::process_tree::ProcessTreeError> for RegistryError {
484    fn from(value: crate::core::process_tree::ProcessTreeError) -> Self {
485        RegistryError::ProcessTree(value.to_string())
486    }
487}
488
489impl From<AgenticWardenError> for RegistryError {
490    fn from(value: AgenticWardenError) -> Self {
491        RegistryError::Map(format!("Agentic error: {}", value))
492    }
493}
494
495impl From<crate::core::shared_map::SharedMapError> for RegistryError {
496    fn from(value: crate::core::shared_map::SharedMapError) -> Self {
497        RegistryError::Shared(value.to_string())
498    }
499}
500
501/// Error context builder
502pub struct ErrorContext {
503    category: Option<ErrorCategory>,
504    severity: Option<ErrorSeverity>,
505    component: Option<String>,
506    operation: Option<String>,
507    user_context: HashMap<String, String>,
508}
509
510impl ErrorContext {
511    pub fn new() -> Self {
512        Self {
513            category: None,
514            severity: None,
515            component: None,
516            operation: None,
517            user_context: HashMap::new(),
518        }
519    }
520
521    pub fn category(mut self, category: ErrorCategory) -> Self {
522        self.category = Some(category);
523        self
524    }
525
526    pub fn severity(mut self, severity: ErrorSeverity) -> Self {
527        self.severity = Some(severity);
528        self
529    }
530
531    pub fn component(mut self, component: impl Into<String>) -> Self {
532        self.component = Some(component.into());
533        self
534    }
535
536    pub fn operation(mut self, operation: impl Into<String>) -> Self {
537        self.operation = Some(operation.into());
538        self
539    }
540
541    pub fn context(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
542        self.user_context.insert(key.into(), value.into());
543        self
544    }
545
546    pub fn wrap_error<T>(
547        self,
548        result: Result<T, impl Into<AgenticWardenError>>,
549    ) -> AgenticResult<T> {
550        result.map_err(|e| {
551            // Apply context to the error if possible
552            // This would require extending the error types to support context
553            // For now, we just return the error as-is
554
555            e.into()
556        })
557    }
558}
559
560impl Default for ErrorContext {
561    fn default() -> Self {
562        Self::new()
563    }
564}
565
566/// Convenience functions for creating common errors
567pub mod errors {
568    use super::*;
569
570    pub fn config_error(message: impl Into<String>) -> AgenticWardenError {
571        AgenticWardenError::Config {
572            message: message.into(),
573            source: None,
574        }
575    }
576
577    pub fn provider_error(
578        provider: impl Into<String>,
579        message: impl Into<String>,
580    ) -> AgenticWardenError {
581        AgenticWardenError::Provider {
582            provider: provider.into(),
583            message: message.into(),
584            error_code: None,
585            source: None,
586        }
587    }
588
589    pub fn task_error(
590        task_id: u64,
591        message: impl Into<String>,
592        exit_code: Option<i32>,
593    ) -> AgenticWardenError {
594        AgenticWardenError::Task {
595            task_id,
596            message: message.into(),
597            exit_code,
598            source: None,
599        }
600    }
601
602    pub fn auth_error(
603        message: impl Into<String>,
604        provider: impl Into<String>,
605    ) -> AgenticWardenError {
606        AgenticWardenError::Auth {
607            message: message.into(),
608            provider: provider.into(),
609            source: None,
610        }
611    }
612
613    pub fn network_error(message: impl Into<String>) -> AgenticWardenError {
614        AgenticWardenError::Network {
615            message: message.into(),
616            url: None,
617            source: None,
618        }
619    }
620
621    pub fn sync_error(operation: SyncOperation, message: impl Into<String>) -> AgenticWardenError {
622        AgenticWardenError::Sync {
623            message: message.into(),
624            operation,
625            source: None,
626        }
627    }
628
629    pub fn sync_error_with_source(
630        operation: SyncOperation,
631        message: impl Into<String>,
632        source: impl std::error::Error + Send + Sync + 'static,
633    ) -> AgenticWardenError {
634        AgenticWardenError::Sync {
635            message: message.into(),
636            operation,
637            source: Some(Box::new(source)),
638        }
639    }
640
641    pub fn filesystem_error(
642        message: impl Into<String>,
643        path: impl Into<String>,
644    ) -> AgenticWardenError {
645        AgenticWardenError::Filesystem {
646            message: message.into(),
647            path: path.into(),
648            source: None,
649        }
650    }
651
652    pub fn tui_error(
653        message: impl Into<String>,
654        component: impl Into<String>,
655    ) -> AgenticWardenError {
656        AgenticWardenError::Tui {
657            message: message.into(),
658            component: component.into(),
659            source: None,
660        }
661    }
662
663    pub fn validation_error(
664        message: impl Into<String>,
665        field: Option<String>,
666        value: Option<String>,
667    ) -> AgenticWardenError {
668        AgenticWardenError::Validation {
669            message: message.into(),
670            field,
671            value,
672        }
673    }
674
675    pub fn dependency_error(
676        message: impl Into<String>,
677        service: Option<String>,
678    ) -> AgenticWardenError {
679        AgenticWardenError::Dependency {
680            message: message.into(),
681            service,
682        }
683    }
684
685    pub fn timeout_error(message: impl Into<String>, timeout_ms: u64) -> AgenticWardenError {
686        AgenticWardenError::Timeout {
687            message: message.into(),
688            timeout_ms,
689            source: None,
690        }
691    }
692
693    pub fn concurrency_error(message: impl Into<String>) -> AgenticWardenError {
694        AgenticWardenError::Concurrency {
695            message: message.into(),
696            operation: None,
697            source: None,
698        }
699    }
700}
701
702/// Error recovery strategies
703#[derive(Debug, Clone)]
704pub enum RecoveryStrategy {
705    /// Retry the operation with exponential backoff
706    Retry {
707        max_attempts: u32,
708        base_delay_ms: u64,
709    },
710    /// Fall back to an alternative approach
711    Fallback { alternative: String },
712    /// Ask user for intervention
713    UserIntervention { message: String },
714    /// Log and continue
715    LogAndContinue,
716    /// Abort the operation
717    Abort,
718}
719
720/// Rich, user-friendly error information used by the TUI and CLI.
721#[derive(Debug, Clone)]
722pub struct UserFacingError {
723    pub title: String,
724    pub message: String,
725    pub hint: Option<String>,
726    pub recovery: RecoveryStrategy,
727}
728
729impl AgenticWardenError {
730    /// Get suggested recovery strategy
731    pub fn recovery_strategy(&self) -> RecoveryStrategy {
732        match self {
733            AgenticWardenError::Sync { operation, .. } => match operation {
734                SyncOperation::GoogleDriveRequest
735                | SyncOperation::NetworkProbe
736                | SyncOperation::Upload
737                | SyncOperation::Download => RecoveryStrategy::Retry {
738                    max_attempts: 3,
739                    base_delay_ms: 2000,
740                },
741                SyncOperation::GoogleDriveAuth | SyncOperation::OAuthCallback => {
742                    RecoveryStrategy::UserIntervention {
743                        message:
744                            "Open the OAuth screen in the TUI to re-authorize Google Drive access"
745                                .to_string(),
746                    }
747                }
748                SyncOperation::StateVerification | SyncOperation::Discovery => {
749                    RecoveryStrategy::LogAndContinue
750                }
751                SyncOperation::DirectoryHashing
752                | SyncOperation::ConfigPacking
753                | SyncOperation::ConfigLoading
754                | SyncOperation::ConfigSaving
755                | SyncOperation::ArchiveExtraction
756                | SyncOperation::Compression => RecoveryStrategy::Abort,
757                SyncOperation::Unknown => RecoveryStrategy::LogAndContinue,
758            },
759            AgenticWardenError::Network { .. } => RecoveryStrategy::Retry {
760                max_attempts: 3,
761                base_delay_ms: 1000,
762            },
763            AgenticWardenError::Provider { .. } => RecoveryStrategy::Fallback {
764                alternative: "Use different provider or offline mode".to_string(),
765            },
766            AgenticWardenError::Auth { .. } => RecoveryStrategy::UserIntervention {
767                message: "Please check your credentials and try again".to_string(),
768            },
769            AgenticWardenError::Timeout { .. } => RecoveryStrategy::Retry {
770                max_attempts: 2,
771                base_delay_ms: 5000,
772            },
773            AgenticWardenError::Config { .. } => RecoveryStrategy::Abort,
774            AgenticWardenError::Filesystem { .. } => RecoveryStrategy::Abort,
775            AgenticWardenError::Resource { .. } => RecoveryStrategy::Abort,
776            AgenticWardenError::Dependency { .. } => RecoveryStrategy::Abort,
777            AgenticWardenError::Validation { .. } => RecoveryStrategy::UserIntervention {
778                message: "Please correct the input and try again".to_string(),
779            },
780            _ => RecoveryStrategy::LogAndContinue,
781        }
782    }
783
784    /// Convert into a user-facing payload with actionable hints.
785    pub fn to_user_facing(&self) -> UserFacingError {
786        let hint = match self {
787            AgenticWardenError::Config { .. } => Some(
788                "Open the Provider screen in the TUI and fix the invalid configuration.".to_string(),
789            ),
790            AgenticWardenError::Provider { .. } => Some(
791                "Switch to another provider or update credentials via `agentic-warden provider`."
792                    .to_string(),
793            ),
794            AgenticWardenError::Task { .. } => Some(
795                "Inspect the generated task log (see the Status screen) for command output."
796                    .to_string(),
797            ),
798            AgenticWardenError::Sync { operation, .. } => match operation {
799                SyncOperation::DirectoryHashing => Some(
800                    "Ensure ~/.claude, ~/.codex or ~/.gemini exist and are readable.".to_string(),
801                ),
802                SyncOperation::ConfigPacking | SyncOperation::Compression => Some(
803                    "Close editors that may lock the files and rerun the push operation."
804                        .to_string(),
805                ),
806                SyncOperation::ConfigLoading | SyncOperation::ConfigSaving => Some(
807                    "Validate JSON files under ~/.aiw/providers and retry.".to_string(),
808                ),
809                SyncOperation::ArchiveExtraction => Some(
810                    "Remove the corrupted archive under ~/.aiw/sync and pull again."
811                        .to_string(),
812                ),
813                SyncOperation::GoogleDriveAuth | SyncOperation::OAuthCallback => Some(
814                    "Open the Push/Pull screen and re-run the Google Drive OAuth flow."
815                        .to_string(),
816                ),
817                SyncOperation::GoogleDriveRequest
818                | SyncOperation::NetworkProbe
819                | SyncOperation::Upload
820                | SyncOperation::Download => Some(
821                    "Check your network connection and confirm the Google Drive API credentials."
822                        .to_string(),
823                ),
824                SyncOperation::StateVerification | SyncOperation::Discovery => Some(
825                    "Refresh the remote state from the Status screen to rebuild local caches."
826                        .to_string(),
827                ),
828                SyncOperation::Unknown => None,
829            },
830            AgenticWardenError::Auth { .. } => Some(
831                "Re-run OAuth from the dashboard or set the provider credentials via env vars."
832                    .to_string(),
833            ),
834            AgenticWardenError::Network { .. } => Some(
835                "Check VPN / proxy settings and retry once the connection stabilizes.".to_string(),
836            ),
837            AgenticWardenError::Filesystem { .. } => Some(
838                "Ensure the path exists and Agentic-Warden has permission to read/write it."
839                    .to_string(),
840            ),
841            AgenticWardenError::Tui { .. } => {
842                Some("Press `r` to refresh the UI or restart the dashboard.".to_string())
843            }
844            AgenticWardenError::Process { .. } => Some(
845                "Confirm the CLI binary is installed and accessible on PATH (set CLAUDE_BIN / CODEX_BIN / GEMINI_BIN if needed).".to_string(),
846            ),
847            AgenticWardenError::Validation { .. } => {
848                Some("Correct the provided value and submit again.".to_string())
849            }
850            AgenticWardenError::Resource { .. } => Some(
851                "Close other Agentic-Warden sessions to free the shared resource.".to_string(),
852            ),
853            AgenticWardenError::Dependency { .. } => Some(
854                "Restart Agentic-Warden to re-initialize background services.".to_string(),
855            ),
856            AgenticWardenError::Timeout { .. } => {
857                Some("Wait a few seconds and retry the operation.".to_string())
858            }
859            AgenticWardenError::Concurrency { .. } => Some(
860                "Let the active operation finish before starting another one.".to_string(),
861            ),
862            AgenticWardenError::Unknown { .. } => Some(
863                "Open ~/.aiw/logs/latest.log for detailed diagnostics.".to_string(),
864            ),
865        };
866
867        UserFacingError {
868            title: format!("{} Error", self.category().display_name()),
869            message: self.user_message(),
870            hint,
871            recovery: self.recovery_strategy(),
872        }
873    }
874}
875
876#[cfg(test)]
877mod tests {
878    use super::*;
879
880    #[test]
881    fn test_error_categories() {
882        let config_err = errors::config_error("test");
883        assert_eq!(config_err.category(), ErrorCategory::Config);
884        assert_eq!(config_err.severity(), ErrorSeverity::High);
885        assert!(!config_err.is_recoverable());
886
887        let network_err = errors::network_error("test");
888        assert_eq!(network_err.category(), ErrorCategory::Network);
889        assert_eq!(network_err.severity(), ErrorSeverity::Medium);
890        assert!(network_err.is_recoverable());
891    }
892
893    #[test]
894    fn test_error_messages() {
895        let provider_err = errors::provider_error("openrouter", "API key invalid");
896        assert!(provider_err.user_message().contains("openrouter"));
897        assert!(provider_err.user_message().contains("API key invalid"));
898
899        let task_err = errors::task_error(123, "Command not found", Some(127));
900        assert!(task_err.user_message().contains("123"));
901        assert!(task_err.user_message().contains("Command not found"));
902    }
903
904    #[test]
905    fn test_recovery_strategies() {
906        let network_err = errors::network_error("Connection failed");
907        match network_err.recovery_strategy() {
908            RecoveryStrategy::Retry {
909                max_attempts,
910                base_delay_ms,
911            } => {
912                assert_eq!(max_attempts, 3);
913                assert_eq!(base_delay_ms, 1000);
914            }
915            _ => panic!("Expected retry strategy"),
916        }
917
918        let config_err = errors::config_error("Invalid syntax");
919        match config_err.recovery_strategy() {
920            RecoveryStrategy::Abort => {} // Expected
921            _ => panic!("Expected abort strategy"),
922        }
923    }
924
925    #[test]
926    fn test_error_context() {
927        let context = ErrorContext::new()
928            .component("test_component")
929            .operation("test_operation")
930            .context("user_id", "123");
931
932        assert_eq!(context.component, Some("test_component".to_string()));
933        assert_eq!(context.operation, Some("test_operation".to_string()));
934        assert_eq!(
935            context.user_context.get("user_id"),
936            Some(&"123".to_string())
937        );
938    }
939
940    #[test]
941    fn sync_operation_helpers_work() {
942        let err = errors::sync_error(SyncOperation::Upload, "network reset");
943        assert_eq!(err.sync_operation(), Some(SyncOperation::Upload));
944
945        let payload = err.to_user_facing();
946        assert!(payload.message.contains("network reset"));
947        assert!(payload.title.contains("Synchronization"));
948        assert!(payload.hint.unwrap().contains("network"));
949    }
950}