1use crate::{AgentError, Result};
7use crate::transaction::Transaction;
8use std::path::Path;
9use uuid::Uuid;
10use tokio::fs;
11
12#[derive(Clone, Default)]
17pub struct Mutator {
18 transaction: Option<Transaction>,
20}
21
22impl Mutator {
23 pub fn new() -> Self {
25 Self::default()
26 }
27
28 pub async fn begin_transaction(&mut self) -> Result<()> {
30 if self.transaction.is_some() {
31 return Err(AgentError::MutationFailed(
32 "Transaction already in progress".to_string(),
33 ));
34 }
35
36 self.transaction = Some(Transaction::begin().await?);
37 Ok(())
38 }
39
40 pub async fn apply_step(&mut self, step: &crate::planner::PlanStep) -> Result<()> {
44 let transaction = self
45 .transaction
46 .as_mut()
47 .ok_or_else(|| AgentError::MutationFailed("No active transaction".to_string()))?;
48
49 match &step.operation {
50 crate::planner::PlanOperation::Rename { old, new: _ } => {
51 let old_path = Path::new(old);
53 let _ = transaction.snapshot_file(old_path).await;
54
55 }
58 crate::planner::PlanOperation::Delete { name } => {
59 let name_path = Path::new(name);
61 transaction.snapshot_file(name_path).await?;
62
63 }
65 crate::planner::PlanOperation::Create { path, content } => {
66 let p = Path::new(path);
68 let _ = transaction.snapshot_file(p).await; fs::write(path, content).await.map_err(|e| {
72 AgentError::MutationFailed(format!("Failed to write {}: {}", path, e))
73 })?;
74 }
75 crate::planner::PlanOperation::Inspect { .. } => {
76 }
78 crate::planner::PlanOperation::Modify { file, .. } => {
79 let file_path = Path::new(file);
81 transaction.snapshot_file(file_path).await?;
82
83 }
85 }
86
87 Ok(())
88 }
89
90 pub async fn rollback(&mut self) -> Result<()> {
94 let transaction = self
95 .transaction
96 .take()
97 .ok_or_else(|| AgentError::MutationFailed("No active transaction".to_string()))?;
98
99 transaction.rollback().await?;
100 Ok(())
101 }
102
103 pub async fn commit_transaction(mut self) -> Result<Uuid> {
107 let transaction = self
108 .transaction
109 .take()
110 .ok_or_else(|| AgentError::MutationFailed("No active transaction".to_string()))?;
111
112 transaction.commit().await
113 }
114
115 pub fn into_transaction(mut self) -> Result<Transaction> {
120 self.transaction
121 .take()
122 .ok_or_else(|| AgentError::MutationFailed("No active transaction".to_string()))
123 }
124
125 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 #[cfg(test)]
149 pub fn transaction(&self) -> Option<&Transaction> {
150 self.transaction.as_ref()
151 }
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157 use tempfile::TempDir;
158
159 #[tokio::test]
160 async fn test_mutator_creation() {
161 let mutator = Mutator::new();
162
163 assert!(mutator.transaction().is_none());
164 }
165
166 #[tokio::test]
167 async fn test_begin_transaction() {
168 let mut mutator = Mutator::new();
169
170 mutator.begin_transaction().await.unwrap();
171 assert!(mutator.transaction().is_some());
172
173 assert!(mutator.begin_transaction().await.is_err());
175 }
176
177 #[tokio::test]
178 async fn test_rollback() {
179 let mut mutator = Mutator::new();
180
181 mutator.begin_transaction().await.unwrap();
182 assert!(mutator.transaction().is_some());
183
184 mutator.rollback().await.unwrap();
185 assert!(mutator.transaction().is_none());
186 }
187
188 #[tokio::test]
189 async fn test_apply_step_create_snapshots_file() {
190 let temp_dir = TempDir::new().unwrap();
191 let file_path = temp_dir.path().join("test.rs");
192
193 let mut mutator = Mutator::new();
194 mutator.begin_transaction().await.unwrap();
195
196 let step = crate::planner::PlanStep {
197 description: "Create test file".to_string(),
198 operation: crate::planner::PlanOperation::Create {
199 path: file_path.to_string_lossy().to_string(),
200 content: "fn test() {}".to_string(),
201 },
202 };
203
204 mutator.apply_step(&step).await.unwrap();
205
206 assert!(file_path.exists());
208 assert!(mutator.transaction().is_some());
210
211 mutator.rollback().await.unwrap();
213 }
214
215 #[tokio::test]
216 async fn test_apply_step_modify_snapshots_file() {
217 let temp_dir = TempDir::new().unwrap();
218 let file_path = temp_dir.path().join("test.rs");
219 tokio::fs::write(&file_path, "original content").await.unwrap();
220
221 let mut mutator = Mutator::new();
222 mutator.begin_transaction().await.unwrap();
223
224 let step = crate::planner::PlanStep {
225 description: "Modify test file".to_string(),
226 operation: crate::planner::PlanOperation::Modify {
227 file: file_path.to_string_lossy().to_string(),
228 start: 0,
229 end: 8,
230 },
231 };
232
233 mutator.apply_step(&step).await.unwrap();
234
235 assert!(mutator.transaction().is_some());
237
238 mutator.rollback().await.unwrap();
240 assert!(file_path.exists());
242 let content = tokio::fs::read_to_string(&file_path).await.unwrap();
243 assert_eq!(content, "original content");
244 }
245
246 #[tokio::test]
247 async fn test_commit_transaction() {
248 let mut mutator = Mutator::new();
249 mutator.begin_transaction().await.unwrap();
250
251 let commit_id = mutator.commit_transaction().await.unwrap();
252
253 assert_ne!(commit_id, Uuid::default());
256 }
257
258 #[tokio::test]
259 async fn test_into_transaction() {
260 let mut mutator = Mutator::new();
261 mutator.begin_transaction().await.unwrap();
262
263 let transaction = mutator.into_transaction().unwrap();
264
265 assert_eq!(transaction.state(), &crate::transaction::TransactionState::Active);
268 }
269
270 #[tokio::test]
271 async fn test_into_transaction_without_active_tx() {
272 let mutator = Mutator::new();
273
274 let result = mutator.into_transaction();
275
276 assert!(result.is_err());
277 }
278
279 #[tokio::test]
280 async fn test_rollback_restores_file_content() {
281 let temp_dir = TempDir::new().unwrap();
282 let file_path = temp_dir.path().join("test.rs");
283 tokio::fs::write(&file_path, "original content").await.unwrap();
284
285 let mut mutator = Mutator::new();
286 mutator.begin_transaction().await.unwrap();
287
288 let step = crate::planner::PlanStep {
290 description: "Create test file".to_string(),
291 operation: crate::planner::PlanOperation::Create {
292 path: file_path.to_string_lossy().to_string(),
293 content: "modified content".to_string(),
294 },
295 };
296
297 mutator.apply_step(&step).await.unwrap();
298
299 let content = tokio::fs::read_to_string(&file_path).await.unwrap();
301 assert_eq!(content, "modified content");
302
303 mutator.rollback().await.unwrap();
305
306 let content = tokio::fs::read_to_string(&file_path).await.unwrap();
308 assert_eq!(content, "original content");
309 }
310
311 #[tokio::test]
312 async fn test_preview() {
313 let mutator = Mutator::new();
314
315 let steps = vec![
316 crate::planner::PlanStep {
317 description: "Create file".to_string(),
318 operation: crate::planner::PlanOperation::Create {
319 path: "/tmp/test.rs".to_string(),
320 content: "fn test() {}".to_string(),
321 },
322 },
323 crate::planner::PlanStep {
324 description: "Delete file".to_string(),
325 operation: crate::planner::PlanOperation::Delete {
326 name: "old.rs".to_string(),
327 },
328 },
329 ];
330
331 let previews = mutator.preview(&steps).await.unwrap();
332
333 assert_eq!(previews.len(), 2);
334 assert!(previews[0].contains("Create"));
335 assert!(previews[0].contains("fn test() {}"));
336 assert!(previews[1].contains("Delete"));
337 }
338}