//! Integration tests for workflow engine use std::{ sync::{ atomic::{AtomicU32, Ordering}, Arc, }, time::Duration, }; use serde::{Deserialize, Serialize}; use smg::workflow::*; use tokio::time::sleep; /// Test workflow data type for integration tests. #[derive(Debug, Clone, Serialize, Deserialize, Default)] struct TestWorkflowData { /// Execution count for tracking step invocations pub execution_count: u32, /// Test key for context sharing tests pub test_key: Option, } impl WorkflowData for TestWorkflowData { fn workflow_type() -> &'static str { "test_workflow" } } // Test step that counts invocations struct CountingStep { counter: Arc, should_succeed_after: u32, } #[async_trait::async_trait] impl StepExecutor for CountingStep { async fn execute( &self, context: &mut WorkflowContext, ) -> WorkflowResult { let count = self.counter.fetch_add(1, Ordering::SeqCst) + 1; // Store count in context context.data.execution_count = count; if count >= self.should_succeed_after { Ok(StepResult::Success) } else { Err(WorkflowError::StepFailed { step_id: StepId::new("counting_step"), message: format!("Not ready yet, attempt {}", count), }) } } } // Test step that always succeeds struct AlwaysSucceedStep; #[async_trait::async_trait] impl StepExecutor for AlwaysSucceedStep { async fn execute( &self, _context: &mut WorkflowContext, ) -> WorkflowResult { Ok(StepResult::Success) } } struct AlwaysFailStep; #[async_trait::async_trait] impl StepExecutor for AlwaysFailStep { async fn execute( &self, _context: &mut WorkflowContext, ) -> WorkflowResult { Ok(StepResult::Failure) } } #[tokio::test] async fn test_simple_workflow_execution() { let engine: WorkflowEngine = WorkflowEngine::new(); // Subscribe to events for logging engine .event_bus() .subscribe(Arc::new(LoggingSubscriber)) .await; // Create a simple workflow let workflow = WorkflowDefinition::new("test_workflow", "Simple Test Workflow") .add_step(StepDefinition::new( "step1", "First Step", Arc::new(AlwaysSucceedStep), )) .add_step(StepDefinition::new( "step2", "Second Step", Arc::new(AlwaysSucceedStep), )); let workflow_id = workflow.id.clone(); engine.register_workflow(workflow).unwrap(); // Start workflow let instance_id = engine .start_workflow(workflow_id, TestWorkflowData::default()) .await .unwrap(); // Wait for completion sleep(Duration::from_millis(100)).await; // Check status let state = engine.get_status(instance_id).await.unwrap(); assert_eq!(state.status, WorkflowStatus::Completed); assert_eq!(state.step_states.len(), 2); } #[tokio::test] async fn test_workflow_with_retry() { let engine: WorkflowEngine = WorkflowEngine::new(); engine .event_bus() .subscribe(Arc::new(LoggingSubscriber)) .await; let counter = Arc::new(AtomicU32::new(0)); // Create workflow with retry logic let workflow = WorkflowDefinition::new("retry_workflow", "Workflow with Retry").add_step( StepDefinition::new( "retry_step", "Step that retries", Arc::new(CountingStep { counter: Arc::clone(&counter), should_succeed_after: 3, }), ) .with_retry(RetryPolicy { max_attempts: 5, backoff: BackoffStrategy::Fixed(Duration::from_millis(10)), }) .with_timeout(Duration::from_secs(5)), ); let workflow_id = workflow.id.clone(); engine.register_workflow(workflow).unwrap(); // Start workflow let instance_id = engine .start_workflow(workflow_id, TestWorkflowData::default()) .await .unwrap(); // Wait for completion sleep(Duration::from_millis(500)).await; // Check that step was retried and eventually succeeded let state = engine.get_status(instance_id).await.unwrap(); assert_eq!(state.status, WorkflowStatus::Completed); let step_state = state.step_states.get(&StepId::new("retry_step")).unwrap(); assert_eq!(step_state.status, StepStatus::Succeeded); assert_eq!(step_state.attempt, 3); // Should have taken 3 attempts // Verify counter assert_eq!(counter.load(Ordering::SeqCst), 3); } #[tokio::test] async fn test_workflow_failure_after_max_retries() { let engine: WorkflowEngine = WorkflowEngine::new(); engine .event_bus() .subscribe(Arc::new(LoggingSubscriber)) .await; let counter = Arc::new(AtomicU32::new(0)); // Create workflow that will fail let workflow = WorkflowDefinition::new("failing_workflow", "Workflow that Fails").add_step( StepDefinition::new( "failing_step", "Step that always fails", Arc::new(CountingStep { counter: Arc::clone(&counter), should_succeed_after: 10, // Will never succeed within max_attempts }), ) .with_retry(RetryPolicy { max_attempts: 3, backoff: BackoffStrategy::Fixed(Duration::from_millis(10)), }) .with_failure_action(FailureAction::FailWorkflow), ); let workflow_id = workflow.id.clone(); engine.register_workflow(workflow).unwrap(); // Start workflow let instance_id = engine .start_workflow(workflow_id, TestWorkflowData::default()) .await .unwrap(); // Wait for completion sleep(Duration::from_millis(500)).await; // Check that workflow failed let state = engine.get_status(instance_id).await.unwrap(); assert_eq!(state.status, WorkflowStatus::Failed); let step_state = state.step_states.get(&StepId::new("failing_step")).unwrap(); assert_eq!(step_state.status, StepStatus::Failed); assert_eq!(step_state.attempt, 3); // Should have tried 3 times // Verify counter assert_eq!(counter.load(Ordering::SeqCst), 3); } #[tokio::test] async fn test_workflow_continue_on_failure() { let engine: WorkflowEngine = WorkflowEngine::new(); engine .event_bus() .subscribe(Arc::new(LoggingSubscriber)) .await; let counter = Arc::new(AtomicU32::new(0)); // Create workflow where first step fails but workflow continues let workflow = WorkflowDefinition::new("continue_workflow", "Continue on Failure") .add_step( StepDefinition::new( "failing_step", "Step that fails", Arc::new(CountingStep { counter: Arc::clone(&counter), should_succeed_after: 10, }), ) .with_retry(RetryPolicy { max_attempts: 2, backoff: BackoffStrategy::Fixed(Duration::from_millis(10)), }) .with_failure_action(FailureAction::ContinueNextStep), ) .add_step(StepDefinition::new( "success_step", "Step that succeeds", Arc::new(AlwaysSucceedStep), )); let workflow_id = workflow.id.clone(); engine.register_workflow(workflow).unwrap(); // Start workflow let instance_id = engine .start_workflow(workflow_id, TestWorkflowData::default()) .await .unwrap(); // Wait for completion sleep(Duration::from_millis(500)).await; // Workflow should complete despite first step failing let state = engine.get_status(instance_id).await.unwrap(); assert_eq!(state.status, WorkflowStatus::Completed); // First step should be skipped let step1_state = state.step_states.get(&StepId::new("failing_step")).unwrap(); assert_eq!(step1_state.status, StepStatus::Skipped); // Second step should succeed let step2_state = state.step_states.get(&StepId::new("success_step")).unwrap(); assert_eq!(step2_state.status, StepStatus::Succeeded); } #[tokio::test] async fn test_workflow_context_sharing() { let engine: WorkflowEngine = WorkflowEngine::new(); struct ContextWriterStep { value: String, } #[async_trait::async_trait] impl StepExecutor for ContextWriterStep { async fn execute( &self, context: &mut WorkflowContext, ) -> WorkflowResult { context.data.test_key = Some(self.value.clone()); Ok(StepResult::Success) } } struct ContextReaderStep { expected_value: String, } #[async_trait::async_trait] impl StepExecutor for ContextReaderStep { async fn execute( &self, context: &mut WorkflowContext, ) -> WorkflowResult { let value = context .data .test_key .as_ref() .ok_or_else(|| WorkflowError::ContextValueNotFound("test_key".to_string()))?; if value == &self.expected_value { Ok(StepResult::Success) } else { Err(WorkflowError::StepFailed { step_id: StepId::new("reader"), message: format!("Expected {}, got {}", self.expected_value, value), }) } } } let workflow = WorkflowDefinition::new("context_workflow", "Context Sharing Test") .add_step(StepDefinition::new( "writer", "Write to context", Arc::new(ContextWriterStep { value: "test_value".to_string(), }), )) .add_step(StepDefinition::new( "reader", "Read from context", Arc::new(ContextReaderStep { expected_value: "test_value".to_string(), }), )); let workflow_id = workflow.id.clone(); engine.register_workflow(workflow).unwrap(); let instance_id = engine .start_workflow(workflow_id, TestWorkflowData::default()) .await .unwrap(); sleep(Duration::from_millis(100)).await; let state = engine.get_status(instance_id).await.unwrap(); assert_eq!(state.status, WorkflowStatus::Completed); } // ============================================================================ // DAG / Parallel Execution Tests // ============================================================================ // Step that records when it starts and ends (for testing parallel execution) struct TimingStep { step_name: String, duration_ms: u64, start_times: Arc>>, end_times: Arc>>, } #[async_trait::async_trait] impl StepExecutor for TimingStep { async fn execute( &self, _context: &mut WorkflowContext, ) -> WorkflowResult { let start = std::time::Instant::now(); self.start_times .write() .push((self.step_name.clone(), start)); sleep(Duration::from_millis(self.duration_ms)).await; let end = std::time::Instant::now(); self.end_times.write().push((self.step_name.clone(), end)); Ok(StepResult::Success) } } #[tokio::test] async fn test_parallel_execution_no_dependencies() { // Steps without dependencies should run in parallel let engine: WorkflowEngine = WorkflowEngine::new(); let start_times: Arc>> = Arc::new(parking_lot::RwLock::new(Vec::new())); let end_times: Arc>> = Arc::new(parking_lot::RwLock::new(Vec::new())); // Three steps, each taking 100ms, no dependencies // If parallel: ~100ms total // If sequential: ~300ms total let workflow = WorkflowDefinition::new("parallel_workflow", "Parallel Test") .add_step(StepDefinition::new( "step_a", "Step A", Arc::new(TimingStep { step_name: "step_a".to_string(), duration_ms: 100, start_times: Arc::clone(&start_times), end_times: Arc::clone(&end_times), }), )) .add_step(StepDefinition::new( "step_b", "Step B", Arc::new(TimingStep { step_name: "step_b".to_string(), duration_ms: 100, start_times: Arc::clone(&start_times), end_times: Arc::clone(&end_times), }), )) .add_step(StepDefinition::new( "step_c", "Step C", Arc::new(TimingStep { step_name: "step_c".to_string(), duration_ms: 100, start_times: Arc::clone(&start_times), end_times: Arc::clone(&end_times), }), )); let workflow_id = workflow.id.clone(); engine.register_workflow(workflow).unwrap(); let overall_start = std::time::Instant::now(); let instance_id = engine .start_workflow(workflow_id, TestWorkflowData::default()) .await .unwrap(); // Wait for completion - give enough time for async scheduling for _ in 0..50 { sleep(Duration::from_millis(50)).await; let state = engine.get_status(instance_id).await.unwrap(); if state.status != WorkflowStatus::Running { break; } } let overall_duration = overall_start.elapsed(); let state = engine.get_status(instance_id).await.unwrap(); assert_eq!(state.status, WorkflowStatus::Completed); // Check that all steps completed assert_eq!(end_times.read().len(), 3); // Verify parallel execution: all steps should start around the same time let starts = start_times.read(); let first_start = starts.iter().map(|(_, t)| t).min().unwrap(); let last_start = starts.iter().map(|(_, t)| t).max().unwrap(); // All starts should be within 100ms of each other (allowing for scheduling variance) let start_spread = last_start.duration_since(*first_start); assert!( start_spread < Duration::from_millis(100), "Steps did not start in parallel, spread: {:?}", start_spread ); // Total duration should be less than sequential (300ms) - use generous threshold assert!( overall_duration < Duration::from_millis(500), "Parallel execution took too long: {:?}", overall_duration ); } #[tokio::test] async fn test_dag_with_dependencies() { // DAG: A and B run in parallel, C waits for both // A ──┐ // ├──> C // B ──┘ let engine: WorkflowEngine = WorkflowEngine::new(); let start_times: Arc>> = Arc::new(parking_lot::RwLock::new(Vec::new())); let end_times: Arc>> = Arc::new(parking_lot::RwLock::new(Vec::new())); let workflow = WorkflowDefinition::new("dag_workflow", "DAG Test") .add_step(StepDefinition::new( "step_a", "Step A", Arc::new(TimingStep { step_name: "step_a".to_string(), duration_ms: 50, start_times: Arc::clone(&start_times), end_times: Arc::clone(&end_times), }), )) .add_step(StepDefinition::new( "step_b", "Step B", Arc::new(TimingStep { step_name: "step_b".to_string(), duration_ms: 100, start_times: Arc::clone(&start_times), end_times: Arc::clone(&end_times), }), )) .add_step( StepDefinition::new( "step_c", "Step C", Arc::new(TimingStep { step_name: "step_c".to_string(), duration_ms: 50, start_times: Arc::clone(&start_times), end_times: Arc::clone(&end_times), }), ) .depends_on(&["step_a", "step_b"]), ); let workflow_id = workflow.id.clone(); engine.register_workflow(workflow).unwrap(); let instance_id = engine .start_workflow(workflow_id, TestWorkflowData::default()) .await .unwrap(); // Poll until workflow completes (or timeout) for _ in 0..50 { sleep(Duration::from_millis(50)).await; let state = engine.get_status(instance_id).await.unwrap(); if state.status != WorkflowStatus::Running { break; } } let state = engine.get_status(instance_id).await.unwrap(); assert_eq!(state.status, WorkflowStatus::Completed); // Verify step C started after both A and B finished let starts = start_times.read(); let ends = end_times.read(); let c_start = starts.iter().find(|(n, _)| n == "step_c").unwrap().1; let a_end = ends.iter().find(|(n, _)| n == "step_a").unwrap().1; let b_end = ends.iter().find(|(n, _)| n == "step_b").unwrap().1; assert!(c_start >= a_end, "Step C started before Step A finished"); assert!(c_start >= b_end, "Step C started before Step B finished"); } #[tokio::test] async fn test_dag_dependency_failure_blocks_dependents() { // If step A fails with FailWorkflow, step B (depends on A) should not run let engine: WorkflowEngine = WorkflowEngine::new(); let b_executed = Arc::new(AtomicU32::new(0)); struct FailingStep; #[async_trait::async_trait] impl StepExecutor for FailingStep { async fn execute( &self, _context: &mut WorkflowContext, ) -> WorkflowResult { Err(WorkflowError::StepFailed { step_id: StepId::new("failing"), message: "Intentional failure".to_string(), }) } fn is_retryable(&self, _error: &WorkflowError) -> bool { false // Disable retries for this test } } struct TrackingStep { counter: Arc, } #[async_trait::async_trait] impl StepExecutor for TrackingStep { async fn execute( &self, _context: &mut WorkflowContext, ) -> WorkflowResult { self.counter.fetch_add(1, Ordering::SeqCst); Ok(StepResult::Success) } } let workflow = WorkflowDefinition::new("blocked_workflow", "Blocked Test") .add_step( StepDefinition::new("step_a", "Step A", Arc::new(FailingStep)) .with_failure_action(FailureAction::FailWorkflow), ) .add_step( StepDefinition::new( "step_b", "Step B", Arc::new(TrackingStep { counter: Arc::clone(&b_executed), }), ) .depends_on(&["step_a"]), ); let workflow_id = workflow.id.clone(); engine.register_workflow(workflow).unwrap(); let instance_id = engine .start_workflow(workflow_id, TestWorkflowData::default()) .await .unwrap(); // Poll until workflow completes (or timeout) for _ in 0..50 { sleep(Duration::from_millis(50)).await; let state = engine.get_status(instance_id).await.unwrap(); if state.status != WorkflowStatus::Running { break; } } let state = engine.get_status(instance_id).await.unwrap(); assert_eq!(state.status, WorkflowStatus::Failed); // Step B should not have executed assert_eq!(b_executed.load(Ordering::SeqCst), 0); } #[test] fn test_dag_validation_cycle_detection() { // Create a workflow with a cycle: A -> B -> C -> A let mut workflow = WorkflowDefinition::new("cyclic_workflow", "Cyclic Test") .add_step( StepDefinition::new("step_a", "Step A", Arc::new(AlwaysSucceedStep)) .depends_on(&["step_c"]), ) .add_step( StepDefinition::new("step_b", "Step B", Arc::new(AlwaysSucceedStep)) .depends_on(&["step_a"]), ) .add_step( StepDefinition::new("step_c", "Step C", Arc::new(AlwaysSucceedStep)) .depends_on(&["step_b"]), ); let result = workflow.validate(); assert!(result.is_err()); assert!(matches!( result.unwrap_err(), ValidationError::CycleDetected(_) )); } #[test] fn test_dag_validation_missing_dependency() { // Create a workflow with a missing dependency let mut workflow = WorkflowDefinition::new("missing_dep_workflow", "Missing Dep Test") .add_step(StepDefinition::new( "step_a", "Step A", Arc::new(AlwaysSucceedStep), )) .add_step( StepDefinition::new("step_b", "Step B", Arc::new(AlwaysSucceedStep)) .depends_on(&["nonexistent_step"]), ); let result = workflow.validate(); assert!(result.is_err()); assert!(matches!( result.unwrap_err(), ValidationError::MissingDependency { .. } )); } #[test] fn test_dag_validation_valid_workflow() { // Create a valid DAG workflow let mut workflow = WorkflowDefinition::new("valid_workflow", "Valid Test") .add_step(StepDefinition::new( "step_a", "Step A", Arc::new(AlwaysSucceedStep), )) .add_step(StepDefinition::new( "step_b", "Step B", Arc::new(AlwaysSucceedStep), )) .add_step( StepDefinition::new("step_c", "Step C", Arc::new(AlwaysSucceedStep)) .depends_on(&["step_a", "step_b"]), ) .add_step( StepDefinition::new("step_d", "Step D", Arc::new(AlwaysSucceedStep)) .depends_on(&["step_c"]), ); let result = workflow.validate(); assert!(result.is_ok()); } // ============================================================================ // Scheduled/Delayed Steps Tests (#24) // ============================================================================ #[tokio::test] async fn test_step_delay() { let engine: WorkflowEngine = WorkflowEngine::new(); // Create a workflow with a 100ms delay let workflow = WorkflowDefinition::new("delay_workflow", "Delay Test").add_step( StepDefinition::new("delayed_step", "Delayed Step", Arc::new(AlwaysSucceedStep)) .with_delay(Duration::from_millis(100)), ); let workflow_id = workflow.id.clone(); engine.register_workflow(workflow).unwrap(); let start = std::time::Instant::now(); let instance_id = engine .start_workflow(workflow_id, TestWorkflowData::default()) .await .unwrap(); // Wait for completion engine .wait_for_completion(instance_id, "test", Duration::from_secs(5)) .await .unwrap(); let duration = start.elapsed(); // Verify delay was applied (should take at least 100ms) // Note: wait_for_completion cleans up state, so we verify via timing assert!( duration >= Duration::from_millis(100), "Step delay not applied, duration: {:?}", duration ); } #[tokio::test] async fn test_step_scheduled_at() { use chrono::Utc; let engine: WorkflowEngine = WorkflowEngine::new(); // Schedule step to run 100ms in the future let scheduled_time = Utc::now() + chrono::Duration::milliseconds(100); let workflow = WorkflowDefinition::new("scheduled_workflow", "Scheduled Test").add_step( StepDefinition::new( "scheduled_step", "Scheduled Step", Arc::new(AlwaysSucceedStep), ) .scheduled_at(scheduled_time), ); let workflow_id = workflow.id.clone(); engine.register_workflow(workflow).unwrap(); let start = std::time::Instant::now(); let instance_id = engine .start_workflow(workflow_id, TestWorkflowData::default()) .await .unwrap(); // Wait for completion engine .wait_for_completion(instance_id, "test", Duration::from_secs(5)) .await .unwrap(); let duration = start.elapsed(); // Verify scheduled time was respected (should take at least 100ms) // Note: wait_for_completion cleans up state, so we verify via timing assert!( duration >= Duration::from_millis(100), "Scheduled time not respected, duration: {:?}", duration ); } // ============================================================================ // Conditional Branching Tests (#25) // ============================================================================ #[tokio::test] async fn test_run_if_true() { let engine: WorkflowEngine = WorkflowEngine::new(); let executed = Arc::new(AtomicU32::new(0)); let executed_clone = Arc::clone(&executed); struct TrackingStep { counter: Arc, } #[async_trait::async_trait] impl StepExecutor for TrackingStep { async fn execute( &self, _context: &mut WorkflowContext, ) -> WorkflowResult { self.counter.fetch_add(1, Ordering::SeqCst); Ok(StepResult::Success) } } // Step with run_if that always returns true let workflow = WorkflowDefinition::new("run_if_true_workflow", "Run If True Test").add_step( StepDefinition::new( "conditional_step", "Conditional Step", Arc::new(TrackingStep { counter: executed }), ) .run_if(|_ctx| true), ); let workflow_id = workflow.id.clone(); engine.register_workflow(workflow).unwrap(); let instance_id = engine .start_workflow(workflow_id, TestWorkflowData::default()) .await .unwrap(); engine .wait_for_completion(instance_id, "test", Duration::from_secs(5)) .await .unwrap(); // Step should have executed (condition was true) // Note: wait_for_completion cleans up state, so we verify via counter assert_eq!(executed_clone.load(Ordering::SeqCst), 1); } #[tokio::test] async fn test_run_if_false() { use tokio::time::sleep; let engine: WorkflowEngine = WorkflowEngine::new(); let executed = Arc::new(AtomicU32::new(0)); let executed_clone = Arc::clone(&executed); struct TrackingStep { counter: Arc, } #[async_trait::async_trait] impl StepExecutor for TrackingStep { async fn execute( &self, _context: &mut WorkflowContext, ) -> WorkflowResult { self.counter.fetch_add(1, Ordering::SeqCst); Ok(StepResult::Success) } } // Step with run_if that always returns false let workflow = WorkflowDefinition::new("run_if_false_workflow", "Run If False Test").add_step( StepDefinition::new( "conditional_step", "Conditional Step", Arc::new(TrackingStep { counter: executed }), ) .run_if(|_ctx| false), ); let workflow_id = workflow.id.clone(); engine.register_workflow(workflow).unwrap(); let instance_id = engine .start_workflow(workflow_id, TestWorkflowData::default()) .await .unwrap(); // Use polling to check status (don't use wait_for_completion which cleans up state) let mut state = engine.get_status(instance_id).await.unwrap(); for _ in 0..50 { if state.status != WorkflowStatus::Running && state.status != WorkflowStatus::Pending { break; } sleep(Duration::from_millis(50)).await; state = engine.get_status(instance_id).await.unwrap(); } assert_eq!(state.status, WorkflowStatus::Completed); // Step should NOT have executed (skipped due to run_if) assert_eq!(executed_clone.load(Ordering::SeqCst), 0); // Verify step was marked as skipped let step_state = state .step_states .get(&StepId::new("conditional_step")) .unwrap(); assert_eq!(step_state.status, StepStatus::Skipped); } #[tokio::test] async fn test_run_if_context_based() { let engine: WorkflowEngine = WorkflowEngine::new(); // Step that sets test_key in context struct SetKeyStep; #[async_trait::async_trait] impl StepExecutor for SetKeyStep { async fn execute( &self, context: &mut WorkflowContext, ) -> WorkflowResult { context.data.test_key = Some("execute_next".to_string()); Ok(StepResult::Success) } } let executed = Arc::new(AtomicU32::new(0)); let executed_clone = Arc::clone(&executed); struct TrackingStep { counter: Arc, } #[async_trait::async_trait] impl StepExecutor for TrackingStep { async fn execute( &self, _context: &mut WorkflowContext, ) -> WorkflowResult { self.counter.fetch_add(1, Ordering::SeqCst); Ok(StepResult::Success) } } // Workflow where second step only runs if first step sets the right key let workflow = WorkflowDefinition::new("context_run_if_workflow", "Context Run If Test") .add_step(StepDefinition::new( "set_key_step", "Set Key", Arc::new(SetKeyStep), )) .add_step( StepDefinition::new( "conditional_step", "Conditional Step", Arc::new(TrackingStep { counter: executed }), ) .depends_on(&["set_key_step"]) .run_if(|ctx| ctx.data.test_key.as_deref() == Some("execute_next")), ); let workflow_id = workflow.id.clone(); engine.register_workflow(workflow).unwrap(); let instance_id = engine .start_workflow(workflow_id, TestWorkflowData::default()) .await .unwrap(); engine .wait_for_completion(instance_id, "test", Duration::from_secs(5)) .await .unwrap(); // Step should have executed because context had the right value // Note: wait_for_completion cleans up state, so we verify via counter assert_eq!(executed_clone.load(Ordering::SeqCst), 1); } #[tokio::test] async fn test_depends_on_any() { // DAG: A and B run in parallel, C waits for ANY (not both) // A ──┐ // ├──> C (any_of) // B ──┘ let engine: WorkflowEngine = WorkflowEngine::new(); let start_times: Arc>> = Arc::new(parking_lot::RwLock::new(Vec::new())); let end_times: Arc>> = Arc::new(parking_lot::RwLock::new(Vec::new())); // A takes 50ms, B takes 200ms // C should start after A finishes (not wait for B) let workflow = WorkflowDefinition::new("depends_on_any_workflow", "Depends On Any Test") .add_step(StepDefinition::new( "step_a", "Step A", Arc::new(TimingStep { step_name: "step_a".to_string(), duration_ms: 50, start_times: Arc::clone(&start_times), end_times: Arc::clone(&end_times), }), )) .add_step(StepDefinition::new( "step_b", "Step B", Arc::new(TimingStep { step_name: "step_b".to_string(), duration_ms: 200, start_times: Arc::clone(&start_times), end_times: Arc::clone(&end_times), }), )) .add_step( StepDefinition::new( "step_c", "Step C", Arc::new(TimingStep { step_name: "step_c".to_string(), duration_ms: 50, start_times: Arc::clone(&start_times), end_times: Arc::clone(&end_times), }), ) .depends_on_any(&["step_a", "step_b"]), ); let workflow_id = workflow.id.clone(); engine.register_workflow(workflow).unwrap(); let instance_id = engine .start_workflow(workflow_id, TestWorkflowData::default()) .await .unwrap(); engine .wait_for_completion(instance_id, "test", Duration::from_secs(5)) .await .unwrap(); // Verify step C started after A finished but before B finished // Note: wait_for_completion cleans up state, so we verify via timing let starts = start_times.read(); let ends = end_times.read(); let c_start = starts.iter().find(|(n, _)| n == "step_c").unwrap().1; let a_end = ends.iter().find(|(n, _)| n == "step_a").unwrap().1; let b_end = ends.iter().find(|(n, _)| n == "step_b").unwrap().1; assert!( c_start >= a_end, "Step C should start after Step A finishes" ); assert!( c_start < b_end, "Step C should start before Step B finishes (any_of semantics)" ); } #[tokio::test] async fn test_depends_on_any_combined_with_depends_on() { // DAG: C requires ALL of [A] AND ANY of [B, D] // A takes 50ms, B takes 100ms, D takes 200ms // C should start after A AND (B or D) complete let engine: WorkflowEngine = WorkflowEngine::new(); let start_times: Arc>> = Arc::new(parking_lot::RwLock::new(Vec::new())); let end_times: Arc>> = Arc::new(parking_lot::RwLock::new(Vec::new())); let workflow = WorkflowDefinition::new("combined_deps_workflow", "Combined Dependencies Test") .add_step(StepDefinition::new( "step_a", "Step A", Arc::new(TimingStep { step_name: "step_a".to_string(), duration_ms: 50, start_times: Arc::clone(&start_times), end_times: Arc::clone(&end_times), }), )) .add_step(StepDefinition::new( "step_b", "Step B", Arc::new(TimingStep { step_name: "step_b".to_string(), duration_ms: 100, start_times: Arc::clone(&start_times), end_times: Arc::clone(&end_times), }), )) .add_step(StepDefinition::new( "step_d", "Step D", Arc::new(TimingStep { step_name: "step_d".to_string(), duration_ms: 200, start_times: Arc::clone(&start_times), end_times: Arc::clone(&end_times), }), )) .add_step( StepDefinition::new( "step_c", "Step C", Arc::new(TimingStep { step_name: "step_c".to_string(), duration_ms: 50, start_times: Arc::clone(&start_times), end_times: Arc::clone(&end_times), }), ) .depends_on(&["step_a"]) // Must wait for A .depends_on_any(&["step_b", "step_d"]), // AND any of B or D ); let workflow_id = workflow.id.clone(); engine.register_workflow(workflow).unwrap(); let instance_id = engine .start_workflow(workflow_id, TestWorkflowData::default()) .await .unwrap(); engine .wait_for_completion(instance_id, "test", Duration::from_secs(5)) .await .unwrap(); // Verify step C started after both A AND B finished // (B finishes at 100ms, which is after A at 50ms) // Note: wait_for_completion cleans up state, so we verify via timing let starts = start_times.read(); let ends = end_times.read(); let c_start = starts.iter().find(|(n, _)| n == "step_c").unwrap().1; let a_end = ends.iter().find(|(n, _)| n == "step_a").unwrap().1; let b_end = ends.iter().find(|(n, _)| n == "step_b").unwrap().1; let d_end = ends.iter().find(|(n, _)| n == "step_d").unwrap().1; assert!( c_start >= a_end, "Step C should start after Step A (depends_on)" ); assert!( c_start >= b_end || c_start >= d_end, "Step C should start after at least one of B or D (depends_on_any)" ); // Since B finishes first (100ms) and A finishes before B, C should start around 100ms assert!( c_start < d_end, "Step C should start before D finishes (any_of semantics)" ); } #[test] fn test_dag_validation_depends_on_any_missing() { // Create a workflow with a missing depends_on_any dependency let mut workflow = WorkflowDefinition::new("missing_any_dep_workflow", "Missing Any Dep Test") .add_step(StepDefinition::new( "step_a", "Step A", Arc::new(AlwaysSucceedStep), )) .add_step( StepDefinition::new("step_b", "Step B", Arc::new(AlwaysSucceedStep)) .depends_on_any(&["nonexistent_step"]), ); let result = workflow.validate(); assert!(result.is_err()); assert!(matches!( result.unwrap_err(), ValidationError::MissingDependency { .. } )); } #[tokio::test] async fn test_depends_on_any_all_fail() { // When ALL depends_on_any dependencies fail, the step should be blocked // Workflow: A and B both fail, C depends_on_any([A, B]) // Expected: C should not run, workflow should fail use tokio::time::sleep; let engine: WorkflowEngine = WorkflowEngine::new(); let c_executed = Arc::new(AtomicU32::new(0)); let c_executed_clone = Arc::clone(&c_executed); struct TrackingStep { counter: Arc, } #[async_trait::async_trait] impl StepExecutor for TrackingStep { async fn execute( &self, _context: &mut WorkflowContext, ) -> WorkflowResult { self.counter.fetch_add(1, Ordering::SeqCst); Ok(StepResult::Success) } } let workflow = WorkflowDefinition::new("all_any_fail_workflow", "All Any Fail Test") .add_step(StepDefinition::new( "step_a", "Step A (fails)", Arc::new(AlwaysFailStep), )) .add_step(StepDefinition::new( "step_b", "Step B (fails)", Arc::new(AlwaysFailStep), )) .add_step( StepDefinition::new( "step_c", "Step C (depends on any of A, B)", Arc::new(TrackingStep { counter: c_executed, }), ) .depends_on_any(&["step_a", "step_b"]), ); let workflow_id = workflow.id.clone(); engine.register_workflow(workflow).unwrap(); let instance_id = engine .start_workflow(workflow_id, TestWorkflowData::default()) .await .unwrap(); // Use polling to check status let mut state = engine.get_status(instance_id).await.unwrap(); for _ in 0..50 { if state.status != WorkflowStatus::Running && state.status != WorkflowStatus::Pending { break; } sleep(Duration::from_millis(50)).await; state = engine.get_status(instance_id).await.unwrap(); } // Workflow should have failed (because all depends_on_any deps failed) assert_eq!(state.status, WorkflowStatus::Failed); // Step C should NOT have executed assert_eq!(c_executed_clone.load(Ordering::SeqCst), 0); } #[tokio::test] async fn test_depends_on_any_one_fails_one_succeeds() { // When only SOME depends_on_any dependencies fail (but at least one succeeds), // the step should still run // Workflow: A fails, B succeeds, C depends_on_any([A, B]) // Expected: C should run (B succeeded) use tokio::time::sleep; let engine: WorkflowEngine = WorkflowEngine::new(); let c_executed = Arc::new(AtomicU32::new(0)); let c_executed_clone = Arc::clone(&c_executed); struct TrackingStep { counter: Arc, } #[async_trait::async_trait] impl StepExecutor for TrackingStep { async fn execute( &self, _context: &mut WorkflowContext, ) -> WorkflowResult { self.counter.fetch_add(1, Ordering::SeqCst); Ok(StepResult::Success) } } // Note: Step A uses ContinueNextStep so its failure doesn't fail the workflow. // This is the correct way to model "any of" semantics where a failing path // shouldn't fail the entire workflow if another path succeeds. let workflow = WorkflowDefinition::new("one_any_fail_workflow", "One Any Fail Test") .add_step( StepDefinition::new("step_a", "Step A (fails)", Arc::new(AlwaysFailStep)) .with_failure_action(FailureAction::ContinueNextStep), ) .add_step(StepDefinition::new( "step_b", "Step B (succeeds)", Arc::new(AlwaysSucceedStep), )) .add_step( StepDefinition::new( "step_c", "Step C (depends on any of A, B)", Arc::new(TrackingStep { counter: c_executed, }), ) .depends_on_any(&["step_a", "step_b"]), ); let workflow_id = workflow.id.clone(); engine.register_workflow(workflow).unwrap(); let instance_id = engine .start_workflow(workflow_id, TestWorkflowData::default()) .await .unwrap(); // Use polling to check status let mut state = engine.get_status(instance_id).await.unwrap(); for _ in 0..50 { if state.status != WorkflowStatus::Running && state.status != WorkflowStatus::Pending { break; } sleep(Duration::from_millis(50)).await; state = engine.get_status(instance_id).await.unwrap(); } // Workflow should have completed (B succeeded, so C could run) assert_eq!(state.status, WorkflowStatus::Completed); // Step C SHOULD have executed (because B succeeded) assert_eq!(c_executed_clone.load(Ordering::SeqCst), 1); }