runtara_workflows/
validation.rs

1// Copyright (C) 2025 SyncMyOrders Sp. z o.o.
2// SPDX-License-Identifier: AGPL-3.0-or-later
3//! Workflow validation for security and correctness.
4//!
5//! This module validates workflows before compilation to ensure:
6//! - Graph structure is valid (entry point exists, no unreachable steps)
7//! - References point to valid steps
8//! - Agents and capabilities exist
9//! - Connection data doesn't leak to non-secure agents
10//! - Configuration values are reasonable
11
12use runtara_dsl::{CompositeInner, ExecutionGraph, InputMapping, MappingValue, Step};
13use std::collections::{HashMap, HashSet};
14
15// ============================================================================
16// Validation Result Types
17// ============================================================================
18
19/// Result of workflow validation containing errors and warnings.
20#[derive(Debug, Clone, Default)]
21pub struct ValidationResult {
22    /// Hard errors that prevent compilation.
23    pub errors: Vec<ValidationError>,
24    /// Soft warnings that don't prevent compilation but indicate potential issues.
25    pub warnings: Vec<ValidationWarning>,
26}
27
28impl ValidationResult {
29    /// Returns true if there are no errors (warnings are allowed).
30    pub fn is_ok(&self) -> bool {
31        self.errors.is_empty()
32    }
33
34    /// Returns true if there are any errors.
35    pub fn has_errors(&self) -> bool {
36        !self.errors.is_empty()
37    }
38
39    /// Returns true if there are any warnings.
40    pub fn has_warnings(&self) -> bool {
41        !self.warnings.is_empty()
42    }
43
44    /// Merge another validation result into this one.
45    pub fn merge(&mut self, other: ValidationResult) {
46        self.errors.extend(other.errors);
47        self.warnings.extend(other.warnings);
48    }
49}
50
51// ============================================================================
52// Validation Errors
53// ============================================================================
54
55/// Errors that can occur during validation.
56#[derive(Debug, Clone)]
57#[allow(missing_docs)] // Fields are self-documenting from variant docs
58pub enum ValidationError {
59    // === Graph Structure Errors ===
60    /// Entry point step does not exist in the workflow.
61    EntryPointNotFound {
62        entry_point: String,
63        available_steps: Vec<String>,
64    },
65    /// A step is not reachable from the entry point.
66    UnreachableStep { step_id: String },
67    /// Workflow has no steps defined.
68    EmptyWorkflow,
69
70    // === Reference Errors ===
71    /// A step reference points to a non-existent step.
72    InvalidStepReference {
73        step_id: String,
74        reference_path: String,
75        referenced_step_id: String,
76        available_steps: Vec<String>,
77    },
78    /// A reference path has invalid syntax.
79    InvalidReferencePath {
80        step_id: String,
81        reference_path: String,
82        reason: String,
83    },
84
85    // === Agent/Capability Errors ===
86    /// Agent does not exist.
87    UnknownAgent {
88        step_id: String,
89        agent_id: String,
90        available_agents: Vec<String>,
91    },
92    /// Capability does not exist for the agent.
93    UnknownCapability {
94        step_id: String,
95        agent_id: String,
96        capability_id: String,
97        available_capabilities: Vec<String>,
98    },
99    /// Required capability input is missing.
100    MissingRequiredInput {
101        step_id: String,
102        agent_id: String,
103        capability_id: String,
104        input_name: String,
105    },
106
107    // === Connection Errors ===
108    /// Unknown integration ID for connection step.
109    UnknownIntegration {
110        step_id: String,
111        integration_id: String,
112        available_integrations: Vec<String>,
113    },
114
115    // === Security Errors ===
116    /// Connection data is referenced by a non-secure agent.
117    ConnectionLeakToNonSecureAgent {
118        connection_step_id: String,
119        agent_step_id: String,
120        agent_id: String,
121    },
122    /// Connection data is referenced by a Finish step.
123    ConnectionLeakToFinish {
124        connection_step_id: String,
125        finish_step_id: String,
126    },
127    /// Connection data is referenced by a Log step.
128    ConnectionLeakToLog {
129        connection_step_id: String,
130        log_step_id: String,
131    },
132
133    // === Child Scenario Errors ===
134    /// Invalid child scenario version format.
135    InvalidChildVersion {
136        step_id: String,
137        child_scenario_id: String,
138        version: String,
139        reason: String,
140    },
141
142    // === Execution Order Errors ===
143    /// A step references another step that hasn't executed yet.
144    StepNotYetExecuted {
145        step_id: String,
146        referenced_step_id: String,
147    },
148
149    // === Variable Errors ===
150    /// A variable reference points to a non-existent variable.
151    UnknownVariable {
152        step_id: String,
153        variable_name: String,
154        available_variables: Vec<String>,
155    },
156
157    // === Type Errors ===
158    /// An immediate value has the wrong type for the expected field.
159    TypeMismatch {
160        step_id: String,
161        field_name: String,
162        expected_type: String,
163        actual_type: String,
164    },
165    /// An enum value is not in the allowed set.
166    InvalidEnumValue {
167        step_id: String,
168        field_name: String,
169        value: String,
170        allowed_values: Vec<String>,
171    },
172
173    // === Naming Errors ===
174    /// Multiple steps have the same name.
175    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            // Graph Structure Errors
182            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            // Reference Errors
209            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            // Agent/Capability Errors
238            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            // Connection Errors
294            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            // Security Errors
314            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            // Child Scenario Errors
350            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            // Execution Order Errors
364            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            // Variable Errors
377            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            // Type Errors
401            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            // Naming Errors
430            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// ============================================================================
445// Validation Warnings
446// ============================================================================
447
448/// Warnings that indicate potential issues but don't prevent compilation.
449#[derive(Debug, Clone)]
450#[allow(missing_docs)] // Fields are self-documenting from variant docs
451pub enum ValidationWarning {
452    /// Unknown input field in agent step.
453    UnknownInputField {
454        step_id: String,
455        agent_id: String,
456        capability_id: String,
457        field_name: String,
458        available_fields: Vec<String>,
459    },
460    /// High retry count may cause long execution times.
461    HighRetryCount {
462        step_id: String,
463        max_retries: u32,
464        recommended_max: u32,
465    },
466    /// Long retry delay may cause long execution times.
467    LongRetryDelay {
468        step_id: String,
469        retry_delay_ms: u64,
470        recommended_max_ms: u64,
471    },
472    /// High parallelism may cause resource issues.
473    HighParallelism {
474        step_id: String,
475        parallelism: u32,
476        recommended_max: u32,
477    },
478    /// High max iterations may indicate infinite loop risk.
479    HighMaxIterations {
480        step_id: String,
481        max_iterations: u32,
482        recommended_max: u32,
483    },
484    /// Long timeout configured.
485    LongTimeout {
486        step_id: String,
487        timeout_ms: u64,
488        recommended_max_ms: u64,
489    },
490    /// Connection step is defined but never referenced.
491    UnusedConnection { step_id: String },
492    /// Step references its own outputs (potential issue except in loops).
493    SelfReference {
494        step_id: String,
495        reference_path: String,
496    },
497    /// A non-Finish step has no outgoing edges (terminal step without explicit Finish).
498    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
619// ============================================================================
620// Main Validation Function
621// ============================================================================
622
623/// Validate a workflow for security and correctness.
624///
625/// Returns a `ValidationResult` containing errors and warnings.
626/// Compilation should fail if there are any errors.
627pub fn validate_workflow(graph: &ExecutionGraph) -> ValidationResult {
628    let mut result = ValidationResult::default();
629
630    // Phase 1: Graph structure validation
631    validate_graph_structure(graph, &mut result);
632
633    // Phase 2: Reference validation
634    validate_references(graph, &mut result);
635
636    // Phase 2.5: Execution order validation
637    validate_execution_order(graph, &mut result);
638
639    // Phase 3: Agent/capability validation
640    validate_agents(graph, &mut result);
641
642    // Phase 4: Configuration warnings
643    validate_configuration(graph, &mut result);
644
645    // Phase 5: Connection validation
646    validate_connections(graph, &mut result);
647
648    // Phase 6: Security validation (connection leakage)
649    validate_security(graph, &mut result);
650
651    // Phase 7: Child scenario validation
652    validate_child_scenarios(graph, &mut result);
653
654    // Phase 8: Step name validation
655    validate_step_names(graph, &mut result);
656
657    result
658}
659
660/// Legacy function for backward compatibility.
661/// Returns only errors (no warnings) as a Vec.
662pub fn validate_workflow_errors(graph: &ExecutionGraph) -> Vec<ValidationError> {
663    validate_workflow(graph).errors
664}
665
666// ============================================================================
667// Phase 1: Graph Structure Validation
668// ============================================================================
669
670fn validate_graph_structure(graph: &ExecutionGraph, result: &mut ValidationResult) {
671    // Check for empty workflow
672    if graph.steps.is_empty() {
673        result.errors.push(ValidationError::EmptyWorkflow);
674        return;
675    }
676
677    // Check entry point exists
678    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    // Build reachability set from entry point
688    let reachable = compute_reachable_steps(graph);
689
690    // Check for unreachable steps
691    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    // Check for dangling steps (non-Finish steps with no outgoing edges)
700    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        // Finish steps are allowed to have no outgoing edges
708        if matches!(step, Step::Finish(_)) {
709            continue;
710        }
711
712        // Check if this step has any outgoing edges
713        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    // Recursively validate subgraph structure (entry point, reachability, etc.)
722    // Note: Each individual validation phase (validate_references, validate_agents, etc.)
723    // handles its own subgraph recursion. This allows parent context (like config.variables
724    // from Split steps) to be properly passed to subgraphs during reference validation.
725    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
738/// Compute the set of steps reachable from the entry point.
739fn compute_reachable_steps(graph: &ExecutionGraph) -> HashSet<String> {
740    let mut reachable = HashSet::new();
741    let mut queue = vec![graph.entry_point.clone()];
742
743    // Build adjacency list from execution plan
744    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
770// ============================================================================
771// Phase 2: Reference Validation
772// ============================================================================
773
774fn validate_references(graph: &ExecutionGraph, result: &mut ValidationResult) {
775    validate_references_with_inherited(graph, &HashSet::new(), result);
776}
777
778/// Validates references in a graph, considering inherited variables from parent scope.
779///
780/// `inherited_variables` contains variable names that are injected from a parent scope
781/// (e.g., from `config.variables` in a Split step). These are valid in addition to
782/// the graph's own declared variables.
783fn 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    // Merge inherited variables with graph's own variables
791    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    // Recursively validate subgraphs
811    for step in graph.steps.values() {
812        match step {
813            Step::Split(split_step) => {
814                // config.variables keys become available as variables.<name> in the subgraph
815                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
831/// Recursively validate references in a MappingValue, including nested Composites.
832fn 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            // Immediate values have no references to validate
851        }
852        MappingValue::Composite(comp_value) => {
853            // Recursively validate all nested MappingValues
854            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    // Check for empty path segments
890    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    // Check for step references
900    if let Some(referenced_step_id) = extract_step_id_from_reference(ref_path) {
901        // Check if step references itself (warning, not error)
902        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        // Check if referenced step exists
910        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    // Check for variable references
921    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
932/// Collect all input mappings from a step.
933fn 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
970// ============================================================================
971// Phase 2.5: Execution Order Validation
972// ============================================================================
973
974/// Validate that step references only refer to steps that have already executed.
975fn validate_execution_order(graph: &ExecutionGraph, result: &mut ValidationResult) {
976    // Build execution order from entry_point and execution_plan
977    let order = compute_execution_order(graph);
978
979    // If order is empty (shouldn't happen if graph validation passed), skip
980    if order.is_empty() {
981        return;
982    }
983
984    // Create position map: step_id -> position in execution order
985    let position_map: HashMap<String, usize> = order
986        .iter()
987        .enumerate()
988        .map(|(i, s)| (s.clone(), i))
989        .collect();
990
991    // Check each step's references
992    for (step_id, step) in &graph.steps {
993        let current_position = match position_map.get(step_id) {
994            Some(pos) => *pos,
995            None => continue, // Step not in order (unreachable, already caught)
996        };
997
998        let mappings = collect_step_mappings(step);
999
1000        for mapping in mappings {
1001            for (_, value) in mapping {
1002                // Extract all step references from this mapping value (including nested composites)
1003                let referenced_step_ids = extract_step_ids_from_mapping_value(value);
1004                for referenced_step_id in referenced_step_ids {
1005                    // Skip self-references - they're handled separately as warnings
1006                    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                    // If referenced step not in position_map, it doesn't exist
1019                    // (already caught by reference validation)
1020                }
1021            }
1022        }
1023    }
1024
1025    // Recursively validate subgraphs
1026    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
1039/// Compute execution order from entry_point following execution_plan edges.
1040/// Returns steps in the order they would execute.
1041fn 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    // Build adjacency list from execution plan
1047    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    // BFS from entry point to establish order
1056    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
1077// ============================================================================
1078// Phase 3: Agent/Capability Validation
1079// ============================================================================
1080
1081fn validate_agents(graph: &ExecutionGraph, result: &mut ValidationResult) {
1082    // Get available agents
1083    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            // Validate agent exists
1091            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            // Validate capability exists
1103            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                // Get available capabilities for this agent
1110                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            // Validate required inputs are provided
1130            if let Some(inputs) = capability_inputs {
1131                // Extract root field names from provided keys.
1132                // Input mappings can use nested paths like "data.field_name" to build nested objects.
1133                // We need to extract the root field name ("data") to check if it's provided.
1134                let provided_keys: HashSet<String> = agent_step
1135                    .input_mapping
1136                    .as_ref()
1137                    .map(|m| {
1138                        m.keys()
1139                            .map(|k| {
1140                                // Extract root field name from nested path
1141                                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                // Check for missing required inputs
1150                for input in &inputs {
1151                    if input.required && !provided_keys.contains(&input.name) {
1152                        // Skip _connection as it's injected automatically
1153                        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                // Check for unknown input fields (warning)
1166                for key in &provided_keys {
1167                    // Skip internal fields
1168                    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                // Validate immediate value types and enum values
1184                if let Some(mapping) = &agent_step.input_mapping {
1185                    // Build field lookup map
1186                    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                                // Check type compatibility
1193                                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                                // Check enum values
1203                                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    // Recursively validate subgraphs
1224    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
1237// ============================================================================
1238// Phase 4: Configuration Validation
1239// ============================================================================
1240
1241// Thresholds for configuration warnings
1242const MAX_RETRY_RECOMMENDED: u32 = 50;
1243const MAX_RETRY_DELAY_MS: u64 = 3_600_000; // 1 hour
1244const MAX_PARALLELISM_RECOMMENDED: u32 = 100;
1245const MAX_ITERATIONS_RECOMMENDED: u32 = 10_000;
1246const MAX_TIMEOUT_MS: u64 = 3_600_000; // 1 hour
1247
1248fn validate_configuration(graph: &ExecutionGraph, result: &mut ValidationResult) {
1249    for (step_id, step) in &graph.steps {
1250        match step {
1251            Step::Agent(agent_step) => {
1252                // Check retry count
1253                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                // Check retry delay
1264                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                // Check timeout
1275                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                    // Check parallelism
1289                    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                    // Check retry count
1300                    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                    // Check timeout
1311                    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                // Recursively validate subgraph
1323                validate_configuration(&split_step.subgraph, result);
1324            }
1325
1326            Step::While(while_step) => {
1327                if let Some(config) = &while_step.config {
1328                    // Check max iterations
1329                    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                    // Check timeout
1340                    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                // Recursively validate subgraph
1352                validate_configuration(&while_step.subgraph, result);
1353            }
1354
1355            Step::StartScenario(start_step) => {
1356                // Check retry count
1357                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                // Check retry delay
1368                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                // Check timeout
1379                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
1395// ============================================================================
1396// Phase 5: Connection Validation
1397// ============================================================================
1398
1399fn validate_connections(graph: &ExecutionGraph, result: &mut ValidationResult) {
1400    // Get available integrations
1401    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    // Collect all connection step IDs
1406    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            // Validate integration ID exists
1413            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    // Check for unused connections
1424    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    // Recursively validate subgraphs
1437    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
1450/// Collect all connection step IDs referenced in the graph.
1451fn 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                // Extract all step references from this mapping value (including nested composites)
1460                for step_id in extract_step_ids_from_mapping_value(value) {
1461                    referenced.insert(step_id);
1462                }
1463            }
1464        }
1465
1466        // Recursively check subgraphs
1467        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
1481// ============================================================================
1482// Phase 6: Security Validation
1483// ============================================================================
1484
1485fn validate_security(graph: &ExecutionGraph, result: &mut ValidationResult) {
1486    // Collect all connection step IDs
1487    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 no connection steps, nothing to validate
1500    if connection_step_ids.is_empty() {
1501        return;
1502    }
1503
1504    // Check each step for connection data leakage
1505    for (step_id, step) in &graph.steps {
1506        match step {
1507            Step::Agent(agent_step) => {
1508                // Check if agent is secure
1509                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                    // Check input mapping for connection references
1515                    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                // Connection data cannot be in workflow outputs
1530                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                // Connection data cannot be logged
1541                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                // Recursively validate subgraph
1552                validate_security(&split_step.subgraph, result);
1553            }
1554            Step::While(while_step) => {
1555                // Recursively validate subgraph
1556                validate_security(&while_step.subgraph, result);
1557            }
1558            Step::Conditional(_)
1559            | Step::Switch(_)
1560            | Step::StartScenario(_)
1561            | Step::Connection(_) => {}
1562        }
1563    }
1564}
1565
1566// ============================================================================
1567// Phase 7: Child Scenario Validation
1568// ============================================================================
1569
1570fn 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            // Validate version format
1574            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    // Recursively validate subgraphs
1601    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
1614// ============================================================================
1615// Phase 8: Step Name Validation
1616// ============================================================================
1617
1618/// Validate that step names are unique across the workflow.
1619fn validate_step_names(graph: &ExecutionGraph, result: &mut ValidationResult) {
1620    let mut name_to_step_ids: HashMap<String, Vec<String>> = HashMap::new();
1621
1622    // Collect all step names recursively
1623    collect_step_names(graph, &mut name_to_step_ids);
1624
1625    // Report duplicates as errors
1626    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
1635/// Recursively collect step names into the map.
1636/// Skips StartScenario subgraphs as they have their own namespace.
1637fn collect_step_names(graph: &ExecutionGraph, name_to_step_ids: &mut HashMap<String, Vec<String>>) {
1638    for (step_id, step) in &graph.steps {
1639        // Get the step name (if any)
1640        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        // Recursively collect from subgraphs
1660        // NOTE: StartScenario steps do NOT have subgraphs in runtara_dsl,
1661        // they reference child scenarios by ID. So we only recurse into Split/While.
1662        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
1674// ============================================================================
1675// Helper Functions
1676// ============================================================================
1677
1678/// Check if an immediate value's type is compatible with the expected field type.
1679/// Returns Some(ValidationError::TypeMismatch) if incompatible, None if compatible.
1680fn 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    // Normalize the expected type to lowercase for matching
1687    let expected_lower = expected_type.to_lowercase();
1688
1689    let is_compatible = match expected_lower.as_str() {
1690        "any" => true, // "any" type accepts any JSON value
1691        "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        // For complex types like Vec<T>, HashMap<K,V>, Option<T>, Value, etc. - allow any value
1700        _ 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        // Unknown types - skip validation (allow any value)
1709        _ => 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
1725/// Get a human-readable name for a JSON value's type.
1726fn 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
1743/// Recursively extract all step IDs from a MappingValue, including nested Composites.
1744fn 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            // Immediate values have no step references
1754        }
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
1771/// Extract step ID from a reference path like "steps.my_step.outputs.foo"
1772/// or "steps['my-step'].outputs.foo" (bracket notation for IDs with special chars)
1773fn extract_step_id_from_reference(ref_path: &str) -> Option<String> {
1774    // Handle bracket notation: steps['step-id'] or steps["step-id"]
1775    // Note: no dot between "steps" and bracket
1776    if ref_path.starts_with("steps[") {
1777        let rest = &ref_path[5..]; // Skip "steps", keep the bracket
1778        if let Some(end) = rest.find(']') {
1779            let inner = &rest[1..end]; // Skip opening bracket
1780            // Remove quotes if present
1781            let step_id = inner.trim_matches(|c| c == '\'' || c == '"');
1782            return Some(step_id.to_string());
1783        }
1784    }
1785
1786    // Handle dot notation: steps.step_id.outputs
1787    if ref_path.starts_with("steps.") {
1788        let rest = &ref_path[6..]; // Skip "steps."
1789
1790        if let Some(dot_pos) = rest.find('.') {
1791            return Some(rest[..dot_pos].to_string());
1792        } else {
1793            // Reference is just "steps.step_id" (unlikely but possible)
1794            return Some(rest.to_string());
1795        }
1796    }
1797    None
1798}
1799
1800/// Extract variable name from a reference path like "variables.my_var" or "variables.counter.value"
1801fn extract_variable_name_from_reference(ref_path: &str) -> Option<String> {
1802    if ref_path.starts_with("variables.") {
1803        let rest = &ref_path[10..]; // Skip "variables."
1804
1805        if let Some(dot_pos) = rest.find('.') {
1806            return Some(rest[..dot_pos].to_string());
1807        } else {
1808            // Reference is just "variables.var_name"
1809            return Some(rest.to_string());
1810        }
1811    }
1812    None
1813}
1814
1815/// Find connection step IDs referenced in an input mapping.
1816fn 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        // Extract all step references from this mapping value (including nested composites)
1824        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
1834/// Get the step type name for error messages.
1835fn 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
1849/// Find the most similar name using Levenshtein distance.
1850fn 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            // Only suggest if distance is reasonable (less than half the target length + 2)
1858            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
1868/// Simple Levenshtein distance implementation.
1869fn 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
1901/// Format milliseconds as human-readable duration.
1902fn 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// ============================================================================
1915// Tests
1916// ============================================================================
1917
1918#[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        // Use a real capability for the transform agent
1936        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() // Default to map for transform
1942        };
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    // === Graph Structure Tests ===
1999
2000    #[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        // Finish step with no outgoing edges is valid
2034        assert!(!result.has_errors());
2035    }
2036
2037    // === Reference Tests ===
2038
2039    #[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    // === Security Tests ===
2093
2094    #[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        // This test verifies that security validation passes when there are no connection steps.
2112        // Agent validation may fail due to inventory not being populated in test context,
2113        // but that's not what this test is checking.
2114        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        // No security errors (may have other errors like unknown capability)
2154        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    // === Configuration Warning Tests ===
2261
2262    #[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    // === Child Scenario Tests ===
2300
2301    #[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    // === Helper Function Tests ===
2371
2372    #[test]
2373    fn test_levenshtein_distance() {
2374        assert_eq!(levenshtein_distance("http", "http"), 0);
2375        // "http" → "htpp": h=h, t=t, t≠p (sub), p=p → distance 1
2376        assert_eq!(levenshtein_distance("http", "htpp"), 1);
2377        // "transform" → "transfrom": transf-r-o-m vs transf-o-r-m → 2 swaps = 2 substitutions
2378        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        // Edge case: just steps.step_id
2432        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        // References that don't start with "steps." should return None
2441        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    // === ValidationResult Tests ===
2447
2448    #[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()); // Warnings don't prevent compilation
2471        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    // === Error Display Tests ===
2491
2492    #[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    // === Warning Display Tests ===
2555
2556    #[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,         // 2 hours
2574            recommended_max_ms: 3_600_000, // 1 hour
2575        };
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    // === Graph Structure Edge Cases ===
2606
2607    #[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        // No execution plan edge from agent to anywhere
2629
2630        let graph = create_basic_graph(steps, "agent");
2631        let result = validate_workflow(&graph);
2632
2633        // Dangling steps are now warnings, not errors
2634        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        // Finish steps don't need outgoing edges - they're terminal
2642        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        // Should not have dangling step warning for Finish
2649        assert!(
2650            !result
2651                .warnings
2652                .iter()
2653                .any(|w| matches!(w, ValidationWarning::DanglingStep { .. }))
2654        );
2655    }
2656
2657    // === Self-Reference Warning ===
2658
2659    #[test]
2660    fn test_self_reference_warning() {
2661        let mut steps = HashMap::new();
2662        let mut mapping = HashMap::new();
2663        // Step references itself
2664        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    // === Configuration Warning Edge Cases ===
2688
2689    #[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), // 5000 seconds
2703                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),    // Normal
2738                retry_delay: Some(1000), // 1 second - normal
2739                timeout: Some(30_000),   // 30 seconds - normal
2740            }),
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        // Should have no configuration warnings
2753        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    // === Child Scenario Version Tests ===
2765
2766    #[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    // === Levenshtein Edge Cases ===
2867
2868    #[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        // "xyz" is too different from any candidate
2886        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    // ============================================================================
2905    // While Step Tests
2906    // ============================================================================
2907
2908    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        // Create an init step that sets up counter and target
2963        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        // Create while step with LT condition
2972        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        // Should not have reference errors for valid references
2998        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        // Init step
3010        steps.insert(
3011            "init".to_string(),
3012            create_agent_step("init", "transform", None),
3013        );
3014
3015        // Create subgraph with its own agent step
3016        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        // Create a simple condition that always evaluates
3044        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        // Should validate subgraph steps
3072        // Check there's no error about the subgraph entry point
3073        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        // NOTE: Condition expression validation is not yet implemented.
3082        // This test verifies that while steps with invalid condition references
3083        // can still be parsed and don't cause panics during validation.
3084        // Future work: add condition reference validation.
3085        let mut steps = HashMap::new();
3086
3087        steps.insert(
3088            "init".to_string(),
3089            create_agent_step("init", "transform", None),
3090        );
3091
3092        // Create condition referencing non-existent step
3093        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        // Condition references are currently not validated at the DSL level
3121        // (they're evaluated at runtime). This test just ensures no panic.
3122        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        // Create complex AND condition
3139        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        // Should not have reference errors for nested conditions
3187        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        // Condition using loop.index (special loop context variable)
3207        let condition = create_lt_condition("loop.index", "steps.init.outputs.maxIterations");
3208
3209        // Subgraph that references _index variable
3210        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        // loop.index is a valid reference in while conditions
3260        // Should not have errors for this special context variable
3261        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    // ============================================================================
3271    // Log Step Tests
3272    // ============================================================================
3273
3274    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        // Basic log step should pass
3307        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        // All log levels should be valid
3360        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        // First, an agent step to produce outputs
3368        steps.insert(
3369            "process".to_string(),
3370            create_agent_step("process", "transform", None),
3371        );
3372
3373        // Log step with context referencing process outputs
3374        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        // Valid context references should pass
3403        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        // Log step with context referencing non-existent step
3418        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        // Should have invalid reference error
3436        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        // Log step with empty context (not None, but empty HashMap)
3446        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        // Empty context should be valid
3463        assert!(
3464            !result.has_errors(),
3465            "Empty context should not cause errors"
3466        );
3467    }
3468
3469    // ============================================================================
3470    // Connection Step Tests
3471    // ============================================================================
3472
3473    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        // Bearer connection to HTTP agent should pass
3518        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        // Connection step that's not used by any agent
3655        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), // No connection reference
3662        );
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        // Should have unused connection warning
3681        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        // Two connection steps
3691        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        // Two HTTP agents using different connections
3701        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        // Multiple connections should be valid
3743        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        // No unused connection warnings
3757        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        // Subgraph with connection step and HTTP agent
3779        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        // Connection in subgraph to secure agent should be valid
3844        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    // ============================================================================
3855    // New Validation Tests - Execution Order, Variables, Types, Enums, Duplicate Names
3856    // ============================================================================
3857
3858    // --- Execution Order Validation Tests ---
3859
3860    #[test]
3861    fn test_forward_reference_error() {
3862        // step1 references step2, but step1 executes before step2
3863        let mut steps = HashMap::new();
3864
3865        // step1 references step2's output, but executes first
3866        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        // step1 -> step2 -> finish (step1 executes before step2)
3881        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        // step2 references step1, and step1 executes before step2 - should be valid
3907        let mut steps = HashMap::new();
3908
3909        steps.insert(
3910            "step1".to_string(),
3911            create_agent_step("step1", "transform", None),
3912        );
3913
3914        // step2 references step1's output - valid because step1 executes first
3915        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        // step1 -> step2 -> finish (step1 executes before step2)
3925        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        // Should not have StepNotYetExecuted error
3940        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    // --- Variable Existence Validation Tests ---
3950
3951    #[test]
3952    fn test_unknown_variable_error() {
3953        let mut steps = HashMap::new();
3954
3955        // Reference a variable that doesn't exist
3956        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        // Reference a variable that exists
3988        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        // Add the variable to the graph
4003        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        // Should not have UnknownVariable error
4014        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        // Test that variables.myVar.nested.path correctly extracts "myVar"
4028        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        // Add the variable to the graph
4048        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        // Should not have UnknownVariable error
4059        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    // --- Duplicate Step Name Tests ---
4069
4070    #[test]
4071    fn test_duplicate_step_names_error() {
4072        let mut steps = HashMap::new();
4073
4074        // Two steps with the same name
4075        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()), // Duplicate name!
4094                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        // Main graph step with a name
4136        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        // Create subgraph with a step that has the same name
4152        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()), // Duplicate name!
4158                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        // Create split step containing the subgraph
4191        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()), // Different name
4252                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    // --- Error Display Tests for New Errors ---
4288
4289    #[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    // --- Helper Function Tests for New Functions ---
4362
4363    #[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        // Note: Bracket notation is not yet supported for variable extraction.
4382        // This test documents the current behavior. Supporting bracket notation
4383        // could be added in the future if needed.
4384        assert_eq!(
4385            extract_variable_name_from_reference("variables['my-var']"),
4386            None // Bracket notation not supported
4387        );
4388        assert_eq!(
4389            extract_variable_name_from_reference("variables[\"my-var\"]"),
4390            None // Bracket notation not supported
4391        );
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        // Whole numbers are reported as "integer"
4407        assert_eq!(get_json_type_name(&serde_json::json!(42)), "integer");
4408        // Floating point numbers are reported as "number"
4409        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        // None means compatible
4419        assert!(
4420            check_type_compatibility("step", "field", "string", &serde_json::json!("hello"))
4421                .is_none()
4422        );
4423        // Some(error) means incompatible
4424        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        // Unknown types should pass (return None) - e.g., Vec<String>, HashMap, custom types
4509        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    // === Split config.variables Scope Tests ===
4525
4526    #[test]
4527    fn test_split_config_variables_available_in_subgraph() {
4528        use runtara_dsl::{ImmediateValue, SplitConfig, SplitStep};
4529
4530        // Create a subgraph that references a variable from config.variables
4531        let mut subgraph_steps = HashMap::new();
4532        let mut mapping = HashMap::new();
4533        mapping.insert("userId".to_string(), ref_value("variables.parentUserId")); // from config.variables
4534        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(), // No variables declared here
4554            input_schema: HashMap::new(),
4555            output_schema: HashMap::new(),
4556            notes: None,
4557            nodes: None,
4558            edges: None,
4559        };
4560
4561        // config.variables injects parentUserId into the subgraph
4562        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        // Should NOT have UnknownVariable error for parentUserId
4607        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        // Create a subgraph that references a variable NOT in config.variables
4624        let mut subgraph_steps = HashMap::new();
4625        let mut mapping = HashMap::new();
4626        mapping.insert("data".to_string(), ref_value("variables.undeclaredVar")); // NOT defined anywhere
4627        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        // config.variables has a different variable (not undeclaredVar)
4655        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        // Should have UnknownVariable error for undeclaredVar
4700        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        // Create a subgraph that references variables from both sources
4714        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        // Declare a variable in the subgraph itself
4731        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        // config.variables provides configVar
4760        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        // Should NOT have any UnknownVariable errors
4805        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}