1use runtara_dsl::{CompositeInner, ExecutionGraph, InputMapping, MappingValue, Step};
13use std::collections::{HashMap, HashSet};
14
15#[derive(Debug, Clone, Default)]
21pub struct ValidationResult {
22 pub errors: Vec<ValidationError>,
24 pub warnings: Vec<ValidationWarning>,
26}
27
28impl ValidationResult {
29 pub fn is_ok(&self) -> bool {
31 self.errors.is_empty()
32 }
33
34 pub fn has_errors(&self) -> bool {
36 !self.errors.is_empty()
37 }
38
39 pub fn has_warnings(&self) -> bool {
41 !self.warnings.is_empty()
42 }
43
44 pub fn merge(&mut self, other: ValidationResult) {
46 self.errors.extend(other.errors);
47 self.warnings.extend(other.warnings);
48 }
49}
50
51#[derive(Debug, Clone)]
57#[allow(missing_docs)] pub enum ValidationError {
59 EntryPointNotFound {
62 entry_point: String,
63 available_steps: Vec<String>,
64 },
65 UnreachableStep { step_id: String },
67 EmptyWorkflow,
69
70 InvalidStepReference {
73 step_id: String,
74 reference_path: String,
75 referenced_step_id: String,
76 available_steps: Vec<String>,
77 },
78 InvalidReferencePath {
80 step_id: String,
81 reference_path: String,
82 reason: String,
83 },
84
85 UnknownAgent {
88 step_id: String,
89 agent_id: String,
90 available_agents: Vec<String>,
91 },
92 UnknownCapability {
94 step_id: String,
95 agent_id: String,
96 capability_id: String,
97 available_capabilities: Vec<String>,
98 },
99 MissingRequiredInput {
101 step_id: String,
102 agent_id: String,
103 capability_id: String,
104 input_name: String,
105 },
106
107 UnknownIntegration {
110 step_id: String,
111 integration_id: String,
112 available_integrations: Vec<String>,
113 },
114
115 ConnectionLeakToNonSecureAgent {
118 connection_step_id: String,
119 agent_step_id: String,
120 agent_id: String,
121 },
122 ConnectionLeakToFinish {
124 connection_step_id: String,
125 finish_step_id: String,
126 },
127 ConnectionLeakToLog {
129 connection_step_id: String,
130 log_step_id: String,
131 },
132
133 InvalidChildVersion {
136 step_id: String,
137 child_scenario_id: String,
138 version: String,
139 reason: String,
140 },
141
142 StepNotYetExecuted {
145 step_id: String,
146 referenced_step_id: String,
147 },
148
149 UnknownVariable {
152 step_id: String,
153 variable_name: String,
154 available_variables: Vec<String>,
155 },
156
157 TypeMismatch {
160 step_id: String,
161 field_name: String,
162 expected_type: String,
163 actual_type: String,
164 },
165 InvalidEnumValue {
167 step_id: String,
168 field_name: String,
169 value: String,
170 allowed_values: Vec<String>,
171 },
172
173 DuplicateStepName { name: String, step_ids: Vec<String> },
176}
177
178impl std::fmt::Display for ValidationError {
179 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
180 match self {
181 ValidationError::EntryPointNotFound {
183 entry_point,
184 available_steps,
185 } => {
186 write!(
187 f,
188 "[E001] Entry point '{}' not found in steps. Available steps: {}",
189 entry_point,
190 if available_steps.is_empty() {
191 "(none)".to_string()
192 } else {
193 available_steps.join(", ")
194 }
195 )
196 }
197 ValidationError::UnreachableStep { step_id } => {
198 write!(
199 f,
200 "[E002] Step '{}' is unreachable from the entry point",
201 step_id
202 )
203 }
204 ValidationError::EmptyWorkflow => {
205 write!(f, "[E004] Workflow has no steps defined")
206 }
207
208 ValidationError::InvalidStepReference {
210 step_id,
211 reference_path,
212 referenced_step_id,
213 available_steps,
214 } => {
215 let suggestion = find_similar_name(referenced_step_id, available_steps);
216 let suggestion_text = suggestion
217 .map(|s| format!(". Did you mean '{}'?", s))
218 .unwrap_or_default();
219 write!(
220 f,
221 "[E010] Step '{}' references '{}' but step '{}' does not exist{}",
222 step_id, reference_path, referenced_step_id, suggestion_text
223 )
224 }
225 ValidationError::InvalidReferencePath {
226 step_id,
227 reference_path,
228 reason,
229 } => {
230 write!(
231 f,
232 "[E011] Step '{}' has invalid reference path '{}': {}",
233 step_id, reference_path, reason
234 )
235 }
236
237 ValidationError::UnknownAgent {
239 step_id,
240 agent_id,
241 available_agents,
242 } => {
243 let suggestion = find_similar_name(agent_id, available_agents);
244 let suggestion_text = suggestion
245 .map(|s| format!(". Did you mean '{}'?", s))
246 .unwrap_or_default();
247 write!(
248 f,
249 "[E020] Step '{}' uses unknown agent '{}'{}\n Available agents: {}",
250 step_id,
251 agent_id,
252 suggestion_text,
253 available_agents.join(", ")
254 )
255 }
256 ValidationError::UnknownCapability {
257 step_id,
258 agent_id,
259 capability_id,
260 available_capabilities,
261 } => {
262 let suggestion = find_similar_name(capability_id, available_capabilities);
263 let suggestion_text = suggestion
264 .map(|s| format!(". Did you mean '{}'?", s))
265 .unwrap_or_default();
266 write!(
267 f,
268 "[E021] Step '{}': agent '{}' has no capability '{}'{}\n Available capabilities: {}",
269 step_id,
270 agent_id,
271 capability_id,
272 suggestion_text,
273 if available_capabilities.is_empty() {
274 "(none)".to_string()
275 } else {
276 available_capabilities.join(", ")
277 }
278 )
279 }
280 ValidationError::MissingRequiredInput {
281 step_id,
282 agent_id,
283 capability_id,
284 input_name,
285 } => {
286 write!(
287 f,
288 "[E022] Step '{}': capability '{}:{}' requires input '{}' but it is not provided",
289 step_id, agent_id, capability_id, input_name
290 )
291 }
292
293 ValidationError::UnknownIntegration {
295 step_id,
296 integration_id,
297 available_integrations,
298 } => {
299 let suggestion = find_similar_name(integration_id, available_integrations);
300 let suggestion_text = suggestion
301 .map(|s| format!(". Did you mean '{}'?", s))
302 .unwrap_or_default();
303 write!(
304 f,
305 "[E030] Connection step '{}' uses unknown integration '{}'{}\n Available integrations: {}",
306 step_id,
307 integration_id,
308 suggestion_text,
309 available_integrations.join(", ")
310 )
311 }
312
313 ValidationError::ConnectionLeakToNonSecureAgent {
315 connection_step_id,
316 agent_step_id,
317 agent_id,
318 } => {
319 write!(
320 f,
321 "[E040] Security violation: Connection step '{}' outputs are referenced by non-secure agent '{}' (step '{}'). \
322 Connection data can only be passed to secure agents (http, sftp).",
323 connection_step_id, agent_id, agent_step_id
324 )
325 }
326 ValidationError::ConnectionLeakToFinish {
327 connection_step_id,
328 finish_step_id,
329 } => {
330 write!(
331 f,
332 "[E041] Security violation: Connection step '{}' outputs are referenced by Finish step '{}'. \
333 Connection data cannot be included in workflow outputs.",
334 connection_step_id, finish_step_id
335 )
336 }
337 ValidationError::ConnectionLeakToLog {
338 connection_step_id,
339 log_step_id,
340 } => {
341 write!(
342 f,
343 "[E042] Security violation: Connection step '{}' outputs are referenced by Log step '{}'. \
344 Connection data cannot be logged.",
345 connection_step_id, log_step_id
346 )
347 }
348
349 ValidationError::InvalidChildVersion {
351 step_id,
352 child_scenario_id,
353 version,
354 reason,
355 } => {
356 write!(
357 f,
358 "[E050] Step '{}': child scenario '{}' has invalid version '{}': {}",
359 step_id, child_scenario_id, version, reason
360 )
361 }
362
363 ValidationError::StepNotYetExecuted {
365 step_id,
366 referenced_step_id,
367 } => {
368 write!(
369 f,
370 "[E012] Step '{}' references step '{}' which has not executed yet. \
371 Steps can only reference outputs from steps that execute before them.",
372 step_id, referenced_step_id
373 )
374 }
375
376 ValidationError::UnknownVariable {
378 step_id,
379 variable_name,
380 available_variables,
381 } => {
382 let suggestion = find_similar_name(variable_name, available_variables);
383 let suggestion_text = suggestion
384 .map(|s| format!(". Did you mean '{}'?", s))
385 .unwrap_or_default();
386 write!(
387 f,
388 "[E013] Step '{}' references unknown variable '{}'{}\n Available variables: {}",
389 step_id,
390 variable_name,
391 suggestion_text,
392 if available_variables.is_empty() {
393 "(none)".to_string()
394 } else {
395 available_variables.join(", ")
396 }
397 )
398 }
399
400 ValidationError::TypeMismatch {
402 step_id,
403 field_name,
404 expected_type,
405 actual_type,
406 } => {
407 write!(
408 f,
409 "[E023] Step '{}': field '{}' expects type '{}' but got '{}'",
410 step_id, field_name, expected_type, actual_type
411 )
412 }
413 ValidationError::InvalidEnumValue {
414 step_id,
415 field_name,
416 value,
417 allowed_values,
418 } => {
419 write!(
420 f,
421 "[E024] Step '{}': field '{}' has invalid value '{}'. Allowed values: {}",
422 step_id,
423 field_name,
424 value,
425 allowed_values.join(", ")
426 )
427 }
428
429 ValidationError::DuplicateStepName { name, step_ids } => {
431 write!(
432 f,
433 "[E060] Multiple steps have the same name '{}': {}",
434 name,
435 step_ids.join(", ")
436 )
437 }
438 }
439 }
440}
441
442impl std::error::Error for ValidationError {}
443
444#[derive(Debug, Clone)]
450#[allow(missing_docs)] pub enum ValidationWarning {
452 UnknownInputField {
454 step_id: String,
455 agent_id: String,
456 capability_id: String,
457 field_name: String,
458 available_fields: Vec<String>,
459 },
460 HighRetryCount {
462 step_id: String,
463 max_retries: u32,
464 recommended_max: u32,
465 },
466 LongRetryDelay {
468 step_id: String,
469 retry_delay_ms: u64,
470 recommended_max_ms: u64,
471 },
472 HighParallelism {
474 step_id: String,
475 parallelism: u32,
476 recommended_max: u32,
477 },
478 HighMaxIterations {
480 step_id: String,
481 max_iterations: u32,
482 recommended_max: u32,
483 },
484 LongTimeout {
486 step_id: String,
487 timeout_ms: u64,
488 recommended_max_ms: u64,
489 },
490 UnusedConnection { step_id: String },
492 SelfReference {
494 step_id: String,
495 reference_path: String,
496 },
497 DanglingStep { step_id: String, step_type: String },
499}
500
501impl std::fmt::Display for ValidationWarning {
502 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
503 match self {
504 ValidationWarning::UnknownInputField {
505 step_id,
506 agent_id,
507 capability_id,
508 field_name,
509 available_fields,
510 } => {
511 let suggestion = find_similar_name(field_name, available_fields);
512 let suggestion_text = suggestion
513 .map(|s| format!(". Did you mean '{}'?", s))
514 .unwrap_or_default();
515 write!(
516 f,
517 "[W020] Step '{}': input '{}' is not a known field for '{}:{}'{}\n Available fields: {}",
518 step_id,
519 field_name,
520 agent_id,
521 capability_id,
522 suggestion_text,
523 if available_fields.is_empty() {
524 "(none)".to_string()
525 } else {
526 available_fields.join(", ")
527 }
528 )
529 }
530 ValidationWarning::HighRetryCount {
531 step_id,
532 max_retries,
533 recommended_max,
534 } => {
535 write!(
536 f,
537 "[W030] Step '{}' has max_retries={}. Consider reducing to {} or less to avoid long execution times.",
538 step_id, max_retries, recommended_max
539 )
540 }
541 ValidationWarning::LongRetryDelay {
542 step_id,
543 retry_delay_ms,
544 recommended_max_ms,
545 } => {
546 write!(
547 f,
548 "[W031] Step '{}' has retry_delay={}ms ({}). Consider reducing to {}ms or less.",
549 step_id,
550 retry_delay_ms,
551 format_duration(*retry_delay_ms),
552 recommended_max_ms
553 )
554 }
555 ValidationWarning::HighParallelism {
556 step_id,
557 parallelism,
558 recommended_max,
559 } => {
560 write!(
561 f,
562 "[W032] Split step '{}' has parallelism={}. Consider reducing to {} or less for resource efficiency.",
563 step_id, parallelism, recommended_max
564 )
565 }
566 ValidationWarning::HighMaxIterations {
567 step_id,
568 max_iterations,
569 recommended_max,
570 } => {
571 write!(
572 f,
573 "[W033] While step '{}' has max_iterations={}. This may indicate an infinite loop risk. Consider {} or less.",
574 step_id, max_iterations, recommended_max
575 )
576 }
577 ValidationWarning::LongTimeout {
578 step_id,
579 timeout_ms,
580 recommended_max_ms,
581 } => {
582 write!(
583 f,
584 "[W034] Step '{}' has timeout={}ms ({}). Consider {} or less, or breaking into smaller steps.",
585 step_id,
586 timeout_ms,
587 format_duration(*timeout_ms),
588 format_duration(*recommended_max_ms)
589 )
590 }
591 ValidationWarning::UnusedConnection { step_id } => {
592 write!(
593 f,
594 "[W040] Connection step '{}' is defined but never referenced by any agent",
595 step_id
596 )
597 }
598 ValidationWarning::SelfReference {
599 step_id,
600 reference_path,
601 } => {
602 write!(
603 f,
604 "[W050] Step '{}' references its own outputs via '{}'. This may cause issues unless in a loop.",
605 step_id, reference_path
606 )
607 }
608 ValidationWarning::DanglingStep { step_id, step_type } => {
609 write!(
610 f,
611 "[W003] Step '{}' ({}) has no outgoing edges but is not a Finish step. The workflow will terminate here without explicit output.",
612 step_id, step_type
613 )
614 }
615 }
616 }
617}
618
619pub fn validate_workflow(graph: &ExecutionGraph) -> ValidationResult {
628 let mut result = ValidationResult::default();
629
630 validate_graph_structure(graph, &mut result);
632
633 validate_references(graph, &mut result);
635
636 validate_execution_order(graph, &mut result);
638
639 validate_agents(graph, &mut result);
641
642 validate_configuration(graph, &mut result);
644
645 validate_connections(graph, &mut result);
647
648 validate_security(graph, &mut result);
650
651 validate_child_scenarios(graph, &mut result);
653
654 validate_step_names(graph, &mut result);
656
657 result
658}
659
660pub fn validate_workflow_errors(graph: &ExecutionGraph) -> Vec<ValidationError> {
663 validate_workflow(graph).errors
664}
665
666fn validate_graph_structure(graph: &ExecutionGraph, result: &mut ValidationResult) {
671 if graph.steps.is_empty() {
673 result.errors.push(ValidationError::EmptyWorkflow);
674 return;
675 }
676
677 if !graph.steps.contains_key(&graph.entry_point) {
679 let available_steps: Vec<String> = graph.steps.keys().cloned().collect();
680 result.errors.push(ValidationError::EntryPointNotFound {
681 entry_point: graph.entry_point.clone(),
682 available_steps,
683 });
684 return;
685 }
686
687 let reachable = compute_reachable_steps(graph);
689
690 for step_id in graph.steps.keys() {
692 if !reachable.contains(step_id) {
693 result.errors.push(ValidationError::UnreachableStep {
694 step_id: step_id.clone(),
695 });
696 }
697 }
698
699 let steps_with_outgoing: HashSet<String> = graph
701 .execution_plan
702 .iter()
703 .map(|e| e.from_step.clone())
704 .collect();
705
706 for (step_id, step) in &graph.steps {
707 if matches!(step, Step::Finish(_)) {
709 continue;
710 }
711
712 if !steps_with_outgoing.contains(step_id) {
714 result.warnings.push(ValidationWarning::DanglingStep {
715 step_id: step_id.clone(),
716 step_type: get_step_type_name(step).to_string(),
717 });
718 }
719 }
720
721 for step in graph.steps.values() {
726 match step {
727 Step::Split(split_step) => {
728 validate_graph_structure(&split_step.subgraph, result);
729 }
730 Step::While(while_step) => {
731 validate_graph_structure(&while_step.subgraph, result);
732 }
733 _ => {}
734 }
735 }
736}
737
738fn compute_reachable_steps(graph: &ExecutionGraph) -> HashSet<String> {
740 let mut reachable = HashSet::new();
741 let mut queue = vec![graph.entry_point.clone()];
742
743 let mut adjacency: HashMap<String, Vec<String>> = HashMap::new();
745 for edge in &graph.execution_plan {
746 adjacency
747 .entry(edge.from_step.clone())
748 .or_default()
749 .push(edge.to_step.clone());
750 }
751
752 while let Some(step_id) = queue.pop() {
753 if reachable.contains(&step_id) {
754 continue;
755 }
756 reachable.insert(step_id.clone());
757
758 if let Some(neighbors) = adjacency.get(&step_id) {
759 for neighbor in neighbors {
760 if !reachable.contains(neighbor) {
761 queue.push(neighbor.clone());
762 }
763 }
764 }
765 }
766
767 reachable
768}
769
770fn validate_references(graph: &ExecutionGraph, result: &mut ValidationResult) {
775 validate_references_with_inherited(graph, &HashSet::new(), result);
776}
777
778fn validate_references_with_inherited(
784 graph: &ExecutionGraph,
785 inherited_variables: &HashSet<String>,
786 result: &mut ValidationResult,
787) {
788 let step_ids: HashSet<String> = graph.steps.keys().cloned().collect();
789
790 let mut variable_names: HashSet<String> = graph.variables.keys().cloned().collect();
792 variable_names.extend(inherited_variables.iter().cloned());
793
794 for (step_id, step) in &graph.steps {
795 let mappings = collect_step_mappings(step);
796
797 for mapping in mappings {
798 for (_, value) in mapping {
799 validate_mapping_value_references(
800 step_id,
801 value,
802 &step_ids,
803 &variable_names,
804 result,
805 );
806 }
807 }
808 }
809
810 for step in graph.steps.values() {
812 match step {
813 Step::Split(split_step) => {
814 let injected_vars: HashSet<String> = split_step
816 .config
817 .as_ref()
818 .and_then(|c| c.variables.as_ref())
819 .map(|v| v.keys().cloned().collect())
820 .unwrap_or_default();
821 validate_references_with_inherited(&split_step.subgraph, &injected_vars, result);
822 }
823 Step::While(while_step) => {
824 validate_references_with_inherited(&while_step.subgraph, &HashSet::new(), result);
825 }
826 _ => {}
827 }
828 }
829}
830
831fn validate_mapping_value_references(
833 step_id: &str,
834 value: &MappingValue,
835 valid_step_ids: &HashSet<String>,
836 valid_variable_names: &HashSet<String>,
837 result: &mut ValidationResult,
838) {
839 match value {
840 MappingValue::Reference(ref_value) => {
841 validate_reference(
842 step_id,
843 &ref_value.value,
844 valid_step_ids,
845 valid_variable_names,
846 result,
847 );
848 }
849 MappingValue::Immediate(_) => {
850 }
852 MappingValue::Composite(comp_value) => {
853 match &comp_value.value {
855 CompositeInner::Object(map) => {
856 for nested_value in map.values() {
857 validate_mapping_value_references(
858 step_id,
859 nested_value,
860 valid_step_ids,
861 valid_variable_names,
862 result,
863 );
864 }
865 }
866 CompositeInner::Array(arr) => {
867 for nested_value in arr {
868 validate_mapping_value_references(
869 step_id,
870 nested_value,
871 valid_step_ids,
872 valid_variable_names,
873 result,
874 );
875 }
876 }
877 }
878 }
879 }
880}
881
882fn validate_reference(
883 step_id: &str,
884 ref_path: &str,
885 valid_step_ids: &HashSet<String>,
886 valid_variable_names: &HashSet<String>,
887 result: &mut ValidationResult,
888) {
889 if ref_path.contains("..") {
891 result.errors.push(ValidationError::InvalidReferencePath {
892 step_id: step_id.to_string(),
893 reference_path: ref_path.to_string(),
894 reason: "empty path segment (consecutive dots)".to_string(),
895 });
896 return;
897 }
898
899 if let Some(referenced_step_id) = extract_step_id_from_reference(ref_path) {
901 if referenced_step_id == step_id {
903 result.warnings.push(ValidationWarning::SelfReference {
904 step_id: step_id.to_string(),
905 reference_path: ref_path.to_string(),
906 });
907 }
908
909 if !valid_step_ids.contains(&referenced_step_id) {
911 result.errors.push(ValidationError::InvalidStepReference {
912 step_id: step_id.to_string(),
913 reference_path: ref_path.to_string(),
914 referenced_step_id: referenced_step_id.clone(),
915 available_steps: valid_step_ids.iter().cloned().collect(),
916 });
917 }
918 }
919
920 if let Some(variable_name) = extract_variable_name_from_reference(ref_path) {
922 if !valid_variable_names.contains(&variable_name) {
923 result.errors.push(ValidationError::UnknownVariable {
924 step_id: step_id.to_string(),
925 variable_name: variable_name.clone(),
926 available_variables: valid_variable_names.iter().cloned().collect(),
927 });
928 }
929 }
930}
931
932fn collect_step_mappings(step: &Step) -> Vec<&InputMapping> {
934 let mut mappings = Vec::new();
935
936 match step {
937 Step::Agent(agent_step) => {
938 if let Some(m) = &agent_step.input_mapping {
939 mappings.push(m);
940 }
941 }
942 Step::Finish(finish_step) => {
943 if let Some(m) = &finish_step.input_mapping {
944 mappings.push(m);
945 }
946 }
947 Step::StartScenario(start_step) => {
948 if let Some(m) = &start_step.input_mapping {
949 mappings.push(m);
950 }
951 }
952 Step::Log(log_step) => {
953 if let Some(m) = &log_step.context {
954 mappings.push(m);
955 }
956 }
957 Step::Split(split_step) => {
958 if let Some(config) = &split_step.config {
959 if let Some(m) = &config.variables {
960 mappings.push(m);
961 }
962 }
963 }
964 Step::Conditional(_) | Step::Switch(_) | Step::While(_) | Step::Connection(_) => {}
965 }
966
967 mappings
968}
969
970fn validate_execution_order(graph: &ExecutionGraph, result: &mut ValidationResult) {
976 let order = compute_execution_order(graph);
978
979 if order.is_empty() {
981 return;
982 }
983
984 let position_map: HashMap<String, usize> = order
986 .iter()
987 .enumerate()
988 .map(|(i, s)| (s.clone(), i))
989 .collect();
990
991 for (step_id, step) in &graph.steps {
993 let current_position = match position_map.get(step_id) {
994 Some(pos) => *pos,
995 None => continue, };
997
998 let mappings = collect_step_mappings(step);
999
1000 for mapping in mappings {
1001 for (_, value) in mapping {
1002 let referenced_step_ids = extract_step_ids_from_mapping_value(value);
1004 for referenced_step_id in referenced_step_ids {
1005 if referenced_step_id == *step_id {
1007 continue;
1008 }
1009
1010 if let Some(ref_position) = position_map.get(&referenced_step_id) {
1011 if *ref_position >= current_position {
1012 result.errors.push(ValidationError::StepNotYetExecuted {
1013 step_id: step_id.clone(),
1014 referenced_step_id: referenced_step_id.clone(),
1015 });
1016 }
1017 }
1018 }
1021 }
1022 }
1023 }
1024
1025 for step in graph.steps.values() {
1027 match step {
1028 Step::Split(split_step) => {
1029 validate_execution_order(&split_step.subgraph, result);
1030 }
1031 Step::While(while_step) => {
1032 validate_execution_order(&while_step.subgraph, result);
1033 }
1034 _ => {}
1035 }
1036 }
1037}
1038
1039fn compute_execution_order(graph: &ExecutionGraph) -> Vec<String> {
1042 let mut order = Vec::new();
1043 let mut visited = HashSet::new();
1044 let mut queue = std::collections::VecDeque::new();
1045
1046 let mut adjacency: HashMap<String, Vec<String>> = HashMap::new();
1048 for edge in &graph.execution_plan {
1049 adjacency
1050 .entry(edge.from_step.clone())
1051 .or_default()
1052 .push(edge.to_step.clone());
1053 }
1054
1055 queue.push_back(graph.entry_point.clone());
1057
1058 while let Some(step_id) = queue.pop_front() {
1059 if visited.contains(&step_id) {
1060 continue;
1061 }
1062 visited.insert(step_id.clone());
1063 order.push(step_id.clone());
1064
1065 if let Some(neighbors) = adjacency.get(&step_id) {
1066 for neighbor in neighbors {
1067 if !visited.contains(neighbor) {
1068 queue.push_back(neighbor.clone());
1069 }
1070 }
1071 }
1072 }
1073
1074 order
1075}
1076
1077fn validate_agents(graph: &ExecutionGraph, result: &mut ValidationResult) {
1082 let available_agents: Vec<String> = runtara_dsl::agent_meta::get_all_agent_modules()
1084 .into_iter()
1085 .map(|m| m.id.to_string())
1086 .collect();
1087
1088 for (step_id, step) in &graph.steps {
1089 if let Step::Agent(agent_step) = step {
1090 let agent_module = runtara_dsl::agent_meta::find_agent_module(&agent_step.agent_id);
1092
1093 if agent_module.is_none() {
1094 result.errors.push(ValidationError::UnknownAgent {
1095 step_id: step_id.clone(),
1096 agent_id: agent_step.agent_id.clone(),
1097 available_agents: available_agents.clone(),
1098 });
1099 continue;
1100 }
1101
1102 let capability_inputs = runtara_dsl::agent_meta::get_capability_inputs(
1104 &agent_step.agent_id,
1105 &agent_step.capability_id,
1106 );
1107
1108 if capability_inputs.is_none() {
1109 let available_capabilities: Vec<String> =
1111 runtara_dsl::agent_meta::get_all_capabilities()
1112 .filter(|c| {
1113 c.module
1114 .map(|m| m.eq_ignore_ascii_case(&agent_step.agent_id))
1115 .unwrap_or(false)
1116 })
1117 .map(|c| c.capability_id.to_string())
1118 .collect();
1119
1120 result.errors.push(ValidationError::UnknownCapability {
1121 step_id: step_id.clone(),
1122 agent_id: agent_step.agent_id.clone(),
1123 capability_id: agent_step.capability_id.clone(),
1124 available_capabilities,
1125 });
1126 continue;
1127 }
1128
1129 if let Some(inputs) = capability_inputs {
1131 let provided_keys: HashSet<String> = agent_step
1135 .input_mapping
1136 .as_ref()
1137 .map(|m| {
1138 m.keys()
1139 .map(|k| {
1140 k.split('.').next().unwrap_or(k).to_string()
1142 })
1143 .collect()
1144 })
1145 .unwrap_or_default();
1146
1147 let available_fields: Vec<String> = inputs.iter().map(|f| f.name.clone()).collect();
1148
1149 for input in &inputs {
1151 if input.required && !provided_keys.contains(&input.name) {
1152 if input.name == "_connection" {
1154 continue;
1155 }
1156 result.errors.push(ValidationError::MissingRequiredInput {
1157 step_id: step_id.clone(),
1158 agent_id: agent_step.agent_id.clone(),
1159 capability_id: agent_step.capability_id.clone(),
1160 input_name: input.name.clone(),
1161 });
1162 }
1163 }
1164
1165 for key in &provided_keys {
1167 if key.starts_with('_') {
1169 continue;
1170 }
1171
1172 if !available_fields.contains(key) {
1173 result.warnings.push(ValidationWarning::UnknownInputField {
1174 step_id: step_id.clone(),
1175 agent_id: agent_step.agent_id.clone(),
1176 capability_id: agent_step.capability_id.clone(),
1177 field_name: key.clone(),
1178 available_fields: available_fields.clone(),
1179 });
1180 }
1181 }
1182
1183 if let Some(mapping) = &agent_step.input_mapping {
1185 let field_map: HashMap<&str, &runtara_dsl::agent_meta::CapabilityField> =
1187 inputs.iter().map(|f| (f.name.as_str(), f)).collect();
1188
1189 for (field_name, value) in mapping {
1190 if let MappingValue::Immediate(imm) = value {
1191 if let Some(field_meta) = field_map.get(field_name.as_str()) {
1192 if let Some(error) = check_type_compatibility(
1194 step_id,
1195 field_name,
1196 &field_meta.type_name,
1197 &imm.value,
1198 ) {
1199 result.errors.push(error);
1200 }
1201
1202 if let Some(enum_values) = &field_meta.enum_values {
1204 if let Some(value_str) = imm.value.as_str() {
1205 if !enum_values.contains(&value_str.to_string()) {
1206 result.errors.push(ValidationError::InvalidEnumValue {
1207 step_id: step_id.clone(),
1208 field_name: field_name.clone(),
1209 value: value_str.to_string(),
1210 allowed_values: enum_values.clone(),
1211 });
1212 }
1213 }
1214 }
1215 }
1216 }
1217 }
1218 }
1219 }
1220 }
1221 }
1222
1223 for step in graph.steps.values() {
1225 match step {
1226 Step::Split(split_step) => {
1227 validate_agents(&split_step.subgraph, result);
1228 }
1229 Step::While(while_step) => {
1230 validate_agents(&while_step.subgraph, result);
1231 }
1232 _ => {}
1233 }
1234 }
1235}
1236
1237const MAX_RETRY_RECOMMENDED: u32 = 50;
1243const MAX_RETRY_DELAY_MS: u64 = 3_600_000; const MAX_PARALLELISM_RECOMMENDED: u32 = 100;
1245const MAX_ITERATIONS_RECOMMENDED: u32 = 10_000;
1246const MAX_TIMEOUT_MS: u64 = 3_600_000; fn validate_configuration(graph: &ExecutionGraph, result: &mut ValidationResult) {
1249 for (step_id, step) in &graph.steps {
1250 match step {
1251 Step::Agent(agent_step) => {
1252 if let Some(max_retries) = agent_step.max_retries {
1254 if max_retries > MAX_RETRY_RECOMMENDED {
1255 result.warnings.push(ValidationWarning::HighRetryCount {
1256 step_id: step_id.clone(),
1257 max_retries,
1258 recommended_max: MAX_RETRY_RECOMMENDED,
1259 });
1260 }
1261 }
1262
1263 if let Some(retry_delay) = agent_step.retry_delay {
1265 if retry_delay > MAX_RETRY_DELAY_MS {
1266 result.warnings.push(ValidationWarning::LongRetryDelay {
1267 step_id: step_id.clone(),
1268 retry_delay_ms: retry_delay,
1269 recommended_max_ms: MAX_RETRY_DELAY_MS,
1270 });
1271 }
1272 }
1273
1274 if let Some(timeout) = agent_step.timeout {
1276 if timeout > MAX_TIMEOUT_MS {
1277 result.warnings.push(ValidationWarning::LongTimeout {
1278 step_id: step_id.clone(),
1279 timeout_ms: timeout,
1280 recommended_max_ms: MAX_TIMEOUT_MS,
1281 });
1282 }
1283 }
1284 }
1285
1286 Step::Split(split_step) => {
1287 if let Some(config) = &split_step.config {
1288 if let Some(parallelism) = config.parallelism {
1290 if parallelism > MAX_PARALLELISM_RECOMMENDED {
1291 result.warnings.push(ValidationWarning::HighParallelism {
1292 step_id: step_id.clone(),
1293 parallelism,
1294 recommended_max: MAX_PARALLELISM_RECOMMENDED,
1295 });
1296 }
1297 }
1298
1299 if let Some(max_retries) = config.max_retries {
1301 if max_retries > MAX_RETRY_RECOMMENDED {
1302 result.warnings.push(ValidationWarning::HighRetryCount {
1303 step_id: step_id.clone(),
1304 max_retries,
1305 recommended_max: MAX_RETRY_RECOMMENDED,
1306 });
1307 }
1308 }
1309
1310 if let Some(timeout) = config.timeout {
1312 if timeout > MAX_TIMEOUT_MS {
1313 result.warnings.push(ValidationWarning::LongTimeout {
1314 step_id: step_id.clone(),
1315 timeout_ms: timeout,
1316 recommended_max_ms: MAX_TIMEOUT_MS,
1317 });
1318 }
1319 }
1320 }
1321
1322 validate_configuration(&split_step.subgraph, result);
1324 }
1325
1326 Step::While(while_step) => {
1327 if let Some(config) = &while_step.config {
1328 if let Some(max_iterations) = config.max_iterations {
1330 if max_iterations > MAX_ITERATIONS_RECOMMENDED {
1331 result.warnings.push(ValidationWarning::HighMaxIterations {
1332 step_id: step_id.clone(),
1333 max_iterations,
1334 recommended_max: MAX_ITERATIONS_RECOMMENDED,
1335 });
1336 }
1337 }
1338
1339 if let Some(timeout) = config.timeout {
1341 if timeout > MAX_TIMEOUT_MS {
1342 result.warnings.push(ValidationWarning::LongTimeout {
1343 step_id: step_id.clone(),
1344 timeout_ms: timeout,
1345 recommended_max_ms: MAX_TIMEOUT_MS,
1346 });
1347 }
1348 }
1349 }
1350
1351 validate_configuration(&while_step.subgraph, result);
1353 }
1354
1355 Step::StartScenario(start_step) => {
1356 if let Some(max_retries) = start_step.max_retries {
1358 if max_retries > MAX_RETRY_RECOMMENDED {
1359 result.warnings.push(ValidationWarning::HighRetryCount {
1360 step_id: step_id.clone(),
1361 max_retries,
1362 recommended_max: MAX_RETRY_RECOMMENDED,
1363 });
1364 }
1365 }
1366
1367 if let Some(retry_delay) = start_step.retry_delay {
1369 if retry_delay > MAX_RETRY_DELAY_MS {
1370 result.warnings.push(ValidationWarning::LongRetryDelay {
1371 step_id: step_id.clone(),
1372 retry_delay_ms: retry_delay,
1373 recommended_max_ms: MAX_RETRY_DELAY_MS,
1374 });
1375 }
1376 }
1377
1378 if let Some(timeout) = start_step.timeout {
1380 if timeout > MAX_TIMEOUT_MS {
1381 result.warnings.push(ValidationWarning::LongTimeout {
1382 step_id: step_id.clone(),
1383 timeout_ms: timeout,
1384 recommended_max_ms: MAX_TIMEOUT_MS,
1385 });
1386 }
1387 }
1388 }
1389
1390 _ => {}
1391 }
1392 }
1393}
1394
1395fn validate_connections(graph: &ExecutionGraph, result: &mut ValidationResult) {
1400 let available_integrations: Vec<String> = runtara_dsl::agent_meta::get_all_connection_types()
1402 .map(|c| c.integration_id.to_string())
1403 .collect();
1404
1405 let mut connection_step_ids: HashSet<String> = HashSet::new();
1407
1408 for (step_id, step) in &graph.steps {
1409 if let Step::Connection(conn_step) = step {
1410 connection_step_ids.insert(step_id.clone());
1411
1412 if runtara_dsl::agent_meta::find_connection_type(&conn_step.integration_id).is_none() {
1414 result.errors.push(ValidationError::UnknownIntegration {
1415 step_id: step_id.clone(),
1416 integration_id: conn_step.integration_id.clone(),
1417 available_integrations: available_integrations.clone(),
1418 });
1419 }
1420 }
1421 }
1422
1423 if !connection_step_ids.is_empty() {
1425 let referenced_connections = collect_referenced_connections(graph);
1426
1427 for conn_id in &connection_step_ids {
1428 if !referenced_connections.contains(conn_id) {
1429 result.warnings.push(ValidationWarning::UnusedConnection {
1430 step_id: conn_id.clone(),
1431 });
1432 }
1433 }
1434 }
1435
1436 for step in graph.steps.values() {
1438 match step {
1439 Step::Split(split_step) => {
1440 validate_connections(&split_step.subgraph, result);
1441 }
1442 Step::While(while_step) => {
1443 validate_connections(&while_step.subgraph, result);
1444 }
1445 _ => {}
1446 }
1447 }
1448}
1449
1450fn collect_referenced_connections(graph: &ExecutionGraph) -> HashSet<String> {
1452 let mut referenced = HashSet::new();
1453
1454 for step in graph.steps.values() {
1455 let mappings = collect_step_mappings(step);
1456
1457 for mapping in mappings {
1458 for value in mapping.values() {
1459 for step_id in extract_step_ids_from_mapping_value(value) {
1461 referenced.insert(step_id);
1462 }
1463 }
1464 }
1465
1466 match step {
1468 Step::Split(split_step) => {
1469 referenced.extend(collect_referenced_connections(&split_step.subgraph));
1470 }
1471 Step::While(while_step) => {
1472 referenced.extend(collect_referenced_connections(&while_step.subgraph));
1473 }
1474 _ => {}
1475 }
1476 }
1477
1478 referenced
1479}
1480
1481fn validate_security(graph: &ExecutionGraph, result: &mut ValidationResult) {
1486 let connection_step_ids: HashSet<String> = graph
1488 .steps
1489 .iter()
1490 .filter_map(|(id, step)| {
1491 if matches!(step, Step::Connection(_)) {
1492 Some(id.clone())
1493 } else {
1494 None
1495 }
1496 })
1497 .collect();
1498
1499 if connection_step_ids.is_empty() {
1501 return;
1502 }
1503
1504 for (step_id, step) in &graph.steps {
1506 match step {
1507 Step::Agent(agent_step) => {
1508 let is_secure = runtara_dsl::agent_meta::find_agent_module(&agent_step.agent_id)
1510 .map(|m| m.secure)
1511 .unwrap_or(false);
1512
1513 if !is_secure {
1514 if let Some(mapping) = &agent_step.input_mapping {
1516 for conn_id in find_connection_references(mapping, &connection_step_ids) {
1517 result
1518 .errors
1519 .push(ValidationError::ConnectionLeakToNonSecureAgent {
1520 connection_step_id: conn_id,
1521 agent_step_id: step_id.clone(),
1522 agent_id: agent_step.agent_id.clone(),
1523 });
1524 }
1525 }
1526 }
1527 }
1528 Step::Finish(finish_step) => {
1529 if let Some(mapping) = &finish_step.input_mapping {
1531 for conn_id in find_connection_references(mapping, &connection_step_ids) {
1532 result.errors.push(ValidationError::ConnectionLeakToFinish {
1533 connection_step_id: conn_id,
1534 finish_step_id: step_id.clone(),
1535 });
1536 }
1537 }
1538 }
1539 Step::Log(log_step) => {
1540 if let Some(mapping) = &log_step.context {
1542 for conn_id in find_connection_references(mapping, &connection_step_ids) {
1543 result.errors.push(ValidationError::ConnectionLeakToLog {
1544 connection_step_id: conn_id,
1545 log_step_id: step_id.clone(),
1546 });
1547 }
1548 }
1549 }
1550 Step::Split(split_step) => {
1551 validate_security(&split_step.subgraph, result);
1553 }
1554 Step::While(while_step) => {
1555 validate_security(&while_step.subgraph, result);
1557 }
1558 Step::Conditional(_)
1559 | Step::Switch(_)
1560 | Step::StartScenario(_)
1561 | Step::Connection(_) => {}
1562 }
1563 }
1564}
1565
1566fn validate_child_scenarios(graph: &ExecutionGraph, result: &mut ValidationResult) {
1571 for (step_id, step) in &graph.steps {
1572 if let Step::StartScenario(start_step) = step {
1573 match &start_step.child_version {
1575 runtara_dsl::ChildVersion::Latest(s) => {
1576 let s_lower = s.to_lowercase();
1577 if s_lower != "latest" && s_lower != "current" {
1578 result.errors.push(ValidationError::InvalidChildVersion {
1579 step_id: step_id.clone(),
1580 child_scenario_id: start_step.child_scenario_id.clone(),
1581 version: s.clone(),
1582 reason: "must be 'latest', 'current', or a version number".to_string(),
1583 });
1584 }
1585 }
1586 runtara_dsl::ChildVersion::Specific(n) => {
1587 if *n < 1 {
1588 result.errors.push(ValidationError::InvalidChildVersion {
1589 step_id: step_id.clone(),
1590 child_scenario_id: start_step.child_scenario_id.clone(),
1591 version: n.to_string(),
1592 reason: "version number must be positive".to_string(),
1593 });
1594 }
1595 }
1596 }
1597 }
1598 }
1599
1600 for step in graph.steps.values() {
1602 match step {
1603 Step::Split(split_step) => {
1604 validate_child_scenarios(&split_step.subgraph, result);
1605 }
1606 Step::While(while_step) => {
1607 validate_child_scenarios(&while_step.subgraph, result);
1608 }
1609 _ => {}
1610 }
1611 }
1612}
1613
1614fn validate_step_names(graph: &ExecutionGraph, result: &mut ValidationResult) {
1620 let mut name_to_step_ids: HashMap<String, Vec<String>> = HashMap::new();
1621
1622 collect_step_names(graph, &mut name_to_step_ids);
1624
1625 for (name, step_ids) in name_to_step_ids {
1627 if step_ids.len() > 1 {
1628 result
1629 .errors
1630 .push(ValidationError::DuplicateStepName { name, step_ids });
1631 }
1632 }
1633}
1634
1635fn collect_step_names(graph: &ExecutionGraph, name_to_step_ids: &mut HashMap<String, Vec<String>>) {
1638 for (step_id, step) in &graph.steps {
1639 let name = match step {
1641 Step::Agent(s) => s.name.as_ref(),
1642 Step::Finish(s) => s.name.as_ref(),
1643 Step::Conditional(s) => s.name.as_ref(),
1644 Step::Split(s) => s.name.as_ref(),
1645 Step::Switch(s) => s.name.as_ref(),
1646 Step::StartScenario(s) => s.name.as_ref(),
1647 Step::While(s) => s.name.as_ref(),
1648 Step::Log(s) => s.name.as_ref(),
1649 Step::Connection(s) => s.name.as_ref(),
1650 };
1651
1652 if let Some(name) = name {
1653 name_to_step_ids
1654 .entry(name.clone())
1655 .or_default()
1656 .push(step_id.clone());
1657 }
1658
1659 match step {
1663 Step::Split(split_step) => {
1664 collect_step_names(&split_step.subgraph, name_to_step_ids);
1665 }
1666 Step::While(while_step) => {
1667 collect_step_names(&while_step.subgraph, name_to_step_ids);
1668 }
1669 _ => {}
1670 }
1671 }
1672}
1673
1674fn check_type_compatibility(
1681 step_id: &str,
1682 field_name: &str,
1683 expected_type: &str,
1684 actual_value: &serde_json::Value,
1685) -> Option<ValidationError> {
1686 let expected_lower = expected_type.to_lowercase();
1688
1689 let is_compatible = match expected_lower.as_str() {
1690 "any" => true, "string" => actual_value.is_string(),
1692 "integer" | "int" | "i32" | "i64" | "u32" | "u64" | "isize" | "usize" => {
1693 actual_value.is_i64() || actual_value.is_u64()
1694 }
1695 "number" | "float" | "f32" | "f64" => actual_value.is_number(),
1696 "boolean" | "bool" => actual_value.is_boolean(),
1697 "array" => actual_value.is_array(),
1698 "object" => actual_value.is_object(),
1699 _ if expected_lower.starts_with("vec<")
1701 || expected_lower.starts_with("hashmap<")
1702 || expected_lower.starts_with("option<")
1703 || expected_lower == "value"
1704 || expected_lower == "json" =>
1705 {
1706 true
1707 }
1708 _ => true,
1710 };
1711
1712 if is_compatible {
1713 None
1714 } else {
1715 let actual_type = get_json_type_name(actual_value);
1716 Some(ValidationError::TypeMismatch {
1717 step_id: step_id.to_string(),
1718 field_name: field_name.to_string(),
1719 expected_type: expected_type.to_string(),
1720 actual_type,
1721 })
1722 }
1723}
1724
1725fn get_json_type_name(value: &serde_json::Value) -> String {
1727 match value {
1728 serde_json::Value::Null => "null".to_string(),
1729 serde_json::Value::Bool(_) => "boolean".to_string(),
1730 serde_json::Value::Number(n) => {
1731 if n.is_i64() || n.is_u64() {
1732 "integer".to_string()
1733 } else {
1734 "number".to_string()
1735 }
1736 }
1737 serde_json::Value::String(_) => "string".to_string(),
1738 serde_json::Value::Array(_) => "array".to_string(),
1739 serde_json::Value::Object(_) => "object".to_string(),
1740 }
1741}
1742
1743fn extract_step_ids_from_mapping_value(value: &MappingValue) -> Vec<String> {
1745 let mut step_ids = Vec::new();
1746 match value {
1747 MappingValue::Reference(ref_value) => {
1748 if let Some(step_id) = extract_step_id_from_reference(&ref_value.value) {
1749 step_ids.push(step_id);
1750 }
1751 }
1752 MappingValue::Immediate(_) => {
1753 }
1755 MappingValue::Composite(comp_value) => match &comp_value.value {
1756 CompositeInner::Object(map) => {
1757 for nested_value in map.values() {
1758 step_ids.extend(extract_step_ids_from_mapping_value(nested_value));
1759 }
1760 }
1761 CompositeInner::Array(arr) => {
1762 for nested_value in arr {
1763 step_ids.extend(extract_step_ids_from_mapping_value(nested_value));
1764 }
1765 }
1766 },
1767 }
1768 step_ids
1769}
1770
1771fn extract_step_id_from_reference(ref_path: &str) -> Option<String> {
1774 if ref_path.starts_with("steps[") {
1777 let rest = &ref_path[5..]; if let Some(end) = rest.find(']') {
1779 let inner = &rest[1..end]; let step_id = inner.trim_matches(|c| c == '\'' || c == '"');
1782 return Some(step_id.to_string());
1783 }
1784 }
1785
1786 if ref_path.starts_with("steps.") {
1788 let rest = &ref_path[6..]; if let Some(dot_pos) = rest.find('.') {
1791 return Some(rest[..dot_pos].to_string());
1792 } else {
1793 return Some(rest.to_string());
1795 }
1796 }
1797 None
1798}
1799
1800fn extract_variable_name_from_reference(ref_path: &str) -> Option<String> {
1802 if ref_path.starts_with("variables.") {
1803 let rest = &ref_path[10..]; if let Some(dot_pos) = rest.find('.') {
1806 return Some(rest[..dot_pos].to_string());
1807 } else {
1808 return Some(rest.to_string());
1810 }
1811 }
1812 None
1813}
1814
1815fn find_connection_references(
1817 mapping: &InputMapping,
1818 connection_step_ids: &HashSet<String>,
1819) -> Vec<String> {
1820 let mut found = Vec::new();
1821
1822 for value in mapping.values() {
1823 for step_id in extract_step_ids_from_mapping_value(value) {
1825 if connection_step_ids.contains(&step_id) {
1826 found.push(step_id);
1827 }
1828 }
1829 }
1830
1831 found
1832}
1833
1834fn get_step_type_name(step: &Step) -> &'static str {
1836 match step {
1837 Step::Agent(_) => "Agent",
1838 Step::Finish(_) => "Finish",
1839 Step::Conditional(_) => "Conditional",
1840 Step::Split(_) => "Split",
1841 Step::Switch(_) => "Switch",
1842 Step::StartScenario(_) => "StartScenario",
1843 Step::While(_) => "While",
1844 Step::Log(_) => "Log",
1845 Step::Connection(_) => "Connection",
1846 }
1847}
1848
1849fn find_similar_name(target: &str, candidates: &[String]) -> Option<String> {
1851 let target_lower = target.to_lowercase();
1852
1853 candidates
1854 .iter()
1855 .filter_map(|candidate| {
1856 let distance = levenshtein_distance(&target_lower, &candidate.to_lowercase());
1857 if distance <= target.len() / 2 + 2 {
1859 Some((candidate.clone(), distance))
1860 } else {
1861 None
1862 }
1863 })
1864 .min_by_key(|(_, d)| *d)
1865 .map(|(name, _)| name)
1866}
1867
1868fn levenshtein_distance(a: &str, b: &str) -> usize {
1870 let a_chars: Vec<char> = a.chars().collect();
1871 let b_chars: Vec<char> = b.chars().collect();
1872 let m = a_chars.len();
1873 let n = b_chars.len();
1874
1875 if m == 0 {
1876 return n;
1877 }
1878 if n == 0 {
1879 return m;
1880 }
1881
1882 let mut prev: Vec<usize> = (0..=n).collect();
1883 let mut curr = vec![0; n + 1];
1884
1885 for i in 1..=m {
1886 curr[0] = i;
1887 for j in 1..=n {
1888 let cost = if a_chars[i - 1] == b_chars[j - 1] {
1889 0
1890 } else {
1891 1
1892 };
1893 curr[j] = (prev[j] + 1).min((curr[j - 1] + 1).min(prev[j - 1] + cost));
1894 }
1895 std::mem::swap(&mut prev, &mut curr);
1896 }
1897
1898 prev[n]
1899}
1900
1901fn format_duration(ms: u64) -> String {
1903 if ms < 1000 {
1904 format!("{}ms", ms)
1905 } else if ms < 60_000 {
1906 format!("{:.1}s", ms as f64 / 1000.0)
1907 } else if ms < 3_600_000 {
1908 format!("{:.1}min", ms as f64 / 60_000.0)
1909 } else {
1910 format!("{:.1}h", ms as f64 / 3_600_000.0)
1911 }
1912}
1913
1914#[cfg(test)]
1919mod tests {
1920 use super::*;
1921 use runtara_dsl::{
1922 AgentStep, ConnectionStep, FinishStep, LogLevel, LogStep, ReferenceValue, StartScenarioStep,
1923 };
1924
1925 fn create_connection_step(id: &str) -> Step {
1926 Step::Connection(ConnectionStep {
1927 id: id.to_string(),
1928 name: None,
1929 connection_id: "test-conn".to_string(),
1930 integration_id: "bearer".to_string(),
1931 })
1932 }
1933
1934 fn create_agent_step(id: &str, agent_id: &str, mapping: Option<InputMapping>) -> Step {
1935 let capability_id = if agent_id == "transform" {
1937 "map".to_string()
1938 } else if agent_id == "http" {
1939 "request".to_string()
1940 } else {
1941 "map".to_string() };
1943 Step::Agent(AgentStep {
1944 id: id.to_string(),
1945 name: None,
1946 agent_id: agent_id.to_string(),
1947 capability_id,
1948 connection_id: None,
1949 input_mapping: mapping,
1950 max_retries: None,
1951 retry_delay: None,
1952 timeout: None,
1953 })
1954 }
1955
1956 fn create_finish_step(id: &str, mapping: Option<InputMapping>) -> Step {
1957 Step::Finish(FinishStep {
1958 id: id.to_string(),
1959 name: None,
1960 input_mapping: mapping,
1961 })
1962 }
1963
1964 fn create_log_step(id: &str, context: Option<InputMapping>) -> Step {
1965 Step::Log(LogStep {
1966 id: id.to_string(),
1967 name: None,
1968 level: LogLevel::Info,
1969 message: "test".to_string(),
1970 context,
1971 })
1972 }
1973
1974 fn ref_value(path: &str) -> MappingValue {
1975 MappingValue::Reference(ReferenceValue {
1976 value: path.to_string(),
1977 type_hint: None,
1978 default: None,
1979 })
1980 }
1981
1982 fn create_basic_graph(steps: HashMap<String, Step>, entry_point: &str) -> ExecutionGraph {
1983 ExecutionGraph {
1984 name: None,
1985 description: None,
1986 steps,
1987 entry_point: entry_point.to_string(),
1988 execution_plan: vec![],
1989 variables: HashMap::new(),
1990 input_schema: HashMap::new(),
1991 output_schema: HashMap::new(),
1992 notes: None,
1993 nodes: None,
1994 edges: None,
1995 }
1996 }
1997
1998 #[test]
2001 fn test_empty_workflow() {
2002 let graph = create_basic_graph(HashMap::new(), "start");
2003 let result = validate_workflow(&graph);
2004 assert!(result.has_errors());
2005 assert!(
2006 result
2007 .errors
2008 .iter()
2009 .any(|e| matches!(e, ValidationError::EmptyWorkflow))
2010 );
2011 }
2012
2013 #[test]
2014 fn test_entry_point_not_found() {
2015 let mut steps = HashMap::new();
2016 steps.insert("finish".to_string(), create_finish_step("finish", None));
2017
2018 let graph = create_basic_graph(steps, "nonexistent");
2019 let result = validate_workflow(&graph);
2020 assert!(result.has_errors());
2021 assert!(result.errors.iter().any(
2022 |e| matches!(e, ValidationError::EntryPointNotFound { entry_point, .. } if entry_point == "nonexistent")
2023 ));
2024 }
2025
2026 #[test]
2027 fn test_valid_simple_workflow() {
2028 let mut steps = HashMap::new();
2029 steps.insert("finish".to_string(), create_finish_step("finish", None));
2030
2031 let graph = create_basic_graph(steps, "finish");
2032 let result = validate_workflow(&graph);
2033 assert!(!result.has_errors());
2035 }
2036
2037 #[test]
2040 fn test_invalid_step_reference() {
2041 let mut steps = HashMap::new();
2042 let mut mapping = HashMap::new();
2043 mapping.insert("data".to_string(), ref_value("steps.nonexistent.outputs"));
2044 steps.insert(
2045 "agent".to_string(),
2046 create_agent_step("agent", "transform", Some(mapping)),
2047 );
2048 steps.insert("finish".to_string(), create_finish_step("finish", None));
2049
2050 let mut graph = create_basic_graph(steps, "agent");
2051 graph.execution_plan = vec![runtara_dsl::ExecutionPlanEdge {
2052 from_step: "agent".to_string(),
2053 to_step: "finish".to_string(),
2054 label: None,
2055 }];
2056
2057 let result = validate_workflow(&graph);
2058 assert!(result.has_errors());
2059 assert!(result.errors.iter().any(
2060 |e| matches!(e, ValidationError::InvalidStepReference { referenced_step_id, .. } if referenced_step_id == "nonexistent")
2061 ));
2062 }
2063
2064 #[test]
2065 fn test_invalid_reference_path_double_dots() {
2066 let mut steps = HashMap::new();
2067 let mut mapping = HashMap::new();
2068 mapping.insert("data".to_string(), ref_value("steps..outputs"));
2069 steps.insert(
2070 "agent".to_string(),
2071 create_agent_step("agent", "transform", Some(mapping)),
2072 );
2073 steps.insert("finish".to_string(), create_finish_step("finish", None));
2074
2075 let mut graph = create_basic_graph(steps, "agent");
2076 graph.execution_plan = vec![runtara_dsl::ExecutionPlanEdge {
2077 from_step: "agent".to_string(),
2078 to_step: "finish".to_string(),
2079 label: None,
2080 }];
2081
2082 let result = validate_workflow(&graph);
2083 assert!(result.has_errors());
2084 assert!(
2085 result
2086 .errors
2087 .iter()
2088 .any(|e| matches!(e, ValidationError::InvalidReferencePath { .. }))
2089 );
2090 }
2091
2092 #[test]
2095 fn test_no_connection_steps_passes() {
2096 let mut steps = HashMap::new();
2097 steps.insert(
2098 "agent".to_string(),
2099 create_agent_step("agent", "transform", None),
2100 );
2101 steps.insert("finish".to_string(), create_finish_step("finish", None));
2102
2103 let mut graph = create_basic_graph(steps, "agent");
2104 graph.execution_plan = vec![runtara_dsl::ExecutionPlanEdge {
2105 from_step: "agent".to_string(),
2106 to_step: "finish".to_string(),
2107 label: None,
2108 }];
2109
2110 let result = validate_workflow(&graph);
2111 let security_errors = result.errors.iter().any(|e| {
2115 matches!(
2116 e,
2117 ValidationError::ConnectionLeakToNonSecureAgent { .. }
2118 | ValidationError::ConnectionLeakToFinish { .. }
2119 | ValidationError::ConnectionLeakToLog { .. }
2120 )
2121 });
2122 assert!(!security_errors, "Expected no security errors");
2123 }
2124
2125 #[test]
2126 fn test_connection_to_secure_agent_passes() {
2127 let mut steps = HashMap::new();
2128 steps.insert("conn".to_string(), create_connection_step("conn"));
2129
2130 let mut mapping = HashMap::new();
2131 mapping.insert("_connection".to_string(), ref_value("steps.conn.outputs"));
2132 steps.insert(
2133 "http_call".to_string(),
2134 create_agent_step("http_call", "http", Some(mapping)),
2135 );
2136 steps.insert("finish".to_string(), create_finish_step("finish", None));
2137
2138 let mut graph = create_basic_graph(steps, "conn");
2139 graph.execution_plan = vec![
2140 runtara_dsl::ExecutionPlanEdge {
2141 from_step: "conn".to_string(),
2142 to_step: "http_call".to_string(),
2143 label: None,
2144 },
2145 runtara_dsl::ExecutionPlanEdge {
2146 from_step: "http_call".to_string(),
2147 to_step: "finish".to_string(),
2148 label: None,
2149 },
2150 ];
2151
2152 let result = validate_workflow(&graph);
2153 assert!(
2155 !result
2156 .errors
2157 .iter()
2158 .any(|e| matches!(e, ValidationError::ConnectionLeakToNonSecureAgent { .. }))
2159 );
2160 }
2161
2162 #[test]
2163 fn test_connection_to_non_secure_agent_fails() {
2164 let mut steps = HashMap::new();
2165 steps.insert("conn".to_string(), create_connection_step("conn"));
2166
2167 let mut mapping = HashMap::new();
2168 mapping.insert("data".to_string(), ref_value("steps.conn.outputs"));
2169 steps.insert(
2170 "transform".to_string(),
2171 create_agent_step("transform", "transform", Some(mapping)),
2172 );
2173 steps.insert("finish".to_string(), create_finish_step("finish", None));
2174
2175 let mut graph = create_basic_graph(steps, "conn");
2176 graph.execution_plan = vec![
2177 runtara_dsl::ExecutionPlanEdge {
2178 from_step: "conn".to_string(),
2179 to_step: "transform".to_string(),
2180 label: None,
2181 },
2182 runtara_dsl::ExecutionPlanEdge {
2183 from_step: "transform".to_string(),
2184 to_step: "finish".to_string(),
2185 label: None,
2186 },
2187 ];
2188
2189 let result = validate_workflow(&graph);
2190 assert!(result.errors.iter().any(|e| matches!(
2191 e,
2192 ValidationError::ConnectionLeakToNonSecureAgent { agent_id, .. } if agent_id == "transform"
2193 )));
2194 }
2195
2196 #[test]
2197 fn test_connection_to_finish_fails() {
2198 let mut steps = HashMap::new();
2199 steps.insert("conn".to_string(), create_connection_step("conn"));
2200
2201 let mut mapping = HashMap::new();
2202 mapping.insert("credentials".to_string(), ref_value("steps.conn.outputs"));
2203 steps.insert(
2204 "finish".to_string(),
2205 create_finish_step("finish", Some(mapping)),
2206 );
2207
2208 let mut graph = create_basic_graph(steps, "conn");
2209 graph.execution_plan = vec![runtara_dsl::ExecutionPlanEdge {
2210 from_step: "conn".to_string(),
2211 to_step: "finish".to_string(),
2212 label: None,
2213 }];
2214
2215 let result = validate_workflow(&graph);
2216 assert!(
2217 result
2218 .errors
2219 .iter()
2220 .any(|e| matches!(e, ValidationError::ConnectionLeakToFinish { .. }))
2221 );
2222 }
2223
2224 #[test]
2225 fn test_connection_to_log_fails() {
2226 let mut steps = HashMap::new();
2227 steps.insert("conn".to_string(), create_connection_step("conn"));
2228
2229 let mut mapping = HashMap::new();
2230 mapping.insert(
2231 "secret".to_string(),
2232 ref_value("steps.conn.outputs.parameters"),
2233 );
2234 steps.insert("log".to_string(), create_log_step("log", Some(mapping)));
2235 steps.insert("finish".to_string(), create_finish_step("finish", None));
2236
2237 let mut graph = create_basic_graph(steps, "conn");
2238 graph.execution_plan = vec![
2239 runtara_dsl::ExecutionPlanEdge {
2240 from_step: "conn".to_string(),
2241 to_step: "log".to_string(),
2242 label: None,
2243 },
2244 runtara_dsl::ExecutionPlanEdge {
2245 from_step: "log".to_string(),
2246 to_step: "finish".to_string(),
2247 label: None,
2248 },
2249 ];
2250
2251 let result = validate_workflow(&graph);
2252 assert!(
2253 result
2254 .errors
2255 .iter()
2256 .any(|e| matches!(e, ValidationError::ConnectionLeakToLog { .. }))
2257 );
2258 }
2259
2260 #[test]
2263 fn test_high_retry_count_warning() {
2264 let mut steps = HashMap::new();
2265 steps.insert(
2266 "agent".to_string(),
2267 Step::Agent(AgentStep {
2268 id: "agent".to_string(),
2269 name: None,
2270 agent_id: "transform".to_string(),
2271 capability_id: "map".to_string(),
2272 connection_id: None,
2273 input_mapping: None,
2274 max_retries: Some(100),
2275 retry_delay: None,
2276 timeout: None,
2277 }),
2278 );
2279 steps.insert("finish".to_string(), create_finish_step("finish", None));
2280
2281 let mut graph = create_basic_graph(steps, "agent");
2282 graph.execution_plan = vec![runtara_dsl::ExecutionPlanEdge {
2283 from_step: "agent".to_string(),
2284 to_step: "finish".to_string(),
2285 label: None,
2286 }];
2287
2288 let result = validate_workflow(&graph);
2289 assert!(result.has_warnings());
2290 assert!(result.warnings.iter().any(|w| matches!(
2291 w,
2292 ValidationWarning::HighRetryCount {
2293 max_retries: 100,
2294 ..
2295 }
2296 )));
2297 }
2298
2299 #[test]
2302 fn test_invalid_child_version() {
2303 let mut steps = HashMap::new();
2304 steps.insert(
2305 "start_child".to_string(),
2306 Step::StartScenario(StartScenarioStep {
2307 id: "start_child".to_string(),
2308 name: None,
2309 child_scenario_id: "child-workflow".to_string(),
2310 child_version: runtara_dsl::ChildVersion::Latest("invalid".to_string()),
2311 input_mapping: None,
2312 max_retries: None,
2313 retry_delay: None,
2314 timeout: None,
2315 }),
2316 );
2317 steps.insert("finish".to_string(), create_finish_step("finish", None));
2318
2319 let mut graph = create_basic_graph(steps, "start_child");
2320 graph.execution_plan = vec![runtara_dsl::ExecutionPlanEdge {
2321 from_step: "start_child".to_string(),
2322 to_step: "finish".to_string(),
2323 label: None,
2324 }];
2325
2326 let result = validate_workflow(&graph);
2327 assert!(result.has_errors());
2328 assert!(
2329 result
2330 .errors
2331 .iter()
2332 .any(|e| matches!(e, ValidationError::InvalidChildVersion { .. }))
2333 );
2334 }
2335
2336 #[test]
2337 fn test_valid_child_version_latest() {
2338 let mut steps = HashMap::new();
2339 steps.insert(
2340 "start_child".to_string(),
2341 Step::StartScenario(StartScenarioStep {
2342 id: "start_child".to_string(),
2343 name: None,
2344 child_scenario_id: "child-workflow".to_string(),
2345 child_version: runtara_dsl::ChildVersion::Latest("latest".to_string()),
2346 input_mapping: None,
2347 max_retries: None,
2348 retry_delay: None,
2349 timeout: None,
2350 }),
2351 );
2352 steps.insert("finish".to_string(), create_finish_step("finish", None));
2353
2354 let mut graph = create_basic_graph(steps, "start_child");
2355 graph.execution_plan = vec![runtara_dsl::ExecutionPlanEdge {
2356 from_step: "start_child".to_string(),
2357 to_step: "finish".to_string(),
2358 label: None,
2359 }];
2360
2361 let result = validate_workflow(&graph);
2362 assert!(
2363 !result
2364 .errors
2365 .iter()
2366 .any(|e| matches!(e, ValidationError::InvalidChildVersion { .. }))
2367 );
2368 }
2369
2370 #[test]
2373 fn test_levenshtein_distance() {
2374 assert_eq!(levenshtein_distance("http", "http"), 0);
2375 assert_eq!(levenshtein_distance("http", "htpp"), 1);
2377 assert_eq!(levenshtein_distance("transform", "transfrom"), 2);
2379 assert_eq!(levenshtein_distance("", "abc"), 3);
2380 assert_eq!(levenshtein_distance("abc", ""), 3);
2381 }
2382
2383 #[test]
2384 fn test_find_similar_name() {
2385 let candidates = vec![
2386 "http".to_string(),
2387 "transform".to_string(),
2388 "utils".to_string(),
2389 ];
2390 assert_eq!(
2391 find_similar_name("htpp", &candidates),
2392 Some("http".to_string())
2393 );
2394 assert_eq!(
2395 find_similar_name("transfrom", &candidates),
2396 Some("transform".to_string())
2397 );
2398 assert_eq!(find_similar_name("completely_different", &candidates), None);
2399 }
2400
2401 #[test]
2402 fn test_format_duration() {
2403 assert_eq!(format_duration(500), "500ms");
2404 assert_eq!(format_duration(1500), "1.5s");
2405 assert_eq!(format_duration(90_000), "1.5min");
2406 assert_eq!(format_duration(5_400_000), "1.5h");
2407 }
2408
2409 #[test]
2410 fn test_extract_step_id_bracket_notation() {
2411 assert_eq!(
2412 extract_step_id_from_reference("steps['my-step'].outputs"),
2413 Some("my-step".to_string())
2414 );
2415 assert_eq!(
2416 extract_step_id_from_reference("steps[\"my-step\"].outputs"),
2417 Some("my-step".to_string())
2418 );
2419 }
2420
2421 #[test]
2422 fn test_extract_step_id_dot_notation() {
2423 assert_eq!(
2424 extract_step_id_from_reference("steps.my_step.outputs"),
2425 Some("my_step".to_string())
2426 );
2427 assert_eq!(
2428 extract_step_id_from_reference("steps.another_step.outputs.data"),
2429 Some("another_step".to_string())
2430 );
2431 assert_eq!(
2433 extract_step_id_from_reference("steps.simple"),
2434 Some("simple".to_string())
2435 );
2436 }
2437
2438 #[test]
2439 fn test_extract_step_id_non_step_reference() {
2440 assert_eq!(extract_step_id_from_reference("variables.foo"), None);
2442 assert_eq!(extract_step_id_from_reference("inputs.data"), None);
2443 assert_eq!(extract_step_id_from_reference("foo.bar"), None);
2444 }
2445
2446 #[test]
2449 fn test_validation_result_is_ok() {
2450 let result = ValidationResult::default();
2451 assert!(result.is_ok());
2452 assert!(!result.has_errors());
2453 assert!(!result.has_warnings());
2454 }
2455
2456 #[test]
2457 fn test_validation_result_with_errors() {
2458 let mut result = ValidationResult::default();
2459 result.errors.push(ValidationError::EmptyWorkflow);
2460 assert!(!result.is_ok());
2461 assert!(result.has_errors());
2462 }
2463
2464 #[test]
2465 fn test_validation_result_with_warnings() {
2466 let mut result = ValidationResult::default();
2467 result.warnings.push(ValidationWarning::UnusedConnection {
2468 step_id: "test".to_string(),
2469 });
2470 assert!(result.is_ok()); assert!(!result.has_errors());
2472 assert!(result.has_warnings());
2473 }
2474
2475 #[test]
2476 fn test_validation_result_merge() {
2477 let mut result1 = ValidationResult::default();
2478 result1.errors.push(ValidationError::EmptyWorkflow);
2479
2480 let mut result2 = ValidationResult::default();
2481 result2.warnings.push(ValidationWarning::UnusedConnection {
2482 step_id: "conn".to_string(),
2483 });
2484
2485 result1.merge(result2);
2486 assert_eq!(result1.errors.len(), 1);
2487 assert_eq!(result1.warnings.len(), 1);
2488 }
2489
2490 #[test]
2493 fn test_error_display_entry_point_not_found() {
2494 let error = ValidationError::EntryPointNotFound {
2495 entry_point: "start".to_string(),
2496 available_steps: vec!["step1".to_string(), "step2".to_string()],
2497 };
2498 let display = format!("{}", error);
2499 assert!(display.contains("[E001]"));
2500 assert!(display.contains("start"));
2501 assert!(display.contains("step1, step2"));
2502 }
2503
2504 #[test]
2505 fn test_error_display_unreachable_step() {
2506 let error = ValidationError::UnreachableStep {
2507 step_id: "orphan".to_string(),
2508 };
2509 let display = format!("{}", error);
2510 assert!(display.contains("[E002]"));
2511 assert!(display.contains("orphan"));
2512 assert!(display.contains("unreachable"));
2513 }
2514
2515 #[test]
2516 fn test_warning_display_dangling_step() {
2517 let warning = ValidationWarning::DanglingStep {
2518 step_id: "dead_end".to_string(),
2519 step_type: "Agent".to_string(),
2520 };
2521 let display = format!("{}", warning);
2522 assert!(display.contains("[W003]"));
2523 assert!(display.contains("dead_end"));
2524 assert!(display.contains("Agent"));
2525 }
2526
2527 #[test]
2528 fn test_error_display_with_suggestion() {
2529 let error = ValidationError::UnknownAgent {
2530 step_id: "step1".to_string(),
2531 agent_id: "htpp".to_string(),
2532 available_agents: vec!["http".to_string(), "transform".to_string()],
2533 };
2534 let display = format!("{}", error);
2535 assert!(display.contains("[E020]"));
2536 assert!(display.contains("htpp"));
2537 assert!(display.contains("Did you mean 'http'?"));
2538 }
2539
2540 #[test]
2541 fn test_error_display_security_violation() {
2542 let error = ValidationError::ConnectionLeakToNonSecureAgent {
2543 connection_step_id: "conn".to_string(),
2544 agent_step_id: "transform_step".to_string(),
2545 agent_id: "transform".to_string(),
2546 };
2547 let display = format!("{}", error);
2548 assert!(display.contains("[E040]"));
2549 assert!(display.contains("Security violation"));
2550 assert!(display.contains("conn"));
2551 assert!(display.contains("transform"));
2552 }
2553
2554 #[test]
2557 fn test_warning_display_high_retry() {
2558 let warning = ValidationWarning::HighRetryCount {
2559 step_id: "step1".to_string(),
2560 max_retries: 100,
2561 recommended_max: 50,
2562 };
2563 let display = format!("{}", warning);
2564 assert!(display.contains("[W030]"));
2565 assert!(display.contains("100"));
2566 assert!(display.contains("50"));
2567 }
2568
2569 #[test]
2570 fn test_warning_display_long_timeout() {
2571 let warning = ValidationWarning::LongTimeout {
2572 step_id: "step1".to_string(),
2573 timeout_ms: 7_200_000, recommended_max_ms: 3_600_000, };
2576 let display = format!("{}", warning);
2577 assert!(display.contains("[W034]"));
2578 assert!(display.contains("2.0h"));
2579 assert!(display.contains("1.0h"));
2580 }
2581
2582 #[test]
2583 fn test_warning_display_unused_connection() {
2584 let warning = ValidationWarning::UnusedConnection {
2585 step_id: "my_conn".to_string(),
2586 };
2587 let display = format!("{}", warning);
2588 assert!(display.contains("[W040]"));
2589 assert!(display.contains("my_conn"));
2590 assert!(display.contains("never referenced"));
2591 }
2592
2593 #[test]
2594 fn test_warning_display_self_reference() {
2595 let warning = ValidationWarning::SelfReference {
2596 step_id: "loop_step".to_string(),
2597 reference_path: "steps.loop_step.outputs.data".to_string(),
2598 };
2599 let display = format!("{}", warning);
2600 assert!(display.contains("[W050]"));
2601 assert!(display.contains("loop_step"));
2602 assert!(display.contains("references its own outputs"));
2603 }
2604
2605 #[test]
2608 fn test_unreachable_step_detection() {
2609 let mut steps = HashMap::new();
2610 steps.insert("start".to_string(), create_finish_step("start", None));
2611 steps.insert("orphan".to_string(), create_finish_step("orphan", None));
2612
2613 let graph = create_basic_graph(steps, "start");
2614 let result = validate_workflow(&graph);
2615
2616 assert!(result.errors.iter().any(
2617 |e| matches!(e, ValidationError::UnreachableStep { step_id } if step_id == "orphan")
2618 ));
2619 }
2620
2621 #[test]
2622 fn test_dangling_agent_step() {
2623 let mut steps = HashMap::new();
2624 steps.insert(
2625 "agent".to_string(),
2626 create_agent_step("agent", "transform", None),
2627 );
2628 let graph = create_basic_graph(steps, "agent");
2631 let result = validate_workflow(&graph);
2632
2633 assert!(result.warnings.iter().any(
2635 |w| matches!(w, ValidationWarning::DanglingStep { step_id, .. } if step_id == "agent")
2636 ));
2637 }
2638
2639 #[test]
2640 fn test_finish_step_allowed_no_outgoing() {
2641 let mut steps = HashMap::new();
2643 steps.insert("finish".to_string(), create_finish_step("finish", None));
2644
2645 let graph = create_basic_graph(steps, "finish");
2646 let result = validate_workflow(&graph);
2647
2648 assert!(
2650 !result
2651 .warnings
2652 .iter()
2653 .any(|w| matches!(w, ValidationWarning::DanglingStep { .. }))
2654 );
2655 }
2656
2657 #[test]
2660 fn test_self_reference_warning() {
2661 let mut steps = HashMap::new();
2662 let mut mapping = HashMap::new();
2663 mapping.insert(
2665 "data".to_string(),
2666 ref_value("steps.my_step.outputs.previous"),
2667 );
2668 steps.insert(
2669 "my_step".to_string(),
2670 create_agent_step("my_step", "transform", Some(mapping)),
2671 );
2672 steps.insert("finish".to_string(), create_finish_step("finish", None));
2673
2674 let mut graph = create_basic_graph(steps, "my_step");
2675 graph.execution_plan = vec![runtara_dsl::ExecutionPlanEdge {
2676 from_step: "my_step".to_string(),
2677 to_step: "finish".to_string(),
2678 label: None,
2679 }];
2680
2681 let result = validate_workflow(&graph);
2682 assert!(result.warnings.iter().any(
2683 |w| matches!(w, ValidationWarning::SelfReference { step_id, .. } if step_id == "my_step")
2684 ));
2685 }
2686
2687 #[test]
2690 fn test_long_retry_delay_warning() {
2691 let mut steps = HashMap::new();
2692 steps.insert(
2693 "agent".to_string(),
2694 Step::Agent(AgentStep {
2695 id: "agent".to_string(),
2696 name: None,
2697 agent_id: "transform".to_string(),
2698 capability_id: "map".to_string(),
2699 connection_id: None,
2700 input_mapping: None,
2701 max_retries: None,
2702 retry_delay: Some(5_000_000), timeout: None,
2704 }),
2705 );
2706 steps.insert("finish".to_string(), create_finish_step("finish", None));
2707
2708 let mut graph = create_basic_graph(steps, "agent");
2709 graph.execution_plan = vec![runtara_dsl::ExecutionPlanEdge {
2710 from_step: "agent".to_string(),
2711 to_step: "finish".to_string(),
2712 label: None,
2713 }];
2714
2715 let result = validate_workflow(&graph);
2716 assert!(result.warnings.iter().any(|w| matches!(
2717 w,
2718 ValidationWarning::LongRetryDelay {
2719 retry_delay_ms: 5_000_000,
2720 ..
2721 }
2722 )));
2723 }
2724
2725 #[test]
2726 fn test_normal_config_no_warnings() {
2727 let mut steps = HashMap::new();
2728 steps.insert(
2729 "agent".to_string(),
2730 Step::Agent(AgentStep {
2731 id: "agent".to_string(),
2732 name: None,
2733 agent_id: "transform".to_string(),
2734 capability_id: "map".to_string(),
2735 connection_id: None,
2736 input_mapping: None,
2737 max_retries: Some(3), retry_delay: Some(1000), timeout: Some(30_000), }),
2741 );
2742 steps.insert("finish".to_string(), create_finish_step("finish", None));
2743
2744 let mut graph = create_basic_graph(steps, "agent");
2745 graph.execution_plan = vec![runtara_dsl::ExecutionPlanEdge {
2746 from_step: "agent".to_string(),
2747 to_step: "finish".to_string(),
2748 label: None,
2749 }];
2750
2751 let result = validate_workflow(&graph);
2752 let config_warnings = result.warnings.iter().any(|w| {
2754 matches!(
2755 w,
2756 ValidationWarning::HighRetryCount { .. }
2757 | ValidationWarning::LongRetryDelay { .. }
2758 | ValidationWarning::LongTimeout { .. }
2759 )
2760 });
2761 assert!(!config_warnings);
2762 }
2763
2764 #[test]
2767 fn test_child_version_current_valid() {
2768 let mut steps = HashMap::new();
2769 steps.insert(
2770 "child".to_string(),
2771 Step::StartScenario(StartScenarioStep {
2772 id: "child".to_string(),
2773 name: None,
2774 child_scenario_id: "other-workflow".to_string(),
2775 child_version: runtara_dsl::ChildVersion::Latest("current".to_string()),
2776 input_mapping: None,
2777 max_retries: None,
2778 retry_delay: None,
2779 timeout: None,
2780 }),
2781 );
2782 steps.insert("finish".to_string(), create_finish_step("finish", None));
2783
2784 let mut graph = create_basic_graph(steps, "child");
2785 graph.execution_plan = vec![runtara_dsl::ExecutionPlanEdge {
2786 from_step: "child".to_string(),
2787 to_step: "finish".to_string(),
2788 label: None,
2789 }];
2790
2791 let result = validate_workflow(&graph);
2792 assert!(
2793 !result
2794 .errors
2795 .iter()
2796 .any(|e| matches!(e, ValidationError::InvalidChildVersion { .. }))
2797 );
2798 }
2799
2800 #[test]
2801 fn test_child_version_specific_valid() {
2802 let mut steps = HashMap::new();
2803 steps.insert(
2804 "child".to_string(),
2805 Step::StartScenario(StartScenarioStep {
2806 id: "child".to_string(),
2807 name: None,
2808 child_scenario_id: "other-workflow".to_string(),
2809 child_version: runtara_dsl::ChildVersion::Specific(5),
2810 input_mapping: None,
2811 max_retries: None,
2812 retry_delay: None,
2813 timeout: None,
2814 }),
2815 );
2816 steps.insert("finish".to_string(), create_finish_step("finish", None));
2817
2818 let mut graph = create_basic_graph(steps, "child");
2819 graph.execution_plan = vec![runtara_dsl::ExecutionPlanEdge {
2820 from_step: "child".to_string(),
2821 to_step: "finish".to_string(),
2822 label: None,
2823 }];
2824
2825 let result = validate_workflow(&graph);
2826 assert!(
2827 !result
2828 .errors
2829 .iter()
2830 .any(|e| matches!(e, ValidationError::InvalidChildVersion { .. }))
2831 );
2832 }
2833
2834 #[test]
2835 fn test_child_version_zero_invalid() {
2836 let mut steps = HashMap::new();
2837 steps.insert(
2838 "child".to_string(),
2839 Step::StartScenario(StartScenarioStep {
2840 id: "child".to_string(),
2841 name: None,
2842 child_scenario_id: "other-workflow".to_string(),
2843 child_version: runtara_dsl::ChildVersion::Specific(0),
2844 input_mapping: None,
2845 max_retries: None,
2846 retry_delay: None,
2847 timeout: None,
2848 }),
2849 );
2850 steps.insert("finish".to_string(), create_finish_step("finish", None));
2851
2852 let mut graph = create_basic_graph(steps, "child");
2853 graph.execution_plan = vec![runtara_dsl::ExecutionPlanEdge {
2854 from_step: "child".to_string(),
2855 to_step: "finish".to_string(),
2856 label: None,
2857 }];
2858
2859 let result = validate_workflow(&graph);
2860 assert!(result.errors.iter().any(|e| matches!(
2861 e,
2862 ValidationError::InvalidChildVersion { reason, .. } if reason.contains("positive")
2863 )));
2864 }
2865
2866 #[test]
2869 fn test_levenshtein_single_char_diff() {
2870 assert_eq!(levenshtein_distance("cat", "bat"), 1);
2871 assert_eq!(levenshtein_distance("cat", "car"), 1);
2872 assert_eq!(levenshtein_distance("cat", "cats"), 1);
2873 }
2874
2875 #[test]
2876 fn test_levenshtein_insertions_deletions() {
2877 assert_eq!(levenshtein_distance("abc", "ab"), 1);
2878 assert_eq!(levenshtein_distance("ab", "abc"), 1);
2879 assert_eq!(levenshtein_distance("", ""), 0);
2880 }
2881
2882 #[test]
2883 fn test_find_similar_name_no_close_match() {
2884 let candidates = vec!["alpha".to_string(), "beta".to_string()];
2885 assert_eq!(find_similar_name("xyz", &candidates), None);
2887 }
2888
2889 #[test]
2890 fn test_find_similar_name_empty_candidates() {
2891 let candidates: Vec<String> = vec![];
2892 assert_eq!(find_similar_name("anything", &candidates), None);
2893 }
2894
2895 #[test]
2896 fn test_find_similar_name_case_insensitive() {
2897 let candidates = vec!["HTTP".to_string(), "Transform".to_string()];
2898 assert_eq!(
2899 find_similar_name("http", &candidates),
2900 Some("HTTP".to_string())
2901 );
2902 }
2903
2904 fn create_while_step(
2909 id: &str,
2910 condition: runtara_dsl::ConditionExpression,
2911 subgraph: ExecutionGraph,
2912 max_iterations: Option<u32>,
2913 ) -> Step {
2914 use runtara_dsl::{WhileConfig, WhileStep};
2915 Step::While(WhileStep {
2916 id: id.to_string(),
2917 name: None,
2918 condition,
2919 subgraph: Box::new(subgraph),
2920 config: Some(WhileConfig {
2921 max_iterations,
2922 timeout: None,
2923 }),
2924 })
2925 }
2926
2927 fn create_lt_condition(left_ref: &str, right_ref: &str) -> runtara_dsl::ConditionExpression {
2928 use runtara_dsl::{
2929 ConditionArgument, ConditionExpression, ConditionOperation, ConditionOperator,
2930 };
2931 ConditionExpression::Operation(ConditionOperation {
2932 op: ConditionOperator::Lt,
2933 arguments: vec![
2934 ConditionArgument::Value(ref_value(left_ref)),
2935 ConditionArgument::Value(ref_value(right_ref)),
2936 ],
2937 })
2938 }
2939
2940 fn create_simple_subgraph() -> ExecutionGraph {
2941 let mut steps = HashMap::new();
2942 steps.insert("finish".to_string(), create_finish_step("finish", None));
2943 ExecutionGraph {
2944 name: None,
2945 description: None,
2946 steps,
2947 entry_point: "finish".to_string(),
2948 execution_plan: vec![],
2949 variables: HashMap::new(),
2950 input_schema: HashMap::new(),
2951 output_schema: HashMap::new(),
2952 notes: None,
2953 nodes: None,
2954 edges: None,
2955 }
2956 }
2957
2958 #[test]
2959 fn test_while_step_valid_condition() {
2960 let mut steps = HashMap::new();
2961
2962 let mut init_mapping = HashMap::new();
2964 init_mapping.insert("counter".to_string(), ref_value("data.counter"));
2965 init_mapping.insert("target".to_string(), ref_value("data.target"));
2966 steps.insert(
2967 "init".to_string(),
2968 create_agent_step("init", "transform", Some(init_mapping)),
2969 );
2970
2971 let condition =
2973 create_lt_condition("steps.init.outputs.counter", "steps.init.outputs.target");
2974 let subgraph = create_simple_subgraph();
2975 steps.insert(
2976 "loop".to_string(),
2977 create_while_step("loop", condition, subgraph, Some(10)),
2978 );
2979
2980 steps.insert("finish".to_string(), create_finish_step("finish", None));
2981
2982 let mut graph = create_basic_graph(steps, "init");
2983 graph.execution_plan = vec![
2984 runtara_dsl::ExecutionPlanEdge {
2985 from_step: "init".to_string(),
2986 to_step: "loop".to_string(),
2987 label: None,
2988 },
2989 runtara_dsl::ExecutionPlanEdge {
2990 from_step: "loop".to_string(),
2991 to_step: "finish".to_string(),
2992 label: None,
2993 },
2994 ];
2995
2996 let result = validate_workflow(&graph);
2997 let ref_errors = result
2999 .errors
3000 .iter()
3001 .any(|e| matches!(e, ValidationError::InvalidStepReference { .. }));
3002 assert!(!ref_errors, "Expected no invalid step reference errors");
3003 }
3004
3005 #[test]
3006 fn test_while_step_nested_subgraph_validation() {
3007 let mut steps = HashMap::new();
3008
3009 steps.insert(
3011 "init".to_string(),
3012 create_agent_step("init", "transform", None),
3013 );
3014
3015 let mut subgraph_steps = HashMap::new();
3017 let mut mapping = HashMap::new();
3018 mapping.insert("data".to_string(), ref_value("data.value"));
3019 subgraph_steps.insert(
3020 "process".to_string(),
3021 create_agent_step("process", "transform", Some(mapping)),
3022 );
3023 subgraph_steps.insert("finish".to_string(), create_finish_step("finish", None));
3024
3025 let subgraph = ExecutionGraph {
3026 name: None,
3027 description: None,
3028 steps: subgraph_steps,
3029 entry_point: "process".to_string(),
3030 execution_plan: vec![runtara_dsl::ExecutionPlanEdge {
3031 from_step: "process".to_string(),
3032 to_step: "finish".to_string(),
3033 label: None,
3034 }],
3035 variables: HashMap::new(),
3036 input_schema: HashMap::new(),
3037 output_schema: HashMap::new(),
3038 notes: None,
3039 nodes: None,
3040 edges: None,
3041 };
3042
3043 use runtara_dsl::{ConditionExpression, ImmediateValue, MappingValue};
3045 let condition = ConditionExpression::Value(MappingValue::Immediate(ImmediateValue {
3046 value: serde_json::json!(true),
3047 }));
3048
3049 steps.insert(
3050 "loop".to_string(),
3051 create_while_step("loop", condition, subgraph, Some(5)),
3052 );
3053
3054 steps.insert("finish".to_string(), create_finish_step("finish", None));
3055
3056 let mut graph = create_basic_graph(steps, "init");
3057 graph.execution_plan = vec![
3058 runtara_dsl::ExecutionPlanEdge {
3059 from_step: "init".to_string(),
3060 to_step: "loop".to_string(),
3061 label: None,
3062 },
3063 runtara_dsl::ExecutionPlanEdge {
3064 from_step: "loop".to_string(),
3065 to_step: "finish".to_string(),
3066 label: None,
3067 },
3068 ];
3069
3070 let result = validate_workflow(&graph);
3071 let subgraph_errors = result.errors.iter().any(|e| {
3074 matches!(e, ValidationError::EntryPointNotFound { entry_point, .. } if entry_point == "process")
3075 });
3076 assert!(!subgraph_errors, "Expected no subgraph entry point errors");
3077 }
3078
3079 #[test]
3080 fn test_while_step_invalid_reference_in_condition() {
3081 let mut steps = HashMap::new();
3086
3087 steps.insert(
3088 "init".to_string(),
3089 create_agent_step("init", "transform", None),
3090 );
3091
3092 let condition = create_lt_condition(
3094 "steps.nonexistent.outputs.value",
3095 "steps.init.outputs.target",
3096 );
3097 let subgraph = create_simple_subgraph();
3098 steps.insert(
3099 "loop".to_string(),
3100 create_while_step("loop", condition, subgraph, Some(10)),
3101 );
3102
3103 steps.insert("finish".to_string(), create_finish_step("finish", None));
3104
3105 let mut graph = create_basic_graph(steps, "init");
3106 graph.execution_plan = vec![
3107 runtara_dsl::ExecutionPlanEdge {
3108 from_step: "init".to_string(),
3109 to_step: "loop".to_string(),
3110 label: None,
3111 },
3112 runtara_dsl::ExecutionPlanEdge {
3113 from_step: "loop".to_string(),
3114 to_step: "finish".to_string(),
3115 label: None,
3116 },
3117 ];
3118
3119 let result = validate_workflow(&graph);
3120 assert!(result.is_ok() || !result.errors.is_empty());
3123 }
3124
3125 #[test]
3126 fn test_while_step_complex_and_condition() {
3127 use runtara_dsl::{
3128 ConditionArgument, ConditionExpression, ConditionOperation, ConditionOperator,
3129 };
3130
3131 let mut steps = HashMap::new();
3132
3133 steps.insert(
3134 "init".to_string(),
3135 create_agent_step("init", "transform", None),
3136 );
3137
3138 let condition = ConditionExpression::Operation(ConditionOperation {
3140 op: ConditionOperator::And,
3141 arguments: vec![
3142 ConditionArgument::Expression(Box::new(ConditionExpression::Operation(
3143 ConditionOperation {
3144 op: ConditionOperator::Gte,
3145 arguments: vec![
3146 ConditionArgument::Value(ref_value("steps.init.outputs.counter")),
3147 ConditionArgument::Value(ref_value("steps.init.outputs.min")),
3148 ],
3149 },
3150 ))),
3151 ConditionArgument::Expression(Box::new(ConditionExpression::Operation(
3152 ConditionOperation {
3153 op: ConditionOperator::Lt,
3154 arguments: vec![
3155 ConditionArgument::Value(ref_value("steps.init.outputs.counter")),
3156 ConditionArgument::Value(ref_value("steps.init.outputs.max")),
3157 ],
3158 },
3159 ))),
3160 ],
3161 });
3162
3163 let subgraph = create_simple_subgraph();
3164 steps.insert(
3165 "loop".to_string(),
3166 create_while_step("loop", condition, subgraph, Some(50)),
3167 );
3168
3169 steps.insert("finish".to_string(), create_finish_step("finish", None));
3170
3171 let mut graph = create_basic_graph(steps, "init");
3172 graph.execution_plan = vec![
3173 runtara_dsl::ExecutionPlanEdge {
3174 from_step: "init".to_string(),
3175 to_step: "loop".to_string(),
3176 label: None,
3177 },
3178 runtara_dsl::ExecutionPlanEdge {
3179 from_step: "loop".to_string(),
3180 to_step: "finish".to_string(),
3181 label: None,
3182 },
3183 ];
3184
3185 let result = validate_workflow(&graph);
3186 let ref_errors = result
3188 .errors
3189 .iter()
3190 .any(|e| matches!(e, ValidationError::InvalidStepReference { .. }));
3191 assert!(
3192 !ref_errors,
3193 "Expected no invalid step reference errors in complex condition"
3194 );
3195 }
3196
3197 #[test]
3198 fn test_while_step_with_loop_index_reference() {
3199 let mut steps = HashMap::new();
3200
3201 steps.insert(
3202 "init".to_string(),
3203 create_agent_step("init", "transform", None),
3204 );
3205
3206 let condition = create_lt_condition("loop.index", "steps.init.outputs.maxIterations");
3208
3209 let mut subgraph_steps = HashMap::new();
3211 let mut mapping = HashMap::new();
3212 mapping.insert("index".to_string(), ref_value("variables._index"));
3213 subgraph_steps.insert(
3214 "process".to_string(),
3215 create_agent_step("process", "transform", Some(mapping)),
3216 );
3217 subgraph_steps.insert("finish".to_string(), create_finish_step("finish", None));
3218
3219 let subgraph = ExecutionGraph {
3220 name: None,
3221 description: None,
3222 steps: subgraph_steps,
3223 entry_point: "process".to_string(),
3224 execution_plan: vec![runtara_dsl::ExecutionPlanEdge {
3225 from_step: "process".to_string(),
3226 to_step: "finish".to_string(),
3227 label: None,
3228 }],
3229 variables: HashMap::new(),
3230 input_schema: HashMap::new(),
3231 output_schema: HashMap::new(),
3232 notes: None,
3233 nodes: None,
3234 edges: None,
3235 };
3236
3237 steps.insert(
3238 "loop".to_string(),
3239 create_while_step("loop", condition, subgraph, Some(100)),
3240 );
3241
3242 steps.insert("finish".to_string(), create_finish_step("finish", None));
3243
3244 let mut graph = create_basic_graph(steps, "init");
3245 graph.execution_plan = vec![
3246 runtara_dsl::ExecutionPlanEdge {
3247 from_step: "init".to_string(),
3248 to_step: "loop".to_string(),
3249 label: None,
3250 },
3251 runtara_dsl::ExecutionPlanEdge {
3252 from_step: "loop".to_string(),
3253 to_step: "finish".to_string(),
3254 label: None,
3255 },
3256 ];
3257
3258 let result = validate_workflow(&graph);
3259 let loop_ref_errors = result.errors.iter().any(|e| {
3262 matches!(e, ValidationError::InvalidReferencePath { reference_path, .. } if reference_path.contains("loop.index"))
3263 });
3264 assert!(
3265 !loop_ref_errors,
3266 "loop.index should be a valid reference in while conditions"
3267 );
3268 }
3269
3270 fn create_log_step_with_level(
3275 id: &str,
3276 level: LogLevel,
3277 message: &str,
3278 context: Option<InputMapping>,
3279 ) -> Step {
3280 Step::Log(LogStep {
3281 id: id.to_string(),
3282 name: None,
3283 level,
3284 message: message.to_string(),
3285 context,
3286 })
3287 }
3288
3289 #[test]
3290 fn test_log_step_valid_info() {
3291 let mut steps = HashMap::new();
3292 steps.insert(
3293 "log".to_string(),
3294 create_log_step_with_level("log", LogLevel::Info, "Test message", None),
3295 );
3296 steps.insert("finish".to_string(), create_finish_step("finish", None));
3297
3298 let mut graph = create_basic_graph(steps, "log");
3299 graph.execution_plan = vec![runtara_dsl::ExecutionPlanEdge {
3300 from_step: "log".to_string(),
3301 to_step: "finish".to_string(),
3302 label: None,
3303 }];
3304
3305 let result = validate_workflow(&graph);
3306 assert!(
3308 !result.has_errors(),
3309 "Basic log step should not cause errors"
3310 );
3311 }
3312
3313 #[test]
3314 fn test_log_step_valid_all_levels() {
3315 let mut steps = HashMap::new();
3316 steps.insert(
3317 "log_debug".to_string(),
3318 create_log_step_with_level("log_debug", LogLevel::Debug, "Debug", None),
3319 );
3320 steps.insert(
3321 "log_info".to_string(),
3322 create_log_step_with_level("log_info", LogLevel::Info, "Info", None),
3323 );
3324 steps.insert(
3325 "log_warn".to_string(),
3326 create_log_step_with_level("log_warn", LogLevel::Warn, "Warn", None),
3327 );
3328 steps.insert(
3329 "log_error".to_string(),
3330 create_log_step_with_level("log_error", LogLevel::Error, "Error", None),
3331 );
3332 steps.insert("finish".to_string(), create_finish_step("finish", None));
3333
3334 let mut graph = create_basic_graph(steps, "log_debug");
3335 graph.execution_plan = vec![
3336 runtara_dsl::ExecutionPlanEdge {
3337 from_step: "log_debug".to_string(),
3338 to_step: "log_info".to_string(),
3339 label: None,
3340 },
3341 runtara_dsl::ExecutionPlanEdge {
3342 from_step: "log_info".to_string(),
3343 to_step: "log_warn".to_string(),
3344 label: None,
3345 },
3346 runtara_dsl::ExecutionPlanEdge {
3347 from_step: "log_warn".to_string(),
3348 to_step: "log_error".to_string(),
3349 label: None,
3350 },
3351 runtara_dsl::ExecutionPlanEdge {
3352 from_step: "log_error".to_string(),
3353 to_step: "finish".to_string(),
3354 label: None,
3355 },
3356 ];
3357
3358 let result = validate_workflow(&graph);
3359 assert!(!result.has_errors(), "All log levels should be valid");
3361 }
3362
3363 #[test]
3364 fn test_log_step_valid_context_mapping() {
3365 let mut steps = HashMap::new();
3366
3367 steps.insert(
3369 "process".to_string(),
3370 create_agent_step("process", "transform", None),
3371 );
3372
3373 let mut context = HashMap::new();
3375 context.insert(
3376 "processResult".to_string(),
3377 ref_value("steps.process.outputs"),
3378 );
3379 context.insert("inputData".to_string(), ref_value("data"));
3380 steps.insert(
3381 "log".to_string(),
3382 create_log_step_with_level("log", LogLevel::Info, "Processing done", Some(context)),
3383 );
3384
3385 steps.insert("finish".to_string(), create_finish_step("finish", None));
3386
3387 let mut graph = create_basic_graph(steps, "process");
3388 graph.execution_plan = vec![
3389 runtara_dsl::ExecutionPlanEdge {
3390 from_step: "process".to_string(),
3391 to_step: "log".to_string(),
3392 label: None,
3393 },
3394 runtara_dsl::ExecutionPlanEdge {
3395 from_step: "log".to_string(),
3396 to_step: "finish".to_string(),
3397 label: None,
3398 },
3399 ];
3400
3401 let result = validate_workflow(&graph);
3402 let ref_errors = result
3404 .errors
3405 .iter()
3406 .any(|e| matches!(e, ValidationError::InvalidStepReference { .. }));
3407 assert!(
3408 !ref_errors,
3409 "Valid context references should not cause errors"
3410 );
3411 }
3412
3413 #[test]
3414 fn test_log_step_invalid_context_reference() {
3415 let mut steps = HashMap::new();
3416
3417 let mut context = HashMap::new();
3419 context.insert("result".to_string(), ref_value("steps.nonexistent.outputs"));
3420 steps.insert(
3421 "log".to_string(),
3422 create_log_step_with_level("log", LogLevel::Info, "Test", Some(context)),
3423 );
3424
3425 steps.insert("finish".to_string(), create_finish_step("finish", None));
3426
3427 let mut graph = create_basic_graph(steps, "log");
3428 graph.execution_plan = vec![runtara_dsl::ExecutionPlanEdge {
3429 from_step: "log".to_string(),
3430 to_step: "finish".to_string(),
3431 label: None,
3432 }];
3433
3434 let result = validate_workflow(&graph);
3435 assert!(result.errors.iter().any(|e| {
3437 matches!(e, ValidationError::InvalidStepReference { referenced_step_id, .. } if referenced_step_id == "nonexistent")
3438 }));
3439 }
3440
3441 #[test]
3442 fn test_log_step_empty_context() {
3443 let mut steps = HashMap::new();
3444
3445 let context = HashMap::new();
3447 steps.insert(
3448 "log".to_string(),
3449 create_log_step_with_level("log", LogLevel::Debug, "Empty context test", Some(context)),
3450 );
3451
3452 steps.insert("finish".to_string(), create_finish_step("finish", None));
3453
3454 let mut graph = create_basic_graph(steps, "log");
3455 graph.execution_plan = vec![runtara_dsl::ExecutionPlanEdge {
3456 from_step: "log".to_string(),
3457 to_step: "finish".to_string(),
3458 label: None,
3459 }];
3460
3461 let result = validate_workflow(&graph);
3462 assert!(
3464 !result.has_errors(),
3465 "Empty context should not cause errors"
3466 );
3467 }
3468
3469 fn create_connection_step_with_type(
3474 id: &str,
3475 connection_id: &str,
3476 integration_id: &str,
3477 ) -> Step {
3478 Step::Connection(ConnectionStep {
3479 id: id.to_string(),
3480 name: None,
3481 connection_id: connection_id.to_string(),
3482 integration_id: integration_id.to_string(),
3483 })
3484 }
3485
3486 #[test]
3487 fn test_connection_step_valid_bearer() {
3488 let mut steps = HashMap::new();
3489 steps.insert(
3490 "conn".to_string(),
3491 create_connection_step_with_type("conn", "my-api", "bearer"),
3492 );
3493
3494 let mut mapping = HashMap::new();
3495 mapping.insert("_connection".to_string(), ref_value("steps.conn.outputs"));
3496 steps.insert(
3497 "http_call".to_string(),
3498 create_agent_step("http_call", "http", Some(mapping)),
3499 );
3500 steps.insert("finish".to_string(), create_finish_step("finish", None));
3501
3502 let mut graph = create_basic_graph(steps, "conn");
3503 graph.execution_plan = vec![
3504 runtara_dsl::ExecutionPlanEdge {
3505 from_step: "conn".to_string(),
3506 to_step: "http_call".to_string(),
3507 label: None,
3508 },
3509 runtara_dsl::ExecutionPlanEdge {
3510 from_step: "http_call".to_string(),
3511 to_step: "finish".to_string(),
3512 label: None,
3513 },
3514 ];
3515
3516 let result = validate_workflow(&graph);
3517 let security_errors = result
3519 .errors
3520 .iter()
3521 .any(|e| matches!(e, ValidationError::ConnectionLeakToNonSecureAgent { .. }));
3522 assert!(
3523 !security_errors,
3524 "Bearer connection to HTTP should be secure"
3525 );
3526 }
3527
3528 #[test]
3529 fn test_connection_step_valid_api_key() {
3530 let mut steps = HashMap::new();
3531 steps.insert(
3532 "conn".to_string(),
3533 create_connection_step_with_type("conn", "my-api", "api_key"),
3534 );
3535
3536 let mut mapping = HashMap::new();
3537 mapping.insert("_connection".to_string(), ref_value("steps.conn.outputs"));
3538 steps.insert(
3539 "http_call".to_string(),
3540 create_agent_step("http_call", "http", Some(mapping)),
3541 );
3542 steps.insert("finish".to_string(), create_finish_step("finish", None));
3543
3544 let mut graph = create_basic_graph(steps, "conn");
3545 graph.execution_plan = vec![
3546 runtara_dsl::ExecutionPlanEdge {
3547 from_step: "conn".to_string(),
3548 to_step: "http_call".to_string(),
3549 label: None,
3550 },
3551 runtara_dsl::ExecutionPlanEdge {
3552 from_step: "http_call".to_string(),
3553 to_step: "finish".to_string(),
3554 label: None,
3555 },
3556 ];
3557
3558 let result = validate_workflow(&graph);
3559 let security_errors = result
3560 .errors
3561 .iter()
3562 .any(|e| matches!(e, ValidationError::ConnectionLeakToNonSecureAgent { .. }));
3563 assert!(
3564 !security_errors,
3565 "API key connection to HTTP should be secure"
3566 );
3567 }
3568
3569 #[test]
3570 fn test_connection_step_valid_basic_auth() {
3571 let mut steps = HashMap::new();
3572 steps.insert(
3573 "conn".to_string(),
3574 create_connection_step_with_type("conn", "my-service", "basic_auth"),
3575 );
3576
3577 let mut mapping = HashMap::new();
3578 mapping.insert("_connection".to_string(), ref_value("steps.conn.outputs"));
3579 steps.insert(
3580 "http_call".to_string(),
3581 create_agent_step("http_call", "http", Some(mapping)),
3582 );
3583 steps.insert("finish".to_string(), create_finish_step("finish", None));
3584
3585 let mut graph = create_basic_graph(steps, "conn");
3586 graph.execution_plan = vec![
3587 runtara_dsl::ExecutionPlanEdge {
3588 from_step: "conn".to_string(),
3589 to_step: "http_call".to_string(),
3590 label: None,
3591 },
3592 runtara_dsl::ExecutionPlanEdge {
3593 from_step: "http_call".to_string(),
3594 to_step: "finish".to_string(),
3595 label: None,
3596 },
3597 ];
3598
3599 let result = validate_workflow(&graph);
3600 let security_errors = result
3601 .errors
3602 .iter()
3603 .any(|e| matches!(e, ValidationError::ConnectionLeakToNonSecureAgent { .. }));
3604 assert!(
3605 !security_errors,
3606 "Basic auth connection to HTTP should be secure"
3607 );
3608 }
3609
3610 #[test]
3611 fn test_connection_step_valid_sftp() {
3612 let mut steps = HashMap::new();
3613 steps.insert(
3614 "conn".to_string(),
3615 create_connection_step_with_type("conn", "sftp-server", "sftp"),
3616 );
3617
3618 let mut mapping = HashMap::new();
3619 mapping.insert("_connection".to_string(), ref_value("steps.conn.outputs"));
3620 steps.insert(
3621 "sftp_call".to_string(),
3622 create_agent_step("sftp_call", "sftp", Some(mapping)),
3623 );
3624 steps.insert("finish".to_string(), create_finish_step("finish", None));
3625
3626 let mut graph = create_basic_graph(steps, "conn");
3627 graph.execution_plan = vec![
3628 runtara_dsl::ExecutionPlanEdge {
3629 from_step: "conn".to_string(),
3630 to_step: "sftp_call".to_string(),
3631 label: None,
3632 },
3633 runtara_dsl::ExecutionPlanEdge {
3634 from_step: "sftp_call".to_string(),
3635 to_step: "finish".to_string(),
3636 label: None,
3637 },
3638 ];
3639
3640 let result = validate_workflow(&graph);
3641 let security_errors = result
3642 .errors
3643 .iter()
3644 .any(|e| matches!(e, ValidationError::ConnectionLeakToNonSecureAgent { .. }));
3645 assert!(
3646 !security_errors,
3647 "SFTP connection to SFTP agent should be secure"
3648 );
3649 }
3650
3651 #[test]
3652 fn test_connection_step_unused_warning() {
3653 let mut steps = HashMap::new();
3654 steps.insert(
3656 "conn".to_string(),
3657 create_connection_step_with_type("conn", "unused-api", "bearer"),
3658 );
3659 steps.insert(
3660 "agent".to_string(),
3661 create_agent_step("agent", "transform", None), );
3663 steps.insert("finish".to_string(), create_finish_step("finish", None));
3664
3665 let mut graph = create_basic_graph(steps, "conn");
3666 graph.execution_plan = vec![
3667 runtara_dsl::ExecutionPlanEdge {
3668 from_step: "conn".to_string(),
3669 to_step: "agent".to_string(),
3670 label: None,
3671 },
3672 runtara_dsl::ExecutionPlanEdge {
3673 from_step: "agent".to_string(),
3674 to_step: "finish".to_string(),
3675 label: None,
3676 },
3677 ];
3678
3679 let result = validate_workflow(&graph);
3680 assert!(result.warnings.iter().any(|w| {
3682 matches!(w, ValidationWarning::UnusedConnection { step_id } if step_id == "conn")
3683 }));
3684 }
3685
3686 #[test]
3687 fn test_connection_multiple_connections() {
3688 let mut steps = HashMap::new();
3689
3690 steps.insert(
3692 "conn1".to_string(),
3693 create_connection_step_with_type("conn1", "api-1", "bearer"),
3694 );
3695 steps.insert(
3696 "conn2".to_string(),
3697 create_connection_step_with_type("conn2", "api-2", "api_key"),
3698 );
3699
3700 let mut mapping1 = HashMap::new();
3702 mapping1.insert("_connection".to_string(), ref_value("steps.conn1.outputs"));
3703 steps.insert(
3704 "call1".to_string(),
3705 create_agent_step("call1", "http", Some(mapping1)),
3706 );
3707
3708 let mut mapping2 = HashMap::new();
3709 mapping2.insert("_connection".to_string(), ref_value("steps.conn2.outputs"));
3710 steps.insert(
3711 "call2".to_string(),
3712 create_agent_step("call2", "http", Some(mapping2)),
3713 );
3714
3715 steps.insert("finish".to_string(), create_finish_step("finish", None));
3716
3717 let mut graph = create_basic_graph(steps, "conn1");
3718 graph.execution_plan = vec![
3719 runtara_dsl::ExecutionPlanEdge {
3720 from_step: "conn1".to_string(),
3721 to_step: "conn2".to_string(),
3722 label: None,
3723 },
3724 runtara_dsl::ExecutionPlanEdge {
3725 from_step: "conn2".to_string(),
3726 to_step: "call1".to_string(),
3727 label: None,
3728 },
3729 runtara_dsl::ExecutionPlanEdge {
3730 from_step: "call1".to_string(),
3731 to_step: "call2".to_string(),
3732 label: None,
3733 },
3734 runtara_dsl::ExecutionPlanEdge {
3735 from_step: "call2".to_string(),
3736 to_step: "finish".to_string(),
3737 label: None,
3738 },
3739 ];
3740
3741 let result = validate_workflow(&graph);
3742 let security_errors = result.errors.iter().any(|e| {
3744 matches!(
3745 e,
3746 ValidationError::ConnectionLeakToNonSecureAgent { .. }
3747 | ValidationError::ConnectionLeakToFinish { .. }
3748 | ValidationError::ConnectionLeakToLog { .. }
3749 )
3750 });
3751 assert!(
3752 !security_errors,
3753 "Multiple valid connections should not cause security errors"
3754 );
3755
3756 let unused_warnings = result
3758 .warnings
3759 .iter()
3760 .any(|w| matches!(w, ValidationWarning::UnusedConnection { .. }));
3761 assert!(
3762 !unused_warnings,
3763 "Used connections should not trigger unused warning"
3764 );
3765 }
3766
3767 #[test]
3768 fn test_connection_in_while_subgraph_to_secure_agent() {
3769 use runtara_dsl::{ConditionExpression, ImmediateValue, MappingValue};
3770
3771 let mut steps = HashMap::new();
3772
3773 steps.insert(
3774 "init".to_string(),
3775 create_agent_step("init", "transform", None),
3776 );
3777
3778 let mut subgraph_steps = HashMap::new();
3780 subgraph_steps.insert(
3781 "conn".to_string(),
3782 create_connection_step_with_type("conn", "rate-limited-api", "bearer"),
3783 );
3784 let mut mapping = HashMap::new();
3785 mapping.insert("_connection".to_string(), ref_value("steps.conn.outputs"));
3786 subgraph_steps.insert(
3787 "call".to_string(),
3788 create_agent_step("call", "http", Some(mapping)),
3789 );
3790 subgraph_steps.insert("finish".to_string(), create_finish_step("finish", None));
3791
3792 let subgraph = ExecutionGraph {
3793 name: None,
3794 description: None,
3795 steps: subgraph_steps,
3796 entry_point: "conn".to_string(),
3797 execution_plan: vec![
3798 runtara_dsl::ExecutionPlanEdge {
3799 from_step: "conn".to_string(),
3800 to_step: "call".to_string(),
3801 label: None,
3802 },
3803 runtara_dsl::ExecutionPlanEdge {
3804 from_step: "call".to_string(),
3805 to_step: "finish".to_string(),
3806 label: None,
3807 },
3808 ],
3809 variables: HashMap::new(),
3810 input_schema: HashMap::new(),
3811 output_schema: HashMap::new(),
3812 notes: None,
3813 nodes: None,
3814 edges: None,
3815 };
3816
3817 let condition = ConditionExpression::Value(MappingValue::Immediate(ImmediateValue {
3818 value: serde_json::json!(true),
3819 }));
3820
3821 steps.insert(
3822 "loop".to_string(),
3823 create_while_step("loop", condition, subgraph, Some(10)),
3824 );
3825
3826 steps.insert("finish".to_string(), create_finish_step("finish", None));
3827
3828 let mut graph = create_basic_graph(steps, "init");
3829 graph.execution_plan = vec![
3830 runtara_dsl::ExecutionPlanEdge {
3831 from_step: "init".to_string(),
3832 to_step: "loop".to_string(),
3833 label: None,
3834 },
3835 runtara_dsl::ExecutionPlanEdge {
3836 from_step: "loop".to_string(),
3837 to_step: "finish".to_string(),
3838 label: None,
3839 },
3840 ];
3841
3842 let result = validate_workflow(&graph);
3843 let security_errors = result
3845 .errors
3846 .iter()
3847 .any(|e| matches!(e, ValidationError::ConnectionLeakToNonSecureAgent { .. }));
3848 assert!(
3849 !security_errors,
3850 "Connection in subgraph to HTTP should be secure"
3851 );
3852 }
3853
3854 #[test]
3861 fn test_forward_reference_error() {
3862 let mut steps = HashMap::new();
3864
3865 let mut mapping = HashMap::new();
3867 mapping.insert("data".to_string(), ref_value("steps.step2.outputs.result"));
3868 steps.insert(
3869 "step1".to_string(),
3870 create_agent_step("step1", "transform", Some(mapping)),
3871 );
3872
3873 steps.insert(
3874 "step2".to_string(),
3875 create_agent_step("step2", "transform", None),
3876 );
3877 steps.insert("finish".to_string(), create_finish_step("finish", None));
3878
3879 let mut graph = create_basic_graph(steps, "step1");
3880 graph.execution_plan = vec![
3882 runtara_dsl::ExecutionPlanEdge {
3883 from_step: "step1".to_string(),
3884 to_step: "step2".to_string(),
3885 label: None,
3886 },
3887 runtara_dsl::ExecutionPlanEdge {
3888 from_step: "step2".to_string(),
3889 to_step: "finish".to_string(),
3890 label: None,
3891 },
3892 ];
3893
3894 let result = validate_workflow(&graph);
3895 assert!(
3896 result.errors.iter().any(|e| {
3897 matches!(e, ValidationError::StepNotYetExecuted { step_id, referenced_step_id }
3898 if step_id == "step1" && referenced_step_id == "step2")
3899 }),
3900 "Expected StepNotYetExecuted error for forward reference"
3901 );
3902 }
3903
3904 #[test]
3905 fn test_valid_backward_reference() {
3906 let mut steps = HashMap::new();
3908
3909 steps.insert(
3910 "step1".to_string(),
3911 create_agent_step("step1", "transform", None),
3912 );
3913
3914 let mut mapping = HashMap::new();
3916 mapping.insert("data".to_string(), ref_value("steps.step1.outputs.result"));
3917 steps.insert(
3918 "step2".to_string(),
3919 create_agent_step("step2", "transform", Some(mapping)),
3920 );
3921 steps.insert("finish".to_string(), create_finish_step("finish", None));
3922
3923 let mut graph = create_basic_graph(steps, "step1");
3924 graph.execution_plan = vec![
3926 runtara_dsl::ExecutionPlanEdge {
3927 from_step: "step1".to_string(),
3928 to_step: "step2".to_string(),
3929 label: None,
3930 },
3931 runtara_dsl::ExecutionPlanEdge {
3932 from_step: "step2".to_string(),
3933 to_step: "finish".to_string(),
3934 label: None,
3935 },
3936 ];
3937
3938 let result = validate_workflow(&graph);
3939 assert!(
3941 !result
3942 .errors
3943 .iter()
3944 .any(|e| matches!(e, ValidationError::StepNotYetExecuted { .. })),
3945 "Expected no StepNotYetExecuted error for valid backward reference"
3946 );
3947 }
3948
3949 #[test]
3952 fn test_unknown_variable_error() {
3953 let mut steps = HashMap::new();
3954
3955 let mut mapping = HashMap::new();
3957 mapping.insert("data".to_string(), ref_value("variables.nonexistent"));
3958 steps.insert(
3959 "agent".to_string(),
3960 create_agent_step("agent", "transform", Some(mapping)),
3961 );
3962 steps.insert("finish".to_string(), create_finish_step("finish", None));
3963
3964 let mut graph = create_basic_graph(steps, "agent");
3965 graph.execution_plan = vec![runtara_dsl::ExecutionPlanEdge {
3966 from_step: "agent".to_string(),
3967 to_step: "finish".to_string(),
3968 label: None,
3969 }];
3970
3971 let result = validate_workflow(&graph);
3972 assert!(
3973 result.errors.iter().any(|e| {
3974 matches!(e, ValidationError::UnknownVariable { variable_name, .. }
3975 if variable_name == "nonexistent")
3976 }),
3977 "Expected UnknownVariable error"
3978 );
3979 }
3980
3981 #[test]
3982 fn test_valid_variable_reference() {
3983 use runtara_dsl::{Variable, VariableType};
3984
3985 let mut steps = HashMap::new();
3986
3987 let mut mapping = HashMap::new();
3989 mapping.insert("data".to_string(), ref_value("variables.myVar"));
3990 steps.insert(
3991 "agent".to_string(),
3992 create_agent_step("agent", "transform", Some(mapping)),
3993 );
3994 steps.insert("finish".to_string(), create_finish_step("finish", None));
3995
3996 let mut graph = create_basic_graph(steps, "agent");
3997 graph.execution_plan = vec![runtara_dsl::ExecutionPlanEdge {
3998 from_step: "agent".to_string(),
3999 to_step: "finish".to_string(),
4000 label: None,
4001 }];
4002 graph.variables.insert(
4004 "myVar".to_string(),
4005 Variable {
4006 var_type: VariableType::String,
4007 value: serde_json::json!("some value"),
4008 description: None,
4009 },
4010 );
4011
4012 let result = validate_workflow(&graph);
4013 assert!(
4015 !result
4016 .errors
4017 .iter()
4018 .any(|e| matches!(e, ValidationError::UnknownVariable { .. })),
4019 "Expected no UnknownVariable error for valid variable reference"
4020 );
4021 }
4022
4023 #[test]
4024 fn test_variable_nested_path_valid() {
4025 use runtara_dsl::{Variable, VariableType};
4026
4027 let mut steps = HashMap::new();
4029
4030 let mut mapping = HashMap::new();
4031 mapping.insert(
4032 "data".to_string(),
4033 ref_value("variables.config.database.host"),
4034 );
4035 steps.insert(
4036 "agent".to_string(),
4037 create_agent_step("agent", "transform", Some(mapping)),
4038 );
4039 steps.insert("finish".to_string(), create_finish_step("finish", None));
4040
4041 let mut graph = create_basic_graph(steps, "agent");
4042 graph.execution_plan = vec![runtara_dsl::ExecutionPlanEdge {
4043 from_step: "agent".to_string(),
4044 to_step: "finish".to_string(),
4045 label: None,
4046 }];
4047 graph.variables.insert(
4049 "config".to_string(),
4050 Variable {
4051 var_type: VariableType::Object,
4052 value: serde_json::json!({"database": {"host": "localhost"}}),
4053 description: None,
4054 },
4055 );
4056
4057 let result = validate_workflow(&graph);
4058 assert!(
4060 !result
4061 .errors
4062 .iter()
4063 .any(|e| matches!(e, ValidationError::UnknownVariable { .. })),
4064 "Expected no UnknownVariable error for nested variable path"
4065 );
4066 }
4067
4068 #[test]
4071 fn test_duplicate_step_names_error() {
4072 let mut steps = HashMap::new();
4073
4074 steps.insert(
4076 "step1".to_string(),
4077 Step::Agent(AgentStep {
4078 id: "step1".to_string(),
4079 name: Some("Fetch Data".to_string()),
4080 agent_id: "transform".to_string(),
4081 capability_id: "map".to_string(),
4082 connection_id: None,
4083 input_mapping: None,
4084 max_retries: None,
4085 retry_delay: None,
4086 timeout: None,
4087 }),
4088 );
4089 steps.insert(
4090 "step2".to_string(),
4091 Step::Agent(AgentStep {
4092 id: "step2".to_string(),
4093 name: Some("Fetch Data".to_string()), agent_id: "transform".to_string(),
4095 capability_id: "map".to_string(),
4096 connection_id: None,
4097 input_mapping: None,
4098 max_retries: None,
4099 retry_delay: None,
4100 timeout: None,
4101 }),
4102 );
4103 steps.insert("finish".to_string(), create_finish_step("finish", None));
4104
4105 let mut graph = create_basic_graph(steps, "step1");
4106 graph.execution_plan = vec![
4107 runtara_dsl::ExecutionPlanEdge {
4108 from_step: "step1".to_string(),
4109 to_step: "step2".to_string(),
4110 label: None,
4111 },
4112 runtara_dsl::ExecutionPlanEdge {
4113 from_step: "step2".to_string(),
4114 to_step: "finish".to_string(),
4115 label: None,
4116 },
4117 ];
4118
4119 let result = validate_workflow(&graph);
4120 assert!(
4121 result.errors.iter().any(|e| {
4122 matches!(e, ValidationError::DuplicateStepName { name, step_ids }
4123 if name == "Fetch Data" && step_ids.len() == 2)
4124 }),
4125 "Expected DuplicateStepName error"
4126 );
4127 }
4128
4129 #[test]
4130 fn test_duplicate_names_in_subgraph() {
4131 use runtara_dsl::SplitStep;
4132
4133 let mut steps = HashMap::new();
4134
4135 steps.insert(
4137 "main_step".to_string(),
4138 Step::Agent(AgentStep {
4139 id: "main_step".to_string(),
4140 name: Some("Process Item".to_string()),
4141 agent_id: "transform".to_string(),
4142 capability_id: "map".to_string(),
4143 connection_id: None,
4144 input_mapping: None,
4145 max_retries: None,
4146 retry_delay: None,
4147 timeout: None,
4148 }),
4149 );
4150
4151 let mut subgraph_steps = HashMap::new();
4153 subgraph_steps.insert(
4154 "sub_step".to_string(),
4155 Step::Agent(AgentStep {
4156 id: "sub_step".to_string(),
4157 name: Some("Process Item".to_string()), agent_id: "transform".to_string(),
4159 capability_id: "map".to_string(),
4160 connection_id: None,
4161 input_mapping: None,
4162 max_retries: None,
4163 retry_delay: None,
4164 timeout: None,
4165 }),
4166 );
4167 subgraph_steps.insert(
4168 "sub_finish".to_string(),
4169 create_finish_step("sub_finish", None),
4170 );
4171
4172 let subgraph = ExecutionGraph {
4173 name: None,
4174 description: None,
4175 steps: subgraph_steps,
4176 entry_point: "sub_step".to_string(),
4177 execution_plan: vec![runtara_dsl::ExecutionPlanEdge {
4178 from_step: "sub_step".to_string(),
4179 to_step: "sub_finish".to_string(),
4180 label: None,
4181 }],
4182 variables: HashMap::new(),
4183 input_schema: HashMap::new(),
4184 output_schema: HashMap::new(),
4185 notes: None,
4186 nodes: None,
4187 edges: None,
4188 };
4189
4190 steps.insert(
4192 "split".to_string(),
4193 Step::Split(SplitStep {
4194 id: "split".to_string(),
4195 name: None,
4196 subgraph: Box::new(subgraph),
4197 config: None,
4198 input_schema: HashMap::new(),
4199 output_schema: HashMap::new(),
4200 }),
4201 );
4202
4203 steps.insert("finish".to_string(), create_finish_step("finish", None));
4204
4205 let mut graph = create_basic_graph(steps, "main_step");
4206 graph.execution_plan = vec![
4207 runtara_dsl::ExecutionPlanEdge {
4208 from_step: "main_step".to_string(),
4209 to_step: "split".to_string(),
4210 label: None,
4211 },
4212 runtara_dsl::ExecutionPlanEdge {
4213 from_step: "split".to_string(),
4214 to_step: "finish".to_string(),
4215 label: None,
4216 },
4217 ];
4218
4219 let result = validate_workflow(&graph);
4220 assert!(
4221 result.errors.iter().any(|e| {
4222 matches!(e, ValidationError::DuplicateStepName { name, step_ids }
4223 if name == "Process Item" && step_ids.len() == 2)
4224 }),
4225 "Expected DuplicateStepName error across main graph and subgraph"
4226 );
4227 }
4228
4229 #[test]
4230 fn test_unique_step_names_no_error() {
4231 let mut steps = HashMap::new();
4232
4233 steps.insert(
4234 "step1".to_string(),
4235 Step::Agent(AgentStep {
4236 id: "step1".to_string(),
4237 name: Some("First Step".to_string()),
4238 agent_id: "transform".to_string(),
4239 capability_id: "map".to_string(),
4240 connection_id: None,
4241 input_mapping: None,
4242 max_retries: None,
4243 retry_delay: None,
4244 timeout: None,
4245 }),
4246 );
4247 steps.insert(
4248 "step2".to_string(),
4249 Step::Agent(AgentStep {
4250 id: "step2".to_string(),
4251 name: Some("Second Step".to_string()), agent_id: "transform".to_string(),
4253 capability_id: "map".to_string(),
4254 connection_id: None,
4255 input_mapping: None,
4256 max_retries: None,
4257 retry_delay: None,
4258 timeout: None,
4259 }),
4260 );
4261 steps.insert("finish".to_string(), create_finish_step("finish", None));
4262
4263 let mut graph = create_basic_graph(steps, "step1");
4264 graph.execution_plan = vec![
4265 runtara_dsl::ExecutionPlanEdge {
4266 from_step: "step1".to_string(),
4267 to_step: "step2".to_string(),
4268 label: None,
4269 },
4270 runtara_dsl::ExecutionPlanEdge {
4271 from_step: "step2".to_string(),
4272 to_step: "finish".to_string(),
4273 label: None,
4274 },
4275 ];
4276
4277 let result = validate_workflow(&graph);
4278 assert!(
4279 !result
4280 .errors
4281 .iter()
4282 .any(|e| matches!(e, ValidationError::DuplicateStepName { .. })),
4283 "Expected no DuplicateStepName error for unique names"
4284 );
4285 }
4286
4287 #[test]
4290 fn test_error_display_step_not_yet_executed() {
4291 let error = ValidationError::StepNotYetExecuted {
4292 step_id: "step1".to_string(),
4293 referenced_step_id: "step2".to_string(),
4294 };
4295 let display = format!("{}", error);
4296 assert!(display.contains("[E012]"));
4297 assert!(display.contains("step1"));
4298 assert!(display.contains("step2"));
4299 assert!(display.contains("has not executed yet"));
4300 }
4301
4302 #[test]
4303 fn test_error_display_unknown_variable() {
4304 let error = ValidationError::UnknownVariable {
4305 step_id: "step1".to_string(),
4306 variable_name: "missing".to_string(),
4307 available_variables: vec!["foo".to_string(), "bar".to_string()],
4308 };
4309 let display = format!("{}", error);
4310 assert!(display.contains("[E013]"));
4311 assert!(display.contains("step1"));
4312 assert!(display.contains("missing"));
4313 assert!(display.contains("foo, bar"));
4314 }
4315
4316 #[test]
4317 fn test_error_display_type_mismatch() {
4318 let error = ValidationError::TypeMismatch {
4319 step_id: "step1".to_string(),
4320 field_name: "count".to_string(),
4321 expected_type: "integer".to_string(),
4322 actual_type: "string".to_string(),
4323 };
4324 let display = format!("{}", error);
4325 assert!(display.contains("[E023]"));
4326 assert!(display.contains("step1"));
4327 assert!(display.contains("count"));
4328 assert!(display.contains("integer"));
4329 assert!(display.contains("string"));
4330 }
4331
4332 #[test]
4333 fn test_error_display_invalid_enum_value() {
4334 let error = ValidationError::InvalidEnumValue {
4335 step_id: "step1".to_string(),
4336 field_name: "method".to_string(),
4337 value: "INVALID".to_string(),
4338 allowed_values: vec!["GET".to_string(), "POST".to_string()],
4339 };
4340 let display = format!("{}", error);
4341 assert!(display.contains("[E024]"));
4342 assert!(display.contains("step1"));
4343 assert!(display.contains("method"));
4344 assert!(display.contains("INVALID"));
4345 assert!(display.contains("GET, POST"));
4346 }
4347
4348 #[test]
4349 fn test_error_display_duplicate_step_name() {
4350 let error = ValidationError::DuplicateStepName {
4351 name: "Fetch Data".to_string(),
4352 step_ids: vec!["step1".to_string(), "step2".to_string()],
4353 };
4354 let display = format!("{}", error);
4355 assert!(display.contains("[E060]"));
4356 assert!(display.contains("Fetch Data"));
4357 assert!(display.contains("step1"));
4358 assert!(display.contains("step2"));
4359 }
4360
4361 #[test]
4364 fn test_extract_variable_name_simple() {
4365 assert_eq!(
4366 extract_variable_name_from_reference("variables.myVar"),
4367 Some("myVar".to_string())
4368 );
4369 }
4370
4371 #[test]
4372 fn test_extract_variable_name_nested() {
4373 assert_eq!(
4374 extract_variable_name_from_reference("variables.config.database"),
4375 Some("config".to_string())
4376 );
4377 }
4378
4379 #[test]
4380 fn test_extract_variable_name_bracket_notation() {
4381 assert_eq!(
4385 extract_variable_name_from_reference("variables['my-var']"),
4386 None );
4388 assert_eq!(
4389 extract_variable_name_from_reference("variables[\"my-var\"]"),
4390 None );
4392 }
4393
4394 #[test]
4395 fn test_extract_variable_name_not_variable() {
4396 assert_eq!(
4397 extract_variable_name_from_reference("steps.step1.outputs"),
4398 None
4399 );
4400 assert_eq!(extract_variable_name_from_reference("data.value"), None);
4401 }
4402
4403 #[test]
4404 fn test_get_json_type_name() {
4405 assert_eq!(get_json_type_name(&serde_json::json!("hello")), "string");
4406 assert_eq!(get_json_type_name(&serde_json::json!(42)), "integer");
4408 assert_eq!(get_json_type_name(&serde_json::json!(42.5)), "number");
4410 assert_eq!(get_json_type_name(&serde_json::json!(true)), "boolean");
4411 assert_eq!(get_json_type_name(&serde_json::json!([1, 2, 3])), "array");
4412 assert_eq!(get_json_type_name(&serde_json::json!({"a": 1})), "object");
4413 assert_eq!(get_json_type_name(&serde_json::json!(null)), "null");
4414 }
4415
4416 #[test]
4417 fn test_check_type_compatibility_string() {
4418 assert!(
4420 check_type_compatibility("step", "field", "string", &serde_json::json!("hello"))
4421 .is_none()
4422 );
4423 assert!(
4425 check_type_compatibility("step", "field", "string", &serde_json::json!(42)).is_some()
4426 );
4427 }
4428
4429 #[test]
4430 fn test_check_type_compatibility_integer() {
4431 assert!(
4432 check_type_compatibility("step", "field", "integer", &serde_json::json!(42)).is_none()
4433 );
4434 assert!(
4435 check_type_compatibility("step", "field", "integer", &serde_json::json!(-10)).is_none()
4436 );
4437 assert!(
4438 check_type_compatibility("step", "field", "integer", &serde_json::json!(42.5))
4439 .is_some()
4440 );
4441 assert!(
4442 check_type_compatibility("step", "field", "integer", &serde_json::json!("42"))
4443 .is_some()
4444 );
4445 }
4446
4447 #[test]
4448 fn test_check_type_compatibility_number() {
4449 assert!(
4450 check_type_compatibility("step", "field", "number", &serde_json::json!(42)).is_none()
4451 );
4452 assert!(
4453 check_type_compatibility("step", "field", "number", &serde_json::json!(42.5)).is_none()
4454 );
4455 assert!(
4456 check_type_compatibility("step", "field", "number", &serde_json::json!("42")).is_some()
4457 );
4458 }
4459
4460 #[test]
4461 fn test_check_type_compatibility_boolean() {
4462 assert!(
4463 check_type_compatibility("step", "field", "boolean", &serde_json::json!(true))
4464 .is_none()
4465 );
4466 assert!(
4467 check_type_compatibility("step", "field", "boolean", &serde_json::json!(false))
4468 .is_none()
4469 );
4470 assert!(
4471 check_type_compatibility("step", "field", "boolean", &serde_json::json!("true"))
4472 .is_some()
4473 );
4474 }
4475
4476 #[test]
4477 fn test_check_type_compatibility_array() {
4478 assert!(
4479 check_type_compatibility("step", "field", "array", &serde_json::json!([1, 2, 3]))
4480 .is_none()
4481 );
4482 assert!(
4483 check_type_compatibility("step", "field", "array", &serde_json::json!([])).is_none()
4484 );
4485 assert!(
4486 check_type_compatibility("step", "field", "array", &serde_json::json!({"a": 1}))
4487 .is_some()
4488 );
4489 }
4490
4491 #[test]
4492 fn test_check_type_compatibility_object() {
4493 assert!(
4494 check_type_compatibility("step", "field", "object", &serde_json::json!({"a": 1}))
4495 .is_none()
4496 );
4497 assert!(
4498 check_type_compatibility("step", "field", "object", &serde_json::json!({})).is_none()
4499 );
4500 assert!(
4501 check_type_compatibility("step", "field", "object", &serde_json::json!([1, 2]))
4502 .is_some()
4503 );
4504 }
4505
4506 #[test]
4507 fn test_check_type_compatibility_unknown_type_passes() {
4508 assert!(
4510 check_type_compatibility(
4511 "step",
4512 "field",
4513 "Vec<String>",
4514 &serde_json::json!("anything")
4515 )
4516 .is_none()
4517 );
4518 assert!(
4519 check_type_compatibility("step", "field", "CustomType", &serde_json::json!(42))
4520 .is_none()
4521 );
4522 }
4523
4524 #[test]
4527 fn test_split_config_variables_available_in_subgraph() {
4528 use runtara_dsl::{ImmediateValue, SplitConfig, SplitStep};
4529
4530 let mut subgraph_steps = HashMap::new();
4532 let mut mapping = HashMap::new();
4533 mapping.insert("userId".to_string(), ref_value("variables.parentUserId")); subgraph_steps.insert(
4535 "sub_agent".to_string(),
4536 create_agent_step("sub_agent", "transform", Some(mapping)),
4537 );
4538 subgraph_steps.insert(
4539 "sub_finish".to_string(),
4540 create_finish_step("sub_finish", None),
4541 );
4542
4543 let subgraph = ExecutionGraph {
4544 name: None,
4545 description: None,
4546 steps: subgraph_steps,
4547 entry_point: "sub_agent".to_string(),
4548 execution_plan: vec![runtara_dsl::ExecutionPlanEdge {
4549 from_step: "sub_agent".to_string(),
4550 to_step: "sub_finish".to_string(),
4551 label: None,
4552 }],
4553 variables: HashMap::new(), input_schema: HashMap::new(),
4555 output_schema: HashMap::new(),
4556 notes: None,
4557 nodes: None,
4558 edges: None,
4559 };
4560
4561 let mut config_variables = HashMap::new();
4563 config_variables.insert(
4564 "parentUserId".to_string(),
4565 MappingValue::Immediate(ImmediateValue {
4566 value: serde_json::json!("user-123"),
4567 }),
4568 );
4569
4570 let config = SplitConfig {
4571 value: MappingValue::Immediate(ImmediateValue {
4572 value: serde_json::json!([1, 2, 3]),
4573 }),
4574 parallelism: None,
4575 sequential: None,
4576 dont_stop_on_failed: None,
4577 variables: Some(config_variables),
4578 max_retries: None,
4579 retry_delay: None,
4580 timeout: None,
4581 };
4582
4583 let mut steps = HashMap::new();
4584 steps.insert(
4585 "split".to_string(),
4586 Step::Split(SplitStep {
4587 id: "split".to_string(),
4588 name: None,
4589 subgraph: Box::new(subgraph),
4590 config: Some(config),
4591 input_schema: HashMap::new(),
4592 output_schema: HashMap::new(),
4593 }),
4594 );
4595 steps.insert("finish".to_string(), create_finish_step("finish", None));
4596
4597 let mut graph = create_basic_graph(steps, "split");
4598 graph.execution_plan = vec![runtara_dsl::ExecutionPlanEdge {
4599 from_step: "split".to_string(),
4600 to_step: "finish".to_string(),
4601 label: None,
4602 }];
4603
4604 let result = validate_workflow(&graph);
4605
4606 let unknown_var_errors: Vec<_> = result
4608 .errors
4609 .iter()
4610 .filter(|e| matches!(e, ValidationError::UnknownVariable { .. }))
4611 .collect();
4612 assert!(
4613 unknown_var_errors.is_empty(),
4614 "config.variables should be available in subgraph; got errors: {:?}",
4615 unknown_var_errors
4616 );
4617 }
4618
4619 #[test]
4620 fn test_split_subgraph_unknown_variable_still_caught() {
4621 use runtara_dsl::{ImmediateValue, SplitConfig, SplitStep};
4622
4623 let mut subgraph_steps = HashMap::new();
4625 let mut mapping = HashMap::new();
4626 mapping.insert("data".to_string(), ref_value("variables.undeclaredVar")); subgraph_steps.insert(
4628 "sub_agent".to_string(),
4629 create_agent_step("sub_agent", "transform", Some(mapping)),
4630 );
4631 subgraph_steps.insert(
4632 "sub_finish".to_string(),
4633 create_finish_step("sub_finish", None),
4634 );
4635
4636 let subgraph = ExecutionGraph {
4637 name: None,
4638 description: None,
4639 steps: subgraph_steps,
4640 entry_point: "sub_agent".to_string(),
4641 execution_plan: vec![runtara_dsl::ExecutionPlanEdge {
4642 from_step: "sub_agent".to_string(),
4643 to_step: "sub_finish".to_string(),
4644 label: None,
4645 }],
4646 variables: HashMap::new(),
4647 input_schema: HashMap::new(),
4648 output_schema: HashMap::new(),
4649 notes: None,
4650 nodes: None,
4651 edges: None,
4652 };
4653
4654 let mut config_variables = HashMap::new();
4656 config_variables.insert(
4657 "someOtherVar".to_string(),
4658 MappingValue::Immediate(ImmediateValue {
4659 value: serde_json::json!("value"),
4660 }),
4661 );
4662
4663 let config = SplitConfig {
4664 value: MappingValue::Immediate(ImmediateValue {
4665 value: serde_json::json!([1]),
4666 }),
4667 parallelism: None,
4668 sequential: None,
4669 dont_stop_on_failed: None,
4670 variables: Some(config_variables),
4671 max_retries: None,
4672 retry_delay: None,
4673 timeout: None,
4674 };
4675
4676 let mut steps = HashMap::new();
4677 steps.insert(
4678 "split".to_string(),
4679 Step::Split(SplitStep {
4680 id: "split".to_string(),
4681 name: None,
4682 subgraph: Box::new(subgraph),
4683 config: Some(config),
4684 input_schema: HashMap::new(),
4685 output_schema: HashMap::new(),
4686 }),
4687 );
4688 steps.insert("finish".to_string(), create_finish_step("finish", None));
4689
4690 let mut graph = create_basic_graph(steps, "split");
4691 graph.execution_plan = vec![runtara_dsl::ExecutionPlanEdge {
4692 from_step: "split".to_string(),
4693 to_step: "finish".to_string(),
4694 label: None,
4695 }];
4696
4697 let result = validate_workflow(&graph);
4698
4699 assert!(
4701 result.errors.iter().any(|e| {
4702 matches!(e, ValidationError::UnknownVariable { variable_name, .. } if variable_name == "undeclaredVar")
4703 }),
4704 "Expected UnknownVariable error for 'undeclaredVar', got: {:?}",
4705 result.errors
4706 );
4707 }
4708
4709 #[test]
4710 fn test_split_both_config_and_subgraph_variables_available() {
4711 use runtara_dsl::{ImmediateValue, SplitConfig, SplitStep, Variable, VariableType};
4712
4713 let mut subgraph_steps = HashMap::new();
4715 let mut mapping = HashMap::new();
4716 mapping.insert("fromConfig".to_string(), ref_value("variables.configVar"));
4717 mapping.insert(
4718 "fromSubgraph".to_string(),
4719 ref_value("variables.subgraphVar"),
4720 );
4721 subgraph_steps.insert(
4722 "sub_agent".to_string(),
4723 create_agent_step("sub_agent", "transform", Some(mapping)),
4724 );
4725 subgraph_steps.insert(
4726 "sub_finish".to_string(),
4727 create_finish_step("sub_finish", None),
4728 );
4729
4730 let mut subgraph_variables = HashMap::new();
4732 subgraph_variables.insert(
4733 "subgraphVar".to_string(),
4734 Variable {
4735 var_type: VariableType::String,
4736 value: serde_json::json!("subgraph-value"),
4737 description: None,
4738 },
4739 );
4740
4741 let subgraph = ExecutionGraph {
4742 name: None,
4743 description: None,
4744 steps: subgraph_steps,
4745 entry_point: "sub_agent".to_string(),
4746 execution_plan: vec![runtara_dsl::ExecutionPlanEdge {
4747 from_step: "sub_agent".to_string(),
4748 to_step: "sub_finish".to_string(),
4749 label: None,
4750 }],
4751 variables: subgraph_variables,
4752 input_schema: HashMap::new(),
4753 output_schema: HashMap::new(),
4754 notes: None,
4755 nodes: None,
4756 edges: None,
4757 };
4758
4759 let mut config_variables = HashMap::new();
4761 config_variables.insert(
4762 "configVar".to_string(),
4763 MappingValue::Immediate(ImmediateValue {
4764 value: serde_json::json!("config-value"),
4765 }),
4766 );
4767
4768 let config = SplitConfig {
4769 value: MappingValue::Immediate(ImmediateValue {
4770 value: serde_json::json!([1]),
4771 }),
4772 parallelism: None,
4773 sequential: None,
4774 dont_stop_on_failed: None,
4775 variables: Some(config_variables),
4776 max_retries: None,
4777 retry_delay: None,
4778 timeout: None,
4779 };
4780
4781 let mut steps = HashMap::new();
4782 steps.insert(
4783 "split".to_string(),
4784 Step::Split(SplitStep {
4785 id: "split".to_string(),
4786 name: None,
4787 subgraph: Box::new(subgraph),
4788 config: Some(config),
4789 input_schema: HashMap::new(),
4790 output_schema: HashMap::new(),
4791 }),
4792 );
4793 steps.insert("finish".to_string(), create_finish_step("finish", None));
4794
4795 let mut graph = create_basic_graph(steps, "split");
4796 graph.execution_plan = vec![runtara_dsl::ExecutionPlanEdge {
4797 from_step: "split".to_string(),
4798 to_step: "finish".to_string(),
4799 label: None,
4800 }];
4801
4802 let result = validate_workflow(&graph);
4803
4804 let unknown_var_errors: Vec<_> = result
4806 .errors
4807 .iter()
4808 .filter(|e| matches!(e, ValidationError::UnknownVariable { .. }))
4809 .collect();
4810 assert!(
4811 unknown_var_errors.is_empty(),
4812 "Both config.variables and subgraph.variables should be available; got errors: {:?}",
4813 unknown_var_errors
4814 );
4815 }
4816}