ricecoder_undo_redo/
persistence.rs

1//! Persistence layer for history storage
2
3use crate::change::Change;
4use crate::checkpoint::Checkpoint;
5use crate::error::UndoRedoError;
6use chrono::{DateTime, Duration, Utc};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::fs;
10use std::path::{Path, PathBuf};
11
12/// Serializable history snapshot for persistence
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct HistorySnapshot {
15    /// All recorded changes
16    pub changes: Vec<Change>,
17    /// All checkpoints
18    pub checkpoints: HashMap<String, Checkpoint>,
19    /// Timestamp when snapshot was created
20    pub snapshot_time: DateTime<Utc>,
21}
22
23impl HistorySnapshot {
24    /// Create a new history snapshot
25    pub fn new(
26        changes: Vec<Change>,
27        checkpoints: HashMap<String, Checkpoint>,
28    ) -> Self {
29        HistorySnapshot {
30            changes,
31            checkpoints,
32            snapshot_time: Utc::now(),
33        }
34    }
35
36    /// Validate the snapshot for consistency
37    pub fn validate(&self) -> Result<(), UndoRedoError> {
38        // Validate all changes
39        for change in &self.changes {
40            change.validate()?;
41        }
42
43        // Validate all checkpoints
44        for checkpoint in self.checkpoints.values() {
45            checkpoint.validate()?;
46        }
47
48        Ok(())
49    }
50}
51
52/// Storage statistics
53#[derive(Debug, Clone)]
54pub struct StorageStats {
55    /// Total size in bytes
56    pub size_bytes: u64,
57    /// Number of entries
58    pub entry_count: usize,
59    /// Oldest entry timestamp
60    pub oldest_entry: Option<DateTime<Utc>>,
61    /// Newest entry timestamp
62    pub newest_entry: Option<DateTime<Utc>>,
63}
64
65/// Manages history persistence to disk
66pub struct HistoryStore {
67    storage_path: PathBuf,
68    history_data: Option<HistorySnapshot>,
69    max_retries: usize,
70}
71
72impl HistoryStore {
73    /// Create a new history store with the given storage path
74    pub fn new(storage_path: impl AsRef<Path>) -> Result<Self, UndoRedoError> {
75        let storage_path = storage_path.as_ref().to_path_buf();
76
77        // Ensure parent directory exists
78        if let Some(parent) = storage_path.parent() {
79            if !parent.exists() {
80                fs::create_dir_all(parent).map_err(|e| {
81                    UndoRedoError::storage_error(format!("Failed to create storage directory: {}", e))
82                })?;
83            }
84        }
85
86        Ok(HistoryStore {
87            storage_path,
88            history_data: None,
89            max_retries: 3,
90        })
91    }
92
93    /// Save history to disk with retry logic
94    pub fn save_history(&mut self, snapshot: HistorySnapshot) -> Result<(), UndoRedoError> {
95        // Validate snapshot before saving
96        snapshot.validate()?;
97
98        let mut last_error = None;
99
100        // Retry with exponential backoff
101        for attempt in 0..self.max_retries {
102            match self.save_history_attempt(&snapshot) {
103                Ok(_) => {
104                    self.history_data = Some(snapshot);
105                    return Ok(());
106                }
107                Err(e) => {
108                    last_error = Some(e);
109                    if attempt < self.max_retries - 1 {
110                        // Exponential backoff: 100ms, 200ms, 400ms
111                        let backoff_ms = 100 * (2_u64.pow(attempt as u32));
112                        std::thread::sleep(std::time::Duration::from_millis(backoff_ms));
113                    }
114                }
115            }
116        }
117
118        Err(last_error.unwrap_or_else(|| {
119            UndoRedoError::storage_error("Failed to save history after retries")
120        }))
121    }
122
123    /// Attempt to save history once
124    fn save_history_attempt(&self, snapshot: &HistorySnapshot) -> Result<(), UndoRedoError> {
125        let json = serde_json::to_string_pretty(snapshot)?;
126        fs::write(&self.storage_path, json).map_err(|e| {
127            UndoRedoError::storage_error(format!("Failed to write history file: {}", e))
128        })?;
129        Ok(())
130    }
131
132    /// Load history from disk with fallback to empty history
133    pub fn load_history(&mut self) -> Result<HistorySnapshot, UndoRedoError> {
134        // If file doesn't exist, return empty history
135        if !self.storage_path.exists() {
136            let empty_snapshot = HistorySnapshot::new(Vec::new(), HashMap::new());
137            self.history_data = Some(empty_snapshot.clone());
138            return Ok(empty_snapshot);
139        }
140
141        // Try to read and deserialize
142        match self.load_history_attempt() {
143            Ok(snapshot) => {
144                self.history_data = Some(snapshot.clone());
145                Ok(snapshot)
146            }
147            Err(e) => {
148                // Log warning and return empty history on load failure
149                eprintln!("Warning: Failed to load history: {}. Starting with empty history.", e);
150                let empty_snapshot = HistorySnapshot::new(Vec::new(), HashMap::new());
151                self.history_data = Some(empty_snapshot.clone());
152                Ok(empty_snapshot)
153            }
154        }
155    }
156
157    /// Attempt to load history once
158    fn load_history_attempt(&self) -> Result<HistorySnapshot, UndoRedoError> {
159        let content = fs::read_to_string(&self.storage_path).map_err(|e| {
160            UndoRedoError::storage_error(format!("Failed to read history file: {}", e))
161        })?;
162
163        let snapshot: HistorySnapshot = serde_json::from_str(&content).map_err(|e| {
164            UndoRedoError::storage_error(format!("Failed to deserialize history: {}", e))
165        })?;
166
167        // Validate loaded snapshot
168        snapshot.validate()?;
169
170        Ok(snapshot)
171    }
172
173    /// Clean up entries older than the specified number of days
174    pub fn cleanup_old_entries(&mut self, retention_days: i64) -> Result<(), UndoRedoError> {
175        let cutoff_time = Utc::now() - Duration::days(retention_days);
176
177        if let Some(snapshot) = &mut self.history_data {
178            // Filter out old changes
179            let original_count = snapshot.changes.len();
180            snapshot.changes.retain(|change| change.timestamp > cutoff_time);
181            let removed_count = original_count - snapshot.changes.len();
182
183            if removed_count > 0 {
184                eprintln!(
185                    "Cleaned up {} old history entries (older than {} days)",
186                    removed_count, retention_days
187                );
188            }
189
190            // Filter out old checkpoints
191            let original_checkpoint_count = snapshot.checkpoints.len();
192            snapshot.checkpoints.retain(|_, cp| cp.created_at > cutoff_time);
193            let removed_checkpoint_count = original_checkpoint_count - snapshot.checkpoints.len();
194
195            if removed_checkpoint_count > 0 {
196                eprintln!(
197                    "Cleaned up {} old checkpoints (older than {} days)",
198                    removed_checkpoint_count, retention_days
199                );
200            }
201        }
202
203        Ok(())
204    }
205
206    /// Get storage statistics
207    pub fn get_storage_stats(&self) -> Result<StorageStats, UndoRedoError> {
208        if !self.storage_path.exists() {
209            return Ok(StorageStats {
210                size_bytes: 0,
211                entry_count: 0,
212                oldest_entry: None,
213                newest_entry: None,
214            });
215        }
216
217        let metadata = fs::metadata(&self.storage_path).map_err(|e| {
218            UndoRedoError::storage_error(format!("Failed to get file metadata: {}", e))
219        })?;
220
221        let size_bytes = metadata.len();
222
223        let (entry_count, oldest_entry, newest_entry) = if let Some(snapshot) = &self.history_data {
224            let entry_count = snapshot.changes.len() + snapshot.checkpoints.len();
225
226            let oldest_entry = snapshot
227                .changes
228                .iter()
229                .map(|c| c.timestamp)
230                .chain(snapshot.checkpoints.values().map(|cp| cp.created_at))
231                .min();
232
233            let newest_entry = snapshot
234                .changes
235                .iter()
236                .map(|c| c.timestamp)
237                .chain(snapshot.checkpoints.values().map(|cp| cp.created_at))
238                .max();
239
240            (entry_count, oldest_entry, newest_entry)
241        } else {
242            (0, None, None)
243        };
244
245        Ok(StorageStats {
246            size_bytes,
247            entry_count,
248            oldest_entry,
249            newest_entry,
250        })
251    }
252
253    /// Check if storage is full (exceeds 1GB limit)
254    pub fn is_storage_full(&self) -> Result<bool, UndoRedoError> {
255        const MAX_STORAGE_BYTES: u64 = 1024 * 1024 * 1024; // 1GB
256        let stats = self.get_storage_stats()?;
257        Ok(stats.size_bytes > MAX_STORAGE_BYTES)
258    }
259
260    /// Handle storage full gracefully by logging warning
261    pub fn handle_storage_full(&self) -> Result<(), UndoRedoError> {
262        if self.is_storage_full()? {
263            eprintln!(
264                "Warning: History storage is full (>1GB). Consider cleaning up old entries."
265            );
266        }
267        Ok(())
268    }
269
270    /// Get the current history data
271    pub fn get_history_data(&self) -> Option<HistorySnapshot> {
272        self.history_data.clone()
273    }
274
275    /// Set the history data
276    pub fn set_history_data(&mut self, snapshot: HistorySnapshot) {
277        self.history_data = Some(snapshot);
278    }
279}
280
281/// Manages storage lifecycle including cleanup and size limits
282pub struct StorageManager {
283    store: HistoryStore,
284    retention_days: i64,
285    max_storage_bytes: u64,
286}
287
288impl StorageManager {
289    /// Create a new storage manager
290    pub fn new(
291        storage_path: impl AsRef<Path>,
292        retention_days: i64,
293        max_storage_bytes: u64,
294    ) -> Result<Self, UndoRedoError> {
295        let store = HistoryStore::new(storage_path)?;
296        Ok(StorageManager {
297            store,
298            retention_days,
299            max_storage_bytes,
300        })
301    }
302
303    /// Create a new storage manager with default settings (30 days retention, 1GB limit)
304    pub fn with_defaults(storage_path: impl AsRef<Path>) -> Result<Self, UndoRedoError> {
305        Self::new(storage_path, 30, 1024 * 1024 * 1024)
306    }
307
308    /// Perform automatic cleanup on session start
309    pub fn cleanup_on_session_start(&mut self) -> Result<(), UndoRedoError> {
310        // Load history first
311        self.store.load_history()?;
312
313        // Clean up old entries
314        self.store.cleanup_old_entries(self.retention_days)?;
315
316        // Check storage size and handle if full
317        if self.store.is_storage_full()? {
318            self.store.handle_storage_full()?;
319        }
320
321        Ok(())
322    }
323
324    /// Perform automatic cleanup on session end
325    pub fn cleanup_on_session_end(&mut self) -> Result<(), UndoRedoError> {
326        // Clean up old entries
327        self.store.cleanup_old_entries(self.retention_days)?;
328
329        // Check storage size and handle if full
330        if self.store.is_storage_full()? {
331            self.store.handle_storage_full()?;
332        }
333
334        Ok(())
335    }
336
337    /// Enforce storage size limit by removing oldest entries
338    pub fn enforce_storage_limit(&mut self) -> Result<(), UndoRedoError> {
339        let stats = self.store.get_storage_stats()?;
340
341        if stats.size_bytes > self.max_storage_bytes {
342            eprintln!(
343                "Storage limit exceeded: {} bytes > {} bytes. Removing oldest entries.",
344                stats.size_bytes, self.max_storage_bytes
345            );
346
347            // Remove entries older than retention period
348            self.store.cleanup_old_entries(self.retention_days)?;
349
350            // If still over limit, remove more aggressively
351            let stats_after = self.store.get_storage_stats()?;
352            if stats_after.size_bytes > self.max_storage_bytes {
353                eprintln!(
354                    "Still over limit after cleanup. Removing entries older than 7 days."
355                );
356                self.store.cleanup_old_entries(7)?;
357            }
358        }
359
360        Ok(())
361    }
362
363    /// Get the underlying history store
364    pub fn get_store(&self) -> &HistoryStore {
365        &self.store
366    }
367
368    /// Get the underlying history store mutably
369    pub fn get_store_mut(&mut self) -> &mut HistoryStore {
370        &mut self.store
371    }
372
373    /// Set retention days
374    pub fn set_retention_days(&mut self, days: i64) {
375        self.retention_days = days;
376    }
377
378    /// Set maximum storage bytes
379    pub fn set_max_storage_bytes(&mut self, bytes: u64) {
380        self.max_storage_bytes = bytes;
381    }
382
383    /// Get retention days
384    pub fn get_retention_days(&self) -> i64 {
385        self.retention_days
386    }
387
388    /// Get maximum storage bytes
389    pub fn get_max_storage_bytes(&self) -> u64 {
390        self.max_storage_bytes
391    }
392}
393
394#[cfg(test)]
395mod tests {
396    use super::*;
397    use crate::change::ChangeType;
398    use std::fs;
399    use tempfile::TempDir;
400
401    #[test]
402    fn test_history_store_create() {
403        let temp_dir = TempDir::new().unwrap();
404        let store_path = temp_dir.path().join("history.json");
405        let store = HistoryStore::new(&store_path);
406        assert!(store.is_ok());
407    }
408
409    #[test]
410    fn test_history_store_save_and_load() {
411        let temp_dir = TempDir::new().unwrap();
412        let store_path = temp_dir.path().join("history.json");
413
414        // Create and save
415        let mut store = HistoryStore::new(&store_path).unwrap();
416        let change = Change::new(
417            "test.txt",
418            "before",
419            "after",
420            "Test change",
421            ChangeType::Modify,
422        )
423        .unwrap();
424        let snapshot = HistorySnapshot::new(vec![change.clone()], HashMap::new());
425        store.save_history(snapshot).unwrap();
426
427        // Load and verify
428        let mut store2 = HistoryStore::new(&store_path).unwrap();
429        let loaded = store2.load_history().unwrap();
430        assert_eq!(loaded.changes.len(), 1);
431        assert_eq!(loaded.changes[0].id, change.id);
432    }
433
434    #[test]
435    fn test_history_store_load_nonexistent() {
436        let temp_dir = TempDir::new().unwrap();
437        let store_path = temp_dir.path().join("nonexistent.json");
438
439        let mut store = HistoryStore::new(&store_path).unwrap();
440        let loaded = store.load_history().unwrap();
441        assert_eq!(loaded.changes.len(), 0);
442        assert_eq!(loaded.checkpoints.len(), 0);
443    }
444
445    #[test]
446    fn test_history_store_cleanup_old_entries() {
447        let temp_dir = TempDir::new().unwrap();
448        let store_path = temp_dir.path().join("history.json");
449
450        let mut store = HistoryStore::new(&store_path).unwrap();
451
452        // Create changes with different timestamps
453        let mut changes = Vec::new();
454        for i in 0..3 {
455            let change = Change::new(
456                "test.txt",
457                "before",
458                "after",
459                &format!("Change {}", i),
460                ChangeType::Modify,
461            )
462            .unwrap();
463            changes.push(change);
464        }
465
466        let snapshot = HistorySnapshot::new(changes, HashMap::new());
467        store.set_history_data(snapshot);
468
469        // Cleanup entries older than 0 days (should remove all)
470        store.cleanup_old_entries(0).unwrap();
471
472        let stats = store.get_storage_stats().unwrap();
473        assert_eq!(stats.entry_count, 0);
474    }
475
476    #[test]
477    fn test_history_store_get_storage_stats() {
478        let temp_dir = TempDir::new().unwrap();
479        let store_path = temp_dir.path().join("history.json");
480
481        let mut store = HistoryStore::new(&store_path).unwrap();
482        let change = Change::new(
483            "test.txt",
484            "before",
485            "after",
486            "Test",
487            ChangeType::Modify,
488        )
489        .unwrap();
490        let snapshot = HistorySnapshot::new(vec![change], HashMap::new());
491        store.save_history(snapshot).unwrap();
492
493        let stats = store.get_storage_stats().unwrap();
494        assert!(stats.size_bytes > 0);
495        assert_eq!(stats.entry_count, 1);
496        assert!(stats.oldest_entry.is_some());
497        assert!(stats.newest_entry.is_some());
498    }
499
500    #[test]
501    fn test_history_store_is_storage_full() {
502        let temp_dir = TempDir::new().unwrap();
503        let store_path = temp_dir.path().join("history.json");
504
505        let mut store = HistoryStore::new(&store_path).unwrap();
506        let change = Change::new(
507            "test.txt",
508            "before",
509            "after",
510            "Test",
511            ChangeType::Modify,
512        )
513        .unwrap();
514        let snapshot = HistorySnapshot::new(vec![change], HashMap::new());
515        store.save_history(snapshot).unwrap();
516
517        let is_full = store.is_storage_full().unwrap();
518        assert!(!is_full); // Small test file should not be full
519    }
520
521    #[test]
522    fn test_history_snapshot_validate() {
523        let change = Change::new(
524            "test.txt",
525            "before",
526            "after",
527            "Test",
528            ChangeType::Modify,
529        )
530        .unwrap();
531        let snapshot = HistorySnapshot::new(vec![change], HashMap::new());
532        assert!(snapshot.validate().is_ok());
533    }
534
535    #[test]
536    fn test_history_store_save_with_retry() {
537        let temp_dir = TempDir::new().unwrap();
538        let store_path = temp_dir.path().join("history.json");
539
540        let mut store = HistoryStore::new(&store_path).unwrap();
541        let change = Change::new(
542            "test.txt",
543            "before",
544            "after",
545            "Test",
546            ChangeType::Modify,
547        )
548        .unwrap();
549        let snapshot = HistorySnapshot::new(vec![change], HashMap::new());
550
551        // Should succeed on first attempt
552        let result = store.save_history(snapshot);
553        assert!(result.is_ok());
554    }
555
556    #[test]
557    fn test_history_store_load_corrupted_file() {
558        let temp_dir = TempDir::new().unwrap();
559        let store_path = temp_dir.path().join("history.json");
560
561        // Write corrupted JSON
562        fs::write(&store_path, "{ invalid json }").unwrap();
563
564        let mut store = HistoryStore::new(&store_path).unwrap();
565        // Should fall back to empty history on load failure
566        let loaded = store.load_history().unwrap();
567        assert_eq!(loaded.changes.len(), 0);
568    }
569
570    #[test]
571    fn test_history_store_multiple_changes_and_checkpoints() {
572        let temp_dir = TempDir::new().unwrap();
573        let store_path = temp_dir.path().join("history.json");
574
575        let mut store = HistoryStore::new(&store_path).unwrap();
576
577        // Create multiple changes
578        let mut changes = Vec::new();
579        for i in 0..5 {
580            let change = Change::new(
581                &format!("file{}.txt", i),
582                "before",
583                "after",
584                &format!("Change {}", i),
585                ChangeType::Modify,
586            )
587            .unwrap();
588            changes.push(change);
589        }
590
591        // Create checkpoints
592        let mut checkpoints = HashMap::new();
593        for i in 0..2 {
594            let mut file_states = HashMap::new();
595            file_states.insert(format!("file{}.txt", i), format!("content{}", i));
596            let checkpoint = Checkpoint::new(
597                format!("Checkpoint {}", i),
598                "description",
599                file_states,
600            )
601            .unwrap();
602            checkpoints.insert(checkpoint.id.clone(), checkpoint);
603        }
604
605        let snapshot = HistorySnapshot::new(changes, checkpoints);
606        store.save_history(snapshot).unwrap();
607
608        // Load and verify
609        let mut store2 = HistoryStore::new(&store_path).unwrap();
610        let loaded = store2.load_history().unwrap();
611        assert_eq!(loaded.changes.len(), 5);
612        assert_eq!(loaded.checkpoints.len(), 2);
613    }
614
615    #[test]
616    fn test_storage_manager_create() {
617        let temp_dir = TempDir::new().unwrap();
618        let store_path = temp_dir.path().join("history.json");
619        let manager = StorageManager::with_defaults(&store_path);
620        assert!(manager.is_ok());
621    }
622
623    #[test]
624    fn test_storage_manager_cleanup_on_session_start() {
625        let temp_dir = TempDir::new().unwrap();
626        let store_path = temp_dir.path().join("history.json");
627
628        let mut manager = StorageManager::with_defaults(&store_path).unwrap();
629        let result = manager.cleanup_on_session_start();
630        assert!(result.is_ok());
631    }
632
633    #[test]
634    fn test_storage_manager_cleanup_on_session_end() {
635        let temp_dir = TempDir::new().unwrap();
636        let store_path = temp_dir.path().join("history.json");
637
638        let mut manager = StorageManager::with_defaults(&store_path).unwrap();
639        let result = manager.cleanup_on_session_end();
640        assert!(result.is_ok());
641    }
642
643    #[test]
644    fn test_storage_manager_enforce_storage_limit() {
645        let temp_dir = TempDir::new().unwrap();
646        let store_path = temp_dir.path().join("history.json");
647
648        let mut manager = StorageManager::new(&store_path, 30, 1024 * 1024 * 1024).unwrap();
649
650        // Create some changes
651        let change = Change::new(
652            "test.txt",
653            "before",
654            "after",
655            "Test",
656            ChangeType::Modify,
657        )
658        .unwrap();
659        let snapshot = HistorySnapshot::new(vec![change], HashMap::new());
660        manager.get_store_mut().save_history(snapshot).unwrap();
661
662        // Enforce limit (should not fail for small file)
663        let result = manager.enforce_storage_limit();
664        assert!(result.is_ok());
665    }
666
667    #[test]
668    fn test_storage_manager_retention_days() {
669        let temp_dir = TempDir::new().unwrap();
670        let store_path = temp_dir.path().join("history.json");
671
672        let mut manager = StorageManager::with_defaults(&store_path).unwrap();
673        assert_eq!(manager.get_retention_days(), 30);
674
675        manager.set_retention_days(7);
676        assert_eq!(manager.get_retention_days(), 7);
677    }
678
679    #[test]
680    fn test_storage_manager_max_storage_bytes() {
681        let temp_dir = TempDir::new().unwrap();
682        let store_path = temp_dir.path().join("history.json");
683
684        let mut manager = StorageManager::with_defaults(&store_path).unwrap();
685        assert_eq!(manager.get_max_storage_bytes(), 1024 * 1024 * 1024);
686
687        manager.set_max_storage_bytes(512 * 1024 * 1024);
688        assert_eq!(manager.get_max_storage_bytes(), 512 * 1024 * 1024);
689    }
690}
691
692#[cfg(test)]
693mod property_tests {
694    use super::*;
695    use crate::change::ChangeType;
696    use proptest::prelude::*;
697    use tempfile::TempDir;
698
699    /// Strategy for generating valid file paths
700    fn file_path_strategy() -> impl Strategy<Value = String> {
701        r"[a-zA-Z0-9_\-./]{1,50}\.rs"
702            .prop_map(|s| s.to_string())
703    }
704
705    /// Strategy for generating valid content
706    fn content_strategy() -> impl Strategy<Value = String> {
707        r"[a-zA-Z0-9\s]{1,100}"
708            .prop_map(|s| s.to_string())
709    }
710
711    proptest! {
712        /// **Feature: ricecoder-undo-redo, Property 6: Persistence Round-Trip**
713        /// *For any* history state, saving and loading SHALL produce an equivalent history
714        /// with all changes intact.
715        /// **Validates: Requirements 5.1, 5.2, 5.3**
716        #[test]
717        fn prop_persistence_round_trip_small(
718            changes_data in prop::collection::vec(
719                (file_path_strategy(), content_strategy(), content_strategy()),
720                1..10
721            ),
722        ) {
723            let temp_dir = TempDir::new().unwrap();
724            let store_path = temp_dir.path().join("history.json");
725
726            let mut changes = Vec::new();
727            for (idx, (file_path, before, after)) in changes_data.iter().enumerate() {
728                prop_assume!(before != after);
729
730                if let Ok(change) = Change::new(
731                    file_path.clone(),
732                    before.clone(),
733                    after.clone(),
734                    format!("Change {}", idx),
735                    ChangeType::Modify,
736                ) {
737                    changes.push(change);
738                }
739            }
740
741            // Save
742            let mut store = HistoryStore::new(&store_path).unwrap();
743            let snapshot = HistorySnapshot::new(changes.clone(), HashMap::new());
744            store.save_history(snapshot).unwrap();
745
746            // Load
747            let mut store2 = HistoryStore::new(&store_path).unwrap();
748            let loaded = store2.load_history().unwrap();
749
750            // Verify
751            prop_assert_eq!(
752                loaded.changes.len(),
753                changes.len(),
754                "Loaded changes count should match saved"
755            );
756
757            for (saved, loaded_change) in changes.iter().zip(loaded.changes.iter()) {
758                prop_assert_eq!(&saved.id, &loaded_change.id, "Change IDs should match");
759                prop_assert_eq!(
760                    &saved.file_path, &loaded_change.file_path,
761                    "File paths should match"
762                );
763                prop_assert_eq!(
764                    &saved.before, &loaded_change.before,
765                    "Before states should match"
766                );
767                prop_assert_eq!(
768                    &saved.after, &loaded_change.after,
769                    "After states should match"
770                );
771            }
772        }
773
774        /// **Feature: ricecoder-undo-redo, Property 6: Persistence Round-Trip**
775        /// *For any* history state with varying sizes, saving and loading SHALL preserve
776        /// all data exactly.
777        /// **Validates: Requirements 5.1, 5.2, 5.3**
778        #[test]
779        fn prop_persistence_round_trip_medium(
780            changes_data in prop::collection::vec(
781                (file_path_strategy(), content_strategy(), content_strategy()),
782                1..100
783            ),
784        ) {
785            let temp_dir = TempDir::new().unwrap();
786            let store_path = temp_dir.path().join("history.json");
787
788            let mut changes = Vec::new();
789            for (idx, (file_path, before, after)) in changes_data.iter().enumerate() {
790                prop_assume!(before != after);
791
792                if let Ok(change) = Change::new(
793                    file_path.clone(),
794                    before.clone(),
795                    after.clone(),
796                    format!("Change {}", idx),
797                    ChangeType::Modify,
798                ) {
799                    changes.push(change);
800                }
801            }
802
803            // Save
804            let mut store = HistoryStore::new(&store_path).unwrap();
805            let snapshot = HistorySnapshot::new(changes.clone(), HashMap::new());
806            store.save_history(snapshot).unwrap();
807
808            // Load
809            let mut store2 = HistoryStore::new(&store_path).unwrap();
810            let loaded = store2.load_history().unwrap();
811
812            // Verify
813            prop_assert_eq!(
814                loaded.changes.len(),
815                changes.len(),
816                "Loaded changes count should match saved"
817            );
818
819            for (saved, loaded_change) in changes.iter().zip(loaded.changes.iter()) {
820                prop_assert_eq!(&saved.id, &loaded_change.id, "Change IDs should match");
821                prop_assert_eq!(
822                    &saved.file_path, &loaded_change.file_path,
823                    "File paths should match"
824                );
825                prop_assert_eq!(
826                    &saved.before, &loaded_change.before,
827                    "Before states should match"
828                );
829                prop_assert_eq!(
830                    &saved.after, &loaded_change.after,
831                    "After states should match"
832                );
833            }
834        }
835    }
836}