ragfs_fuse/
ops.rs

1//! Operations manager for agent file management.
2//!
3//! This module provides a structured interface for file operations with JSON feedback,
4//! designed for AI agents to manage files through the virtual `.ops/` directory.
5
6use chrono::{DateTime, Utc};
7use ragfs_core::VectorStore;
8use serde::{Deserialize, Serialize};
9use std::fs;
10use std::path::PathBuf;
11use std::sync::Arc;
12use tokio::sync::{RwLock, mpsc};
13use tracing::{debug, info, warn};
14use uuid::Uuid;
15
16use crate::safety::{HistoryOperation, SafetyManager, UndoData};
17
18/// A single file operation.
19#[derive(Debug, Clone, Serialize, Deserialize)]
20#[serde(tag = "op", rename_all = "snake_case")]
21pub enum Operation {
22    /// Create a new file with content
23    Create { path: PathBuf, content: String },
24    /// Delete a file (soft delete if safety layer is enabled)
25    Delete { path: PathBuf },
26    /// Move/rename a file
27    Move { src: PathBuf, dst: PathBuf },
28    /// Copy a file
29    Copy { src: PathBuf, dst: PathBuf },
30    /// Write content to a file (overwrite or append)
31    Write {
32        path: PathBuf,
33        content: String,
34        #[serde(default)]
35        append: bool,
36    },
37    /// Create a directory
38    Mkdir { path: PathBuf },
39    /// Create a symbolic link
40    Symlink { target: PathBuf, link: PathBuf },
41}
42
43/// Batch operation request.
44#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct BatchRequest {
46    /// List of operations to perform
47    pub operations: Vec<Operation>,
48    /// If true, rollback all operations if any fails
49    #[serde(default)]
50    pub atomic: bool,
51    /// If true, only validate without executing
52    #[serde(default)]
53    pub dry_run: bool,
54}
55
56/// Result of a single operation.
57#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct OperationResult {
59    /// Unique identifier for this operation
60    pub id: Uuid,
61    /// Whether the operation succeeded
62    pub success: bool,
63    /// Type of operation performed
64    pub operation: String,
65    /// Primary path involved
66    pub path: PathBuf,
67    /// Error message if failed
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub error: Option<String>,
70    /// Timestamp of the operation
71    pub timestamp: DateTime<Utc>,
72    /// Whether the file was indexed/reindexed
73    pub indexed: bool,
74    /// ID for undoing this operation (if reversible)
75    #[serde(skip_serializing_if = "Option::is_none")]
76    pub undo_id: Option<Uuid>,
77}
78
79impl OperationResult {
80    /// Create a successful operation result.
81    pub fn success(operation: &str, path: PathBuf, indexed: bool) -> Self {
82        Self {
83            id: Uuid::new_v4(),
84            success: true,
85            operation: operation.to_string(),
86            path,
87            error: None,
88            timestamp: Utc::now(),
89            indexed,
90            undo_id: Some(Uuid::new_v4()),
91        }
92    }
93
94    /// Create a failed operation result.
95    pub fn failure(operation: &str, path: PathBuf, error: String) -> Self {
96        Self {
97            id: Uuid::new_v4(),
98            success: false,
99            operation: operation.to_string(),
100            path,
101            error: Some(error),
102            timestamp: Utc::now(),
103            indexed: false,
104            undo_id: None,
105        }
106    }
107}
108
109/// Result of batch operations.
110#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct BatchResult {
112    /// Unique identifier for this batch
113    pub id: Uuid,
114    /// Whether all operations succeeded
115    pub success: bool,
116    /// Total number of operations
117    pub total: usize,
118    /// Number of successful operations
119    pub succeeded: usize,
120    /// Number of failed operations
121    pub failed: usize,
122    /// Individual operation results
123    pub results: Vec<OperationResult>,
124    /// Timestamp of the batch
125    pub timestamp: DateTime<Utc>,
126    /// Whether a rollback was performed (for atomic batches)
127    #[serde(skip_serializing_if = "Option::is_none")]
128    pub rollback_performed: Option<bool>,
129    /// Details about rollback if it was attempted
130    #[serde(skip_serializing_if = "Option::is_none")]
131    pub rollback_details: Option<RollbackDetails>,
132}
133
134/// Details about rollback execution for atomic batches.
135#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct RollbackDetails {
137    /// Number of operations rolled back successfully
138    pub rolled_back: usize,
139    /// Number of operations that failed to rollback
140    pub rollback_failures: usize,
141    /// Errors encountered during rollback
142    pub errors: Vec<RollbackError>,
143}
144
145/// Error encountered during a rollback operation.
146#[derive(Debug, Clone, Serialize, Deserialize)]
147pub struct RollbackError {
148    /// Index of the operation in the original batch
149    pub operation_index: usize,
150    /// Error message
151    pub error: String,
152}
153
154/// Data needed to rollback an operation.
155/// This is internal to batch operations and more detailed than `UndoData`.
156#[derive(Debug, Clone)]
157enum RollbackData {
158    /// Rollback a create by deleting the created file
159    Create { created_path: PathBuf },
160    /// Rollback a delete by restoring from trash or backup content
161    Delete {
162        original_path: PathBuf,
163        trash_id: Option<Uuid>,
164        content_backup: Option<Vec<u8>>,
165    },
166    /// Rollback a move by moving back
167    Move { src: PathBuf, dst: PathBuf },
168    /// Rollback a copy by deleting the copy
169    Copy { copied_path: PathBuf },
170    /// Rollback a write by restoring previous content
171    Write {
172        path: PathBuf,
173        previous_content: Option<Vec<u8>>,
174        file_existed: bool,
175    },
176    /// Rollback a mkdir by removing the directory
177    Mkdir { created_path: PathBuf },
178    /// Rollback a symlink by removing the link
179    Symlink { link_path: PathBuf },
180}
181
182/// Journal entry tracking an executed operation for potential rollback.
183#[derive(Debug, Clone)]
184struct JournalEntry {
185    /// Index of the operation in the batch
186    operation_index: usize,
187    /// Data needed to rollback this operation
188    rollback_data: RollbackData,
189}
190
191/// Operations manager for file operations with feedback.
192pub struct OpsManager {
193    /// Source directory root
194    source: PathBuf,
195    /// Vector store for indexing operations
196    store: Option<Arc<dyn VectorStore>>,
197    /// Channel to send reindex requests
198    reindex_sender: Option<mpsc::Sender<PathBuf>>,
199    /// Safety manager for trash/history/undo (optional)
200    safety_manager: Option<Arc<SafetyManager>>,
201    /// Last operation result (for .result file)
202    last_result: Arc<RwLock<Option<OperationResult>>>,
203    /// Last batch result
204    last_batch_result: Arc<RwLock<Option<BatchResult>>>,
205}
206
207impl OpsManager {
208    /// Create a new operations manager.
209    pub fn new(
210        source: PathBuf,
211        store: Option<Arc<dyn VectorStore>>,
212        reindex_sender: Option<mpsc::Sender<PathBuf>>,
213    ) -> Self {
214        Self {
215            source,
216            store,
217            reindex_sender,
218            safety_manager: None,
219            last_result: Arc::new(RwLock::new(None)),
220            last_batch_result: Arc::new(RwLock::new(None)),
221        }
222    }
223
224    /// Create operations manager with safety manager.
225    pub fn with_safety(
226        source: PathBuf,
227        store: Option<Arc<dyn VectorStore>>,
228        reindex_sender: Option<mpsc::Sender<PathBuf>>,
229        safety_manager: Arc<SafetyManager>,
230    ) -> Self {
231        Self {
232            source,
233            store,
234            reindex_sender,
235            safety_manager: Some(safety_manager),
236            last_result: Arc::new(RwLock::new(None)),
237            last_batch_result: Arc::new(RwLock::new(None)),
238        }
239    }
240
241    /// Set the safety manager.
242    pub fn set_safety_manager(&mut self, safety_manager: Arc<SafetyManager>) {
243        self.safety_manager = Some(safety_manager);
244    }
245
246    /// Log operation to history if safety manager is available.
247    fn log_to_history(&self, operation: HistoryOperation, undo_data: Option<UndoData>) {
248        if let Some(ref safety) = self.safety_manager {
249            safety.log_success(operation, undo_data);
250        }
251    }
252
253    /// Log failure to history if safety manager is available.
254    fn log_failure_to_history(&self, operation: HistoryOperation, error: &str) {
255        if let Some(ref safety) = self.safety_manager {
256            safety.log_failure(operation, error.to_string());
257        }
258    }
259
260    /// Resolve a path relative to the source directory.
261    fn resolve_path(&self, path: &PathBuf) -> PathBuf {
262        if path.is_absolute() {
263            path.clone()
264        } else {
265            self.source.join(path)
266        }
267    }
268
269    /// Trigger reindexing for a path.
270    async fn trigger_reindex(&self, path: &PathBuf) -> bool {
271        if let Some(ref sender) = self.reindex_sender {
272            match sender.send(path.clone()).await {
273                Ok(()) => {
274                    debug!("Reindex triggered for {:?}", path);
275                    true
276                }
277                Err(e) => {
278                    warn!("Failed to trigger reindex for {:?}: {}", path, e);
279                    false
280                }
281            }
282        } else {
283            false
284        }
285    }
286
287    /// Delete from vector store.
288    async fn delete_from_store(&self, path: &PathBuf) {
289        if let Some(ref store) = self.store
290            && let Err(e) = store.delete_by_file_path(path).await
291        {
292            warn!("Failed to delete {:?} from store: {e}", path);
293        }
294    }
295
296    /// Update path in vector store.
297    async fn update_store_path(&self, from: &PathBuf, to: &PathBuf) {
298        if let Some(ref store) = self.store
299            && let Err(e) = store.update_file_path(from, to).await
300        {
301            warn!("Failed to update path in store {:?} -> {:?}: {e}", from, to);
302        }
303    }
304
305    /// Execute an operation and capture rollback data for atomic batches.
306    /// Returns (result, `rollback_data`) tuple.
307    async fn execute_with_rollback(
308        &self,
309        op: &Operation,
310    ) -> (OperationResult, Option<RollbackData>) {
311        match op {
312            Operation::Create { path, content } => {
313                let resolved = self.resolve_path(path);
314                let result = self.create(path, content).await;
315                let rollback = if result.success {
316                    Some(RollbackData::Create {
317                        created_path: resolved,
318                    })
319                } else {
320                    None
321                };
322                (result, rollback)
323            }
324            Operation::Delete { path } => {
325                let resolved = self.resolve_path(path);
326                // Capture content before delete for rollback (in case of hard delete)
327                let content_backup = if resolved.exists() && resolved.is_file() {
328                    fs::read(&resolved).ok()
329                } else {
330                    None
331                };
332
333                let result = self.delete(path).await;
334                let rollback = if result.success {
335                    Some(RollbackData::Delete {
336                        original_path: resolved,
337                        trash_id: result.undo_id, // From soft delete
338                        content_backup: if result.undo_id.is_none() {
339                            content_backup
340                        } else {
341                            None
342                        },
343                    })
344                } else {
345                    None
346                };
347                (result, rollback)
348            }
349            Operation::Move { src, dst } => {
350                let resolved_src = self.resolve_path(src);
351                let resolved_dst = self.resolve_path(dst);
352                let result = self.move_file(src, dst).await;
353                let rollback = if result.success {
354                    Some(RollbackData::Move {
355                        src: resolved_dst,
356                        dst: resolved_src,
357                    })
358                } else {
359                    None
360                };
361                (result, rollback)
362            }
363            Operation::Copy { src, dst } => {
364                let resolved_dst = self.resolve_path(dst);
365                let result = self.copy(src, dst).await;
366                let rollback = if result.success {
367                    Some(RollbackData::Copy {
368                        copied_path: resolved_dst,
369                    })
370                } else {
371                    None
372                };
373                (result, rollback)
374            }
375            Operation::Write {
376                path,
377                content,
378                append,
379            } => {
380                let resolved = self.resolve_path(path);
381                let file_existed = resolved.exists();
382                let previous_content = if file_existed {
383                    fs::read(&resolved).ok()
384                } else {
385                    None
386                };
387
388                let result = self.write(path, content, *append).await;
389                let rollback = if result.success {
390                    Some(RollbackData::Write {
391                        path: resolved,
392                        previous_content,
393                        file_existed,
394                    })
395                } else {
396                    None
397                };
398                (result, rollback)
399            }
400            Operation::Mkdir { path } => {
401                let resolved = self.resolve_path(path);
402                let result = self.mkdir(path).await;
403                let rollback = if result.success {
404                    Some(RollbackData::Mkdir {
405                        created_path: resolved,
406                    })
407                } else {
408                    None
409                };
410                (result, rollback)
411            }
412            Operation::Symlink { target, link } => {
413                let resolved_link = self.resolve_path(link);
414                let result = self.symlink(target, link).await;
415                let rollback = if result.success {
416                    Some(RollbackData::Symlink {
417                        link_path: resolved_link,
418                    })
419                } else {
420                    None
421                };
422                (result, rollback)
423            }
424        }
425    }
426
427    /// Rollback a single operation.
428    async fn rollback_operation(&self, rollback_data: &RollbackData) -> Result<(), String> {
429        match rollback_data {
430            RollbackData::Create { created_path } => {
431                // Undo create by deleting the file
432                if created_path.exists() {
433                    fs::remove_file(created_path)
434                        .map_err(|e| format!("Failed to rollback create: {e}"))?;
435                    // Remove from vector store
436                    self.delete_from_store(created_path).await;
437                }
438                Ok(())
439            }
440            RollbackData::Delete {
441                original_path,
442                trash_id,
443                content_backup,
444            } => {
445                // Try to restore from trash first
446                if let Some(id) = trash_id
447                    && let Some(ref safety) = self.safety_manager
448                {
449                    return safety
450                        .restore(*id)
451                        .await
452                        .map(|_| ())
453                        .map_err(|e| format!("Failed to restore from trash: {e}"));
454                }
455                // Fall back to content backup
456                if let Some(content) = content_backup {
457                    if let Some(parent) = original_path.parent() {
458                        fs::create_dir_all(parent)
459                            .map_err(|e| format!("Failed to create parent dir: {e}"))?;
460                    }
461                    fs::write(original_path, content)
462                        .map_err(|e| format!("Failed to restore content: {e}"))?;
463                    self.trigger_reindex(original_path).await;
464                    Ok(())
465                } else {
466                    Err("Cannot rollback delete: no trash entry or content backup".into())
467                }
468            }
469            RollbackData::Move { src, dst } => {
470                // Move back (src is current location, dst is original location)
471                if src.exists() {
472                    if let Some(parent) = dst.parent() {
473                        fs::create_dir_all(parent)
474                            .map_err(|e| format!("Failed to create parent dir: {e}"))?;
475                    }
476                    fs::rename(src, dst).map_err(|e| format!("Failed to rollback move: {e}"))?;
477                    self.update_store_path(src, dst).await;
478                }
479                Ok(())
480            }
481            RollbackData::Copy { copied_path } => {
482                // Delete the copy
483                if copied_path.exists() {
484                    fs::remove_file(copied_path)
485                        .map_err(|e| format!("Failed to rollback copy: {e}"))?;
486                    self.delete_from_store(copied_path).await;
487                }
488                Ok(())
489            }
490            RollbackData::Write {
491                path,
492                previous_content,
493                file_existed,
494            } => {
495                if *file_existed {
496                    if let Some(content) = previous_content {
497                        fs::write(path, content)
498                            .map_err(|e| format!("Failed to rollback write: {e}"))?;
499                    } else {
500                        return Err("Cannot rollback write: no previous content saved".into());
501                    }
502                } else {
503                    // File didn't exist before, delete it
504                    if path.exists() {
505                        fs::remove_file(path)
506                            .map_err(|e| format!("Failed to rollback write (delete): {e}"))?;
507                        self.delete_from_store(path).await;
508                    }
509                }
510                self.trigger_reindex(path).await;
511                Ok(())
512            }
513            RollbackData::Mkdir { created_path } => {
514                // Undo mkdir by removing the directory (only if empty)
515                if created_path.exists() && created_path.is_dir() {
516                    fs::remove_dir(created_path)
517                        .map_err(|e| format!("Failed to rollback mkdir: {e}"))?;
518                }
519                Ok(())
520            }
521            RollbackData::Symlink { link_path } => {
522                // Undo symlink by removing the link
523                if link_path.exists() || link_path.symlink_metadata().is_ok() {
524                    fs::remove_file(link_path)
525                        .map_err(|e| format!("Failed to rollback symlink: {e}"))?;
526                }
527                Ok(())
528            }
529        }
530    }
531
532    /// Perform rollback of journal entries in reverse order.
533    async fn perform_rollback(&self, journal: &[JournalEntry]) -> RollbackDetails {
534        let mut rolled_back = 0;
535        let mut rollback_failures = 0;
536        let mut errors = Vec::new();
537
538        // Rollback in reverse order
539        for entry in journal.iter().rev() {
540            match self.rollback_operation(&entry.rollback_data).await {
541                Ok(()) => {
542                    rolled_back += 1;
543                    info!("Rolled back operation index {}", entry.operation_index);
544                }
545                Err(e) => {
546                    rollback_failures += 1;
547                    warn!(
548                        "Failed to rollback operation index {}: {}",
549                        entry.operation_index, e
550                    );
551                    errors.push(RollbackError {
552                        operation_index: entry.operation_index,
553                        error: e,
554                    });
555                }
556            }
557        }
558
559        RollbackDetails {
560            rolled_back,
561            rollback_failures,
562            errors,
563        }
564    }
565
566    /// Create a new file with content.
567    pub async fn create(&self, path: &PathBuf, content: &str) -> OperationResult {
568        let resolved = self.resolve_path(path);
569        debug!("ops::create {:?}", resolved);
570
571        // Check if file already exists
572        if resolved.exists() {
573            return OperationResult::failure("create", path.clone(), "File already exists".into());
574        }
575
576        // Ensure parent directory exists
577        if let Some(parent) = resolved.parent()
578            && !parent.exists()
579            && let Err(e) = fs::create_dir_all(parent)
580        {
581            return OperationResult::failure(
582                "create",
583                path.clone(),
584                format!("Failed to create parent directory: {e}"),
585            );
586        }
587
588        // Create the file
589        match fs::write(&resolved, content) {
590            Ok(()) => {
591                info!("Created file: {:?}", resolved);
592                let indexed = self.trigger_reindex(&resolved).await;
593                let result = OperationResult::success("create", path.clone(), indexed);
594
595                // Log to history
596                self.log_to_history(
597                    HistoryOperation::Create {
598                        path: resolved.clone(),
599                    },
600                    Some(UndoData::Create { path: resolved }),
601                );
602
603                *self.last_result.write().await = Some(result.clone());
604                result
605            }
606            Err(e) => {
607                let error_msg = format!("Failed to create file: {e}");
608                self.log_failure_to_history(
609                    HistoryOperation::Create { path: resolved },
610                    &error_msg,
611                );
612
613                let result = OperationResult::failure("create", path.clone(), error_msg);
614                *self.last_result.write().await = Some(result.clone());
615                result
616            }
617        }
618    }
619
620    /// Delete a file.
621    /// Uses soft delete (move to trash) if safety manager is available.
622    pub async fn delete(&self, path: &PathBuf) -> OperationResult {
623        let resolved = self.resolve_path(path);
624        debug!("ops::delete {:?}", resolved);
625
626        if !resolved.exists() {
627            return OperationResult::failure("delete", path.clone(), "File not found".into());
628        }
629
630        if resolved.is_dir() {
631            return OperationResult::failure(
632                "delete",
633                path.clone(),
634                "Cannot delete directory with .delete, use rmdir".into(),
635            );
636        }
637
638        // Delete from store first
639        self.delete_from_store(&resolved).await;
640
641        // Try soft delete if safety manager is available
642        if let Some(ref safety) = self.safety_manager {
643            match safety.soft_delete(&resolved).await {
644                Ok(entry) => {
645                    info!("Soft deleted file: {:?} -> trash/{}", resolved, entry.id);
646
647                    // Log to history with undo data
648                    self.log_to_history(
649                        HistoryOperation::Delete {
650                            path: resolved,
651                            trash_id: Some(entry.id),
652                        },
653                        Some(UndoData::Delete { trash_id: entry.id }),
654                    );
655
656                    let mut result = OperationResult::success("delete", path.clone(), false);
657                    result.undo_id = Some(entry.id);
658                    *self.last_result.write().await = Some(result.clone());
659                    return result;
660                }
661                Err(e) => {
662                    warn!("Soft delete failed, falling back to hard delete: {e}");
663                }
664            }
665        }
666
667        // Hard delete (fallback or no safety manager)
668        match fs::remove_file(&resolved) {
669            Ok(()) => {
670                info!("Hard deleted file: {:?}", resolved);
671
672                self.log_to_history(
673                    HistoryOperation::Delete {
674                        path: resolved,
675                        trash_id: None,
676                    },
677                    None, // Hard delete is not reversible
678                );
679
680                let result = OperationResult::success("delete", path.clone(), false);
681                *self.last_result.write().await = Some(result.clone());
682                result
683            }
684            Err(e) => {
685                let error_msg = format!("Failed to delete file: {e}");
686                self.log_failure_to_history(
687                    HistoryOperation::Delete {
688                        path: resolved,
689                        trash_id: None,
690                    },
691                    &error_msg,
692                );
693
694                let result = OperationResult::failure("delete", path.clone(), error_msg);
695                *self.last_result.write().await = Some(result.clone());
696                result
697            }
698        }
699    }
700
701    /// Move/rename a file.
702    pub async fn move_file(&self, src: &PathBuf, dst: &PathBuf) -> OperationResult {
703        let resolved_src = self.resolve_path(src);
704        let resolved_dst = self.resolve_path(dst);
705        debug!("ops::move {:?} -> {:?}", resolved_src, resolved_dst);
706
707        if !resolved_src.exists() {
708            return OperationResult::failure("move", src.clone(), "Source file not found".into());
709        }
710
711        if resolved_dst.exists() {
712            return OperationResult::failure(
713                "move",
714                src.clone(),
715                "Destination already exists".into(),
716            );
717        }
718
719        // Ensure parent directory exists
720        if let Some(parent) = resolved_dst.parent()
721            && !parent.exists()
722            && let Err(e) = fs::create_dir_all(parent)
723        {
724            return OperationResult::failure(
725                "move",
726                src.clone(),
727                format!("Failed to create parent directory: {e}"),
728            );
729        }
730
731        // Move the file
732        match fs::rename(&resolved_src, &resolved_dst) {
733            Ok(()) => {
734                info!("Moved: {:?} -> {:?}", resolved_src, resolved_dst);
735                self.update_store_path(&resolved_src, &resolved_dst).await;
736
737                // Log to history
738                self.log_to_history(
739                    HistoryOperation::Move {
740                        src: resolved_src.clone(),
741                        dst: resolved_dst.clone(),
742                    },
743                    Some(UndoData::Move {
744                        src: resolved_dst,
745                        dst: resolved_src,
746                    }),
747                );
748
749                let result = OperationResult::success("move", dst.clone(), true);
750                *self.last_result.write().await = Some(result.clone());
751                result
752            }
753            Err(e) => {
754                let error_msg = format!("Failed to move file: {e}");
755                self.log_failure_to_history(
756                    HistoryOperation::Move {
757                        src: resolved_src,
758                        dst: resolved_dst,
759                    },
760                    &error_msg,
761                );
762
763                let result = OperationResult::failure("move", src.clone(), error_msg);
764                *self.last_result.write().await = Some(result.clone());
765                result
766            }
767        }
768    }
769
770    /// Copy a file.
771    pub async fn copy(&self, src: &PathBuf, dst: &PathBuf) -> OperationResult {
772        let resolved_src = self.resolve_path(src);
773        let resolved_dst = self.resolve_path(dst);
774        debug!("ops::copy {:?} -> {:?}", resolved_src, resolved_dst);
775
776        if !resolved_src.exists() {
777            return OperationResult::failure("copy", src.clone(), "Source file not found".into());
778        }
779
780        if resolved_dst.exists() {
781            return OperationResult::failure(
782                "copy",
783                src.clone(),
784                "Destination already exists".into(),
785            );
786        }
787
788        // Ensure parent directory exists
789        if let Some(parent) = resolved_dst.parent()
790            && !parent.exists()
791            && let Err(e) = fs::create_dir_all(parent)
792        {
793            return OperationResult::failure(
794                "copy",
795                src.clone(),
796                format!("Failed to create parent directory: {e}"),
797            );
798        }
799
800        // Copy the file
801        match fs::copy(&resolved_src, &resolved_dst) {
802            Ok(_) => {
803                info!("Copied: {:?} -> {:?}", resolved_src, resolved_dst);
804                let indexed = self.trigger_reindex(&resolved_dst).await;
805
806                // Log to history
807                self.log_to_history(
808                    HistoryOperation::Copy {
809                        src: resolved_src,
810                        dst: resolved_dst.clone(),
811                    },
812                    Some(UndoData::Copy { path: resolved_dst }),
813                );
814
815                let result = OperationResult::success("copy", dst.clone(), indexed);
816                *self.last_result.write().await = Some(result.clone());
817                result
818            }
819            Err(e) => {
820                let error_msg = format!("Failed to copy file: {e}");
821                self.log_failure_to_history(
822                    HistoryOperation::Copy {
823                        src: resolved_src,
824                        dst: resolved_dst,
825                    },
826                    &error_msg,
827                );
828
829                let result = OperationResult::failure("copy", src.clone(), error_msg);
830                *self.last_result.write().await = Some(result.clone());
831                result
832            }
833        }
834    }
835
836    /// Write content to a file.
837    pub async fn write(&self, path: &PathBuf, content: &str, append: bool) -> OperationResult {
838        let resolved = self.resolve_path(path);
839        debug!("ops::write {:?} (append={})", resolved, append);
840
841        let write_result = if append {
842            use std::io::Write;
843            fs::OpenOptions::new()
844                .create(true)
845                .append(true)
846                .open(&resolved)
847                .and_then(|mut f| f.write_all(content.as_bytes()))
848        } else {
849            fs::write(&resolved, content)
850        };
851
852        match write_result {
853            Ok(()) => {
854                info!("Wrote to file: {:?}", resolved);
855                let indexed = self.trigger_reindex(&resolved).await;
856
857                // Log to history (write is not reversible without storing previous content)
858                self.log_to_history(
859                    HistoryOperation::Write {
860                        path: resolved,
861                        append,
862                    },
863                    None, // No undo data - would need to store previous content
864                );
865
866                let result = OperationResult::success("write", path.clone(), indexed);
867                *self.last_result.write().await = Some(result.clone());
868                result
869            }
870            Err(e) => {
871                let error_msg = format!("Failed to write file: {e}");
872                self.log_failure_to_history(
873                    HistoryOperation::Write {
874                        path: resolved,
875                        append,
876                    },
877                    &error_msg,
878                );
879
880                let result = OperationResult::failure("write", path.clone(), error_msg);
881                *self.last_result.write().await = Some(result.clone());
882                result
883            }
884        }
885    }
886
887    /// Create a directory.
888    pub async fn mkdir(&self, path: &PathBuf) -> OperationResult {
889        let resolved = self.resolve_path(path);
890        debug!("ops::mkdir {:?}", resolved);
891
892        if resolved.exists() {
893            return OperationResult::failure("mkdir", path.clone(), "Path already exists".into());
894        }
895
896        match fs::create_dir_all(&resolved) {
897            Ok(()) => {
898                info!("Created directory: {:?}", resolved);
899
900                // Note: Directories are not indexed, so we pass false
901                let mut result = OperationResult::success("mkdir", path.clone(), false);
902                // mkdir undo_id refers to the operation, not trash
903                result.undo_id = Some(Uuid::new_v4());
904
905                *self.last_result.write().await = Some(result.clone());
906                result
907            }
908            Err(e) => {
909                let error_msg = format!("Failed to create directory: {e}");
910                let result = OperationResult::failure("mkdir", path.clone(), error_msg);
911                *self.last_result.write().await = Some(result.clone());
912                result
913            }
914        }
915    }
916
917    /// Create a symbolic link.
918    #[cfg(unix)]
919    pub async fn symlink(&self, target: &PathBuf, link: &PathBuf) -> OperationResult {
920        let resolved_target = self.resolve_path(target);
921        let resolved_link = self.resolve_path(link);
922        debug!("ops::symlink {:?} -> {:?}", resolved_link, resolved_target);
923
924        if resolved_link.exists() {
925            return OperationResult::failure(
926                "symlink",
927                link.clone(),
928                "Link path already exists".into(),
929            );
930        }
931
932        // Ensure parent directory exists
933        if let Some(parent) = resolved_link.parent()
934            && !parent.exists()
935            && let Err(e) = fs::create_dir_all(parent)
936        {
937            return OperationResult::failure(
938                "symlink",
939                link.clone(),
940                format!("Failed to create parent directory: {e}"),
941            );
942        }
943
944        match std::os::unix::fs::symlink(&resolved_target, &resolved_link) {
945            Ok(()) => {
946                info!(
947                    "Created symlink: {:?} -> {:?}",
948                    resolved_link, resolved_target
949                );
950
951                let mut result = OperationResult::success("symlink", link.clone(), false);
952                result.undo_id = Some(Uuid::new_v4());
953
954                *self.last_result.write().await = Some(result.clone());
955                result
956            }
957            Err(e) => {
958                let error_msg = format!("Failed to create symlink: {e}");
959                let result = OperationResult::failure("symlink", link.clone(), error_msg);
960                *self.last_result.write().await = Some(result.clone());
961                result
962            }
963        }
964    }
965
966    /// Create a symbolic link (non-Unix fallback - not supported).
967    #[cfg(not(unix))]
968    pub async fn symlink(&self, _target: &PathBuf, link: &PathBuf) -> OperationResult {
969        OperationResult::failure(
970            "symlink",
971            link.clone(),
972            "Symlinks are not supported on this platform".into(),
973        )
974    }
975
976    /// Execute a single operation.
977    pub async fn execute_operation(&self, op: &Operation) -> OperationResult {
978        match op {
979            Operation::Create { path, content } => self.create(path, content).await,
980            Operation::Delete { path } => self.delete(path).await,
981            Operation::Move { src, dst } => self.move_file(src, dst).await,
982            Operation::Copy { src, dst } => self.copy(src, dst).await,
983            Operation::Write {
984                path,
985                content,
986                append,
987            } => self.write(path, content, *append).await,
988            Operation::Mkdir { path } => self.mkdir(path).await,
989            Operation::Symlink { target, link } => self.symlink(target, link).await,
990        }
991    }
992
993    /// Execute a batch of operations.
994    pub async fn batch(&self, request: BatchRequest) -> BatchResult {
995        let batch_id = Uuid::new_v4();
996        let total = request.operations.len();
997        let mut results = Vec::with_capacity(total);
998        let mut succeeded = 0;
999        let mut failed = 0;
1000        let mut journal: Vec<JournalEntry> = Vec::new();
1001
1002        debug!(
1003            "ops::batch {} operations (atomic={}, dry_run={})",
1004            total, request.atomic, request.dry_run
1005        );
1006
1007        if request.dry_run {
1008            // Just validate operations without executing
1009            for op in &request.operations {
1010                let result = self.validate_operation(op);
1011                if result.success {
1012                    succeeded += 1;
1013                } else {
1014                    failed += 1;
1015                }
1016                results.push(result);
1017            }
1018
1019            let batch_result = BatchResult {
1020                id: batch_id,
1021                success: failed == 0,
1022                total,
1023                succeeded,
1024                failed,
1025                results,
1026                timestamp: Utc::now(),
1027                rollback_performed: None,
1028                rollback_details: None,
1029            };
1030
1031            *self.last_batch_result.write().await = Some(batch_result.clone());
1032            return batch_result;
1033        }
1034
1035        // Execute operations with journal for atomic rollback
1036        for (index, op) in request.operations.iter().enumerate() {
1037            let (result, rollback_data) = self.execute_with_rollback(op).await;
1038
1039            if result.success {
1040                succeeded += 1;
1041                // Record in journal for potential rollback
1042                if request.atomic
1043                    && let Some(rd) = rollback_data
1044                {
1045                    journal.push(JournalEntry {
1046                        operation_index: index,
1047                        rollback_data: rd,
1048                    });
1049                }
1050                results.push(result);
1051            } else {
1052                failed += 1;
1053                results.push(result);
1054
1055                if request.atomic && !journal.is_empty() {
1056                    // Perform rollback of all previously successful operations
1057                    info!(
1058                        "Batch failed at operation {}, rolling back {} operations",
1059                        index,
1060                        journal.len()
1061                    );
1062                    let rollback_details = self.perform_rollback(&journal).await;
1063                    let rollback_success = rollback_details.rollback_failures == 0;
1064
1065                    let batch_result = BatchResult {
1066                        id: batch_id,
1067                        success: false,
1068                        total,
1069                        succeeded,
1070                        failed,
1071                        results,
1072                        timestamp: Utc::now(),
1073                        rollback_performed: Some(rollback_success),
1074                        rollback_details: Some(rollback_details),
1075                    };
1076
1077                    *self.last_batch_result.write().await = Some(batch_result.clone());
1078                    return batch_result;
1079                } else if request.atomic {
1080                    // First operation failed, no rollback needed
1081                    let batch_result = BatchResult {
1082                        id: batch_id,
1083                        success: false,
1084                        total,
1085                        succeeded,
1086                        failed,
1087                        results,
1088                        timestamp: Utc::now(),
1089                        rollback_performed: None,
1090                        rollback_details: None,
1091                    };
1092
1093                    *self.last_batch_result.write().await = Some(batch_result.clone());
1094                    return batch_result;
1095                }
1096                // Non-atomic: continue with next operation
1097            }
1098        }
1099
1100        let batch_result = BatchResult {
1101            id: batch_id,
1102            success: failed == 0,
1103            total,
1104            succeeded,
1105            failed,
1106            results,
1107            timestamp: Utc::now(),
1108            rollback_performed: None,
1109            rollback_details: None,
1110        };
1111
1112        *self.last_batch_result.write().await = Some(batch_result.clone());
1113        batch_result
1114    }
1115
1116    /// Validate an operation without executing it.
1117    fn validate_operation(&self, op: &Operation) -> OperationResult {
1118        match op {
1119            Operation::Create { path, .. } => {
1120                let resolved = self.resolve_path(path);
1121                if resolved.exists() {
1122                    OperationResult::failure("create", path.clone(), "File already exists".into())
1123                } else {
1124                    OperationResult::success("create", path.clone(), false)
1125                }
1126            }
1127            Operation::Delete { path } => {
1128                let resolved = self.resolve_path(path);
1129                if !resolved.exists() {
1130                    OperationResult::failure("delete", path.clone(), "File not found".into())
1131                } else if resolved.is_dir() {
1132                    OperationResult::failure(
1133                        "delete",
1134                        path.clone(),
1135                        "Cannot delete directory".into(),
1136                    )
1137                } else {
1138                    OperationResult::success("delete", path.clone(), false)
1139                }
1140            }
1141            Operation::Move { src, dst } => {
1142                let resolved_src = self.resolve_path(src);
1143                let resolved_dst = self.resolve_path(dst);
1144                if !resolved_src.exists() {
1145                    OperationResult::failure("move", src.clone(), "Source not found".into())
1146                } else if resolved_dst.exists() {
1147                    OperationResult::failure(
1148                        "move",
1149                        src.clone(),
1150                        "Destination already exists".into(),
1151                    )
1152                } else {
1153                    OperationResult::success("move", dst.clone(), false)
1154                }
1155            }
1156            Operation::Copy { src, dst } => {
1157                let resolved_src = self.resolve_path(src);
1158                let resolved_dst = self.resolve_path(dst);
1159                if !resolved_src.exists() {
1160                    OperationResult::failure("copy", src.clone(), "Source not found".into())
1161                } else if resolved_dst.exists() {
1162                    OperationResult::failure(
1163                        "copy",
1164                        src.clone(),
1165                        "Destination already exists".into(),
1166                    )
1167                } else {
1168                    OperationResult::success("copy", dst.clone(), false)
1169                }
1170            }
1171            Operation::Write { path, .. } => {
1172                // Write can always succeed (creates file if not exists)
1173                OperationResult::success("write", path.clone(), false)
1174            }
1175            Operation::Mkdir { path } => {
1176                let resolved = self.resolve_path(path);
1177                if resolved.exists() {
1178                    OperationResult::failure("mkdir", path.clone(), "Path already exists".into())
1179                } else {
1180                    OperationResult::success("mkdir", path.clone(), false)
1181                }
1182            }
1183            Operation::Symlink { target: _, link } => {
1184                let resolved_link = self.resolve_path(link);
1185                if resolved_link.exists() {
1186                    OperationResult::failure(
1187                        "symlink",
1188                        link.clone(),
1189                        "Link path already exists".into(),
1190                    )
1191                } else {
1192                    OperationResult::success("symlink", link.clone(), false)
1193                }
1194            }
1195        }
1196    }
1197
1198    /// Get the last operation result as JSON.
1199    pub async fn get_last_result(&self) -> Vec<u8> {
1200        let result = self.last_result.read().await;
1201        if let Some(r) = &*result {
1202            serde_json::to_string_pretty(r)
1203                .unwrap_or_else(|_| "{}".to_string())
1204                .into_bytes()
1205        } else {
1206            let empty = serde_json::json!({
1207                "message": "No operations performed yet"
1208            });
1209            serde_json::to_string_pretty(&empty)
1210                .unwrap_or_default()
1211                .into_bytes()
1212        }
1213    }
1214
1215    /// Get the last batch result as JSON.
1216    #[allow(dead_code)]
1217    pub async fn get_last_batch_result(&self) -> Vec<u8> {
1218        let result = self.last_batch_result.read().await;
1219        if let Some(r) = &*result {
1220            serde_json::to_string_pretty(r)
1221                .unwrap_or_else(|_| "{}".to_string())
1222                .into_bytes()
1223        } else {
1224            let empty = serde_json::json!({
1225                "message": "No batch operations performed yet"
1226            });
1227            serde_json::to_string_pretty(&empty)
1228                .unwrap_or_default()
1229                .into_bytes()
1230        }
1231    }
1232
1233    /// Parse and execute a create operation from string input.
1234    /// Format: "path\ncontent"
1235    pub async fn parse_and_create(&self, input: &str) -> OperationResult {
1236        let parts: Vec<&str> = input.splitn(2, '\n').collect();
1237        if parts.len() < 2 {
1238            return OperationResult::failure(
1239                "create",
1240                PathBuf::new(),
1241                "Invalid format. Expected: path\\ncontent".into(),
1242            );
1243        }
1244        let path = PathBuf::from(parts[0].trim());
1245        let content = parts[1];
1246        self.create(&path, content).await
1247    }
1248
1249    /// Parse and execute a delete operation from string input.
1250    /// Format: "path"
1251    pub async fn parse_and_delete(&self, input: &str) -> OperationResult {
1252        let path = PathBuf::from(input.trim());
1253        if path.as_os_str().is_empty() {
1254            return OperationResult::failure(
1255                "delete",
1256                PathBuf::new(),
1257                "Invalid format. Expected: path".into(),
1258            );
1259        }
1260        self.delete(&path).await
1261    }
1262
1263    /// Parse and execute a move operation from string input.
1264    /// Format: "src\ndst"
1265    pub async fn parse_and_move(&self, input: &str) -> OperationResult {
1266        let parts: Vec<&str> = input.splitn(2, '\n').collect();
1267        if parts.len() < 2 {
1268            return OperationResult::failure(
1269                "move",
1270                PathBuf::new(),
1271                "Invalid format. Expected: src\\ndst".into(),
1272            );
1273        }
1274        let src = PathBuf::from(parts[0].trim());
1275        let dst = PathBuf::from(parts[1].trim());
1276        self.move_file(&src, &dst).await
1277    }
1278
1279    /// Parse and execute a batch operation from JSON input.
1280    pub async fn parse_and_batch(&self, input: &str) -> BatchResult {
1281        match serde_json::from_str::<BatchRequest>(input) {
1282            Ok(request) => self.batch(request).await,
1283            Err(e) => BatchResult {
1284                id: Uuid::new_v4(),
1285                success: false,
1286                total: 0,
1287                succeeded: 0,
1288                failed: 1,
1289                results: vec![OperationResult::failure(
1290                    "batch",
1291                    PathBuf::new(),
1292                    format!("Invalid JSON: {e}"),
1293                )],
1294                timestamp: Utc::now(),
1295                rollback_performed: None,
1296                rollback_details: None,
1297            },
1298        }
1299    }
1300}
1301
1302#[cfg(test)]
1303mod tests {
1304    use super::*;
1305    use tempfile::TempDir;
1306
1307    fn create_test_manager() -> (OpsManager, TempDir) {
1308        let temp_dir = TempDir::new().unwrap();
1309        let manager = OpsManager::new(temp_dir.path().to_path_buf(), None, None);
1310        (manager, temp_dir)
1311    }
1312
1313    #[tokio::test]
1314    async fn test_create_file() {
1315        let (manager, _temp) = create_test_manager();
1316        let path = PathBuf::from("test.txt");
1317
1318        let result = manager.create(&path, "Hello, World!").await;
1319
1320        assert!(result.success);
1321        assert_eq!(result.operation, "create");
1322        assert!(manager.resolve_path(&path).exists());
1323    }
1324
1325    #[tokio::test]
1326    async fn test_create_file_already_exists() {
1327        let (manager, temp) = create_test_manager();
1328        let path = PathBuf::from("existing.txt");
1329        fs::write(temp.path().join("existing.txt"), "content").unwrap();
1330
1331        let result = manager.create(&path, "new content").await;
1332
1333        assert!(!result.success);
1334        assert!(result.error.unwrap().contains("already exists"));
1335    }
1336
1337    #[tokio::test]
1338    async fn test_delete_file() {
1339        let (manager, temp) = create_test_manager();
1340        let path = PathBuf::from("to_delete.txt");
1341        fs::write(temp.path().join("to_delete.txt"), "content").unwrap();
1342
1343        let result = manager.delete(&path).await;
1344
1345        assert!(result.success);
1346        assert!(!manager.resolve_path(&path).exists());
1347    }
1348
1349    #[tokio::test]
1350    async fn test_delete_nonexistent() {
1351        let (manager, _temp) = create_test_manager();
1352        let path = PathBuf::from("nonexistent.txt");
1353
1354        let result = manager.delete(&path).await;
1355
1356        assert!(!result.success);
1357        assert!(result.error.unwrap().contains("not found"));
1358    }
1359
1360    #[tokio::test]
1361    async fn test_move_file() {
1362        let (manager, temp) = create_test_manager();
1363        let src = PathBuf::from("source.txt");
1364        let dst = PathBuf::from("dest.txt");
1365        fs::write(temp.path().join("source.txt"), "content").unwrap();
1366
1367        let result = manager.move_file(&src, &dst).await;
1368
1369        assert!(result.success);
1370        assert!(!manager.resolve_path(&src).exists());
1371        assert!(manager.resolve_path(&dst).exists());
1372    }
1373
1374    #[tokio::test]
1375    async fn test_copy_file() {
1376        let (manager, temp) = create_test_manager();
1377        let src = PathBuf::from("original.txt");
1378        let dst = PathBuf::from("copy.txt");
1379        fs::write(temp.path().join("original.txt"), "content").unwrap();
1380
1381        let result = manager.copy(&src, &dst).await;
1382
1383        assert!(result.success);
1384        assert!(manager.resolve_path(&src).exists());
1385        assert!(manager.resolve_path(&dst).exists());
1386    }
1387
1388    #[tokio::test]
1389    async fn test_write_file() {
1390        let (manager, _temp) = create_test_manager();
1391        let path = PathBuf::from("write_test.txt");
1392
1393        let result = manager.write(&path, "content", false).await;
1394
1395        assert!(result.success);
1396        let content = fs::read_to_string(manager.resolve_path(&path)).unwrap();
1397        assert_eq!(content, "content");
1398    }
1399
1400    #[tokio::test]
1401    async fn test_write_append() {
1402        let (manager, temp) = create_test_manager();
1403        let path = PathBuf::from("append_test.txt");
1404        fs::write(temp.path().join("append_test.txt"), "first").unwrap();
1405
1406        let result = manager.write(&path, " second", true).await;
1407
1408        assert!(result.success);
1409        let content = fs::read_to_string(manager.resolve_path(&path)).unwrap();
1410        assert_eq!(content, "first second");
1411    }
1412
1413    #[tokio::test]
1414    async fn test_batch_operations() {
1415        let (manager, _temp) = create_test_manager();
1416
1417        let request = BatchRequest {
1418            operations: vec![
1419                Operation::Create {
1420                    path: PathBuf::from("file1.txt"),
1421                    content: "content1".to_string(),
1422                },
1423                Operation::Create {
1424                    path: PathBuf::from("file2.txt"),
1425                    content: "content2".to_string(),
1426                },
1427            ],
1428            atomic: false,
1429            dry_run: false,
1430        };
1431
1432        let result = manager.batch(request).await;
1433
1434        assert!(result.success);
1435        assert_eq!(result.total, 2);
1436        assert_eq!(result.succeeded, 2);
1437        assert_eq!(result.failed, 0);
1438    }
1439
1440    #[tokio::test]
1441    async fn test_batch_dry_run() {
1442        let (manager, _temp) = create_test_manager();
1443
1444        let request = BatchRequest {
1445            operations: vec![Operation::Create {
1446                path: PathBuf::from("dry_run.txt"),
1447                content: "content".to_string(),
1448            }],
1449            atomic: false,
1450            dry_run: true,
1451        };
1452
1453        let result = manager.batch(request).await;
1454
1455        assert!(result.success);
1456        // File should NOT be created in dry run
1457        assert!(!manager.resolve_path(&PathBuf::from("dry_run.txt")).exists());
1458    }
1459
1460    #[tokio::test]
1461    async fn test_parse_and_create() {
1462        let (manager, _temp) = create_test_manager();
1463
1464        let result = manager.parse_and_create("test.txt\nHello!").await;
1465
1466        assert!(result.success);
1467        let content = fs::read_to_string(manager.resolve_path(&PathBuf::from("test.txt"))).unwrap();
1468        assert_eq!(content, "Hello!");
1469    }
1470
1471    #[tokio::test]
1472    async fn test_parse_and_move() {
1473        let (manager, temp) = create_test_manager();
1474        fs::write(temp.path().join("src.txt"), "content").unwrap();
1475
1476        let result = manager.parse_and_move("src.txt\ndst.txt").await;
1477
1478        assert!(result.success);
1479    }
1480
1481    #[tokio::test]
1482    async fn test_parse_and_batch() {
1483        let (manager, _temp) = create_test_manager();
1484
1485        let json = r#"{"operations":[{"op":"create","path":"batch.txt","content":"test"}]}"#;
1486        let result = manager.parse_and_batch(json).await;
1487
1488        assert!(result.success);
1489    }
1490
1491    #[tokio::test]
1492    async fn test_last_result() {
1493        let (manager, _temp) = create_test_manager();
1494
1495        // Initially no result
1496        let initial = manager.get_last_result().await;
1497        let initial_str = String::from_utf8(initial).unwrap();
1498        assert!(initial_str.contains("No operations"));
1499
1500        // After an operation
1501        manager.create(&PathBuf::from("test.txt"), "content").await;
1502        let after = manager.get_last_result().await;
1503        let after_str = String::from_utf8(after).unwrap();
1504        assert!(after_str.contains("create"));
1505        assert!(after_str.contains("success"));
1506    }
1507
1508    #[test]
1509    fn test_operation_result_success() {
1510        let result = OperationResult::success("test", PathBuf::from("/test"), true);
1511        assert!(result.success);
1512        assert!(result.error.is_none());
1513        assert!(result.undo_id.is_some());
1514    }
1515
1516    #[test]
1517    fn test_operation_result_failure() {
1518        let result = OperationResult::failure("test", PathBuf::from("/test"), "error".into());
1519        assert!(!result.success);
1520        assert!(result.error.is_some());
1521        assert!(result.undo_id.is_none());
1522    }
1523
1524    #[tokio::test]
1525    async fn test_atomic_batch_rollback_on_failure() {
1526        let (manager, temp) = create_test_manager();
1527
1528        // Create a file that will cause the second create to fail
1529        fs::write(temp.path().join("existing.txt"), "exists").unwrap();
1530
1531        let request = BatchRequest {
1532            operations: vec![
1533                Operation::Create {
1534                    path: PathBuf::from("new.txt"),
1535                    content: "content".to_string(),
1536                },
1537                Operation::Create {
1538                    path: PathBuf::from("existing.txt"), // Will fail
1539                    content: "content".to_string(),
1540                },
1541            ],
1542            atomic: true,
1543            dry_run: false,
1544        };
1545
1546        let result = manager.batch(request).await;
1547
1548        assert!(!result.success);
1549        assert_eq!(result.succeeded, 1);
1550        assert_eq!(result.failed, 1);
1551        assert_eq!(result.rollback_performed, Some(true));
1552        assert!(result.rollback_details.is_some());
1553        let details = result.rollback_details.unwrap();
1554        assert_eq!(details.rolled_back, 1);
1555        assert_eq!(details.rollback_failures, 0);
1556        // First file should be rolled back (deleted)
1557        assert!(!temp.path().join("new.txt").exists());
1558    }
1559
1560    #[tokio::test]
1561    async fn test_atomic_batch_rollback_move_operations() {
1562        let (manager, temp) = create_test_manager();
1563
1564        fs::write(temp.path().join("file1.txt"), "content1").unwrap();
1565
1566        let request = BatchRequest {
1567            operations: vec![
1568                Operation::Move {
1569                    src: PathBuf::from("file1.txt"),
1570                    dst: PathBuf::from("moved1.txt"),
1571                },
1572                Operation::Delete {
1573                    path: PathBuf::from("nonexistent.txt"), // Will fail
1574                },
1575            ],
1576            atomic: true,
1577            dry_run: false,
1578        };
1579
1580        let result = manager.batch(request).await;
1581
1582        assert!(!result.success);
1583        assert_eq!(result.rollback_performed, Some(true));
1584        // Move should be rolled back
1585        assert!(temp.path().join("file1.txt").exists());
1586        assert!(!temp.path().join("moved1.txt").exists());
1587    }
1588
1589    #[tokio::test]
1590    async fn test_atomic_batch_rollback_write_restores_content() {
1591        let (manager, temp) = create_test_manager();
1592
1593        fs::write(temp.path().join("existing.txt"), "original content").unwrap();
1594
1595        let request = BatchRequest {
1596            operations: vec![
1597                Operation::Write {
1598                    path: PathBuf::from("existing.txt"),
1599                    content: "modified content".to_string(),
1600                    append: false,
1601                },
1602                Operation::Delete {
1603                    path: PathBuf::from("nonexistent.txt"), // Will fail
1604                },
1605            ],
1606            atomic: true,
1607            dry_run: false,
1608        };
1609
1610        let result = manager.batch(request).await;
1611
1612        assert!(!result.success);
1613        assert_eq!(result.rollback_performed, Some(true));
1614        // Content should be restored
1615        let content = fs::read_to_string(temp.path().join("existing.txt")).unwrap();
1616        assert_eq!(content, "original content");
1617    }
1618
1619    #[tokio::test]
1620    async fn test_non_atomic_batch_no_rollback() {
1621        let (manager, temp) = create_test_manager();
1622
1623        let request = BatchRequest {
1624            operations: vec![
1625                Operation::Create {
1626                    path: PathBuf::from("file1.txt"),
1627                    content: "content".to_string(),
1628                },
1629                Operation::Delete {
1630                    path: PathBuf::from("nonexistent.txt"), // Will fail
1631                },
1632                Operation::Create {
1633                    path: PathBuf::from("file2.txt"),
1634                    content: "content".to_string(),
1635                },
1636            ],
1637            atomic: false, // Non-atomic
1638            dry_run: false,
1639        };
1640
1641        let result = manager.batch(request).await;
1642
1643        assert!(!result.success);
1644        assert!(result.rollback_performed.is_none());
1645        // Both files should exist (no rollback)
1646        assert!(temp.path().join("file1.txt").exists());
1647        assert!(temp.path().join("file2.txt").exists());
1648    }
1649
1650    #[tokio::test]
1651    async fn test_atomic_batch_first_op_fails_no_rollback_needed() {
1652        let (manager, temp) = create_test_manager();
1653
1654        // Create a file that will cause the first create to fail
1655        fs::write(temp.path().join("existing.txt"), "exists").unwrap();
1656
1657        let request = BatchRequest {
1658            operations: vec![
1659                Operation::Create {
1660                    path: PathBuf::from("existing.txt"), // Will fail immediately
1661                    content: "content".to_string(),
1662                },
1663                Operation::Create {
1664                    path: PathBuf::from("new.txt"),
1665                    content: "content".to_string(),
1666                },
1667            ],
1668            atomic: true,
1669            dry_run: false,
1670        };
1671
1672        let result = manager.batch(request).await;
1673
1674        assert!(!result.success);
1675        assert_eq!(result.succeeded, 0);
1676        assert_eq!(result.failed, 1);
1677        // No rollback needed since first operation failed
1678        assert!(result.rollback_performed.is_none());
1679        assert!(result.rollback_details.is_none());
1680        // Second operation should not have been attempted
1681        assert!(!temp.path().join("new.txt").exists());
1682    }
1683
1684    #[tokio::test]
1685    async fn test_atomic_batch_copy_rollback() {
1686        let (manager, temp) = create_test_manager();
1687
1688        fs::write(temp.path().join("source.txt"), "source content").unwrap();
1689
1690        let request = BatchRequest {
1691            operations: vec![
1692                Operation::Copy {
1693                    src: PathBuf::from("source.txt"),
1694                    dst: PathBuf::from("copied.txt"),
1695                },
1696                Operation::Delete {
1697                    path: PathBuf::from("nonexistent.txt"), // Will fail
1698                },
1699            ],
1700            atomic: true,
1701            dry_run: false,
1702        };
1703
1704        let result = manager.batch(request).await;
1705
1706        assert!(!result.success);
1707        assert_eq!(result.rollback_performed, Some(true));
1708        // Copy should be rolled back (deleted)
1709        assert!(temp.path().join("source.txt").exists());
1710        assert!(!temp.path().join("copied.txt").exists());
1711    }
1712
1713    #[tokio::test]
1714    async fn test_mkdir() {
1715        let (manager, temp) = create_test_manager();
1716        let path = PathBuf::from("new_directory");
1717
1718        let result = manager.mkdir(&path).await;
1719
1720        assert!(result.success);
1721        assert_eq!(result.operation, "mkdir");
1722        assert!(temp.path().join("new_directory").is_dir());
1723    }
1724
1725    #[tokio::test]
1726    async fn test_mkdir_nested() {
1727        let (manager, temp) = create_test_manager();
1728        let path = PathBuf::from("parent/child/grandchild");
1729
1730        let result = manager.mkdir(&path).await;
1731
1732        assert!(result.success);
1733        assert!(temp.path().join("parent/child/grandchild").is_dir());
1734    }
1735
1736    #[tokio::test]
1737    async fn test_mkdir_already_exists() {
1738        let (manager, temp) = create_test_manager();
1739        let path = PathBuf::from("existing_dir");
1740        fs::create_dir(temp.path().join("existing_dir")).unwrap();
1741
1742        let result = manager.mkdir(&path).await;
1743
1744        assert!(!result.success);
1745        assert!(result.error.unwrap().contains("already exists"));
1746    }
1747
1748    #[tokio::test]
1749    #[cfg(unix)]
1750    async fn test_symlink() {
1751        let (manager, temp) = create_test_manager();
1752        let target = PathBuf::from("target_file.txt");
1753        let link = PathBuf::from("link_to_target");
1754
1755        // Create target file
1756        fs::write(temp.path().join("target_file.txt"), "content").unwrap();
1757
1758        let result = manager.symlink(&target, &link).await;
1759
1760        assert!(result.success);
1761        assert_eq!(result.operation, "symlink");
1762        let link_path = temp.path().join("link_to_target");
1763        assert!(link_path.symlink_metadata().is_ok());
1764    }
1765
1766    #[tokio::test]
1767    #[cfg(unix)]
1768    async fn test_symlink_already_exists() {
1769        let (manager, temp) = create_test_manager();
1770        let target = PathBuf::from("target.txt");
1771        let link = PathBuf::from("existing_link");
1772
1773        // Create existing link path
1774        fs::write(temp.path().join("existing_link"), "content").unwrap();
1775
1776        let result = manager.symlink(&target, &link).await;
1777
1778        assert!(!result.success);
1779        assert!(result.error.unwrap().contains("already exists"));
1780    }
1781
1782    #[tokio::test]
1783    async fn test_atomic_batch_mkdir_rollback() {
1784        let (manager, temp) = create_test_manager();
1785
1786        let request = BatchRequest {
1787            operations: vec![
1788                Operation::Mkdir {
1789                    path: PathBuf::from("new_dir"),
1790                },
1791                Operation::Delete {
1792                    path: PathBuf::from("nonexistent.txt"), // Will fail
1793                },
1794            ],
1795            atomic: true,
1796            dry_run: false,
1797        };
1798
1799        let result = manager.batch(request).await;
1800
1801        assert!(!result.success);
1802        assert_eq!(result.rollback_performed, Some(true));
1803        // Directory should be rolled back (removed)
1804        assert!(!temp.path().join("new_dir").exists());
1805    }
1806}