Skip to main content

forge_agent/
mutate.rs

1//! Mutation engine - Transaction-based code mutation.
2//!
3//! This module implements the mutation phase, applying changes through
4//! the edit module with transaction support.
5
6use crate::{AgentError, Result};
7use tokio::fs;
8
9/// Mutator for transaction-based code changes.
10///
11/// The Mutator uses the EditModule to apply changes atomically.
12#[derive(Clone, Default)]
13pub struct Mutator {
14    /// Current transaction state
15    transaction: Option<Transaction>,
16}
17
18/// Active transaction state.
19#[derive(Clone, Debug)]
20struct Transaction {
21    /// Steps applied in this transaction
22    applied_steps: Vec<String>,
23    /// Original state for rollback
24    rollback_state: Vec<RollbackState>,
25}
26
27/// Rollback state for a transaction.
28#[derive(Clone, Debug)]
29struct RollbackState {
30    /// File path
31    file: String,
32    /// Original content
33    original_content: String,
34}
35
36impl Mutator {
37    /// Creates a new mutator.
38    pub fn new() -> Self {
39        Self::default()
40    }
41
42    /// Begins a new transaction.
43    pub async fn begin_transaction(&mut self) -> Result<()> {
44        if self.transaction.is_some() {
45            return Err(AgentError::MutationFailed(
46                "Transaction already in progress".to_string(),
47            ));
48        }
49
50        self.transaction = Some(Transaction {
51            applied_steps: Vec::new(),
52            rollback_state: Vec::new(),
53        });
54
55        Ok(())
56    }
57
58    /// Applies a single step in the current transaction.
59    pub async fn apply_step(&mut self, step: &crate::planner::PlanStep) -> Result<()> {
60        let transaction = self
61            .transaction
62            .as_mut()
63            .ok_or_else(|| AgentError::MutationFailed("No active transaction".to_string()))?;
64
65        match &step.operation {
66            crate::planner::PlanOperation::Rename { old, new } => {
67                // Record for rollback
68                transaction
69                    .applied_steps
70                    .push(format!("Rename {} to {}", old, new));
71            }
72            crate::planner::PlanOperation::Delete { name } => {
73                transaction.applied_steps.push(format!("Delete {}", name));
74            }
75            crate::planner::PlanOperation::Create { path, content } => {
76                // Save original for rollback
77                if let Ok(original_content) = fs::read_to_string(path).await {
78                    transaction.rollback_state.push(RollbackState {
79                        file: path.clone(),
80                        original_content,
81                    });
82                }
83
84                // Write new content
85                fs::write(path, content).await.map_err(|e| {
86                    AgentError::MutationFailed(format!("Failed to write {}: {}", path, e))
87                })?;
88
89                transaction.applied_steps.push(format!("Create {}", path));
90            }
91            crate::planner::PlanOperation::Inspect { .. } => {
92                // Inspect doesn't modify files
93            }
94            crate::planner::PlanOperation::Modify { file, .. } => {
95                if let Ok(original_content) = std::fs::read_to_string(file) {
96                    transaction.rollback_state.push(RollbackState {
97                        file: file.clone(),
98                        original_content,
99                    });
100                }
101                transaction.applied_steps.push(format!("Modify {}", file));
102            }
103        }
104
105        Ok(())
106    }
107
108    /// Rolls back the current transaction.
109    pub async fn rollback(&mut self) -> Result<()> {
110        let transaction = self
111            .transaction
112            .take()
113            .ok_or_else(|| AgentError::MutationFailed("No active transaction".to_string()))?;
114
115        // Rollback in reverse order
116        for state in transaction.rollback_state.iter().rev() {
117            std::fs::write(&state.file, &state.original_content).map_err(|e| {
118                AgentError::MutationFailed(format!("Rollback failed for {}: {}", state.file, e))
119            })?;
120        }
121
122        Ok(())
123    }
124
125    /// Returns preview of changes without applying.
126    pub async fn preview(&self, steps: &[crate::planner::PlanStep]) -> Result<Vec<String>> {
127        let mut previews = Vec::new();
128
129        for step in steps {
130            match &step.operation {
131                crate::planner::PlanOperation::Create { path, content } => {
132                    previews.push(format!("Create {}:\n{}", path, content));
133                }
134                crate::planner::PlanOperation::Delete { name } => {
135                    previews.push(format!("Delete {}", name));
136                }
137                crate::planner::PlanOperation::Rename { old, new } => {
138                    previews.push(format!("Rename {} to {}", old, new));
139                }
140                _ => {}
141            }
142        }
143
144        Ok(previews)
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    #[tokio::test]
153    async fn test_mutator_creation() {
154        let mutator = Mutator::new();
155
156        assert!(mutator.transaction.is_none());
157    }
158
159    #[tokio::test]
160    async fn test_begin_transaction() {
161        let mut mutator = Mutator::new();
162
163        mutator.begin_transaction().await.unwrap();
164        assert!(mutator.transaction.is_some());
165
166        // Second begin should fail
167        assert!(mutator.begin_transaction().await.is_err());
168    }
169
170    #[tokio::test]
171    async fn test_rollback() {
172        let mut mutator = Mutator::new();
173
174        mutator.begin_transaction().await.unwrap();
175        assert!(mutator.transaction.is_some());
176
177        mutator.rollback().await.unwrap();
178        assert!(mutator.transaction.is_none());
179    }
180}