Skip to main content

scouter_evaluate/tasks/
evaluator.rs

1use crate::error::EvaluationError;
2use regex::Regex;
3use scouter_types::genai::ValueExt;
4use scouter_types::genai::{traits::TaskAccessor, AssertionResult, ComparisonOperator};
5use serde_json::Value;
6use std::sync::OnceLock;
7use tracing::instrument;
8
9const REGEX_FIELD_PARSE_PATTERN: &str = r"[a-zA-Z_][a-zA-Z0-9_]*|\[[0-9]+\]";
10static PATH_REGEX: OnceLock<Regex> = OnceLock::new();
11
12pub struct FieldEvaluator;
13
14/// Utility for extracting field values from JSON-like structures
15/// Supports: "field", "field.subfield", "field[0]", "field[0].subfield"
16impl FieldEvaluator {
17    /// Extracts the value at the specified context path from the given JSON value
18    /// # Arguments
19    /// * `json` - The JSON value to extract from
20    /// * `context_path` - The dot/bracket notation path to the desired field
21    /// # Returns
22    /// The extracted JSON value or an EvaluationError if the path is invalid
23    #[instrument(skip_all)]
24    pub fn extract_field_value<'a>(
25        json: &'a Value,
26        context_path: &str,
27    ) -> Result<&'a Value, EvaluationError> {
28        let path_segments = Self::parse_field_path(context_path)?;
29        let mut current_value = json;
30
31        for segment in path_segments {
32            current_value = match segment {
33                PathSegment::Field(field_name) => current_value
34                    .get(&field_name)
35                    .ok_or_else(|| EvaluationError::FieldNotFound(field_name))?,
36                PathSegment::Index(index) => current_value
37                    .get(index)
38                    .ok_or_else(|| EvaluationError::IndexNotFound(index))?,
39            };
40        }
41
42        Ok(current_value)
43    }
44
45    /// Parses a context path string into segments for navigation
46    /// # Arguments
47    /// * `path` - The context path string
48    /// # Returns
49    /// A vector of PathSegment enums representing the parsed path
50    fn parse_field_path(path: &str) -> Result<Vec<PathSegment>, EvaluationError> {
51        let regex = PATH_REGEX.get_or_init(|| {
52            Regex::new(REGEX_FIELD_PARSE_PATTERN)
53                .expect("Invalid regex pattern in REGEX_FIELD_PARSE_PATTERN")
54        });
55
56        let mut segments = Vec::new();
57
58        for capture in regex.find_iter(path) {
59            let segment_str = capture.as_str();
60
61            if segment_str.starts_with('[') && segment_str.ends_with(']') {
62                // Array index: [0], [1], etc.
63                let index_str = &segment_str[1..segment_str.len() - 1];
64                let index: usize = index_str
65                    .parse()
66                    .map_err(|_| EvaluationError::InvalidArrayIndex(index_str.to_string()))?;
67                segments.push(PathSegment::Index(index));
68            } else {
69                // Field name: field, subfield, etc.
70                segments.push(PathSegment::Field(segment_str.to_string()));
71            }
72        }
73
74        if segments.is_empty() {
75            return Err(EvaluationError::EmptyFieldPath);
76        }
77
78        Ok(segments)
79    }
80}
81
82#[derive(Debug, Clone, PartialEq)]
83enum PathSegment {
84    Field(String),
85    Index(usize),
86}
87
88#[derive(Debug, Clone)]
89pub struct AssertionEvaluator;
90
91impl AssertionEvaluator {
92    /// Main function for evaluating an assertion
93    /// # Arguments
94    /// * `json_value` - The JSON value to evaluate against
95    /// * `assertion` - The assertion to evaluate
96    /// # Returns
97    /// An AssertionResult indicating whether the assertion passed or failed
98    pub fn evaluate_assertion<T: TaskAccessor>(
99        json_value: &Value,
100        assertion: &T,
101    ) -> Result<AssertionResult, EvaluationError> {
102        // if value is array and context path is provider, it is assumed that user
103        // wants to evaluate each element of array
104        if let Value::Array(items) = json_value {
105            if assertion.context_path().is_some() {
106                return Self::evaluate_over_array_items(items, assertion, assertion.context_path());
107            }
108        }
109
110        let actual_value: &Value = if let Some(context_path) = assertion.context_path() {
111            FieldEvaluator::extract_field_value(json_value, context_path)?
112        } else {
113            json_value
114        };
115
116        // if actual value is array and operator is not a native array operator, evaluate assertion over each item in array and aggregate results
117        if let Value::Array(items) = actual_value {
118            if !Self::is_array_native_operator(assertion.operator()) {
119                return Self::evaluate_over_array_items(
120                    items,
121                    assertion,
122                    assertion.item_context_path(),
123                );
124            }
125        }
126
127        let expected = Self::resolve_expected_value(json_value, assertion.expected_value())?;
128
129        let comparable_actual =
130            match Self::transform_for_comparison(actual_value, assertion.operator()) {
131                Ok(val) => val,
132                Err(err) => {
133                    // return assertion result failure with error message
134                    return Ok(AssertionResult::new(
135                        false,
136                        (*actual_value).clone(),
137                        format!(
138                            "✗ Assertion '{}' failed during transformation: {}",
139                            assertion.id(),
140                            err
141                        ),
142                        expected.clone(),
143                    ));
144                }
145            };
146
147        let passed = Self::compare_values(&comparable_actual, assertion.operator(), expected)?;
148        let messages = if passed {
149            format!("✓ Assertion '{}' passed", assertion.id())
150        } else {
151            format!(
152                "✗ Assertion '{}' failed: expected {}, got {}",
153                assertion.id(),
154                serde_json::to_string(expected).unwrap_or_default(),
155                serde_json::to_string(&comparable_actual).unwrap_or_default()
156            )
157        };
158
159        let assertion_result =
160            AssertionResult::new(passed, comparable_actual, messages, expected.clone());
161
162        Ok(assertion_result)
163    }
164
165    fn resolve_expected_value<'a>(
166        context: &'a Value,
167        expected: &'a Value,
168    ) -> Result<&'a Value, EvaluationError> {
169        match expected {
170            Value::String(s) if s.starts_with("${") && s.ends_with("}") => {
171                // Extract context path from template: "${field.path}" -> "field.path"
172                let context_path = &s[2..s.len() - 1];
173                let resolved = FieldEvaluator::extract_field_value(context, context_path)?;
174                Ok(resolved)
175            }
176            _ => Ok(expected),
177        }
178    }
179
180    /// Transforms a value based on the comparison operator
181    /// This is mainly used to convert array, string and map types that have
182    /// length to their length for length-based comparisons
183    /// # Arguments
184    /// * `value` - The value to transform
185    /// * `operator` - The comparison operator
186    /// # Returns
187    /// The transformed value or an EvaluationError if transformation fails
188    fn transform_for_comparison(
189        value: &Value,
190        operator: &ComparisonOperator,
191    ) -> Result<Value, EvaluationError> {
192        match operator {
193            // Only HasLength should convert to length
194            ComparisonOperator::HasLengthEqual
195            | ComparisonOperator::HasLengthGreaterThan
196            | ComparisonOperator::HasLengthLessThan
197            | ComparisonOperator::HasLengthGreaterThanOrEqual
198            | ComparisonOperator::HasLengthLessThanOrEqual => {
199                if let Some(len) = value.to_length() {
200                    Ok(Value::Number(len.into()))
201                } else {
202                    Err(EvaluationError::CannotGetLength(format!("{:?}", value)))
203                }
204            }
205            // Numeric comparisons should require actual numbers
206            ComparisonOperator::LessThan
207            | ComparisonOperator::LessThanOrEqual
208            | ComparisonOperator::GreaterThan
209            | ComparisonOperator::GreaterThanOrEqual => {
210                if value.is_number() {
211                    Ok(value.clone())
212                } else {
213                    Err(EvaluationError::CannotCompareNonNumericValues)
214                }
215            }
216            // All other operators pass through unchanged
217            _ => Ok(value.clone()),
218        }
219    }
220
221    fn are_same_type(actual: &Value, expected: &Value) -> bool {
222        match (actual, expected) {
223            (Value::Null, Value::Null) => true,
224            (Value::Bool(_), Value::Bool(_)) => true,
225            (Value::Number(a), Value::Number(b)) => {
226                // match underlying numeric types
227                (a.is_i64() && b.is_i64())
228                    || (a.is_u64() && b.is_u64())
229                    || (a.is_f64() && b.is_f64())
230            }
231            (Value::String(_), Value::String(_)) => true,
232            (Value::Array(_), Value::Array(_)) => true,
233            (Value::Object(_), Value::Object(_)) => true,
234            _ => false,
235        }
236    }
237
238    /// Normalizes values for comparison by coercing numeric types
239    ///
240    /// Handles cases where semantically equal values have different JSON types:
241    /// - 300.0 (float) and 300 (int) should be considered equal
242    /// - Preserves original type for non-numeric comparisons
243    /// - this isn't going to cover every edge case, but should handle the common scenarios like
244    fn normalize_for_comparison(actual: &Value, expected: &Value) -> (Value, Value) {
245        match (actual, expected) {
246            // Both are already numbers - try to normalize to comparable form
247            (Value::Number(a), Value::Number(e)) => {
248                let a_num = a.as_f64().unwrap_or(0.0);
249                let e_num = e.as_f64().unwrap_or(0.0);
250
251                // If both represent integers, compare as integers
252                if a_num.fract() == 0.0 && e_num.fract() == 0.0 {
253                    (
254                        Value::Number((a_num as i64).into()),
255                        Value::Number((e_num as i64).into()),
256                    )
257                } else {
258                    // Compare as floats
259                    (serde_json::json!(a_num), serde_json::json!(e_num))
260                }
261            }
262            // One is string, one is number - try to parse string as number
263            (Value::String(s), Value::Number(n)) | (Value::Number(n), Value::String(s)) => {
264                if let Ok(parsed) = s.parse::<f64>() {
265                    let num_val = n.as_f64().unwrap_or(0.0);
266                    (serde_json::json!(parsed), serde_json::json!(num_val))
267                } else {
268                    (actual.clone(), expected.clone())
269                }
270            }
271            // No normalization needed
272            _ => (actual.clone(), expected.clone()),
273        }
274    }
275
276    fn compare_values(
277        actual: &Value,
278        operator: &ComparisonOperator,
279        expected: &Value,
280    ) -> Result<bool, EvaluationError> {
281        // resolve
282
283        match operator {
284            // Existing operators
285            ComparisonOperator::Equals => {
286                // check type first to avoid unnecessary coercion/clones
287                if !Self::are_same_type(actual, expected) {
288                    let (norm_actual, norm_expected) =
289                        Self::normalize_for_comparison(actual, expected);
290                    Ok(norm_actual == norm_expected)
291                } else {
292                    Ok(actual == expected)
293                }
294            }
295            ComparisonOperator::NotEqual => {
296                if !Self::are_same_type(actual, expected) {
297                    let (norm_actual, norm_expected) =
298                        Self::normalize_for_comparison(actual, expected);
299                    Ok(norm_actual != norm_expected)
300                } else {
301                    Ok(actual != expected)
302                }
303            }
304            ComparisonOperator::GreaterThan => {
305                Self::compare_numeric(actual, expected, |a, b| a > b)
306            }
307            ComparisonOperator::GreaterThanOrEqual => {
308                Self::compare_numeric(actual, expected, |a, b| a >= b)
309            }
310            ComparisonOperator::LessThan => Self::compare_numeric(actual, expected, |a, b| a < b),
311            ComparisonOperator::LessThanOrEqual => {
312                Self::compare_numeric(actual, expected, |a, b| a <= b)
313            }
314            ComparisonOperator::HasLengthEqual => {
315                Self::compare_numeric(actual, expected, |a, b| a == b)
316            }
317            ComparisonOperator::HasLengthGreaterThan => {
318                Self::compare_numeric(actual, expected, |a, b| a > b)
319            }
320            ComparisonOperator::HasLengthLessThan => {
321                Self::compare_numeric(actual, expected, |a, b| a < b)
322            }
323            ComparisonOperator::HasLengthGreaterThanOrEqual => {
324                Self::compare_numeric(actual, expected, |a, b| a >= b)
325            }
326            ComparisonOperator::HasLengthLessThanOrEqual => {
327                Self::compare_numeric(actual, expected, |a, b| a <= b)
328            }
329            ComparisonOperator::Contains => Self::check_contains(actual, expected),
330            ComparisonOperator::NotContains => Ok(!Self::check_contains(actual, expected)?),
331            ComparisonOperator::StartsWith => Self::check_starts_with(actual, expected),
332            ComparisonOperator::EndsWith => Self::check_ends_with(actual, expected),
333            ComparisonOperator::Matches => Self::check_regex_match(actual, expected),
334
335            // Type Validation Operators
336            ComparisonOperator::IsNumeric => Ok(actual.is_number()),
337            ComparisonOperator::IsString => Ok(actual.is_string()),
338            ComparisonOperator::IsBoolean => Ok(actual.is_boolean()),
339            ComparisonOperator::IsNull => Ok(actual.is_null()),
340            ComparisonOperator::IsArray => Ok(actual.is_array()),
341            ComparisonOperator::IsObject => Ok(actual.is_object()),
342
343            // Pattern & Format Validators
344            ComparisonOperator::IsEmail => Self::check_is_email(actual),
345            ComparisonOperator::IsUrl => Self::check_is_url(actual),
346            ComparisonOperator::IsUuid => Self::check_is_uuid(actual),
347            ComparisonOperator::IsIso8601 => Self::check_is_iso8601(actual),
348            ComparisonOperator::IsJson => Self::check_is_json(actual),
349            ComparisonOperator::MatchesRegex => Self::check_regex_match(actual, expected),
350
351            // Numeric Range Operators
352            ComparisonOperator::InRange => Self::check_in_range(actual, expected),
353            ComparisonOperator::NotInRange => Ok(!Self::check_in_range(actual, expected)?),
354            ComparisonOperator::IsPositive => Self::check_is_positive(actual),
355            ComparisonOperator::IsNegative => Self::check_is_negative(actual),
356            ComparisonOperator::IsZero => Self::check_is_zero(actual),
357
358            // Collection/Array Operators
359            ComparisonOperator::ContainsAll => Self::check_contains_all(actual, expected),
360            ComparisonOperator::ContainsAny => Self::check_contains_any(actual, expected),
361            ComparisonOperator::ContainsNone => Self::check_contains_none(actual, expected),
362            ComparisonOperator::IsEmpty => Self::check_is_empty(actual),
363            ComparisonOperator::IsNotEmpty => Ok(!Self::check_is_empty(actual)?),
364            ComparisonOperator::HasUniqueItems => Self::check_has_unique_items(actual),
365            ComparisonOperator::SequenceMatches => Self::check_sequence_matches(actual, expected),
366
367            // String Operators
368            ComparisonOperator::IsAlphabetic => Self::check_is_alphabetic(actual),
369            ComparisonOperator::IsAlphanumeric => Self::check_is_alphanumeric(actual),
370            ComparisonOperator::IsLowerCase => Self::check_is_lowercase(actual),
371            ComparisonOperator::IsUpperCase => Self::check_is_uppercase(actual),
372            ComparisonOperator::ContainsWord => Self::check_contains_word(actual, expected),
373
374            // Comparison with Tolerance
375            ComparisonOperator::ApproximatelyEquals => {
376                Self::check_approximately_equals(actual, expected)
377            }
378        }
379    }
380
381    // Comparison helpers - all work with references
382    fn compare_numeric<F>(
383        actual: &Value,
384        expected: &Value,
385        comparator: F,
386    ) -> Result<bool, EvaluationError>
387    where
388        F: Fn(f64, f64) -> bool,
389    {
390        let actual_num = actual
391            .as_numeric()
392            .ok_or(EvaluationError::CannotCompareNonNumericValues)?;
393        let expected_num = expected
394            .as_numeric()
395            .ok_or(EvaluationError::CannotCompareNonNumericValues)?;
396
397        Ok(comparator(actual_num, expected_num))
398    }
399
400    fn check_contains(actual: &Value, expected: &Value) -> Result<bool, EvaluationError> {
401        match (actual, expected) {
402            (Value::String(s), Value::String(substr)) => Ok(s.contains(substr)),
403            (Value::Array(arr), expected_item) => Ok(arr.contains(expected_item)),
404            _ => Err(EvaluationError::InvalidContainsOperation),
405        }
406    }
407
408    fn check_starts_with(actual: &Value, expected: &Value) -> Result<bool, EvaluationError> {
409        match (actual, expected) {
410            (Value::String(s), Value::String(prefix)) => Ok(s.starts_with(prefix)),
411            _ => Err(EvaluationError::InvalidStartsWithOperation),
412        }
413    }
414
415    fn check_ends_with(actual: &Value, expected: &Value) -> Result<bool, EvaluationError> {
416        match (actual, expected) {
417            (Value::String(s), Value::String(suffix)) => Ok(s.ends_with(suffix)),
418            _ => Err(EvaluationError::InvalidEndsWithOperation),
419        }
420    }
421
422    fn check_regex_match(actual: &Value, expected: &Value) -> Result<bool, EvaluationError> {
423        match (actual, expected) {
424            (Value::String(s), Value::String(pattern)) => {
425                let regex = Regex::new(pattern)?;
426                Ok(regex.is_match(s))
427            }
428            _ => Err(EvaluationError::InvalidRegexOperation),
429        }
430    }
431
432    // Pattern & Format Validation Helpers
433    fn check_is_email(actual: &Value) -> Result<bool, EvaluationError> {
434        match actual {
435            Value::String(s) => {
436                let email_regex = Regex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$")
437                    .map_err(EvaluationError::RegexError)?;
438                Ok(email_regex.is_match(s))
439            }
440            _ => Err(EvaluationError::InvalidEmailOperation),
441        }
442    }
443
444    fn check_is_url(actual: &Value) -> Result<bool, EvaluationError> {
445        match actual {
446            Value::String(s) => {
447                let url_regex = Regex::new(
448                    r"^https?://[a-zA-Z0-9][a-zA-Z0-9-]*(\.[a-zA-Z0-9][a-zA-Z0-9-]*)*(/.*)?$",
449                )
450                .map_err(EvaluationError::RegexError)?;
451                Ok(url_regex.is_match(s))
452            }
453            _ => Err(EvaluationError::InvalidUrlOperation),
454        }
455    }
456
457    fn check_is_uuid(actual: &Value) -> Result<bool, EvaluationError> {
458        match actual {
459            Value::String(s) => {
460                let uuid_regex = Regex::new(
461                    r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"
462                ).map_err(EvaluationError::RegexError)?;
463                Ok(uuid_regex.is_match(s))
464            }
465            _ => Err(EvaluationError::InvalidUuidOperation),
466        }
467    }
468
469    fn check_is_iso8601(actual: &Value) -> Result<bool, EvaluationError> {
470        match actual {
471            Value::String(s) => {
472                // ISO 8601 date-time format
473                let iso_regex = Regex::new(
474                    r"^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})?)?$",
475                )
476                .map_err(EvaluationError::RegexError)?;
477                Ok(iso_regex.is_match(s))
478            }
479            _ => Err(EvaluationError::InvalidIso8601Operation),
480        }
481    }
482
483    fn check_is_json(actual: &Value) -> Result<bool, EvaluationError> {
484        match actual {
485            Value::String(s) => Ok(serde_json::from_str::<Value>(s).is_ok()),
486            _ => Err(EvaluationError::InvalidJsonOperation),
487        }
488    }
489
490    // Numeric Range Helpers
491    fn check_in_range(actual: &Value, expected: &Value) -> Result<bool, EvaluationError> {
492        let actual_num = actual
493            .as_numeric()
494            .ok_or(EvaluationError::CannotCompareNonNumericValues)?;
495
496        match expected {
497            Value::Array(range) if range.len() == 2 => {
498                let min = range[0]
499                    .as_numeric()
500                    .ok_or(EvaluationError::InvalidRangeFormat)?;
501                let max = range[1]
502                    .as_numeric()
503                    .ok_or(EvaluationError::InvalidRangeFormat)?;
504                Ok(actual_num >= min && actual_num <= max)
505            }
506            _ => Err(EvaluationError::InvalidRangeFormat),
507        }
508    }
509
510    fn check_sequence_matches(actual: &Value, expected: &Value) -> Result<bool, EvaluationError> {
511        match (actual, expected) {
512            (Value::Array(actual_arr), Value::Array(expected_arr)) => {
513                Ok(actual_arr == expected_arr)
514            }
515            _ => Err(EvaluationError::InvalidSequenceMatchesOperation),
516        }
517    }
518
519    fn check_is_positive(actual: &Value) -> Result<bool, EvaluationError> {
520        let num = actual
521            .as_numeric()
522            .ok_or(EvaluationError::CannotCompareNonNumericValues)?;
523        Ok(num > 0.0)
524    }
525
526    fn check_is_negative(actual: &Value) -> Result<bool, EvaluationError> {
527        let num = actual
528            .as_numeric()
529            .ok_or(EvaluationError::CannotCompareNonNumericValues)?;
530        Ok(num < 0.0)
531    }
532
533    fn check_is_zero(actual: &Value) -> Result<bool, EvaluationError> {
534        let num = actual
535            .as_numeric()
536            .ok_or(EvaluationError::CannotCompareNonNumericValues)?;
537        Ok(num == 0.0)
538    }
539
540    // Collection/Array Helpers
541    fn check_contains_all(actual: &Value, expected: &Value) -> Result<bool, EvaluationError> {
542        match (actual, expected) {
543            (Value::Array(arr), Value::Array(required)) => {
544                Ok(required.iter().all(|item| arr.contains(item)))
545            }
546            _ => Err(EvaluationError::InvalidContainsAllOperation),
547        }
548    }
549
550    fn check_contains_any(actual: &Value, expected: &Value) -> Result<bool, EvaluationError> {
551        match (actual, expected) {
552            (Value::Array(arr), Value::Array(candidates)) => {
553                Ok(candidates.iter().any(|item| arr.contains(item)))
554            }
555            (Value::String(s), Value::Array(keywords)) => Ok(keywords.iter().any(|keyword| {
556                if let Value::String(kw) = keyword {
557                    s.contains(kw)
558                } else {
559                    false
560                }
561            })),
562            _ => Err(EvaluationError::InvalidContainsAnyOperation),
563        }
564    }
565
566    fn check_contains_none(actual: &Value, expected: &Value) -> Result<bool, EvaluationError> {
567        match (actual, expected) {
568            (Value::Array(arr), Value::Array(forbidden)) => {
569                Ok(!forbidden.iter().any(|item| arr.contains(item)))
570            }
571            _ => Err(EvaluationError::InvalidContainsNoneOperation),
572        }
573    }
574
575    fn check_is_empty(actual: &Value) -> Result<bool, EvaluationError> {
576        match actual {
577            Value::String(s) => Ok(s.is_empty()),
578            Value::Null => Ok(true),
579            Value::Array(arr) => Ok(arr.is_empty()),
580            Value::Object(obj) => Ok(obj.is_empty()),
581            _ => Err(EvaluationError::InvalidEmptyOperation),
582        }
583    }
584
585    fn check_has_unique_items(actual: &Value) -> Result<bool, EvaluationError> {
586        match actual {
587            Value::Array(arr) => {
588                let mut seen = std::collections::HashSet::new();
589                for item in arr {
590                    let json_str = serde_json::to_string(item)
591                        .map_err(|_| EvaluationError::InvalidUniqueItemsOperation)?;
592                    if !seen.insert(json_str) {
593                        return Ok(false);
594                    }
595                }
596                Ok(true)
597            }
598            _ => Err(EvaluationError::InvalidUniqueItemsOperation),
599        }
600    }
601
602    // String Helpers
603    fn check_is_alphabetic(actual: &Value) -> Result<bool, EvaluationError> {
604        match actual {
605            Value::String(s) => Ok(s.chars().all(|c| c.is_alphabetic())),
606            _ => Err(EvaluationError::InvalidAlphabeticOperation),
607        }
608    }
609
610    fn check_is_alphanumeric(actual: &Value) -> Result<bool, EvaluationError> {
611        match actual {
612            Value::String(s) => Ok(s.chars().all(|c| c.is_alphanumeric())),
613            _ => Err(EvaluationError::InvalidAlphanumericOperation),
614        }
615    }
616
617    fn check_is_lowercase(actual: &Value) -> Result<bool, EvaluationError> {
618        match actual {
619            Value::String(s) => {
620                let has_letters = s.chars().any(|c| c.is_alphabetic());
621                if !has_letters {
622                    return Err(EvaluationError::InvalidCaseOperation);
623                }
624                Ok(s.chars()
625                    .filter(|c| c.is_alphabetic())
626                    .all(|c| c.is_lowercase()))
627            }
628            _ => Err(EvaluationError::InvalidCaseOperation),
629        }
630    }
631
632    fn check_is_uppercase(actual: &Value) -> Result<bool, EvaluationError> {
633        match actual {
634            Value::String(s) => {
635                let has_letters = s.chars().any(|c| c.is_alphabetic());
636                if !has_letters {
637                    return Err(EvaluationError::InvalidCaseOperation);
638                }
639                Ok(s.chars()
640                    .filter(|c| c.is_alphabetic())
641                    .all(|c| c.is_uppercase()))
642            }
643            _ => Err(EvaluationError::InvalidCaseOperation),
644        }
645    }
646
647    fn check_contains_word(actual: &Value, expected: &Value) -> Result<bool, EvaluationError> {
648        match (actual, expected) {
649            (Value::String(s), Value::String(word)) => {
650                let word_regex = Regex::new(&format!(r"\b{}\b", regex::escape(word)))
651                    .map_err(EvaluationError::RegexError)?;
652                Ok(word_regex.is_match(s))
653            }
654            _ => Err(EvaluationError::InvalidContainsWordOperation),
655        }
656    }
657
658    /// Returns true for operators that should work on an array as a whole,
659    /// rather than iterating over its items.
660    fn is_array_native_operator(operator: &ComparisonOperator) -> bool {
661        matches!(
662            operator,
663            // Array-aggregate operators
664            ComparisonOperator::HasLengthEqual
665                | ComparisonOperator::HasLengthGreaterThan
666                | ComparisonOperator::HasLengthLessThan
667                | ComparisonOperator::HasLengthGreaterThanOrEqual
668                | ComparisonOperator::HasLengthLessThanOrEqual
669                | ComparisonOperator::ContainsAll
670                | ComparisonOperator::ContainsAny
671                | ComparisonOperator::ContainsNone
672                | ComparisonOperator::IsEmpty
673                | ComparisonOperator::IsNotEmpty
674                | ComparisonOperator::HasUniqueItems
675                | ComparisonOperator::SequenceMatches
676                // Contains on arrays means "array contains element" — preserve that behaviour
677                | ComparisonOperator::Contains
678                // Type-identity operators check what the value IS, not its contents
679                | ComparisonOperator::IsNumeric
680                | ComparisonOperator::IsString
681                | ComparisonOperator::IsBoolean
682                | ComparisonOperator::IsNull
683                | ComparisonOperator::IsArray
684                | ComparisonOperator::IsObject
685        )
686    }
687
688    /// Evaluates an assertion against every item in `items`.
689    ///
690    /// * When `context_path` is `Some`, each item is expected to be a JSON object and the
691    ///   specified field is extracted before comparison.
692    /// * When `context_path` is `None`, the item itself is compared directly.
693    ///
694    /// The assertion fails as soon as any item fails; all items must pass for success.
695    #[instrument(skip_all)]
696    fn evaluate_over_array_items<T: TaskAccessor>(
697        items: &[Value],
698        assertion: &T,
699        context_path: Option<&str>,
700    ) -> Result<AssertionResult, EvaluationError> {
701        let expected = assertion.expected_value();
702        let array_value = Value::Array(items.to_vec());
703
704        for (idx, item) in items.iter().enumerate() {
705            let actual_item: &Value = match context_path {
706                Some(fp) => match FieldEvaluator::extract_field_value(item, fp) {
707                    Ok(v) => v,
708                    Err(err) => {
709                        return Ok(AssertionResult::new(
710                            false,
711                            array_value,
712                            format!(
713                                "✗ Assertion '{}' failed: item[{}] missing field '{}': {}",
714                                assertion.id(),
715                                idx,
716                                fp,
717                                err
718                            ),
719                            expected.clone(),
720                        ));
721                    }
722                },
723                None => item,
724            };
725
726            let comparable = match Self::transform_for_comparison(actual_item, assertion.operator())
727            {
728                Ok(v) => v,
729                Err(err) => {
730                    return Ok(AssertionResult::new(
731                        false,
732                        array_value,
733                        format!(
734                            "✗ Assertion '{}' failed at item[{}] during transformation: {}",
735                            assertion.id(),
736                            idx,
737                            err
738                        ),
739                        expected.clone(),
740                    ));
741                }
742            };
743
744            let passed = Self::compare_values(&comparable, assertion.operator(), expected)?;
745            if !passed {
746                let msg = format!(
747                    "✗ Assertion '{}' failed at item[{}]: expected {}, got {}",
748                    assertion.id(),
749                    idx,
750                    serde_json::to_string(expected).unwrap_or_default(),
751                    serde_json::to_string(&comparable).unwrap_or_default()
752                );
753                return Ok(AssertionResult::new(
754                    false,
755                    comparable,
756                    msg,
757                    expected.clone(),
758                ));
759            }
760        }
761
762        Ok(AssertionResult::new(
763            true,
764            array_value,
765            format!(
766                "✓ Assertion '{}' passed for all {} item(s)",
767                assertion.id(),
768                items.len()
769            ),
770            expected.clone(),
771        ))
772    }
773
774    // Tolerance Comparison Helper
775    fn check_approximately_equals(
776        actual: &Value,
777        expected: &Value,
778    ) -> Result<bool, EvaluationError> {
779        match expected {
780            Value::Array(arr) if arr.len() == 2 => {
781                let actual_num = actual
782                    .as_numeric()
783                    .ok_or(EvaluationError::CannotCompareNonNumericValues)?;
784                let expected_num = arr[0]
785                    .as_numeric()
786                    .ok_or(EvaluationError::InvalidToleranceFormat)?;
787                let tolerance = arr[1]
788                    .as_numeric()
789                    .ok_or(EvaluationError::InvalidToleranceFormat)?;
790
791                Ok((actual_num - expected_num).abs() <= tolerance)
792            }
793            _ => Err(EvaluationError::InvalidToleranceFormat),
794        }
795    }
796}
797#[cfg(test)]
798mod tests {
799    use super::*;
800    use scouter_types::genai::AssertionTask;
801    use scouter_types::genai::EvaluationTaskType;
802    use serde_json::json;
803
804    // Test data matching your StructuredTaskOutput example
805    fn get_test_json() -> Value {
806        json!({
807            "tasks": ["task1", "task2", "task3"],
808            "status": "in_progress",
809            "metadata": {
810                "created_by": "user_123",
811                "priority": "high",
812                "tags": ["urgent", "backend"],
813                "nested": {
814                    "deep": {
815                        "value": "found_it"
816                    }
817                }
818            },
819            "counts": {
820                "total": 42,
821                "completed": 15
822            },
823            "empty_array": [],
824            "single_item": ["only_one"]
825        })
826    }
827
828    fn priority_assertion() -> AssertionTask {
829        AssertionTask {
830            id: "priority_check".to_string(),
831            context_path: Some("metadata.priority".to_string()),
832            operator: ComparisonOperator::Equals,
833            expected_value: Value::String("high".to_string()),
834            description: Some("Check if priority is high".to_string()),
835            task_type: EvaluationTaskType::Assertion,
836            depends_on: vec![],
837            item_context_path: None,
838            result: None,
839            condition: false,
840        }
841    }
842
843    fn match_assertion() -> AssertionTask {
844        AssertionTask {
845            id: "status_match".to_string(),
846            context_path: Some("status".to_string()),
847            operator: ComparisonOperator::Matches,
848            expected_value: Value::String(r"^in_.*$".to_string()),
849            description: Some("Status should start with 'in_'".to_string()),
850            task_type: EvaluationTaskType::Assertion,
851            depends_on: vec![],
852            item_context_path: None,
853            result: None,
854            condition: false,
855        }
856    }
857
858    fn length_assertion() -> AssertionTask {
859        AssertionTask {
860            id: "tasks_length".to_string(),
861            context_path: Some("tasks".to_string()),
862            operator: ComparisonOperator::HasLengthEqual,
863            expected_value: Value::Number(3.into()),
864            description: Some("There should be 3 tasks".to_string()),
865            task_type: EvaluationTaskType::Assertion,
866            depends_on: vec![],
867            item_context_path: None,
868            result: None,
869            condition: false,
870        }
871    }
872
873    fn length_assertion_greater() -> AssertionTask {
874        AssertionTask {
875            id: "tasks_length_gte".to_string(),
876            context_path: Some("tasks".to_string()),
877            operator: ComparisonOperator::HasLengthGreaterThanOrEqual,
878            expected_value: Value::Number(2.into()),
879            description: Some("There should be more than 2 tasks".to_string()),
880            task_type: EvaluationTaskType::Assertion,
881            depends_on: vec![],
882            item_context_path: None,
883            result: None,
884            condition: false,
885        }
886    }
887
888    fn length_assertion_less() -> AssertionTask {
889        AssertionTask {
890            id: "tasks_length_lte".to_string(),
891            context_path: Some("tasks".to_string()),
892            operator: ComparisonOperator::HasLengthLessThanOrEqual,
893            expected_value: Value::Number(5.into()),
894            description: Some("There should be less than 5 tasks".to_string()),
895            task_type: EvaluationTaskType::Assertion,
896            depends_on: vec![],
897            item_context_path: None,
898            result: None,
899            condition: false,
900        }
901    }
902
903    fn contains_assertion() -> AssertionTask {
904        AssertionTask {
905            id: "tags_contains".to_string(),
906            context_path: Some("metadata.tags".to_string()),
907            operator: ComparisonOperator::Contains,
908            expected_value: Value::String("backend".to_string()),
909            description: Some("Tags should contain 'backend'".to_string()),
910            task_type: EvaluationTaskType::Assertion,
911            depends_on: vec![],
912            item_context_path: None,
913            result: None,
914            condition: false,
915        }
916    }
917
918    fn not_equal_assertion() -> AssertionTask {
919        AssertionTask {
920            id: "status_not_equal".to_string(),
921            context_path: Some("status".to_string()),
922            operator: ComparisonOperator::NotEqual,
923            expected_value: Value::String("completed".to_string()),
924            description: Some("Status should not be completed".to_string()),
925            task_type: EvaluationTaskType::Assertion,
926            depends_on: vec![],
927            item_context_path: None,
928            result: None,
929            condition: false,
930        }
931    }
932
933    fn greater_than_assertion() -> AssertionTask {
934        AssertionTask {
935            id: "total_greater".to_string(),
936            context_path: Some("counts.total".to_string()),
937            operator: ComparisonOperator::GreaterThan,
938            expected_value: Value::Number(40.into()),
939            description: Some("Total should be greater than 40".to_string()),
940            task_type: EvaluationTaskType::Assertion,
941            depends_on: vec![],
942            item_context_path: None,
943            result: None,
944            condition: false,
945        }
946    }
947
948    fn less_than_assertion() -> AssertionTask {
949        AssertionTask {
950            id: "completed_less".to_string(),
951            context_path: Some("counts.completed".to_string()),
952            operator: ComparisonOperator::LessThan,
953            expected_value: Value::Number(20.into()),
954            description: Some("Completed should be less than 20".to_string()),
955            task_type: EvaluationTaskType::Assertion,
956            depends_on: vec![],
957            item_context_path: None,
958            result: None,
959            condition: false,
960        }
961    }
962
963    fn not_contains_assertion() -> AssertionTask {
964        AssertionTask {
965            id: "tags_not_contains".to_string(),
966            context_path: Some("metadata.tags".to_string()),
967            operator: ComparisonOperator::NotContains,
968            expected_value: Value::String("frontend".to_string()),
969            description: Some("Tags should not contain 'frontend'".to_string()),
970            task_type: EvaluationTaskType::Assertion,
971            depends_on: vec![],
972            item_context_path: None,
973            result: None,
974            condition: false,
975        }
976    }
977
978    fn starts_with_assertion() -> AssertionTask {
979        AssertionTask {
980            id: "status_starts_with".to_string(),
981            context_path: Some("status".to_string()),
982            operator: ComparisonOperator::StartsWith,
983            expected_value: Value::String("in_".to_string()),
984            description: Some("Status should start with 'in_'".to_string()),
985            task_type: EvaluationTaskType::Assertion,
986            depends_on: vec![],
987            item_context_path: None,
988            result: None,
989            condition: false,
990        }
991    }
992
993    fn ends_with_assertion() -> AssertionTask {
994        AssertionTask {
995            id: "status_ends_with".to_string(),
996            context_path: Some("status".to_string()),
997            operator: ComparisonOperator::EndsWith,
998            expected_value: Value::String("_progress".to_string()),
999            description: Some("Status should end with '_progress'".to_string()),
1000            task_type: EvaluationTaskType::Assertion,
1001            depends_on: vec![],
1002            item_context_path: None,
1003            result: None,
1004            condition: false,
1005        }
1006    }
1007
1008    #[test]
1009    fn test_parse_field_path_simple_field() {
1010        let segments = FieldEvaluator::parse_field_path("status").unwrap();
1011        assert_eq!(segments, vec![PathSegment::Field("status".to_string())]);
1012    }
1013
1014    #[test]
1015    fn test_parse_field_path_nested_field() {
1016        let segments = FieldEvaluator::parse_field_path("metadata.created_by").unwrap();
1017        assert_eq!(
1018            segments,
1019            vec![
1020                PathSegment::Field("metadata".to_string()),
1021                PathSegment::Field("created_by".to_string())
1022            ]
1023        );
1024    }
1025
1026    #[test]
1027    fn test_parse_field_path_array_index() {
1028        let segments = FieldEvaluator::parse_field_path("tasks[0]").unwrap();
1029        assert_eq!(
1030            segments,
1031            vec![
1032                PathSegment::Field("tasks".to_string()),
1033                PathSegment::Index(0)
1034            ]
1035        );
1036    }
1037
1038    #[test]
1039    fn test_parse_field_path_complex() {
1040        let segments = FieldEvaluator::parse_field_path("metadata.tags[1]").unwrap();
1041        assert_eq!(
1042            segments,
1043            vec![
1044                PathSegment::Field("metadata".to_string()),
1045                PathSegment::Field("tags".to_string()),
1046                PathSegment::Index(1)
1047            ]
1048        );
1049    }
1050
1051    #[test]
1052    fn test_parse_field_path_deep_nested() {
1053        let segments = FieldEvaluator::parse_field_path("metadata.nested.deep.value").unwrap();
1054        assert_eq!(
1055            segments,
1056            vec![
1057                PathSegment::Field("metadata".to_string()),
1058                PathSegment::Field("nested".to_string()),
1059                PathSegment::Field("deep".to_string()),
1060                PathSegment::Field("value".to_string())
1061            ]
1062        );
1063    }
1064
1065    #[test]
1066    fn test_parse_field_path_underscore_field() {
1067        let segments = FieldEvaluator::parse_field_path("created_by").unwrap();
1068        assert_eq!(segments, vec![PathSegment::Field("created_by".to_string())]);
1069    }
1070
1071    #[test]
1072    fn test_parse_field_path_empty_string() {
1073        let result = FieldEvaluator::parse_field_path("");
1074        assert!(result.is_err());
1075        assert!(result
1076            .unwrap_err()
1077            .to_string()
1078            .contains("Empty context path"));
1079    }
1080
1081    #[test]
1082    fn test_extract_simple_field() {
1083        let json = get_test_json();
1084        let result = FieldEvaluator::extract_field_value(&json, "status").unwrap();
1085        assert_eq!(*result, json!("in_progress"));
1086    }
1087
1088    #[test]
1089    fn test_extract_array_field() {
1090        let json = get_test_json();
1091        let result = FieldEvaluator::extract_field_value(&json, "tasks").unwrap();
1092        assert_eq!(*result, json!(["task1", "task2", "task3"]));
1093    }
1094
1095    #[test]
1096    fn test_extract_array_element() {
1097        let json = get_test_json();
1098        let result = FieldEvaluator::extract_field_value(&json, "tasks[0]").unwrap();
1099        assert_eq!(*result, json!("task1"));
1100
1101        let result = FieldEvaluator::extract_field_value(&json, "tasks[2]").unwrap();
1102        assert_eq!(*result, json!("task3"));
1103    }
1104
1105    #[test]
1106    fn test_extract_nested_field() {
1107        let json = get_test_json();
1108        let result = FieldEvaluator::extract_field_value(&json, "metadata.created_by").unwrap();
1109        assert_eq!(*result, json!("user_123"));
1110
1111        let result = FieldEvaluator::extract_field_value(&json, "metadata.priority").unwrap();
1112        assert_eq!(*result, json!("high"));
1113    }
1114
1115    #[test]
1116    fn test_extract_nested_array_element() {
1117        let json = get_test_json();
1118        let result = FieldEvaluator::extract_field_value(&json, "metadata.tags[0]").unwrap();
1119        assert_eq!(*result, json!("urgent"));
1120
1121        let result = FieldEvaluator::extract_field_value(&json, "metadata.tags[1]").unwrap();
1122        assert_eq!(*result, json!("backend"));
1123    }
1124
1125    #[test]
1126    fn test_extract_deep_nested_field() {
1127        let json = get_test_json();
1128        let result =
1129            FieldEvaluator::extract_field_value(&json, "metadata.nested.deep.value").unwrap();
1130        assert_eq!(*result, json!("found_it"));
1131    }
1132
1133    #[test]
1134    fn test_extract_numeric_field() {
1135        let json = get_test_json();
1136        let result = FieldEvaluator::extract_field_value(&json, "counts.total").unwrap();
1137        assert_eq!(*result, json!(42));
1138
1139        let result = FieldEvaluator::extract_field_value(&json, "counts.completed").unwrap();
1140        assert_eq!(*result, json!(15));
1141    }
1142
1143    #[test]
1144    fn test_extract_empty_array() {
1145        let json = get_test_json();
1146        let result = FieldEvaluator::extract_field_value(&json, "empty_array").unwrap();
1147        assert_eq!(*result, json!([]));
1148    }
1149
1150    #[test]
1151    fn test_extract_single_item_array() {
1152        let json = get_test_json();
1153        let result = FieldEvaluator::extract_field_value(&json, "single_item[0]").unwrap();
1154        assert_eq!(*result, json!("only_one"));
1155    }
1156
1157    #[test]
1158    fn test_extract_nonexistent_field() {
1159        let json = get_test_json();
1160        let result = FieldEvaluator::extract_field_value(&json, "nonexistent");
1161        assert!(result.is_err());
1162        assert!(result
1163            .unwrap_err()
1164            .to_string()
1165            .contains("Field 'nonexistent' not found"));
1166    }
1167
1168    #[test]
1169    fn test_extract_nonexistent_nested_field() {
1170        let json = get_test_json();
1171        let result = FieldEvaluator::extract_field_value(&json, "metadata.nonexistent");
1172        assert!(result.is_err());
1173        assert!(result
1174            .unwrap_err()
1175            .to_string()
1176            .contains("Field 'nonexistent' not found"));
1177    }
1178
1179    #[test]
1180    fn test_extract_array_index_out_of_bounds() {
1181        let json = get_test_json();
1182        let result = FieldEvaluator::extract_field_value(&json, "tasks[99]");
1183        assert!(result.is_err());
1184        assert!(result
1185            .unwrap_err()
1186            .to_string()
1187            .contains("Index 99 not found"));
1188    }
1189
1190    #[test]
1191    fn test_extract_array_index_on_non_array() {
1192        let json = get_test_json();
1193        let result = FieldEvaluator::extract_field_value(&json, "status[0]");
1194        assert!(result.is_err());
1195        assert!(result
1196            .unwrap_err()
1197            .to_string()
1198            .contains("Index 0 not found"));
1199    }
1200
1201    #[test]
1202    fn test_extract_field_on_array_element() {
1203        let json = json!({
1204            "users": [
1205                {"name": "Alice", "age": 30},
1206                {"name": "Bob", "age": 25}
1207            ]
1208        });
1209
1210        let result = FieldEvaluator::extract_field_value(&json, "users[0].name").unwrap();
1211        assert_eq!(*result, json!("Alice"));
1212
1213        let result = FieldEvaluator::extract_field_value(&json, "users[1].age").unwrap();
1214        assert_eq!(*result, json!(25));
1215    }
1216
1217    #[test]
1218    fn test_structured_task_output_scenarios() {
1219        // Test scenarios based on your StructuredTaskOutput example
1220        let json = json!({
1221            "tasks": ["setup_database", "create_api", "write_tests"],
1222            "status": "in_progress"
1223        });
1224
1225        // Test extracting the tasks array
1226        let tasks = FieldEvaluator::extract_field_value(&json, "tasks").unwrap();
1227        assert!(tasks.is_array());
1228        assert_eq!(tasks.as_array().unwrap().len(), 3);
1229
1230        // Test extracting individual task items
1231        let first_task = FieldEvaluator::extract_field_value(&json, "tasks[0]").unwrap();
1232        assert_eq!(*first_task, json!("setup_database"));
1233
1234        // Test extracting status
1235        let status = FieldEvaluator::extract_field_value(&json, "status").unwrap();
1236        assert_eq!(*status, json!("in_progress"));
1237    }
1238
1239    #[test]
1240    fn test_real_world_llm_response_structure() {
1241        // Test with a more complex LLM response structure
1242        let json = json!({
1243            "analysis": {
1244                "sentiment": "positive",
1245                "confidence": 0.85,
1246                "keywords": ["innovation", "growth", "success"]
1247            },
1248            "recommendations": [
1249                {
1250                    "action": "increase_investment",
1251                    "priority": "high",
1252                    "estimated_impact": 0.75
1253                },
1254                {
1255                    "action": "expand_team",
1256                    "priority": "medium",
1257                    "estimated_impact": 0.60
1258                }
1259            ],
1260            "summary": "Overall positive outlook with strong growth potential"
1261        });
1262
1263        // Test nested object extraction
1264        let sentiment = FieldEvaluator::extract_field_value(&json, "analysis.sentiment").unwrap();
1265        assert_eq!(*sentiment, json!("positive"));
1266
1267        // Test array of objects
1268        let first_action =
1269            FieldEvaluator::extract_field_value(&json, "recommendations[0].action").unwrap();
1270        assert_eq!(*first_action, json!("increase_investment"));
1271
1272        // Test numeric extraction
1273        let confidence = FieldEvaluator::extract_field_value(&json, "analysis.confidence").unwrap();
1274        assert_eq!(*confidence, json!(0.85));
1275        // Test array element extraction
1276        let first_keyword =
1277            FieldEvaluator::extract_field_value(&json, "analysis.keywords[0]").unwrap();
1278        assert_eq!(*first_keyword, json!("innovation"));
1279    }
1280
1281    #[test]
1282    fn test_assertion_equals_pass() {
1283        let json = get_test_json();
1284        let assertion = priority_assertion();
1285        let result = AssertionEvaluator::evaluate_assertion(&json, &assertion).unwrap();
1286
1287        assert!(result.passed);
1288        assert_eq!(result.actual, json!("high"));
1289        assert!(result.message.contains("passed"));
1290    }
1291
1292    #[test]
1293    fn test_assertion_equals_fail() {
1294        let json = get_test_json();
1295        let mut assertion = priority_assertion();
1296        assertion.expected_value = Value::String("low".to_string());
1297
1298        let result = AssertionEvaluator::evaluate_assertion(&json, &assertion).unwrap();
1299
1300        assert!(!result.passed);
1301        assert_eq!(result.actual, json!("high"));
1302        assert!(result.message.contains("failed"));
1303    }
1304
1305    #[test]
1306    fn test_assertion_not_equal_pass() {
1307        let json = get_test_json();
1308        let assertion = not_equal_assertion();
1309        let result = AssertionEvaluator::evaluate_assertion(&json, &assertion).unwrap();
1310
1311        assert!(result.passed);
1312        assert_eq!(result.actual, json!("in_progress"));
1313    }
1314
1315    #[test]
1316    fn test_assertion_not_equal_fail() {
1317        let json = get_test_json();
1318        let mut assertion = not_equal_assertion();
1319        assertion.expected_value = Value::String("in_progress".to_string());
1320
1321        let result = AssertionEvaluator::evaluate_assertion(&json, &assertion).unwrap();
1322
1323        assert!(!result.passed);
1324    }
1325
1326    #[test]
1327    fn test_assertion_greater_than_pass() {
1328        let json = get_test_json();
1329        let assertion = greater_than_assertion();
1330        let result = AssertionEvaluator::evaluate_assertion(&json, &assertion).unwrap();
1331
1332        assert!(result.passed);
1333        assert_eq!(result.actual, json!(42));
1334    }
1335
1336    #[test]
1337    fn test_assertion_greater_than_fail() {
1338        let json = get_test_json();
1339        let mut assertion = greater_than_assertion();
1340        assertion.expected_value = Value::Number(50.into());
1341
1342        let result = AssertionEvaluator::evaluate_assertion(&json, &assertion).unwrap();
1343
1344        assert!(!result.passed);
1345    }
1346
1347    #[test]
1348    fn test_assertion_greater_than_or_equal_pass() {
1349        let json = get_test_json();
1350        let assertion = length_assertion_greater();
1351        let result = AssertionEvaluator::evaluate_assertion(&json, &assertion).unwrap();
1352
1353        assert!(result.passed);
1354    }
1355
1356    #[test]
1357    fn test_assertion_greater_than_or_equal_equal_case() {
1358        let json = get_test_json();
1359        let mut assertion = length_assertion_greater();
1360        assertion.expected_value = Value::Number(3.into());
1361
1362        let result = AssertionEvaluator::evaluate_assertion(&json, &assertion).unwrap();
1363
1364        assert!(result.passed);
1365    }
1366
1367    #[test]
1368    fn test_assertion_less_than_pass() {
1369        let json = get_test_json();
1370        let assertion = less_than_assertion();
1371        let result = AssertionEvaluator::evaluate_assertion(&json, &assertion).unwrap();
1372
1373        assert!(result.passed);
1374        assert_eq!(result.actual, json!(15));
1375    }
1376
1377    #[test]
1378    fn test_assertion_less_than_fail() {
1379        let json = get_test_json();
1380        let mut assertion = less_than_assertion();
1381        assertion.expected_value = Value::Number(10.into());
1382
1383        let result = AssertionEvaluator::evaluate_assertion(&json, &assertion).unwrap();
1384
1385        assert!(!result.passed);
1386    }
1387
1388    #[test]
1389    fn test_assertion_less_than_or_equal_pass() {
1390        let json = get_test_json();
1391        let assertion = length_assertion_less();
1392        let result = AssertionEvaluator::evaluate_assertion(&json, &assertion).unwrap();
1393
1394        assert!(result.passed);
1395    }
1396
1397    #[test]
1398    fn test_assertion_less_than_or_equal_equal_case() {
1399        let json = get_test_json();
1400        let mut assertion = length_assertion_less();
1401        assertion.expected_value = Value::Number(3.into());
1402
1403        let result = AssertionEvaluator::evaluate_assertion(&json, &assertion).unwrap();
1404
1405        assert!(result.passed);
1406    }
1407
1408    #[test]
1409    fn test_assertion_has_length_pass() {
1410        let json = get_test_json();
1411        let assertion = length_assertion();
1412        let result = AssertionEvaluator::evaluate_assertion(&json, &assertion).unwrap();
1413
1414        assert!(result.passed);
1415    }
1416
1417    #[test]
1418    fn test_assertion_has_length_fail() {
1419        let json = get_test_json();
1420        let mut assertion = length_assertion();
1421        assertion.expected_value = Value::Number(5.into());
1422
1423        let result = AssertionEvaluator::evaluate_assertion(&json, &assertion).unwrap();
1424
1425        assert!(!result.passed);
1426    }
1427
1428    #[test]
1429    fn test_assertion_has_length_string() {
1430        let json = json!({"name": "test_user"});
1431        let assertion = AssertionTask {
1432            id: "name_length".to_string(),
1433            context_path: Some("name".to_string()),
1434            operator: ComparisonOperator::HasLengthEqual,
1435            expected_value: Value::Number(9.into()),
1436            description: Some("Name should have 9 characters".to_string()),
1437            task_type: EvaluationTaskType::Assertion,
1438            depends_on: vec![],
1439            item_context_path: None,
1440            result: None,
1441            condition: false,
1442        };
1443
1444        let result = AssertionEvaluator::evaluate_assertion(&json, &assertion).unwrap();
1445
1446        assert!(result.passed);
1447    }
1448
1449    #[test]
1450    fn test_assertion_contains_array_pass() {
1451        let json = get_test_json();
1452        let assertion = contains_assertion();
1453        let result = AssertionEvaluator::evaluate_assertion(&json, &assertion).unwrap();
1454
1455        assert!(result.passed);
1456    }
1457
1458    #[test]
1459    fn test_assertion_contains_array_fail() {
1460        let json = get_test_json();
1461        let mut assertion = contains_assertion();
1462        assertion.expected_value = Value::String("frontend".to_string());
1463
1464        let result = AssertionEvaluator::evaluate_assertion(&json, &assertion).unwrap();
1465
1466        assert!(!result.passed);
1467    }
1468
1469    #[test]
1470    fn test_assertion_contains_string_pass() {
1471        let json = get_test_json();
1472        let assertion = AssertionTask {
1473            id: "status_contains_prog".to_string(),
1474            context_path: Some("status".to_string()),
1475            operator: ComparisonOperator::Contains,
1476            expected_value: Value::String("progress".to_string()),
1477            description: Some("Status should contain 'progress'".to_string()),
1478            task_type: EvaluationTaskType::Assertion,
1479            depends_on: vec![],
1480            item_context_path: None,
1481            result: None,
1482            condition: false,
1483        };
1484
1485        let result = AssertionEvaluator::evaluate_assertion(&json, &assertion).unwrap();
1486
1487        assert!(result.passed);
1488    }
1489
1490    #[test]
1491    fn test_assertion_not_contains_pass() {
1492        let json = get_test_json();
1493        let assertion = not_contains_assertion();
1494        let result = AssertionEvaluator::evaluate_assertion(&json, &assertion).unwrap();
1495
1496        assert!(result.passed);
1497    }
1498
1499    #[test]
1500    fn test_assertion_not_contains_fail() {
1501        let json = get_test_json();
1502        let mut assertion = not_contains_assertion();
1503        assertion.expected_value = Value::String("backend".to_string());
1504
1505        let result = AssertionEvaluator::evaluate_assertion(&json, &assertion).unwrap();
1506
1507        assert!(!result.passed);
1508    }
1509
1510    #[test]
1511    fn test_assertion_starts_with_pass() {
1512        let json = get_test_json();
1513        let assertion = starts_with_assertion();
1514        let result = AssertionEvaluator::evaluate_assertion(&json, &assertion).unwrap();
1515
1516        assert!(result.passed);
1517    }
1518
1519    #[test]
1520    fn test_assertion_starts_with_fail() {
1521        let json = get_test_json();
1522        let mut assertion = starts_with_assertion();
1523        assertion.expected_value = Value::String("completed".to_string());
1524
1525        let result = AssertionEvaluator::evaluate_assertion(&json, &assertion).unwrap();
1526
1527        assert!(!result.passed);
1528    }
1529
1530    #[test]
1531    fn test_assertion_ends_with_pass() {
1532        let json = get_test_json();
1533        let assertion = ends_with_assertion();
1534        let result = AssertionEvaluator::evaluate_assertion(&json, &assertion).unwrap();
1535
1536        assert!(result.passed);
1537    }
1538
1539    #[test]
1540    fn test_assertion_ends_with_fail() {
1541        let json = get_test_json();
1542        let mut assertion = ends_with_assertion();
1543        assertion.expected_value = Value::String("_pending".to_string());
1544
1545        let result = AssertionEvaluator::evaluate_assertion(&json, &assertion).unwrap();
1546
1547        assert!(!result.passed);
1548    }
1549
1550    #[test]
1551    fn test_assertion_matches_pass() {
1552        let json = get_test_json();
1553        let assertion = match_assertion();
1554        let result = AssertionEvaluator::evaluate_assertion(&json, &assertion).unwrap();
1555
1556        assert!(result.passed);
1557    }
1558
1559    #[test]
1560    fn test_assertion_matches_fail() {
1561        let json = get_test_json();
1562        let mut assertion = match_assertion();
1563        assertion.expected_value = Value::String(r"^completed.*$".to_string());
1564
1565        let result = AssertionEvaluator::evaluate_assertion(&json, &assertion).unwrap();
1566
1567        assert!(!result.passed);
1568    }
1569
1570    #[test]
1571    fn test_assertion_matches_complex_regex() {
1572        let json = get_test_json();
1573        let assertion = AssertionTask {
1574            id: "user_format".to_string(),
1575            context_path: Some("metadata.created_by".to_string()),
1576            operator: ComparisonOperator::Matches,
1577            expected_value: Value::String(r"^user_\d+$".to_string()),
1578            description: Some("User ID should match format user_###".to_string()),
1579            task_type: EvaluationTaskType::Assertion,
1580            depends_on: vec![],
1581            item_context_path: None,
1582            result: None,
1583            condition: false,
1584        };
1585
1586        let result = AssertionEvaluator::evaluate_assertion(&json, &assertion).unwrap();
1587
1588        assert!(result.passed);
1589    }
1590
1591    #[test]
1592    fn test_assertion_no_field_path_evaluates_root() {
1593        let json = json!({"status": "active"});
1594        let assertion = AssertionTask {
1595            id: "root_check".to_string(),
1596            context_path: None,
1597            operator: ComparisonOperator::Equals,
1598            expected_value: json!({"status": "active"}),
1599            description: Some("Check entire root object".to_string()),
1600            task_type: EvaluationTaskType::Assertion,
1601            depends_on: vec![],
1602            result: None,
1603            condition: false,
1604            item_context_path: None,
1605        };
1606
1607        let result = AssertionEvaluator::evaluate_assertion(&json, &assertion).unwrap();
1608
1609        assert!(result.passed);
1610    }
1611
1612    #[test]
1613    fn test_assertion_empty_array_length() {
1614        let json = get_test_json();
1615        let assertion = AssertionTask {
1616            id: "empty_array_length".to_string(),
1617            context_path: Some("empty_array".to_string()),
1618            operator: ComparisonOperator::HasLengthEqual,
1619            expected_value: Value::Number(0.into()),
1620            description: Some("Empty array should have length 0".to_string()),
1621            task_type: EvaluationTaskType::Assertion,
1622            depends_on: vec![],
1623            item_context_path: None,
1624            result: None,
1625            condition: false,
1626        };
1627
1628        let result = AssertionEvaluator::evaluate_assertion(&json, &assertion).unwrap();
1629
1630        assert!(result.passed);
1631    }
1632
1633    #[test]
1634    fn test_assertion_numeric_comparison_with_floats() {
1635        let json = json!({"score": 85.5});
1636        let assertion = AssertionTask {
1637            id: "score_check".to_string(),
1638            context_path: Some("score".to_string()),
1639            operator: ComparisonOperator::GreaterThanOrEqual,
1640            expected_value: json!(85.0),
1641            description: Some("Score should be at least 85".to_string()),
1642            task_type: EvaluationTaskType::Assertion,
1643            depends_on: vec![],
1644            item_context_path: None,
1645            result: None,
1646            condition: false,
1647        };
1648
1649        let result = AssertionEvaluator::evaluate_assertion(&json, &assertion).unwrap();
1650
1651        assert!(result.passed);
1652    }
1653
1654    #[test]
1655    fn test_assertion_error_field_not_found() {
1656        let json = get_test_json();
1657        let assertion = AssertionTask {
1658            id: "missing_field".to_string(),
1659            context_path: Some("nonexistent.field".to_string()),
1660            operator: ComparisonOperator::Equals,
1661            expected_value: Value::String("value".to_string()),
1662            description: Some("Should fail with field not found".to_string()),
1663            task_type: EvaluationTaskType::Assertion,
1664            depends_on: vec![],
1665            item_context_path: None,
1666            result: None,
1667            condition: false,
1668        };
1669
1670        let result = AssertionEvaluator::evaluate_assertion(&json, &assertion);
1671
1672        assert!(result.is_err());
1673        assert!(result.unwrap_err().to_string().contains("not found"));
1674    }
1675
1676    #[test]
1677    fn test_assertion_error_invalid_regex() {
1678        let json = get_test_json();
1679        let assertion = AssertionTask {
1680            id: "bad_regex".to_string(),
1681            context_path: Some("status".to_string()),
1682            operator: ComparisonOperator::Matches,
1683            expected_value: Value::String("[invalid(".to_string()),
1684            description: Some("Invalid regex pattern".to_string()),
1685            task_type: EvaluationTaskType::Assertion,
1686            depends_on: vec![],
1687            item_context_path: None,
1688            result: None,
1689            condition: false,
1690        };
1691
1692        let result = AssertionEvaluator::evaluate_assertion(&json, &assertion);
1693
1694        assert!(result.is_err());
1695    }
1696
1697    #[test]
1698    fn test_assertion_error_type_mismatch_starts_with() {
1699        let json = get_test_json();
1700        let assertion = AssertionTask {
1701            id: "type_mismatch".to_string(),
1702            context_path: Some("counts.total".to_string()),
1703            operator: ComparisonOperator::StartsWith,
1704            expected_value: Value::String("4".to_string()),
1705            description: Some("Cannot use StartsWith on number".to_string()),
1706            task_type: EvaluationTaskType::Assertion,
1707            depends_on: vec![],
1708            item_context_path: None,
1709            result: None,
1710            condition: false,
1711        };
1712
1713        let result = AssertionEvaluator::evaluate_assertion(&json, &assertion);
1714
1715        assert!(result.is_err());
1716    }
1717
1718    #[test]
1719    fn test_assertion_error_type_mismatch_numeric_comparison() {
1720        let json = json!({"value": "not_a_number"});
1721        let assertion = AssertionTask {
1722            id: "numeric_on_string".to_string(),
1723            context_path: Some("value".to_string()),
1724            operator: ComparisonOperator::GreaterThan,
1725            expected_value: Value::Number(10.into()),
1726            description: Some("Cannot compare string with number".to_string()),
1727            task_type: EvaluationTaskType::Assertion,
1728            depends_on: vec![],
1729            item_context_path: None,
1730            result: None,
1731            condition: false,
1732        };
1733
1734        let result = AssertionEvaluator::evaluate_assertion(&json, &assertion).unwrap();
1735        assert!(!result.passed);
1736    }
1737
1738    #[test]
1739    fn test_is_numeric_pass() {
1740        let json = json!({"value": 42});
1741        let assertion = AssertionTask {
1742            id: "type_check".to_string(),
1743            context_path: Some("value".to_string()),
1744            operator: ComparisonOperator::IsNumeric,
1745            expected_value: Value::Bool(true),
1746            description: Some("Value should be numeric".to_string()),
1747            task_type: EvaluationTaskType::Assertion,
1748            depends_on: vec![],
1749            item_context_path: None,
1750            result: None,
1751            condition: false,
1752        };
1753
1754        let result = AssertionEvaluator::evaluate_assertion(&json, &assertion).unwrap();
1755        assert!(result.passed);
1756    }
1757
1758    #[test]
1759    fn test_is_string_pass() {
1760        let json = json!({"value": "hello"});
1761        let assertion = AssertionTask {
1762            id: "type_check".to_string(),
1763            context_path: Some("value".to_string()),
1764            operator: ComparisonOperator::IsString,
1765            expected_value: Value::Bool(true),
1766            description: None,
1767            task_type: EvaluationTaskType::Assertion,
1768            depends_on: vec![],
1769            item_context_path: None,
1770            result: None,
1771            condition: false,
1772        };
1773
1774        let result = AssertionEvaluator::evaluate_assertion(&json, &assertion).unwrap();
1775        assert!(result.passed);
1776    }
1777
1778    #[test]
1779    fn test_is_array_pass() {
1780        let json = json!({"value": [1, 2, 3]});
1781        let assertion = AssertionTask {
1782            id: "type_check".to_string(),
1783            context_path: Some("value".to_string()),
1784            operator: ComparisonOperator::IsArray,
1785            expected_value: Value::Bool(true),
1786            description: None,
1787            task_type: EvaluationTaskType::Assertion,
1788            depends_on: vec![],
1789            item_context_path: None,
1790            result: None,
1791            condition: false,
1792        };
1793
1794        let result = AssertionEvaluator::evaluate_assertion(&json, &assertion).unwrap();
1795        assert!(result.passed);
1796    }
1797
1798    // Format Validation Tests
1799    #[test]
1800    fn test_is_email_pass() {
1801        let json = json!({"email": "user@example.com"});
1802        let assertion = AssertionTask {
1803            id: "email_check".to_string(),
1804            context_path: Some("email".to_string()),
1805            operator: ComparisonOperator::IsEmail,
1806            expected_value: Value::Bool(true),
1807            description: None,
1808            task_type: EvaluationTaskType::Assertion,
1809            depends_on: vec![],
1810            item_context_path: None,
1811            result: None,
1812            condition: false,
1813        };
1814
1815        let result = AssertionEvaluator::evaluate_assertion(&json, &assertion).unwrap();
1816        assert!(result.passed);
1817    }
1818
1819    #[test]
1820    fn test_is_email_fail() {
1821        let json = json!({"email": "not-an-email"});
1822        let assertion = AssertionTask {
1823            id: "email_check".to_string(),
1824            context_path: Some("email".to_string()),
1825            operator: ComparisonOperator::IsEmail,
1826            expected_value: Value::Bool(true),
1827            description: None,
1828            task_type: EvaluationTaskType::Assertion,
1829            depends_on: vec![],
1830            item_context_path: None,
1831            result: None,
1832            condition: false,
1833        };
1834
1835        let result = AssertionEvaluator::evaluate_assertion(&json, &assertion).unwrap();
1836        assert!(!result.passed);
1837    }
1838
1839    #[test]
1840    fn test_is_url_pass() {
1841        let json = json!({"url": "https://example.com"});
1842        let assertion = AssertionTask {
1843            id: "url_check".to_string(),
1844            context_path: Some("url".to_string()),
1845            operator: ComparisonOperator::IsUrl,
1846            expected_value: Value::Bool(true),
1847            description: None,
1848            task_type: EvaluationTaskType::Assertion,
1849            depends_on: vec![],
1850            item_context_path: None,
1851            result: None,
1852            condition: false,
1853        };
1854
1855        let result = AssertionEvaluator::evaluate_assertion(&json, &assertion).unwrap();
1856        assert!(result.passed);
1857    }
1858
1859    #[test]
1860    fn test_is_uuid_pass() {
1861        let json = json!({"id": "550e8400-e29b-41d4-a716-446655440000"});
1862        let assertion = AssertionTask {
1863            id: "uuid_check".to_string(),
1864            context_path: Some("id".to_string()),
1865            operator: ComparisonOperator::IsUuid,
1866            expected_value: Value::Bool(true),
1867            description: None,
1868            task_type: EvaluationTaskType::Assertion,
1869            depends_on: vec![],
1870            item_context_path: None,
1871            result: None,
1872            condition: false,
1873        };
1874
1875        let result = AssertionEvaluator::evaluate_assertion(&json, &assertion).unwrap();
1876        assert!(result.passed);
1877    }
1878
1879    #[test]
1880    fn test_is_iso8601_pass() {
1881        let json = json!({"timestamp": "2024-01-05T10:30:00Z"});
1882        let assertion = AssertionTask {
1883            id: "iso_check".to_string(),
1884            context_path: Some("timestamp".to_string()),
1885            operator: ComparisonOperator::IsIso8601,
1886            expected_value: Value::Bool(true),
1887            description: None,
1888            task_type: EvaluationTaskType::Assertion,
1889            depends_on: vec![],
1890            item_context_path: None,
1891            result: None,
1892            condition: false,
1893        };
1894
1895        let result = AssertionEvaluator::evaluate_assertion(&json, &assertion).unwrap();
1896        assert!(result.passed);
1897    }
1898
1899    #[test]
1900    fn test_is_json_pass() {
1901        let json = json!({"data": r#"{"key": "value"}"#});
1902        let assertion = AssertionTask {
1903            id: "json_check".to_string(),
1904            context_path: Some("data".to_string()),
1905            operator: ComparisonOperator::IsJson,
1906            expected_value: Value::Bool(true),
1907            description: None,
1908            task_type: EvaluationTaskType::Assertion,
1909            depends_on: vec![],
1910            item_context_path: None,
1911            result: None,
1912            condition: false,
1913        };
1914
1915        let result = AssertionEvaluator::evaluate_assertion(&json, &assertion).unwrap();
1916        assert!(result.passed);
1917    }
1918
1919    // Range Tests
1920    #[test]
1921    fn test_in_range_pass() {
1922        let json = json!({"score": 75});
1923        let assertion = AssertionTask {
1924            id: "range_check".to_string(),
1925            context_path: Some("score".to_string()),
1926            operator: ComparisonOperator::InRange,
1927            expected_value: json!([0, 100]),
1928            description: None,
1929            task_type: EvaluationTaskType::Assertion,
1930            depends_on: vec![],
1931            item_context_path: None,
1932            result: None,
1933            condition: false,
1934        };
1935
1936        let result = AssertionEvaluator::evaluate_assertion(&json, &assertion).unwrap();
1937        assert!(result.passed);
1938    }
1939
1940    #[test]
1941    fn test_in_range_fail() {
1942        let json = json!({"score": 150});
1943        let assertion = AssertionTask {
1944            id: "range_check".to_string(),
1945            context_path: Some("score".to_string()),
1946            operator: ComparisonOperator::InRange,
1947            expected_value: json!([0, 100]),
1948            description: None,
1949            task_type: EvaluationTaskType::Assertion,
1950            depends_on: vec![],
1951            item_context_path: None,
1952            result: None,
1953            condition: false,
1954        };
1955
1956        let result = AssertionEvaluator::evaluate_assertion(&json, &assertion).unwrap();
1957        assert!(!result.passed);
1958    }
1959
1960    #[test]
1961    fn test_is_positive_pass() {
1962        let json = json!({"value": 42});
1963        let assertion = AssertionTask {
1964            id: "positive_check".to_string(),
1965            context_path: Some("value".to_string()),
1966            operator: ComparisonOperator::IsPositive,
1967            expected_value: Value::Bool(true),
1968            description: None,
1969            task_type: EvaluationTaskType::Assertion,
1970            depends_on: vec![],
1971            item_context_path: None,
1972            result: None,
1973            condition: false,
1974        };
1975
1976        let result = AssertionEvaluator::evaluate_assertion(&json, &assertion).unwrap();
1977        assert!(result.passed);
1978    }
1979
1980    #[test]
1981    fn test_is_negative_pass() {
1982        let json = json!({"value": -42});
1983        let assertion = AssertionTask {
1984            id: "negative_check".to_string(),
1985            context_path: Some("value".to_string()),
1986            operator: ComparisonOperator::IsNegative,
1987            expected_value: Value::Bool(true),
1988            description: None,
1989            task_type: EvaluationTaskType::Assertion,
1990            depends_on: vec![],
1991            item_context_path: None,
1992            result: None,
1993            condition: false,
1994        };
1995
1996        let result = AssertionEvaluator::evaluate_assertion(&json, &assertion).unwrap();
1997        assert!(result.passed);
1998    }
1999
2000    // Collection Tests
2001    #[test]
2002    fn test_contains_all_pass() {
2003        let json = json!({"tags": ["rust", "python", "javascript", "go"]});
2004        let assertion = AssertionTask {
2005            id: "contains_all_check".to_string(),
2006            context_path: Some("tags".to_string()),
2007            operator: ComparisonOperator::ContainsAll,
2008            expected_value: json!(["rust", "python"]),
2009            description: None,
2010            task_type: EvaluationTaskType::Assertion,
2011            depends_on: vec![],
2012            item_context_path: None,
2013            result: None,
2014            condition: false,
2015        };
2016
2017        let result = AssertionEvaluator::evaluate_assertion(&json, &assertion).unwrap();
2018        assert!(result.passed);
2019    }
2020
2021    #[test]
2022    fn test_contains_any_pass() {
2023        let json = json!({"tags": ["rust", "python"]});
2024        let assertion = AssertionTask {
2025            id: "contains_any_check".to_string(),
2026            context_path: Some("tags".to_string()),
2027            operator: ComparisonOperator::ContainsAny,
2028            expected_value: json!(["python", "java", "c++"]),
2029            description: None,
2030            task_type: EvaluationTaskType::Assertion,
2031            depends_on: vec![],
2032            item_context_path: None,
2033            result: None,
2034            condition: false,
2035        };
2036
2037        let result = AssertionEvaluator::evaluate_assertion(&json, &assertion).unwrap();
2038        assert!(result.passed);
2039    }
2040
2041    #[test]
2042    fn test_is_empty_pass() {
2043        let json = json!({"list": []});
2044        let assertion = AssertionTask {
2045            id: "empty_check".to_string(),
2046            context_path: Some("list".to_string()),
2047            operator: ComparisonOperator::IsEmpty,
2048            expected_value: Value::Bool(true),
2049            description: None,
2050            task_type: EvaluationTaskType::Assertion,
2051            depends_on: vec![],
2052            item_context_path: None,
2053            result: None,
2054            condition: false,
2055        };
2056
2057        let result = AssertionEvaluator::evaluate_assertion(&json, &assertion).unwrap();
2058        assert!(result.passed);
2059    }
2060
2061    #[test]
2062    fn test_has_unique_items_pass() {
2063        let json = json!({"items": [1, 2, 3, 4]});
2064        let assertion = AssertionTask {
2065            id: "unique_check".to_string(),
2066            context_path: Some("items".to_string()),
2067            operator: ComparisonOperator::HasUniqueItems,
2068            expected_value: Value::Bool(true),
2069            description: None,
2070            task_type: EvaluationTaskType::Assertion,
2071            depends_on: vec![],
2072            item_context_path: None,
2073            result: None,
2074            condition: false,
2075        };
2076
2077        let result = AssertionEvaluator::evaluate_assertion(&json, &assertion).unwrap();
2078        assert!(result.passed);
2079    }
2080
2081    #[test]
2082    fn test_has_unique_items_fail() {
2083        let json = json!({"items": [1, 2, 2, 3]});
2084        let assertion = AssertionTask {
2085            id: "unique_check".to_string(),
2086            context_path: Some("items".to_string()),
2087            operator: ComparisonOperator::HasUniqueItems,
2088            expected_value: Value::Bool(true),
2089            description: None,
2090            task_type: EvaluationTaskType::Assertion,
2091            depends_on: vec![],
2092            item_context_path: None,
2093            result: None,
2094            condition: false,
2095        };
2096
2097        let result = AssertionEvaluator::evaluate_assertion(&json, &assertion).unwrap();
2098        assert!(!result.passed);
2099    }
2100
2101    // String Tests
2102    #[test]
2103    fn test_is_alphabetic_pass() {
2104        let json = json!({"text": "HelloWorld"});
2105        let assertion = AssertionTask {
2106            id: "alpha_check".to_string(),
2107            context_path: Some("text".to_string()),
2108            operator: ComparisonOperator::IsAlphabetic,
2109            expected_value: Value::Bool(true),
2110            description: None,
2111            task_type: EvaluationTaskType::Assertion,
2112            depends_on: vec![],
2113            item_context_path: None,
2114            result: None,
2115            condition: false,
2116        };
2117
2118        let result = AssertionEvaluator::evaluate_assertion(&json, &assertion).unwrap();
2119        assert!(result.passed);
2120    }
2121
2122    #[test]
2123    fn test_is_alphanumeric_pass() {
2124        let json = json!({"text": "Hello123"});
2125        let assertion = AssertionTask {
2126            id: "alphanum_check".to_string(),
2127            context_path: Some("text".to_string()),
2128            operator: ComparisonOperator::IsAlphanumeric,
2129            expected_value: Value::Bool(true),
2130            description: None,
2131            task_type: EvaluationTaskType::Assertion,
2132            depends_on: vec![],
2133            item_context_path: None,
2134            result: None,
2135            condition: false,
2136        };
2137
2138        let result = AssertionEvaluator::evaluate_assertion(&json, &assertion).unwrap();
2139        assert!(result.passed);
2140    }
2141
2142    #[test]
2143    fn test_is_lowercase_pass() {
2144        let json = json!({"text": "hello world"});
2145        let assertion = AssertionTask {
2146            id: "lowercase_check".to_string(),
2147            context_path: Some("text".to_string()),
2148            operator: ComparisonOperator::IsLowerCase,
2149            expected_value: Value::Bool(true),
2150            description: None,
2151            task_type: EvaluationTaskType::Assertion,
2152            depends_on: vec![],
2153            item_context_path: None,
2154            result: None,
2155            condition: false,
2156        };
2157
2158        let result = AssertionEvaluator::evaluate_assertion(&json, &assertion).unwrap();
2159        assert!(result.passed);
2160    }
2161
2162    #[test]
2163    fn test_is_uppercase_pass() {
2164        let json = json!({"text": "HELLO WORLD"});
2165        let assertion = AssertionTask {
2166            id: "uppercase_check".to_string(),
2167            context_path: Some("text".to_string()),
2168            operator: ComparisonOperator::IsUpperCase,
2169            expected_value: Value::Bool(true),
2170            description: None,
2171            task_type: EvaluationTaskType::Assertion,
2172            depends_on: vec![],
2173            item_context_path: None,
2174            result: None,
2175            condition: false,
2176        };
2177
2178        let result = AssertionEvaluator::evaluate_assertion(&json, &assertion).unwrap();
2179        assert!(result.passed);
2180    }
2181
2182    #[test]
2183    fn test_contains_word_pass() {
2184        let json = json!({"text": "The quick brown fox"});
2185        let assertion = AssertionTask {
2186            id: "word_check".to_string(),
2187            context_path: Some("text".to_string()),
2188            operator: ComparisonOperator::ContainsWord,
2189            expected_value: Value::String("quick".to_string()),
2190            description: None,
2191            task_type: EvaluationTaskType::Assertion,
2192            depends_on: vec![],
2193            item_context_path: None,
2194            result: None,
2195            condition: false,
2196        };
2197
2198        let result = AssertionEvaluator::evaluate_assertion(&json, &assertion).unwrap();
2199        assert!(result.passed);
2200    }
2201
2202    #[test]
2203    fn test_contains_word_fail() {
2204        let json = json!({"text": "The quickly brown fox"});
2205        let assertion = AssertionTask {
2206            id: "word_check".to_string(),
2207            context_path: Some("text".to_string()),
2208            operator: ComparisonOperator::ContainsWord,
2209            expected_value: Value::String("quick".to_string()),
2210            description: None,
2211            task_type: EvaluationTaskType::Assertion,
2212            depends_on: vec![],
2213            item_context_path: None,
2214            result: None,
2215            condition: false,
2216        };
2217
2218        let result = AssertionEvaluator::evaluate_assertion(&json, &assertion).unwrap();
2219        assert!(!result.passed);
2220    }
2221
2222    // Tolerance Tests
2223    #[test]
2224    fn test_approximately_equals_pass() {
2225        let json = json!({"value": 100.5});
2226        let assertion = AssertionTask {
2227            id: "approx_check".to_string(),
2228            context_path: Some("value".to_string()),
2229            operator: ComparisonOperator::ApproximatelyEquals,
2230            expected_value: json!([100.0, 1.0]),
2231            description: None,
2232            task_type: EvaluationTaskType::Assertion,
2233            depends_on: vec![],
2234            item_context_path: None,
2235            result: None,
2236            condition: false,
2237        };
2238
2239        let result = AssertionEvaluator::evaluate_assertion(&json, &assertion).unwrap();
2240        assert!(result.passed);
2241    }
2242
2243    #[test]
2244    fn test_approximately_equals_fail() {
2245        let json = json!({"value": 102.0});
2246        let assertion = AssertionTask {
2247            id: "approx_check".to_string(),
2248            context_path: Some("value".to_string()),
2249            operator: ComparisonOperator::ApproximatelyEquals,
2250            expected_value: json!([100.0, 1.0]),
2251            description: None,
2252            task_type: EvaluationTaskType::Assertion,
2253            depends_on: vec![],
2254            item_context_path: None,
2255            result: None,
2256            condition: false,
2257        };
2258
2259        let result = AssertionEvaluator::evaluate_assertion(&json, &assertion).unwrap();
2260        assert!(!result.passed);
2261    }
2262
2263    // ── Array iteration tests ────────────────────────────────────────────────
2264
2265    /// Root is an array of objects; context_path extracts a numeric field from each item.
2266    /// All items satisfy the condition → PASS.
2267    #[test]
2268    fn test_array_of_objects_field_path_all_pass() {
2269        let json = json!([{"my_key": 10}, {"my_key": 7}]);
2270        let assertion = AssertionTask {
2271            id: "array_field_gte".to_string(),
2272            context_path: Some("my_key".to_string()),
2273            operator: ComparisonOperator::GreaterThanOrEqual,
2274            expected_value: json!(5),
2275            description: None,
2276            task_type: EvaluationTaskType::Assertion,
2277            depends_on: vec![],
2278            item_context_path: None,
2279            result: None,
2280            condition: false,
2281        };
2282
2283        let result = AssertionEvaluator::evaluate_assertion(&json, &assertion).unwrap();
2284        assert!(result.passed);
2285    }
2286
2287    /// One item fails the comparison → overall FAIL.
2288    #[test]
2289    fn test_array_of_objects_field_path_one_fails() {
2290        let json = json!([{"my_key": 10}, {"my_key": 3}]);
2291        let assertion = AssertionTask {
2292            id: "array_field_gte_fail".to_string(),
2293            context_path: Some("my_key".to_string()),
2294            operator: ComparisonOperator::GreaterThanOrEqual,
2295            expected_value: json!(5),
2296            description: None,
2297            task_type: EvaluationTaskType::Assertion,
2298            depends_on: vec![],
2299            item_context_path: None,
2300            result: None,
2301            condition: false,
2302        };
2303
2304        let result = AssertionEvaluator::evaluate_assertion(&json, &assertion).unwrap();
2305        assert!(!result.passed);
2306    }
2307
2308    /// Field is missing in one of the items → FAIL.
2309    #[test]
2310    fn test_array_of_objects_missing_field_fails() {
2311        let json = json!([{"my_key": 10}, {"other": 3}]);
2312        let assertion = AssertionTask {
2313            id: "missing_field".to_string(),
2314            context_path: Some("my_key".to_string()),
2315            operator: ComparisonOperator::GreaterThanOrEqual,
2316            expected_value: json!(5),
2317            description: None,
2318            task_type: EvaluationTaskType::Assertion,
2319            depends_on: vec![],
2320            item_context_path: None,
2321            result: None,
2322            condition: false,
2323        };
2324
2325        let result = AssertionEvaluator::evaluate_assertion(&json, &assertion).unwrap();
2326        assert!(!result.passed);
2327    }
2328
2329    /// Root is an array of scalars; no context_path; value operator iterates each item.
2330    #[test]
2331    fn test_array_of_scalars_no_field_path_all_pass() {
2332        let json = json!([10, 20, 30]);
2333        let assertion = AssertionTask {
2334            id: "scalar_gt".to_string(),
2335            context_path: None,
2336            operator: ComparisonOperator::GreaterThan,
2337            expected_value: json!(5),
2338            description: None,
2339            task_type: EvaluationTaskType::Assertion,
2340            depends_on: vec![],
2341            item_context_path: None,
2342            result: None,
2343            condition: false,
2344        };
2345
2346        let result = AssertionEvaluator::evaluate_assertion(&json, &assertion).unwrap();
2347        assert!(result.passed);
2348    }
2349
2350    /// Root is an array of scalars; one item fails → overall FAIL.
2351    #[test]
2352    fn test_array_of_scalars_no_field_path_one_fails() {
2353        let json = json!([10, 3, 30]);
2354        let assertion = AssertionTask {
2355            id: "scalar_gt_fail".to_string(),
2356            context_path: None,
2357            operator: ComparisonOperator::GreaterThan,
2358            expected_value: json!(5),
2359            description: None,
2360            task_type: EvaluationTaskType::Assertion,
2361            depends_on: vec![],
2362            item_context_path: None,
2363            result: None,
2364            condition: false,
2365        };
2366
2367        let result = AssertionEvaluator::evaluate_assertion(&json, &assertion).unwrap();
2368        assert!(!result.passed);
2369    }
2370
2371    /// Array of strings; IsEmail iterates and validates each item.
2372    #[test]
2373    fn test_array_of_strings_is_email_all_pass() {
2374        let json = json!(["alice@example.com", "bob@example.org"]);
2375        let assertion = AssertionTask {
2376            id: "email_check".to_string(),
2377            context_path: None,
2378            operator: ComparisonOperator::IsEmail,
2379            expected_value: json!(null),
2380            description: None,
2381            task_type: EvaluationTaskType::Assertion,
2382            depends_on: vec![],
2383            item_context_path: None,
2384            result: None,
2385            condition: false,
2386        };
2387
2388        let result = AssertionEvaluator::evaluate_assertion(&json, &assertion).unwrap();
2389        assert!(result.passed);
2390    }
2391
2392    /// IsNumeric is array-native (type-identity check): an array itself is not numeric → FAIL.
2393    #[test]
2394    fn test_array_of_numbers_is_numeric_fails() {
2395        let json = json!([5, 10]);
2396        let assertion = AssertionTask {
2397            id: "is_numeric_on_array".to_string(),
2398            context_path: None,
2399            operator: ComparisonOperator::IsNumeric,
2400            expected_value: json!(null),
2401            description: None,
2402            task_type: EvaluationTaskType::Assertion,
2403            depends_on: vec![],
2404            item_context_path: None,
2405            result: None,
2406            condition: false,
2407        };
2408
2409        let result = AssertionEvaluator::evaluate_assertion(&json, &assertion).unwrap();
2410        assert!(!result.passed);
2411    }
2412
2413    /// HasLength is array-native: operates on the array as a whole, not its items.
2414    #[test]
2415    fn test_array_has_length_native() {
2416        let json = json!([1, 2, 3]);
2417        let assertion = AssertionTask {
2418            id: "has_length".to_string(),
2419            context_path: None,
2420            operator: ComparisonOperator::HasLengthEqual,
2421            expected_value: json!(3),
2422            description: None,
2423            task_type: EvaluationTaskType::Assertion,
2424            depends_on: vec![],
2425            item_context_path: None,
2426            result: None,
2427            condition: false,
2428        };
2429
2430        let result = AssertionEvaluator::evaluate_assertion(&json, &assertion).unwrap();
2431        assert!(result.passed);
2432    }
2433
2434    /// context_path resolves to an array of scalars at a nested key; iteration applies.
2435    #[test]
2436    fn test_nested_array_via_field_path_iterates() {
2437        let json = json!({"scores": [8, 9, 7]});
2438        let assertion = AssertionTask {
2439            id: "nested_scores".to_string(),
2440            context_path: Some("scores".to_string()),
2441            operator: ComparisonOperator::GreaterThan,
2442            expected_value: json!(5),
2443            description: None,
2444            task_type: EvaluationTaskType::Assertion,
2445            depends_on: vec![],
2446            item_context_path: None,
2447            result: None,
2448            condition: false,
2449        };
2450
2451        let result = AssertionEvaluator::evaluate_assertion(&json, &assertion).unwrap();
2452        assert!(result.passed);
2453    }
2454
2455    /// Verify the exact passing scenario from the requirements.
2456    #[test]
2457    fn test_requirements_passing_example() {
2458        let json = json!([{"my_key": 10}]);
2459        let assertion = AssertionTask {
2460            id: "req_example".to_string(),
2461            context_path: Some("my_key".to_string()),
2462            operator: ComparisonOperator::GreaterThanOrEqual,
2463            expected_value: json!(5),
2464            description: None,
2465            task_type: EvaluationTaskType::Assertion,
2466            depends_on: vec![],
2467            item_context_path: None,
2468            result: None,
2469            condition: false,
2470        };
2471
2472        let result = AssertionEvaluator::evaluate_assertion(&json, &assertion).unwrap();
2473        assert!(result.passed);
2474    }
2475}