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
14impl FieldEvaluator {
17 #[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 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 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 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 pub fn evaluate_assertion<T: TaskAccessor>(
99 json_value: &Value,
100 assertion: &T,
101 ) -> Result<AssertionResult, EvaluationError> {
102 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 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 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 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 fn transform_for_comparison(
189 value: &Value,
190 operator: &ComparisonOperator,
191 ) -> Result<Value, EvaluationError> {
192 match operator {
193 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 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 _ => 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 (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 fn normalize_for_comparison(actual: &Value, expected: &Value) -> (Value, Value) {
245 match (actual, expected) {
246 (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 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 (serde_json::json!(a_num), serde_json::json!(e_num))
260 }
261 }
262 (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 _ => (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 match operator {
284 ComparisonOperator::Equals => {
286 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 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 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 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 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 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 ComparisonOperator::ApproximatelyEquals => {
376 Self::check_approximately_equals(actual, expected)
377 }
378 }
379 }
380
381 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 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 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 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 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 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 fn is_array_native_operator(operator: &ComparisonOperator) -> bool {
661 matches!(
662 operator,
663 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 | ComparisonOperator::Contains
678 | ComparisonOperator::IsNumeric
680 | ComparisonOperator::IsString
681 | ComparisonOperator::IsBoolean
682 | ComparisonOperator::IsNull
683 | ComparisonOperator::IsArray
684 | ComparisonOperator::IsObject
685 )
686 }
687
688 #[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 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 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 let json = json!({
1221 "tasks": ["setup_database", "create_api", "write_tests"],
1222 "status": "in_progress"
1223 });
1224
1225 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 let first_task = FieldEvaluator::extract_field_value(&json, "tasks[0]").unwrap();
1232 assert_eq!(*first_task, json!("setup_database"));
1233
1234 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 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 let sentiment = FieldEvaluator::extract_field_value(&json, "analysis.sentiment").unwrap();
1265 assert_eq!(*sentiment, json!("positive"));
1266
1267 let first_action =
1269 FieldEvaluator::extract_field_value(&json, "recommendations[0].action").unwrap();
1270 assert_eq!(*first_action, json!("increase_investment"));
1271
1272 let confidence = FieldEvaluator::extract_field_value(&json, "analysis.confidence").unwrap();
1274 assert_eq!(*confidence, json!(0.85));
1275 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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}