1use crate::error::{ExecutionError, ExecutionResult};
7use crate::models::{ExecutionPlan, ExecutionStep, StepAction, StepResult};
8use std::time::Instant;
9use tracing::{debug, error, info, warn};
10
11pub struct StepExecutor {
19 current_step_index: usize,
21 completed_steps: Vec<StepResult>,
23 skip_on_error: bool,
25}
26
27impl StepExecutor {
28 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 pub fn with_skip_on_error(mut self, skip: bool) -> Self {
39 self.skip_on_error = skip;
40 self
41 }
42
43 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 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 pub fn current_step_index(&self) -> usize {
167 self.current_step_index
168 }
169
170 pub fn completed_steps(&self) -> &[StepResult] {
172 &self.completed_steps
173 }
174
175 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 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 fn handle_create_file(&self, path: &str, content: &str) -> ExecutionResult<()> {
199 debug!(path = %path, content_len = content.len(), "Creating file");
200
201 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 fn handle_modify_file(&self, path: &str, diff: &str) -> ExecutionResult<()> {
213 debug!(path = %path, diff_len = diff.len(), "Modifying file");
214
215 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 debug!(path = %path, "File modification would be applied here");
230
231 info!(path = %path, "File modified successfully");
232 Ok(())
233 }
234
235 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 fn handle_run_command(&self, command: &str, args: &[String]) -> ExecutionResult<()> {
249 debug!(command = %command, args_count = args.len(), "Running command");
250
251 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 fn handle_run_tests(&self, pattern: &Option<String>) -> ExecutionResult<()> {
281 debug!(pattern = ?pattern, "Running tests");
282
283 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()); }
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 let _ = result.duration;
399 }
400}