clnrm_core/otel/validators/
status.rs

1//! Span status code validation for fake-green detection
2//!
3//! Validates OTEL span status codes (OK, ERROR, UNSET) with glob pattern support.
4//! Detects when tests falsely report success.
5
6use crate::error::{CleanroomError, Result};
7use crate::validation::span_validator::SpanData;
8use glob::Pattern;
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11
12/// Status code matching OTEL span status
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(rename_all = "UPPERCASE")]
15pub enum StatusCode {
16    /// Status not set (default)
17    Unset,
18    /// Operation completed successfully
19    Ok,
20    /// Operation encountered an error
21    Error,
22}
23
24impl StatusCode {
25    /// Parse status code from string
26    pub fn parse(s: &str) -> Result<Self> {
27        match s.to_uppercase().as_str() {
28            "UNSET" => Ok(StatusCode::Unset),
29            "OK" => Ok(StatusCode::Ok),
30            "ERROR" => Ok(StatusCode::Error),
31            _ => Err(CleanroomError::validation_error(format!(
32                "Invalid status code '{}'. Must be UNSET, OK, or ERROR",
33                s
34            ))),
35        }
36    }
37
38    /// Get string representation
39    pub fn as_str(&self) -> &'static str {
40        match self {
41            StatusCode::Unset => "UNSET",
42            StatusCode::Ok => "OK",
43            StatusCode::Error => "ERROR",
44        }
45    }
46}
47
48/// Status validation result
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct ValidationResult {
51    /// Whether validation passed
52    pub passed: bool,
53    /// Validation error messages
54    pub errors: Vec<String>,
55    /// Number of spans checked
56    pub spans_checked: usize,
57}
58
59impl ValidationResult {
60    /// Create a passing result
61    pub fn pass(spans_checked: usize) -> Self {
62        Self {
63            passed: true,
64            errors: Vec::new(),
65            spans_checked,
66        }
67    }
68
69    /// Add an error
70    pub fn add_error(&mut self, error: String) {
71        self.passed = false;
72        self.errors.push(error);
73    }
74}
75
76/// Status expectation with glob pattern support
77#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct StatusExpectation {
79    /// Expected status for all spans
80    #[serde(skip_serializing_if = "Option::is_none")]
81    pub all: Option<StatusCode>,
82
83    /// Expected status by name pattern (glob -> status)
84    #[serde(skip_serializing_if = "Option::is_none")]
85    pub by_name: Option<HashMap<String, StatusCode>>,
86}
87
88impl StatusExpectation {
89    /// Create a new empty status expectation
90    pub fn new() -> Self {
91        Self {
92            all: None,
93            by_name: None,
94        }
95    }
96
97    /// Set expected status for all spans
98    pub fn with_all(mut self, status: StatusCode) -> Self {
99        self.all = Some(status);
100        self
101    }
102
103    /// Add expected status for spans matching a name pattern
104    pub fn with_name_pattern(mut self, pattern: String, status: StatusCode) -> Self {
105        self.by_name.get_or_insert_with(HashMap::new).insert(pattern, status);
106        self
107    }
108
109    /// Validate status expectations against spans
110    ///
111    /// # Arguments
112    /// * `spans` - All spans to validate
113    ///
114    /// # Returns
115    /// * `Result<ValidationResult>` - Validation result
116    ///
117    /// # Errors
118    /// * Invalid glob pattern
119    /// * No spans match pattern
120    /// * Status code mismatch
121    pub fn validate(&self, spans: &[SpanData]) -> Result<ValidationResult> {
122        let mut result = ValidationResult::pass(0);
123
124        // Validate all spans if "all" is set
125        if let Some(expected_all) = self.all {
126            for span in spans {
127                result.spans_checked += 1;
128                let actual = self.get_span_status(span)?;
129                if actual != expected_all {
130                    result.add_error(format!(
131                        "Status validation failed: span '{}' has status {} but expected {} (fake-green: incorrect status)",
132                        span.name, actual.as_str(), expected_all.as_str()
133                    ));
134                }
135            }
136        }
137
138        // Validate by_name patterns
139        if let Some(ref patterns) = self.by_name {
140            for (pattern, expected_status) in patterns {
141                let glob_pattern = Pattern::new(pattern).map_err(|e| {
142                    CleanroomError::validation_error(format!(
143                        "Invalid glob pattern '{}': {}",
144                        pattern, e
145                    ))
146                })?;
147
148                // Find matching spans
149                let matching_spans: Vec<_> = spans
150                    .iter()
151                    .filter(|s| glob_pattern.matches(&s.name))
152                    .collect();
153
154                if matching_spans.is_empty() {
155                    result.add_error(format!(
156                        "Status validation failed: no spans match pattern '{}' (fake-green: spans never created?)",
157                        pattern
158                    ));
159                    continue;
160                }
161
162                // Check status of each matching span
163                for span in matching_spans {
164                    result.spans_checked += 1;
165                    let actual = self.get_span_status(span)?;
166                    if actual != *expected_status {
167                        result.add_error(format!(
168                            "Status validation failed: span '{}' has status {} but pattern '{}' expects {} (fake-green: incorrect status)",
169                            span.name, actual.as_str(), pattern, expected_status.as_str()
170                        ));
171                    }
172                }
173            }
174        }
175
176        Ok(result)
177    }
178
179    /// Get span status from attributes or default to Unset
180    fn get_span_status(&self, span: &SpanData) -> Result<StatusCode> {
181        // Check for otel.status_code attribute
182        if let Some(status_value) = span.attributes.get("otel.status_code") {
183            if let Some(status_str) = status_value.as_str() {
184                return StatusCode::parse(status_str);
185            }
186        }
187
188        // Default to Unset if not specified
189        Ok(StatusCode::Unset)
190    }
191}
192
193impl Default for StatusExpectation {
194    fn default() -> Self {
195        Self::new()
196    }
197}
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202    use std::collections::HashMap;
203
204    fn create_span(name: &str, status: Option<StatusCode>) -> SpanData {
205        let mut attributes = HashMap::new();
206        if let Some(s) = status {
207            attributes.insert("otel.status_code".to_string(), serde_json::Value::String(s.as_str().to_string()));
208        }
209
210        SpanData {
211            name: name.to_string(),
212            span_id: format!("span_{}", name),
213            trace_id: "trace123".to_string(),
214            parent_span_id: None,
215            attributes,
216            start_time_unix_nano: Some(1000000000),
217            end_time_unix_nano: Some(1100000000),
218            kind: None,
219            events: None,
220            resource_attributes: HashMap::new(),
221        }
222    }
223
224    #[test]
225    fn test_status_code_parse() -> Result<()> {
226        // Act & Assert
227        assert_eq!(StatusCode::parse("UNSET")?, StatusCode::Unset);
228        assert_eq!(StatusCode::parse("unset")?, StatusCode::Unset);
229        assert_eq!(StatusCode::parse("OK")?, StatusCode::Ok);
230        assert_eq!(StatusCode::parse("ok")?, StatusCode::Ok);
231        assert_eq!(StatusCode::parse("ERROR")?, StatusCode::Error);
232        assert_eq!(StatusCode::parse("error")?, StatusCode::Error);
233        assert!(StatusCode::parse("INVALID").is_err());
234        Ok(())
235    }
236
237    #[test]
238    fn test_status_expectation_all() -> Result<()> {
239        // Arrange
240        let spans = vec![
241            create_span("span1", Some(StatusCode::Ok)),
242            create_span("span2", Some(StatusCode::Ok)),
243        ];
244        let expectation = StatusExpectation::new().with_all(StatusCode::Ok);
245
246        // Act
247        let result = expectation.validate(&spans)?;
248
249        // Assert
250        assert!(result.passed);
251        assert_eq!(result.spans_checked, 2);
252        Ok(())
253    }
254
255    #[test]
256    fn test_status_expectation_all_failure() -> Result<()> {
257        // Arrange
258        let spans = vec![
259            create_span("span1", Some(StatusCode::Ok)),
260            create_span("span2", Some(StatusCode::Error)), // Wrong!
261        ];
262        let expectation = StatusExpectation::new().with_all(StatusCode::Ok);
263
264        // Act
265        let result = expectation.validate(&spans)?;
266
267        // Assert
268        assert!(!result.passed);
269        assert!(!result.errors.is_empty());
270        Ok(())
271    }
272
273    #[test]
274    fn test_status_expectation_by_name() -> Result<()> {
275        // Arrange
276        let spans = vec![
277            create_span("container.start", Some(StatusCode::Ok)),
278            create_span("container.exec", Some(StatusCode::Ok)),
279            create_span("test.failed", Some(StatusCode::Error)),
280        ];
281        let expectation = StatusExpectation::new()
282            .with_name_pattern("container.*".to_string(), StatusCode::Ok)
283            .with_name_pattern("test.*".to_string(), StatusCode::Error);
284
285        // Act
286        let result = expectation.validate(&spans)?;
287
288        // Assert
289        assert!(result.passed);
290        Ok(())
291    }
292
293    #[test]
294    fn test_status_expectation_by_name_failure() -> Result<()> {
295        // Arrange
296        let spans = vec![
297            create_span("container.start", Some(StatusCode::Error)), // Wrong!
298        ];
299        let expectation = StatusExpectation::new()
300            .with_name_pattern("container.*".to_string(), StatusCode::Ok);
301
302        // Act
303        let result = expectation.validate(&spans)?;
304
305        // Assert
306        assert!(!result.passed);
307        assert!(!result.errors.is_empty());
308        Ok(())
309    }
310
311    #[test]
312    fn test_status_expectation_default_unset() -> Result<()> {
313        // Arrange
314        let spans = vec![create_span("span1", None)]; // No status set
315        let expectation = StatusExpectation::new().with_all(StatusCode::Unset);
316
317        // Act
318        let result = expectation.validate(&spans)?;
319
320        // Assert
321        assert!(result.passed);
322        Ok(())
323    }
324}