1use 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
16static VARIABLE_REGEX: Lazy<Regex> =
18 Lazy::new(|| Regex::new(r"\{\{([^}]+)\}\}").expect("Invalid regex pattern"));
19
20#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
22pub struct FlowStepResult {
23 pub step_id: String,
25 pub success: bool,
27 pub response: Option<Value>,
29 pub error: Option<String>,
31 pub duration_ms: u64,
33 pub extracted_variables: HashMap<String, Value>,
35}
36
37#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
39pub struct FlowExecutionResult {
40 pub flow_id: String,
42 pub success: bool,
44 pub step_results: Vec<FlowStepResult>,
46 pub final_variables: HashMap<String, Value>,
48 pub total_duration_ms: u64,
50 pub error: Option<String>,
52}
53
54pub struct FlowExecutor {
59 variables: HashMap<String, Value>,
61 http_client: Client,
63}
64
65impl FlowExecutor {
66 pub fn new() -> Self {
68 Self {
69 variables: HashMap::new(),
70 http_client: Client::new(),
71 }
72 }
73
74 pub fn with_variables(variables: HashMap<String, Value>) -> Self {
76 Self {
77 variables,
78 http_client: Client::new(),
79 }
80 }
81
82 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 for (key, value) in &flow.variables {
94 self.variables.insert(key.clone(), value.clone());
95 }
96
97 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; }
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 if let Some(ref condition) = step.condition {
114 if !self.evaluate_condition(condition)? {
115 continue; }
117 }
118
119 match step.step_type {
121 StepType::Loop => {
122 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 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 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 let connections = flow.connections.iter().filter(|c| c.from_step_id == step_id);
143
144 for connection in connections {
145 if let Some(ref condition) = connection.condition {
147 if !self.evaluate_condition(condition)? {
148 continue; }
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 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 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 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 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 (true, None, None)
210 }
211 StepType::Delay => {
212 (true, None, None)
214 }
215 StepType::Loop => {
216 (false, None, Some("Loop steps must be handled at flow level".to_string()))
219 }
220 StepType::Parallel => {
221 (false, None, Some("Parallel steps must be handled at flow level".to_string()))
224 }
225 };
226
227 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 async fn execute_api_call(&self, step: &FlowStep) -> (bool, Option<Value>, Option<String>) {
252 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 let endpoint = self.substitute_variables(endpoint);
269 let body = step.body.as_ref().map(|b| self.substitute_variables_in_value(b));
270
271 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 for (key, value) in &step.headers {
289 let header_value = self.substitute_variables(value);
290 request = request.header(key, &header_value);
291 }
292
293 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 match request.send().await {
302 Ok(response) => {
303 let status = response.status();
304 let status_code = status.as_u16();
305
306 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 let response_body = match response.text().await {
325 Ok(text) => {
326 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 let full_response = serde_json::json!({
341 "status": status_code,
342 "headers": {}, "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 fn substitute_variables(&self, text: &str) -> String {
354 VARIABLE_REGEX
355 .replace_all(text, |caps: ®ex::Captures| {
356 let var_name = caps.get(1).unwrap().as_str().trim();
357 self.variables
358 .get(var_name)
359 .map(|v| {
360 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)) })
369 .to_string()
370 }
371
372 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 fn evaluate_condition(&self, condition: &FlowCondition) -> Result<bool> {
392 let expression = self.substitute_variables(&condition.expression);
394
395 let left_value = if expression.starts_with("{{") && expression.ends_with("}}") {
397 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 serde_json::from_str(&expression).unwrap_or(Value::String(expression))
409 };
410
411 let right_value = &condition.value;
412
413 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 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 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 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 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 for iteration in 0..loop_count {
495 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 if let Some(condition_value) = loop_condition {
502 if let Some(condition_str) = condition_value.as_str() {
503 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; }
515 }
516 }
517
518 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 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 async fn execute_parallel(
541 &mut self,
542 parallel_step: &FlowStep,
543 flow: &FlowDefinition,
544 ) -> Result<Vec<FlowStepResult>> {
545 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 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 let mut tasks = Vec::new();
566
567 for child_step in child_steps {
568 let variables_clone = self.variables.clone();
570 let step_clone = child_step.clone();
571 let http_client = self.http_client.clone();
572
573 let task = tokio::spawn(async move {
575 let mut branch_executor = FlowExecutor {
577 variables: variables_clone,
578 http_client,
579 };
580
581 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 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 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 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 fn extract_value(&self, json: &Value, path: &str) -> Option<Value> {
658 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 let _ = executor;
787 }
788
789 #[test]
790 fn test_flow_executor_default() {
791 let executor = FlowExecutor::default();
792 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 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}