From 364dfd9da3fe448ee3f0a790770fc8324af56a0c Mon Sep 17 00:00:00 2001 From: Shunsuke Hayashi Date: Sun, 23 Nov 2025 00:28:39 +0900 Subject: [PATCH] test: Add comprehensive Workflow and DAG tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Workflow tests (14 new tests): - Test StepCondition, RetryConfig, FailurePolicy defaults - Test StepStatus and WorkflowStatus equality - Test WorkflowContext with variables and results - Test StepResult and WorkflowResult creation - Test Workflow with output variables - Test step and failure policy variants DAG tests (23 new tests): - Test TaskId creation and display - Test Task with priority and estimated time - Test TaskNode operations (add/remove dependencies) - Test TaskLevel creation and management - Test TaskGraph operations (count, parallelism, levels) - Test TaskGraphBuilder with priority - Test DAGError variants - Test complex parallel graphs Total: 730 tests passing (228 core, 498 TUI, 4 CLI) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- crates/miyabi-core/src/dag.rs | 261 +++++++++++++++++++++++++++++ crates/miyabi-core/src/workflow.rs | 200 ++++++++++++++++++++++ 2 files changed, 461 insertions(+) diff --git a/crates/miyabi-core/src/dag.rs b/crates/miyabi-core/src/dag.rs index deba411..0f30446 100644 --- a/crates/miyabi-core/src/dag.rs +++ b/crates/miyabi-core/src/dag.rs @@ -559,4 +559,265 @@ mod tests { assert!(roots.contains(&id1)); assert!(roots.contains(&id2)); } + + #[test] + fn test_task_id_default() { + let id = TaskId::default(); + assert!(!id.to_string().is_empty()); + } + + #[test] + fn test_task_with_priority() { + let task = Task::new("priority-task", "Task with priority") + .with_priority(10); + + assert_eq!(task.priority, 10); + assert_eq!(task.name, "priority-task"); + } + + #[test] + fn test_task_with_estimated_time() { + let task = Task::new("timed-task", "Task with time estimate") + .with_estimated_time(120); + + assert_eq!(task.estimated_time, Some(120)); + } + + #[test] + fn test_task_node_is_root() { + let task = Task::new("root-task", "Root task"); + let node = TaskNode::new(task); + + assert!(node.is_root()); + assert_eq!(node.indegree(), 0); + } + + #[test] + fn test_task_node_add_dependency() { + let task1 = Task::new("task1", "First task"); + let task2 = Task::new("task2", "Second task"); + let mut node = TaskNode::new(task1); + let dep_id = task2.id; + + node.add_dependency(dep_id); + assert!(!node.is_root()); + assert_eq!(node.indegree(), 1); + assert!(node.dependencies.contains(&dep_id)); + } + + #[test] + fn test_task_node_add_dependent() { + let task1 = Task::new("task1", "First task"); + let task2 = Task::new("task2", "Second task"); + let mut node = TaskNode::new(task1); + let dependent_id = task2.id; + + node.add_dependent(dependent_id); + assert!(node.dependents.contains(&dependent_id)); + } + + #[test] + fn test_task_node_no_duplicate_dependencies() { + let task1 = Task::new("task1", "First task"); + let task2 = Task::new("task2", "Second task"); + let mut node = TaskNode::new(task1); + let dep_id = task2.id; + + node.add_dependency(dep_id); + node.add_dependency(dep_id); // Add again + + assert_eq!(node.dependencies.len(), 1); + } + + #[test] + fn test_task_level_creation() { + let level = TaskLevel::new(0); + assert_eq!(level.level, 0); + assert!(level.is_empty()); + assert_eq!(level.task_count(), 0); + } + + #[test] + fn test_task_level_add_task() { + let mut level = TaskLevel::new(1); + let task = Task::new("task1", "First task"); + + level.add_task(task); + assert!(!level.is_empty()); + assert_eq!(level.task_count(), 1); + } + + #[test] + fn test_graph_task_count() { + let mut graph = TaskGraph::new(4); + assert_eq!(graph.task_count(), 0); + + let task = Task::new("task", "Test task"); + graph.add_task(task); + assert_eq!(graph.task_count(), 1); + } + + #[test] + fn test_graph_max_parallelism() { + let graph = TaskGraph::new(8); + assert_eq!(graph.max_parallelism(), 8); + } + + #[test] + fn test_graph_get_node() { + let mut graph = TaskGraph::new(4); + let task = Task::new("test", "Test task"); + let id = graph.add_task(task); + + let node = graph.get_node(&id); + assert!(node.is_some()); + assert_eq!(node.unwrap().task.name, "test"); + } + + #[test] + fn test_graph_task_ids() { + let mut graph = TaskGraph::new(4); + let task1 = Task::new("task1", "First"); + let task2 = Task::new("task2", "Second"); + + let id1 = graph.add_task(task1); + let id2 = graph.add_task(task2); + + let ids = graph.task_ids(); + assert_eq!(ids.len(), 2); + assert!(ids.contains(&id1)); + assert!(ids.contains(&id2)); + } + + #[test] + fn test_graph_levels() { + let mut graph = TaskGraph::new(4); + + let task1 = Task::new("task1", "First"); + let task2 = Task::new("task2", "Second"); + + let id1 = graph.add_task(task1); + let id2 = graph.add_task(task2); + + graph.add_dependency(id2, id1).unwrap(); + graph.build_levels().unwrap(); + + let levels = graph.levels(); + assert!(!levels.is_empty()); + // Tasks are added to levels as their dependencies are satisfied + assert!(graph.level_count() >= 1); + } + + #[test] + fn test_builder_with_priority() { + let graph = TaskGraphBuilder::new(4) + .add_task_with_priority("high-priority", "Important task", 100) + .build() + .unwrap(); + + assert_eq!(graph.task_count(), 1); + } + + #[test] + fn test_dag_error_task_not_found() { + let error = DAGError::TaskNotFound("missing-task".to_string()); + let display = format!("{}", error); + assert!(display.contains("missing-task")); + } + + #[test] + fn test_dag_error_circular_dependency() { + let error = DAGError::CircularDependency("cycle detected".to_string()); + let display = format!("{}", error); + assert!(display.contains("cycle")); + } + + #[test] + fn test_dag_error_invalid_graph() { + let error = DAGError::InvalidGraph("invalid".to_string()); + let display = format!("{}", error); + assert!(display.contains("invalid")); + } + + #[test] + fn test_complex_parallel_graph() { + // Create a graph with multiple parallel paths + // Root -> [A, B, C] -> Merge + let mut graph = TaskGraph::new(4); + + let root = Task::new("root", "Root task"); + let a = Task::new("a", "Task A"); + let b = Task::new("b", "Task B"); + let c = Task::new("c", "Task C"); + let merge = Task::new("merge", "Merge task"); + + let root_id = graph.add_task(root); + let a_id = graph.add_task(a); + let b_id = graph.add_task(b); + let c_id = graph.add_task(c); + let merge_id = graph.add_task(merge); + + graph.add_dependency(a_id, root_id).unwrap(); + graph.add_dependency(b_id, root_id).unwrap(); + graph.add_dependency(c_id, root_id).unwrap(); + graph.add_dependency(merge_id, a_id).unwrap(); + graph.add_dependency(merge_id, b_id).unwrap(); + graph.add_dependency(merge_id, c_id).unwrap(); + + graph.build_levels().unwrap(); + + // All 5 tasks should be assigned to levels + assert!(graph.level_count() >= 1); + let total_tasks: usize = graph.levels().iter().map(|l| l.task_count()).sum(); + assert_eq!(total_tasks, 5); + } + + #[test] + fn test_builder_dependency_error() { + let result = TaskGraphBuilder::new(4) + .add_task("task1", "First") + .add_dependency("task1", "nonexistent"); + + assert!(result.is_err()); + } + + #[test] + fn test_single_task_graph() { + let mut graph = TaskGraph::new(4); + let task = Task::new("single", "Single task"); + graph.add_task(task); + + graph.build_levels().unwrap(); + + assert_eq!(graph.level_count(), 1); + assert_eq!(graph.levels()[0].task_count(), 1); + } + + #[test] + fn test_task_id_display() { + let id = TaskId::new(); + let display = id.to_string(); + + // Should be a valid UUID string + assert!(!display.is_empty()); + assert!(display.contains('-')); // UUIDs contain hyphens + } + + #[test] + fn test_independent_tasks_parallel() { + // All independent tasks should be in the same level + let mut graph = TaskGraph::new(10); + + for i in 0..5 { + let task = Task::new(format!("task{}", i), format!("Task {}", i)); + graph.add_task(task); + } + + graph.build_levels().unwrap(); + + // All tasks are independent, so they should be in level 0 + // (with max_parallelism=10, they can all run in parallel) + assert_eq!(graph.level_count(), 1); + assert_eq!(graph.levels()[0].task_count(), 5); + } } diff --git a/crates/miyabi-core/src/workflow.rs b/crates/miyabi-core/src/workflow.rs index 414e573..36e65a1 100644 --- a/crates/miyabi-core/src/workflow.rs +++ b/crates/miyabi-core/src/workflow.rs @@ -691,4 +691,204 @@ mod tests { assert_eq!(result.status, WorkflowStatus::Completed); assert_eq!(result.steps.len(), 2); } + + #[test] + fn test_step_condition_default() { + let condition = StepCondition::default(); + assert!(matches!(condition, StepCondition::Always)); + } + + #[test] + fn test_retry_config_default() { + let config = RetryConfig::default(); + assert_eq!(config.max_attempts, 1); + assert_eq!(config.delay, 5); + } + + #[test] + fn test_failure_policy_default() { + let policy = FailurePolicy::default(); + assert!(matches!(policy, FailurePolicy::Stop)); + } + + #[test] + fn test_step_status_equality() { + assert_eq!(StepStatus::Pending, StepStatus::Pending); + assert_eq!(StepStatus::Completed, StepStatus::Completed); + assert_ne!(StepStatus::Pending, StepStatus::Completed); + assert_ne!(StepStatus::Failed, StepStatus::Skipped); + } + + #[test] + fn test_workflow_status_equality() { + assert_eq!(WorkflowStatus::Completed, WorkflowStatus::Completed); + assert_ne!(WorkflowStatus::Completed, WorkflowStatus::Failed); + } + + #[test] + fn test_workflow_context_with_variables() { + let mut vars = HashMap::new(); + vars.insert("key1".to_string(), "value1".to_string()); + vars.insert("key2".to_string(), "value2".to_string()); + + let context = WorkflowContext::new().with_variables(vars); + assert_eq!(context.get_variable("key1"), Some(&"value1".to_string())); + assert_eq!(context.get_variable("key2"), Some(&"value2".to_string())); + } + + #[test] + fn test_workflow_context_add_result() { + let mut context = WorkflowContext::new(); + let result = StepResult { + step_id: "test-step".to_string(), + status: StepStatus::Completed, + output: Some("output".to_string()), + error: None, + duration_ms: 100, + }; + + context.add_result(result); + let retrieved = context.get_result("test-step"); + assert!(retrieved.is_some()); + assert_eq!(retrieved.unwrap().status, StepStatus::Completed); + } + + #[test] + fn test_step_result_creation() { + let result = StepResult { + step_id: "test-step".to_string(), + status: StepStatus::Completed, + output: Some("test output".to_string()), + error: None, + duration_ms: 150, + }; + + assert_eq!(result.step_id, "test-step"); + assert_eq!(result.status, StepStatus::Completed); + assert_eq!(result.output, Some("test output".to_string())); + assert!(result.error.is_none()); + assert_eq!(result.duration_ms, 150); + } + + #[test] + fn test_workflow_result_creation() { + let result = WorkflowResult { + workflow_name: "test-workflow".to_string(), + status: WorkflowStatus::Completed, + steps: vec![], + duration_ms: 1000, + }; + + assert_eq!(result.workflow_name, "test-workflow"); + assert_eq!(result.status, WorkflowStatus::Completed); + assert!(result.steps.is_empty()); + assert_eq!(result.duration_ms, 1000); + } + + #[tokio::test] + async fn test_workflow_with_output_variables() { + let mut manager = WorkflowManager::new(); + let workflow = Workflow { + name: "output-test".to_string(), + description: "Test output variables".to_string(), + steps: vec![ + WorkflowStep { + id: "step1".to_string(), + name: "Step 1".to_string(), + agent: None, + task: "First task".to_string(), + depends_on: vec![], + condition: None, + timeout: 30, + retry: RetryConfig::default(), + output: Some("first_result".to_string()), + }, + WorkflowStep { + id: "step2".to_string(), + name: "Step 2".to_string(), + agent: None, + task: "Use ${first_result}".to_string(), + depends_on: vec!["step1".to_string()], + condition: None, + timeout: 30, + retry: RetryConfig::default(), + output: None, + }, + ], + variables: HashMap::new(), + on_failure: FailurePolicy::Stop, + }; + + manager.register(workflow); + let result = manager.execute("output-test", HashMap::new()).await.unwrap(); + + assert_eq!(result.status, WorkflowStatus::Completed); + assert_eq!(result.steps.len(), 2); + } + + #[test] + fn test_workflow_with_global_variables() { + let mut vars = HashMap::new(); + vars.insert("project".to_string(), "miyabi".to_string()); + + let workflow = Workflow { + name: "vars-test".to_string(), + description: "Test global variables".to_string(), + steps: vec![], + variables: vars, + on_failure: FailurePolicy::Stop, + }; + + assert_eq!(workflow.variables.get("project"), Some(&"miyabi".to_string())); + } + + #[test] + fn test_workflow_step_with_all_fields() { + let step = WorkflowStep { + id: "complete-step".to_string(), + name: "Complete Step".to_string(), + agent: Some("test-agent".to_string()), + task: "Complete task".to_string(), + depends_on: vec!["dep1".to_string(), "dep2".to_string()], + condition: Some(StepCondition::OnSuccess), + timeout: 600, + retry: RetryConfig { + max_attempts: 3, + delay: 10, + }, + output: Some("result".to_string()), + }; + + assert_eq!(step.id, "complete-step"); + assert_eq!(step.agent, Some("test-agent".to_string())); + assert_eq!(step.depends_on.len(), 2); + assert_eq!(step.timeout, 600); + assert_eq!(step.retry.max_attempts, 3); + } + + #[test] + fn test_step_condition_variants() { + let always = StepCondition::Always; + let on_success = StepCondition::OnSuccess; + let on_failure = StepCondition::OnFailure; + let if_expr = StepCondition::If { + expression: "true".to_string(), + }; + + assert!(matches!(always, StepCondition::Always)); + assert!(matches!(on_success, StepCondition::OnSuccess)); + assert!(matches!(on_failure, StepCondition::OnFailure)); + assert!(matches!(if_expr, StepCondition::If { .. })); + } + + #[test] + fn test_failure_policy_variants() { + let stop = FailurePolicy::Stop; + let continue_policy = FailurePolicy::Continue; + let cleanup = FailurePolicy::Cleanup; + + assert!(matches!(stop, FailurePolicy::Stop)); + assert!(matches!(continue_policy, FailurePolicy::Continue)); + assert!(matches!(cleanup, FailurePolicy::Cleanup)); + } }