phlow_runtime/
test_runner.rs

1use crate::loader::{Loader, load_module};
2use crate::settings::Settings;
3use crossbeam::channel;
4use log::{debug, error};
5use phlow_engine::phs::{self, build_engine};
6use phlow_engine::script::Script;
7use phlow_engine::{Context, Phlow};
8use phlow_sdk::otel::init_tracing_subscriber;
9use phlow_sdk::prelude::json;
10use phlow_sdk::structs::{ModulePackage, ModuleSetup, Modules};
11use phlow_sdk::valu3::prelude::*;
12use phlow_sdk::valu3::value::Value;
13use std::collections::HashMap;
14use std::fmt::{Debug, Write};
15use std::sync::Arc;
16use tokio::sync::{Mutex, oneshot};
17
18#[derive(Debug, Clone)]
19struct SingleTestReport {
20    ok: bool,
21    message: String,
22    main: Value,
23    initial_payload: Value,
24    result: Value,
25}
26
27#[derive(Debug)]
28#[allow(dead_code)]
29pub struct TestResult {
30    pub index: usize,
31    pub passed: bool,
32    pub message: String,
33    pub describe: Option<String>,
34}
35
36#[derive(Debug)]
37#[allow(dead_code)]
38pub struct TestSummary {
39    pub total: usize,
40    pub passed: usize,
41    pub failed: usize,
42    pub results: Vec<TestResult>,
43}
44
45pub async fn run_tests(
46    loader: Loader,
47    test_filter: Option<&str>,
48    settings: Settings,
49) -> Result<TestSummary, String> {
50    debug!("run_tests");
51    // Get tests from loader.tests
52    let tests = loader
53        .tests
54        .as_ref()
55        .ok_or("No tests found in the phlow file")?;
56    let steps = &loader.steps;
57
58    if !tests.is_array() {
59        return Err(format!("Tests must be an array, got: {:?}", tests));
60    }
61
62    let test_cases = tests.as_array().unwrap();
63
64    // Helpers to support nested describes/its
65    fn is_group(v: &Value) -> bool {
66        v.get("tests").map(|t| t.is_array()).unwrap_or(false)
67    }
68
69    fn group_name(v: &Value) -> Option<String> {
70        v.get("describe")
71            .or_else(|| v.get("name"))
72            .map(|s| s.as_string())
73    }
74
75    fn leaf_title(v: &Value) -> Option<String> {
76        v.get("it")
77            .or_else(|| v.get("describe"))
78            .map(|s| s.as_string())
79    }
80
81    fn path_matches_filter(path: &[String], title: &str, filter: &str) -> bool {
82        let mut full = path.join(" › ");
83        if !full.is_empty() {
84            full.push_str(" › ");
85        }
86        full.push_str(title);
87        full.contains(filter)
88    }
89
90    fn count_leafs(items: &Value, filter: Option<&str>, ancestors: &Vec<String>) -> usize {
91        let mut count = 0usize;
92        if let Some(arr) = items.as_array() {
93            for item in &arr.values {
94                if is_group(item) {
95                    let mut new_path = ancestors.clone();
96                    if let Some(name) = group_name(item) {
97                        new_path.push(name);
98                    }
99                    count += count_leafs(&item.get("tests").unwrap(), filter, &new_path);
100                } else {
101                    // leaf
102                    let title = leaf_title(item).unwrap_or_else(|| "".to_string());
103                    if let Some(f) = filter {
104                        if path_matches_filter(ancestors, &title, f) {
105                            count += 1;
106                        }
107                    } else {
108                        count += 1;
109                    }
110                }
111            }
112        }
113        count
114    }
115
116    // Count total leaf tests considering the optional filter
117    let total = count_leafs(tests, test_filter, &Vec::new());
118
119    if total == 0 {
120        if let Some(filter) = test_filter {
121            println!("⚠️  No tests match filter: '{}'", filter);
122        } else {
123            println!("⚠️  No tests to run");
124        }
125
126        return Ok(TestSummary {
127            total: 0,
128            passed: 0,
129            failed: 0,
130            results: Vec::new(),
131        });
132    }
133
134    if let Some(filter) = test_filter {
135        println!(
136            "🧪 Running {} test(s) matching '{}' (out of {} total)...",
137            total,
138            filter,
139            test_cases.len()
140        );
141    } else {
142        println!("🧪 Running {} test(s)...", total);
143    }
144    println!();
145
146    // Load modules following the same pattern as Runtime::run
147    let modules = load_modules_like_runtime(&loader, settings)
148        .await
149        .map_err(|e| format!("Failed to load modules for tests: {}", e))?;
150
151    // Create flow from steps
152    let workflow = json!({
153        "steps": steps
154    });
155
156    let phlow = Phlow::try_from_value(&workflow, Some(modules))
157        .map_err(|e| format!("Failed to create phlow: {}", e))?;
158
159    // Run tests (com suporte a describe aninhado) usando uma lista de ações síncrona
160    let mut results = Vec::new();
161    let mut passed = 0;
162    let mut executed = 0usize;
163    // Global "test" map shared across tests
164    let tests_global = Arc::new(Mutex::new(json!({})));
165    let engine = build_engine(None);
166
167    enum Action {
168        Heading {
169            name: String,
170            depth: usize,
171        },
172        Test {
173            case: Value,
174            path: Vec<String>,
175            title: String,
176            depth: usize,
177        },
178    }
179
180    fn build_actions(
181        items: &Value,
182        filter: Option<&str>,
183        path: &mut Vec<String>,
184        depth: usize,
185        out: &mut Vec<Action>,
186    ) {
187        if let Some(arr) = items.as_array() {
188            for item in &arr.values {
189                if is_group(item) {
190                    let name = group_name(item).unwrap_or_else(|| "(group)".to_string());
191                    // Check if any leaf inside matches filter
192                    let group_has = {
193                        fn inner_count(v: &Value, f: Option<&str>, mut p: Vec<String>) -> usize {
194                            let mut c = 0usize;
195                            if let Some(name) = group_name(v) {
196                                p.push(name);
197                            }
198                            if let Some(ts) = v.get("tests").and_then(|t| t.as_array()) {
199                                for x in &ts.values {
200                                    if is_group(x) {
201                                        c += inner_count(x, f, p.clone());
202                                    } else {
203                                        let title = leaf_title(x).unwrap_or_else(|| "".to_string());
204                                        if let Some(ff) = f {
205                                            let mut s = p.join(" › ");
206                                            if !s.is_empty() {
207                                                s.push_str(" › ");
208                                            }
209                                            s.push_str(&title);
210                                            if s.contains(ff) {
211                                                c += 1;
212                                            }
213                                        } else {
214                                            c += 1;
215                                        }
216                                    }
217                                }
218                            }
219                            c
220                        }
221                        inner_count(item, filter, path.clone())
222                    };
223                    if group_has == 0 {
224                        continue;
225                    }
226                    out.push(Action::Heading {
227                        name: name.clone(),
228                        depth,
229                    });
230                    path.push(name);
231                    build_actions(&item.get("tests").unwrap(), filter, path, depth + 1, out);
232                    path.pop();
233                } else {
234                    let title = leaf_title(item).unwrap_or_else(|| "(test)".to_string());
235                    let mut full = path.join(" › ");
236                    if !full.is_empty() {
237                        full.push_str(" › ");
238                    }
239                    full.push_str(&title);
240                    if let Some(f) = filter {
241                        if !full.contains(f) {
242                            continue;
243                        }
244                    }
245                    out.push(Action::Test {
246                        case: item.clone(),
247                        path: path.clone(),
248                        title,
249                        depth,
250                    });
251                }
252            }
253        }
254    }
255
256    let mut actions: Vec<Action> = Vec::new();
257    let mut status_map: HashMap<String, bool> = HashMap::new();
258    let mut path_stack: Vec<String> = Vec::new();
259    build_actions(tests, test_filter, &mut path_stack, 0, &mut actions);
260
261    let mut failed_details: Vec<(String, SingleTestReport)> = Vec::new();
262
263    for action in actions {
264        match action {
265            Action::Heading { name, depth } => {
266                debug!("Test Group: {} (depth {})", name, depth);
267            }
268            Action::Test {
269                case,
270                path,
271                title,
272                depth,
273            } => {
274                executed += 1;
275                let mut full = path.join(" › ");
276                if !full.is_empty() {
277                    full.push_str(" › ");
278                }
279                full.push_str(&title);
280
281                let rep =
282                    run_single_test(&case, &phlow, tests_global.clone(), engine.clone()).await;
283                if rep.ok {
284                    debug!("Test Passed: {} (depth {})", full, depth);
285                    passed += 1;
286                    status_map.insert(full.clone(), true);
287                    results.push(TestResult {
288                        index: executed,
289                        passed: true,
290                        message: rep.message.clone(),
291                        describe: Some(full.clone()),
292                    });
293                } else {
294                    debug!("Test Failed: {} (depth {})", full, depth);
295                    status_map.insert(full.clone(), false);
296                    results.push(TestResult {
297                        index: executed,
298                        passed: false,
299                        message: rep.message.clone(),
300                        describe: Some(full.clone()),
301                    });
302                    failed_details.push((full.clone(), rep));
303                }
304            }
305        }
306    }
307
308    let failed = executed - passed;
309    println!();
310    println!("📊 Test Results:");
311    println!("   Total: {}", executed);
312    println!("   Passed: {} ✅", passed);
313    println!("   Failed: {} ❌", failed);
314
315    if failed > 0 {
316        println!();
317        println!("❌ Some tests failed!");
318    } else {
319        println!();
320        println!("🎉 All tests passed!");
321    }
322
323    // Print a final tree view of describes and tests with pass/fail
324    {
325        fn is_group(v: &Value) -> bool {
326            v.get("tests").map(|t| t.is_array()).unwrap_or(false)
327        }
328        fn group_name(v: &Value) -> Option<String> {
329            v.get("describe")
330                .or_else(|| v.get("name"))
331                .map(|s| s.as_string())
332        }
333        fn leaf_title(v: &Value) -> Option<String> {
334            v.get("it")
335                .or_else(|| v.get("describe"))
336                .map(|s| s.as_string())
337        }
338
339        fn collect_visible_children<'a>(
340            value: &'a Value,
341            filter: Option<&str>,
342            path: &Vec<String>,
343        ) -> Vec<&'a Value> {
344            let mut out = Vec::new();
345            if let Some(arr) = value.as_array() {
346                for item in &arr.values {
347                    if is_group(item) {
348                        let mut p = path.clone();
349                        if let Some(n) = group_name(item) {
350                            p.push(n);
351                        }
352                        // if group has any visible child, include it
353                        let inner =
354                            collect_visible_children(&item.get("tests").unwrap(), filter, &p);
355                        if !inner.is_empty() {
356                            out.push(item);
357                        }
358                    } else {
359                        let title = leaf_title(item).unwrap_or_else(|| "".to_string());
360                        let mut full = path.join(" › ");
361                        if !full.is_empty() {
362                            full.push_str(" › ");
363                        }
364                        full.push_str(&title);
365                        if let Some(f) = filter {
366                            if !full.contains(f) {
367                                continue;
368                            }
369                        }
370                        out.push(item);
371                    }
372                }
373            }
374            out
375        }
376
377        fn print_tree(
378            nodes: &Value,
379            filter: Option<&str>,
380            path: &mut Vec<String>,
381            prefix: &str,
382            status: &HashMap<String, bool>,
383        ) {
384            let visible = collect_visible_children(nodes, filter, path);
385            let len = visible.len();
386            for (idx, node) in visible.into_iter().enumerate() {
387                let last = idx + 1 == len;
388                let (branch, next_prefix) = if last {
389                    ("└── ", format!("{}    ", prefix))
390                } else {
391                    ("├── ", format!("{}│   ", prefix))
392                };
393                if is_group(node) {
394                    let name = group_name(node).unwrap_or_else(|| "(group)".to_string());
395                    println!("{}{}describe: {}", prefix, branch, name);
396                    path.push(name);
397                    print_tree(
398                        &node.get("tests").unwrap(),
399                        filter,
400                        path,
401                        &next_prefix,
402                        status,
403                    );
404                    path.pop();
405                } else {
406                    let title = leaf_title(node).unwrap_or_else(|| "(test)".to_string());
407                    let mut full = path.join(" › ");
408                    if !full.is_empty() {
409                        full.push_str(" › ");
410                    }
411                    full.push_str(&title);
412                    let icon = match status.get(&full) {
413                        Some(true) => "✅",
414                        Some(false) => "❌",
415                        None => "•",
416                    };
417                    println!("{}{}{} it: {}", prefix, branch, icon, title);
418                }
419            }
420        }
421
422        println!("\n🌲 Test Tree:");
423        let mut p: Vec<String> = Vec::new();
424        print_tree(tests, test_filter, &mut p, "", &status_map);
425    }
426
427    // Print details for failed tests: inputs and outputs, formatted (in red)
428    if failed > 0 {
429        // ANSI Red start
430        println!("\n\x1b[31m🧾 Failed tests details:");
431        for (full_name, rep) in failed_details.iter() {
432            println!("\n{}:", full_name);
433            // Entrada
434            println!("  Entrada:");
435            println!("    main: {}", rep.main);
436            if !rep.initial_payload.is_undefined() {
437                println!("    payload: {}", rep.initial_payload);
438            }
439            // Saída
440            println!("  Saída:");
441            println!("    payload: {}", rep.result);
442        }
443        // ANSI Reset
444        println!("\x1b[0m");
445    }
446
447    Ok(TestSummary {
448        total: executed,
449        passed,
450        failed,
451        results,
452    })
453}
454
455async fn run_single_test(
456    test_case: &Value,
457    phlow: &Phlow,
458    test: Arc<Mutex<Value>>,
459    engine: Arc<phlow_engine::phs::Engine>,
460) -> SingleTestReport {
461    let tests_snapshot = { test.lock().await.clone() };
462    let mut context = Context::from_tests(tests_snapshot.clone());
463
464    // Extract test inputs
465    let main_value = {
466        let data = test_case.get("main").cloned().unwrap_or(Value::Undefined);
467
468        if data.is_undefined() {
469            Value::Undefined
470        } else {
471            match Script::try_build(engine.clone(), &data) {
472                Ok(script) => match script.evaluate(&context) {
473                    Ok(val) => val.to_value(),
474                    Err(e) => {
475                        return SingleTestReport {
476                            ok: false,
477                            message: format!("Failed to evaluate main script: {}", e),
478                            main: Value::Undefined,
479                            initial_payload: Value::Undefined,
480                            result: Value::Undefined,
481                        };
482                    }
483                },
484                Err(e) => {
485                    return SingleTestReport {
486                        ok: false,
487                        message: format!("Failed to build main script: {}", e),
488                        main: Value::Undefined,
489                        initial_payload: Value::Undefined,
490                        result: Value::Undefined,
491                    };
492                }
493            }
494        }
495    };
496    let initial_payload = {
497        let data = test_case
498            .get("payload")
499            .cloned()
500            .unwrap_or(Value::Undefined);
501
502        if data.is_undefined() {
503            Value::Undefined
504        } else {
505            match Script::try_build(engine.clone(), &Value::from(data)) {
506                Ok(script) => match script.evaluate(&context) {
507                    Ok(val) => val.to_value(),
508                    Err(e) => {
509                        return SingleTestReport {
510                            ok: false,
511                            message: format!("Failed to evaluate payload script: {}", e),
512                            main: main_value.clone(),
513                            initial_payload: Value::Undefined,
514                            result: Value::Undefined,
515                        };
516                    }
517                },
518                Err(e) => {
519                    return SingleTestReport {
520                        ok: false,
521                        message: format!("Failed to build payload script: {}", e),
522                        main: main_value.clone(),
523                        initial_payload: Value::Undefined,
524                        result: Value::Undefined,
525                    };
526                }
527            }
528        }
529    };
530
531    debug!(
532        "Running test with main: {:?}, payload: {:?}",
533        main_value, initial_payload
534    );
535
536    if !main_value.is_undefined() {
537        context = context.clone_with_main(main_value.clone());
538    }
539
540    // Set initial payload if provided
541    if !initial_payload.is_undefined() {
542        context = context.clone_with_output(initial_payload.clone());
543    }
544
545    // Execute the workflow
546    let result = {
547        let exec = match phlow.execute(&mut context).await {
548            Ok(v) => v,
549            Err(e) => {
550                return SingleTestReport {
551                    ok: false,
552                    message: format!("Execution failed: {}", e),
553                    main: main_value.clone(),
554                    initial_payload: initial_payload.clone(),
555                    result: Value::Undefined,
556                };
557            }
558        };
559        exec.unwrap_or(Value::Undefined)
560    };
561
562    // Check assertions
563    // Identify this test id (used to store into global tests)
564    let test_id = test_case
565        .get("id")
566        .map(|v| v.as_string())
567        .or_else(|| test_case.get("describe").map(|v| v.as_string()))
568        .or_else(|| test_case.get("it").map(|v| v.as_string()))
569        .unwrap_or_else(|| "current".to_string());
570
571    if let Some(assert_eq_value) = test_case.get("assert_eq") {
572        // ANSI escape code for red: \x1b[31m ... \x1b[0m
573        if deep_equals(&result, assert_eq_value) {
574            // Update global tests map with this test execution
575            {
576                let mut guard = test.lock().await;
577                let mut map: HashMap<String, Value> = HashMap::new();
578                if let Some(obj) = guard.as_object() {
579                    for (k, v) in obj.iter() {
580                        map.insert(k.to_string(), v.clone());
581                    }
582                }
583                map.insert(
584                    test_id.clone(),
585                    json!({
586                        "id": test_id.clone(),
587                        "describe": test_case.get("describe").cloned().unwrap_or(Value::Undefined),
588                        "main": main_value.clone(),
589                        "payload": result.clone(),
590                    }),
591                );
592                *guard = Value::from(map);
593            }
594
595            SingleTestReport {
596                ok: true,
597                message: format!("Expected and got: {}", result),
598                main: main_value.clone(),
599                initial_payload: initial_payload.clone(),
600                result: result.clone(),
601            }
602        } else {
603            let mut msg = String::new();
604            write!(
605                &mut msg,
606                "Expected \x1b[34m{}\x1b[0m, got \x1b[31m{}\x1b[0m",
607                assert_eq_value, result
608            )
609            .unwrap();
610            // Update global tests map with this test execution even on failure
611            {
612                let mut guard = test.lock().await;
613                let mut map: HashMap<String, Value> = HashMap::new();
614                if let Some(obj) = guard.as_object() {
615                    for (k, v) in obj.iter() {
616                        map.insert(k.to_string(), v.clone());
617                    }
618                }
619                map.insert(
620                    test_id.clone(),
621                    json!({
622                        "id": test_id.clone(),
623                        "describe": test_case.get("describe").cloned().unwrap_or(Value::Undefined),
624                        "main": main_value.clone(),
625                        "payload": result.clone(),
626                    }),
627                );
628                *guard = Value::from(map);
629            }
630
631            SingleTestReport {
632                ok: false,
633                message: msg,
634                main: main_value.clone(),
635                initial_payload: initial_payload.clone(),
636                result: result.clone(),
637            }
638        }
639    } else if let Some(assert_expr) = test_case.get("assert") {
640        // For assert expressions, we need to evaluate them
641        let assertion_result = match evaluate_assertion(
642            assert_expr,
643            main_value.clone(),
644            tests_snapshot,
645            result.clone(),
646        ) {
647            Ok(v) => v,
648            Err(e) => {
649                return SingleTestReport {
650                    ok: false,
651                    message: format!("Assertion error: {}. payload: {}", e, result),
652                    main: main_value.clone(),
653                    initial_payload: initial_payload.clone(),
654                    result: result.clone(),
655                };
656            }
657        };
658
659        if assertion_result {
660            // Update global tests map with this test execution
661            {
662                let mut guard = test.lock().await;
663                let mut map: HashMap<String, Value> = HashMap::new();
664                if let Some(obj) = guard.as_object() {
665                    for (k, v) in obj.iter() {
666                        map.insert(k.to_string(), v.clone());
667                    }
668                }
669                map.insert(
670                    test_id.clone(),
671                    json!({
672                        "id": test_id.clone(),
673                        "describe": test_case.get("describe").cloned().unwrap_or(Value::Undefined),
674                        "main": main_value.clone(),
675                        "payload": result.clone(),
676                    }),
677                );
678                *guard = Value::from(map);
679            }
680
681            SingleTestReport {
682                ok: true,
683                message: format!("Assertion passed: {}", assert_expr),
684                main: main_value.clone(),
685                initial_payload: initial_payload.clone(),
686                result: result.clone(),
687            }
688        } else {
689            // Print the full payload when an assert fails
690            // Update global tests map with this test execution even on failure
691            {
692                let mut guard = test.lock().await;
693                let mut map: HashMap<String, Value> = HashMap::new();
694                if let Some(obj) = guard.as_object() {
695                    for (k, v) in obj.iter() {
696                        map.insert(k.to_string(), v.clone());
697                    }
698                }
699                map.insert(
700                    test_id.clone(),
701                    json!({
702                        "id": test_id.clone(),
703                        "describe": test_case.get("describe").cloned().unwrap_or(Value::Undefined),
704                        "main": main_value.clone(),
705                        "payload": result.clone(),
706                    }),
707                );
708                *guard = Value::from(map);
709            }
710
711            SingleTestReport {
712                ok: false,
713                message: format!(
714                    "Assertion failed: {}. payload: \x1b[31m{}\x1b[0m",
715                    assert_expr, result
716                ),
717                main: main_value.clone(),
718                initial_payload: initial_payload.clone(),
719                result: result.clone(),
720            }
721        }
722    } else {
723        // Update global tests map with this test execution even when no assertion is defined
724        {
725            let mut guard = test.lock().await;
726            let mut map: HashMap<String, Value> = HashMap::new();
727            if let Some(obj) = guard.as_object() {
728                for (k, v) in obj.iter() {
729                    map.insert(k.to_string(), v.clone());
730                }
731            }
732            map.insert(
733                test_id.clone(),
734                json!({
735                    "id": test_id.clone(),
736                    "describe": test_case.get("describe").cloned().unwrap_or(Value::Undefined),
737                    "main": main_value.clone(),
738                    "payload": result.clone(),
739                }),
740            );
741            *guard = Value::from(map);
742        }
743
744        SingleTestReport {
745            ok: false,
746            message: "No assertion found (assert or assert_eq required)".to_string(),
747            main: main_value.clone(),
748            initial_payload: initial_payload.clone(),
749            result: result.clone(),
750        }
751    }
752}
753
754// Load modules following the exact same pattern as Runtime::run
755// but without creating main_sender channels since we don't need them for tests
756async fn load_modules_like_runtime(
757    loader: &Loader,
758    settings: Settings,
759) -> Result<Arc<Modules>, String> {
760    let mut modules = Modules::default();
761
762    // Initialize tracing subscriber
763    let guard = init_tracing_subscriber(loader.app_data.clone());
764    let dispatch = guard.dispatch.clone();
765
766    let engine = build_engine(None);
767
768    // Load modules exactly like Runtime::run does
769    for (id, module) in loader.modules.iter().enumerate() {
770        let (setup_sender, setup_receive) =
771            oneshot::channel::<Option<channel::Sender<ModulePackage>>>();
772
773        // For tests, we never pass main_sender to prevent modules from starting servers/loops
774        let main_sender = None;
775
776        let with = {
777            let script = phs::Script::try_build(engine.clone(), &module.with)
778                .map_err(|e| format!("Failed to build script for module {}: {}", module.name, e))?;
779
780            script.evaluate_without_context().map_err(|e| {
781                format!(
782                    "Failed to evaluate script for module {}: {}",
783                    module.name, e
784                )
785            })?
786        };
787
788        let setup = ModuleSetup {
789            id,
790            setup_sender,
791            main_sender,
792            with,
793            dispatch: dispatch.clone(),
794            app_data: loader.app_data.clone(),
795            is_test_mode: true,
796        };
797
798        let module_target = module.module.clone();
799        let module_version = module.version.clone();
800        let is_local_path = module.local_path.is_some();
801        let local_path = module.local_path.clone();
802        let module_name = module.name.clone();
803        let settings = settings.clone();
804
805        debug!(
806            "Module debug: name={}, is_local_path={}, local_path={:?}",
807            module_name, is_local_path, local_path
808        );
809
810        // Load module in separate thread - same as Runtime::run
811        std::thread::spawn(move || {
812            let result = load_module(setup, &module_target, &module_version, local_path, settings);
813
814            if let Err(err) = result {
815                error!("Test runtime Error Load Module: {:?}", err)
816            }
817        });
818
819        debug!(
820            "Module {} loaded with name \"{}\" and version \"{}\"",
821            module.module, module.name, module.version
822        );
823
824        // Wait for module registration - same as Runtime::run
825        match setup_receive.await {
826            Ok(Some(sender)) => {
827                debug!("Module \"{}\" registered", module.name);
828                modules.register(module.clone(), sender);
829            }
830            Ok(None) => {
831                debug!("Module \"{}\" did not register", module.name);
832            }
833            Err(err) => {
834                return Err(format!(
835                    "Module \"{}\" registration failed: {}",
836                    module.name, err
837                ));
838            }
839        }
840    }
841
842    Ok(Arc::new(modules))
843}
844
845/// Deep equality comparison for JSON values that ignores object property order
846/// and compares structure recursively
847fn deep_equals(a: &Value, b: &Value) -> bool {
848    match (a, b) {
849        // Same type comparisons
850        (Value::Null, Value::Null) => true,
851        (Value::Boolean(a), Value::Boolean(b)) => a == b,
852        (Value::Number(a), Value::Number(b)) => {
853            // Compare numeric values regardless of internal type representation
854            let a_val = a.to_f64().unwrap_or(0.0);
855            let b_val = b.to_f64().unwrap_or(0.0);
856            (a_val - b_val).abs() < f64::EPSILON
857        }
858        (Value::String(a), Value::String(b)) => a == b,
859
860        // Array comparison - order matters for arrays
861        (Value::Array(a), Value::Array(b)) => {
862            if a.len() != b.len() {
863                return false;
864            }
865            a.values
866                .iter()
867                .zip(b.values.iter())
868                .all(|(a_val, b_val)| deep_equals(a_val, b_val))
869        }
870
871        // Object comparison - order doesn't matter for objects
872        (Value::Object(a), Value::Object(b)) => {
873            if a.len() != b.len() {
874                return false;
875            }
876
877            // Check if all keys from a exist in b with equal values
878            for (key, a_val) in a.iter() {
879                let key_str = key.to_string();
880                match b.get(key_str.as_str()) {
881                    Some(b_val) => {
882                        if !deep_equals(a_val, b_val) {
883                            return false;
884                        }
885                    }
886                    None => return false,
887                }
888            }
889
890            true
891        }
892
893        // Different types are not equal
894        _ => false,
895    }
896}
897
898fn evaluate_assertion(
899    assert_expr: &Value,
900    main: Value,
901    tests: Value,
902    result: Value,
903) -> Result<bool, String> {
904    // Create a simple evaluation context
905    let engine = build_engine(None);
906
907    // Convert the assertion expression to a script
908    let script = Script::try_build(engine, assert_expr)
909        .map_err(|e| format!("Failed to build assertion script: {}", e))?;
910
911    // Create a context where 'payload' refers to the result and 'test'/'steps' point to global tests map
912    let mut context = Context::from_main_tests(main, tests);
913
914    context.add_step_payload(Some(result));
915
916    match script.evaluate(&context) {
917        Ok(Value::Boolean(b)) => Ok(b),
918        Ok(Value::String(s)) if s == "true".into() => Ok(true),
919        Ok(Value::String(s)) if s == "false".into() => Ok(false),
920        Ok(other) => Err(format!("Assertion must return boolean, got: {}", other)),
921        Err(e) => Err(format!("Failed to evaluate assertion script: {}", e)),
922    }
923}