Jesús Pérez fe4d138a14
Some checks failed
Rust CI / Security Audit (push) Has been cancelled
Rust CI / Check + Test + Lint (nightly) (push) Has been cancelled
Rust CI / Check + Test + Lint (stable) (push) Has been cancelled
feat: CLI arguments, distribution management, and approval gates
- Add CLI support (--config, --help) with env var override for backend/agents
  - Implement distro justfile recipes: list-targets, install-targets, build-target, install
  - Fix OpenTelemetry API incompatibilities and remove deprecated calls
  - Add tokio "time" feature for timeout support
  - Fix Cargo profile warnings and Nushell script syntax
  - Update all dead_code warnings with strategic annotations
  - Zero compiler warnings in vapora codebase
  - Comprehensive CHANGELOG documenting risk-based approval gates system
2026-02-03 21:35:00 +00:00

239 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,
}
}
}
#[allow(dead_code)]
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 {
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);
}
}