Skip to main content

voirs_cli/workflow/
state.rs

1//! Workflow State Management
2//!
3//! Manages persistent state for workflow execution, enabling resume capability.
4
5use super::executor::StepResult;
6use crate::error::Result;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::path::PathBuf;
10
11/// Execution state enum
12#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
13pub enum ExecutionState {
14    /// Not started
15    Pending,
16    /// Currently running
17    Running,
18    /// Completed successfully
19    Completed,
20    /// Failed
21    Failed,
22    /// Stopped by user
23    Stopped,
24    /// Paused (for future use)
25    Paused,
26}
27
28/// Workflow state for persistence
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct WorkflowState {
31    /// Workflow name
32    pub workflow_name: String,
33    /// Current execution state
34    pub state: ExecutionState,
35    /// Current variables
36    pub variables: HashMap<String, serde_json::Value>,
37    /// Completed steps
38    pub completed_steps: HashMap<String, StepResult>,
39    /// Skipped steps
40    pub skipped_steps: Vec<String>,
41    /// Current step being executed
42    pub current_step: Option<String>,
43    /// Total retries performed
44    pub total_retries: usize,
45    /// Last update timestamp
46    pub last_updated: chrono::DateTime<chrono::Utc>,
47}
48
49impl WorkflowState {
50    /// Create new workflow state
51    pub fn new(workflow_name: String) -> Self {
52        Self {
53            workflow_name,
54            state: ExecutionState::Pending,
55            variables: HashMap::new(),
56            completed_steps: HashMap::new(),
57            skipped_steps: Vec::new(),
58            current_step: None,
59            total_retries: 0,
60            last_updated: chrono::Utc::now(),
61        }
62    }
63
64    /// Mark as running
65    pub fn mark_running(&mut self) {
66        self.state = ExecutionState::Running;
67        self.last_updated = chrono::Utc::now();
68    }
69
70    /// Mark as completed
71    pub fn mark_completed(&mut self) {
72        self.state = ExecutionState::Completed;
73        self.last_updated = chrono::Utc::now();
74    }
75
76    /// Mark as failed
77    pub fn mark_failed(&mut self) {
78        self.state = ExecutionState::Failed;
79        self.last_updated = chrono::Utc::now();
80    }
81
82    /// Update current step
83    pub fn set_current_step(&mut self, step_name: Option<String>) {
84        self.current_step = step_name;
85        self.last_updated = chrono::Utc::now();
86    }
87
88    /// Check if can be resumed
89    pub fn can_resume(&self) -> bool {
90        matches!(
91            self.state,
92            ExecutionState::Failed | ExecutionState::Stopped | ExecutionState::Paused
93        )
94    }
95}
96
97/// State manager for persistence
98pub struct StateManager {
99    storage_dir: PathBuf,
100}
101
102impl StateManager {
103    /// Create new state manager
104    pub fn new(storage_dir: PathBuf) -> Self {
105        Self { storage_dir }
106    }
107
108    /// Save workflow state
109    pub async fn save(&self, workflow_name: &str, state: &WorkflowState) -> Result<()> {
110        tokio::fs::create_dir_all(&self.storage_dir).await?;
111
112        let filename = format!("{}.state.json", workflow_name);
113        let path = self.storage_dir.join(filename);
114
115        let json = serde_json::to_string_pretty(state)?;
116        tokio::fs::write(path, json).await?;
117
118        Ok(())
119    }
120
121    /// Load workflow state
122    pub async fn load(&self, workflow_name: &str) -> Result<WorkflowState> {
123        let filename = format!("{}.state.json", workflow_name);
124        let path = self.storage_dir.join(filename);
125
126        let json = tokio::fs::read_to_string(path).await?;
127        let state = serde_json::from_str(&json)?;
128
129        Ok(state)
130    }
131
132    /// Delete workflow state
133    pub async fn delete(&self, workflow_name: &str) -> Result<()> {
134        let filename = format!("{}.state.json", workflow_name);
135        let path = self.storage_dir.join(filename);
136
137        if path.exists() {
138            tokio::fs::remove_file(path).await?;
139        }
140
141        Ok(())
142    }
143
144    /// List all saved states
145    pub async fn list_states(&self) -> Result<Vec<String>> {
146        let mut states = Vec::new();
147
148        if !self.storage_dir.exists() {
149            return Ok(states);
150        }
151
152        let mut entries = tokio::fs::read_dir(&self.storage_dir).await?;
153
154        while let Some(entry) = entries.next_entry().await? {
155            let path = entry.path();
156            if let Some(filename) = path.file_name() {
157                if let Some(name_str) = filename.to_str() {
158                    if name_str.ends_with(".state.json") {
159                        let workflow_name = name_str.strip_suffix(".state.json").unwrap();
160                        states.push(workflow_name.to_string());
161                    }
162                }
163            }
164        }
165
166        Ok(states)
167    }
168
169    /// Check if state exists
170    pub async fn exists(&self, workflow_name: &str) -> bool {
171        let filename = format!("{}.state.json", workflow_name);
172        let path = self.storage_dir.join(filename);
173        path.exists()
174    }
175
176    /// Get state info without loading full state
177    pub async fn get_info(&self, workflow_name: &str) -> Result<StateInfo> {
178        let state = self.load(workflow_name).await?;
179
180        Ok(StateInfo {
181            workflow_name: state.workflow_name,
182            state: state.state,
183            completed_steps: state.completed_steps.len(),
184            total_steps: state.completed_steps.len() + state.skipped_steps.len(),
185            current_step: state.current_step,
186            last_updated: state.last_updated,
187        })
188    }
189}
190
191/// State information summary
192#[derive(Debug, Clone, Serialize, Deserialize)]
193pub struct StateInfo {
194    /// Workflow name
195    pub workflow_name: String,
196    /// Execution state
197    pub state: ExecutionState,
198    /// Number of completed steps
199    pub completed_steps: usize,
200    /// Total steps
201    pub total_steps: usize,
202    /// Current step
203    pub current_step: Option<String>,
204    /// Last update time
205    pub last_updated: chrono::DateTime<chrono::Utc>,
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211    use std::env;
212
213    #[test]
214    fn test_workflow_state_creation() {
215        let state = WorkflowState::new("test-workflow".to_string());
216        assert_eq!(state.workflow_name, "test-workflow");
217        assert_eq!(state.state, ExecutionState::Pending);
218        assert_eq!(state.completed_steps.len(), 0);
219    }
220
221    #[test]
222    fn test_workflow_state_transitions() {
223        let mut state = WorkflowState::new("test".to_string());
224
225        assert_eq!(state.state, ExecutionState::Pending);
226
227        state.mark_running();
228        assert_eq!(state.state, ExecutionState::Running);
229
230        state.mark_completed();
231        assert_eq!(state.state, ExecutionState::Completed);
232    }
233
234    #[test]
235    fn test_workflow_state_can_resume() {
236        let mut state = WorkflowState::new("test".to_string());
237
238        state.mark_failed();
239        assert!(state.can_resume());
240
241        state.state = ExecutionState::Stopped;
242        assert!(state.can_resume());
243
244        state.state = ExecutionState::Paused;
245        assert!(state.can_resume());
246
247        state.mark_completed();
248        assert!(!state.can_resume());
249    }
250
251    #[tokio::test]
252    async fn test_state_manager_creation() {
253        let temp_dir = env::temp_dir().join("voirs_state_test");
254        let manager = StateManager::new(temp_dir);
255        // Just verify creation works
256        assert!(true);
257    }
258
259    #[tokio::test]
260    async fn test_state_manager_save_and_load() {
261        let temp_dir = env::temp_dir().join("voirs_state_test_2");
262        let manager = StateManager::new(temp_dir);
263
264        let mut state = WorkflowState::new("test-workflow".to_string());
265        state.mark_running();
266        state
267            .variables
268            .insert("key1".to_string(), serde_json::json!("value1"));
269
270        manager.save("test-workflow", &state).await.unwrap();
271
272        let loaded_state = manager.load("test-workflow").await.unwrap();
273        assert_eq!(loaded_state.workflow_name, "test-workflow");
274        assert_eq!(loaded_state.state, ExecutionState::Running);
275        assert_eq!(loaded_state.variables.len(), 1);
276    }
277
278    #[tokio::test]
279    async fn test_state_manager_exists() {
280        let temp_dir = env::temp_dir().join("voirs_state_test_3");
281        let manager = StateManager::new(temp_dir);
282
283        assert!(!manager.exists("nonexistent").await);
284
285        let state = WorkflowState::new("exists-test".to_string());
286        manager.save("exists-test", &state).await.unwrap();
287
288        assert!(manager.exists("exists-test").await);
289    }
290
291    #[tokio::test]
292    async fn test_state_manager_delete() {
293        let temp_dir = env::temp_dir().join("voirs_state_test_4");
294        let manager = StateManager::new(temp_dir);
295
296        let state = WorkflowState::new("delete-test".to_string());
297        manager.save("delete-test", &state).await.unwrap();
298
299        assert!(manager.exists("delete-test").await);
300
301        manager.delete("delete-test").await.unwrap();
302
303        assert!(!manager.exists("delete-test").await);
304    }
305
306    #[tokio::test]
307    async fn test_state_manager_list_states() {
308        let temp_dir = env::temp_dir().join("voirs_state_test_5");
309        let manager = StateManager::new(temp_dir);
310
311        let state1 = WorkflowState::new("workflow1".to_string());
312        let state2 = WorkflowState::new("workflow2".to_string());
313
314        manager.save("workflow1", &state1).await.unwrap();
315        manager.save("workflow2", &state2).await.unwrap();
316
317        let states = manager.list_states().await.unwrap();
318        assert!(states.len() >= 2);
319        assert!(
320            states.contains(&"workflow1".to_string()) || states.contains(&"workflow2".to_string())
321        );
322    }
323}