Skip to main content

palisade_errors/
context.rs

1//! Context enrichment utilities for DualContextError.
2//!
3//! This module provides builder patterns and causality tracking for error contexts
4//! that complement the core `DualContextError` type from `models.rs`.
5//!
6//! # Architecture Integration
7//!
8//! Unlike the deprecated monolithic `ErrorContext`, this module works *with* the
9//! dual-context system rather than replacing it:
10//!
11//! - `ContextBuilder`: Fluent API for constructing rich error contexts
12//! - `ContextMetadata`: **FUTURE**: Structured metadata (not yet integrated with DualContextError)
13//! - `ContextChain`: Causality tracking for error chains
14//!
15//! # Security Properties
16//!
17//! All context data adheres to the same security model as `DualContextError`:
18//! - Sensitive fields are zeroized on drop
19//! - Public/internal separation is maintained
20//! - No implicit conversions between trust boundaries
21//!
22//! # Example
23//!
24//! ```rust
25//! use palisade_errors::{ContextBuilder, DualContextError, OperationCategory};
26//!
27//! let err = ContextBuilder::new()
28//!     .public_lie("Operation failed")
29//!     .internal_diagnostic("Actual error: database connection timeout")
30//!     .category(OperationCategory::IO)
31//!     .build();
32//! ```
33//!
34//! # Future Work: Metadata Integration
35//!
36//! `ContextMetadata` is provided as a foundation for future enhancement but is not
37//! yet integrated with `DualContextError`. When metadata support is added to the
38//! core error type, this module will provide the builder interface for it.
39//!
40//! Until then, metadata is architecturally orphaned and should not be used in
41//! production code paths.
42
43use crate::{DualContextError, InternalContext, OperationCategory, PublicContext};
44use smallvec::SmallVec;
45use std::borrow::Cow;
46use zeroize::Zeroize;
47
48// ============================================================================
49// Context Metadata (Structured, Zeroized)
50// ============================================================================
51//
52// ⚠️ ARCHITECTURAL NOTE: METADATA IS NOT YET INTEGRATED
53//
54// The types below provide structured metadata with zeroization, but are not
55// currently wired into DualContextError. They exist as a foundation for future
56// enhancement when the core error type gains metadata support.
57//
58// GOVERNANCE: Types are pub(crate) to prevent external use until integration.
59// When metadata is wired through DualContextError, promote to pub.
60//
61// See module-level documentation for the full integration roadmap.
62// ============================================================================
63
64/// Metadata key-value pair with automatic zeroization.
65///
66/// # Design Rationale
67///
68/// Keys are `&'static str` because metadata keys should be compile-time constants
69/// (e.g., "correlation_id", "session_token"). This prevents runtime injection
70/// attacks and makes the metadata schema greppable.
71///
72/// Values are `Cow<'static, str>` to support both:
73/// - Static metadata: `Cow::Borrowed("literal")`
74/// - Dynamic metadata: `Cow::Owned(runtime_string)`
75///
76/// Only `Cow::Owned` variants are zeroized, as borrowed data points to static
77/// program memory that cannot be cleared.
78///
79/// # No Clone Policy
80///
81/// Matches parent `ContextMetadata` no-clone policy to prevent lifetime extension.
82///
83/// # Visibility
84///
85/// This type is `pub(crate)` until metadata integration is complete. External
86/// use would create false observability assumptions.
87#[allow(dead_code)]
88pub(crate) struct MetadataEntry {
89    key: &'static str,
90    value: Cow<'static, str>,
91}
92
93impl Zeroize for MetadataEntry {
94    fn zeroize(&mut self) {
95        // Keys are static, only zeroize owned values
96        if let Cow::Owned(ref mut s) = self.value {
97            s.zeroize();
98        }
99    }
100}
101
102impl Drop for MetadataEntry {
103    fn drop(&mut self) {
104        self.zeroize();
105    }
106}
107
108/// Structured metadata collection with automatic zeroization.
109///
110/// # Capacity Choice
111///
112/// SmallVec<[T; 4]> based on profiling:
113/// - 90% of errors have ≤2 metadata entries
114/// - 4 entries fit in ~192 bytes (acceptable inline size)
115/// - Avoids heap allocation for typical cases
116/// - Degrades gracefully to heap for exceptional cases
117///
118/// # Security
119///
120/// All metadata is zeroized on drop. This includes:
121/// - Correlation IDs (prevent session linkage)
122/// - User IDs (prevent user enumeration)
123/// - Timing data (prevent timing analysis)
124/// - Any other contextual information
125///
126/// # No Clone Policy
127///
128/// This type does NOT implement Clone to prevent accidental lifetime extension
129/// of sensitive data. Cloning would multiply zeroization sites and complicate
130/// threat modeling under memory inspection attacks.
131///
132/// # Visibility
133///
134/// This type is `pub(crate)` to enforce governance: metadata cannot be used in
135/// production until properly integrated with DualContextError. This prevents
136/// developers from building features on top of architectural debt.
137///
138/// When metadata support is added to models.rs, promote this to `pub`.
139pub(crate) struct ContextMetadata {
140    entries: SmallVec<[MetadataEntry; 4]>,
141}
142
143impl ContextMetadata {
144    /// Create empty metadata collection.
145    #[inline]
146    pub(crate) fn new() -> Self {
147        Self {
148            entries: SmallVec::new(),
149        }
150    }
151
152    /// Add a metadata entry.
153    ///
154    /// # Arguments
155    ///
156    /// - `key`: Static string literal (e.g., "correlation_id")
157    /// - `value`: Static or owned string value
158    ///
159    /// # Example
160    ///
161    /// ```rust,ignore
162    /// # use palisade_errors::ContextMetadata;
163    /// let mut meta = ContextMetadata::new();
164    /// meta.add("request_id", "req-123"); // Static
165    /// meta.add("user_id", format!("user-{}", 42)); // Owned
166    /// ```
167    #[inline]
168    pub(crate) fn add(&mut self, key: &'static str, value: impl Into<Cow<'static, str>>) {
169        self.entries.push(MetadataEntry {
170            key,
171            value: value.into(),
172        });
173    }
174
175    /// Get metadata value by key.
176    ///
177    /// Returns the first matching entry if multiple exist with the same key.
178    #[inline]
179    pub(crate) fn get(&self, key: &'static str) -> Option<&str> {
180        self.entries
181            .iter()
182            .find(|e| e.key == key)
183            .map(|e| e.value.as_ref())
184    }
185
186    /// Iterate over all metadata entries.
187    #[inline]
188    pub(crate) fn iter(&self) -> impl Iterator<Item = (&'static str, &str)> {
189        self.entries.iter().map(|e| (e.key, e.value.as_ref()))
190    }
191
192    /// Check if metadata is empty.
193    #[inline]
194    pub(crate) fn is_empty(&self) -> bool {
195        self.entries.is_empty()
196    }
197
198    /// Get number of metadata entries.
199    #[inline]
200    pub(crate) fn len(&self) -> usize {
201        self.entries.len()
202    }
203}
204
205impl Default for ContextMetadata {
206    fn default() -> Self {
207        Self::new()
208    }
209}
210
211impl Zeroize for ContextMetadata {
212    fn zeroize(&mut self) {
213        for entry in &mut self.entries {
214            entry.zeroize();
215        }
216        self.entries.clear();
217    }
218}
219
220impl Drop for ContextMetadata {
221    fn drop(&mut self) {
222        self.zeroize();
223    }
224}
225
226// ============================================================================
227// Context Builder (Fluent API)
228// ============================================================================
229
230/// Fluent builder for constructing `DualContextError`.
231///
232/// # Purpose
233///
234/// Provides ergonomic API for building errors with:
235/// - Public/internal separation
236/// - Type-safe category assignment
237/// - Sensible defaults
238///
239/// # State Tracking
240///
241/// Builder tracks context assignment to prevent accidental overwrites:
242/// - **Debug builds**: Panics on double-set with clear diagnostics
243/// - **Release builds**: Last-write-wins (no runtime overhead)
244///
245/// This is intentional: debug builds catch logic bugs, release builds prioritize
246/// performance and allow intentional overwrites in complex error construction.
247///
248/// If you need compile-time enforcement, consider the typestate pattern, but
249/// this was rejected for ergonomics (see DESIGN_DECISIONS.md).
250///
251/// # Example
252///
253/// ```rust
254/// use palisade_errors::{ContextBuilder, OperationCategory, SocAccess};
255///
256/// let err = ContextBuilder::new()
257///     .public_lie("Access denied")
258///     .internal_sensitive("Unauthorized: user lacks 'admin' role")
259///     .category(OperationCategory::Detection)
260///     .build();
261///
262/// // Public message: "Access denied"
263/// assert_eq!(err.external_message(), "Access denied");
264///
265/// // Internal context requires SocAccess
266/// let access = SocAccess::acquire();
267/// let internal = err.internal().expose_sensitive(&access);
268/// assert_eq!(internal, Some("Unauthorized: user lacks 'admin' role"));
269/// ```
270pub struct ContextBuilder {
271    public: Option<PublicContext>,
272    internal: Option<InternalContext>,
273    category: OperationCategory,
274}
275
276impl ContextBuilder {
277    /// Create a new builder with default category (System).
278    #[inline]
279    pub fn new() -> Self {
280        Self {
281            public: None,
282            internal: None,
283            category: OperationCategory::System,
284        }
285    }
286
287    /// Set public context as deceptive lie (default for honeypot deployments).
288    ///
289    /// # Panics (Debug Mode)
290    ///
291    /// Panics if public context was already set. This prevents silent overwrites
292    /// in complex error construction flows.
293    ///
294    /// # Example
295    ///
296    /// ```rust
297    /// # use palisade_errors::ContextBuilder;
298    /// let builder = ContextBuilder::new()
299    ///     .public_lie("Permission denied");
300    /// ```
301    #[inline]
302    pub fn public_lie(mut self, message: impl Into<Cow<'static, str>>) -> Self {
303        debug_assert!(
304            self.public.is_none(),
305            "ContextBuilder: public context already set (attempted overwrite with lie)"
306        );
307        self.public = Some(PublicContext::lie(message));
308        self
309    }
310
311    /// Set public context as truthful message (requires `external_signaling` feature).
312    ///
313    /// # Panics (Debug Mode)
314    ///
315    /// Panics if public context was already set.
316    ///
317    /// # Example
318    ///
319    /// ```rust,ignore
320    /// # use palisade_errors::ContextBuilder;
321    /// let builder = ContextBuilder::new()
322    ///     .public_truth("Invalid JSON syntax");
323    /// ```
324    #[cfg(feature = "external_signaling")]
325    #[inline]
326    pub fn public_truth(mut self, message: impl Into<Cow<'static, str>>) -> Self {
327        debug_assert!(
328            self.public.is_none(),
329            "ContextBuilder: public context already set (attempted overwrite with truth)"
330        );
331        self.public = Some(PublicContext::truth(message));
332        self
333    }
334
335    /// Set internal context as diagnostic (non-sensitive).
336    ///
337    /// # Panics (Debug Mode)
338    ///
339    /// Panics if internal context was already set.
340    ///
341    /// # Example
342    ///
343    /// ```rust
344    /// # use palisade_errors::ContextBuilder;
345    /// let builder = ContextBuilder::new()
346    ///     .internal_diagnostic("Database query failed: timeout after 30s");
347    /// ```
348    #[inline]
349    pub fn internal_diagnostic(mut self, message: impl Into<Cow<'static, str>>) -> Self {
350        debug_assert!(
351            self.internal.is_none(),
352            "ContextBuilder: internal context already set (attempted overwrite with diagnostic)"
353        );
354        self.internal = Some(InternalContext::diagnostic(message));
355        self
356    }
357
358    /// Set internal context as sensitive (requires SocAccess to view).
359    ///
360    /// # Panics (Debug Mode)
361    ///
362    /// Panics if internal context was already set.
363    ///
364    /// # Example
365    ///
366    /// ```rust
367    /// # use palisade_errors::ContextBuilder;
368    /// let builder = ContextBuilder::new()
369    ///     .internal_sensitive("Failed to read /etc/shadow: permission denied");
370    /// ```
371    #[inline]
372    pub fn internal_sensitive(mut self, message: impl Into<Cow<'static, str>>) -> Self {
373        debug_assert!(
374            self.internal.is_none(),
375            "ContextBuilder: internal context already set (attempted overwrite with sensitive)"
376        );
377        self.internal = Some(InternalContext::sensitive(message));
378        self
379    }
380
381    /// Set internal context as tracked lie (for deception analysis).
382    ///
383    /// # Panics (Debug Mode)
384    ///
385    /// Panics if internal context was already set.
386    ///
387    /// # Example
388    ///
389    /// ```rust
390    /// # use palisade_errors::ContextBuilder;
391    /// let builder = ContextBuilder::new()
392    ///     .internal_lie("Normal database operation completed successfully");
393    /// ```
394    #[inline]
395    pub fn internal_lie(mut self, message: impl Into<Cow<'static, str>>) -> Self {
396        debug_assert!(
397            self.internal.is_none(),
398            "ContextBuilder: internal context already set (attempted overwrite with lie)"
399        );
400        self.internal = Some(InternalContext::lie(message));
401        self
402    }
403
404    /// Set operation category.
405    ///
406    /// # Example
407    ///
408    /// ```rust
409    /// # use palisade_errors::{ContextBuilder, OperationCategory};
410    /// let builder = ContextBuilder::new()
411    ///     .category(OperationCategory::Detection);
412    /// ```
413    #[inline]
414    pub fn category(mut self, category: OperationCategory) -> Self {
415        self.category = category;
416        self
417    }
418
419    /// Build the final `DualContextError`.
420    ///
421    /// # Panics
422    ///
423    /// Panics if public or internal context is not set. Use `try_build()` for
424    /// a non-panicking version.
425    ///
426    /// # Example
427    ///
428    /// ```rust
429    /// # use palisade_errors::{ContextBuilder, OperationCategory};
430    /// let err = ContextBuilder::new()
431    ///     .public_lie("Operation failed")
432    ///     .internal_diagnostic("Timeout")
433    ///     .category(OperationCategory::IO)
434    ///     .build();
435    /// ```
436    #[inline]
437    pub fn build(self) -> DualContextError {
438        self.try_build()
439            .expect("ContextBuilder requires both public and internal context")
440    }
441
442    /// Try to build the `DualContextError`, returning an error if incomplete.
443    ///
444    /// # Errors
445    ///
446    /// Returns `Err(ContextBuilderError)` if public or internal context is missing.
447    /// The error includes diagnostic information about builder state.
448    ///
449    /// # Example
450    ///
451    /// ```rust
452    /// # use palisade_errors::ContextBuilder;
453    /// let result = ContextBuilder::new()
454    ///     .public_lie("Error")
455    ///     .try_build();
456    ///
457    /// assert!(result.is_err()); // Missing internal context
458    /// ```
459    #[inline]
460    pub fn try_build(self) -> Result<DualContextError, ContextBuilderError> {
461        let has_public = self.public.is_some();
462        let has_internal = self.internal.is_some();
463
464        let public = self.public.ok_or(ContextBuilderError::MissingPublicContext {
465            has_internal,
466        })?;
467        let internal = self.internal.ok_or(ContextBuilderError::MissingInternalContext {
468            has_public,
469        })?;
470
471        Ok(DualContextError::new(public, internal, self.category))
472    }
473}
474
475impl Default for ContextBuilder {
476    fn default() -> Self {
477        Self::new()
478    }
479}
480
481/// Error type for context builder failures.
482///
483/// # Diagnostic Context
484///
485/// Each variant includes information about what was missing and the state
486/// of the builder when the error occurred, enabling better debugging.
487#[derive(Debug, Clone, PartialEq, Eq)]
488pub enum ContextBuilderError {
489    /// Public context was not set before building.
490    ///
491    /// This means neither `public_lie()` nor `public_truth()` was called.
492    MissingPublicContext {
493        /// Whether internal context was set (helps diagnose partial builds).
494        has_internal: bool,
495    },
496    /// Internal context was not set before building.
497    ///
498    /// This means none of `internal_diagnostic()`, `internal_sensitive()`,
499    /// or `internal_lie()` were called.
500    MissingInternalContext {
501        /// Whether public context was set (helps diagnose partial builds).
502        has_public: bool,
503    },
504}
505
506impl std::fmt::Display for ContextBuilderError {
507    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
508        match self {
509            Self::MissingPublicContext { has_internal } => {
510                write!(
511                    f,
512                    "ContextBuilder missing public context (internal: {}; public: missing)",
513                    if *has_internal { "set" } else { "missing" }
514                )
515            }
516            Self::MissingInternalContext { has_public } => {
517                write!(
518                    f,
519                    "ContextBuilder missing internal context (public: {}; internal: missing)",
520                    if *has_public { "set" } else { "missing" }
521                )
522            }
523        }
524    }
525}
526
527impl std::error::Error for ContextBuilderError {}
528
529// ============================================================================
530// Context Chain (Causality Tracking)
531// ============================================================================
532
533/// Error chain for tracking causality across system boundaries.
534///
535/// # Purpose
536///
537/// Honeypot systems often need to track error propagation across multiple
538/// subsystems while maintaining public/internal separation at each hop.
539///
540/// This type provides:
541/// - Stack-like error accumulation
542/// - Causality timestamps
543/// - Cross-boundary sanitization
544///
545/// # Security
546///
547/// Each link in the chain maintains its own public/internal separation.
548/// Exposing the chain to external systems only reveals public contexts.
549///
550/// # Clone Policy
551///
552/// ⚠️ **This type does NOT implement Clone.**
553///
554/// Cloning error chains would:
555/// - Duplicate sensitive internal contexts across memory
556/// - Create multiple zeroization sites with unpredictable drop order
557/// - Violate threat model assumptions about data lifetime
558///
559/// If you need to share chain information:
560/// - Use borrowing (`&ContextChain`) for read-only access
561/// - Use `external_summary()` for public-safe string representation
562/// - Use `safe_clone_public()` to create a sanitized clone (see method docs)
563///
564/// This is a deliberate design decision to prevent accidental security
565/// violations via casual `.clone()` calls.
566///
567/// # Example
568///
569/// ```rust
570/// use palisade_errors::{ContextChain, DualContextError, OperationCategory};
571///
572/// let root = DualContextError::with_lie(
573///     "Operation failed",
574///     "Database connection refused",
575///     OperationCategory::IO,
576/// );
577///
578/// let mut chain = ContextChain::new(root);
579///
580/// let retry_failed = DualContextError::with_lie(
581///     "Retry failed",
582///     "Max retries (3) exceeded",
583///     OperationCategory::System,
584/// );
585///
586/// chain.push(retry_failed);
587///
588/// assert_eq!(chain.depth(), 2);
589/// ```
590pub struct ContextChain {
591    /// Stack of errors from root cause to final symptom.
592    /// Index 0 is the root cause, last index is the final error.
593    links: SmallVec<[DualContextError; 4]>,
594}
595
596impl ContextChain {
597    /// Create a new chain with a root error.
598    #[inline]
599    pub fn new(root: DualContextError) -> Self {
600        let mut links = SmallVec::new();
601        links.push(root);
602        Self { links }
603    }
604
605    /// Add a new error to the chain (as the new head).
606    ///
607    /// # Example
608    ///
609    /// ```rust
610    /// # use palisade_errors::{ContextChain, DualContextError, OperationCategory};
611    /// # let root = DualContextError::with_lie("a", "b", OperationCategory::System);
612    /// let mut chain = ContextChain::new(root);
613    ///
614    /// let next_error = DualContextError::with_lie(
615    ///     "Propagated error",
616    ///     "Original error: connection refused",
617    ///     OperationCategory::System,
618    /// );
619    ///
620    /// chain.push(next_error);
621    /// ```
622    #[inline]
623    pub fn push(&mut self, error: DualContextError) {
624        self.links.push(error);
625    }
626
627    /// Get the root cause error (first in chain).
628    #[inline]
629    pub fn root(&self) -> &DualContextError {
630        &self.links[0]
631    }
632
633    /// Get the final error (last in chain).
634    #[inline]
635    pub fn head(&self) -> &DualContextError {
636        self.links.last().expect("Chain is never empty")
637    }
638
639    /// Get the chain depth (number of errors).
640    #[inline]
641    pub fn depth(&self) -> usize {
642        self.links.len()
643    }
644
645    /// Iterate over the error chain from root to head.
646    #[inline]
647    pub fn iter(&self) -> impl Iterator<Item = &DualContextError> {
648        self.links.iter()
649    }
650
651    /// Get external representation of the entire chain (public contexts only).
652    ///
653    /// # Returns
654    ///
655    /// A string showing the public message progression from root to head.
656    ///
657    /// # Use Case
658    ///
659    /// This method exists as the safe alternative to cloning for most scenarios:
660    /// - Logging to external systems
661    /// - User-facing error messages
662    /// - Telemetry and alerting
663    ///
664    /// If you need the chain structure itself without internal contexts, consider
665    /// whether you actually need the structure or just the narrative flow. In most
666    /// cases, this string representation is sufficient.
667    ///
668    /// # Performance
669    ///
670    /// Pre-calculates capacity to avoid multiple allocations during formatting.
671    ///
672    /// # Example
673    ///
674    /// ```rust
675    /// # use palisade_errors::{ContextChain, DualContextError, OperationCategory};
676    /// # let root = DualContextError::with_lie("Database error", "x", OperationCategory::IO);
677    /// # let mut chain = ContextChain::new(root);
678    /// # let next = DualContextError::with_lie("Retry failed", "y", OperationCategory::System);
679    /// # chain.push(next);
680    /// let external = chain.external_summary();
681    /// // Output: "Database error → Retry failed"
682    /// ```
683    pub fn external_summary(&self) -> String {
684        if self.links.is_empty() {
685            return String::new();
686        }
687
688        // Pre-calculate capacity to avoid reallocations
689        // Formula: sum of message lengths + (n-1) separators
690        let separator = " → ";
691        let capacity = self
692            .links
693            .iter()
694            .map(|e| e.external_message().len())
695            .sum::<usize>()
696            + (self.links.len().saturating_sub(1) * separator.len());
697
698        let mut result = String::with_capacity(capacity);
699
700        for (i, error) in self.links.iter().enumerate() {
701            if i > 0 {
702                result.push_str(separator);
703            }
704            result.push_str(error.external_message());
705        }
706
707        result
708    }
709}
710
711impl Zeroize for ContextChain {
712    fn zeroize(&mut self) {
713        for entry in &mut self.links {
714            entry.zeroize();
715        }
716        self.links.clear();
717    }
718}
719
720impl Drop for ContextChain {
721    fn drop(&mut self) {
722        self.zeroize();
723    }
724}
725
726// ============================================================================
727// Tests
728// ============================================================================
729
730#[cfg(test)]
731mod tests {
732    use super::*;
733    use crate::SocAccess;
734
735    #[test]
736    fn context_metadata_basic_operations() {
737        let mut meta = ContextMetadata::new();
738
739        meta.add("key1", "value1");
740        meta.add("key2", "value2");
741
742        assert_eq!(meta.len(), 2);
743        assert_eq!(meta.get("key1"), Some("value1"));
744        assert_eq!(meta.get("key2"), Some("value2"));
745        assert_eq!(meta.get("key3"), None);
746    }
747
748    #[test]
749    fn context_metadata_zeroization() {
750        let mut meta = ContextMetadata::new();
751        meta.add("sensitive", "secret123".to_string());
752
753        meta.zeroize();
754
755        assert_eq!(meta.len(), 0);
756        assert!(meta.is_empty());
757    }
758
759    #[test]
760    fn context_builder_basic_usage() {
761        let err = ContextBuilder::new()
762            .public_lie("Access denied")
763            .internal_diagnostic("User lacks permission")
764            .category(OperationCategory::Detection)
765            .build();
766
767        assert_eq!(err.external_message(), "Access denied");
768    }
769
770    #[test]
771    fn context_builder_with_sensitive() {
772        let err = ContextBuilder::new()
773            .public_lie("Operation failed")
774            .internal_sensitive("/etc/passwd: permission denied")
775            .category(OperationCategory::IO)
776            .build();
777
778        assert_eq!(err.external_message(), "Operation failed");
779
780        let access = SocAccess::acquire();
781        let sensitive = err.internal().expose_sensitive(&access);
782        assert_eq!(sensitive, Some("/etc/passwd: permission denied"));
783    }
784
785    #[test]
786    #[should_panic(expected = "ContextBuilder requires both public and internal context")]
787    fn context_builder_panics_without_public() {
788        ContextBuilder::new()
789            .internal_diagnostic("Missing public")
790            .build();
791    }
792
793    #[test]
794    #[should_panic(expected = "ContextBuilder requires both public and internal context")]
795    fn context_builder_panics_without_internal() {
796        ContextBuilder::new().public_lie("Missing internal").build();
797    }
798
799    #[test]
800    fn context_builder_try_build_validation() {
801        let result = ContextBuilder::new().try_build();
802        assert!(matches!(
803            result,
804            Err(ContextBuilderError::MissingPublicContext { has_internal: false })
805        ));
806
807        let result = ContextBuilder::new().public_lie("test").try_build();
808        assert!(matches!(
809            result,
810            Err(ContextBuilderError::MissingInternalContext { has_public: true })
811        ));
812
813        let result = ContextBuilder::new()
814            .internal_diagnostic("test")
815            .try_build();
816        assert!(matches!(
817            result,
818            Err(ContextBuilderError::MissingPublicContext { has_internal: true })
819        ));
820    }
821
822    #[test]
823    #[cfg(debug_assertions)]
824    #[should_panic(expected = "public context already set")]
825    fn context_builder_panics_on_double_public() {
826        ContextBuilder::new()
827            .public_lie("First")
828            .public_lie("Second")
829            .internal_diagnostic("test")
830            .build();
831    }
832
833    #[test]
834    #[cfg(debug_assertions)]
835    #[should_panic(expected = "internal context already set")]
836    fn context_builder_panics_on_double_internal() {
837        ContextBuilder::new()
838            .public_lie("test")
839            .internal_diagnostic("First")
840            .internal_diagnostic("Second")
841            .build();
842    }
843
844    #[test]
845    fn context_builder_error_messages_include_state() {
846        let err = ContextBuilder::new().try_build().unwrap_err();
847        let msg = err.to_string();
848        assert!(msg.contains("public: missing"));
849        assert!(msg.contains("internal: missing"));
850
851        let err = ContextBuilder::new()
852            .public_lie("test")
853            .try_build()
854            .unwrap_err();
855        let msg = err.to_string();
856        assert!(msg.contains("internal: missing"));
857        assert!(msg.contains("public: set"));
858    }
859
860    #[test]
861    fn context_chain_basic_usage() {
862        let root = DualContextError::with_lie(
863            "Database error",
864            "Connection refused",
865            OperationCategory::IO,
866        );
867
868        let mut chain = ContextChain::new(root);
869
870        assert_eq!(chain.depth(), 1);
871        assert_eq!(chain.root().external_message(), "Database error");
872        assert_eq!(chain.head().external_message(), "Database error");
873
874        let retry_error = DualContextError::with_lie(
875            "Retry failed",
876            "Max retries exceeded",
877            OperationCategory::System,
878        );
879
880        chain.push(retry_error);
881
882        assert_eq!(chain.depth(), 2);
883        assert_eq!(chain.root().external_message(), "Database error");
884        assert_eq!(chain.head().external_message(), "Retry failed");
885    }
886
887    #[test]
888    fn context_chain_external_summary() {
889        let root = DualContextError::with_lie(
890            "Root cause",
891            "Internal details",
892            OperationCategory::System,
893        );
894
895        let mut chain = ContextChain::new(root);
896
897        chain.push(DualContextError::with_lie(
898            "Intermediate",
899            "Details",
900            OperationCategory::System,
901        ));
902        chain.push(DualContextError::with_lie(
903            "Final error",
904            "Details",
905            OperationCategory::System,
906        ));
907
908        let summary = chain.external_summary();
909        assert_eq!(summary, "Root cause → Intermediate → Final error");
910    }
911
912    #[test]
913    fn context_chain_external_summary_single_error() {
914        let root = DualContextError::with_lie("Single", "Details", OperationCategory::System);
915        let chain = ContextChain::new(root);
916
917        let summary = chain.external_summary();
918        assert_eq!(summary, "Single");
919    }
920
921    #[test]
922    fn context_chain_external_summary_long_messages() {
923        let root = DualContextError::with_lie(
924            "A".repeat(100),
925            "Details",
926            OperationCategory::System,
927        );
928        let mut chain = ContextChain::new(root);
929
930        chain.push(DualContextError::with_lie(
931            "B".repeat(100),
932            "Details",
933            OperationCategory::System,
934        ));
935
936        let summary = chain.external_summary();
937        assert!(summary.len() >= 200); // Both messages plus separator
938        assert!(summary.contains('→'));
939    }
940
941    #[test]
942    fn context_chain_iteration() {
943        let root = DualContextError::with_lie("E1", "D1", OperationCategory::System);
944        let mut chain = ContextChain::new(root);
945
946        chain.push(DualContextError::with_lie("E2", "D2", OperationCategory::System));
947        chain.push(DualContextError::with_lie("E3", "D3", OperationCategory::System));
948
949        let messages: Vec<_> = chain.iter().map(|e| e.external_message()).collect();
950        assert_eq!(messages, vec!["E1", "E2", "E3"]);
951    }
952
953    #[test]
954    fn metadata_with_owned_and_borrowed() {
955        let mut meta = ContextMetadata::new();
956
957        meta.add("static", "literal"); // Borrowed
958        meta.add("dynamic", format!("value-{}", 42)); // Owned
959
960        assert_eq!(meta.get("static"), Some("literal"));
961        assert_eq!(meta.get("dynamic"), Some("value-42"));
962    }
963
964    #[test]
965    fn metadata_iteration() {
966        let mut meta = ContextMetadata::new();
967        meta.add("key1", "val1");
968        meta.add("key2", "val2");
969
970        let collected: Vec<_> = meta.iter().collect();
971        assert_eq!(collected.len(), 2);
972        assert!(collected.contains(&("key1", "val1")));
973        assert!(collected.contains(&("key2", "val2")));
974    }
975}