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 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 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 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 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 let modules = load_modules_like_runtime(&loader, settings)
148 .await
149 .map_err(|e| format!("Failed to load modules for tests: {}", e))?;
150
151 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 let mut results = Vec::new();
161 let mut passed = 0;
162 let mut executed = 0usize;
163 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 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 {
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 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 if failed > 0 {
429 println!("\n\x1b[31m🧾 Failed tests details:");
431 for (full_name, rep) in failed_details.iter() {
432 println!("\n{}:", full_name);
433 println!(" Entrada:");
435 println!(" main: {}", rep.main);
436 if !rep.initial_payload.is_undefined() {
437 println!(" payload: {}", rep.initial_payload);
438 }
439 println!(" Saída:");
441 println!(" payload: {}", rep.result);
442 }
443 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 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 if !initial_payload.is_undefined() {
542 context = context.clone_with_output(initial_payload.clone());
543 }
544
545 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 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 if deep_equals(&result, assert_eq_value) {
574 {
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 {
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 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 {
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 {
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 {
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
754async fn load_modules_like_runtime(
757 loader: &Loader,
758 settings: Settings,
759) -> Result<Arc<Modules>, String> {
760 let mut modules = Modules::default();
761
762 let guard = init_tracing_subscriber(loader.app_data.clone());
764 let dispatch = guard.dispatch.clone();
765
766 let engine = build_engine(None);
767
768 for (id, module) in loader.modules.iter().enumerate() {
770 let (setup_sender, setup_receive) =
771 oneshot::channel::<Option<channel::Sender<ModulePackage>>>();
772
773 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 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 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
845fn deep_equals(a: &Value, b: &Value) -> bool {
848 match (a, b) {
849 (Value::Null, Value::Null) => true,
851 (Value::Boolean(a), Value::Boolean(b)) => a == b,
852 (Value::Number(a), Value::Number(b)) => {
853 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 (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 (Value::Object(a), Value::Object(b)) => {
873 if a.len() != b.len() {
874 return false;
875 }
876
877 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 _ => 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 let engine = build_engine(None);
906
907 let script = Script::try_build(engine, assert_expr)
909 .map_err(|e| format!("Failed to build assertion script: {}", e))?;
910
911 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}