mockforge_core/scenario_studio/
flow.rs

1//! Flow execution engine
2//!
3//! This module provides the execution engine for business flows defined in the Scenario Studio.
4
5use crate::error::{Error, Result};
6use crate::scenario_studio::types::{
7    ConditionOperator, FlowCondition, FlowDefinition, FlowStep, StepType,
8};
9use chrono::Utc;
10use once_cell::sync::Lazy;
11use regex::Regex;
12use reqwest::Client;
13use serde_json::Value;
14use std::collections::HashMap;
15
16/// Regex for variable substitution (e.g., "{{variable_name}}")
17static VARIABLE_REGEX: Lazy<Regex> =
18    Lazy::new(|| Regex::new(r"\{\{([^}]+)\}\}").expect("Invalid regex pattern"));
19
20/// Result of executing a flow step
21#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
22pub struct FlowStepResult {
23    /// ID of the step that was executed
24    pub step_id: String,
25    /// Whether the step executed successfully
26    pub success: bool,
27    /// Response data (if applicable)
28    pub response: Option<Value>,
29    /// Error message (if execution failed)
30    pub error: Option<String>,
31    /// Duration in milliseconds
32    pub duration_ms: u64,
33    /// Variables extracted from the response
34    pub extracted_variables: HashMap<String, Value>,
35}
36
37/// Result of executing an entire flow
38#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
39pub struct FlowExecutionResult {
40    /// ID of the flow that was executed
41    pub flow_id: String,
42    /// Whether the flow completed successfully
43    pub success: bool,
44    /// Results for each step that was executed
45    pub step_results: Vec<FlowStepResult>,
46    /// Final variables after flow execution
47    pub final_variables: HashMap<String, Value>,
48    /// Total duration in milliseconds
49    pub total_duration_ms: u64,
50    /// Error message (if execution failed)
51    pub error: Option<String>,
52}
53
54/// Flow execution engine
55///
56/// Executes business flows defined in the Scenario Studio, handling
57/// step sequencing, conditions, and variable extraction.
58pub struct FlowExecutor {
59    /// Variables available during execution
60    variables: HashMap<String, Value>,
61    /// HTTP client for API calls
62    http_client: Client,
63}
64
65impl FlowExecutor {
66    /// Create a new flow executor
67    pub fn new() -> Self {
68        Self {
69            variables: HashMap::new(),
70            http_client: Client::new(),
71        }
72    }
73
74    /// Create a new flow executor with initial variables
75    pub fn with_variables(variables: HashMap<String, Value>) -> Self {
76        Self {
77            variables,
78            http_client: Client::new(),
79        }
80    }
81
82    /// Execute a flow definition
83    ///
84    /// This method executes the flow steps in order, following connections
85    /// and evaluating conditions to determine the execution path.
86    pub async fn execute(&mut self, flow: &FlowDefinition) -> Result<FlowExecutionResult> {
87        let start_time = Utc::now();
88        let mut step_results = Vec::new();
89        let mut executed_step_ids = std::collections::HashSet::new();
90        let mut current_step_ids = self.find_start_steps(flow);
91
92        // Initialize variables from flow
93        for (key, value) in &flow.variables {
94            self.variables.insert(key.clone(), value.clone());
95        }
96
97        // Execute flow until no more steps to execute
98        while !current_step_ids.is_empty() {
99            let mut next_step_ids = Vec::new();
100
101            for step_id in current_step_ids {
102                if executed_step_ids.contains(&step_id) {
103                    continue; // Skip already executed steps to prevent infinite loops
104                }
105
106                let step = flow
107                    .steps
108                    .iter()
109                    .find(|s| s.id == step_id)
110                    .ok_or_else(|| Error::validation(format!("Step {} not found", step_id)))?;
111
112                // Check if step condition is met
113                if let Some(ref condition) = step.condition {
114                    if !self.evaluate_condition(condition)? {
115                        continue; // Skip this step if condition is not met
116                    }
117                }
118
119                // Handle special step types
120                match step.step_type {
121                    StepType::Loop => {
122                        // Execute loop: get child steps and iterate
123                        let loop_results = self.execute_loop(step, flow).await?;
124                        step_results.extend(loop_results);
125                        executed_step_ids.insert(step_id.clone());
126                    }
127                    StepType::Parallel => {
128                        // Execute parallel: get child steps and run in parallel
129                        let parallel_results = self.execute_parallel(step, flow).await?;
130                        step_results.extend(parallel_results);
131                        executed_step_ids.insert(step_id.clone());
132                    }
133                    _ => {
134                        // Execute the step normally
135                        let step_result = self.execute_step(step).await?;
136                        step_results.push(step_result.clone());
137                        executed_step_ids.insert(step_id.clone());
138                    }
139                }
140
141                // Find next steps based on connections
142                let connections = flow.connections.iter().filter(|c| c.from_step_id == step_id);
143
144                for connection in connections {
145                    // Check connection condition if present
146                    if let Some(ref condition) = connection.condition {
147                        if !self.evaluate_condition(condition)? {
148                            continue; // Skip this connection if condition is not met
149                        }
150                    }
151
152                    if !executed_step_ids.contains(&connection.to_step_id) {
153                        next_step_ids.push(connection.to_step_id.clone());
154                    }
155                }
156            }
157
158            current_step_ids = next_step_ids;
159        }
160
161        let end_time = Utc::now();
162        let total_duration_ms = (end_time - start_time).num_milliseconds() as u64;
163
164        let success = step_results.iter().all(|r| r.success);
165
166        // Extract error before moving step_results
167        let error = if success {
168            None
169        } else {
170            step_results.iter().find_map(|r| r.error.as_ref()).cloned()
171        };
172
173        Ok(FlowExecutionResult {
174            flow_id: flow.id.clone(),
175            success,
176            step_results,
177            final_variables: self.variables.clone(),
178            total_duration_ms,
179            error,
180        })
181    }
182
183    /// Find the starting steps in a flow (steps with no incoming connections)
184    fn find_start_steps(&self, flow: &FlowDefinition) -> Vec<String> {
185        let has_incoming: std::collections::HashSet<String> =
186            flow.connections.iter().map(|c| c.to_step_id.clone()).collect();
187
188        flow.steps
189            .iter()
190            .filter(|s| !has_incoming.contains(&s.id))
191            .map(|s| s.id.clone())
192            .collect()
193    }
194
195    /// Execute a single flow step
196    async fn execute_step(&mut self, step: &FlowStep) -> Result<FlowStepResult> {
197        let start_time = Utc::now();
198        let mut extracted_variables = HashMap::new();
199
200        // Apply delay if specified
201        if let Some(delay_ms) = step.delay_ms {
202            tokio::time::sleep(tokio::time::Duration::from_millis(delay_ms)).await;
203        }
204
205        let (success, response, error) = match step.step_type {
206            StepType::ApiCall => self.execute_api_call(step).await,
207            StepType::Condition => {
208                // Conditions are evaluated before step execution
209                (true, None, None)
210            }
211            StepType::Delay => {
212                // Delay is already applied above
213                (true, None, None)
214            }
215            StepType::Loop => {
216                // Loop execution is handled at the flow level, not step level
217                // This should not be reached in normal execution
218                (false, None, Some("Loop steps must be handled at flow level".to_string()))
219            }
220            StepType::Parallel => {
221                // Parallel execution is handled at the flow level, not step level
222                // This should not be reached in normal execution
223                (false, None, Some("Parallel steps must be handled at flow level".to_string()))
224            }
225        };
226
227        // Extract variables from response
228        if let Some(ref resp) = response {
229            for (key, path) in &step.extract {
230                if let Some(value) = self.extract_value(resp, path) {
231                    extracted_variables.insert(key.clone(), value.clone());
232                    self.variables.insert(key.clone(), value);
233                }
234            }
235        }
236
237        let end_time = Utc::now();
238        let duration_ms = (end_time - start_time).num_milliseconds() as u64;
239
240        Ok(FlowStepResult {
241            step_id: step.id.clone(),
242            success,
243            response,
244            error,
245            duration_ms,
246            extracted_variables,
247        })
248    }
249
250    /// Execute an API call step
251    async fn execute_api_call(&self, step: &FlowStep) -> (bool, Option<Value>, Option<String>) {
252        // Get method and endpoint
253        let method = match step.method.as_ref() {
254            Some(m) => m,
255            None => {
256                return (false, None, Some("API call step missing method".to_string()));
257            }
258        };
259
260        let endpoint = match step.endpoint.as_ref() {
261            Some(e) => e,
262            None => {
263                return (false, None, Some("API call step missing endpoint".to_string()));
264            }
265        };
266
267        // Substitute variables in endpoint and body
268        let endpoint = self.substitute_variables(endpoint);
269        let body = step.body.as_ref().map(|b| self.substitute_variables_in_value(b));
270
271        // Build request
272        let method = match method.to_uppercase().as_str() {
273            "GET" => reqwest::Method::GET,
274            "POST" => reqwest::Method::POST,
275            "PUT" => reqwest::Method::PUT,
276            "PATCH" => reqwest::Method::PATCH,
277            "DELETE" => reqwest::Method::DELETE,
278            "HEAD" => reqwest::Method::HEAD,
279            "OPTIONS" => reqwest::Method::OPTIONS,
280            _ => {
281                return (false, None, Some(format!("Unsupported HTTP method: {}", method)));
282            }
283        };
284
285        let mut request = self.http_client.request(method, &endpoint);
286
287        // Add headers
288        for (key, value) in &step.headers {
289            let header_value = self.substitute_variables(value);
290            request = request.header(key, &header_value);
291        }
292
293        // Add body if present
294        if let Some(ref body_value) = body {
295            if let Ok(json_body) = serde_json::to_string(body_value) {
296                request = request.header("Content-Type", "application/json").body(json_body);
297            }
298        }
299
300        // Execute request
301        match request.send().await {
302            Ok(response) => {
303                let status = response.status();
304                let status_code = status.as_u16();
305
306                // Check expected status if specified
307                if let Some(expected) = step.expected_status {
308                    if status_code != expected {
309                        return (
310                            false,
311                            Some(serde_json::json!({
312                                "status": status_code,
313                                "error": format!("Expected status {}, got {}", expected, status_code)
314                            })),
315                            Some(format!(
316                                "Status code mismatch: expected {}, got {}",
317                                expected, status_code
318                            )),
319                        );
320                    }
321                }
322
323                // Parse response body
324                let response_body = match response.text().await {
325                    Ok(text) => {
326                        // Try to parse as JSON, fallback to string
327                        serde_json::from_str(&text).unwrap_or_else(|_| {
328                            serde_json::json!({
329                                "body": text,
330                                "status": status_code
331                            })
332                        })
333                    }
334                    Err(e) => {
335                        return (false, None, Some(format!("Failed to read response body: {}", e)));
336                    }
337                };
338
339                // Build full response object
340                let full_response = serde_json::json!({
341                    "status": status_code,
342                    "headers": {}, // Could extract headers if needed
343                    "body": response_body
344                });
345
346                (true, Some(full_response), None)
347            }
348            Err(e) => (false, None, Some(format!("API call failed: {}", e))),
349        }
350    }
351
352    /// Substitute variables in a string (e.g., "{{variable_name}}")
353    fn substitute_variables(&self, text: &str) -> String {
354        VARIABLE_REGEX
355            .replace_all(text, |caps: &regex::Captures| {
356                let var_name = caps.get(1).unwrap().as_str().trim();
357                self.variables
358                    .get(var_name)
359                    .map(|v| {
360                        // Convert value to string
361                        if let Some(s) = v.as_str() {
362                            s.to_string()
363                        } else {
364                            v.to_string()
365                        }
366                    })
367                    .unwrap_or_else(|| format!("{{{{{}}}}}", var_name)) // Keep original if not found
368            })
369            .to_string()
370    }
371
372    /// Substitute variables in a JSON value
373    fn substitute_variables_in_value(&self, value: &Value) -> Value {
374        match value {
375            Value::String(s) => Value::String(self.substitute_variables(s)),
376            Value::Object(map) => {
377                let mut new_map = serde_json::Map::new();
378                for (k, v) in map {
379                    new_map.insert(k.clone(), self.substitute_variables_in_value(v));
380                }
381                Value::Object(new_map)
382            }
383            Value::Array(arr) => {
384                Value::Array(arr.iter().map(|v| self.substitute_variables_in_value(v)).collect())
385            }
386            _ => value.clone(),
387        }
388    }
389
390    /// Evaluate a flow condition
391    fn evaluate_condition(&self, condition: &FlowCondition) -> Result<bool> {
392        // Substitute variables in expression
393        let expression = self.substitute_variables(&condition.expression);
394
395        // Get the value to compare (from variables or literal)
396        let left_value = if expression.starts_with("{{") && expression.ends_with("}}") {
397            // Extract variable name
398            let var_name = expression
399                .strip_prefix("{{")
400                .and_then(|s| s.strip_suffix("}}"))
401                .map(|s| s.trim());
402            var_name
403                .and_then(|name| self.variables.get(name))
404                .cloned()
405                .unwrap_or(Value::Null)
406        } else {
407            // Try to parse as JSON value
408            serde_json::from_str(&expression).unwrap_or(Value::String(expression))
409        };
410
411        let right_value = &condition.value;
412
413        // Apply operator
414        let result = match condition.operator {
415            ConditionOperator::Eq => left_value == *right_value,
416            ConditionOperator::Ne => left_value != *right_value,
417            ConditionOperator::Gt => self.compare_values(&left_value, right_value, |a, b| a > b),
418            ConditionOperator::Gte => self.compare_values(&left_value, right_value, |a, b| a >= b),
419            ConditionOperator::Lt => self.compare_values(&left_value, right_value, |a, b| a < b),
420            ConditionOperator::Lte => self.compare_values(&left_value, right_value, |a, b| a <= b),
421            ConditionOperator::Contains => {
422                if let (Some(left_str), Some(right_str)) =
423                    (left_value.as_str(), right_value.as_str())
424                {
425                    left_str.contains(right_str)
426                } else {
427                    false
428                }
429            }
430            ConditionOperator::NotContains => {
431                if let (Some(left_str), Some(right_str)) =
432                    (left_value.as_str(), right_value.as_str())
433                {
434                    !left_str.contains(right_str)
435                } else {
436                    true
437                }
438            }
439            ConditionOperator::Matches => {
440                if let (Some(left_str), Some(right_str)) =
441                    (left_value.as_str(), right_value.as_str())
442                {
443                    Regex::new(right_str).map(|re| re.is_match(left_str)).unwrap_or(false)
444                } else {
445                    false
446                }
447            }
448            ConditionOperator::Exists => left_value != Value::Null,
449        };
450
451        Ok(result)
452    }
453
454    /// Compare two values numerically
455    fn compare_values<F>(&self, left: &Value, right: &Value, cmp: F) -> bool
456    where
457        F: Fn(f64, f64) -> bool,
458    {
459        match (left.as_f64(), right.as_f64()) {
460            (Some(l), Some(r)) => cmp(l, r),
461            _ => false,
462        }
463    }
464
465    /// Execute a loop step
466    ///
467    /// Loops execute their child steps multiple times based on loop configuration
468    /// stored in step metadata (e.g., "loop_count" or "loop_condition").
469    async fn execute_loop(
470        &mut self,
471        loop_step: &FlowStep,
472        flow: &FlowDefinition,
473    ) -> Result<Vec<FlowStepResult>> {
474        let mut all_results = Vec::new();
475
476        // Get loop configuration from metadata
477        let loop_count = loop_step.metadata.get("loop_count").and_then(|v| v.as_u64()).unwrap_or(1);
478
479        let loop_condition = loop_step.metadata.get("loop_condition");
480
481        // Find child steps (steps connected from this loop step)
482        let child_step_ids: Vec<String> = flow
483            .connections
484            .iter()
485            .filter(|c| c.from_step_id == loop_step.id)
486            .map(|c| c.to_step_id.clone())
487            .collect();
488
489        if child_step_ids.is_empty() {
490            return Ok(all_results);
491        }
492
493        // Execute loop iterations
494        for iteration in 0..loop_count {
495            // Set loop iteration variable
496            self.variables
497                .insert("loop_iteration".to_string(), serde_json::json!(iteration));
498            self.variables.insert("loop_index".to_string(), serde_json::json!(iteration));
499
500            // Check loop condition if specified
501            if let Some(condition_value) = loop_condition {
502                if let Some(condition_str) = condition_value.as_str() {
503                    // Evaluate condition (simplified - could be enhanced)
504                    let condition_result = self
505                        .evaluate_condition(&FlowCondition {
506                            expression: condition_str.to_string(),
507                            operator: ConditionOperator::Eq,
508                            value: Value::Bool(true),
509                        })
510                        .unwrap_or(false);
511
512                    if !condition_result {
513                        break; // Exit loop if condition fails
514                    }
515                }
516            }
517
518            // Execute child steps for this iteration
519            for child_step_id in &child_step_ids {
520                if let Some(child_step) = flow.steps.iter().find(|s| s.id == *child_step_id) {
521                    // Check if step condition is met
522                    if let Some(ref condition) = child_step.condition {
523                        if !self.evaluate_condition(condition)? {
524                            continue;
525                        }
526                    }
527
528                    let step_result = self.execute_step(child_step).await?;
529                    all_results.push(step_result);
530                }
531            }
532        }
533
534        Ok(all_results)
535    }
536
537    /// Execute a parallel step
538    ///
539    /// Parallel steps execute their child steps concurrently.
540    async fn execute_parallel(
541        &mut self,
542        parallel_step: &FlowStep,
543        flow: &FlowDefinition,
544    ) -> Result<Vec<FlowStepResult>> {
545        // Find child steps (steps connected from this parallel step)
546        let child_step_ids: Vec<String> = flow
547            .connections
548            .iter()
549            .filter(|c| c.from_step_id == parallel_step.id)
550            .map(|c| c.to_step_id.clone())
551            .collect();
552
553        if child_step_ids.is_empty() {
554            return Ok(Vec::new());
555        }
556
557        // Collect child steps
558        let child_steps: Vec<&FlowStep> = child_step_ids
559            .iter()
560            .filter_map(|step_id| flow.steps.iter().find(|s| s.id == *step_id))
561            .collect();
562
563        // Execute all child steps in parallel using tokio::spawn
564        // Note: We need to clone variables for each parallel execution
565        let mut tasks = Vec::new();
566
567        for child_step in child_steps {
568            // Clone variables for this parallel branch
569            let variables_clone = self.variables.clone();
570            let step_clone = child_step.clone();
571            let http_client = self.http_client.clone();
572
573            // Create a task for this parallel step
574            let task = tokio::spawn(async move {
575                // Create a temporary executor for this parallel branch
576                let mut branch_executor = FlowExecutor {
577                    variables: variables_clone,
578                    http_client,
579                };
580
581                // Check condition if present
582                if let Some(ref condition) = step_clone.condition {
583                    match branch_executor.evaluate_condition(condition) {
584                        Ok(true) => {}
585                        Ok(false) => {
586                            return FlowStepResult {
587                                step_id: step_clone.id.clone(),
588                                success: false,
589                                response: None,
590                                error: Some("Condition not met".to_string()),
591                                duration_ms: 0,
592                                extracted_variables: HashMap::new(),
593                            };
594                        }
595                        Err(e) => {
596                            return FlowStepResult {
597                                step_id: step_clone.id.clone(),
598                                success: false,
599                                response: None,
600                                error: Some(format!("Condition evaluation error: {}", e)),
601                                duration_ms: 0,
602                                extracted_variables: HashMap::new(),
603                            };
604                        }
605                    }
606                }
607
608                // Execute the step
609                branch_executor
610                    .execute_step(&step_clone)
611                    .await
612                    .unwrap_or_else(|e| FlowStepResult {
613                        step_id: step_clone.id.clone(),
614                        success: false,
615                        response: None,
616                        error: Some(format!("Execution error: {}", e)),
617                        duration_ms: 0,
618                        extracted_variables: HashMap::new(),
619                    })
620            });
621
622            tasks.push(task);
623        }
624
625        // Wait for all parallel tasks to complete
626        let mut results = Vec::new();
627        for task in tasks {
628            match task.await {
629                Ok(result) => {
630                    results.push(result);
631                }
632                Err(e) => {
633                    results.push(FlowStepResult {
634                        step_id: "unknown".to_string(),
635                        success: false,
636                        response: None,
637                        error: Some(format!("Parallel task error: {}", e)),
638                        duration_ms: 0,
639                        extracted_variables: HashMap::new(),
640                    });
641                }
642            }
643        }
644
645        // Merge variables from all parallel branches
646        // (Last write wins for conflicts)
647        for result in &results {
648            for (key, value) in &result.extracted_variables {
649                self.variables.insert(key.clone(), value.clone());
650            }
651        }
652
653        Ok(results)
654    }
655
656    /// Extract a value from a JSON object using a path expression
657    fn extract_value(&self, json: &Value, path: &str) -> Option<Value> {
658        // Simple path extraction (e.g., "body.id" or "status")
659        let parts: Vec<&str> = path.split('.').collect();
660        let mut current = json;
661
662        for part in parts {
663            match current {
664                Value::Object(map) => {
665                    current = map.get(part)?;
666                }
667                Value::Array(arr) => {
668                    let index: usize = part.parse().ok()?;
669                    current = arr.get(index)?;
670                }
671                _ => return None,
672            }
673        }
674
675        Some(current.clone())
676    }
677}
678
679impl Default for FlowExecutor {
680    fn default() -> Self {
681        Self::new()
682    }
683}
684
685#[cfg(test)]
686mod tests {
687    use super::*;
688    use serde_json::json;
689
690    #[test]
691    fn test_flow_step_result_creation() {
692        let mut extracted = HashMap::new();
693        extracted.insert("user_id".to_string(), json!("123"));
694
695        let result = FlowStepResult {
696            step_id: "step-1".to_string(),
697            success: true,
698            response: Some(json!({"status": "ok"})),
699            error: None,
700            duration_ms: 150,
701            extracted_variables: extracted.clone(),
702        };
703
704        assert_eq!(result.step_id, "step-1");
705        assert!(result.success);
706        assert!(result.response.is_some());
707        assert!(result.error.is_none());
708        assert_eq!(result.duration_ms, 150);
709        assert_eq!(result.extracted_variables.len(), 1);
710    }
711
712    #[test]
713    fn test_flow_step_result_with_error() {
714        let result = FlowStepResult {
715            step_id: "step-2".to_string(),
716            success: false,
717            response: None,
718            error: Some("Request failed".to_string()),
719            duration_ms: 50,
720            extracted_variables: HashMap::new(),
721        };
722
723        assert!(!result.success);
724        assert!(result.error.is_some());
725        assert_eq!(result.error.unwrap(), "Request failed");
726    }
727
728    #[test]
729    fn test_flow_execution_result_creation() {
730        let step_result = FlowStepResult {
731            step_id: "step-1".to_string(),
732            success: true,
733            response: None,
734            error: None,
735            duration_ms: 100,
736            extracted_variables: HashMap::new(),
737        };
738
739        let mut final_vars = HashMap::new();
740        final_vars.insert("result".to_string(), json!("success"));
741
742        let result = FlowExecutionResult {
743            flow_id: "flow-123".to_string(),
744            success: true,
745            step_results: vec![step_result],
746            final_variables: final_vars.clone(),
747            total_duration_ms: 200,
748            error: None,
749        };
750
751        assert_eq!(result.flow_id, "flow-123");
752        assert!(result.success);
753        assert_eq!(result.step_results.len(), 1);
754        assert_eq!(result.final_variables.len(), 1);
755        assert_eq!(result.total_duration_ms, 200);
756    }
757
758    #[test]
759    fn test_flow_execution_result_with_error() {
760        let step_result = FlowStepResult {
761            step_id: "step-1".to_string(),
762            success: false,
763            response: None,
764            error: Some("Step failed".to_string()),
765            duration_ms: 50,
766            extracted_variables: HashMap::new(),
767        };
768
769        let result = FlowExecutionResult {
770            flow_id: "flow-456".to_string(),
771            success: false,
772            step_results: vec![step_result],
773            final_variables: HashMap::new(),
774            total_duration_ms: 100,
775            error: Some("Flow execution failed".to_string()),
776        };
777
778        assert!(!result.success);
779        assert!(result.error.is_some());
780    }
781
782    #[test]
783    fn test_flow_executor_new() {
784        let executor = FlowExecutor::new();
785        // Just verify it can be created
786        let _ = executor;
787    }
788
789    #[test]
790    fn test_flow_executor_default() {
791        let executor = FlowExecutor::default();
792        // Just verify it can be created
793        let _ = executor;
794    }
795
796    #[test]
797    fn test_flow_executor_with_variables() {
798        let mut variables = HashMap::new();
799        variables.insert("api_key".to_string(), json!("secret123"));
800        variables.insert("base_url".to_string(), json!("https://api.example.com"));
801
802        let executor = FlowExecutor::with_variables(variables);
803        // Just verify it can be created
804        let _ = executor;
805    }
806
807    #[test]
808    fn test_flow_step_result_clone() {
809        let result1 = FlowStepResult {
810            step_id: "step-1".to_string(),
811            success: true,
812            response: Some(json!({"status": "ok"})),
813            error: None,
814            duration_ms: 100,
815            extracted_variables: HashMap::new(),
816        };
817        let result2 = result1.clone();
818        assert_eq!(result1.step_id, result2.step_id);
819        assert_eq!(result1.success, result2.success);
820    }
821
822    #[test]
823    fn test_flow_step_result_debug() {
824        let result = FlowStepResult {
825            step_id: "step-1".to_string(),
826            success: true,
827            response: None,
828            error: None,
829            duration_ms: 150,
830            extracted_variables: HashMap::new(),
831        };
832        let debug_str = format!("{:?}", result);
833        assert!(debug_str.contains("FlowStepResult"));
834    }
835
836    #[test]
837    fn test_flow_step_result_serialization() {
838        let result = FlowStepResult {
839            step_id: "step-1".to_string(),
840            success: true,
841            response: Some(json!({"data": "test"})),
842            error: None,
843            duration_ms: 200,
844            extracted_variables: HashMap::from([("var1".to_string(), json!("value1"))]),
845        };
846        let json = serde_json::to_string(&result).unwrap();
847        assert!(json.contains("step-1"));
848        assert!(json.contains("true"));
849    }
850
851    #[test]
852    fn test_flow_execution_result_clone() {
853        let result1 = FlowExecutionResult {
854            flow_id: "flow-1".to_string(),
855            success: true,
856            step_results: vec![],
857            final_variables: HashMap::new(),
858            total_duration_ms: 100,
859            error: None,
860        };
861        let result2 = result1.clone();
862        assert_eq!(result1.flow_id, result2.flow_id);
863        assert_eq!(result1.success, result2.success);
864    }
865
866    #[test]
867    fn test_flow_execution_result_debug() {
868        let result = FlowExecutionResult {
869            flow_id: "flow-123".to_string(),
870            success: false,
871            step_results: vec![],
872            final_variables: HashMap::new(),
873            total_duration_ms: 50,
874            error: Some("Error".to_string()),
875        };
876        let debug_str = format!("{:?}", result);
877        assert!(debug_str.contains("FlowExecutionResult"));
878    }
879
880    #[test]
881    fn test_flow_execution_result_serialization() {
882        let step_result = FlowStepResult {
883            step_id: "step-1".to_string(),
884            success: true,
885            response: Some(json!({"id": 1})),
886            error: None,
887            duration_ms: 100,
888            extracted_variables: HashMap::new(),
889        };
890        let result = FlowExecutionResult {
891            flow_id: "flow-456".to_string(),
892            success: true,
893            step_results: vec![step_result],
894            final_variables: HashMap::from([("result".to_string(), json!("success"))]),
895            total_duration_ms: 200,
896            error: None,
897        };
898        let json = serde_json::to_string(&result).unwrap();
899        assert!(json.contains("flow-456"));
900        assert!(json.contains("step-1"));
901    }
902
903    #[test]
904    fn test_flow_step_result_with_all_fields() {
905        let mut extracted = HashMap::new();
906        extracted.insert("user_id".to_string(), json!("123"));
907        extracted.insert("token".to_string(), json!("abc123"));
908        extracted.insert("expires_at".to_string(), json!("2024-01-01"));
909
910        let result = FlowStepResult {
911            step_id: "step-auth".to_string(),
912            success: true,
913            response: Some(json!({
914                "user": {"id": 123, "name": "Alice"},
915                "token": "abc123",
916                "expires_at": "2024-01-01"
917            })),
918            error: None,
919            duration_ms: 250,
920            extracted_variables: extracted.clone(),
921        };
922
923        assert_eq!(result.extracted_variables.len(), 3);
924        assert!(result.response.is_some());
925        assert_eq!(result.duration_ms, 250);
926    }
927
928    #[test]
929    fn test_flow_execution_result_with_multiple_steps() {
930        let step1 = FlowStepResult {
931            step_id: "step-1".to_string(),
932            success: true,
933            response: Some(json!({"id": 1})),
934            error: None,
935            duration_ms: 100,
936            extracted_variables: HashMap::from([("id".to_string(), json!(1))]),
937        };
938        let step2 = FlowStepResult {
939            step_id: "step-2".to_string(),
940            success: true,
941            response: Some(json!({"status": "ok"})),
942            error: None,
943            duration_ms: 150,
944            extracted_variables: HashMap::new(),
945        };
946        let step3 = FlowStepResult {
947            step_id: "step-3".to_string(),
948            success: true,
949            response: None,
950            error: None,
951            duration_ms: 50,
952            extracted_variables: HashMap::new(),
953        };
954
955        let result = FlowExecutionResult {
956            flow_id: "flow-multi".to_string(),
957            success: true,
958            step_results: vec![step1, step2, step3],
959            final_variables: HashMap::from([
960                ("id".to_string(), json!(1)),
961                ("status".to_string(), json!("ok")),
962            ]),
963            total_duration_ms: 300,
964            error: None,
965        };
966
967        assert_eq!(result.step_results.len(), 3);
968        assert_eq!(result.final_variables.len(), 2);
969        assert_eq!(result.total_duration_ms, 300);
970    }
971
972    #[test]
973    fn test_flow_step_result_with_extracted_variables() {
974        let mut extracted = HashMap::new();
975        extracted.insert("order_id".to_string(), json!("order-123"));
976        extracted.insert("total".to_string(), json!(99.99));
977        extracted.insert("currency".to_string(), json!("USD"));
978
979        let result = FlowStepResult {
980            step_id: "step-checkout".to_string(),
981            success: true,
982            response: Some(json!({
983                "order": {"id": "order-123", "total": 99.99, "currency": "USD"}
984            })),
985            error: None,
986            duration_ms: 300,
987            extracted_variables: extracted.clone(),
988        };
989
990        assert_eq!(result.extracted_variables.len(), 3);
991        assert_eq!(result.extracted_variables.get("order_id"), Some(&json!("order-123")));
992    }
993
994    #[test]
995    fn test_flow_execution_result_with_error_and_steps() {
996        let step1 = FlowStepResult {
997            step_id: "step-1".to_string(),
998            success: true,
999            response: Some(json!({"id": 1})),
1000            error: None,
1001            duration_ms: 100,
1002            extracted_variables: HashMap::new(),
1003        };
1004        let step2 = FlowStepResult {
1005            step_id: "step-2".to_string(),
1006            success: false,
1007            response: None,
1008            error: Some("Connection timeout".to_string()),
1009            duration_ms: 5000,
1010            extracted_variables: HashMap::new(),
1011        };
1012
1013        let result = FlowExecutionResult {
1014            flow_id: "flow-error".to_string(),
1015            success: false,
1016            step_results: vec![step1, step2],
1017            final_variables: HashMap::new(),
1018            total_duration_ms: 5100,
1019            error: Some("Flow failed at step-2: Connection timeout".to_string()),
1020        };
1021
1022        assert!(!result.success);
1023        assert_eq!(result.step_results.len(), 2);
1024        assert!(result.error.is_some());
1025        assert_eq!(result.total_duration_ms, 5100);
1026    }
1027}