riglr_core/
error.rs

1//! Error types for riglr-core.
2
3use serde::Serialize;
4use thiserror::Error;
5
6/// Helper function to serialize Duration as seconds for JSON compatibility
7fn serialize_duration_as_secs<S>(
8    duration: &Option<std::time::Duration>,
9    serializer: S,
10) -> std::result::Result<S::Ok, S::Error>
11where
12    S: serde::Serializer,
13{
14    match duration {
15        Some(d) => serializer.serialize_some(&d.as_secs()),
16        None => serializer.serialize_none(),
17    }
18}
19
20/// Main error type for riglr-core operations.
21#[derive(Error, Debug, Serialize)]
22pub enum CoreError {
23    /// Queue operation failed
24    #[error("Queue error: {0}")]
25    Queue(String),
26
27    /// Job execution failed
28    #[error("Job execution error: {0}")]
29    JobExecution(String),
30
31    /// Serialization/deserialization failed
32    #[error("Serialization error: {0}")]
33    #[serde(skip)]
34    Serialization(#[from] serde_json::Error),
35
36    /// Redis connection error (only available with redis feature)
37    #[cfg(feature = "redis")]
38    #[error("Redis error: {0}")]
39    #[serde(skip)]
40    Redis(#[from] redis::RedisError),
41
42    /// Generic error
43    #[error("Core error: {0}")]
44    Generic(String),
45
46    /// Invalid input provided
47    #[error("Invalid input: {0}")]
48    InvalidInput(String),
49}
50
51/// Result type alias for riglr-core operations.
52pub type Result<T> = std::result::Result<T, CoreError>;
53
54/// Worker-specific error type for distinguishing system-level worker failures
55/// from tool execution failures.
56#[derive(Error, Debug, Serialize)]
57pub enum WorkerError {
58    /// Tool not found in the worker's registry
59    #[error("Tool '{tool_name}' not found in worker registry")]
60    ToolNotFound {
61        /// Name of the tool that was not found
62        tool_name: String,
63    },
64
65    /// Failed to acquire semaphore for concurrency control
66    #[error("Failed to acquire semaphore for tool '{tool_name}': {source_message}")]
67    SemaphoreAcquisition {
68        /// Name of the tool for which semaphore acquisition failed
69        tool_name: String,
70        /// The error message that caused the semaphore acquisition failure
71        source_message: String,
72    },
73
74    /// Idempotency store operation failed
75    #[error("Idempotency store operation failed: {source_message}")]
76    IdempotencyStore {
77        /// The error message that caused the idempotency store operation to fail
78        source_message: String,
79    },
80
81    /// Job serialization/deserialization error
82    #[error("Job serialization error: {source_message}")]
83    JobSerialization {
84        /// The JSON serialization error message
85        source_message: String,
86    },
87
88    /// Tool execution exceeded configured timeout
89    #[error("Tool execution timed out after {timeout:?}")]
90    ExecutionTimeout {
91        /// The duration after which the execution timed out
92        timeout: std::time::Duration,
93    },
94
95    /// Internal worker system error
96    #[error("Internal worker error: {message}")]
97    Internal {
98        /// Human-readable description of the internal error
99        message: String,
100    },
101}
102
103impl From<&str> for CoreError {
104    fn from(err: &str) -> Self {
105        Self::Generic(err.to_string())
106    }
107}
108
109/// Tool-specific error type for distinguishing retriable vs permanent failures.
110#[derive(Debug, Clone, Serialize, serde::Deserialize)]
111pub enum ToolError {
112    /// Operation can be retried
113    Retriable {
114        /// The typed error source (not serialized)
115        #[serde(skip)]
116        source: Option<std::sync::Arc<dyn std::error::Error + Send + Sync>>,
117        /// The error message for serialization
118        source_message: String,
119        /// Additional context about the error
120        context: String,
121    },
122
123    /// Rate limited, retry after delay
124    RateLimited {
125        /// The typed error source (not serialized)
126        #[serde(skip)]
127        source: Option<std::sync::Arc<dyn std::error::Error + Send + Sync>>,
128        /// The error message for serialization
129        source_message: String,
130        /// Additional context about the rate limiting
131        context: String,
132        /// Optional duration to wait before retrying (in seconds)
133        #[serde(serialize_with = "serialize_duration_as_secs")]
134        retry_after: Option<std::time::Duration>,
135    },
136
137    /// Permanent error, do not retry
138    Permanent {
139        /// The typed error source (not serialized)
140        #[serde(skip)]
141        source: Option<std::sync::Arc<dyn std::error::Error + Send + Sync>>,
142        /// The error message for serialization
143        source_message: String,
144        /// Additional context about the permanent error
145        context: String,
146    },
147
148    /// Invalid input provided
149    InvalidInput {
150        /// The typed error source (not serialized)
151        #[serde(skip)]
152        source: Option<std::sync::Arc<dyn std::error::Error + Send + Sync>>,
153        /// The error message for serialization
154        source_message: String,
155        /// Description of what input was invalid
156        context: String,
157    },
158
159    /// Signer context error
160    SignerContext(String),
161}
162
163impl PartialEq for ToolError {
164    fn eq(&self, other: &Self) -> bool {
165        use ToolError::*;
166        match (self, other) {
167            (
168                Retriable {
169                    source_message: s1,
170                    context: c1,
171                    ..
172                },
173                Retriable {
174                    source_message: s2,
175                    context: c2,
176                    ..
177                },
178            ) => s1 == s2 && c1 == c2,
179            (
180                RateLimited {
181                    source_message: s1,
182                    context: c1,
183                    retry_after: r1,
184                    ..
185                },
186                RateLimited {
187                    source_message: s2,
188                    context: c2,
189                    retry_after: r2,
190                    ..
191                },
192            ) => s1 == s2 && c1 == c2 && r1 == r2,
193            (
194                Permanent {
195                    source_message: s1,
196                    context: c1,
197                    ..
198                },
199                Permanent {
200                    source_message: s2,
201                    context: c2,
202                    ..
203                },
204            ) => s1 == s2 && c1 == c2,
205            (
206                InvalidInput {
207                    source_message: s1,
208                    context: c1,
209                    ..
210                },
211                InvalidInput {
212                    source_message: s2,
213                    context: c2,
214                    ..
215                },
216            ) => s1 == s2 && c1 == c2,
217            (SignerContext(s1), SignerContext(s2)) => s1 == s2,
218            _ => false,
219        }
220    }
221}
222
223impl ToolError {
224    /// Creates a retriable error with context and source preservation
225    pub fn retriable_with_source<E: std::error::Error + Send + Sync + 'static>(
226        source: E,
227        context: impl Into<String>,
228    ) -> Self {
229        let source_message = source.to_string();
230        Self::Retriable {
231            source: Some(std::sync::Arc::new(source)),
232            source_message,
233            context: context.into(),
234        }
235    }
236
237    /// Creates a permanent error with context and source preservation
238    pub fn permanent_with_source<E: std::error::Error + Send + Sync + 'static>(
239        source: E,
240        context: impl Into<String>,
241    ) -> Self {
242        let source_message = source.to_string();
243        Self::Permanent {
244            source: Some(std::sync::Arc::new(source)),
245            source_message,
246            context: context.into(),
247        }
248    }
249
250    /// Creates a rate limited error with optional retry duration
251    pub fn rate_limited_with_source<E: std::error::Error + Send + Sync + 'static>(
252        source: E,
253        context: impl Into<String>,
254        retry_after: Option<std::time::Duration>,
255    ) -> Self {
256        let source_message = source.to_string();
257        Self::RateLimited {
258            source: Some(std::sync::Arc::new(source)),
259            source_message,
260            context: context.into(),
261            retry_after,
262        }
263    }
264
265    /// Creates an invalid input error
266    pub fn invalid_input_with_source<E: std::error::Error + Send + Sync + 'static>(
267        source: E,
268        context: impl Into<String>,
269    ) -> Self {
270        let source_message = source.to_string();
271        Self::InvalidInput {
272            source: Some(std::sync::Arc::new(source)),
273            source_message,
274            context: context.into(),
275        }
276    }
277
278    /// Returns whether this error is retriable
279    #[must_use]
280    pub const fn is_retriable(&self) -> bool {
281        matches!(self, Self::Retriable { .. } | Self::RateLimited { .. })
282    }
283
284    /// Returns the retry delay if this is a rate limited error
285    #[must_use]
286    pub const fn retry_after(&self) -> Option<std::time::Duration> {
287        match self {
288            Self::RateLimited { retry_after, .. } => *retry_after,
289            _ => None,
290        }
291    }
292
293    /// Checks if the error is rate limited (for compatibility)
294    #[must_use]
295    pub const fn is_rate_limited(&self) -> bool {
296        matches!(self, Self::RateLimited { .. })
297    }
298
299    /// Creates a retriable error from a string message
300    pub fn retriable_string<S: Into<String>>(msg: S) -> Self {
301        let msg = msg.into();
302        Self::Retriable {
303            source: None,
304            source_message: msg.clone(),
305            context: msg,
306        }
307    }
308
309    /// Creates a permanent error from a string message
310    pub fn permanent_string<S: Into<String>>(msg: S) -> Self {
311        let msg = msg.into();
312        Self::Permanent {
313            source: None,
314            source_message: msg.clone(),
315            context: msg,
316        }
317    }
318
319    /// Creates a rate limited error from a string message
320    pub fn rate_limited_string<S: Into<String>>(msg: S) -> Self {
321        let msg = msg.into();
322        Self::RateLimited {
323            source: None,
324            source_message: msg.clone(),
325            context: msg,
326            retry_after: None,
327        }
328    }
329
330    /// Creates an invalid input error from a string message
331    pub fn invalid_input_string<S: Into<String>>(msg: S) -> Self {
332        let msg = msg.into();
333        Self::InvalidInput {
334            source: None,
335            source_message: msg.clone(),
336            context: msg,
337        }
338    }
339
340    /// Check if the error contains a specific substring in its string representation
341    pub fn contains(&self, needle: &str) -> bool {
342        self.to_string().contains(needle)
343    }
344
345    /// Creates a retriable error from any error type.
346    pub fn retriable_from_error<E: std::error::Error + Send + Sync + 'static>(
347        source: E,
348        context: impl Into<String>,
349    ) -> Self {
350        Self::retriable_with_source(source, context)
351    }
352
353    /// Creates a permanent error from any error type.
354    pub fn permanent_from_error<E: std::error::Error + Send + Sync + 'static>(
355        source: E,
356        context: impl Into<String>,
357    ) -> Self {
358        Self::permanent_with_source(source, context)
359    }
360}
361
362impl std::fmt::Display for ToolError {
363    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
364        match self {
365            Self::Retriable {
366                context,
367                source_message,
368                ..
369            } => {
370                write!(
371                    f,
372                    "Operation can be retried: {} - {}",
373                    context, source_message
374                )
375            }
376            Self::RateLimited {
377                context,
378                source_message,
379                ..
380            } => {
381                write!(f, "Rate limited: {} - {}", context, source_message)
382            }
383            Self::Permanent {
384                context,
385                source_message,
386                ..
387            } => {
388                write!(f, "Permanent error: {} - {}", context, source_message)
389            }
390            Self::InvalidInput {
391                context,
392                source_message,
393                ..
394            } => {
395                write!(f, "Invalid input: {} - {}", context, source_message)
396            }
397            Self::SignerContext(msg) => {
398                write!(f, "Signer context error: {}", msg)
399            }
400        }
401    }
402}
403
404impl std::error::Error for ToolError {
405    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
406        match self {
407            Self::Retriable { source, .. }
408            | Self::RateLimited { source, .. }
409            | Self::Permanent { source, .. }
410            | Self::InvalidInput { source, .. } => source
411                .as_ref()
412                .map(|e| e.as_ref() as &(dyn std::error::Error + 'static)),
413            Self::SignerContext(_) => None,
414        }
415    }
416}
417
418/// Simple error wrapper for string messages
419#[derive(Error, Debug)]
420#[error("{0}")]
421#[allow(dead_code)]
422struct StringError(String);
423
424// Conversion from SignerError to ToolError
425impl From<crate::signer::SignerError> for ToolError {
426    fn from(err: crate::signer::SignerError) -> Self {
427        Self::SignerContext(err.to_string())
428    }
429}
430
431/// # Error Classification: Explicit is Better
432///
433/// Generic `From` implementations for common error types like `anyhow::Error` and `String`
434/// have been intentionally removed. This is a deliberate design choice to enforce
435/// explicit error classification at the point of creation.
436///
437/// Automatically classifying unknown errors as `Permanent` is a safe default but can hide
438/// bugs where transient, retriable errors are not handled correctly. By requiring
439/// explicit classification, developers are forced to make a conscious decision about
440/// the nature of the error.
441///
442/// ## Best Practices
443///
444/// Always use the explicit constructors to create `ToolError` instances:
445///
446/// ```rust
447/// use riglr_core::ToolError;
448/// use std::io::Error as IoError;
449///
450/// // ✅ Explicitly classify known transient errors as retriable.
451/// let network_error = IoError::new(std::io::ErrorKind::TimedOut, "Connection timeout");
452/// let tool_error = ToolError::retriable_with_source(network_error, "API call failed");
453///
454/// // ✅ Explicitly classify rate limiting errors.
455/// let rate_limit_error = IoError::new(std::io::ErrorKind::Other, "Rate limited");
456/// let tool_error = ToolError::rate_limited_with_source(
457///     rate_limit_error,
458///     "API rate limit exceeded",
459///     Some(std::time::Duration::from_secs(60))
460/// );
461///
462/// // ✅ Explicitly classify user input errors.
463/// let input_error = IoError::new(std::io::ErrorKind::InvalidInput, "Bad address");
464/// let tool_error = ToolError::invalid_input_with_source(input_error, "Invalid address format");
465///
466/// // ✅ Explicitly classify all other unrecoverable errors as permanent.
467/// let auth_error = IoError::new(std::io::ErrorKind::PermissionDenied, "Invalid API key");
468/// let tool_error = ToolError::permanent_with_source(auth_error, "Authentication failed");
469/// ```
470impl From<serde_json::Error> for ToolError {
471    fn from(err: serde_json::Error) -> Self {
472        Self::permanent_with_source(err, "JSON serialization/deserialization failed")
473    }
474}
475
476// Removed generic From<&str> implementation - use explicit error constructors instead
477
478#[cfg(test)]
479mod tests {
480    use super::*;
481    use std::error::Error;
482
483    #[derive(Error, Debug)]
484    #[error("Test error")]
485    struct TestError;
486
487    #[test]
488    fn test_error_source_preservation() {
489        let original_error = TestError;
490        let tool_error = ToolError::retriable_with_source(original_error, "Test context");
491
492        // Verify source is preserved
493        assert!(tool_error.source().is_some());
494
495        // Verify we can downcast to original error type
496        let source = tool_error.source().unwrap();
497        assert!(source.downcast_ref::<TestError>().is_some());
498    }
499
500    #[test]
501    fn test_error_classification() {
502        let retriable = ToolError::retriable_with_source(TestError, "Retriable test");
503        assert!(retriable.is_retriable());
504
505        let permanent = ToolError::permanent_with_source(TestError, "Permanent test");
506        assert!(!permanent.is_retriable());
507
508        let rate_limited = ToolError::rate_limited_with_source(
509            TestError,
510            "Rate limited test",
511            Some(std::time::Duration::from_secs(60)),
512        );
513        assert!(rate_limited.is_retriable());
514        assert_eq!(
515            rate_limited.retry_after(),
516            Some(std::time::Duration::from_secs(60))
517        );
518    }
519
520    #[test]
521    fn test_string_error_creation() {
522        let retriable = ToolError::retriable_string("Network timeout");
523        assert!(retriable.is_retriable());
524        assert!(!retriable.is_rate_limited());
525
526        let rate_limited = ToolError::rate_limited_string("API rate limit exceeded");
527        assert!(rate_limited.is_retriable());
528        assert!(rate_limited.is_rate_limited());
529
530        let permanent = ToolError::permanent_string("Invalid parameters");
531        assert!(!permanent.is_retriable());
532        assert!(!permanent.is_rate_limited());
533    }
534
535    // Tests for CoreError enum variants and Display implementations
536    #[test]
537    fn test_core_error_queue_variant() {
538        let error = CoreError::Queue("Failed to connect".to_string());
539        assert_eq!(error.to_string(), "Queue error: Failed to connect");
540    }
541
542    #[test]
543    fn test_core_error_job_execution_variant() {
544        let error = CoreError::JobExecution("Job failed to run".to_string());
545        assert_eq!(error.to_string(), "Job execution error: Job failed to run");
546    }
547
548    #[test]
549    fn test_core_error_serialization_variant() {
550        // Create a real JSON parsing error by parsing invalid JSON
551        let json_error = serde_json::from_str::<serde_json::Value>("invalid json").unwrap_err();
552        let error = CoreError::Serialization(json_error);
553        assert!(error.to_string().contains("Serialization error:"));
554    }
555
556    #[test]
557    fn test_core_error_generic_variant() {
558        let error = CoreError::Generic("Something went wrong".to_string());
559        assert_eq!(error.to_string(), "Core error: Something went wrong");
560    }
561
562    #[test]
563    fn test_core_error_from_str() {
564        let error = CoreError::from("Test error message");
565        assert_eq!(error.to_string(), "Core error: Test error message");
566    }
567
568    #[test]
569    fn test_core_error_from_empty_str() {
570        let error = CoreError::from("");
571        assert_eq!(error.to_string(), "Core error: ");
572    }
573
574    // Tests for WorkerError enum variants and Display implementations
575    #[test]
576    fn test_worker_error_tool_not_found() {
577        let error = WorkerError::ToolNotFound {
578            tool_name: "missing_tool".to_string(),
579        };
580        assert_eq!(
581            error.to_string(),
582            "Tool 'missing_tool' not found in worker registry"
583        );
584    }
585
586    #[test]
587    fn test_worker_error_semaphore_acquisition() {
588        let error = WorkerError::SemaphoreAcquisition {
589            tool_name: "test_tool".to_string(),
590            source_message: "Test error".to_string(),
591        };
592        assert_eq!(
593            error.to_string(),
594            "Failed to acquire semaphore for tool 'test_tool': Test error"
595        );
596    }
597
598    #[test]
599    fn test_worker_error_idempotency_store() {
600        let error = WorkerError::IdempotencyStore {
601            source_message: "Test error".to_string(),
602        };
603        assert_eq!(
604            error.to_string(),
605            "Idempotency store operation failed: Test error"
606        );
607    }
608
609    #[test]
610    fn test_worker_error_job_serialization() {
611        let error = WorkerError::JobSerialization {
612            source_message: "invalid JSON format".to_string(),
613        };
614        assert!(error.to_string().contains("Job serialization error:"));
615    }
616
617    #[test]
618    fn test_worker_error_execution_timeout() {
619        let timeout = std::time::Duration::from_secs(30);
620        let error = WorkerError::ExecutionTimeout { timeout };
621        assert!(error
622            .to_string()
623            .contains("Tool execution timed out after 30"));
624    }
625
626    #[test]
627    fn test_worker_error_internal() {
628        let error = WorkerError::Internal {
629            message: "Internal system failure".to_string(),
630        };
631        assert_eq!(
632            error.to_string(),
633            "Internal worker error: Internal system failure"
634        );
635    }
636
637    // Tests for ToolError additional methods and edge cases
638    #[test]
639    fn test_tool_error_invalid_input_with_source() {
640        let error = ToolError::invalid_input_with_source(TestError, "Bad input data");
641        assert!(!error.is_retriable());
642        assert!(!error.is_rate_limited());
643        assert_eq!(error.retry_after(), None);
644        assert_eq!(
645            error.to_string(),
646            "Invalid input: Bad input data - Test error"
647        );
648    }
649
650    #[test]
651    fn test_tool_error_invalid_input_string() {
652        let error = ToolError::invalid_input_string("Invalid JSON format");
653        assert!(!error.is_retriable());
654        assert!(!error.is_rate_limited());
655        assert_eq!(error.retry_after(), None);
656        assert_eq!(
657            error.to_string(),
658            "Invalid input: Invalid JSON format - Invalid JSON format"
659        );
660    }
661
662    #[test]
663    fn test_tool_error_rate_limited_with_no_retry_after() {
664        let error = ToolError::rate_limited_with_source(TestError, "Rate limit hit", None);
665        assert!(error.is_retriable());
666        assert!(error.is_rate_limited());
667        assert_eq!(error.retry_after(), None);
668    }
669
670    #[test]
671    fn test_tool_error_permanent_variant() {
672        let error = ToolError::Permanent {
673            source: None,
674            source_message: "Test error".to_string(),
675            context: "Authentication failed".to_string(),
676        };
677        assert!(!error.is_retriable());
678        assert!(!error.is_rate_limited());
679        assert_eq!(error.retry_after(), None);
680    }
681
682    #[test]
683    fn test_tool_error_retriable_variant() {
684        let error = ToolError::Retriable {
685            source: None,
686            source_message: "Test error".to_string(),
687            context: "Network issue".to_string(),
688        };
689        assert!(error.is_retriable());
690        assert!(!error.is_rate_limited());
691        assert_eq!(error.retry_after(), None);
692    }
693
694    #[test]
695    fn test_tool_error_invalid_input_variant() {
696        let error = ToolError::InvalidInput {
697            source: None,
698            source_message: "Test error".to_string(),
699            context: "Missing required field".to_string(),
700        };
701        assert!(!error.is_retriable());
702        assert!(!error.is_rate_limited());
703        assert_eq!(error.retry_after(), None);
704    }
705
706    // Tests for explicit error constructors (replacing removed From implementations)
707    #[test]
708    fn test_tool_error_explicit_anyhow_error() {
709        let anyhow_error = anyhow::anyhow!("Something went wrong");
710        let tool_error =
711            ToolError::permanent_string(format!("An unknown error occurred: {}", anyhow_error));
712        assert!(!tool_error.is_retriable());
713        assert!(!tool_error.is_rate_limited());
714        assert!(tool_error.to_string().contains("An unknown error occurred"));
715    }
716
717    #[test]
718    fn test_tool_error_explicit_boxed_error() {
719        let test_error = TestError;
720        let tool_error =
721            ToolError::permanent_with_source(test_error, "A required resource was not found");
722        assert!(!tool_error.is_retriable());
723        assert!(!tool_error.is_rate_limited());
724        assert_eq!(tool_error.retry_after(), None);
725        assert!(
726            matches!(tool_error, ToolError::Permanent { ref context, .. } if context == "A required resource was not found")
727        );
728    }
729
730    #[test]
731    fn test_tool_error_explicit_string() {
732        let error_msg = "Database connection failed".to_string();
733        let tool_error = ToolError::retriable_string(error_msg);
734        assert!(tool_error.is_retriable());
735        assert!(!tool_error.is_rate_limited());
736        assert!(tool_error
737            .to_string()
738            .contains("Database connection failed"));
739    }
740
741    #[test]
742    fn test_tool_error_explicit_str_ref() {
743        let error_msg = "Authentication token expired";
744        let tool_error = ToolError::permanent_string(error_msg);
745        assert!(!tool_error.is_retriable());
746        assert!(!tool_error.is_rate_limited());
747        assert!(tool_error.to_string().contains(error_msg));
748    }
749
750    #[test]
751    fn test_tool_error_explicit_empty_string() {
752        let tool_error = ToolError::permanent_string("");
753        assert!(!tool_error.is_retriable());
754        assert!(!tool_error.is_rate_limited());
755    }
756
757    #[test]
758    fn test_tool_error_explicit_empty_str() {
759        let tool_error = ToolError::permanent_string("");
760        assert!(!tool_error.is_retriable());
761        assert!(!tool_error.is_rate_limited());
762    }
763
764    // Test StringError struct (even though it's private, it's used internally)
765    #[test]
766    fn test_string_error_creation_via_tool_error() {
767        let error = ToolError::permanent_string("Test message");
768        // permanent_string doesn't create a source, just sets the message
769        assert!(error.source().is_none());
770        assert_eq!(
771            error.to_string(),
772            "Permanent error: Test message - Test message"
773        );
774    }
775
776    // Tests for edge cases with different input types
777    #[test]
778    fn test_tool_error_constructors_with_various_string_types() {
779        // Test with String
780        let error1 = ToolError::retriable_with_source(TestError, String::from("String type"));
781        assert!(
782            matches!(error1, ToolError::Retriable { ref context, .. } if context == "String type")
783        );
784
785        // Test with &str
786        let error2 = ToolError::permanent_with_source(TestError, "str type");
787        assert!(
788            matches!(error2, ToolError::Permanent { ref context, .. } if context == "str type")
789        );
790
791        // Test with owned String
792        let owned_string = "owned string".to_owned();
793        let error3 = ToolError::rate_limited_with_source(TestError, owned_string, None);
794        assert!(
795            matches!(error3, ToolError::RateLimited { ref context, .. } if context == "owned string")
796        );
797    }
798
799    // Test Result type alias
800    #[test]
801    fn test_result_type_alias_ok() {
802        let result: Result<i32> = Ok(42);
803        assert!(result.is_ok());
804        assert_eq!(result.unwrap(), 42);
805    }
806
807    #[test]
808    fn test_result_type_alias_err() {
809        let result: Result<i32> = Err(CoreError::Generic("Test error".to_string()));
810        assert!(result.is_err());
811        let error = result.unwrap_err();
812        assert_eq!(error.to_string(), "Core error: Test error");
813    }
814
815    // Test const functions
816    #[test]
817    fn test_const_functions_compilation() {
818        // These tests ensure the const functions compile and work correctly
819        const fn test_is_retriable() -> bool {
820            // We can't create enum variants in const context easily, so just test compilation
821            true
822        }
823
824        const fn test_retry_after() -> bool {
825            // Test compilation of const function
826            true
827        }
828
829        const fn test_is_rate_limited() -> bool {
830            // Test compilation of const function
831            true
832        }
833
834        assert!(test_is_retriable());
835        assert!(test_retry_after());
836        assert!(test_is_rate_limited());
837    }
838
839    // Test Debug implementations (derived)
840    #[test]
841    fn test_debug_implementations() {
842        let core_error = CoreError::Queue("debug test".to_string());
843        let debug_str = format!("{:?}", core_error);
844        assert!(debug_str.contains("Queue"));
845        assert!(debug_str.contains("debug test"));
846
847        let worker_error = WorkerError::ToolNotFound {
848            tool_name: "debug_tool".to_string(),
849        };
850        let debug_str = format!("{:?}", worker_error);
851        assert!(debug_str.contains("ToolNotFound"));
852        assert!(debug_str.contains("debug_tool"));
853
854        let tool_error = ToolError::retriable_string("debug test");
855        let debug_str = format!("{:?}", tool_error);
856        assert!(debug_str.contains("Retriable"));
857    }
858
859    // Additional edge case: Test with very long strings
860    #[test]
861    fn test_errors_with_long_strings() {
862        let long_string = "a".repeat(1000);
863        let error = CoreError::Generic(long_string.clone());
864        assert!(error.to_string().contains(&long_string));
865
866        let tool_error = ToolError::permanent_string(long_string.clone());
867        assert!(tool_error.to_string().contains(&long_string));
868    }
869
870    // Test error source chain
871    #[test]
872    fn test_error_source_chain() {
873        let original = TestError;
874        let tool_error = ToolError::retriable_with_source(original, "Context");
875
876        // Test source method
877        let source = tool_error.source();
878        assert!(source.is_some());
879
880        // Test that we can downcast
881        let concrete_source = source.unwrap().downcast_ref::<TestError>();
882        assert!(concrete_source.is_some());
883    }
884
885    // Test ToolError SignerContext variant
886    #[test]
887    fn test_tool_error_signer_context_variant() {
888        use crate::signer::SignerError;
889
890        let signer_error = SignerError::NoSignerContext;
891        let tool_error = ToolError::SignerContext(signer_error.to_string());
892
893        assert!(!tool_error.is_retriable());
894        assert!(!tool_error.is_rate_limited());
895        assert_eq!(tool_error.retry_after(), None);
896        assert!(tool_error.to_string().contains("Signer context error"));
897    }
898
899    #[test]
900    fn test_tool_error_from_signer_error() {
901        use crate::signer::SignerError;
902
903        let signer_error = SignerError::Configuration("Invalid config".to_string());
904        let tool_error = ToolError::from(signer_error);
905
906        assert!(
907            matches!(tool_error, ToolError::SignerContext(ref inner) if inner.to_string().contains("Invalid configuration: Invalid config"))
908        );
909    }
910
911    #[test]
912    fn test_tool_error_from_serde_json_error() {
913        // Create a real JSON parsing error by parsing invalid JSON
914        let json_error = serde_json::from_str::<serde_json::Value>("{ invalid }").unwrap_err();
915        let tool_error = ToolError::from(json_error);
916
917        assert!(!tool_error.is_retriable());
918        assert!(!tool_error.is_rate_limited());
919        assert!(tool_error
920            .to_string()
921            .contains("JSON serialization/deserialization failed"));
922        assert!(
923            matches!(tool_error, ToolError::Permanent { ref context, .. } if context == "JSON serialization/deserialization failed")
924        );
925    }
926
927    // Test Core Error Serialization from serde_json::Error
928    #[test]
929    fn test_core_error_from_serde_json_error() {
930        // Create a real JSON parsing error by parsing invalid JSON
931        let json_error = serde_json::from_str::<serde_json::Value>("{ invalid }").unwrap_err();
932        let core_error = CoreError::from(json_error);
933
934        assert!(
935            matches!(core_error, CoreError::Serialization(_))
936                && core_error.to_string().contains("Serialization error:")
937        );
938    }
939
940    // Test Redis feature (conditional compilation)
941    #[cfg(feature = "redis")]
942    #[test]
943    fn test_core_error_redis_variant() {
944        use redis::RedisError;
945
946        // Create a Redis error (we'll simulate one)
947        let redis_error = RedisError::from((redis::ErrorKind::TypeError, "Type error"));
948        let core_error = CoreError::Redis(redis_error);
949
950        assert!(core_error.to_string().contains("Redis error:"));
951    }
952
953    #[cfg(feature = "redis")]
954    #[test]
955    fn test_core_error_from_redis_error() {
956        use redis::RedisError;
957
958        let redis_error = RedisError::from((redis::ErrorKind::IoError, "Connection failed"));
959        let core_error = CoreError::from(redis_error);
960
961        assert!(
962            matches!(core_error, CoreError::Redis(_))
963                && core_error.to_string().contains("Redis error:")
964        );
965    }
966
967    // Test various Duration formats for ExecutionTimeout
968    #[test]
969    fn test_worker_error_execution_timeout_various_durations() {
970        // Test zero duration
971        let timeout_zero = std::time::Duration::from_secs(0);
972        let error_zero = WorkerError::ExecutionTimeout {
973            timeout: timeout_zero,
974        };
975        assert_eq!(error_zero.to_string(), "Tool execution timed out after 0ns");
976
977        // Test milliseconds
978        let timeout_millis = std::time::Duration::from_millis(500);
979        let error_millis = WorkerError::ExecutionTimeout {
980            timeout: timeout_millis,
981        };
982        assert_eq!(
983            error_millis.to_string(),
984            "Tool execution timed out after 500ms"
985        );
986
987        // Test minutes
988        let timeout_minutes = std::time::Duration::from_secs(120);
989        let error_minutes = WorkerError::ExecutionTimeout {
990            timeout: timeout_minutes,
991        };
992        assert_eq!(
993            error_minutes.to_string(),
994            "Tool execution timed out after 120s"
995        );
996    }
997
998    // Test edge cases for RateLimited retry_after
999    #[test]
1000    fn test_rate_limited_retry_after_edge_cases() {
1001        // Test with zero duration
1002        let error1 = ToolError::rate_limited_with_source(
1003            TestError,
1004            "Rate limited",
1005            Some(std::time::Duration::from_secs(0)),
1006        );
1007        assert_eq!(
1008            error1.retry_after(),
1009            Some(std::time::Duration::from_secs(0))
1010        );
1011
1012        // Test with very large duration
1013        let error2 = ToolError::rate_limited_with_source(
1014            TestError,
1015            "Rate limited",
1016            Some(std::time::Duration::from_secs(u64::MAX)),
1017        );
1018        assert_eq!(
1019            error2.retry_after(),
1020            Some(std::time::Duration::from_secs(u64::MAX))
1021        );
1022    }
1023
1024    // Test all match arms in is_retriable
1025    #[test]
1026    fn test_is_retriable_all_variants() {
1027        let retriable = ToolError::Retriable {
1028            source: None,
1029            source_message: "Test error".to_string(),
1030            context: "test".to_string(),
1031        };
1032        assert!(retriable.is_retriable());
1033
1034        let rate_limited = ToolError::RateLimited {
1035            source: None,
1036            source_message: "Test error".to_string(),
1037            context: "test".to_string(),
1038            retry_after: None,
1039        };
1040        assert!(rate_limited.is_retriable());
1041
1042        let permanent = ToolError::Permanent {
1043            source: None,
1044            source_message: "Test error".to_string(),
1045            context: "test".to_string(),
1046        };
1047        assert!(!permanent.is_retriable());
1048
1049        let invalid_input = ToolError::InvalidInput {
1050            source: None,
1051            source_message: "Test error".to_string(),
1052            context: "test".to_string(),
1053        };
1054        assert!(!invalid_input.is_retriable());
1055
1056        let signer_context =
1057            ToolError::SignerContext(crate::signer::SignerError::NoSignerContext.to_string());
1058        assert!(!signer_context.is_retriable());
1059    }
1060
1061    // Test all match arms in retry_after
1062    #[test]
1063    fn test_retry_after_all_variants() {
1064        let rate_limited_with_delay = ToolError::RateLimited {
1065            source: None,
1066            source_message: "Test error".to_string(),
1067            context: "test".to_string(),
1068            retry_after: Some(std::time::Duration::from_secs(30)),
1069        };
1070        assert_eq!(
1071            rate_limited_with_delay.retry_after(),
1072            Some(std::time::Duration::from_secs(30))
1073        );
1074
1075        let rate_limited_no_delay = ToolError::RateLimited {
1076            source: None,
1077            source_message: "Test error".to_string(),
1078            context: "test".to_string(),
1079            retry_after: None,
1080        };
1081        assert_eq!(rate_limited_no_delay.retry_after(), None);
1082
1083        let retriable = ToolError::Retriable {
1084            source: None,
1085            source_message: "Test error".to_string(),
1086            context: "test".to_string(),
1087        };
1088        assert_eq!(retriable.retry_after(), None);
1089
1090        let permanent = ToolError::Permanent {
1091            source: None,
1092            source_message: "Test error".to_string(),
1093            context: "test".to_string(),
1094        };
1095        assert_eq!(permanent.retry_after(), None);
1096
1097        let invalid_input = ToolError::InvalidInput {
1098            source: None,
1099            source_message: "Test error".to_string(),
1100            context: "test".to_string(),
1101        };
1102        assert_eq!(invalid_input.retry_after(), None);
1103
1104        let signer_context =
1105            ToolError::SignerContext(crate::signer::SignerError::NoSignerContext.to_string());
1106        assert_eq!(signer_context.retry_after(), None);
1107    }
1108
1109    // Test all match arms in is_rate_limited
1110    #[test]
1111    fn test_is_rate_limited_all_variants() {
1112        let rate_limited = ToolError::RateLimited {
1113            source: None,
1114            source_message: "Test error".to_string(),
1115            context: "test".to_string(),
1116            retry_after: None,
1117        };
1118        assert!(rate_limited.is_rate_limited());
1119
1120        let retriable = ToolError::Retriable {
1121            source: None,
1122            source_message: "Test error".to_string(),
1123            context: "test".to_string(),
1124        };
1125        assert!(!retriable.is_rate_limited());
1126
1127        let permanent = ToolError::Permanent {
1128            source: None,
1129            source_message: "Test error".to_string(),
1130            context: "test".to_string(),
1131        };
1132        assert!(!permanent.is_rate_limited());
1133
1134        let invalid_input = ToolError::InvalidInput {
1135            source: None,
1136            source_message: "Test error".to_string(),
1137            context: "test".to_string(),
1138        };
1139        assert!(!invalid_input.is_rate_limited());
1140
1141        let signer_context =
1142            ToolError::SignerContext(crate::signer::SignerError::NoSignerContext.to_string());
1143        assert!(!signer_context.is_rate_limited());
1144    }
1145
1146    // Test that must_use attributes don't cause warnings
1147    #[test]
1148    fn test_must_use_methods() {
1149        let error = ToolError::retriable_string("test");
1150
1151        // These methods have #[must_use] attribute
1152        let _ = error.is_retriable();
1153        let _ = error.retry_after();
1154        let _ = error.is_rate_limited();
1155    }
1156}