Skip to main content

jugar_probar/brick/
pipeline.rs

1//! BrickPipeline: Orchestration for multi-brick workflows (PROBAR-SPEC-009-P9)
2//!
3//! Provides pipeline orchestration with:
4//! - Sequential dependencies (brick A → brick B)
5//! - Parallel execution (bricks A and B concurrently)
6//! - Failure handling and checkpointing
7//! - Privacy-aware routing
8//!
9//! # Design Philosophy
10//!
11//! BrickPipeline applies the batuta orchestration patterns to brick execution.
12//! Every brick can be a pipeline stage with validation and audit trail.
13//!
14//! # Example
15//!
16//! ```rust,ignore
17//! use probar::brick::pipeline::{BrickPipeline, BrickStage, PrivacyTier};
18//!
19//! let whisper_pipeline = BrickPipeline::new()
20//!     .stage(AudioCaptureBrick::new())
21//!     .stage(MelSpectrogramBrick::new())
22//!     .stage(EncoderBrick::new())
23//!     .stage(DecoderBrick::new())
24//!     .with_privacy(PrivacyTier::Sovereign)
25//!     .with_checkpointing(Duration::from_secs(5));
26//!
27//! let output = whisper_pipeline.run(input).await?;
28//! ```
29
30// Allow missing docs for enum variant fields - context is clear from variant name
31#![allow(missing_docs)]
32
33use super::{Brick, BrickError};
34use std::collections::HashMap;
35use std::fmt::Debug;
36use std::time::{Duration, Instant};
37
38/// Result type for pipeline operations
39pub type PipelineResult<T> = Result<T, PipelineError>;
40
41/// Pipeline execution context
42#[derive(Debug, Clone)]
43pub struct PipelineContext {
44    /// Named data values in the pipeline
45    pub data: HashMap<String, PipelineData>,
46    /// Metadata for audit trail
47    pub metadata: PipelineMetadata,
48    /// Execution trace for debugging
49    pub trace: Vec<StageTrace>,
50}
51
52impl PipelineContext {
53    /// Create a new empty context
54    #[must_use]
55    pub fn new() -> Self {
56        Self {
57            data: HashMap::new(),
58            metadata: PipelineMetadata::new(),
59            trace: Vec::new(),
60        }
61    }
62
63    /// Create context from initial input
64    #[must_use]
65    pub fn from_input(name: &str, data: PipelineData) -> Self {
66        let mut ctx = Self::new();
67        ctx.data.insert(name.to_string(), data);
68        ctx
69    }
70
71    /// Get data by name
72    pub fn get(&self, name: &str) -> Option<&PipelineData> {
73        self.data.get(name)
74    }
75
76    /// Set data by name
77    pub fn set(&mut self, name: impl Into<String>, data: PipelineData) {
78        self.data.insert(name.into(), data);
79    }
80
81    /// Add a stage trace entry
82    pub fn add_trace(&mut self, trace: StageTrace) {
83        self.trace.push(trace);
84    }
85}
86
87impl Default for PipelineContext {
88    fn default() -> Self {
89        Self::new()
90    }
91}
92
93/// Data types that can flow through pipelines
94#[derive(Debug, Clone)]
95pub enum PipelineData {
96    /// Raw bytes
97    Bytes(Vec<u8>),
98    /// Float tensor
99    FloatTensor { data: Vec<f32>, shape: Vec<usize> },
100    /// String value
101    Text(String),
102    /// JSON value
103    Json(serde_json::Value),
104    /// Integer value
105    Int(i64),
106    /// Boolean value
107    Bool(bool),
108}
109
110impl PipelineData {
111    /// Create a float tensor
112    #[must_use]
113    pub fn tensor(data: Vec<f32>, shape: Vec<usize>) -> Self {
114        Self::FloatTensor { data, shape }
115    }
116
117    /// Get as float tensor
118    pub fn as_tensor(&self) -> Option<(&[f32], &[usize])> {
119        match self {
120            Self::FloatTensor { data, shape } => Some((data, shape)),
121            _ => None,
122        }
123    }
124
125    /// Get as text
126    pub fn as_text(&self) -> Option<&str> {
127        match self {
128            Self::Text(s) => Some(s),
129            _ => None,
130        }
131    }
132}
133
134/// Metadata for pipeline execution
135#[derive(Debug, Clone)]
136pub struct PipelineMetadata {
137    /// Pipeline run ID
138    pub run_id: String,
139    /// Start time
140    pub started_at: Option<Instant>,
141    /// Custom tags
142    pub tags: HashMap<String, String>,
143}
144
145impl PipelineMetadata {
146    /// Create new metadata with generated run ID
147    #[must_use]
148    pub fn new() -> Self {
149        Self {
150            run_id: format!("run-{}", uuid_v4()),
151            started_at: None,
152            tags: HashMap::new(),
153        }
154    }
155
156    /// Add a tag
157    pub fn tag(&mut self, key: impl Into<String>, value: impl Into<String>) {
158        self.tags.insert(key.into(), value.into());
159    }
160}
161
162impl Default for PipelineMetadata {
163    fn default() -> Self {
164        Self::new()
165    }
166}
167
168/// Execution trace for a single stage
169#[derive(Debug, Clone)]
170pub struct StageTrace {
171    /// Stage name
172    pub stage_name: String,
173    /// Execution duration
174    pub duration: Duration,
175    /// Whether stage succeeded
176    pub success: bool,
177    /// Error message if failed
178    pub error: Option<String>,
179}
180
181/// Privacy tier for pipeline execution
182#[derive(Debug, Clone, Copy, PartialEq, Eq)]
183pub enum PrivacyTier {
184    /// Local-only execution (no network calls)
185    Sovereign,
186    /// VPC-only (private cloud, no public APIs)
187    Private,
188    /// Cloud-enabled (spillover to external APIs)
189    Standard,
190}
191
192impl Default for PrivacyTier {
193    fn default() -> Self {
194        Self::Standard
195    }
196}
197
198/// Validation result from a stage
199#[derive(Debug, Clone)]
200pub struct ValidationResult {
201    /// Whether validation passed
202    pub valid: bool,
203    /// Validation messages (warnings and errors)
204    pub messages: Vec<ValidationMessage>,
205}
206
207impl ValidationResult {
208    /// Create a passing validation result
209    #[must_use]
210    pub fn ok() -> Self {
211        Self {
212            valid: true,
213            messages: Vec::new(),
214        }
215    }
216
217    /// Create a failing validation result
218    #[must_use]
219    pub fn fail(reason: impl Into<String>) -> Self {
220        Self {
221            valid: false,
222            messages: vec![ValidationMessage {
223                level: ValidationLevel::Error,
224                message: reason.into(),
225            }],
226        }
227    }
228
229    /// Add a warning
230    pub fn warn(&mut self, message: impl Into<String>) {
231        self.messages.push(ValidationMessage {
232            level: ValidationLevel::Warning,
233            message: message.into(),
234        });
235    }
236}
237
238/// Validation message level
239#[derive(Debug, Clone, Copy, PartialEq, Eq)]
240pub enum ValidationLevel {
241    /// Informational message
242    Info,
243    /// Warning (execution continues)
244    Warning,
245    /// Error (execution blocked)
246    Error,
247}
248
249/// A validation message
250#[derive(Debug, Clone)]
251pub struct ValidationMessage {
252    /// Message severity
253    pub level: ValidationLevel,
254    /// Message content
255    pub message: String,
256}
257
258/// Pipeline error type
259#[derive(Debug, Clone)]
260pub enum PipelineError {
261    /// Stage validation failed
262    ValidationFailed { stage: String, reason: String },
263    /// Stage execution failed
264    ExecutionFailed { stage: String, reason: String },
265    /// Missing required input
266    MissingInput { stage: String, input: String },
267    /// Privacy tier violation
268    PrivacyViolation { tier: PrivacyTier, reason: String },
269    /// Checkpoint failed
270    CheckpointFailed { reason: String },
271    /// Brick error
272    BrickError(String),
273}
274
275impl std::fmt::Display for PipelineError {
276    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
277        match self {
278            Self::ValidationFailed { stage, reason } => {
279                write!(f, "Validation failed at stage '{}': {}", stage, reason)
280            }
281            Self::ExecutionFailed { stage, reason } => {
282                write!(f, "Execution failed at stage '{}': {}", stage, reason)
283            }
284            Self::MissingInput { stage, input } => {
285                write!(f, "Missing input '{}' for stage '{}'", input, stage)
286            }
287            Self::PrivacyViolation { tier, reason } => {
288                write!(f, "Privacy tier {:?} violated: {}", tier, reason)
289            }
290            Self::CheckpointFailed { reason } => {
291                write!(f, "Checkpoint failed: {}", reason)
292            }
293            Self::BrickError(msg) => write!(f, "Brick error: {}", msg),
294        }
295    }
296}
297
298impl std::error::Error for PipelineError {}
299
300impl From<BrickError> for PipelineError {
301    fn from(e: BrickError) -> Self {
302        Self::BrickError(e.to_string())
303    }
304}
305
306/// Trait for bricks that can be pipeline stages
307pub trait BrickStage: Brick + Send + Sync {
308    /// Execute the stage
309    fn execute(&self, ctx: PipelineContext) -> PipelineResult<PipelineContext>;
310
311    /// Validate before execution (Jidoka pattern)
312    fn validate(&self, ctx: &PipelineContext) -> ValidationResult;
313
314    /// Get required input names
315    fn required_inputs(&self) -> &[&str] {
316        &[]
317    }
318
319    /// Get output names
320    fn output_names(&self) -> &[&str] {
321        &[]
322    }
323}
324
325/// Audit entry for pipeline execution
326#[derive(Debug, Clone)]
327pub struct AuditEntry {
328    /// Stage name
329    pub stage: String,
330    /// Timestamp
331    pub timestamp: Instant,
332    /// Duration
333    pub duration: Duration,
334    /// Success status
335    pub success: bool,
336    /// Input data keys
337    pub inputs: Vec<String>,
338    /// Output data keys
339    pub outputs: Vec<String>,
340}
341
342/// Audit trail collector
343#[derive(Debug, Default)]
344pub struct PipelineAuditCollector {
345    entries: Vec<AuditEntry>,
346}
347
348impl PipelineAuditCollector {
349    /// Create a new collector
350    #[must_use]
351    pub fn new() -> Self {
352        Self {
353            entries: Vec::new(),
354        }
355    }
356
357    /// Record a stage execution
358    pub fn record(&mut self, stage: &str, duration: Duration, success: bool) {
359        self.entries.push(AuditEntry {
360            stage: stage.to_string(),
361            timestamp: Instant::now(),
362            duration,
363            success,
364            inputs: Vec::new(),
365            outputs: Vec::new(),
366        });
367    }
368
369    /// Get all entries
370    pub fn entries(&self) -> &[AuditEntry] {
371        &self.entries
372    }
373
374    /// Get total execution time
375    pub fn total_duration(&self) -> Duration {
376        self.entries.iter().map(|e| e.duration).sum()
377    }
378}
379
380/// Checkpoint state for fault tolerance
381#[derive(Debug, Clone)]
382pub struct Checkpoint {
383    /// Stage index
384    pub stage_index: usize,
385    /// Context at checkpoint
386    pub context: PipelineContext,
387    /// Timestamp
388    pub created_at: Instant,
389}
390
391/// BrickPipeline: Orchestrates multi-brick workflows
392pub struct BrickPipeline {
393    /// Pipeline name
394    name: String,
395    /// Ordered stages
396    stages: Vec<Box<dyn BrickStage>>,
397    /// Privacy tier
398    privacy_tier: PrivacyTier,
399    /// Checkpoint interval
400    checkpoint_interval: Option<Duration>,
401    /// Audit collector
402    audit_collector: PipelineAuditCollector,
403    /// Last checkpoint
404    last_checkpoint: Option<Checkpoint>,
405}
406
407impl BrickPipeline {
408    /// Create a new pipeline
409    #[must_use]
410    pub fn new(name: impl Into<String>) -> Self {
411        Self {
412            name: name.into(),
413            stages: Vec::new(),
414            privacy_tier: PrivacyTier::Standard,
415            checkpoint_interval: None,
416            audit_collector: PipelineAuditCollector::new(),
417            last_checkpoint: None,
418        }
419    }
420
421    /// Add a stage to the pipeline
422    #[must_use]
423    pub fn stage<S: BrickStage + 'static>(mut self, stage: S) -> Self {
424        self.stages.push(Box::new(stage));
425        self
426    }
427
428    /// Set privacy tier
429    #[must_use]
430    pub fn with_privacy(mut self, tier: PrivacyTier) -> Self {
431        self.privacy_tier = tier;
432        self
433    }
434
435    /// Enable checkpointing
436    #[must_use]
437    pub fn with_checkpointing(mut self, interval: Duration) -> Self {
438        self.checkpoint_interval = Some(interval);
439        self
440    }
441
442    /// Run the pipeline
443    pub fn run(&mut self, input: PipelineContext) -> PipelineResult<PipelineContext> {
444        let mut ctx = input;
445        ctx.metadata.started_at = Some(Instant::now());
446
447        let start_index = self
448            .last_checkpoint
449            .as_ref()
450            .map(|c| c.stage_index)
451            .unwrap_or(0);
452
453        // Restore from checkpoint if available
454        if let Some(checkpoint) = &self.last_checkpoint {
455            ctx = checkpoint.context.clone();
456        }
457
458        let mut last_checkpoint_time = Instant::now();
459
460        for (i, stage) in self.stages.iter().enumerate().skip(start_index) {
461            let stage_name = stage.brick_name();
462
463            // Jidoka: validate before execution
464            let validation = stage.validate(&ctx);
465            if !validation.valid {
466                let reason = validation
467                    .messages
468                    .iter()
469                    .filter(|m| m.level == ValidationLevel::Error)
470                    .map(|m| m.message.as_str())
471                    .collect::<Vec<_>>()
472                    .join("; ");
473
474                return Err(PipelineError::ValidationFailed {
475                    stage: stage_name.to_string(),
476                    reason,
477                });
478            }
479
480            // Execute stage (clone ctx for error recovery)
481            let start = Instant::now();
482            let ctx_for_error = ctx.clone();
483            let result = stage.execute(ctx);
484            let duration = start.elapsed();
485
486            match result {
487                Ok(mut new_ctx) => {
488                    new_ctx.add_trace(StageTrace {
489                        stage_name: stage_name.to_string(),
490                        duration,
491                        success: true,
492                        error: None,
493                    });
494
495                    self.audit_collector.record(stage_name, duration, true);
496
497                    // Checkpoint if interval exceeded
498                    if let Some(interval) = self.checkpoint_interval {
499                        if last_checkpoint_time.elapsed() >= interval {
500                            self.last_checkpoint = Some(Checkpoint {
501                                stage_index: i + 1,
502                                context: new_ctx.clone(),
503                                created_at: Instant::now(),
504                            });
505                            last_checkpoint_time = Instant::now();
506                        }
507                    }
508
509                    ctx = new_ctx;
510                }
511                Err(e) => {
512                    self.audit_collector.record(stage_name, duration, false);
513
514                    // Use saved context for trace in error case
515                    let mut error_ctx = ctx_for_error;
516                    error_ctx.add_trace(StageTrace {
517                        stage_name: stage_name.to_string(),
518                        duration,
519                        success: false,
520                        error: Some(e.to_string()),
521                    });
522
523                    return Err(PipelineError::ExecutionFailed {
524                        stage: stage_name.to_string(),
525                        reason: e.to_string(),
526                    });
527                }
528            }
529        }
530
531        // Clear checkpoint on successful completion
532        self.last_checkpoint = None;
533
534        Ok(ctx)
535    }
536
537    /// Get the pipeline name
538    #[must_use]
539    pub fn name(&self) -> &str {
540        &self.name
541    }
542
543    /// Get the number of stages
544    #[must_use]
545    pub fn stage_count(&self) -> usize {
546        self.stages.len()
547    }
548
549    /// Get audit trail
550    pub fn audit_trail(&self) -> &[AuditEntry] {
551        self.audit_collector.entries()
552    }
553
554    /// Get privacy tier
555    #[must_use]
556    pub fn privacy_tier(&self) -> PrivacyTier {
557        self.privacy_tier
558    }
559}
560
561impl Debug for BrickPipeline {
562    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
563        f.debug_struct("BrickPipeline")
564            .field("name", &self.name)
565            .field("stage_count", &self.stages.len())
566            .field("privacy_tier", &self.privacy_tier)
567            .finish()
568    }
569}
570
571/// Generate a simple UUID v4 (non-cryptographic)
572fn uuid_v4() -> String {
573    use std::time::{SystemTime, UNIX_EPOCH};
574    let now = SystemTime::now()
575        .duration_since(UNIX_EPOCH)
576        .unwrap_or_default();
577    format!("{:x}{:x}", now.as_nanos(), std::process::id())
578}
579
580#[cfg(test)]
581#[allow(clippy::unwrap_used, clippy::expect_used)]
582mod tests {
583    use super::*;
584    use crate::brick::{BrickAssertion, BrickBudget, BrickVerification};
585
586    // ============================================================
587    // Test Stage Implementation
588    // ============================================================
589
590    struct TestStage {
591        name: &'static str,
592        should_fail: bool,
593    }
594
595    impl Brick for TestStage {
596        fn brick_name(&self) -> &'static str {
597            self.name
598        }
599
600        fn assertions(&self) -> &[BrickAssertion] {
601            &[]
602        }
603
604        fn budget(&self) -> BrickBudget {
605            BrickBudget::uniform(100)
606        }
607
608        fn verify(&self) -> BrickVerification {
609            BrickVerification {
610                passed: vec![],
611                failed: vec![],
612                verification_time: Duration::from_micros(10),
613            }
614        }
615
616        fn to_html(&self) -> String {
617            String::new()
618        }
619
620        fn to_css(&self) -> String {
621            String::new()
622        }
623    }
624
625    impl BrickStage for TestStage {
626        fn execute(&self, mut ctx: PipelineContext) -> PipelineResult<PipelineContext> {
627            if self.should_fail {
628                return Err(PipelineError::ExecutionFailed {
629                    stage: self.name.to_string(),
630                    reason: "Test failure".into(),
631                });
632            }
633            ctx.set(
634                format!("{}_output", self.name),
635                PipelineData::Text("done".into()),
636            );
637            Ok(ctx)
638        }
639
640        fn validate(&self, _ctx: &PipelineContext) -> ValidationResult {
641            ValidationResult::ok()
642        }
643    }
644
645    /// A stage that fails validation
646    struct FailingValidationStage {
647        name: &'static str,
648    }
649
650    impl Brick for FailingValidationStage {
651        fn brick_name(&self) -> &'static str {
652            self.name
653        }
654
655        fn assertions(&self) -> &[BrickAssertion] {
656            &[]
657        }
658
659        fn budget(&self) -> BrickBudget {
660            BrickBudget::uniform(100)
661        }
662
663        fn verify(&self) -> BrickVerification {
664            BrickVerification {
665                passed: vec![],
666                failed: vec![],
667                verification_time: Duration::from_micros(10),
668            }
669        }
670
671        fn to_html(&self) -> String {
672            String::new()
673        }
674
675        fn to_css(&self) -> String {
676            String::new()
677        }
678    }
679
680    impl BrickStage for FailingValidationStage {
681        fn execute(&self, ctx: PipelineContext) -> PipelineResult<PipelineContext> {
682            Ok(ctx)
683        }
684
685        fn validate(&self, _ctx: &PipelineContext) -> ValidationResult {
686            ValidationResult::fail("Validation error")
687        }
688    }
689
690    // ============================================================
691    // PipelineContext tests
692    // ============================================================
693
694    #[test]
695    fn test_pipeline_context_new() {
696        let ctx = PipelineContext::new();
697        assert!(ctx.data.is_empty());
698        assert!(ctx.trace.is_empty());
699    }
700
701    #[test]
702    fn test_pipeline_context_default() {
703        let ctx = PipelineContext::default();
704        assert!(ctx.data.is_empty());
705    }
706
707    #[test]
708    fn test_pipeline_context_from_input() {
709        let ctx = PipelineContext::from_input("input", PipelineData::Text("hello".into()));
710        assert!(ctx.get("input").is_some());
711    }
712
713    #[test]
714    fn test_pipeline_context() {
715        let mut ctx = PipelineContext::new();
716        ctx.set("test", PipelineData::Text("hello".into()));
717
718        assert!(ctx.get("test").is_some());
719        assert!(ctx.get("missing").is_none());
720    }
721
722    #[test]
723    fn test_pipeline_context_add_trace() {
724        let mut ctx = PipelineContext::new();
725        ctx.add_trace(StageTrace {
726            stage_name: "test".to_string(),
727            duration: Duration::from_millis(10),
728            success: true,
729            error: None,
730        });
731
732        assert_eq!(ctx.trace.len(), 1);
733        assert_eq!(ctx.trace[0].stage_name, "test");
734    }
735
736    #[test]
737    fn test_pipeline_context_clone() {
738        let mut ctx = PipelineContext::new();
739        ctx.set("key", PipelineData::Int(42));
740
741        let cloned = ctx.clone();
742        assert!(cloned.get("key").is_some());
743    }
744
745    // ============================================================
746    // PipelineData tests
747    // ============================================================
748
749    #[test]
750    fn test_pipeline_data_tensor() {
751        let data = PipelineData::tensor(vec![1.0, 2.0, 3.0], vec![3]);
752
753        let (values, shape) = data.as_tensor().unwrap();
754        assert_eq!(values, &[1.0, 2.0, 3.0]);
755        assert_eq!(shape, &[3]);
756    }
757
758    #[test]
759    fn test_pipeline_data_as_tensor_none() {
760        let data = PipelineData::Text("hello".into());
761        assert!(data.as_tensor().is_none());
762    }
763
764    #[test]
765    fn test_pipeline_data_as_text() {
766        let data = PipelineData::Text("hello".into());
767        assert_eq!(data.as_text(), Some("hello"));
768    }
769
770    #[test]
771    fn test_pipeline_data_as_text_none() {
772        let data = PipelineData::Int(42);
773        assert!(data.as_text().is_none());
774    }
775
776    #[test]
777    fn test_pipeline_data_bytes() {
778        let data = PipelineData::Bytes(vec![1, 2, 3, 4]);
779        if let PipelineData::Bytes(bytes) = data {
780            assert_eq!(bytes, vec![1, 2, 3, 4]);
781        } else {
782            panic!("Expected Bytes variant");
783        }
784    }
785
786    #[test]
787    fn test_pipeline_data_json() {
788        let json = serde_json::json!({"key": "value"});
789        let data = PipelineData::Json(json.clone());
790        if let PipelineData::Json(value) = data {
791            assert_eq!(value, json);
792        } else {
793            panic!("Expected Json variant");
794        }
795    }
796
797    #[test]
798    fn test_pipeline_data_int() {
799        let data = PipelineData::Int(-42);
800        if let PipelineData::Int(val) = data {
801            assert_eq!(val, -42);
802        } else {
803            panic!("Expected Int variant");
804        }
805    }
806
807    #[test]
808    fn test_pipeline_data_bool() {
809        let data = PipelineData::Bool(true);
810        if let PipelineData::Bool(val) = data {
811            assert!(val);
812        } else {
813            panic!("Expected Bool variant");
814        }
815    }
816
817    #[test]
818    fn test_pipeline_data_clone_and_debug() {
819        let data = PipelineData::Text("test".into());
820        let cloned = data;
821        assert!(format!("{:?}", cloned).contains("Text"));
822    }
823
824    // ============================================================
825    // PipelineMetadata tests
826    // ============================================================
827
828    #[test]
829    fn test_pipeline_metadata_new() {
830        let meta = PipelineMetadata::new();
831        assert!(meta.run_id.starts_with("run-"));
832        assert!(meta.started_at.is_none());
833        assert!(meta.tags.is_empty());
834    }
835
836    #[test]
837    fn test_pipeline_metadata_default() {
838        let meta = PipelineMetadata::default();
839        assert!(meta.run_id.starts_with("run-"));
840    }
841
842    #[test]
843    fn test_pipeline_metadata_tag() {
844        let mut meta = PipelineMetadata::new();
845        meta.tag("env", "test");
846        meta.tag("version", "1.0");
847
848        assert_eq!(meta.tags.get("env"), Some(&"test".to_string()));
849        assert_eq!(meta.tags.get("version"), Some(&"1.0".to_string()));
850    }
851
852    #[test]
853    fn test_pipeline_metadata_clone_and_debug() {
854        let meta = PipelineMetadata::new();
855        let cloned = meta;
856        assert!(format!("{:?}", cloned).contains("PipelineMetadata"));
857    }
858
859    // ============================================================
860    // StageTrace tests
861    // ============================================================
862
863    #[test]
864    fn test_stage_trace_clone_and_debug() {
865        let trace = StageTrace {
866            stage_name: "test".to_string(),
867            duration: Duration::from_millis(100),
868            success: true,
869            error: None,
870        };
871
872        let cloned = trace;
873        assert_eq!(cloned.stage_name, "test");
874        assert!(cloned.success);
875        assert!(format!("{:?}", cloned).contains("StageTrace"));
876    }
877
878    #[test]
879    fn test_stage_trace_with_error() {
880        let trace = StageTrace {
881            stage_name: "failed".to_string(),
882            duration: Duration::from_millis(50),
883            success: false,
884            error: Some("Something went wrong".to_string()),
885        };
886
887        assert!(!trace.success);
888        assert_eq!(trace.error, Some("Something went wrong".to_string()));
889    }
890
891    // ============================================================
892    // PrivacyTier tests
893    // ============================================================
894
895    #[test]
896    fn test_privacy_tier_default() {
897        let tier = PrivacyTier::default();
898        assert_eq!(tier, PrivacyTier::Standard);
899    }
900
901    #[test]
902    fn test_privacy_tier_equality() {
903        assert_eq!(PrivacyTier::Sovereign, PrivacyTier::Sovereign);
904        assert_ne!(PrivacyTier::Sovereign, PrivacyTier::Private);
905        assert_ne!(PrivacyTier::Private, PrivacyTier::Standard);
906    }
907
908    #[test]
909    fn test_privacy_tier_debug_and_clone() {
910        let tier = PrivacyTier::Private;
911        let cloned = tier;
912        assert!(format!("{:?}", cloned).contains("Private"));
913    }
914
915    // ============================================================
916    // ValidationResult tests
917    // ============================================================
918
919    #[test]
920    fn test_validation_result_ok() {
921        let ok = ValidationResult::ok();
922        assert!(ok.valid);
923        assert!(ok.messages.is_empty());
924    }
925
926    #[test]
927    fn test_validation_result_fail() {
928        let fail = ValidationResult::fail("test error");
929        assert!(!fail.valid);
930        assert_eq!(fail.messages.len(), 1);
931        assert_eq!(fail.messages[0].level, ValidationLevel::Error);
932        assert_eq!(fail.messages[0].message, "test error");
933    }
934
935    #[test]
936    fn test_validation_result_warn() {
937        let mut result = ValidationResult::ok();
938        result.warn("warning message");
939
940        assert!(result.valid);
941        assert_eq!(result.messages.len(), 1);
942        assert_eq!(result.messages[0].level, ValidationLevel::Warning);
943    }
944
945    #[test]
946    fn test_validation_result_clone_and_debug() {
947        let result = ValidationResult::fail("error");
948        let cloned = result;
949        assert!(format!("{:?}", cloned).contains("ValidationResult"));
950    }
951
952    // ============================================================
953    // ValidationLevel tests
954    // ============================================================
955
956    #[test]
957    fn test_validation_level_equality() {
958        assert_eq!(ValidationLevel::Info, ValidationLevel::Info);
959        assert_eq!(ValidationLevel::Warning, ValidationLevel::Warning);
960        assert_eq!(ValidationLevel::Error, ValidationLevel::Error);
961        assert_ne!(ValidationLevel::Info, ValidationLevel::Error);
962    }
963
964    #[test]
965    fn test_validation_level_debug_and_clone() {
966        let level = ValidationLevel::Warning;
967        let cloned = level;
968        assert!(format!("{:?}", cloned).contains("Warning"));
969    }
970
971    // ============================================================
972    // ValidationMessage tests
973    // ============================================================
974
975    #[test]
976    fn test_validation_message_clone_and_debug() {
977        let msg = ValidationMessage {
978            level: ValidationLevel::Error,
979            message: "test".to_string(),
980        };
981
982        let cloned = msg;
983        assert_eq!(cloned.message, "test");
984        assert!(format!("{:?}", cloned).contains("ValidationMessage"));
985    }
986
987    // ============================================================
988    // PipelineError tests
989    // ============================================================
990
991    #[test]
992    fn test_pipeline_error_validation_failed() {
993        let err = PipelineError::ValidationFailed {
994            stage: "test".to_string(),
995            reason: "bad input".to_string(),
996        };
997
998        let display = format!("{}", err);
999        assert!(display.contains("Validation failed"));
1000        assert!(display.contains("test"));
1001        assert!(display.contains("bad input"));
1002    }
1003
1004    #[test]
1005    fn test_pipeline_error_execution_failed() {
1006        let err = PipelineError::ExecutionFailed {
1007            stage: "compute".to_string(),
1008            reason: "timeout".to_string(),
1009        };
1010
1011        let display = format!("{}", err);
1012        assert!(display.contains("Execution failed"));
1013        assert!(display.contains("compute"));
1014    }
1015
1016    #[test]
1017    fn test_pipeline_error_missing_input() {
1018        let err = PipelineError::MissingInput {
1019            stage: "transform".to_string(),
1020            input: "data".to_string(),
1021        };
1022
1023        let display = format!("{}", err);
1024        assert!(display.contains("Missing input"));
1025        assert!(display.contains("data"));
1026        assert!(display.contains("transform"));
1027    }
1028
1029    #[test]
1030    fn test_pipeline_error_privacy_violation() {
1031        let err = PipelineError::PrivacyViolation {
1032            tier: PrivacyTier::Sovereign,
1033            reason: "external API call".to_string(),
1034        };
1035
1036        let display = format!("{}", err);
1037        assert!(display.contains("Privacy tier"));
1038        assert!(display.contains("Sovereign"));
1039    }
1040
1041    #[test]
1042    fn test_pipeline_error_checkpoint_failed() {
1043        let err = PipelineError::CheckpointFailed {
1044            reason: "disk full".to_string(),
1045        };
1046
1047        let display = format!("{}", err);
1048        assert!(display.contains("Checkpoint failed"));
1049        assert!(display.contains("disk full"));
1050    }
1051
1052    #[test]
1053    fn test_pipeline_error_brick_error() {
1054        let err = PipelineError::BrickError("brick error".to_string());
1055
1056        let display = format!("{}", err);
1057        assert!(display.contains("Brick error"));
1058    }
1059
1060    #[test]
1061    fn test_pipeline_error_from_brick_error() {
1062        use crate::brick::{BrickAssertion, BrickError};
1063        let brick_err = BrickError::AssertionFailed {
1064            assertion: BrickAssertion::ElementPresent("test".to_string()),
1065            reason: "failed".to_string(),
1066        };
1067
1068        let pipeline_err: PipelineError = brick_err.into();
1069        if let PipelineError::BrickError(msg) = pipeline_err {
1070            assert!(msg.contains("test"));
1071        } else {
1072            panic!("Expected BrickError variant");
1073        }
1074    }
1075
1076    #[test]
1077    fn test_pipeline_error_is_error_trait() {
1078        let err: Box<dyn std::error::Error> = Box::new(PipelineError::CheckpointFailed {
1079            reason: "test".to_string(),
1080        });
1081
1082        assert!(err.to_string().contains("Checkpoint"));
1083    }
1084
1085    // ============================================================
1086    // PipelineAuditCollector tests
1087    // ============================================================
1088
1089    #[test]
1090    fn test_audit_collector_new() {
1091        let collector = PipelineAuditCollector::new();
1092        assert!(collector.entries().is_empty());
1093    }
1094
1095    #[test]
1096    fn test_audit_collector_default() {
1097        let collector = PipelineAuditCollector::default();
1098        assert!(collector.entries().is_empty());
1099    }
1100
1101    #[test]
1102    fn test_audit_collector() {
1103        let mut collector = PipelineAuditCollector::new();
1104        collector.record("stage1", Duration::from_millis(100), true);
1105        collector.record("stage2", Duration::from_millis(50), true);
1106
1107        assert_eq!(collector.entries().len(), 2);
1108        assert_eq!(collector.total_duration(), Duration::from_millis(150));
1109    }
1110
1111    #[test]
1112    fn test_audit_collector_record_failure() {
1113        let mut collector = PipelineAuditCollector::new();
1114        collector.record("failed", Duration::from_millis(25), false);
1115
1116        assert_eq!(collector.entries().len(), 1);
1117        assert!(!collector.entries()[0].success);
1118    }
1119
1120    #[test]
1121    fn test_audit_collector_debug() {
1122        let collector = PipelineAuditCollector::new();
1123        assert!(format!("{:?}", collector).contains("PipelineAuditCollector"));
1124    }
1125
1126    // ============================================================
1127    // AuditEntry tests
1128    // ============================================================
1129
1130    #[test]
1131    fn test_audit_entry_clone_and_debug() {
1132        let entry = AuditEntry {
1133            stage: "test".to_string(),
1134            timestamp: Instant::now(),
1135            duration: Duration::from_millis(100),
1136            success: true,
1137            inputs: vec!["input1".to_string()],
1138            outputs: vec!["output1".to_string()],
1139        };
1140
1141        let cloned = entry;
1142        assert_eq!(cloned.stage, "test");
1143        assert!(format!("{:?}", cloned).contains("AuditEntry"));
1144    }
1145
1146    // ============================================================
1147    // Checkpoint tests
1148    // ============================================================
1149
1150    #[test]
1151    fn test_checkpoint_clone_and_debug() {
1152        let checkpoint = Checkpoint {
1153            stage_index: 2,
1154            context: PipelineContext::new(),
1155            created_at: Instant::now(),
1156        };
1157
1158        let cloned = checkpoint;
1159        assert_eq!(cloned.stage_index, 2);
1160        assert!(format!("{:?}", cloned).contains("Checkpoint"));
1161    }
1162
1163    // ============================================================
1164    // BrickPipeline tests
1165    // ============================================================
1166
1167    #[test]
1168    fn test_pipeline_basic() {
1169        let mut pipeline = BrickPipeline::new("test")
1170            .stage(TestStage {
1171                name: "stage1",
1172                should_fail: false,
1173            })
1174            .stage(TestStage {
1175                name: "stage2",
1176                should_fail: false,
1177            });
1178
1179        let ctx = PipelineContext::new();
1180        let result = pipeline.run(ctx);
1181
1182        assert!(result.is_ok());
1183        let output = result.unwrap();
1184        assert!(output.get("stage1_output").is_some());
1185        assert!(output.get("stage2_output").is_some());
1186    }
1187
1188    #[test]
1189    fn test_pipeline_failure() {
1190        let mut pipeline = BrickPipeline::new("test")
1191            .stage(TestStage {
1192                name: "stage1",
1193                should_fail: false,
1194            })
1195            .stage(TestStage {
1196                name: "stage2",
1197                should_fail: true,
1198            });
1199
1200        let ctx = PipelineContext::new();
1201        let result = pipeline.run(ctx);
1202
1203        assert!(result.is_err());
1204        match result {
1205            Err(PipelineError::ExecutionFailed { stage, .. }) => {
1206                assert_eq!(stage, "stage2");
1207            }
1208            _ => panic!("Expected ExecutionFailed"),
1209        }
1210    }
1211
1212    #[test]
1213    fn test_pipeline_validation_failure() {
1214        let mut pipeline =
1215            BrickPipeline::new("test").stage(FailingValidationStage { name: "validator" });
1216
1217        let ctx = PipelineContext::new();
1218        let result = pipeline.run(ctx);
1219
1220        assert!(result.is_err());
1221        match result {
1222            Err(PipelineError::ValidationFailed { stage, reason }) => {
1223                assert_eq!(stage, "validator");
1224                assert!(reason.contains("Validation error"));
1225            }
1226            _ => panic!("Expected ValidationFailed"),
1227        }
1228    }
1229
1230    #[test]
1231    fn test_pipeline_privacy_tier() {
1232        let pipeline = BrickPipeline::new("test").with_privacy(PrivacyTier::Sovereign);
1233
1234        assert_eq!(pipeline.privacy_tier(), PrivacyTier::Sovereign);
1235    }
1236
1237    #[test]
1238    fn test_pipeline_name() {
1239        let pipeline = BrickPipeline::new("my-pipeline");
1240        assert_eq!(pipeline.name(), "my-pipeline");
1241    }
1242
1243    #[test]
1244    fn test_pipeline_stage_count() {
1245        let pipeline = BrickPipeline::new("test")
1246            .stage(TestStage {
1247                name: "s1",
1248                should_fail: false,
1249            })
1250            .stage(TestStage {
1251                name: "s2",
1252                should_fail: false,
1253            })
1254            .stage(TestStage {
1255                name: "s3",
1256                should_fail: false,
1257            });
1258
1259        assert_eq!(pipeline.stage_count(), 3);
1260    }
1261
1262    #[test]
1263    fn test_pipeline_empty() {
1264        let mut pipeline = BrickPipeline::new("empty");
1265
1266        let ctx = PipelineContext::new();
1267        let result = pipeline.run(ctx);
1268
1269        assert!(result.is_ok());
1270    }
1271
1272    #[test]
1273    fn test_pipeline_with_checkpointing() {
1274        let pipeline =
1275            BrickPipeline::new("checkpointed").with_checkpointing(Duration::from_secs(5));
1276
1277        // Just verify it compiles and sets the interval
1278        assert_eq!(pipeline.name(), "checkpointed");
1279    }
1280
1281    #[test]
1282    fn test_pipeline_audit_trail() {
1283        let mut pipeline = BrickPipeline::new("audited")
1284            .stage(TestStage {
1285                name: "step1",
1286                should_fail: false,
1287            })
1288            .stage(TestStage {
1289                name: "step2",
1290                should_fail: false,
1291            });
1292
1293        let ctx = PipelineContext::new();
1294        let _ = pipeline.run(ctx);
1295
1296        let trail = pipeline.audit_trail();
1297        assert_eq!(trail.len(), 2);
1298        assert!(trail[0].success);
1299        assert!(trail[1].success);
1300    }
1301
1302    #[test]
1303    fn test_pipeline_audit_trail_with_failure() {
1304        let mut pipeline = BrickPipeline::new("audited")
1305            .stage(TestStage {
1306                name: "success",
1307                should_fail: false,
1308            })
1309            .stage(TestStage {
1310                name: "failure",
1311                should_fail: true,
1312            });
1313
1314        let ctx = PipelineContext::new();
1315        let _ = pipeline.run(ctx);
1316
1317        let trail = pipeline.audit_trail();
1318        assert_eq!(trail.len(), 2);
1319        assert!(trail[0].success);
1320        assert!(!trail[1].success);
1321    }
1322
1323    #[test]
1324    fn test_pipeline_debug() {
1325        let pipeline = BrickPipeline::new("debug-test")
1326            .with_privacy(PrivacyTier::Private)
1327            .stage(TestStage {
1328                name: "s1",
1329                should_fail: false,
1330            });
1331
1332        let debug_str = format!("{:?}", pipeline);
1333        assert!(debug_str.contains("BrickPipeline"));
1334        assert!(debug_str.contains("debug-test"));
1335        assert!(debug_str.contains("Private"));
1336    }
1337
1338    #[test]
1339    fn test_pipeline_context_metadata_started_at() {
1340        let mut pipeline = BrickPipeline::new("test").stage(TestStage {
1341            name: "s1",
1342            should_fail: false,
1343        });
1344
1345        let ctx = PipelineContext::new();
1346        let result = pipeline.run(ctx).unwrap();
1347
1348        assert!(result.metadata.started_at.is_some());
1349    }
1350
1351    #[test]
1352    fn test_pipeline_traces_recorded() {
1353        let mut pipeline = BrickPipeline::new("traced").stage(TestStage {
1354            name: "traced_stage",
1355            should_fail: false,
1356        });
1357
1358        let ctx = PipelineContext::new();
1359        let result = pipeline.run(ctx).unwrap();
1360
1361        assert_eq!(result.trace.len(), 1);
1362        assert_eq!(result.trace[0].stage_name, "traced_stage");
1363        assert!(result.trace[0].success);
1364        assert!(result.trace[0].error.is_none());
1365    }
1366
1367    // ============================================================
1368    // BrickStage trait tests
1369    // ============================================================
1370
1371    #[test]
1372    fn test_brick_stage_default_required_inputs() {
1373        let stage = TestStage {
1374            name: "test",
1375            should_fail: false,
1376        };
1377
1378        assert!(stage.required_inputs().is_empty());
1379    }
1380
1381    #[test]
1382    fn test_brick_stage_default_output_names() {
1383        let stage = TestStage {
1384            name: "test",
1385            should_fail: false,
1386        };
1387
1388        assert!(stage.output_names().is_empty());
1389    }
1390
1391    // ============================================================
1392    // uuid_v4 function test
1393    // ============================================================
1394
1395    #[test]
1396    fn test_uuid_generation() {
1397        // Test that metadata run_id is unique
1398        let meta1 = PipelineMetadata::new();
1399        let meta2 = PipelineMetadata::new();
1400
1401        // They should both start with "run-"
1402        assert!(meta1.run_id.starts_with("run-"));
1403        assert!(meta2.run_id.starts_with("run-"));
1404    }
1405
1406    // ============================================================
1407    // Integration tests
1408    // ============================================================
1409
1410    #[test]
1411    fn test_full_pipeline_workflow() {
1412        let mut pipeline = BrickPipeline::new("full-workflow")
1413            .with_privacy(PrivacyTier::Private)
1414            .stage(TestStage {
1415                name: "input",
1416                should_fail: false,
1417            })
1418            .stage(TestStage {
1419                name: "transform",
1420                should_fail: false,
1421            })
1422            .stage(TestStage {
1423                name: "output",
1424                should_fail: false,
1425            });
1426
1427        let ctx = PipelineContext::from_input("initial", PipelineData::Text("start".into()));
1428        let result = pipeline.run(ctx).unwrap();
1429
1430        // Check all stages executed
1431        assert!(result.get("input_output").is_some());
1432        assert!(result.get("transform_output").is_some());
1433        assert!(result.get("output_output").is_some());
1434
1435        // Check traces
1436        assert_eq!(result.trace.len(), 3);
1437
1438        // Check audit trail
1439        assert_eq!(pipeline.audit_trail().len(), 3);
1440    }
1441
1442    #[test]
1443    fn test_pipeline_with_tensor_data() {
1444        let mut pipeline = BrickPipeline::new("tensor-pipeline").stage(TestStage {
1445            name: "process",
1446            should_fail: false,
1447        });
1448
1449        let ctx = PipelineContext::from_input(
1450            "tensor",
1451            PipelineData::tensor(vec![1.0, 2.0, 3.0, 4.0], vec![2, 2]),
1452        );
1453
1454        let result = pipeline.run(ctx).unwrap();
1455
1456        // Original tensor data should still be accessible
1457        let tensor = result.get("tensor").unwrap();
1458        let (data, shape) = tensor.as_tensor().unwrap();
1459        assert_eq!(data.len(), 4);
1460        assert_eq!(shape, &[2, 2]);
1461    }
1462
1463    // ============================================================
1464    // Additional coverage tests
1465    // ============================================================
1466
1467    /// A slow stage for testing checkpointing
1468    struct SlowStage {
1469        name: &'static str,
1470        delay_ms: u64,
1471    }
1472
1473    impl Brick for SlowStage {
1474        fn brick_name(&self) -> &'static str {
1475            self.name
1476        }
1477
1478        fn assertions(&self) -> &[BrickAssertion] {
1479            &[]
1480        }
1481
1482        fn budget(&self) -> BrickBudget {
1483            BrickBudget::uniform(100)
1484        }
1485
1486        fn verify(&self) -> BrickVerification {
1487            BrickVerification {
1488                passed: vec![],
1489                failed: vec![],
1490                verification_time: Duration::from_micros(10),
1491            }
1492        }
1493
1494        fn to_html(&self) -> String {
1495            String::new()
1496        }
1497
1498        fn to_css(&self) -> String {
1499            String::new()
1500        }
1501    }
1502
1503    impl BrickStage for SlowStage {
1504        fn execute(&self, mut ctx: PipelineContext) -> PipelineResult<PipelineContext> {
1505            // Simulate slow execution
1506            std::thread::sleep(Duration::from_millis(self.delay_ms));
1507            ctx.set(
1508                format!("{}_output", self.name),
1509                PipelineData::Text("slow done".into()),
1510            );
1511            Ok(ctx)
1512        }
1513
1514        fn validate(&self, _ctx: &PipelineContext) -> ValidationResult {
1515            ValidationResult::ok()
1516        }
1517    }
1518
1519    /// A stage with multiple validation errors
1520    struct MultiErrorValidationStage {
1521        name: &'static str,
1522    }
1523
1524    impl Brick for MultiErrorValidationStage {
1525        fn brick_name(&self) -> &'static str {
1526            self.name
1527        }
1528
1529        fn assertions(&self) -> &[BrickAssertion] {
1530            &[]
1531        }
1532
1533        fn budget(&self) -> BrickBudget {
1534            BrickBudget::uniform(100)
1535        }
1536
1537        fn verify(&self) -> BrickVerification {
1538            BrickVerification {
1539                passed: vec![],
1540                failed: vec![],
1541                verification_time: Duration::from_micros(10),
1542            }
1543        }
1544
1545        fn to_html(&self) -> String {
1546            String::new()
1547        }
1548
1549        fn to_css(&self) -> String {
1550            String::new()
1551        }
1552    }
1553
1554    impl BrickStage for MultiErrorValidationStage {
1555        fn execute(&self, ctx: PipelineContext) -> PipelineResult<PipelineContext> {
1556            Ok(ctx)
1557        }
1558
1559        fn validate(&self, _ctx: &PipelineContext) -> ValidationResult {
1560            let mut result = ValidationResult {
1561                valid: false,
1562                messages: vec![
1563                    ValidationMessage {
1564                        level: ValidationLevel::Error,
1565                        message: "First error".to_string(),
1566                    },
1567                    ValidationMessage {
1568                        level: ValidationLevel::Error,
1569                        message: "Second error".to_string(),
1570                    },
1571                    ValidationMessage {
1572                        level: ValidationLevel::Warning,
1573                        message: "A warning".to_string(),
1574                    },
1575                    ValidationMessage {
1576                        level: ValidationLevel::Info,
1577                        message: "Some info".to_string(),
1578                    },
1579                ],
1580            };
1581            // Add another warning to test warn() method
1582            result.warn("Another warning");
1583            result
1584        }
1585    }
1586
1587    /// A stage with custom required inputs and outputs
1588    struct CustomIOStage {
1589        name: &'static str,
1590        inputs: &'static [&'static str],
1591        outputs: &'static [&'static str],
1592    }
1593
1594    impl Brick for CustomIOStage {
1595        fn brick_name(&self) -> &'static str {
1596            self.name
1597        }
1598
1599        fn assertions(&self) -> &[BrickAssertion] {
1600            &[]
1601        }
1602
1603        fn budget(&self) -> BrickBudget {
1604            BrickBudget::uniform(100)
1605        }
1606
1607        fn verify(&self) -> BrickVerification {
1608            BrickVerification {
1609                passed: vec![],
1610                failed: vec![],
1611                verification_time: Duration::from_micros(10),
1612            }
1613        }
1614
1615        fn to_html(&self) -> String {
1616            String::new()
1617        }
1618
1619        fn to_css(&self) -> String {
1620            String::new()
1621        }
1622    }
1623
1624    impl BrickStage for CustomIOStage {
1625        fn execute(&self, mut ctx: PipelineContext) -> PipelineResult<PipelineContext> {
1626            for output in self.outputs {
1627                ctx.set((*output).to_string(), PipelineData::Text("output".into()));
1628            }
1629            Ok(ctx)
1630        }
1631
1632        fn validate(&self, _ctx: &PipelineContext) -> ValidationResult {
1633            ValidationResult::ok()
1634        }
1635
1636        fn required_inputs(&self) -> &[&str] {
1637            self.inputs
1638        }
1639
1640        fn output_names(&self) -> &[&str] {
1641            self.outputs
1642        }
1643    }
1644
1645    #[test]
1646    fn test_pipeline_checkpointing_triggers() {
1647        // Use very short checkpoint interval (1ms) to ensure checkpoint is created
1648        let mut pipeline = BrickPipeline::new("checkpoint-test")
1649            .with_checkpointing(Duration::from_millis(1))
1650            .stage(SlowStage {
1651                name: "slow1",
1652                delay_ms: 5,
1653            })
1654            .stage(SlowStage {
1655                name: "slow2",
1656                delay_ms: 5,
1657            })
1658            .stage(SlowStage {
1659                name: "slow3",
1660                delay_ms: 5,
1661            });
1662
1663        let ctx = PipelineContext::new();
1664        let result = pipeline.run(ctx);
1665
1666        assert!(result.is_ok());
1667        let output = result.unwrap();
1668        assert!(output.get("slow1_output").is_some());
1669        assert!(output.get("slow2_output").is_some());
1670        assert!(output.get("slow3_output").is_some());
1671    }
1672
1673    #[test]
1674    fn test_pipeline_multi_error_validation() {
1675        let mut pipeline =
1676            BrickPipeline::new("multi-error").stage(MultiErrorValidationStage { name: "multi" });
1677
1678        let ctx = PipelineContext::new();
1679        let result = pipeline.run(ctx);
1680
1681        assert!(result.is_err());
1682        match result {
1683            Err(PipelineError::ValidationFailed { stage, reason }) => {
1684                assert_eq!(stage, "multi");
1685                // Should contain both error messages joined by semicolons
1686                assert!(reason.contains("First error"));
1687                assert!(reason.contains("Second error"));
1688                // Should NOT contain warnings or info
1689                assert!(!reason.contains("warning"));
1690                assert!(!reason.contains("info"));
1691            }
1692            _ => panic!("Expected ValidationFailed"),
1693        }
1694    }
1695
1696    #[test]
1697    fn test_custom_io_stage_inputs_outputs() {
1698        let stage = CustomIOStage {
1699            name: "custom",
1700            inputs: &["input1", "input2"],
1701            outputs: &["output1", "output2"],
1702        };
1703
1704        assert_eq!(stage.required_inputs(), &["input1", "input2"]);
1705        assert_eq!(stage.output_names(), &["output1", "output2"]);
1706    }
1707
1708    #[test]
1709    fn test_pipeline_with_custom_io_stage() {
1710        let mut pipeline = BrickPipeline::new("custom-io").stage(CustomIOStage {
1711            name: "custom",
1712            inputs: &["in"],
1713            outputs: &["out1", "out2"],
1714        });
1715
1716        let ctx = PipelineContext::from_input("in", PipelineData::Text("input".into()));
1717        let result = pipeline.run(ctx).unwrap();
1718
1719        assert!(result.get("out1").is_some());
1720        assert!(result.get("out2").is_some());
1721    }
1722
1723    #[test]
1724    fn test_validation_level_info() {
1725        // Test Info level specifically
1726        let msg = ValidationMessage {
1727            level: ValidationLevel::Info,
1728            message: "Informational message".to_string(),
1729        };
1730
1731        assert_eq!(msg.level, ValidationLevel::Info);
1732        assert!(format!("{:?}", msg.level).contains("Info"));
1733    }
1734
1735    #[test]
1736    fn test_pipeline_error_clone() {
1737        // Test cloning of all error variants
1738        let err1 = PipelineError::ValidationFailed {
1739            stage: "s".to_string(),
1740            reason: "r".to_string(),
1741        };
1742        let cloned1 = err1;
1743        assert!(matches!(cloned1, PipelineError::ValidationFailed { .. }));
1744
1745        let err2 = PipelineError::ExecutionFailed {
1746            stage: "s".to_string(),
1747            reason: "r".to_string(),
1748        };
1749        let cloned2 = err2;
1750        assert!(matches!(cloned2, PipelineError::ExecutionFailed { .. }));
1751
1752        let err3 = PipelineError::MissingInput {
1753            stage: "s".to_string(),
1754            input: "i".to_string(),
1755        };
1756        let cloned3 = err3;
1757        assert!(matches!(cloned3, PipelineError::MissingInput { .. }));
1758
1759        let err4 = PipelineError::PrivacyViolation {
1760            tier: PrivacyTier::Sovereign,
1761            reason: "r".to_string(),
1762        };
1763        let cloned4 = err4;
1764        assert!(matches!(cloned4, PipelineError::PrivacyViolation { .. }));
1765
1766        let err5 = PipelineError::CheckpointFailed {
1767            reason: "r".to_string(),
1768        };
1769        let cloned5 = err5;
1770        assert!(matches!(cloned5, PipelineError::CheckpointFailed { .. }));
1771
1772        let err6 = PipelineError::BrickError("e".to_string());
1773        let cloned6 = err6;
1774        assert!(matches!(cloned6, PipelineError::BrickError(_)));
1775    }
1776
1777    #[test]
1778    fn test_pipeline_error_debug() {
1779        let err = PipelineError::ValidationFailed {
1780            stage: "test".to_string(),
1781            reason: "debug test".to_string(),
1782        };
1783        let debug_str = format!("{:?}", err);
1784        assert!(debug_str.contains("ValidationFailed"));
1785    }
1786
1787    #[test]
1788    fn test_validation_result_multiple_warnings() {
1789        let mut result = ValidationResult::ok();
1790        result.warn("warning 1");
1791        result.warn("warning 2");
1792        result.warn("warning 3");
1793
1794        assert!(result.valid);
1795        assert_eq!(result.messages.len(), 3);
1796        for msg in &result.messages {
1797            assert_eq!(msg.level, ValidationLevel::Warning);
1798        }
1799    }
1800
1801    #[test]
1802    fn test_pipeline_data_debug_variants() {
1803        // Test Debug for all PipelineData variants
1804        let bytes = PipelineData::Bytes(vec![1, 2, 3]);
1805        assert!(format!("{:?}", bytes).contains("Bytes"));
1806
1807        let tensor = PipelineData::FloatTensor {
1808            data: vec![1.0],
1809            shape: vec![1],
1810        };
1811        assert!(format!("{:?}", tensor).contains("FloatTensor"));
1812
1813        let text = PipelineData::Text("hello".into());
1814        assert!(format!("{:?}", text).contains("Text"));
1815
1816        let json = PipelineData::Json(serde_json::json!({}));
1817        assert!(format!("{:?}", json).contains("Json"));
1818
1819        let int = PipelineData::Int(42);
1820        assert!(format!("{:?}", int).contains("Int"));
1821
1822        let boolean = PipelineData::Bool(false);
1823        assert!(format!("{:?}", boolean).contains("Bool"));
1824    }
1825
1826    #[test]
1827    fn test_pipeline_context_set_with_string() {
1828        let mut ctx = PipelineContext::new();
1829        // Test set() with String instead of &str
1830        ctx.set(String::from("key"), PipelineData::Int(123));
1831
1832        assert!(ctx.get("key").is_some());
1833    }
1834
1835    #[test]
1836    fn test_pipeline_metadata_tag_with_string() {
1837        let mut meta = PipelineMetadata::new();
1838        // Test tag() with String instead of &str
1839        meta.tag(String::from("key"), String::from("value"));
1840
1841        assert_eq!(meta.tags.get("key"), Some(&"value".to_string()));
1842    }
1843
1844    #[test]
1845    fn test_audit_collector_total_duration_empty() {
1846        let collector = PipelineAuditCollector::new();
1847        assert_eq!(collector.total_duration(), Duration::ZERO);
1848    }
1849
1850    #[test]
1851    fn test_privacy_tier_copy() {
1852        let tier = PrivacyTier::Sovereign;
1853        let copied = tier;
1854        assert_eq!(tier, copied);
1855        assert_eq!(tier, PrivacyTier::Sovereign);
1856    }
1857
1858    #[test]
1859    fn test_stage_trace_error_field() {
1860        let trace = StageTrace {
1861            stage_name: "error_stage".to_string(),
1862            duration: Duration::from_secs(1),
1863            success: false,
1864            error: Some("error message".to_string()),
1865        };
1866
1867        assert_eq!(trace.error.as_deref(), Some("error message"));
1868    }
1869
1870    #[test]
1871    fn test_pipeline_run_clears_checkpoint_on_success() {
1872        let mut pipeline = BrickPipeline::new("clear-checkpoint")
1873            .with_checkpointing(Duration::from_millis(1))
1874            .stage(SlowStage {
1875                name: "slow",
1876                delay_ms: 5,
1877            });
1878
1879        let ctx = PipelineContext::new();
1880        let result = pipeline.run(ctx);
1881
1882        assert!(result.is_ok());
1883        // After successful run, checkpoint should be cleared
1884        // (internal state - verified by running again successfully)
1885        let ctx2 = PipelineContext::new();
1886        let result2 = pipeline.run(ctx2);
1887        assert!(result2.is_ok());
1888    }
1889
1890    #[test]
1891    fn test_validation_message_levels() {
1892        let info = ValidationMessage {
1893            level: ValidationLevel::Info,
1894            message: "info".to_string(),
1895        };
1896        let warning = ValidationMessage {
1897            level: ValidationLevel::Warning,
1898            message: "warning".to_string(),
1899        };
1900        let error = ValidationMessage {
1901            level: ValidationLevel::Error,
1902            message: "error".to_string(),
1903        };
1904
1905        assert_ne!(info.level, warning.level);
1906        assert_ne!(warning.level, error.level);
1907        assert_ne!(info.level, error.level);
1908    }
1909
1910    #[test]
1911    fn test_pipeline_data_clone_all_variants() {
1912        let bytes = PipelineData::Bytes(vec![1, 2, 3]);
1913        let _ = bytes;
1914
1915        let tensor = PipelineData::FloatTensor {
1916            data: vec![1.0, 2.0],
1917            shape: vec![2],
1918        };
1919        let _ = tensor;
1920
1921        let text = PipelineData::Text("test".into());
1922        let _ = text;
1923
1924        let json = PipelineData::Json(serde_json::json!({"key": "value"}));
1925        let _ = json;
1926
1927        let int = PipelineData::Int(-100);
1928        let _ = int;
1929
1930        let boolean = PipelineData::Bool(true);
1931        let _ = boolean;
1932    }
1933
1934    #[test]
1935    fn test_pipeline_with_input_context() {
1936        let mut pipeline = BrickPipeline::new("with-input").stage(TestStage {
1937            name: "process",
1938            should_fail: false,
1939        });
1940
1941        // Test running with pre-populated context
1942        let mut ctx = PipelineContext::new();
1943        ctx.set("input1", PipelineData::Text("value1".into()));
1944        ctx.set("input2", PipelineData::Int(42));
1945        ctx.metadata.tag("env", "test");
1946
1947        let result = pipeline.run(ctx).unwrap();
1948
1949        // Original inputs should still be present
1950        assert!(result.get("input1").is_some());
1951        assert!(result.get("input2").is_some());
1952        // Stage output should be present
1953        assert!(result.get("process_output").is_some());
1954    }
1955
1956    #[test]
1957    fn test_uuid_v4_generates_unique_ids() {
1958        // Generate multiple run IDs and verify they're unique
1959        let mut ids = std::collections::HashSet::new();
1960        for _ in 0..100 {
1961            let meta = PipelineMetadata::new();
1962            ids.insert(meta.run_id);
1963        }
1964        // Should have generated 100 unique IDs (or very close due to timing)
1965        assert!(ids.len() >= 90);
1966    }
1967
1968    #[test]
1969    fn test_pipeline_debug_format_complete() {
1970        let pipeline = BrickPipeline::new("debug-complete")
1971            .with_privacy(PrivacyTier::Sovereign)
1972            .stage(TestStage {
1973                name: "s1",
1974                should_fail: false,
1975            })
1976            .stage(TestStage {
1977                name: "s2",
1978                should_fail: false,
1979            });
1980
1981        let debug_str = format!("{:?}", pipeline);
1982        assert!(debug_str.contains("BrickPipeline"));
1983        assert!(debug_str.contains("debug-complete"));
1984        assert!(debug_str.contains("stage_count"));
1985        assert!(debug_str.contains('2'));
1986        assert!(debug_str.contains("Sovereign"));
1987    }
1988
1989    #[test]
1990    fn test_checkpoint_fields() {
1991        let ctx = PipelineContext::from_input("test", PipelineData::Text("data".into()));
1992        let checkpoint = Checkpoint {
1993            stage_index: 5,
1994            context: ctx,
1995            created_at: Instant::now(),
1996        };
1997
1998        assert_eq!(checkpoint.stage_index, 5);
1999        assert!(checkpoint.context.get("test").is_some());
2000    }
2001
2002    #[test]
2003    fn test_audit_entry_fields() {
2004        let entry = AuditEntry {
2005            stage: "my_stage".to_string(),
2006            timestamp: Instant::now(),
2007            duration: Duration::from_millis(250),
2008            success: false,
2009            inputs: vec!["a".to_string(), "b".to_string()],
2010            outputs: vec!["c".to_string()],
2011        };
2012
2013        assert_eq!(entry.stage, "my_stage");
2014        assert_eq!(entry.duration, Duration::from_millis(250));
2015        assert!(!entry.success);
2016        assert_eq!(entry.inputs.len(), 2);
2017        assert_eq!(entry.outputs.len(), 1);
2018    }
2019
2020    #[test]
2021    fn test_pipeline_error_display_all_variants() {
2022        // Ensure all Display implementations are covered
2023        let errors = vec![
2024            PipelineError::ValidationFailed {
2025                stage: "stg".to_string(),
2026                reason: "rsn".to_string(),
2027            },
2028            PipelineError::ExecutionFailed {
2029                stage: "stg".to_string(),
2030                reason: "rsn".to_string(),
2031            },
2032            PipelineError::MissingInput {
2033                stage: "stg".to_string(),
2034                input: "inp".to_string(),
2035            },
2036            PipelineError::PrivacyViolation {
2037                tier: PrivacyTier::Private,
2038                reason: "rsn".to_string(),
2039            },
2040            PipelineError::CheckpointFailed {
2041                reason: "rsn".to_string(),
2042            },
2043            PipelineError::BrickError("err".to_string()),
2044        ];
2045
2046        for err in errors {
2047            let display = format!("{}", err);
2048            assert!(!display.is_empty());
2049        }
2050    }
2051
2052    #[test]
2053    fn test_pipeline_context_trace_with_error() {
2054        let mut ctx = PipelineContext::new();
2055        ctx.add_trace(StageTrace {
2056            stage_name: "failing".to_string(),
2057            duration: Duration::from_millis(50),
2058            success: false,
2059            error: Some("Detailed error message".to_string()),
2060        });
2061
2062        assert_eq!(ctx.trace.len(), 1);
2063        assert!(!ctx.trace[0].success);
2064        assert!(ctx.trace[0].error.is_some());
2065        assert!(ctx.trace[0]
2066            .error
2067            .as_ref()
2068            .unwrap()
2069            .contains("Detailed error"));
2070    }
2071
2072    /// A stage that sets a checkpoint marker so we can detect if checkpoint was restored
2073    struct CheckpointMarkerStage {
2074        name: &'static str,
2075        marker_value: &'static str,
2076    }
2077
2078    impl Brick for CheckpointMarkerStage {
2079        fn brick_name(&self) -> &'static str {
2080            self.name
2081        }
2082
2083        fn assertions(&self) -> &[BrickAssertion] {
2084            &[]
2085        }
2086
2087        fn budget(&self) -> BrickBudget {
2088            BrickBudget::uniform(100)
2089        }
2090
2091        fn verify(&self) -> BrickVerification {
2092            BrickVerification {
2093                passed: vec![],
2094                failed: vec![],
2095                verification_time: Duration::from_micros(10),
2096            }
2097        }
2098
2099        fn to_html(&self) -> String {
2100            String::new()
2101        }
2102
2103        fn to_css(&self) -> String {
2104            String::new()
2105        }
2106    }
2107
2108    impl BrickStage for CheckpointMarkerStage {
2109        fn execute(&self, mut ctx: PipelineContext) -> PipelineResult<PipelineContext> {
2110            ctx.set(
2111                format!("{}_marker", self.name),
2112                PipelineData::Text(self.marker_value.to_string()),
2113            );
2114            Ok(ctx)
2115        }
2116
2117        fn validate(&self, _ctx: &PipelineContext) -> ValidationResult {
2118            ValidationResult::ok()
2119        }
2120    }
2121
2122    #[test]
2123    fn test_pipeline_checkpoint_restoration() {
2124        // Create a pipeline with checkpointing
2125        let mut pipeline = BrickPipeline::new("checkpoint-restore-test")
2126            .with_checkpointing(Duration::from_nanos(1))
2127            .stage(SlowStage {
2128                name: "stage1",
2129                delay_ms: 2,
2130            })
2131            .stage(SlowStage {
2132                name: "stage2",
2133                delay_ms: 2,
2134            });
2135
2136        // First run - creates checkpoint
2137        let ctx = PipelineContext::new();
2138        let result1 = pipeline.run(ctx);
2139        assert!(result1.is_ok());
2140
2141        // Simulate failure and re-run - checkpoint would be used if present
2142        // Note: after successful completion checkpoint is cleared,
2143        // so this tests the clearing behavior
2144        let ctx2 = PipelineContext::new();
2145        let result2 = pipeline.run(ctx2);
2146        assert!(result2.is_ok());
2147    }
2148
2149    #[test]
2150    fn test_pipeline_start_index_from_checkpoint() {
2151        // Manually set up a pipeline with a checkpoint to test start_index logic
2152        let mut pipeline = BrickPipeline::new("start-index-test")
2153            .stage(TestStage {
2154                name: "stage1",
2155                should_fail: false,
2156            })
2157            .stage(TestStage {
2158                name: "stage2",
2159                should_fail: false,
2160            })
2161            .stage(TestStage {
2162                name: "stage3",
2163                should_fail: false,
2164            });
2165
2166        // Manually set a checkpoint at stage index 1 (skip first stage)
2167        let checkpoint_ctx = PipelineContext::from_input("checkpoint_data", PipelineData::Int(42));
2168        pipeline.last_checkpoint = Some(Checkpoint {
2169            stage_index: 1,
2170            context: checkpoint_ctx,
2171            created_at: Instant::now(),
2172        });
2173
2174        // Run with fresh context - should restore from checkpoint
2175        let fresh_ctx = PipelineContext::new();
2176        let result = pipeline.run(fresh_ctx).unwrap();
2177
2178        // Should have stage2 and stage3 outputs (stage1 skipped)
2179        assert!(result.get("stage2_output").is_some());
2180        assert!(result.get("stage3_output").is_some());
2181        // stage1_output should NOT be present since we skipped it
2182        assert!(result.get("stage1_output").is_none());
2183        // checkpoint_data should be present since we restored from checkpoint
2184        assert!(result.get("checkpoint_data").is_some());
2185    }
2186
2187    #[test]
2188    fn test_pipeline_checkpoint_context_restored() {
2189        // Verify that checkpoint context is actually restored
2190        let mut pipeline = BrickPipeline::new("context-restore-test")
2191            .stage(TestStage {
2192                name: "stage1",
2193                should_fail: false,
2194            })
2195            .stage(TestStage {
2196                name: "stage2",
2197                should_fail: false,
2198            });
2199
2200        // Create checkpoint with specific data
2201        let mut checkpoint_ctx = PipelineContext::new();
2202        checkpoint_ctx.set("restored_key", PipelineData::Text("restored_value".into()));
2203
2204        pipeline.last_checkpoint = Some(Checkpoint {
2205            stage_index: 0,
2206            context: checkpoint_ctx,
2207            created_at: Instant::now(),
2208        });
2209
2210        // Run should use checkpoint context
2211        let input_ctx =
2212            PipelineContext::from_input("input_key", PipelineData::Text("input_value".into()));
2213        let result = pipeline.run(input_ctx).unwrap();
2214
2215        // Restored context should have the checkpoint data
2216        assert!(result.get("restored_key").is_some());
2217        // Input context's data should NOT be present (checkpoint overwrites)
2218        assert!(result.get("input_key").is_none());
2219    }
2220
2221    #[test]
2222    fn test_multiple_checkpoints_during_run() {
2223        // Test that multiple checkpoints are created during a long run
2224        let mut pipeline = BrickPipeline::new("multi-checkpoint")
2225            .with_checkpointing(Duration::from_millis(1))
2226            .stage(SlowStage {
2227                name: "s1",
2228                delay_ms: 3,
2229            })
2230            .stage(SlowStage {
2231                name: "s2",
2232                delay_ms: 3,
2233            })
2234            .stage(SlowStage {
2235                name: "s3",
2236                delay_ms: 3,
2237            })
2238            .stage(SlowStage {
2239                name: "s4",
2240                delay_ms: 3,
2241            });
2242
2243        let ctx = PipelineContext::new();
2244        let result = pipeline.run(ctx);
2245
2246        assert!(result.is_ok());
2247        let output = result.unwrap();
2248        assert!(output.get("s1_output").is_some());
2249        assert!(output.get("s2_output").is_some());
2250        assert!(output.get("s3_output").is_some());
2251        assert!(output.get("s4_output").is_some());
2252    }
2253
2254    #[test]
2255    fn test_checkpoint_not_created_when_interval_not_exceeded() {
2256        // Use a very long interval so checkpoint is never created
2257        let mut pipeline = BrickPipeline::new("no-checkpoint")
2258            .with_checkpointing(Duration::from_secs(3600)) // 1 hour
2259            .stage(TestStage {
2260                name: "fast1",
2261                should_fail: false,
2262            })
2263            .stage(TestStage {
2264                name: "fast2",
2265                should_fail: false,
2266            });
2267
2268        let ctx = PipelineContext::new();
2269        let result = pipeline.run(ctx);
2270
2271        assert!(result.is_ok());
2272        // Checkpoint should be None after run (cleared on success)
2273        assert!(pipeline.last_checkpoint.is_none());
2274    }
2275
2276    #[test]
2277    fn test_all_privacy_tier_variants_in_debug() {
2278        // Ensure all PrivacyTier variants are covered in Debug
2279        let sovereign = PrivacyTier::Sovereign;
2280        let private = PrivacyTier::Private;
2281        let standard = PrivacyTier::Standard;
2282
2283        assert!(format!("{:?}", sovereign).contains("Sovereign"));
2284        assert!(format!("{:?}", private).contains("Private"));
2285        assert!(format!("{:?}", standard).contains("Standard"));
2286    }
2287
2288    #[test]
2289    fn test_pipeline_error_debug_all_variants() {
2290        // Test Debug for all PipelineError variants
2291        let errors: Vec<PipelineError> = vec![
2292            PipelineError::ValidationFailed {
2293                stage: "s".to_string(),
2294                reason: "r".to_string(),
2295            },
2296            PipelineError::ExecutionFailed {
2297                stage: "s".to_string(),
2298                reason: "r".to_string(),
2299            },
2300            PipelineError::MissingInput {
2301                stage: "s".to_string(),
2302                input: "i".to_string(),
2303            },
2304            PipelineError::PrivacyViolation {
2305                tier: PrivacyTier::Sovereign,
2306                reason: "r".to_string(),
2307            },
2308            PipelineError::CheckpointFailed {
2309                reason: "r".to_string(),
2310            },
2311            PipelineError::BrickError("e".to_string()),
2312        ];
2313
2314        for err in errors {
2315            let debug_str = format!("{:?}", err);
2316            assert!(!debug_str.is_empty());
2317        }
2318    }
2319
2320    #[test]
2321    fn test_pipeline_run_with_zero_stages() {
2322        let mut pipeline = BrickPipeline::new("zero-stages");
2323
2324        let ctx = PipelineContext::from_input("data", PipelineData::Bool(true));
2325        let result = pipeline.run(ctx).unwrap();
2326
2327        // Input should still be present
2328        assert!(result.get("data").is_some());
2329        // started_at should be set
2330        assert!(result.metadata.started_at.is_some());
2331    }
2332
2333    #[test]
2334    fn test_validation_result_fail_with_different_messages() {
2335        let fail1 = ValidationResult::fail("error message");
2336        assert!(!fail1.valid);
2337        assert_eq!(fail1.messages.len(), 1);
2338
2339        let fail2 = ValidationResult::fail(String::from("string message"));
2340        assert!(!fail2.valid);
2341        assert_eq!(fail2.messages.len(), 1);
2342    }
2343
2344    #[test]
2345    fn test_pipeline_data_tensor_multidimensional() {
2346        let data =
2347            PipelineData::tensor(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0], vec![2, 2, 2]);
2348
2349        let (values, shape) = data.as_tensor().unwrap();
2350        assert_eq!(values.len(), 8);
2351        assert_eq!(shape, &[2, 2, 2]);
2352    }
2353
2354    #[test]
2355    fn test_audit_collector_records_multiple() {
2356        let mut collector = PipelineAuditCollector::new();
2357
2358        collector.record("stage1", Duration::from_millis(10), true);
2359        collector.record("stage2", Duration::from_millis(20), true);
2360        collector.record("stage3", Duration::from_millis(30), false);
2361        collector.record("stage4", Duration::from_millis(40), true);
2362
2363        assert_eq!(collector.entries().len(), 4);
2364        assert_eq!(collector.total_duration(), Duration::from_millis(100));
2365
2366        // Verify individual entries
2367        assert_eq!(collector.entries()[0].stage, "stage1");
2368        assert!(collector.entries()[0].success);
2369        assert!(!collector.entries()[2].success);
2370    }
2371
2372    // ============================================================
2373    // Additional coverage tests for 95%+ target
2374    // ============================================================
2375
2376    #[test]
2377    fn test_checkpoint_marker_stage_execute() {
2378        // Use CheckpointMarkerStage to remove dead_code warning and cover its execute path
2379        let stage = CheckpointMarkerStage {
2380            name: "marker",
2381            marker_value: "test_marker",
2382        };
2383
2384        let ctx = PipelineContext::new();
2385        let result = stage.execute(ctx).unwrap();
2386
2387        assert!(result.get("marker_marker").is_some());
2388        if let Some(PipelineData::Text(value)) = result.get("marker_marker") {
2389            assert_eq!(value, "test_marker");
2390        } else {
2391            panic!("Expected Text variant");
2392        }
2393    }
2394
2395    #[test]
2396    fn test_checkpoint_marker_stage_validate() {
2397        let stage = CheckpointMarkerStage {
2398            name: "marker",
2399            marker_value: "val",
2400        };
2401
2402        let ctx = PipelineContext::new();
2403        let validation = stage.validate(&ctx);
2404
2405        assert!(validation.valid);
2406    }
2407
2408    #[test]
2409    fn test_checkpoint_marker_stage_brick_impl() {
2410        let stage = CheckpointMarkerStage {
2411            name: "test_marker",
2412            marker_value: "v",
2413        };
2414
2415        assert_eq!(stage.brick_name(), "test_marker");
2416        assert!(stage.assertions().is_empty());
2417        assert!(stage.to_html().is_empty());
2418        assert!(stage.to_css().is_empty());
2419
2420        let budget = stage.budget();
2421        assert_eq!(budget.total_ms, 100);
2422
2423        let verify = stage.verify();
2424        assert!(verify.passed.is_empty());
2425        assert!(verify.failed.is_empty());
2426    }
2427
2428    #[test]
2429    fn test_slow_stage_brick_impl() {
2430        let stage = SlowStage {
2431            name: "slow_test",
2432            delay_ms: 1,
2433        };
2434
2435        assert_eq!(stage.brick_name(), "slow_test");
2436        assert!(stage.assertions().is_empty());
2437        assert!(stage.to_html().is_empty());
2438        assert!(stage.to_css().is_empty());
2439
2440        let budget = stage.budget();
2441        assert_eq!(budget.total_ms, 100);
2442
2443        let verify = stage.verify();
2444        assert!(verify.passed.is_empty());
2445    }
2446
2447    #[test]
2448    fn test_slow_stage_validate() {
2449        let stage = SlowStage {
2450            name: "slow",
2451            delay_ms: 1,
2452        };
2453
2454        let ctx = PipelineContext::new();
2455        let validation = stage.validate(&ctx);
2456
2457        assert!(validation.valid);
2458    }
2459
2460    #[test]
2461    fn test_multi_error_validation_stage_brick_impl() {
2462        let stage = MultiErrorValidationStage { name: "multi_err" };
2463
2464        assert_eq!(stage.brick_name(), "multi_err");
2465        assert!(stage.assertions().is_empty());
2466        assert!(stage.to_html().is_empty());
2467        assert!(stage.to_css().is_empty());
2468
2469        let budget = stage.budget();
2470        assert_eq!(budget.total_ms, 100);
2471
2472        let verify = stage.verify();
2473        assert!(verify.passed.is_empty());
2474    }
2475
2476    #[test]
2477    fn test_multi_error_validation_stage_execute() {
2478        let stage = MultiErrorValidationStage { name: "multi" };
2479
2480        let ctx = PipelineContext::new();
2481        let result = stage.execute(ctx);
2482
2483        // Execute always succeeds
2484        assert!(result.is_ok());
2485    }
2486
2487    #[test]
2488    fn test_custom_io_stage_brick_impl() {
2489        let stage = CustomIOStage {
2490            name: "custom_io",
2491            inputs: &["a"],
2492            outputs: &["b"],
2493        };
2494
2495        assert_eq!(stage.brick_name(), "custom_io");
2496        assert!(stage.assertions().is_empty());
2497        assert!(stage.to_html().is_empty());
2498        assert!(stage.to_css().is_empty());
2499
2500        let budget = stage.budget();
2501        assert_eq!(budget.total_ms, 100);
2502
2503        let verify = stage.verify();
2504        assert!(verify.passed.is_empty());
2505    }
2506
2507    #[test]
2508    fn test_custom_io_stage_validate() {
2509        let stage = CustomIOStage {
2510            name: "custom",
2511            inputs: &[],
2512            outputs: &[],
2513        };
2514
2515        let ctx = PipelineContext::new();
2516        let validation = stage.validate(&ctx);
2517
2518        assert!(validation.valid);
2519    }
2520
2521    #[test]
2522    fn test_failing_validation_stage_brick_impl() {
2523        let stage = FailingValidationStage { name: "fail_val" };
2524
2525        assert_eq!(stage.brick_name(), "fail_val");
2526        assert!(stage.assertions().is_empty());
2527        assert!(stage.to_html().is_empty());
2528        assert!(stage.to_css().is_empty());
2529
2530        let budget = stage.budget();
2531        assert_eq!(budget.total_ms, 100);
2532
2533        let verify = stage.verify();
2534        assert!(verify.passed.is_empty());
2535    }
2536
2537    #[test]
2538    fn test_test_stage_brick_impl_full() {
2539        let stage = TestStage {
2540            name: "test_brick",
2541            should_fail: false,
2542        };
2543
2544        assert_eq!(stage.brick_name(), "test_brick");
2545        assert!(stage.assertions().is_empty());
2546        assert!(stage.to_html().is_empty());
2547        assert!(stage.to_css().is_empty());
2548
2549        let budget = stage.budget();
2550        assert_eq!(budget.total_ms, 100);
2551
2552        let verify = stage.verify();
2553        assert!(verify.passed.is_empty());
2554        assert!(verify.failed.is_empty());
2555    }
2556
2557    #[test]
2558    fn test_pipeline_failure_records_trace() {
2559        let mut pipeline = BrickPipeline::new("failure-trace")
2560            .stage(TestStage {
2561                name: "success_stage",
2562                should_fail: false,
2563            })
2564            .stage(TestStage {
2565                name: "fail_stage",
2566                should_fail: true,
2567            });
2568
2569        let ctx = PipelineContext::new();
2570        let result = pipeline.run(ctx);
2571
2572        assert!(result.is_err());
2573
2574        // Check audit trail includes both stages
2575        let trail = pipeline.audit_trail();
2576        assert_eq!(trail.len(), 2);
2577        assert!(trail[0].success);
2578        assert!(!trail[1].success);
2579    }
2580
2581    #[test]
2582    fn test_pipeline_data_all_variants_as_methods() {
2583        // Test as_tensor on non-tensor types
2584        let bytes = PipelineData::Bytes(vec![1, 2]);
2585        assert!(bytes.as_tensor().is_none());
2586        assert!(bytes.as_text().is_none());
2587
2588        let json = PipelineData::Json(serde_json::json!({}));
2589        assert!(json.as_tensor().is_none());
2590        assert!(json.as_text().is_none());
2591
2592        let int = PipelineData::Int(42);
2593        assert!(int.as_tensor().is_none());
2594        assert!(int.as_text().is_none());
2595
2596        let boolean = PipelineData::Bool(true);
2597        assert!(boolean.as_tensor().is_none());
2598        assert!(boolean.as_text().is_none());
2599    }
2600
2601    #[test]
2602    fn test_pipeline_with_checkpoint_marker_stage() {
2603        let mut pipeline = BrickPipeline::new("marker-pipeline")
2604            .with_checkpointing(Duration::from_millis(1))
2605            .stage(CheckpointMarkerStage {
2606                name: "mark1",
2607                marker_value: "first",
2608            })
2609            .stage(SlowStage {
2610                name: "slow",
2611                delay_ms: 5,
2612            })
2613            .stage(CheckpointMarkerStage {
2614                name: "mark2",
2615                marker_value: "second",
2616            });
2617
2618        let ctx = PipelineContext::new();
2619        let result = pipeline.run(ctx).unwrap();
2620
2621        assert!(result.get("mark1_marker").is_some());
2622        assert!(result.get("mark2_marker").is_some());
2623        assert!(result.get("slow_output").is_some());
2624    }
2625
2626    #[test]
2627    fn test_pipeline_error_from_brick_error_explicit() {
2628        use crate::brick::BrickError;
2629
2630        let brick_err = BrickError::MissingChild {
2631            expected: "child_brick".to_string(),
2632        };
2633        let pipeline_err = PipelineError::from(brick_err);
2634
2635        match pipeline_err {
2636            PipelineError::BrickError(msg) => {
2637                assert!(msg.contains("child_brick"));
2638            }
2639            _ => panic!("Expected BrickError variant"),
2640        }
2641    }
2642
2643    #[test]
2644    fn test_pipeline_context_multiple_traces() {
2645        let mut ctx = PipelineContext::new();
2646
2647        for i in 0..5 {
2648            ctx.add_trace(StageTrace {
2649                stage_name: format!("stage_{}", i),
2650                duration: Duration::from_millis(10 * i as u64),
2651                success: i % 2 == 0,
2652                error: if i % 2 == 1 {
2653                    Some(format!("Error at stage {}", i))
2654                } else {
2655                    None
2656                },
2657            });
2658        }
2659
2660        assert_eq!(ctx.trace.len(), 5);
2661        assert!(ctx.trace[0].success);
2662        assert!(!ctx.trace[1].success);
2663        assert!(ctx.trace[1].error.is_some());
2664    }
2665
2666    #[test]
2667    fn test_validation_result_with_info_level() {
2668        let result = ValidationResult {
2669            valid: true,
2670            messages: vec![ValidationMessage {
2671                level: ValidationLevel::Info,
2672                message: "Just some info".to_string(),
2673            }],
2674        };
2675
2676        assert!(result.valid);
2677        assert_eq!(result.messages.len(), 1);
2678        assert_eq!(result.messages[0].level, ValidationLevel::Info);
2679    }
2680
2681    #[test]
2682    fn test_pipeline_metadata_multiple_tags() {
2683        let mut meta = PipelineMetadata::new();
2684
2685        meta.tag("key1", "value1");
2686        meta.tag("key2", "value2");
2687        meta.tag("key3", "value3");
2688        // Overwrite a key
2689        meta.tag("key1", "new_value1");
2690
2691        assert_eq!(meta.tags.len(), 3);
2692        assert_eq!(meta.tags.get("key1"), Some(&"new_value1".to_string()));
2693    }
2694
2695    #[test]
2696    fn test_pipeline_context_get_nonexistent() {
2697        let ctx = PipelineContext::new();
2698
2699        assert!(ctx.get("nonexistent").is_none());
2700        assert!(ctx.get("").is_none());
2701        assert!(ctx.get("some_key").is_none());
2702    }
2703
2704    #[test]
2705    fn test_pipeline_data_empty_tensor() {
2706        let data = PipelineData::tensor(vec![], vec![0]);
2707
2708        let (values, shape) = data.as_tensor().unwrap();
2709        assert!(values.is_empty());
2710        assert_eq!(shape, &[0]);
2711    }
2712
2713    #[test]
2714    fn test_pipeline_data_empty_text() {
2715        let data = PipelineData::Text(String::new());
2716
2717        assert_eq!(data.as_text(), Some(""));
2718    }
2719
2720    #[test]
2721    fn test_audit_entry_with_empty_io() {
2722        let entry = AuditEntry {
2723            stage: "empty_io".to_string(),
2724            timestamp: Instant::now(),
2725            duration: Duration::from_nanos(1),
2726            success: true,
2727            inputs: Vec::new(),
2728            outputs: Vec::new(),
2729        };
2730
2731        assert!(entry.inputs.is_empty());
2732        assert!(entry.outputs.is_empty());
2733    }
2734
2735    #[test]
2736    fn test_checkpoint_with_empty_context() {
2737        let checkpoint = Checkpoint {
2738            stage_index: 0,
2739            context: PipelineContext::new(),
2740            created_at: Instant::now(),
2741        };
2742
2743        assert_eq!(checkpoint.stage_index, 0);
2744        assert!(checkpoint.context.data.is_empty());
2745    }
2746
2747    #[test]
2748    fn test_pipeline_run_single_stage() {
2749        let mut pipeline = BrickPipeline::new("single").stage(TestStage {
2750            name: "only",
2751            should_fail: false,
2752        });
2753
2754        let ctx = PipelineContext::new();
2755        let result = pipeline.run(ctx).unwrap();
2756
2757        assert!(result.get("only_output").is_some());
2758        assert_eq!(result.trace.len(), 1);
2759    }
2760
2761    #[test]
2762    fn test_pipeline_first_stage_fails() {
2763        let mut pipeline = BrickPipeline::new("first-fail").stage(TestStage {
2764            name: "first",
2765            should_fail: true,
2766        });
2767
2768        let ctx = PipelineContext::new();
2769        let result = pipeline.run(ctx);
2770
2771        assert!(result.is_err());
2772        match result {
2773            Err(PipelineError::ExecutionFailed { stage, .. }) => {
2774                assert_eq!(stage, "first");
2775            }
2776            _ => panic!("Expected ExecutionFailed"),
2777        }
2778    }
2779
2780    #[test]
2781    fn test_pipeline_first_stage_validation_fails() {
2782        let mut pipeline = BrickPipeline::new("first-val-fail")
2783            .stage(FailingValidationStage { name: "first_fail" });
2784
2785        let ctx = PipelineContext::new();
2786        let result = pipeline.run(ctx);
2787
2788        assert!(result.is_err());
2789        match result {
2790            Err(PipelineError::ValidationFailed { stage, .. }) => {
2791                assert_eq!(stage, "first_fail");
2792            }
2793            _ => panic!("Expected ValidationFailed"),
2794        }
2795    }
2796
2797    #[test]
2798    fn test_pipeline_checkpoint_skip_first_stage() {
2799        let mut pipeline = BrickPipeline::new("skip-first")
2800            .stage(TestStage {
2801                name: "skipped",
2802                should_fail: false,
2803            })
2804            .stage(TestStage {
2805                name: "executed",
2806                should_fail: false,
2807            });
2808
2809        // Set checkpoint to skip first stage
2810        pipeline.last_checkpoint = Some(Checkpoint {
2811            stage_index: 1,
2812            context: PipelineContext::from_input("from_checkpoint", PipelineData::Bool(true)),
2813            created_at: Instant::now(),
2814        });
2815
2816        let ctx = PipelineContext::new();
2817        let result = pipeline.run(ctx).unwrap();
2818
2819        // skipped_output should NOT be present
2820        assert!(result.get("skipped_output").is_none());
2821        // executed_output should be present
2822        assert!(result.get("executed_output").is_some());
2823        // Checkpoint data should be present
2824        assert!(result.get("from_checkpoint").is_some());
2825    }
2826
2827    #[test]
2828    fn test_pipeline_all_stages_skipped_by_checkpoint() {
2829        let mut pipeline = BrickPipeline::new("all-skipped")
2830            .stage(TestStage {
2831                name: "s1",
2832                should_fail: false,
2833            })
2834            .stage(TestStage {
2835                name: "s2",
2836                should_fail: false,
2837            });
2838
2839        // Set checkpoint to skip all stages
2840        pipeline.last_checkpoint = Some(Checkpoint {
2841            stage_index: 2, // Skip all
2842            context: PipelineContext::from_input("final_data", PipelineData::Int(999)),
2843            created_at: Instant::now(),
2844        });
2845
2846        let ctx = PipelineContext::new();
2847        let result = pipeline.run(ctx).unwrap();
2848
2849        // No stage outputs should be present
2850        assert!(result.get("s1_output").is_none());
2851        assert!(result.get("s2_output").is_none());
2852        // Checkpoint data should be present
2853        assert!(result.get("final_data").is_some());
2854    }
2855
2856    #[test]
2857    fn test_pipeline_with_many_stages() {
2858        let mut pipeline = BrickPipeline::new("many-stages");
2859
2860        for i in 0..10 {
2861            pipeline = pipeline.stage(TestStage {
2862                name: Box::leak(format!("stage_{}", i).into_boxed_str()),
2863                should_fail: false,
2864            });
2865        }
2866
2867        assert_eq!(pipeline.stage_count(), 10);
2868
2869        let ctx = PipelineContext::new();
2870        let result = pipeline.run(ctx).unwrap();
2871
2872        assert_eq!(result.trace.len(), 10);
2873    }
2874
2875    #[test]
2876    fn test_pipeline_error_std_error_trait() {
2877        let err = PipelineError::MissingInput {
2878            stage: "s".to_string(),
2879            input: "i".to_string(),
2880        };
2881
2882        // Test that it implements std::error::Error
2883        fn accepts_error<E: std::error::Error>(_e: &E) {}
2884        accepts_error(&err);
2885
2886        // source() should return None for this error type
2887        assert!(std::error::Error::source(&err).is_none());
2888    }
2889
2890    #[test]
2891    fn test_pipeline_context_set_overwrite() {
2892        let mut ctx = PipelineContext::new();
2893
2894        ctx.set("key", PipelineData::Int(1));
2895        assert!(matches!(ctx.get("key"), Some(PipelineData::Int(1))));
2896
2897        ctx.set("key", PipelineData::Int(2));
2898        assert!(matches!(ctx.get("key"), Some(PipelineData::Int(2))));
2899
2900        ctx.set("key", PipelineData::Text("text".into()));
2901        assert!(matches!(ctx.get("key"), Some(PipelineData::Text(_))));
2902    }
2903
2904    #[test]
2905    fn test_stage_trace_zero_duration() {
2906        let trace = StageTrace {
2907            stage_name: "instant".to_string(),
2908            duration: Duration::ZERO,
2909            success: true,
2910            error: None,
2911        };
2912
2913        assert_eq!(trace.duration, Duration::ZERO);
2914    }
2915
2916    #[test]
2917    fn test_pipeline_with_privacy_and_checkpointing() {
2918        let pipeline = BrickPipeline::new("full-config")
2919            .with_privacy(PrivacyTier::Sovereign)
2920            .with_checkpointing(Duration::from_secs(10))
2921            .stage(TestStage {
2922                name: "s1",
2923                should_fail: false,
2924            });
2925
2926        assert_eq!(pipeline.privacy_tier(), PrivacyTier::Sovereign);
2927        assert_eq!(pipeline.stage_count(), 1);
2928    }
2929
2930    #[test]
2931    fn test_pipeline_json_data_complex() {
2932        let complex_json = serde_json::json!({
2933            "array": [1, 2, 3],
2934            "nested": {
2935                "key": "value",
2936                "number": 42
2937            },
2938            "boolean": true,
2939            "null_value": null
2940        });
2941
2942        let data = PipelineData::Json(complex_json);
2943
2944        if let PipelineData::Json(value) = data {
2945            assert_eq!(value["array"][0], 1);
2946            assert_eq!(value["nested"]["key"], "value");
2947        } else {
2948            panic!("Expected Json variant");
2949        }
2950    }
2951
2952    #[test]
2953    fn test_pipeline_bytes_large() {
2954        let large_bytes: Vec<u8> = (0..=255).collect();
2955        let data = PipelineData::Bytes(large_bytes);
2956
2957        if let PipelineData::Bytes(bytes) = data {
2958            assert_eq!(bytes.len(), 256);
2959            assert_eq!(bytes[0], 0);
2960            assert_eq!(bytes[255], 255);
2961        } else {
2962            panic!("Expected Bytes variant");
2963        }
2964    }
2965
2966    #[test]
2967    fn test_validation_result_fail_empty_reason() {
2968        let result = ValidationResult::fail("");
2969
2970        assert!(!result.valid);
2971        assert_eq!(result.messages[0].message, "");
2972    }
2973
2974    #[test]
2975    fn test_pipeline_context_from_input_preserves_metadata() {
2976        let ctx = PipelineContext::from_input("key", PipelineData::Bool(false));
2977
2978        assert!(ctx.metadata.run_id.starts_with("run-"));
2979        assert!(ctx.trace.is_empty());
2980    }
2981
2982    #[test]
2983    fn test_pipeline_stage_middle_fails() {
2984        let mut pipeline = BrickPipeline::new("middle-fail")
2985            .stage(TestStage {
2986                name: "first",
2987                should_fail: false,
2988            })
2989            .stage(TestStage {
2990                name: "middle",
2991                should_fail: true,
2992            })
2993            .stage(TestStage {
2994                name: "last",
2995                should_fail: false,
2996            });
2997
2998        let ctx = PipelineContext::new();
2999        let result = pipeline.run(ctx);
3000
3001        assert!(result.is_err());
3002
3003        // Audit trail should have 2 entries (first success, middle fail)
3004        let trail = pipeline.audit_trail();
3005        assert_eq!(trail.len(), 2);
3006    }
3007
3008    #[test]
3009    fn test_pipeline_stage_last_fails() {
3010        let mut pipeline = BrickPipeline::new("last-fail")
3011            .stage(TestStage {
3012                name: "first",
3013                should_fail: false,
3014            })
3015            .stage(TestStage {
3016                name: "second",
3017                should_fail: false,
3018            })
3019            .stage(TestStage {
3020                name: "last",
3021                should_fail: true,
3022            });
3023
3024        let ctx = PipelineContext::new();
3025        let result = pipeline.run(ctx);
3026
3027        assert!(result.is_err());
3028
3029        let trail = pipeline.audit_trail();
3030        assert_eq!(trail.len(), 3);
3031        assert!(trail[0].success);
3032        assert!(trail[1].success);
3033        assert!(!trail[2].success);
3034    }
3035
3036    #[test]
3037    fn test_pipeline_checkpoint_at_exact_interval() {
3038        // Test checkpoint creation at exactly the interval boundary
3039        let mut pipeline = BrickPipeline::new("exact-interval")
3040            .with_checkpointing(Duration::from_millis(0)) // Immediate checkpoint
3041            .stage(TestStage {
3042                name: "s1",
3043                should_fail: false,
3044            })
3045            .stage(TestStage {
3046                name: "s2",
3047                should_fail: false,
3048            });
3049
3050        let ctx = PipelineContext::new();
3051        let result = pipeline.run(ctx);
3052
3053        assert!(result.is_ok());
3054        // Checkpoint should be cleared after successful run
3055        assert!(pipeline.last_checkpoint.is_none());
3056    }
3057
3058    #[test]
3059    fn test_validation_level_copy_and_clone() {
3060        let info = ValidationLevel::Info;
3061        let copied = info;
3062        let cloned = copied;
3063
3064        assert_eq!(info, copied);
3065        assert_eq!(copied, cloned);
3066    }
3067
3068    #[test]
3069    fn test_privacy_tier_all_variants_equality() {
3070        let tiers = [
3071            PrivacyTier::Sovereign,
3072            PrivacyTier::Private,
3073            PrivacyTier::Standard,
3074        ];
3075
3076        for (i, tier1) in tiers.iter().enumerate() {
3077            for (j, tier2) in tiers.iter().enumerate() {
3078                if i == j {
3079                    assert_eq!(tier1, tier2);
3080                } else {
3081                    assert_ne!(tier1, tier2);
3082                }
3083            }
3084        }
3085    }
3086
3087    #[test]
3088    fn test_pipeline_metadata_started_at_is_set() {
3089        let mut pipeline = BrickPipeline::new("started").stage(TestStage {
3090            name: "s",
3091            should_fail: false,
3092        });
3093
3094        let ctx = PipelineContext::new();
3095        assert!(ctx.metadata.started_at.is_none());
3096
3097        let result = pipeline.run(ctx).unwrap();
3098        assert!(result.metadata.started_at.is_some());
3099    }
3100
3101    #[test]
3102    fn test_pipeline_tensor_high_dimensional() {
3103        let data = PipelineData::tensor(
3104            vec![1.0; 24], // 2 * 3 * 4 = 24 elements
3105            vec![2, 3, 4],
3106        );
3107
3108        let (values, shape) = data.as_tensor().unwrap();
3109        assert_eq!(values.len(), 24);
3110        assert_eq!(shape.len(), 3);
3111    }
3112
3113    #[test]
3114    fn test_pipeline_context_debug() {
3115        let ctx = PipelineContext::from_input("debug_key", PipelineData::Int(42));
3116        let debug_str = format!("{:?}", ctx);
3117
3118        assert!(debug_str.contains("PipelineContext"));
3119        assert!(debug_str.contains("debug_key"));
3120    }
3121
3122    #[test]
3123    fn test_stage_trace_long_error_message() {
3124        let long_error = "Error ".repeat(1000);
3125        let trace = StageTrace {
3126            stage_name: "long_error".to_string(),
3127            duration: Duration::from_millis(1),
3128            success: false,
3129            error: Some(long_error.clone()),
3130        };
3131
3132        assert_eq!(trace.error.as_ref().unwrap().len(), long_error.len());
3133    }
3134
3135    #[test]
3136    fn test_pipeline_with_checkpoint_and_failure() {
3137        let mut pipeline = BrickPipeline::new("checkpoint-fail")
3138            .with_checkpointing(Duration::from_nanos(1))
3139            .stage(SlowStage {
3140                name: "slow",
3141                delay_ms: 2,
3142            })
3143            .stage(TestStage {
3144                name: "fail",
3145                should_fail: true,
3146            });
3147
3148        let ctx = PipelineContext::new();
3149        let result = pipeline.run(ctx);
3150
3151        assert!(result.is_err());
3152    }
3153
3154    #[test]
3155    fn test_audit_collector_single_entry_duration() {
3156        let mut collector = PipelineAuditCollector::new();
3157        collector.record("single", Duration::from_secs(5), true);
3158
3159        assert_eq!(collector.total_duration(), Duration::from_secs(5));
3160    }
3161}