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}