clnrm_core/validation/
span_validator.rs

1//! Span validator for OTEL self-testing
2//!
3//! Validates that clnrm produced expected OTEL spans to prove functionality.
4//! This enables "testing via telemetry" - validating framework behavior by
5//! analyzing the spans it emitted.
6
7use crate::error::{CleanroomError, Result};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use std::path::Path;
11
12/// OTEL span kind enumeration
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(rename_all = "lowercase")]
15pub enum SpanKind {
16    /// Internal span (default)
17    Internal,
18    /// Server span (receiving a request)
19    Server,
20    /// Client span (making a request)
21    Client,
22    /// Producer span (message queue producer)
23    Producer,
24    /// Consumer span (message queue consumer)
25    Consumer,
26}
27
28impl SpanKind {
29    /// Parse span kind from string (custom parser, not std::str::FromStr trait)
30    pub fn parse_kind(s: &str) -> Result<Self> {
31        match s.to_lowercase().as_str() {
32            "internal" => Ok(SpanKind::Internal),
33            "server" => Ok(SpanKind::Server),
34            "client" => Ok(SpanKind::Client),
35            "producer" => Ok(SpanKind::Producer),
36            "consumer" => Ok(SpanKind::Consumer),
37            _ => Err(CleanroomError::validation_error(format!(
38                "Invalid span kind: '{}'. Must be one of: internal, server, client, producer, consumer",
39                s
40            ))),
41        }
42    }
43
44    /// Convert to OTEL integer representation
45    pub fn to_otel_int(&self) -> i32 {
46        match self {
47            SpanKind::Internal => 1,
48            SpanKind::Server => 2,
49            SpanKind::Client => 3,
50            SpanKind::Producer => 4,
51            SpanKind::Consumer => 5,
52        }
53    }
54
55    /// Parse from OTEL integer representation
56    pub fn from_otel_int(i: i32) -> Result<Self> {
57        match i {
58            1 => Ok(SpanKind::Internal),
59            2 => Ok(SpanKind::Server),
60            3 => Ok(SpanKind::Client),
61            4 => Ok(SpanKind::Producer),
62            5 => Ok(SpanKind::Consumer),
63            _ => Err(CleanroomError::validation_error(format!(
64                "Invalid OTEL span kind integer: {}",
65                i
66            ))),
67        }
68    }
69}
70
71/// Represents a single OTEL span from the collector's file exporter
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct SpanData {
74    /// Span name (e.g., "clnrm.run", "clnrm.test")
75    pub name: String,
76    /// Span attributes as key-value pairs
77    pub attributes: HashMap<String, serde_json::Value>,
78    /// Trace ID this span belongs to
79    pub trace_id: String,
80    /// Span ID
81    pub span_id: String,
82    /// Parent span ID (if any)
83    pub parent_span_id: Option<String>,
84    /// Span start time (Unix timestamp in nanoseconds)
85    pub start_time_unix_nano: Option<u64>,
86    /// Span end time (Unix timestamp in nanoseconds)
87    pub end_time_unix_nano: Option<u64>,
88    /// Span kind (internal, server, client, producer, consumer)
89    pub kind: Option<SpanKind>,
90    /// Span events (array of event names)
91    pub events: Option<Vec<String>>,
92    /// Resource attributes (shared across all spans in a resource)
93    #[serde(default)]
94    pub resource_attributes: HashMap<String, serde_json::Value>,
95}
96
97impl SpanData {
98    /// Calculate span duration in milliseconds
99    pub fn duration_ms(&self) -> Option<f64> {
100        match (self.start_time_unix_nano, self.end_time_unix_nano) {
101            (Some(start), Some(end)) => {
102                if end >= start {
103                    Some((end - start) as f64 / 1_000_000.0)
104                } else {
105                    None
106                }
107            }
108            _ => None,
109        }
110    }
111}
112
113/// Assertion types for span validation
114#[derive(Debug, Clone)]
115pub enum SpanAssertion {
116    /// Assert a span with given name exists
117    SpanExists { name: String },
118    /// Assert exact count of spans with given name
119    SpanCount { name: String, count: usize },
120    /// Assert span has specific attribute with value
121    SpanAttribute {
122        name: String,
123        attribute_key: String,
124        attribute_value: String,
125    },
126    /// Assert span hierarchy (parent-child relationship)
127    SpanHierarchy { parent: String, child: String },
128
129    // NEW PRD-aligned assertions
130    /// Assert span kind (internal, server, client, producer, consumer)
131    SpanKind { name: String, kind: SpanKind },
132
133    /// Assert all attributes match (attrs.all from PRD)
134    /// All key-value pairs must be present in the span
135    SpanAllAttributes {
136        name: String,
137        attributes: HashMap<String, String>,
138    },
139
140    /// Assert at least one attribute pattern matches (attrs.any from PRD)
141    /// Patterns are in format "key=value"
142    /// At least one pattern must match
143    SpanAnyAttributes {
144        name: String,
145        attribute_patterns: Vec<String>,
146    },
147
148    /// Assert span has specific events (events.any from PRD)
149    /// At least one event name must be present
150    SpanEvents { name: String, events: Vec<String> },
151
152    /// Assert span duration is within bounds (duration_ms from PRD)
153    /// Both min and max are optional
154    SpanDuration {
155        name: String,
156        min_ms: Option<u64>,
157        max_ms: Option<u64>,
158    },
159}
160
161/// Validation failure details for precise error reporting
162#[derive(Debug, Clone, Serialize, Deserialize)]
163pub struct FailureDetails {
164    /// Rule that failed (e.g., "expect.span[clnrm.run].attrs.all")
165    pub rule: String,
166    /// Span name that was validated
167    pub span_name: String,
168    /// Expected value
169    pub expected: String,
170    /// Actual value (if any)
171    pub actual: Option<String>,
172    /// Human-readable error message
173    pub message: String,
174}
175
176/// Validation result with detailed pass/fail information
177#[derive(Debug, Clone, Serialize, Deserialize)]
178pub struct ValidationResult {
179    /// Whether all validations passed
180    pub passed: bool,
181    /// List of failures (empty if passed)
182    pub failures: Vec<FailureDetails>,
183    /// Number of validations performed
184    pub validations_count: usize,
185}
186
187impl ValidationResult {
188    /// Create a successful validation result
189    pub fn success(validations_count: usize) -> Self {
190        Self {
191            passed: true,
192            failures: Vec::new(),
193            validations_count,
194        }
195    }
196
197    /// Create a failed validation result
198    pub fn failure(failure: FailureDetails) -> Self {
199        Self {
200            passed: false,
201            failures: vec![failure],
202            validations_count: 1,
203        }
204    }
205
206    /// Add a failure to the result
207    pub fn add_failure(&mut self, failure: FailureDetails) {
208        self.passed = false;
209        self.failures.push(failure);
210    }
211
212    /// Merge multiple validation results
213    pub fn merge(results: Vec<ValidationResult>) -> Self {
214        let passed = results.iter().all(|r| r.passed);
215        let failures: Vec<FailureDetails> = results
216            .iter()
217            .flat_map(|r| r.failures.clone())
218            .collect();
219        let validations_count: usize = results.iter().map(|r| r.validations_count).sum();
220
221        Self {
222            passed,
223            failures,
224            validations_count,
225        }
226    }
227}
228
229/// Span validator for OTEL self-testing
230pub struct SpanValidator {
231    /// Loaded span data from OTEL collector export
232    pub(crate) spans: Vec<SpanData>,
233}
234
235impl SpanValidator {
236    /// Create a new SpanValidator by loading spans from a JSON file
237    ///
238    /// The file should be in the format produced by OTEL collector's file exporter.
239    ///
240    /// # Arguments
241    /// * `path` - Path to the spans JSON file
242    ///
243    /// # Returns
244    /// * `Result<Self>` - SpanValidator instance or error
245    ///
246    /// # Errors
247    /// * File read errors
248    /// * JSON parsing errors
249    pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
250        let content = std::fs::read_to_string(path.as_ref()).map_err(|e| {
251            CleanroomError::config_error(format!("Failed to read spans file: {}", e))
252        })?;
253
254        Self::from_json(&content)
255    }
256
257    /// Create a new SpanValidator from JSON string
258    ///
259    /// # Arguments
260    /// * `json` - JSON string containing span data
261    ///
262    /// # Returns
263    /// * `Result<Self>` - SpanValidator instance or error
264    ///
265    /// # Errors
266    /// * JSON parsing errors
267    pub fn from_json(json: &str) -> Result<Self> {
268        // OTEL file exporter produces newline-delimited JSON (NDJSON)
269        // Each line is a complete JSON object representing one or more spans
270        let mut all_spans = Vec::new();
271
272        for line in json.lines() {
273            let line = line.trim();
274            if line.is_empty() {
275                continue;
276            }
277
278            // Try parsing as an array of spans first
279            if let Ok(spans) = serde_json::from_str::<Vec<SpanData>>(line) {
280                all_spans.extend(spans);
281            } else if let Ok(span) = serde_json::from_str::<SpanData>(line) {
282                // Single span
283                all_spans.push(span);
284            } else {
285                // Try parsing as OTEL JSON format with resource spans
286                if let Ok(value) = serde_json::from_str::<serde_json::Value>(line) {
287                    if let Some(spans) = Self::extract_spans_from_otel_format(&value) {
288                        all_spans.extend(spans);
289                    }
290                }
291            }
292        }
293
294        Ok(Self { spans: all_spans })
295    }
296
297    /// Extract spans from OTEL collector JSON format
298    fn extract_spans_from_otel_format(value: &serde_json::Value) -> Option<Vec<SpanData>> {
299        let mut spans = Vec::new();
300
301        // Navigate OTEL structure: resourceSpans -> scopeSpans -> spans
302        if let Some(resource_spans) = value.get("resourceSpans").and_then(|v| v.as_array()) {
303            for resource_span in resource_spans {
304                if let Some(scope_spans) =
305                    resource_span.get("scopeSpans").and_then(|v| v.as_array())
306                {
307                    for scope_span in scope_spans {
308                        if let Some(span_array) = scope_span.get("spans").and_then(|v| v.as_array())
309                        {
310                            for span_obj in span_array {
311                                if let Some(span) = Self::parse_otel_span(span_obj) {
312                                    spans.push(span);
313                                }
314                            }
315                        }
316                    }
317                }
318            }
319        }
320
321        if spans.is_empty() {
322            None
323        } else {
324            Some(spans)
325        }
326    }
327
328    /// Parse a single OTEL span object
329    fn parse_otel_span(span_obj: &serde_json::Value) -> Option<SpanData> {
330        let name = span_obj.get("name")?.as_str()?.to_string();
331        let trace_id = span_obj.get("traceId")?.as_str()?.to_string();
332        let span_id = span_obj.get("spanId")?.as_str()?.to_string();
333        let parent_span_id = span_obj
334            .get("parentSpanId")
335            .and_then(|v| v.as_str())
336            .map(|s| s.to_string());
337
338        let start_time_unix_nano = span_obj
339            .get("startTimeUnixNano")
340            .and_then(|v| v.as_str())
341            .and_then(|s| s.parse::<u64>().ok());
342
343        let end_time_unix_nano = span_obj
344            .get("endTimeUnixNano")
345            .and_then(|v| v.as_str())
346            .and_then(|s| s.parse::<u64>().ok());
347
348        // Parse span kind
349        let kind = span_obj
350            .get("kind")
351            .and_then(|v| v.as_i64())
352            .and_then(|i| SpanKind::from_otel_int(i as i32).ok());
353
354        // Parse attributes
355        let mut attributes = HashMap::new();
356        if let Some(attrs) = span_obj.get("attributes").and_then(|v| v.as_array()) {
357            for attr in attrs {
358                if let (Some(key), Some(value)) =
359                    (attr.get("key").and_then(|k| k.as_str()), attr.get("value"))
360                {
361                    attributes.insert(key.to_string(), value.clone());
362                }
363            }
364        }
365
366        // Parse events
367        let events = span_obj
368            .get("events")
369            .and_then(|v| v.as_array())
370            .map(|events_array| {
371                events_array
372                    .iter()
373                    .filter_map(|event| {
374                        event.get("name").and_then(|n| n.as_str()).map(String::from)
375                    })
376                    .collect()
377            });
378
379        Some(SpanData {
380            name,
381            attributes,
382            trace_id,
383            span_id,
384            parent_span_id,
385            start_time_unix_nano,
386            end_time_unix_nano,
387            kind,
388            events,
389            resource_attributes: HashMap::new(),
390        })
391    }
392
393    /// Get all spans
394    pub fn spans(&self) -> &[SpanData] {
395        &self.spans
396    }
397
398    /// Find spans by name
399    pub fn find_spans_by_name(&self, name: &str) -> Vec<&SpanData> {
400        self.spans.iter().filter(|s| s.name == name).collect()
401    }
402
403    /// Find span by exact trace ID and span ID
404    pub fn find_span(&self, trace_id: &str, span_id: &str) -> Option<&SpanData> {
405        self.spans
406            .iter()
407            .find(|s| s.trace_id == trace_id && s.span_id == span_id)
408    }
409
410    /// Check if a span with the given name exists
411    pub fn has_span(&self, name: &str) -> bool {
412        self.spans.iter().any(|s| s.name == name)
413    }
414
415    /// Count spans with given name
416    pub fn count_spans(&self, name: &str) -> usize {
417        self.spans.iter().filter(|s| s.name == name).count()
418    }
419
420    /// Validate spans against PRD-style expectations with detailed error tracking
421    ///
422    /// This is the primary validation method that processes SpanExpectationConfig
423    /// from TOML and returns structured validation results.
424    ///
425    /// # Arguments
426    /// * `expectations` - List of span expectations from TOML `[[expect.span]]` blocks
427    ///
428    /// # Returns
429    /// * `Result<ValidationResult>` - Detailed validation results with failure tracking
430    pub fn validate_expectations(
431        &self,
432        expectations: &[crate::config::SpanExpectationConfig],
433    ) -> Result<ValidationResult> {
434        let mut results = Vec::new();
435
436        for expectation in expectations {
437            let result = self.validate_single_expectation(expectation)?;
438            results.push(result);
439        }
440
441        Ok(ValidationResult::merge(results))
442    }
443
444    /// Validate a single span expectation
445    fn validate_single_expectation(
446        &self,
447        expectation: &crate::config::SpanExpectationConfig,
448    ) -> Result<ValidationResult> {
449        let span_name = &expectation.name;
450
451        // 1. Check span existence
452        let matching_spans = self.find_spans_by_name(span_name);
453        if matching_spans.is_empty() {
454            return Ok(ValidationResult::failure(FailureDetails {
455                rule: format!("expect.span[{}].existence", span_name),
456                span_name: span_name.clone(),
457                expected: format!("Span '{}' to exist", span_name),
458                actual: None,
459                message: format!("Span '{}' not found in trace", span_name),
460            }));
461        }
462
463        let mut validation_count = 1; // existence check
464        let mut failures = Vec::new();
465
466        // Find first matching span for detailed validation
467        // In production, we may want to validate all matching spans
468        let span = matching_spans[0];
469
470        // 2. Validate parent relationship
471        if let Some(ref parent_name) = expectation.parent {
472            validation_count += 1;
473            if let Some(failure) = self.validate_parent_relationship(span, parent_name, span_name)
474            {
475                failures.push(failure);
476            }
477        }
478
479        // 3. Validate span kind
480        if let Some(ref kind_str) = expectation.kind {
481            validation_count += 1;
482            if let Some(failure) = self.validate_span_kind(span, kind_str, span_name)? {
483                failures.push(failure);
484            }
485        }
486
487        // 4. Validate attributes
488        if let Some(ref attrs_config) = expectation.attrs {
489            // attrs.all - ALL attributes must match
490            if let Some(ref all_attrs) = attrs_config.all {
491                validation_count += all_attrs.len();
492                if let Some(failure) = self.validate_attrs_all(span, all_attrs, span_name) {
493                    failures.push(failure);
494                }
495            }
496
497            // attrs.any - At least ONE attribute must match
498            if let Some(ref any_attrs) = attrs_config.any {
499                validation_count += 1;
500                if let Some(failure) = self.validate_attrs_any(span, any_attrs, span_name) {
501                    failures.push(failure);
502                }
503            }
504        }
505
506        // 5. Validate events
507        if let Some(ref events_config) = expectation.events {
508            if let Some(ref any_events) = events_config.any {
509                validation_count += 1;
510                if let Some(failure) = self.validate_events_any(span, any_events, span_name) {
511                    failures.push(failure);
512                }
513            }
514
515            if let Some(ref all_events) = events_config.all {
516                validation_count += all_events.len();
517                if let Some(failure) = self.validate_events_all(span, all_events, span_name) {
518                    failures.push(failure);
519                }
520            }
521        }
522
523        // 6. Validate duration
524        if let Some(ref duration_config) = expectation.duration_ms {
525            validation_count += 1;
526            if let Some(failure) = self.validate_duration(span, duration_config, span_name) {
527                failures.push(failure);
528            }
529        }
530
531        if failures.is_empty() {
532            Ok(ValidationResult::success(validation_count))
533        } else {
534            Ok(ValidationResult {
535                passed: false,
536                failures,
537                validations_count: validation_count,
538            })
539        }
540    }
541
542    /// Validate parent relationship
543    fn validate_parent_relationship(
544        &self,
545        span: &SpanData,
546        parent_name: &str,
547        span_name: &str,
548    ) -> Option<FailureDetails> {
549        // Find parent spans by name
550        let parent_spans = self.find_spans_by_name(parent_name);
551        if parent_spans.is_empty() {
552            return Some(FailureDetails {
553                rule: format!("expect.span[{}].parent", span_name),
554                span_name: span_name.to_string(),
555                expected: format!("Parent span '{}'", parent_name),
556                actual: None,
557                message: format!(
558                    "Parent span '{}' not found for span '{}'",
559                    parent_name, span_name
560                ),
561            });
562        }
563
564        // Check if span has a parent_span_id matching any of the parent spans
565        if let Some(ref parent_id) = span.parent_span_id {
566            if parent_spans.iter().any(|p| &p.span_id == parent_id) {
567                return None; // Valid parent relationship
568            }
569
570            // Parent exists but IDs don't match
571            Some(FailureDetails {
572                rule: format!("expect.span[{}].parent", span_name),
573                span_name: span_name.to_string(),
574                expected: format!("Parent span '{}'", parent_name),
575                actual: Some(format!("Different parent (ID: {})", parent_id)),
576                message: format!(
577                    "Span '{}' parent mismatch: expected '{}', found different parent",
578                    span_name, parent_name
579                ),
580            })
581        } else {
582            // Span has no parent
583            Some(FailureDetails {
584                rule: format!("expect.span[{}].parent", span_name),
585                span_name: span_name.to_string(),
586                expected: format!("Parent span '{}'", parent_name),
587                actual: Some("none".to_string()),
588                message: format!(
589                    "Span '{}' parent mismatch: expected '{}', found none",
590                    span_name, parent_name
591                ),
592            })
593        }
594    }
595
596    /// Validate span kind
597    fn validate_span_kind(
598        &self,
599        span: &SpanData,
600        kind_str: &str,
601        span_name: &str,
602    ) -> Result<Option<FailureDetails>> {
603        let expected_kind = SpanKind::parse_kind(kind_str)?;
604
605        match span.kind {
606            Some(actual_kind) if actual_kind == expected_kind => Ok(None),
607            Some(actual_kind) => Ok(Some(FailureDetails {
608                rule: format!("expect.span[{}].kind", span_name),
609                span_name: span_name.to_string(),
610                expected: format!("{:?}", expected_kind),
611                actual: Some(format!("{:?}", actual_kind)),
612                message: format!(
613                    "Span '{}' kind mismatch: expected {:?}, found {:?}",
614                    span_name, expected_kind, actual_kind
615                ),
616            })),
617            None => Ok(Some(FailureDetails {
618                rule: format!("expect.span[{}].kind", span_name),
619                span_name: span_name.to_string(),
620                expected: format!("{:?}", expected_kind),
621                actual: None,
622                message: format!(
623                    "Span '{}' kind mismatch: expected {:?}, found none",
624                    span_name, expected_kind
625                ),
626            })),
627        }
628    }
629
630    /// Validate attrs.all - ALL attributes must match exactly
631    fn validate_attrs_all(
632        &self,
633        span: &SpanData,
634        all_attrs: &HashMap<String, String>,
635        span_name: &str,
636    ) -> Option<FailureDetails> {
637        let mut missing = Vec::new();
638
639        for (key, expected_value) in all_attrs {
640            let matches = span
641                .attributes
642                .get(key)
643                .and_then(|v| v.as_str())
644                .map(|v| v == expected_value)
645                .unwrap_or(false);
646
647            if !matches {
648                let actual = span
649                    .attributes
650                    .get(key)
651                    .and_then(|v| v.as_str())
652                    .map(|s| s.to_string());
653
654                if actual.is_none() {
655                    missing.push(format!("{}={}", key, expected_value));
656                } else {
657                    missing.push(format!(
658                        "{}={} (found: {})",
659                        key,
660                        expected_value,
661                        actual.unwrap_or_default()
662                    ));
663                }
664            }
665        }
666
667        if missing.is_empty() {
668            None
669        } else {
670            Some(FailureDetails {
671                rule: format!("expect.span[{}].attrs.all", span_name),
672                span_name: span_name.to_string(),
673                expected: format!("All attributes: {:?}", all_attrs),
674                actual: Some(format!("Missing/incorrect: [{}]", missing.join(", "))),
675                message: format!(
676                    "Span '{}' missing required attributes: [{}]",
677                    span_name,
678                    missing.join(", ")
679                ),
680            })
681        }
682    }
683
684    /// Validate attrs.any - At least ONE attribute must be present
685    fn validate_attrs_any(
686        &self,
687        span: &SpanData,
688        any_attrs: &HashMap<String, String>,
689        span_name: &str,
690    ) -> Option<FailureDetails> {
691        let has_any = any_attrs.iter().any(|(key, expected_value)| {
692            span.attributes
693                .get(key)
694                .and_then(|v| v.as_str())
695                .map(|v| v == expected_value)
696                .unwrap_or(false)
697        });
698
699        if has_any {
700            None
701        } else {
702            let patterns: Vec<String> = any_attrs
703                .iter()
704                .map(|(k, v)| format!("{}={}", k, v))
705                .collect();
706
707            Some(FailureDetails {
708                rule: format!("expect.span[{}].attrs.any", span_name),
709                span_name: span_name.to_string(),
710                expected: format!("Any of: [{}]", patterns.join(", ")),
711                actual: None,
712                message: format!(
713                    "Span '{}' missing any of required attributes: [{}]",
714                    span_name,
715                    patterns.join(", ")
716                ),
717            })
718        }
719    }
720
721    /// Validate events.any - At least ONE event must be present
722    fn validate_events_any(
723        &self,
724        span: &SpanData,
725        any_events: &[String],
726        span_name: &str,
727    ) -> Option<FailureDetails> {
728        if let Some(ref span_events) = span.events {
729            let has_any = any_events.iter().any(|event| span_events.contains(event));
730
731            if has_any {
732                return None;
733            }
734        }
735
736        Some(FailureDetails {
737            rule: format!("expect.span[{}].events.any", span_name),
738            span_name: span_name.to_string(),
739            expected: format!("Any of: [{}]", any_events.join(", ")),
740            actual: span.events.as_ref().map(|events| format!("{:?}", events)),
741            message: format!(
742                "Span '{}' missing required events: [{}]",
743                span_name,
744                any_events.join(", ")
745            ),
746        })
747    }
748
749    /// Validate events.all - ALL events must be present
750    fn validate_events_all(
751        &self,
752        span: &SpanData,
753        all_events: &[String],
754        span_name: &str,
755    ) -> Option<FailureDetails> {
756        if let Some(ref span_events) = span.events {
757            let missing: Vec<&String> = all_events
758                .iter()
759                .filter(|event| !span_events.contains(event))
760                .collect();
761
762            if missing.is_empty() {
763                return None;
764            }
765
766            return Some(FailureDetails {
767                rule: format!("expect.span[{}].events.all", span_name),
768                span_name: span_name.to_string(),
769                expected: format!("All of: [{}]", all_events.join(", ")),
770                actual: Some(format!("Missing: {:?}", missing)),
771                message: format!(
772                    "Span '{}' missing required events: {:?}",
773                    span_name, missing
774                ),
775            });
776        }
777
778        Some(FailureDetails {
779            rule: format!("expect.span[{}].events.all", span_name),
780            span_name: span_name.to_string(),
781            expected: format!("All of: [{}]", all_events.join(", ")),
782            actual: None,
783            message: format!("Span '{}' has no events", span_name),
784        })
785    }
786
787    /// Validate duration constraints
788    fn validate_duration(
789        &self,
790        span: &SpanData,
791        duration_config: &crate::config::DurationBoundConfig,
792        span_name: &str,
793    ) -> Option<FailureDetails> {
794        let duration_ms = span.duration_ms()?;
795
796        // Check minimum duration
797        if let Some(min) = duration_config.min {
798            if duration_ms < min {
799                return Some(FailureDetails {
800                    rule: format!("expect.span[{}].duration_ms.min", span_name),
801                    span_name: span_name.to_string(),
802                    expected: format!("duration >= {}ms", min),
803                    actual: Some(format!("{}ms", duration_ms)),
804                    message: format!(
805                        "Span '{}' duration {}ms < min {}ms",
806                        span_name, duration_ms, min
807                    ),
808                });
809            }
810        }
811
812        // Check maximum duration
813        if let Some(max) = duration_config.max {
814            if duration_ms > max {
815                return Some(FailureDetails {
816                    rule: format!("expect.span[{}].duration_ms.max", span_name),
817                    span_name: span_name.to_string(),
818                    expected: format!("duration <= {}ms", max),
819                    actual: Some(format!("{}ms", duration_ms)),
820                    message: format!(
821                        "Span '{}' duration {}ms > max {}ms",
822                        span_name, duration_ms, max
823                    ),
824                });
825            }
826        }
827
828        None
829    }
830
831    /// Get the first failure from validation results (for error reporting)
832    pub fn first_failure(result: &ValidationResult) -> Option<&FailureDetails> {
833        result.failures.first()
834    }
835
836    /// Validate a single assertion
837    pub fn validate_assertion(&self, assertion: &SpanAssertion) -> Result<()> {
838        match assertion {
839            SpanAssertion::SpanExists { name } => {
840                if !self.has_span(name) {
841                    return Err(CleanroomError::validation_error(format!(
842                        "Span assertion failed: span '{}' does not exist",
843                        name
844                    )));
845                }
846                Ok(())
847            }
848            SpanAssertion::SpanCount { name, count } => {
849                let actual_count = self.count_spans(name);
850                if actual_count != *count {
851                    return Err(CleanroomError::validation_error(format!(
852                        "Span count assertion failed: expected {} spans named '{}', found {}",
853                        count, name, actual_count
854                    )));
855                }
856                Ok(())
857            }
858            SpanAssertion::SpanAttribute {
859                name,
860                attribute_key,
861                attribute_value,
862            } => {
863                let spans = self.find_spans_by_name(name);
864                if spans.is_empty() {
865                    return Err(CleanroomError::validation_error(format!(
866                        "Span attribute assertion failed: span '{}' does not exist",
867                        name
868                    )));
869                }
870
871                // Check if any span has the expected attribute
872                let has_attribute = spans.iter().any(|span| {
873                    // SAFE: unwrap_or with safe default (false) - missing attribute means no match
874                    span.attributes
875                        .get(attribute_key)
876                        .and_then(|v| v.as_str())
877                        .map(|v| v == attribute_value)
878                        .unwrap_or(false)
879                });
880
881                if !has_attribute {
882                    return Err(CleanroomError::validation_error(format!(
883                        "Span attribute assertion failed: no span '{}' has attribute '{}' = '{}'",
884                        name, attribute_key, attribute_value
885                    )));
886                }
887                Ok(())
888            }
889            SpanAssertion::SpanHierarchy { parent, child } => {
890                let parent_spans = self.find_spans_by_name(parent);
891                let child_spans = self.find_spans_by_name(child);
892
893                if parent_spans.is_empty() {
894                    return Err(CleanroomError::validation_error(format!(
895                        "Span hierarchy assertion failed: parent span '{}' does not exist",
896                        parent
897                    )));
898                }
899
900                if child_spans.is_empty() {
901                    return Err(CleanroomError::validation_error(format!(
902                        "Span hierarchy assertion failed: child span '{}' does not exist",
903                        child
904                    )));
905                }
906
907                // Check if any child span has any of the parent spans as its parent
908                let has_hierarchy = child_spans.iter().any(|child_span| {
909                    if let Some(parent_id) = &child_span.parent_span_id {
910                        parent_spans.iter().any(|p| &p.span_id == parent_id)
911                    } else {
912                        false
913                    }
914                });
915
916                if !has_hierarchy {
917                    return Err(CleanroomError::validation_error(format!(
918                        "Span hierarchy assertion failed: no '{}' span is a child of '{}' span",
919                        child, parent
920                    )));
921                }
922                Ok(())
923            }
924
925            // NEW PRD-aligned assertion implementations
926            SpanAssertion::SpanKind { name, kind } => {
927                let spans = self.find_spans_by_name(name);
928                if spans.is_empty() {
929                    return Err(CleanroomError::validation_error(format!(
930                        "Span kind assertion failed: span '{}' does not exist",
931                        name
932                    )));
933                }
934
935                // Check if any span has the expected kind
936                // SAFE: unwrap_or with safe default (false) - missing kind means no match
937                let has_kind = spans
938                    .iter()
939                    .any(|span| span.kind.map(|k| k == *kind).unwrap_or(false));
940
941                if !has_kind {
942                    return Err(CleanroomError::validation_error(format!(
943                        "Span kind assertion failed: no span '{}' has kind '{:?}'",
944                        name, kind
945                    )));
946                }
947                Ok(())
948            }
949
950            SpanAssertion::SpanAllAttributes { name, attributes } => {
951                let spans = self.find_spans_by_name(name);
952                if spans.is_empty() {
953                    return Err(CleanroomError::validation_error(format!(
954                        "Span all attributes assertion failed: span '{}' does not exist",
955                        name
956                    )));
957                }
958
959                // Check if any span has ALL the expected attributes
960                let has_all_attributes = spans.iter().any(|span| {
961                    attributes.iter().all(|(key, expected_value)| {
962                        // SAFE: unwrap_or with safe default (false) - missing attribute means no match
963                        span.attributes
964                            .get(key)
965                            .and_then(|v| v.as_str())
966                            .map(|v| v == expected_value)
967                            .unwrap_or(false)
968                    })
969                });
970
971                if !has_all_attributes {
972                    let missing: Vec<String> = attributes
973                        .iter()
974                        .filter(|(key, expected_value)| {
975                            !spans.iter().any(|span| {
976                                // SAFE: unwrap_or with safe default (false) - missing attribute means no match
977                                span.attributes
978                                    .get(*key)
979                                    .and_then(|v| v.as_str())
980                                    .map(|v| v == *expected_value)
981                                    .unwrap_or(false)
982                            })
983                        })
984                        .map(|(k, v)| format!("{}={}", k, v))
985                        .collect();
986
987                    return Err(CleanroomError::validation_error(format!(
988                        "Span all attributes assertion failed: span '{}' is missing attributes: [{}]",
989                        name,
990                        missing.join(", ")
991                    )));
992                }
993                Ok(())
994            }
995
996            SpanAssertion::SpanAnyAttributes {
997                name,
998                attribute_patterns,
999            } => {
1000                let spans = self.find_spans_by_name(name);
1001                if spans.is_empty() {
1002                    return Err(CleanroomError::validation_error(format!(
1003                        "Span any attributes assertion failed: span '{}' does not exist",
1004                        name
1005                    )));
1006                }
1007
1008                // Parse patterns and check if ANY pattern matches
1009                let has_any_match = spans.iter().any(|span| {
1010                    attribute_patterns.iter().any(|pattern| {
1011                        if let Some((key, value)) = pattern.split_once('=') {
1012                            // SAFE: unwrap_or with safe default (false) - missing attribute means no match
1013                            span.attributes
1014                                .get(key)
1015                                .and_then(|v| v.as_str())
1016                                .map(|v| v == value)
1017                                .unwrap_or(false)
1018                        } else {
1019                            false
1020                        }
1021                    })
1022                });
1023
1024                if !has_any_match {
1025                    return Err(CleanroomError::validation_error(format!(
1026                        "Span any attributes assertion failed: span '{}' does not have any of the patterns: [{}]",
1027                        name,
1028                        attribute_patterns.join(", ")
1029                    )));
1030                }
1031                Ok(())
1032            }
1033
1034            SpanAssertion::SpanEvents { name, events } => {
1035                let spans = self.find_spans_by_name(name);
1036                if spans.is_empty() {
1037                    return Err(CleanroomError::validation_error(format!(
1038                        "Span events assertion failed: span '{}' does not exist",
1039                        name
1040                    )));
1041                }
1042
1043                // Check if any span has at least one of the expected events
1044                let has_any_event = spans.iter().any(|span| {
1045                    if let Some(span_events) = &span.events {
1046                        events.iter().any(|event| span_events.contains(event))
1047                    } else {
1048                        false
1049                    }
1050                });
1051
1052                if !has_any_event {
1053                    return Err(CleanroomError::validation_error(format!(
1054                        "Span events assertion failed: span '{}' does not have any of the events: [{}]",
1055                        name,
1056                        events.join(", ")
1057                    )));
1058                }
1059                Ok(())
1060            }
1061
1062            SpanAssertion::SpanDuration {
1063                name,
1064                min_ms,
1065                max_ms,
1066            } => {
1067                let spans = self.find_spans_by_name(name);
1068                if spans.is_empty() {
1069                    return Err(CleanroomError::validation_error(format!(
1070                        "Span duration assertion failed: span '{}' does not exist",
1071                        name
1072                    )));
1073                }
1074
1075                // Check if any span has duration within bounds
1076                let has_valid_duration = spans.iter().any(|span| {
1077                    if let Some(duration) = span.duration_ms() {
1078                        let duration_u64 = duration as u64;
1079
1080                        let min_ok = min_ms.map(|min| duration_u64 >= min).unwrap_or(true);
1081                        let max_ok = max_ms.map(|max| duration_u64 <= max).unwrap_or(true);
1082
1083                        min_ok && max_ok
1084                    } else {
1085                        false
1086                    }
1087                });
1088
1089                if !has_valid_duration {
1090                    let bounds = match (min_ms, max_ms) {
1091                        (Some(min), Some(max)) => format!("between {}ms and {}ms", min, max),
1092                        (Some(min), None) => format!("at least {}ms", min),
1093                        (None, Some(max)) => format!("at most {}ms", max),
1094                        (None, None) => "any duration".to_string(),
1095                    };
1096
1097                    return Err(CleanroomError::validation_error(format!(
1098                        "Span duration assertion failed: span '{}' does not have duration {}",
1099                        name, bounds
1100                    )));
1101                }
1102                Ok(())
1103            }
1104        }
1105    }
1106
1107    /// Validate multiple assertions
1108    pub fn validate_assertions(&self, assertions: &[SpanAssertion]) -> Result<()> {
1109        for assertion in assertions {
1110            self.validate_assertion(assertion)?;
1111        }
1112        Ok(())
1113    }
1114
1115    /// Get a span by name (returns first match)
1116    pub fn get_span(&self, name: &str) -> Option<&SpanData> {
1117        self.spans.iter().find(|s| s.name == name)
1118    }
1119
1120    /// Get a span by span_id
1121    pub fn get_span_by_id(&self, span_id: &str) -> Option<&SpanData> {
1122        self.spans.iter().find(|s| s.span_id == span_id)
1123    }
1124
1125    /// Get all spans (for iteration)
1126    pub fn all_spans(&self) -> &[SpanData] {
1127        &self.spans
1128    }
1129}
1130
1131#[cfg(test)]
1132mod tests {
1133    use super::*;
1134    use crate::config::SpanAttributesConfig;
1135
1136    #[test]
1137    fn test_span_validator_from_json_empty() {
1138        // Arrange
1139        let json = "";
1140
1141        // Act
1142        let validator = SpanValidator::from_json(json).unwrap();
1143
1144        // Assert
1145        assert_eq!(validator.spans().len(), 0);
1146    }
1147
1148    #[test]
1149    fn test_span_validator_single_span() {
1150        // Arrange
1151        let json = r#"{"name":"test.span","trace_id":"abc123","span_id":"span1","parent_span_id":null,"attributes":{}}"#;
1152
1153        // Act
1154        let validator = SpanValidator::from_json(json).unwrap();
1155
1156        // Assert
1157        assert_eq!(validator.spans().len(), 1);
1158        assert_eq!(validator.spans()[0].name, "test.span");
1159    }
1160
1161    #[test]
1162    fn test_span_exists_assertion() {
1163        // Arrange
1164        let json = r#"{"name":"clnrm.run","trace_id":"abc123","span_id":"span1","parent_span_id":null,"attributes":{}}"#;
1165        let validator = SpanValidator::from_json(json).unwrap();
1166        let assertion = SpanAssertion::SpanExists {
1167            name: "clnrm.run".to_string(),
1168        };
1169
1170        // Act
1171        let result = validator.validate_assertion(&assertion);
1172
1173        // Assert
1174        assert!(result.is_ok());
1175    }
1176
1177    #[test]
1178    fn test_span_exists_assertion_fails() {
1179        // Arrange
1180        let json = r#"{"name":"clnrm.run","trace_id":"abc123","span_id":"span1","parent_span_id":null,"attributes":{}}"#;
1181        let validator = SpanValidator::from_json(json).unwrap();
1182        let assertion = SpanAssertion::SpanExists {
1183            name: "clnrm.test".to_string(),
1184        };
1185
1186        // Act
1187        let result = validator.validate_assertion(&assertion);
1188
1189        // Assert
1190        assert!(result.is_err());
1191    }
1192
1193    #[test]
1194    fn test_span_count_assertion() {
1195        // Arrange
1196        let json = r#"{"name":"clnrm.test","trace_id":"abc123","span_id":"span1","parent_span_id":null,"attributes":{}}
1197{"name":"clnrm.test","trace_id":"abc123","span_id":"span2","parent_span_id":null,"attributes":{}}"#;
1198        let validator = SpanValidator::from_json(json).unwrap();
1199        let assertion = SpanAssertion::SpanCount {
1200            name: "clnrm.test".to_string(),
1201            count: 2,
1202        };
1203
1204        // Act
1205        let result = validator.validate_assertion(&assertion);
1206
1207        // Assert
1208        assert!(result.is_ok());
1209    }
1210
1211    #[test]
1212    fn test_span_hierarchy_assertion() {
1213        // Arrange
1214        let json = r#"{"name":"clnrm.run","trace_id":"abc123","span_id":"parent1","parent_span_id":null,"attributes":{}}
1215{"name":"clnrm.test","trace_id":"abc123","span_id":"child1","parent_span_id":"parent1","attributes":{}}"#;
1216        let validator = SpanValidator::from_json(json).unwrap();
1217        let assertion = SpanAssertion::SpanHierarchy {
1218            parent: "clnrm.run".to_string(),
1219            child: "clnrm.test".to_string(),
1220        };
1221
1222        // Act
1223        let result = validator.validate_assertion(&assertion);
1224
1225        // Assert
1226        assert!(result.is_ok());
1227    }
1228
1229    // =========================================================================
1230    // PRD-ALIGNED VALIDATION TESTS
1231    // =========================================================================
1232
1233    #[test]
1234    fn test_span_existence_validation() -> Result<()> {
1235        // Arrange
1236        use crate::config::SpanExpectationConfig;
1237
1238        let expectation = SpanExpectationConfig {
1239            name: "test.span".to_string(),
1240            parent: None,
1241            kind: None,
1242            attrs: None,
1243            events: None,
1244            duration_ms: None,
1245        };
1246
1247        // Missing span
1248        let empty_validator = SpanValidator::from_json("")?;
1249        let result = empty_validator.validate_expectations(&[expectation.clone()])?;
1250
1251        // Assert - failure case
1252        assert!(!result.passed);
1253        assert_eq!(result.failures.len(), 1);
1254        assert_eq!(result.failures[0].span_name, "test.span");
1255        assert!(result.failures[0].message.contains("not found"));
1256
1257        // Present span
1258        let json = r#"{"name":"test.span","trace_id":"abc123","span_id":"span1","parent_span_id":null,"attributes":{},"start_time_unix_nano":1000000000,"end_time_unix_nano":2000000000}"#;
1259        let validator_with_span = SpanValidator::from_json(json)?;
1260        let result_success = validator_with_span.validate_expectations(&[expectation])?;
1261
1262        // Assert - success case
1263        assert!(
1264            result_success.passed,
1265            "Expected pass but got failures: {:?}",
1266            result_success.failures
1267        );
1268        assert_eq!(result_success.failures.len(), 0);
1269
1270        Ok(())
1271    }
1272
1273    #[test]
1274    fn test_attrs_all_validation() -> Result<()> {
1275        // Arrange
1276        use crate::config::SpanExpectationConfig;
1277
1278        let mut attrs = HashMap::new();
1279        attrs.insert("result".to_string(), "pass".to_string());
1280
1281        let expectation = SpanExpectationConfig {
1282            name: "test.span".to_string(),
1283            parent: None,
1284            kind: None,
1285            attrs: Some(SpanAttributesConfig {
1286                all: Some(attrs.clone()),
1287                any: None,
1288            }),
1289            events: None,
1290            duration_ms: None,
1291        };
1292
1293        // Missing attribute
1294        let json_no_attr = r#"{"name":"test.span","trace_id":"abc123","span_id":"span1","parent_span_id":null,"attributes":{}}"#;
1295        let validator_no_attr = SpanValidator::from_json(json_no_attr)?;
1296        let result_fail = validator_no_attr.validate_expectations(&[expectation.clone()])?;
1297
1298        // Assert - failure
1299        assert!(!result_fail.passed);
1300        assert!(result_fail.failures[0]
1301            .message
1302            .contains("missing required attributes"));
1303
1304        // Correct attribute
1305        let json_with_attr = r#"{"name":"test.span","trace_id":"abc123","span_id":"span1","parent_span_id":null,"attributes":{"result":"pass"}}"#;
1306        let validator_with_attr = SpanValidator::from_json(json_with_attr)?;
1307        let result_success = validator_with_attr.validate_expectations(&[expectation])?;
1308
1309        // Assert - success
1310        assert!(result_success.passed);
1311        assert_eq!(result_success.failures.len(), 0);
1312
1313        Ok(())
1314    }
1315
1316    #[test]
1317    fn test_events_any_validation() -> Result<()> {
1318        // Arrange
1319        use crate::config::{SpanEventsConfig, SpanExpectationConfig};
1320
1321        let expectation = SpanExpectationConfig {
1322            name: "test.span".to_string(),
1323            parent: None,
1324            kind: None,
1325            attrs: None,
1326            events: Some(SpanEventsConfig {
1327                any: Some(vec!["event1".to_string(), "event2".to_string()]),
1328                all: None,
1329            }),
1330            duration_ms: None,
1331        };
1332
1333        // No events
1334        let json_no_events = r#"{"name":"test.span","trace_id":"abc123","span_id":"span1","parent_span_id":null,"attributes":{}}"#;
1335        let validator_no_events = SpanValidator::from_json(json_no_events)?;
1336        let result_fail = validator_no_events.validate_expectations(&[expectation.clone()])?;
1337
1338        // Assert - failure
1339        assert!(!result_fail.passed);
1340        assert!(result_fail.failures[0].message.contains("missing required events"));
1341
1342        // Has one required event
1343        let json_with_event = r#"{"name":"test.span","trace_id":"abc123","span_id":"span1","parent_span_id":null,"attributes":{},"events":["event1"]}"#;
1344        let validator_with_event = SpanValidator::from_json(json_with_event)?;
1345        let result_success = validator_with_event.validate_expectations(&[expectation])?;
1346
1347        // Assert - success
1348        assert!(result_success.passed);
1349        assert_eq!(result_success.failures.len(), 0);
1350
1351        Ok(())
1352    }
1353
1354    #[test]
1355    fn test_duration_validation() -> Result<()> {
1356        // Arrange
1357        use crate::config::{DurationBoundConfig, SpanExpectationConfig};
1358
1359        let expectation = SpanExpectationConfig {
1360            name: "test.span".to_string(),
1361            parent: None,
1362            kind: None,
1363            attrs: None,
1364            events: None,
1365            duration_ms: Some(DurationBoundConfig {
1366                min: Some(10.0),
1367                max: Some(1000.0),
1368            }),
1369        };
1370
1371        // Too short (5ms)
1372        let json_short = r#"{"name":"test.span","trace_id":"abc123","span_id":"span1","parent_span_id":null,"attributes":{},"start_time_unix_nano":1000000000,"end_time_unix_nano":1005000000}"#;
1373        let validator_short = SpanValidator::from_json(json_short)?;
1374        let result_short = validator_short.validate_expectations(&[expectation.clone()])?;
1375
1376        // Assert - too short
1377        assert!(!result_short.passed);
1378        assert!(result_short.failures[0].message.contains("< min"));
1379
1380        // Just right (100ms)
1381        let json_ok = r#"{"name":"test.span","trace_id":"abc123","span_id":"span1","parent_span_id":null,"attributes":{},"start_time_unix_nano":1000000000,"end_time_unix_nano":1100000000}"#;
1382        let validator_ok = SpanValidator::from_json(json_ok)?;
1383        let result_ok = validator_ok.validate_expectations(&[expectation.clone()])?;
1384
1385        // Assert - success
1386        assert!(result_ok.passed);
1387        assert_eq!(result_ok.failures.len(), 0);
1388
1389        // Too long (2000ms)
1390        let json_long = r#"{"name":"test.span","trace_id":"abc123","span_id":"span1","parent_span_id":null,"attributes":{},"start_time_unix_nano":1000000000,"end_time_unix_nano":3000000000}"#;
1391        let validator_long = SpanValidator::from_json(json_long)?;
1392        let result_long = validator_long.validate_expectations(&[expectation])?;
1393
1394        // Assert - too long
1395        assert!(!result_long.passed);
1396        assert!(result_long.failures[0].message.contains("> max"));
1397
1398        Ok(())
1399    }
1400
1401    #[test]
1402    fn test_parent_relationship_validation() -> Result<()> {
1403        // Arrange
1404        use crate::config::SpanExpectationConfig;
1405
1406        let expectation = SpanExpectationConfig {
1407            name: "clnrm.step:hello_world".to_string(),
1408            parent: Some("clnrm.run".to_string()),
1409            kind: None,
1410            attrs: None,
1411            events: None,
1412            duration_ms: None,
1413        };
1414
1415        // Missing parent
1416        let json_no_parent = r#"{"name":"clnrm.step:hello_world","trace_id":"abc123","span_id":"child1","parent_span_id":null,"attributes":{}}"#;
1417        let validator_no_parent = SpanValidator::from_json(json_no_parent)?;
1418        let result_no_parent = validator_no_parent.validate_expectations(&[expectation.clone()])?;
1419
1420        // Assert - missing parent
1421        assert!(!result_no_parent.passed);
1422        assert!(result_no_parent.failures[0]
1423            .message
1424            .contains("parent mismatch"));
1425        assert!(result_no_parent.failures[0].message.contains("found none"));
1426
1427        // Correct parent relationship
1428        let json_with_parent = r#"{"name":"clnrm.run","trace_id":"abc123","span_id":"parent1","parent_span_id":null,"attributes":{}}
1429{"name":"clnrm.step:hello_world","trace_id":"abc123","span_id":"child1","parent_span_id":"parent1","attributes":{}}"#;
1430        let validator_with_parent = SpanValidator::from_json(json_with_parent)?;
1431        let result_with_parent = validator_with_parent.validate_expectations(&[expectation])?;
1432
1433        // Assert - success
1434        assert!(result_with_parent.passed);
1435        assert_eq!(result_with_parent.failures.len(), 0);
1436
1437        Ok(())
1438    }
1439
1440    #[test]
1441    fn test_span_kind_validation() -> Result<()> {
1442        // Arrange
1443        use crate::config::SpanExpectationConfig;
1444
1445        let expectation = SpanExpectationConfig {
1446            name: "test.span".to_string(),
1447            parent: None,
1448            kind: Some("internal".to_string()),
1449            attrs: None,
1450            events: None,
1451            duration_ms: None,
1452        };
1453
1454        // Wrong kind
1455        let json_wrong_kind = r#"{"name":"test.span","trace_id":"abc123","span_id":"span1","parent_span_id":null,"attributes":{},"kind":3}"#;
1456        let validator_wrong = SpanValidator::from_json(json_wrong_kind)?;
1457        let result_wrong = validator_wrong.validate_expectations(&[expectation.clone()])?;
1458
1459        // Assert - wrong kind
1460        assert!(!result_wrong.passed);
1461        assert!(result_wrong.failures[0].message.contains("kind mismatch"));
1462
1463        // Correct kind (internal = 1)
1464        let json_correct_kind = r#"{"name":"test.span","trace_id":"abc123","span_id":"span1","parent_span_id":null,"attributes":{},"kind":1}"#;
1465        let validator_correct = SpanValidator::from_json(json_correct_kind)?;
1466        let result_correct = validator_correct.validate_expectations(&[expectation])?;
1467
1468        // Assert - success
1469        assert!(result_correct.passed);
1470        assert_eq!(result_correct.failures.len(), 0);
1471
1472        Ok(())
1473    }
1474
1475    #[test]
1476    fn test_validation_result_merge() {
1477        // Arrange
1478        let result1 = ValidationResult::success(3);
1479        let result2 = ValidationResult::success(2);
1480        let result3 = ValidationResult::failure(FailureDetails {
1481            rule: "test.rule".to_string(),
1482            span_name: "test.span".to_string(),
1483            expected: "something".to_string(),
1484            actual: None,
1485            message: "Test failure".to_string(),
1486        });
1487
1488        // Act
1489        let merged_success = ValidationResult::merge(vec![result1.clone(), result2.clone()]);
1490        let merged_failure = ValidationResult::merge(vec![result1, result2, result3]);
1491
1492        // Assert
1493        assert!(merged_success.passed);
1494        assert_eq!(merged_success.validations_count, 5);
1495        assert_eq!(merged_success.failures.len(), 0);
1496
1497        assert!(!merged_failure.passed);
1498        assert_eq!(merged_failure.validations_count, 6);
1499        assert_eq!(merged_failure.failures.len(), 1);
1500    }
1501
1502    #[test]
1503    fn test_first_failure_helper() {
1504        // Arrange
1505        let result_success = ValidationResult::success(5);
1506        let result_with_failure = ValidationResult::failure(FailureDetails {
1507            rule: "test.rule".to_string(),
1508            span_name: "test.span".to_string(),
1509            expected: "expected".to_string(),
1510            actual: Some("actual".to_string()),
1511            message: "Test failure message".to_string(),
1512        });
1513
1514        // Act
1515        let no_failure = SpanValidator::first_failure(&result_success);
1516        let has_failure = SpanValidator::first_failure(&result_with_failure);
1517
1518        // Assert
1519        assert!(no_failure.is_none());
1520        assert!(has_failure.is_some());
1521        assert_eq!(has_failure.unwrap().message, "Test failure message");
1522    }
1523
1524    #[test]
1525    fn test_multiple_expectations_validation() -> Result<()> {
1526        // Arrange - complex scenario with multiple expectations
1527        use crate::config::{
1528            DurationBoundConfig, SpanAttributesConfig, SpanEventsConfig, SpanExpectationConfig,
1529        };
1530
1531        let mut attrs = HashMap::new();
1532        attrs.insert("result".to_string(), "pass".to_string());
1533
1534        let expectations = vec![
1535            // Expectation 1: clnrm.run with attributes and duration
1536            SpanExpectationConfig {
1537                name: "clnrm.run".to_string(),
1538                parent: None,
1539                kind: Some("internal".to_string()),
1540                attrs: Some(SpanAttributesConfig {
1541                    all: Some(attrs.clone()),
1542                    any: None,
1543                }),
1544                events: None,
1545                duration_ms: Some(DurationBoundConfig {
1546                    min: Some(10.0),
1547                    max: Some(600000.0),
1548                }),
1549            },
1550            // Expectation 2: clnrm.step with parent and events
1551            SpanExpectationConfig {
1552                name: "clnrm.step:hello_world".to_string(),
1553                parent: Some("clnrm.run".to_string()),
1554                kind: None,
1555                attrs: None,
1556                events: Some(SpanEventsConfig {
1557                    any: Some(vec![
1558                        "container.start".to_string(),
1559                        "container.exec".to_string(),
1560                    ]),
1561                    all: None,
1562                }),
1563                duration_ms: None,
1564            },
1565        ];
1566
1567        let json = r#"{"name":"clnrm.run","trace_id":"abc123","span_id":"parent1","parent_span_id":null,"attributes":{"result":"pass"},"kind":1,"start_time_unix_nano":1000000000,"end_time_unix_nano":1100000000}
1568{"name":"clnrm.step:hello_world","trace_id":"abc123","span_id":"child1","parent_span_id":"parent1","attributes":{},"events":["container.start","container.exec"]}"#;
1569
1570        let validator = SpanValidator::from_json(json)?;
1571
1572        // Act
1573        let result = validator.validate_expectations(&expectations)?;
1574
1575        // Assert
1576        assert!(result.passed, "Validation should pass: {:?}", result.failures);
1577        assert_eq!(result.failures.len(), 0);
1578        assert!(result.validations_count >= expectations.len());
1579
1580        Ok(())
1581    }
1582}