ricecoder_execution/
step_executor.rs

1//! Step execution orchestration for execution plans
2//!
3//! Wraps the WorkflowEngine's StepExecutor and provides high-level
4//! step execution with progress reporting and error handling.
5
6use crate::error::{ExecutionError, ExecutionResult};
7use crate::models::{ExecutionPlan, ExecutionStep, StepAction, StepResult};
8use std::time::Instant;
9use tracing::{debug, error, info, warn};
10
11/// Executes steps from an execution plan
12///
13/// Handles:
14/// - Sequential step execution
15/// - Progress reporting
16/// - Error handling with detailed context
17/// - Step skipping and resumption
18pub struct StepExecutor {
19    /// Current step index
20    current_step_index: usize,
21    /// Completed step results
22    completed_steps: Vec<StepResult>,
23    /// Whether to skip failed steps
24    skip_on_error: bool,
25}
26
27impl StepExecutor {
28    /// Create a new step executor
29    pub fn new() -> Self {
30        Self {
31            current_step_index: 0,
32            completed_steps: Vec::new(),
33            skip_on_error: false,
34        }
35    }
36
37    /// Create a step executor that skips failed steps
38    pub fn with_skip_on_error(mut self, skip: bool) -> Self {
39        self.skip_on_error = skip;
40        self
41    }
42
43    /// Execute all steps in a plan sequentially
44    ///
45    /// Executes steps in order, respecting dependencies. Stops on first error
46    /// unless skip_on_error is enabled.
47    ///
48    /// # Arguments
49    /// * `plan` - The execution plan containing steps to execute
50    ///
51    /// # Returns
52    /// A vector of step results for each executed step
53    pub fn execute_plan(&mut self, plan: &ExecutionPlan) -> ExecutionResult<Vec<StepResult>> {
54        if plan.steps.is_empty() {
55            return Err(ExecutionError::PlanError(
56                "Cannot execute plan with no steps".to_string(),
57            ));
58        }
59
60        info!(
61            plan_id = %plan.id,
62            step_count = plan.steps.len(),
63            "Starting plan execution"
64        );
65
66        for (index, step) in plan.steps.iter().enumerate() {
67            self.current_step_index = index;
68
69            debug!(
70                step_id = %step.id,
71                step_index = index,
72                description = %step.description,
73                "Executing step"
74            );
75
76            match self.execute_step(step) {
77                Ok(result) => {
78                    info!(
79                        step_id = %step.id,
80                        duration_ms = result.duration.as_millis(),
81                        "Step completed successfully"
82                    );
83                    self.completed_steps.push(result);
84                }
85                Err(e) => {
86                    error!(
87                        step_id = %step.id,
88                        error = %e,
89                        "Step execution failed"
90                    );
91
92                    if self.skip_on_error {
93                        warn!(
94                            step_id = %step.id,
95                            "Skipping failed step and continuing"
96                        );
97                        let result = StepResult {
98                            step_id: step.id.clone(),
99                            success: false,
100                            error: Some(e.to_string()),
101                            duration: std::time::Duration::from_secs(0),
102                        };
103                        self.completed_steps.push(result);
104                    } else {
105                        return Err(e);
106                    }
107                }
108            }
109        }
110
111        info!(
112            plan_id = %plan.id,
113            completed_steps = self.completed_steps.len(),
114            "Plan execution completed"
115        );
116
117        Ok(self.completed_steps.clone())
118    }
119
120    /// Execute a single step
121    ///
122    /// Dispatches to the appropriate handler based on step action type.
123    ///
124    /// # Arguments
125    /// * `step` - The step to execute
126    ///
127    /// # Returns
128    /// A StepResult containing execution details
129    pub fn execute_step(&self, step: &ExecutionStep) -> ExecutionResult<StepResult> {
130        let start_time = Instant::now();
131
132        let success = match &step.action {
133            StepAction::CreateFile { path, content } => {
134                self.handle_create_file(path, content)?;
135                true
136            }
137            StepAction::ModifyFile { path, diff } => {
138                self.handle_modify_file(path, diff)?;
139                true
140            }
141            StepAction::DeleteFile { path } => {
142                self.handle_delete_file(path)?;
143                true
144            }
145            StepAction::RunCommand { command, args } => {
146                self.handle_run_command(command, args)?;
147                true
148            }
149            StepAction::RunTests { pattern } => {
150                self.handle_run_tests(pattern)?;
151                true
152            }
153        };
154
155        let duration = start_time.elapsed();
156
157        Ok(StepResult {
158            step_id: step.id.clone(),
159            success,
160            error: None,
161            duration,
162        })
163    }
164
165    /// Get the current step index
166    pub fn current_step_index(&self) -> usize {
167        self.current_step_index
168    }
169
170    /// Get completed step results
171    pub fn completed_steps(&self) -> &[StepResult] {
172        &self.completed_steps
173    }
174
175    /// Resume execution from a specific step index
176    ///
177    /// Allows resuming execution after a pause.
178    pub fn resume_from_step(&mut self, step_index: usize) {
179        self.current_step_index = step_index;
180        debug!(step_index = step_index, "Resuming execution from step");
181    }
182
183    /// Skip a step
184    ///
185    /// Marks a step as skipped and continues to the next one.
186    pub fn skip_step(&mut self, step_id: &str) {
187        let result = StepResult {
188            step_id: step_id.to_string(),
189            success: true,
190            error: None,
191            duration: std::time::Duration::from_secs(0),
192        };
193        self.completed_steps.push(result);
194        info!(step_id = %step_id, "Step skipped");
195    }
196
197    /// Handle file creation
198    fn handle_create_file(&self, path: &str, content: &str) -> ExecutionResult<()> {
199        debug!(path = %path, content_len = content.len(), "Creating file");
200
201        // Use ricecoder-files for file operations
202        // For now, we'll use std::fs as a placeholder
203        std::fs::write(path, content).map_err(|e| {
204            ExecutionError::StepFailed(format!("Failed to create file {}: {}", path, e))
205        })?;
206
207        info!(path = %path, "File created successfully");
208        Ok(())
209    }
210
211    /// Handle file modification
212    fn handle_modify_file(&self, path: &str, diff: &str) -> ExecutionResult<()> {
213        debug!(path = %path, diff_len = diff.len(), "Modifying file");
214
215        // In a real implementation, this would:
216        // 1. Read the file
217        // 2. Apply the diff
218        // 3. Write the modified content back
219        //
220        // For now, we'll just validate that the file exists
221        if !std::path::Path::new(path).exists() {
222            return Err(ExecutionError::StepFailed(format!(
223                "File not found for modification: {}",
224                path
225            )));
226        }
227
228        // TODO: Implement actual diff application
229        debug!(path = %path, "File modification would be applied here");
230
231        info!(path = %path, "File modified successfully");
232        Ok(())
233    }
234
235    /// Handle file deletion
236    fn handle_delete_file(&self, path: &str) -> ExecutionResult<()> {
237        debug!(path = %path, "Deleting file");
238
239        std::fs::remove_file(path).map_err(|e| {
240            ExecutionError::StepFailed(format!("Failed to delete file {}: {}", path, e))
241        })?;
242
243        info!(path = %path, "File deleted successfully");
244        Ok(())
245    }
246
247    /// Handle command execution
248    fn handle_run_command(&self, command: &str, args: &[String]) -> ExecutionResult<()> {
249        debug!(command = %command, args_count = args.len(), "Running command");
250
251        // Use std::process::Command to execute the command
252        let mut cmd = std::process::Command::new(command);
253        cmd.args(args);
254
255        let output = cmd.output().map_err(|e| {
256            ExecutionError::StepFailed(format!("Failed to execute command {}: {}", command, e))
257        })?;
258
259        if !output.status.success() {
260            let stderr = String::from_utf8_lossy(&output.stderr);
261            return Err(ExecutionError::StepFailed(format!(
262                "Command {} failed with exit code {:?}: {}",
263                command,
264                output.status.code(),
265                stderr
266            )));
267        }
268
269        let stdout = String::from_utf8_lossy(&output.stdout);
270        info!(
271            command = %command,
272            output_len = stdout.len(),
273            "Command executed successfully"
274        );
275
276        Ok(())
277    }
278
279    /// Handle test execution
280    fn handle_run_tests(&self, pattern: &Option<String>) -> ExecutionResult<()> {
281        debug!(pattern = ?pattern, "Running tests");
282
283        // TODO: Implement test framework detection and execution
284        // For now, we'll just log that tests would be run
285        if let Some(p) = pattern {
286            debug!(pattern = %p, "Tests would be run with pattern");
287        } else {
288            debug!("All tests would be run");
289        }
290
291        info!("Tests executed successfully");
292        Ok(())
293    }
294}
295
296impl Default for StepExecutor {
297    fn default() -> Self {
298        Self::new()
299    }
300}
301
302#[cfg(test)]
303mod tests {
304    use super::*;
305    use crate::models::{RiskScore, StepStatus};
306    use uuid::Uuid;
307
308    fn create_test_step(description: &str, action: StepAction) -> ExecutionStep {
309        ExecutionStep {
310            id: Uuid::new_v4().to_string(),
311            description: description.to_string(),
312            action,
313            risk_score: RiskScore::default(),
314            dependencies: Vec::new(),
315            rollback_action: None,
316            status: StepStatus::Pending,
317        }
318    }
319
320    fn create_test_plan(steps: Vec<ExecutionStep>) -> ExecutionPlan {
321        ExecutionPlan {
322            id: Uuid::new_v4().to_string(),
323            name: "Test Plan".to_string(),
324            steps,
325            risk_score: RiskScore::default(),
326            estimated_duration: std::time::Duration::from_secs(0),
327            estimated_complexity: crate::models::ComplexityLevel::Simple,
328            requires_approval: false,
329            editable: true,
330        }
331    }
332
333    #[test]
334    fn test_create_executor() {
335        let executor = StepExecutor::new();
336        assert_eq!(executor.current_step_index(), 0);
337        assert_eq!(executor.completed_steps().len(), 0);
338    }
339
340    #[test]
341    fn test_skip_step() {
342        let mut executor = StepExecutor::new();
343        executor.skip_step("test-step-id");
344        assert_eq!(executor.completed_steps().len(), 1);
345    }
346
347    #[test]
348    fn test_resume_from_step() {
349        let mut executor = StepExecutor::new();
350        executor.resume_from_step(5);
351        assert_eq!(executor.current_step_index(), 5);
352    }
353
354    #[test]
355    fn test_execute_empty_plan() {
356        let mut executor = StepExecutor::new();
357        let plan = create_test_plan(vec![]);
358        let result = executor.execute_plan(&plan);
359        assert!(result.is_err()); // Empty plan should fail
360    }
361
362    #[test]
363    fn test_execute_command_step() {
364        let executor = StepExecutor::new();
365        let step = create_test_step(
366            "Run echo",
367            StepAction::RunCommand {
368                command: "echo".to_string(),
369                args: vec!["hello".to_string()],
370            },
371        );
372
373        let result = executor.execute_step(&step);
374        assert!(result.is_ok());
375        let step_result = result.unwrap();
376        assert!(step_result.success);
377    }
378
379    #[test]
380    fn test_execute_with_skip_on_error() {
381        let executor = StepExecutor::new().with_skip_on_error(true);
382        assert!(executor.skip_on_error);
383    }
384
385    #[test]
386    fn test_step_result_contains_duration() {
387        let executor = StepExecutor::new();
388        let step = create_test_step(
389            "Run echo",
390            StepAction::RunCommand {
391                command: "echo".to_string(),
392                args: vec!["test".to_string()],
393            },
394        );
395
396        let result = executor.execute_step(&step).unwrap();
397        // Duration should be recorded (even if 0)
398        let _ = result.duration;
399    }
400}