ricecoder_execution/
modes.rs

1//! Execution modes for controlling how plans are executed
2//!
3//! Supports three execution modes:
4//! - Automatic: Execute all steps without user intervention
5//! - StepByStep: Require approval for each step
6//! - DryRun: Preview changes without applying them
7
8use 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/// Configuration for execution modes
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct ModeConfig {
17    /// Default execution mode
18    pub default_mode: ExecutionMode,
19    /// Whether to skip approval gates for Low/Medium risk
20    pub skip_low_medium_approval: bool,
21    /// Whether to always require approval for Critical risk
22    pub always_approve_critical: bool,
23    /// Path to configuration file
24    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
38/// Automatic mode executor
39///
40/// Executes all steps without user intervention. Skips approval gates
41/// except for Critical risk level.
42pub struct AutomaticModeExecutor {
43    config: ModeConfig,
44}
45
46impl AutomaticModeExecutor {
47    /// Create a new automatic mode executor
48    pub fn new(config: ModeConfig) -> Self {
49        Self { config }
50    }
51
52    /// Check if approval is required for a plan
53    ///
54    /// In automatic mode, approval is only required for Critical risk.
55    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    /// Execute plan in automatic mode
64    ///
65    /// Executes all steps sequentially without user intervention.
66    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            // In automatic mode, we execute all steps
82            // Actual execution is handled by StepExecutor
83        }
84
85        info!(
86            plan_id = %plan.id,
87            "Automatic mode execution completed"
88        );
89
90        Ok(())
91    }
92}
93
94/// Step-by-step mode executor
95///
96/// Requires approval for each step. Allows skipping individual steps
97/// and supports pause/resume between steps.
98pub struct StepByStepModeExecutor {
99    #[allow(dead_code)]
100    config: ModeConfig,
101    /// Steps that have been approved
102    approved_steps: Vec<String>,
103    /// Steps that have been skipped
104    skipped_steps: Vec<String>,
105}
106
107impl StepByStepModeExecutor {
108    /// Create a new step-by-step mode executor
109    pub fn new(config: ModeConfig) -> Self {
110        Self {
111            config,
112            approved_steps: Vec::new(),
113            skipped_steps: Vec::new(),
114        }
115    }
116
117    /// Request approval for a step
118    ///
119    /// Returns true if the step is approved, false if rejected.
120    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        // In a real implementation, this would show a UI prompt
128        // For now, we'll return true (approved)
129        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    /// Skip a step
140    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    /// Check if a step has been approved
154    pub fn is_approved(&self, step_id: &str) -> bool {
155        self.approved_steps.contains(&step_id.to_string())
156    }
157
158    /// Check if a step has been skipped
159    pub fn is_skipped(&self, step_id: &str) -> bool {
160        self.skipped_steps.contains(&step_id.to_string())
161    }
162
163    /// Get approved steps
164    pub fn approved_steps(&self) -> &[String] {
165        &self.approved_steps
166    }
167
168    /// Get skipped steps
169    pub fn skipped_steps(&self) -> &[String] {
170        &self.skipped_steps
171    }
172
173    /// Execute plan in step-by-step mode
174    ///
175    /// Requires approval for each step before execution.
176    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            // Request approval for this step
192            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
206/// Dry-run mode executor
207///
208/// Previews all changes without applying them. Shows what would be
209/// created, modified, or deleted.
210pub struct DryRunModeExecutor {
211    #[allow(dead_code)]
212    config: ModeConfig,
213    /// Changes that would be made
214    preview_changes: Vec<PreviewChange>,
215}
216
217/// A change that would be made in dry-run mode
218#[derive(Debug, Clone, Serialize, Deserialize)]
219pub struct PreviewChange {
220    /// Step ID
221    pub step_id: String,
222    /// Type of change
223    pub change_type: ChangeType,
224    /// Path affected
225    pub path: String,
226    /// Description of the change
227    pub description: String,
228}
229
230/// Type of change in dry-run mode
231#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
232pub enum ChangeType {
233    /// File would be created
234    Create,
235    /// File would be modified
236    Modify,
237    /// File would be deleted
238    Delete,
239    /// Command would be executed
240    Command,
241    /// Tests would be run
242    Test,
243}
244
245impl DryRunModeExecutor {
246    /// Create a new dry-run mode executor
247    pub fn new(config: ModeConfig) -> Self {
248        Self {
249            config,
250            preview_changes: Vec::new(),
251        }
252    }
253
254    /// Preview a step without executing it
255    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    /// Get all preview changes
306    pub fn preview_changes(&self) -> &[PreviewChange] {
307        &self.preview_changes
308    }
309
310    /// Get summary of changes
311    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    /// Execute plan in dry-run mode
339    ///
340    /// Previews all changes without applying them.
341    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/// Summary of changes in dry-run mode
369#[derive(Debug, Clone, Serialize, Deserialize)]
370pub struct DryRunSummary {
371    /// Total number of changes
372    pub total_changes: usize,
373    /// Number of files to create
374    pub creates: usize,
375    /// Number of files to modify
376    pub modifies: usize,
377    /// Number of files to delete
378    pub deletes: usize,
379    /// Number of commands to run
380    pub commands: usize,
381    /// Number of test runs
382    pub tests: usize,
383}
384
385/// Mode persistence for remembering user preferences
386pub struct ModePersistence {
387    config_path: String,
388}
389
390impl ModePersistence {
391    /// Create a new mode persistence handler
392    pub fn new(config_path: String) -> Self {
393        Self { config_path }
394    }
395
396    /// Load mode configuration from file
397    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    /// Save mode configuration to file
423    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    /// Load mode with fallback to default
451    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}