// vapora-backend: Workflow state machine // Phase 3: State management for workflow lifecycle use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub enum WorkflowStatus { Created, Planning, InProgress, Blocked, Completed, Failed, RolledBack, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub enum StepStatus { Pending, Running, Completed, Failed, Skipped, Blocked, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Workflow { pub id: String, pub title: String, pub status: WorkflowStatus, pub phases: Vec, pub created_at: DateTime, pub started_at: Option>, pub completed_at: Option>, pub estimated_completion: Option>, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Phase { pub id: String, pub name: String, pub status: StepStatus, pub steps: Vec, pub parallel: bool, pub estimated_hours: f32, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct WorkflowStep { pub id: String, pub name: String, pub agent_role: String, pub status: StepStatus, pub depends_on: Vec, pub can_parallelize: bool, pub started_at: Option>, pub completed_at: Option>, pub result: Option, pub error: Option, } impl Default for WorkflowStep { fn default() -> Self { Self { id: String::new(), name: String::new(), agent_role: String::new(), status: StepStatus::Pending, depends_on: Vec::new(), can_parallelize: false, started_at: None, completed_at: None, result: None, error: None, } } } impl Workflow { /// Create a new workflow pub fn new(id: String, title: String, phases: Vec) -> Self { Self { id, title, status: WorkflowStatus::Created, phases, created_at: Utc::now(), started_at: None, completed_at: None, estimated_completion: None, } } /// Check if transition is allowed pub fn can_transition(&self, to: &WorkflowStatus) -> bool { matches!( (&self.status, to), (WorkflowStatus::Created, WorkflowStatus::Planning) | (WorkflowStatus::Planning, WorkflowStatus::InProgress) | (WorkflowStatus::InProgress, WorkflowStatus::Completed) | (WorkflowStatus::InProgress, WorkflowStatus::Failed) | (WorkflowStatus::InProgress, WorkflowStatus::Blocked) | (WorkflowStatus::Blocked, WorkflowStatus::InProgress) | (WorkflowStatus::Failed, WorkflowStatus::RolledBack) ) } /// Transition to new state pub fn transition(&mut self, to: WorkflowStatus) -> Result<(), String> { if !self.can_transition(&to) { return Err(format!( "Cannot transition from {:?} to {:?}", self.status, to )); } match &to { WorkflowStatus::InProgress => { self.started_at = Some(Utc::now()); } WorkflowStatus::Completed | WorkflowStatus::Failed | WorkflowStatus::RolledBack => { self.completed_at = Some(Utc::now()); } _ => {} } self.status = to; Ok(()) } /// Check if all steps are completed pub fn all_steps_completed(&self) -> bool { self.phases.iter().all(|p| { p.steps .iter() .all(|s| matches!(s.status, StepStatus::Completed | StepStatus::Skipped)) }) } /// Check if any step has failed pub fn any_step_failed(&self) -> bool { self.phases.iter().any(|p| { p.steps .iter() .any(|s| matches!(s.status, StepStatus::Failed)) }) } /// Get workflow progress percentage pub fn progress_percent(&self) -> u32 { let total_steps: usize = self.phases.iter().map(|p| p.steps.len()).sum(); if total_steps == 0 { return 0; } let completed_steps: usize = self .phases .iter() .flat_map(|p| &p.steps) .filter(|s| matches!(s.status, StepStatus::Completed | StepStatus::Skipped)) .count(); ((completed_steps as f64 / total_steps as f64) * 100.0) as u32 } } #[cfg(test)] mod tests { use super::*; #[test] fn test_workflow_creation() { let workflow = Workflow::new("wf-1".to_string(), "Test Workflow".to_string(), vec![]); assert_eq!(workflow.id, "wf-1"); assert_eq!(workflow.status, WorkflowStatus::Created); assert!(workflow.started_at.is_none()); } #[test] fn test_valid_transitions() { let mut workflow = Workflow::new("wf-1".to_string(), "Test".to_string(), vec![]); assert!(workflow.transition(WorkflowStatus::Planning).is_ok()); assert_eq!(workflow.status, WorkflowStatus::Planning); assert!(workflow.transition(WorkflowStatus::InProgress).is_ok()); assert_eq!(workflow.status, WorkflowStatus::InProgress); assert!(workflow.started_at.is_some()); assert!(workflow.transition(WorkflowStatus::Completed).is_ok()); assert_eq!(workflow.status, WorkflowStatus::Completed); assert!(workflow.completed_at.is_some()); } #[test] fn test_invalid_transition() { let mut workflow = Workflow::new("wf-1".to_string(), "Test".to_string(), vec![]); let result = workflow.transition(WorkflowStatus::Completed); assert!(result.is_err()); } #[test] fn test_progress_calculation() { let mut workflow = Workflow::new( "wf-1".to_string(), "Test".to_string(), vec![Phase { id: "p1".to_string(), name: "Phase 1".to_string(), status: StepStatus::Running, parallel: false, estimated_hours: 2.0, steps: vec![ WorkflowStep { id: "s1".to_string(), status: StepStatus::Completed, ..Default::default() }, WorkflowStep { id: "s2".to_string(), status: StepStatus::Running, ..Default::default() }, ], }], ); assert_eq!(workflow.progress_percent(), 50); workflow.phases[0].steps[1].status = StepStatus::Completed; assert_eq!(workflow.progress_percent(), 100); } }