1use crate::error::{ExecutionError, ExecutionResult};
9use crate::models::{ExecutionMode, ExecutionPlan, ExecutionStep, StepAction};
10use serde::{Deserialize, Serialize};
11use std::path::Path;
12use tracing::{debug, info, warn};
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct ModeConfig {
17 pub default_mode: ExecutionMode,
19 pub skip_low_medium_approval: bool,
21 pub always_approve_critical: bool,
23 pub config_path: Option<String>,
25}
26
27impl Default for ModeConfig {
28 fn default() -> Self {
29 Self {
30 default_mode: ExecutionMode::Automatic,
31 skip_low_medium_approval: true,
32 always_approve_critical: true,
33 config_path: None,
34 }
35 }
36}
37
38pub struct AutomaticModeExecutor {
43 config: ModeConfig,
44}
45
46impl AutomaticModeExecutor {
47 pub fn new(config: ModeConfig) -> Self {
49 Self { config }
50 }
51
52 pub fn requires_approval(&self, plan: &ExecutionPlan) -> bool {
56 if !self.config.always_approve_critical {
57 return false;
58 }
59
60 plan.risk_score.level == crate::models::RiskLevel::Critical
61 }
62
63 pub fn execute(&self, plan: &ExecutionPlan) -> ExecutionResult<()> {
67 info!(
68 plan_id = %plan.id,
69 step_count = plan.steps.len(),
70 "Executing plan in automatic mode"
71 );
72
73 for (index, step) in plan.steps.iter().enumerate() {
74 debug!(
75 step_index = index,
76 step_id = %step.id,
77 description = %step.description,
78 "Executing step in automatic mode"
79 );
80
81 }
84
85 info!(
86 plan_id = %plan.id,
87 "Automatic mode execution completed"
88 );
89
90 Ok(())
91 }
92}
93
94pub struct StepByStepModeExecutor {
99 #[allow(dead_code)]
100 config: ModeConfig,
101 approved_steps: Vec<String>,
103 skipped_steps: Vec<String>,
105}
106
107impl StepByStepModeExecutor {
108 pub fn new(config: ModeConfig) -> Self {
110 Self {
111 config,
112 approved_steps: Vec::new(),
113 skipped_steps: Vec::new(),
114 }
115 }
116
117 pub fn request_approval(&mut self, step: &ExecutionStep) -> ExecutionResult<bool> {
121 debug!(
122 step_id = %step.id,
123 description = %step.description,
124 "Requesting approval for step"
125 );
126
127 self.approved_steps.push(step.id.clone());
130
131 info!(
132 step_id = %step.id,
133 "Step approved"
134 );
135
136 Ok(true)
137 }
138
139 pub fn skip_step(&mut self, step_id: &str) -> ExecutionResult<()> {
141 debug!(step_id = %step_id, "Skipping step");
142
143 self.skipped_steps.push(step_id.to_string());
144
145 info!(
146 step_id = %step_id,
147 "Step skipped"
148 );
149
150 Ok(())
151 }
152
153 pub fn is_approved(&self, step_id: &str) -> bool {
155 self.approved_steps.contains(&step_id.to_string())
156 }
157
158 pub fn is_skipped(&self, step_id: &str) -> bool {
160 self.skipped_steps.contains(&step_id.to_string())
161 }
162
163 pub fn approved_steps(&self) -> &[String] {
165 &self.approved_steps
166 }
167
168 pub fn skipped_steps(&self) -> &[String] {
170 &self.skipped_steps
171 }
172
173 pub fn execute(&mut self, plan: &ExecutionPlan) -> ExecutionResult<()> {
177 info!(
178 plan_id = %plan.id,
179 step_count = plan.steps.len(),
180 "Executing plan in step-by-step mode"
181 );
182
183 for (index, step) in plan.steps.iter().enumerate() {
184 debug!(
185 step_index = index,
186 step_id = %step.id,
187 description = %step.description,
188 "Processing step in step-by-step mode"
189 );
190
191 self.request_approval(step)?;
193 }
194
195 info!(
196 plan_id = %plan.id,
197 approved_count = self.approved_steps.len(),
198 skipped_count = self.skipped_steps.len(),
199 "Step-by-step mode execution completed"
200 );
201
202 Ok(())
203 }
204}
205
206pub struct DryRunModeExecutor {
211 #[allow(dead_code)]
212 config: ModeConfig,
213 preview_changes: Vec<PreviewChange>,
215}
216
217#[derive(Debug, Clone, Serialize, Deserialize)]
219pub struct PreviewChange {
220 pub step_id: String,
222 pub change_type: ChangeType,
224 pub path: String,
226 pub description: String,
228}
229
230#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
232pub enum ChangeType {
233 Create,
235 Modify,
237 Delete,
239 Command,
241 Test,
243}
244
245impl DryRunModeExecutor {
246 pub fn new(config: ModeConfig) -> Self {
248 Self {
249 config,
250 preview_changes: Vec::new(),
251 }
252 }
253
254 pub fn preview_step(&mut self, step: &ExecutionStep) -> ExecutionResult<()> {
256 debug!(
257 step_id = %step.id,
258 description = %step.description,
259 "Previewing step in dry-run mode"
260 );
261
262 let change = match &step.action {
263 StepAction::CreateFile { path, content } => PreviewChange {
264 step_id: step.id.clone(),
265 change_type: ChangeType::Create,
266 path: path.clone(),
267 description: format!("Create file with {} bytes", content.len()),
268 },
269 StepAction::ModifyFile { path, diff } => PreviewChange {
270 step_id: step.id.clone(),
271 change_type: ChangeType::Modify,
272 path: path.clone(),
273 description: format!("Modify file with diff ({} bytes)", diff.len()),
274 },
275 StepAction::DeleteFile { path } => PreviewChange {
276 step_id: step.id.clone(),
277 change_type: ChangeType::Delete,
278 path: path.clone(),
279 description: "Delete file".to_string(),
280 },
281 StepAction::RunCommand { command, args } => PreviewChange {
282 step_id: step.id.clone(),
283 change_type: ChangeType::Command,
284 path: command.clone(),
285 description: format!("Run command with {} args", args.len()),
286 },
287 StepAction::RunTests { pattern } => PreviewChange {
288 step_id: step.id.clone(),
289 change_type: ChangeType::Test,
290 path: pattern.clone().unwrap_or_else(|| "all".to_string()),
291 description: "Run tests".to_string(),
292 },
293 };
294
295 self.preview_changes.push(change);
296
297 info!(
298 step_id = %step.id,
299 "Step previewed in dry-run mode"
300 );
301
302 Ok(())
303 }
304
305 pub fn preview_changes(&self) -> &[PreviewChange] {
307 &self.preview_changes
308 }
309
310 pub fn get_summary(&self) -> DryRunSummary {
312 let mut creates = 0;
313 let mut modifies = 0;
314 let mut deletes = 0;
315 let mut commands = 0;
316 let mut tests = 0;
317
318 for change in &self.preview_changes {
319 match change.change_type {
320 ChangeType::Create => creates += 1,
321 ChangeType::Modify => modifies += 1,
322 ChangeType::Delete => deletes += 1,
323 ChangeType::Command => commands += 1,
324 ChangeType::Test => tests += 1,
325 }
326 }
327
328 DryRunSummary {
329 total_changes: self.preview_changes.len(),
330 creates,
331 modifies,
332 deletes,
333 commands,
334 tests,
335 }
336 }
337
338 pub fn execute(&mut self, plan: &ExecutionPlan) -> ExecutionResult<()> {
342 info!(
343 plan_id = %plan.id,
344 step_count = plan.steps.len(),
345 "Executing plan in dry-run mode"
346 );
347
348 for step in &plan.steps {
349 self.preview_step(step)?;
350 }
351
352 let summary = self.get_summary();
353 info!(
354 plan_id = %plan.id,
355 total_changes = summary.total_changes,
356 creates = summary.creates,
357 modifies = summary.modifies,
358 deletes = summary.deletes,
359 commands = summary.commands,
360 tests = summary.tests,
361 "Dry-run mode execution completed"
362 );
363
364 Ok(())
365 }
366}
367
368#[derive(Debug, Clone, Serialize, Deserialize)]
370pub struct DryRunSummary {
371 pub total_changes: usize,
373 pub creates: usize,
375 pub modifies: usize,
377 pub deletes: usize,
379 pub commands: usize,
381 pub tests: usize,
383}
384
385pub struct ModePersistence {
387 config_path: String,
388}
389
390impl ModePersistence {
391 pub fn new(config_path: String) -> Self {
393 Self { config_path }
394 }
395
396 pub fn load_mode(&self) -> ExecutionResult<ExecutionMode> {
398 debug!(config_path = %self.config_path, "Loading execution mode from config");
399
400 if !Path::new(&self.config_path).exists() {
401 debug!(config_path = %self.config_path, "Config file not found, using default");
402 return Ok(ExecutionMode::default());
403 }
404
405 let content = std::fs::read_to_string(&self.config_path).map_err(|e| {
406 ExecutionError::ValidationError(format!("Failed to read mode config: {}", e))
407 })?;
408
409 let config: ModeConfig = serde_yaml::from_str(&content).map_err(|e| {
410 ExecutionError::ValidationError(format!("Failed to parse mode config: {}", e))
411 })?;
412
413 info!(
414 config_path = %self.config_path,
415 mode = ?config.default_mode,
416 "Execution mode loaded from config"
417 );
418
419 Ok(config.default_mode)
420 }
421
422 pub fn save_mode(&self, mode: ExecutionMode) -> ExecutionResult<()> {
424 debug!(config_path = %self.config_path, mode = ?mode, "Saving execution mode to config");
425
426 let config = ModeConfig {
427 default_mode: mode,
428 skip_low_medium_approval: true,
429 always_approve_critical: true,
430 config_path: Some(self.config_path.clone()),
431 };
432
433 let yaml = serde_yaml::to_string(&config).map_err(|e| {
434 ExecutionError::ValidationError(format!("Failed to serialize mode config: {}", e))
435 })?;
436
437 std::fs::write(&self.config_path, yaml).map_err(|e| {
438 ExecutionError::ValidationError(format!("Failed to write mode config: {}", e))
439 })?;
440
441 info!(
442 config_path = %self.config_path,
443 mode = ?mode,
444 "Execution mode saved to config"
445 );
446
447 Ok(())
448 }
449
450 pub fn load_mode_or_default(&self) -> ExecutionMode {
452 match self.load_mode() {
453 Ok(mode) => mode,
454 Err(e) => {
455 warn!(error = %e, "Failed to load mode, using default");
456 ExecutionMode::default()
457 }
458 }
459 }
460}
461
462#[cfg(test)]
463mod tests {
464 use super::*;
465 use crate::models::{ComplexityLevel, RiskLevel, RiskScore};
466
467 fn create_test_plan(risk_level: RiskLevel) -> ExecutionPlan {
468 ExecutionPlan {
469 id: "test-plan".to_string(),
470 name: "Test Plan".to_string(),
471 steps: vec![],
472 risk_score: RiskScore {
473 level: risk_level,
474 score: 0.5,
475 factors: vec![],
476 },
477 estimated_duration: std::time::Duration::from_secs(10),
478 estimated_complexity: ComplexityLevel::Simple,
479 requires_approval: false,
480 editable: true,
481 }
482 }
483
484 #[test]
485 fn test_automatic_mode_low_risk() {
486 let config = ModeConfig::default();
487 let executor = AutomaticModeExecutor::new(config);
488 let plan = create_test_plan(RiskLevel::Low);
489
490 assert!(!executor.requires_approval(&plan));
491 }
492
493 #[test]
494 fn test_automatic_mode_critical_risk() {
495 let config = ModeConfig::default();
496 let executor = AutomaticModeExecutor::new(config);
497 let plan = create_test_plan(RiskLevel::Critical);
498
499 assert!(executor.requires_approval(&plan));
500 }
501
502 #[test]
503 fn test_automatic_mode_execute() {
504 let config = ModeConfig::default();
505 let executor = AutomaticModeExecutor::new(config);
506 let plan = create_test_plan(RiskLevel::Low);
507
508 let result = executor.execute(&plan);
509 assert!(result.is_ok());
510 }
511
512 #[test]
513 fn test_step_by_step_mode_approval() {
514 let config = ModeConfig::default();
515 let mut executor = StepByStepModeExecutor::new(config);
516
517 let step = ExecutionStep::new(
518 "Test step".to_string(),
519 StepAction::RunCommand {
520 command: "echo".to_string(),
521 args: vec!["test".to_string()],
522 },
523 );
524
525 let result = executor.request_approval(&step);
526 assert!(result.is_ok());
527 assert!(executor.is_approved(&step.id));
528 }
529
530 #[test]
531 fn test_step_by_step_mode_skip() {
532 let config = ModeConfig::default();
533 let mut executor = StepByStepModeExecutor::new(config);
534
535 let step_id = "test-step";
536 let result = executor.skip_step(step_id);
537 assert!(result.is_ok());
538 assert!(executor.is_skipped(step_id));
539 }
540
541 #[test]
542 fn test_dry_run_mode_preview_create() {
543 let config = ModeConfig::default();
544 let mut executor = DryRunModeExecutor::new(config);
545
546 let step = ExecutionStep::new(
547 "Create file".to_string(),
548 StepAction::CreateFile {
549 path: "/tmp/test.txt".to_string(),
550 content: "test content".to_string(),
551 },
552 );
553
554 let result = executor.preview_step(&step);
555 assert!(result.is_ok());
556 assert_eq!(executor.preview_changes().len(), 1);
557 assert_eq!(
558 executor.preview_changes()[0].change_type,
559 ChangeType::Create
560 );
561 }
562
563 #[test]
564 fn test_dry_run_mode_preview_delete() {
565 let config = ModeConfig::default();
566 let mut executor = DryRunModeExecutor::new(config);
567
568 let step = ExecutionStep::new(
569 "Delete file".to_string(),
570 StepAction::DeleteFile {
571 path: "/tmp/test.txt".to_string(),
572 },
573 );
574
575 let result = executor.preview_step(&step);
576 assert!(result.is_ok());
577 assert_eq!(executor.preview_changes().len(), 1);
578 assert_eq!(
579 executor.preview_changes()[0].change_type,
580 ChangeType::Delete
581 );
582 }
583
584 #[test]
585 fn test_dry_run_mode_summary() {
586 let config = ModeConfig::default();
587 let mut executor = DryRunModeExecutor::new(config);
588
589 let step1 = ExecutionStep::new(
590 "Create file".to_string(),
591 StepAction::CreateFile {
592 path: "/tmp/test1.txt".to_string(),
593 content: "content".to_string(),
594 },
595 );
596
597 let step2 = ExecutionStep::new(
598 "Delete file".to_string(),
599 StepAction::DeleteFile {
600 path: "/tmp/test2.txt".to_string(),
601 },
602 );
603
604 executor.preview_step(&step1).unwrap();
605 executor.preview_step(&step2).unwrap();
606
607 let summary = executor.get_summary();
608 assert_eq!(summary.total_changes, 2);
609 assert_eq!(summary.creates, 1);
610 assert_eq!(summary.deletes, 1);
611 }
612
613 #[test]
614 fn test_mode_config_default() {
615 let config = ModeConfig::default();
616 assert_eq!(config.default_mode, ExecutionMode::Automatic);
617 assert!(config.skip_low_medium_approval);
618 assert!(config.always_approve_critical);
619 }
620
621 #[test]
622 fn test_mode_persistence_default() {
623 let persistence = ModePersistence::new("/tmp/nonexistent_config.yaml".to_string());
624 let mode = persistence.load_mode_or_default();
625 assert_eq!(mode, ExecutionMode::Automatic);
626 }
627}