236 lines
6.8 KiB
Rust
236 lines
6.8 KiB
Rust
|
|
// 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<Phase>,
|
||
|
|
pub created_at: DateTime<Utc>,
|
||
|
|
pub started_at: Option<DateTime<Utc>>,
|
||
|
|
pub completed_at: Option<DateTime<Utc>>,
|
||
|
|
pub estimated_completion: Option<DateTime<Utc>>,
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
|
|
pub struct Phase {
|
||
|
|
pub id: String,
|
||
|
|
pub name: String,
|
||
|
|
pub status: StepStatus,
|
||
|
|
pub steps: Vec<WorkflowStep>,
|
||
|
|
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<String>,
|
||
|
|
pub can_parallelize: bool,
|
||
|
|
pub started_at: Option<DateTime<Utc>>,
|
||
|
|
pub completed_at: Option<DateTime<Utc>>,
|
||
|
|
pub result: Option<String>,
|
||
|
|
pub error: Option<String>,
|
||
|
|
}
|
||
|
|
|
||
|
|
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<Phase>) -> 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 {
|
||
|
|
match (&self.status, to) {
|
||
|
|
(WorkflowStatus::Created, WorkflowStatus::Planning) => true,
|
||
|
|
(WorkflowStatus::Planning, WorkflowStatus::InProgress) => true,
|
||
|
|
(WorkflowStatus::InProgress, WorkflowStatus::Completed) => true,
|
||
|
|
(WorkflowStatus::InProgress, WorkflowStatus::Failed) => true,
|
||
|
|
(WorkflowStatus::InProgress, WorkflowStatus::Blocked) => true,
|
||
|
|
(WorkflowStatus::Blocked, WorkflowStatus::InProgress) => true,
|
||
|
|
(WorkflowStatus::Failed, WorkflowStatus::RolledBack) => true,
|
||
|
|
_ => false,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// 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);
|
||
|
|
}
|
||
|
|
}
|