blz_core/
error.rs

1//! Error types and handling for blz-core operations.
2//!
3//! This module provides a comprehensive error type that covers all possible failures
4//! in the blz cache system. Errors are categorized for easier handling and include
5//! context about recoverability for retry logic.
6//!
7//! ## Error Categories
8//!
9//! Errors are organized into logical categories:
10//!
11//! - **I/O Errors**: File system operations, disk access
12//! - **Network Errors**: HTTP requests, connectivity issues  
13//! - **Parse Errors**: Markdown parsing, TOML/JSON deserialization
14//! - **Index Errors**: Search index operations
15//! - **Storage Errors**: Cache storage operations
16//! - **Configuration Errors**: Invalid settings or config files
17//! - **Resource Errors**: Memory limits, timeouts, quotas
18//!
19//! ## Recovery Hints
20//!
21//! Errors include information about whether they might be recoverable through retries:
22//!
23//! ```rust
24//! use blz_core::{Error, Result, MarkdownParser};
25//!
26//! fn handle_operation() -> Result<()> {
27//!     match perform_operation() {
28//!         Err(e) if e.is_recoverable() => {
29//!             println!("Temporary failure, retrying...");
30//!             // Implement retry logic
31//!         }
32//!         Err(e) => {
33//!             println!("Permanent failure: {}", e);
34//!             println!("Category: {}", e.category());
35//!         }
36//!         Ok(()) => println!("Success"),
37//!     }
38//!     Ok(())
39//! }
40//!
41//! fn perform_operation() -> Result<()> { Ok(()) }
42//! ```
43
44use thiserror::Error;
45
46/// The main error type for blz-core operations.
47///
48/// All public functions in blz-core return `Result<T, Error>` for consistent error handling.
49/// The error type includes automatic conversion from common standard library errors and
50/// provides additional metadata for error handling logic.
51///
52/// ## Error Source Chain
53///
54/// Errors maintain the full error chain through the `source()` method, allowing
55/// for detailed error inspection and debugging.
56///
57/// ## Display vs Debug
58///
59/// - `Display` provides user-friendly error messages
60/// - `Debug` includes full error details and source chain information
61#[derive(Error, Debug)]
62pub enum Error {
63    /// I/O operation failed.
64    ///
65    /// Covers file system operations like reading/writing files, creating directories,
66    /// checking file permissions, etc. The underlying `std::io::Error` is preserved
67    /// to maintain detailed error information.
68    ///
69    /// ## Recoverability
70    ///
71    /// Some I/O errors are recoverable (timeouts, interruptions), while others
72    /// are permanent (permission denied, file not found).
73    #[error("IO error: {0}")]
74    Io(#[from] std::io::Error),
75
76    /// Network operation failed.
77    ///
78    /// Covers HTTP requests for fetching llms.txt files, checking `ETags`,
79    /// and other network operations. The underlying `reqwest::Error` is preserved
80    /// for detailed connection information.
81    ///
82    /// ## Recoverability
83    ///
84    /// Connection and timeout errors are typically recoverable, while
85    /// authentication and malformed URL errors are permanent.
86    #[error("Network error: {0}")]
87    Network(#[from] reqwest::Error),
88
89    /// Parsing operation failed.
90    ///
91    /// Occurs when markdown content cannot be parsed, TOML/JSON deserialization
92    /// fails, or content doesn't match expected format.
93    ///
94    /// ## Common Causes
95    ///
96    /// - Malformed markdown syntax
97    /// - Invalid TOML configuration
98    /// - Unexpected content structure
99    /// - Character encoding issues
100    #[error("Parse error: {0}")]
101    Parse(String),
102
103    /// Search index operation failed.
104    ///
105    /// Covers failures in creating, updating, or querying the search index.
106    /// This includes Tantivy-related errors and index corruption.
107    ///
108    /// ## Common Causes
109    ///
110    /// - Index corruption
111    /// - Disk space exhaustion during indexing
112    /// - Invalid search queries
113    /// - Schema version mismatches
114    #[error("Index error: {0}")]
115    Index(String),
116
117    /// Storage operation failed.
118    ///
119    /// Covers cache storage operations beyond basic file I/O, such as
120    /// managing archived versions, checksum validation, and cache consistency.
121    ///
122    /// ## Common Causes
123    ///
124    /// - Cache corruption
125    /// - Concurrent access conflicts
126    /// - Checksum mismatches
127    /// - Archive management failures
128    #[error("Storage error: {0}")]
129    Storage(String),
130
131    /// Configuration is invalid or inaccessible.
132    ///
133    /// Occurs when configuration files are malformed, contain invalid values,
134    /// or cannot be accessed due to permissions or path issues.
135    ///
136    /// ## Common Causes
137    ///
138    /// - Invalid TOML syntax in config files
139    /// - Missing required configuration fields
140    /// - Configuration values outside valid ranges
141    /// - Config directory creation failures
142    #[error("Configuration error: {0}")]
143    Config(String),
144
145    /// Requested resource was not found.
146    ///
147    /// Used for missing files, non-existent sources, or requested content
148    /// that doesn't exist in the cache.
149    ///
150    /// ## Common Causes
151    ///
152    /// - Requested source alias doesn't exist
153    /// - File was deleted after being indexed
154    /// - Cache was cleared but references remain
155    #[error("Not found: {0}")]
156    NotFound(String),
157
158    /// URL is malformed or invalid.
159    ///
160    /// Occurs when URLs provided for llms.txt sources cannot be parsed
161    /// or contain invalid characters/schemes.
162    ///
163    /// ## Common Causes
164    ///
165    /// - Malformed URLs in configuration
166    /// - Unsupported URL schemes
167    /// - Invalid characters in URLs
168    #[error("Invalid URL: {0}")]
169    InvalidUrl(String),
170
171    /// Resource limit was exceeded.
172    ///
173    /// Used when operations exceed configured limits such as memory usage,
174    /// file size, or processing time constraints.
175    ///
176    /// ## Common Causes
177    ///
178    /// - Document exceeds maximum size limit
179    /// - Memory usage exceeds configured threshold
180    /// - Too many concurrent operations
181    #[error("Resource limited: {0}")]
182    ResourceLimited(String),
183
184    /// Operation timed out.
185    ///
186    /// Used for operations that exceed their configured timeout duration.
187    /// This is typically recoverable with retry logic.
188    ///
189    /// ## Common Causes
190    ///
191    /// - Network request timeouts
192    /// - Long-running parsing operations
193    /// - Index operations on large documents
194    #[error("Timeout: {0}")]
195    Timeout(String),
196
197    /// Serialization or deserialization failed.
198    ///
199    /// Occurs when converting between data formats (JSON, TOML, binary)
200    /// fails due to incompatible formats or corruption.
201    ///
202    /// ## Common Causes
203    ///
204    /// - JSON/TOML syntax errors
205    /// - Schema version mismatches
206    /// - Data corruption
207    /// - Incompatible format versions
208    #[error("Serialization error: {0}")]
209    Serialization(String),
210
211    /// Generic error for uncategorized failures.
212    ///
213    /// Used for errors that don't fit other categories or for
214    /// wrapping third-party errors that don't have specific mappings.
215    #[error("{0}")]
216    Other(String),
217}
218
219impl From<serde_json::Error> for Error {
220    fn from(err: serde_json::Error) -> Self {
221        Self::Serialization(err.to_string())
222    }
223}
224
225impl From<toml::ser::Error> for Error {
226    fn from(err: toml::ser::Error) -> Self {
227        Self::Serialization(err.to_string())
228    }
229}
230
231impl From<toml::de::Error> for Error {
232    fn from(err: toml::de::Error) -> Self {
233        Self::Serialization(err.to_string())
234    }
235}
236
237impl Error {
238    /// Check if the error might be recoverable through retry logic.
239    ///
240    /// Returns `true` for errors that are typically temporary and might succeed
241    /// if the operation is retried after a delay. This includes network timeouts,
242    /// connection failures, and temporary I/O issues.
243    ///
244    /// # Returns
245    ///
246    /// - `true` for potentially recoverable errors (timeouts, connection issues)
247    /// - `false` for permanent errors (parse failures, invalid configuration)
248    ///
249    /// # Examples
250    ///
251    /// ```rust
252    /// use blz_core::{Error, Result};
253    /// use std::io;
254    ///
255    /// let recoverable_errors = vec![
256    ///     Error::Timeout("Request timed out".to_string()),
257    ///     Error::Io(io::Error::new(io::ErrorKind::TimedOut, "timeout")),
258    ///     Error::Io(io::Error::new(io::ErrorKind::Interrupted, "interrupted")),
259    /// ];
260    ///
261    /// let permanent_errors = vec![
262    ///     Error::Parse("Invalid markdown".to_string()),
263    ///     Error::Config("Missing field".to_string()),
264    ///     Error::InvalidUrl("Not a URL".to_string()),
265    /// ];
266    ///
267    /// for error in recoverable_errors {
268    ///     assert!(error.is_recoverable());
269    /// }
270    ///
271    /// for error in permanent_errors {
272    ///     assert!(!error.is_recoverable());
273    /// }
274    /// ```
275    ///
276    /// ## Retry Strategy
277    ///
278    /// When an error is recoverable, consider implementing exponential backoff:
279    ///
280    /// ```rust
281    /// use blz_core::{Error, Result};
282    /// use std::time::Duration;
283    ///
284    /// async fn retry_operation<F, T>(mut op: F, max_attempts: u32) -> Result<T>
285    /// where
286    ///     F: FnMut() -> Result<T>,
287    /// {
288    ///     let mut attempts = 0;
289    ///     let mut delay = Duration::from_millis(100);
290    ///
291    ///     loop {
292    ///         match op() {
293    ///             Ok(result) => return Ok(result),
294    ///             Err(e) if e.is_recoverable() && attempts < max_attempts => {
295    ///                 attempts += 1;
296    ///                 tokio::time::sleep(delay).await;
297    ///                 delay *= 2; // Exponential backoff
298    ///             }
299    ///             Err(e) => return Err(e),
300    ///         }
301    ///     }
302    /// }
303    ///
304    /// // Example usage:
305    /// // let result = retry_operation(|| fetch_document(), 3).await?;
306    /// ```
307    #[must_use]
308    pub fn is_recoverable(&self) -> bool {
309        match self {
310            Self::Network(e) => {
311                // Consider connection errors as recoverable
312                e.is_timeout() || e.is_connect()
313            },
314            Self::Timeout(_) => true,
315            Self::Io(e) => {
316                // Consider temporary I/O errors as recoverable
317                matches!(
318                    e.kind(),
319                    std::io::ErrorKind::TimedOut | std::io::ErrorKind::Interrupted
320                )
321            },
322            _ => false,
323        }
324    }
325
326    /// Get the error category as a string identifier.
327    ///
328    /// Returns a static string that categorizes the error type for logging,
329    /// metrics collection, and error handling logic. This is useful for
330    /// grouping errors in monitoring systems or implementing category-specific
331    /// error handling.
332    ///
333    /// # Returns
334    ///
335    /// A static string representing the error category:
336    ///
337    /// - `"io"` - File system and I/O operations
338    /// - `"network"` - HTTP requests and network operations
339    /// - `"parse"` - Content parsing and format conversion
340    /// - `"index"` - Search index operations
341    /// - `"storage"` - Cache storage and management
342    /// - `"config"` - Configuration and settings
343    /// - `"not_found"` - Missing resources or files
344    /// - `"invalid_url"` - URL format and validation
345    /// - `"resource_limited"` - Resource constraints and limits
346    /// - `"timeout"` - Operation timeouts
347    /// - `"serialization"` - Data format conversion
348    /// - `"other"` - Uncategorized errors
349    ///
350    /// # Examples
351    ///
352    /// ```rust
353    /// use blz_core::Error;
354    /// use std::collections::HashMap;
355    ///
356    /// // Track error counts by category
357    /// let mut error_counts: HashMap<String, u32> = HashMap::new();
358    ///
359    /// fn record_error(error: &Error, counts: &mut HashMap<String, u32>) {
360    ///     let category = error.category().to_string();
361    ///     *counts.entry(category).or_insert(0) += 1;
362    /// }
363    ///
364    /// // Usage in error handling
365    /// let errors = vec![
366    ///     Error::Parse("Invalid format".to_string()),
367    ///     Error::Config("Missing field".to_string()),
368    ///     Error::NotFound("Resource not found".to_string()),
369    /// ];
370    ///
371    /// for error in &errors {
372    ///     record_error(error, &mut error_counts);
373    /// }
374    /// ```
375    ///
376    /// ## Structured Logging
377    ///
378    /// ```rust
379    /// use blz_core::Error;
380    ///
381    /// fn log_error(error: &Error) {
382    ///     println!(
383    ///         "{{\"level\":\"error\",\"category\":\"{}\",\"message\":\"{}\"}}",
384    ///         error.category(),
385    ///         error
386    ///     );
387    /// }
388    /// ```
389    #[must_use]
390    pub const fn category(&self) -> &'static str {
391        match self {
392            Self::Io(_) => "io",
393            Self::Network(_) => "network",
394            Self::Parse(_) => "parse",
395            Self::Index(_) => "index",
396            Self::Storage(_) => "storage",
397            Self::Config(_) => "config",
398            Self::NotFound(_) => "not_found",
399            Self::InvalidUrl(_) => "invalid_url",
400            Self::ResourceLimited(_) => "resource_limited",
401            Self::Timeout(_) => "timeout",
402            Self::Serialization(_) => "serialization",
403            Self::Other(_) => "other",
404        }
405    }
406}
407
408/// Convenience type alias for `std::result::Result<T, Error>`.
409///
410/// This type is used throughout blz-core for consistent error handling.
411/// It's equivalent to `std::result::Result<T, blz_core::Error>` but more concise.
412///
413/// # Examples
414///
415/// ```rust
416/// use blz_core::{Result, Config};
417///
418/// fn load_config() -> Result<Config> {
419///     Config::load() // Returns Result<Config>
420/// }
421///
422/// fn main() -> Result<()> {
423///     let config = load_config()?;
424///     println!("Loaded config with root: {}", config.paths.root.display());
425///     Ok(())
426/// }
427/// ```
428pub type Result<T> = std::result::Result<T, Error>;
429
430#[cfg(test)]
431#[allow(
432    clippy::panic,
433    clippy::disallowed_macros,
434    clippy::unwrap_used,
435    clippy::unnecessary_wraps
436)]
437mod tests {
438    use super::*;
439    use proptest::prelude::*;
440    use std::io;
441
442    #[test]
443    fn test_error_display_formatting() {
444        // Given: Different error variants
445        let errors = vec![
446            Error::Parse("invalid syntax".to_string()),
447            Error::Index("search failed".to_string()),
448            Error::Storage("disk full".to_string()),
449            Error::Config("missing field".to_string()),
450            Error::NotFound("document".to_string()),
451            Error::InvalidUrl("not a url".to_string()),
452            Error::ResourceLimited("too many requests".to_string()),
453            Error::Timeout("operation timed out".to_string()),
454            Error::Other("unknown error".to_string()),
455        ];
456
457        for error in errors {
458            // When: Converting to string
459            let error_string = error.to_string();
460
461            // Then: Should contain descriptive information
462            assert!(!error_string.is_empty());
463            match error {
464                Error::Parse(msg) => {
465                    assert!(error_string.contains("Parse error"));
466                    assert!(error_string.contains(&msg));
467                },
468                Error::Index(msg) => {
469                    assert!(error_string.contains("Index error"));
470                    assert!(error_string.contains(&msg));
471                },
472                Error::Storage(msg) => {
473                    assert!(error_string.contains("Storage error"));
474                    assert!(error_string.contains(&msg));
475                },
476                Error::Config(msg) => {
477                    assert!(error_string.contains("Configuration error"));
478                    assert!(error_string.contains(&msg));
479                },
480                Error::NotFound(msg) => {
481                    assert!(error_string.contains("Not found"));
482                    assert!(error_string.contains(&msg));
483                },
484                Error::InvalidUrl(msg) => {
485                    assert!(error_string.contains("Invalid URL"));
486                    assert!(error_string.contains(&msg));
487                },
488                Error::ResourceLimited(msg) => {
489                    assert!(error_string.contains("Resource limited"));
490                    assert!(error_string.contains(&msg));
491                },
492                Error::Timeout(msg) => {
493                    assert!(error_string.contains("Timeout"));
494                    assert!(error_string.contains(&msg));
495                },
496                Error::Other(msg) => {
497                    assert_eq!(error_string, msg);
498                },
499                _ => {},
500            }
501        }
502    }
503
504    #[test]
505    fn test_error_from_io_error() {
506        // Given: Different types of I/O errors
507        let io_errors = vec![
508            io::Error::new(io::ErrorKind::NotFound, "file not found"),
509            io::Error::new(io::ErrorKind::PermissionDenied, "access denied"),
510            io::Error::new(io::ErrorKind::TimedOut, "operation timed out"),
511            io::Error::new(io::ErrorKind::Interrupted, "interrupted"),
512        ];
513
514        for io_err in io_errors {
515            // When: Converting to our Error type
516            let error: Error = io_err.into();
517
518            // Then: Should be IO error variant
519            match error {
520                Error::Io(inner) => {
521                    assert!(!inner.to_string().is_empty());
522                },
523                _ => panic!("Expected IO error variant"),
524            }
525        }
526    }
527
528    #[test]
529    fn test_error_from_reqwest_error() {
530        // Given: Mock reqwest errors (using builder pattern since reqwest errors are opaque)
531        // Note: We can't easily create reqwest::Error instances in tests, so we'll focus on
532        // what we can test about the conversion
533
534        // This test ensures the From implementation exists and compiles
535        // In practice, reqwest errors would come from actual HTTP operations
536        fn create_network_error_result() -> Result<()> {
537            // This would typically come from reqwest operations
538            let _client = reqwest::Client::new();
539            // We can't easily trigger a reqwest error in tests without network calls
540            // but we can verify the error type conversion works by checking the variant
541            Ok(())
542        }
543
544        // When/Then: The conversion should compile and work (tested implicitly)
545        assert!(create_network_error_result().is_ok());
546    }
547
548    #[test]
549    fn test_error_categories() {
550        // Given: All error variants
551        let error_categories = vec![
552            (Error::Io(io::Error::other("test")), "io"),
553            (Error::Parse("test".to_string()), "parse"),
554            (Error::Index("test".to_string()), "index"),
555            (Error::Storage("test".to_string()), "storage"),
556            (Error::Config("test".to_string()), "config"),
557            (Error::NotFound("test".to_string()), "not_found"),
558            (Error::InvalidUrl("test".to_string()), "invalid_url"),
559            (
560                Error::ResourceLimited("test".to_string()),
561                "resource_limited",
562            ),
563            (Error::Timeout("test".to_string()), "timeout"),
564            (Error::Serialization("test".to_string()), "serialization"),
565            (Error::Other("test".to_string()), "other"),
566        ];
567
568        for (error, expected_category) in error_categories {
569            // When: Getting error category
570            let category = error.category();
571
572            // Then: Should match expected category
573            assert_eq!(category, expected_category);
574        }
575    }
576
577    #[test]
578    fn test_error_recoverability() {
579        // Given: Various error scenarios
580        let recoverable_errors = vec![
581            Error::Io(io::Error::new(io::ErrorKind::TimedOut, "timeout")),
582            Error::Io(io::Error::new(io::ErrorKind::Interrupted, "interrupted")),
583            Error::Timeout("request timeout".to_string()),
584        ];
585
586        let non_recoverable_errors = vec![
587            Error::Io(io::Error::new(io::ErrorKind::NotFound, "not found")),
588            Error::Io(io::Error::new(io::ErrorKind::PermissionDenied, "denied")),
589            Error::Parse("bad syntax".to_string()),
590            Error::Index("corrupt index".to_string()),
591            Error::Storage("disk failure".to_string()),
592            Error::Config("invalid config".to_string()),
593            Error::NotFound("missing".to_string()),
594            Error::InvalidUrl("bad url".to_string()),
595            Error::ResourceLimited("quota exceeded".to_string()),
596            Error::Other("generic error".to_string()),
597        ];
598
599        // When/Then: Testing recoverability
600        for error in recoverable_errors {
601            assert!(
602                error.is_recoverable(),
603                "Expected {error:?} to be recoverable"
604            );
605        }
606
607        for error in non_recoverable_errors {
608            assert!(
609                !error.is_recoverable(),
610                "Expected {error:?} to be non-recoverable"
611            );
612        }
613    }
614
615    #[test]
616    fn test_error_debug_formatting() {
617        // Given: Error with detailed information
618        let error = Error::Parse("Failed to parse JSON at line 42".to_string());
619
620        // When: Debug formatting
621        let debug_str = format!("{error:?}");
622
623        // Then: Should contain variant name and message
624        assert!(debug_str.contains("Parse"));
625        assert!(debug_str.contains("Failed to parse JSON at line 42"));
626    }
627
628    #[test]
629    fn test_error_chain_source() {
630        // Given: IO error that can be converted to our error type
631        let io_error = io::Error::new(io::ErrorKind::PermissionDenied, "access denied");
632        let blz_error: Error = io_error.into();
633
634        // When: Checking error source
635        let source = std::error::Error::source(&blz_error);
636
637        // Then: Should maintain the source chain
638        assert!(source.is_some());
639        let source_str = source.unwrap().to_string();
640        assert!(source_str.contains("access denied"));
641    }
642
643    #[test]
644    fn test_result_type_alias() {
645        // Given: Function that returns our Result type
646        fn test_function() -> Result<i32> {
647            Ok(42)
648        }
649
650        fn test_error_function() -> Result<i32> {
651            Err(Error::Other("test error".to_string()))
652        }
653
654        // When: Using the Result type
655        let ok_result = test_function();
656        let err_result = test_error_function();
657
658        // Then: Should work as expected
659        assert!(ok_result.is_ok());
660        assert_eq!(ok_result.unwrap(), 42);
661
662        assert!(err_result.is_err());
663        if let Err(Error::Other(msg)) = err_result {
664            assert_eq!(msg, "test error");
665        } else {
666            panic!("Expected Other error");
667        }
668    }
669
670    // Property-based tests
671    proptest! {
672        #[test]
673        fn test_parse_error_with_arbitrary_messages(msg in r".{0,1000}") {
674            let error = Error::Parse(msg.clone());
675            let error_string = error.to_string();
676
677            prop_assert!(error_string.contains("Parse error"));
678            prop_assert!(error_string.contains(&msg));
679            prop_assert_eq!(error.category(), "parse");
680            prop_assert!(!error.is_recoverable());
681        }
682
683        #[test]
684        fn test_index_error_with_arbitrary_messages(msg in r".{0,1000}") {
685            let error = Error::Index(msg.clone());
686            let error_string = error.to_string();
687
688            prop_assert!(error_string.contains("Index error"));
689            prop_assert!(error_string.contains(&msg));
690            prop_assert_eq!(error.category(), "index");
691            prop_assert!(!error.is_recoverable());
692        }
693
694        #[test]
695        fn test_storage_error_with_arbitrary_messages(msg in r".{0,1000}") {
696            let error = Error::Storage(msg.clone());
697            let error_string = error.to_string();
698
699            prop_assert!(error_string.contains("Storage error"));
700            prop_assert!(error_string.contains(&msg));
701            prop_assert_eq!(error.category(), "storage");
702            prop_assert!(!error.is_recoverable());
703        }
704
705        #[test]
706        fn test_config_error_with_arbitrary_messages(msg in r".{0,1000}") {
707            let error = Error::Config(msg.clone());
708            let error_string = error.to_string();
709
710            prop_assert!(error_string.contains("Configuration error"));
711            prop_assert!(error_string.contains(&msg));
712            prop_assert_eq!(error.category(), "config");
713            prop_assert!(!error.is_recoverable());
714        }
715
716        #[test]
717        fn test_other_error_with_arbitrary_messages(msg in r".{0,1000}") {
718            let error = Error::Other(msg.clone());
719            let error_string = error.to_string();
720
721            prop_assert_eq!(error_string, msg);
722            prop_assert_eq!(error.category(), "other");
723            prop_assert!(!error.is_recoverable());
724        }
725    }
726
727    // Security-focused tests
728    #[test]
729    fn test_error_with_malicious_messages() {
730        // Given: Error messages with potentially malicious content
731        let long_message = "very_long_message_".repeat(1000);
732        let malicious_messages = vec![
733            "\n\r\x00\x01malicious",
734            "<script>alert('xss')</script>",
735            "'; DROP TABLE users; --",
736            "../../../etc/passwd",
737            "\u{202e}reverse text\u{202d}",
738            &long_message,
739        ];
740
741        for malicious_msg in malicious_messages {
742            // When: Creating errors with malicious messages
743            let errors = vec![
744                Error::Parse(malicious_msg.to_string()),
745                Error::Index(malicious_msg.to_string()),
746                Error::Storage(malicious_msg.to_string()),
747                Error::Config(malicious_msg.to_string()),
748                Error::NotFound(malicious_msg.to_string()),
749                Error::InvalidUrl(malicious_msg.to_string()),
750                Error::ResourceLimited(malicious_msg.to_string()),
751                Error::Timeout(malicious_msg.to_string()),
752                Error::Other(malicious_msg.to_string()),
753            ];
754
755            for error in errors {
756                // Then: Should handle malicious content safely
757                let error_string = error.to_string();
758                assert!(!error_string.is_empty());
759
760                // Should preserve the malicious content (not sanitize it)
761                // This is intentional - error handling should not modify user content
762                // Sanitization should happen at display time if needed
763                assert!(error_string.contains(malicious_msg));
764            }
765        }
766    }
767
768    #[test]
769    fn test_error_with_unicode_messages() {
770        // Given: Error messages with Unicode content
771        let unicode_messages = vec![
772            "エラーが発生しました",  // Japanese
773            "حدث خطأ",               // Arabic
774            "Произошла ошибка",      // Russian
775            "🚨 Błąd krytyczny! 🚨", // Polish with emojis
776            "Error: файл не найден", // Mixed languages
777        ];
778
779        for unicode_msg in unicode_messages {
780            // When: Creating errors with Unicode messages
781            let error = Error::Parse(unicode_msg.to_string());
782
783            // Then: Should handle Unicode correctly
784            let error_string = error.to_string();
785            assert!(error_string.contains(unicode_msg));
786            assert_eq!(error.category(), "parse");
787        }
788    }
789
790    #[test]
791    fn test_error_empty_messages() {
792        // Given: Errors with empty messages
793        let errors_with_empty_msgs = vec![
794            Error::Parse(String::new()),
795            Error::Index(String::new()),
796            Error::Storage(String::new()),
797            Error::Config(String::new()),
798            Error::NotFound(String::new()),
799            Error::InvalidUrl(String::new()),
800            Error::ResourceLimited(String::new()),
801            Error::Timeout(String::new()),
802            Error::Other(String::new()),
803        ];
804
805        for error in errors_with_empty_msgs {
806            // When: Converting to string
807            let error_string = error.to_string();
808
809            // Then: Check error formatting behavior
810            if let Error::Other(_) = error {
811                // Other errors just show the message (which is empty)
812                assert_eq!(error_string, "");
813            } else {
814                // All other errors have descriptive prefixes even with empty messages
815                assert!(!error_string.is_empty());
816                assert!(
817                    error_string.contains(':'),
818                    "Error should contain colon separator: '{error_string}'"
819                );
820            }
821        }
822    }
823
824    #[test]
825    fn test_error_size() {
826        // Given: Error enum
827        // When: Checking size
828        let error_size = std::mem::size_of::<Error>();
829
830        // Then: Should be reasonably sized (not too large)
831        // This helps ensure the error type is efficient to pass around
832        assert!(error_size <= 64, "Error type too large: {error_size} bytes");
833    }
834}