1#![allow(dead_code)] use anyhow::Error as AnyhowError;
9use std::collections::HashMap;
10use std::fmt;
11use std::io;
12use thiserror::Error;
13
14#[derive(Error, Debug)]
16pub enum AgenticWardenError {
17 #[error("Configuration error: {message}")]
19 Config {
20 message: String,
21 source: Option<Box<dyn std::error::Error + Send + Sync>>,
22 },
23
24 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[error("Validation error: {message}")]
92 Validation {
93 message: String,
94 field: Option<String>,
95 value: Option<String>,
96 },
97
98 #[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 #[error("Dependency injection error: {message}")]
108 Dependency {
109 message: String,
110 service: Option<String>,
111 },
112
113 #[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 #[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 #[error("Unknown error: {message}")]
131 Unknown {
132 message: String,
133 source: Option<Box<dyn std::error::Error + Send + Sync>>,
134 },
135}
136
137#[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 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 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 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 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 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 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#[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#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
450pub enum ErrorSeverity {
451 Low,
452 Medium,
453 High,
454 Critical,
455}
456
457pub type AgenticResult<T> = Result<T, AgenticWardenError>;
459
460#[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
501pub 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 e.into()
556 })
557 }
558}
559
560impl Default for ErrorContext {
561 fn default() -> Self {
562 Self::new()
563 }
564}
565
566pub 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#[derive(Debug, Clone)]
704pub enum RecoveryStrategy {
705 Retry {
707 max_attempts: u32,
708 base_delay_ms: u64,
709 },
710 Fallback { alternative: String },
712 UserIntervention { message: String },
714 LogAndContinue,
716 Abort,
718}
719
720#[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 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 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 => {} _ => 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}