Skip to main content

ryo_storage/txlog/
replay.rs

1//! Mutation replay engine for deterministic session reproduction.
2//!
3//! This module provides the ability to replay recorded mutations on files,
4//! enabling Git-like reproducibility of coding sessions.
5//!
6//! # Architecture
7//!
8//! ```text
9//! TxLog (recorded session)
10//!   │
11//!   ├── MutationApplied { mutation_data, pre_state, post_state }
12//!   │         │
13//!   │         └── SerializableMutation
14//!   │                   │
15//!   │                   └── to_mutation() → Box<dyn Mutation>
16//!   │
17//!   └── ReplayEngine
18//!             │
19//!             ├── replay_all() → Apply all mutations sequentially
20//!             ├── replay_verified() → Verify pre/post states during replay
21//!             └── replay_to() → Replay to specific checkpoint
22//! ```
23//!
24//! # Example
25//!
26//! ```ignore
27//! use ryo_core::txlog::{TxLog, ReplayEngine, ReplayMode};
28//! use ryo_source::pure::PureFile;
29//!
30//! // Load a recorded session
31//! let log = storage.load("session-id")?;
32//!
33//! // Start with initial file state
34//! let mut file = PureFile::from_source("fn main() {}")?;
35//!
36//! // Replay all mutations
37//! let engine = ReplayEngine::new(&log);
38//! let result = engine.replay_all(&mut file)?;
39//!
40//! println!("Replayed {} mutations, {} changes", result.mutations_applied, result.total_changes);
41//! ```
42
43use super::{TxAction, TxEntry, TxLog};
44use crate::storage::{InMemoryStateStore, StateRef, StateStore};
45use ryo_mutations::SerializableMutation;
46use ryo_source::pure::PureFile;
47use std::path::Path;
48use thiserror::Error;
49
50/// Errors that can occur during replay.
51#[derive(Debug, Error)]
52pub enum ReplayError {
53    /// The entry is replayable in principle but its serialized mutation
54    /// payload is absent. Carries the entry id.
55    #[error("Mutation data missing for entry #{0}")]
56    MissingMutationData(u64),
57
58    /// The serialized mutation payload failed to deserialize.
59    #[error("Failed to parse mutation data: {0}")]
60    ParseError(String),
61
62    /// The mutation type is a generic placeholder that cannot be executed
63    /// during replay. Carries the offending mutation type name.
64    #[error("Mutation type '{0}' cannot be replayed (Generic mutation)")]
65    UnreplayableMutation(String),
66
67    /// Pre-state verification failed for an entry.
68    #[error("Pre-state mismatch at entry #{id}: expected {expected}, got {actual}")]
69    PreStateMismatch {
70        /// Entry id at which verification failed.
71        id: u64,
72        /// Rendered expected state reference.
73        expected: String,
74        /// Rendered actual state reference.
75        actual: String,
76    },
77
78    /// Post-state verification failed for an entry.
79    #[error("Post-state mismatch at entry #{id}: expected {expected}, got {actual}")]
80    PostStateMismatch {
81        /// Entry id at which verification failed.
82        id: u64,
83        /// Rendered expected state reference.
84        expected: String,
85        /// Rendered actual state reference.
86        actual: String,
87    },
88
89    /// The mutation engine reported a failure while replaying the entry.
90    #[error("Mutation failed: {0}")]
91    MutationFailed(String),
92}
93
94/// Result of a replay operation.
95#[derive(Debug, Clone)]
96pub struct ReplayResult {
97    /// Number of mutations successfully applied.
98    pub mutations_applied: usize,
99    /// Total number of changes made.
100    pub total_changes: usize,
101    /// Number of entries skipped (non-mutation or missing data).
102    pub skipped: usize,
103    /// Warnings during replay (non-fatal issues).
104    pub warnings: Vec<String>,
105}
106
107impl ReplayResult {
108    fn new() -> Self {
109        Self {
110            mutations_applied: 0,
111            total_changes: 0,
112            skipped: 0,
113            warnings: Vec::new(),
114        }
115    }
116}
117
118/// Replay mode configuration.
119#[derive(Debug, Clone, Copy, Default)]
120pub enum ReplayMode {
121    /// Apply mutations without verification (fastest).
122    #[default]
123    Fast,
124    /// Verify pre-state before each mutation.
125    VerifyPre,
126    /// Verify post-state after each mutation.
127    VerifyPost,
128    /// Verify both pre and post states (strictest).
129    VerifyBoth,
130}
131
132impl ReplayMode {
133    /// Returns `true` if this mode requires pre-state verification before
134    /// applying each mutation.
135    pub fn verify_pre(&self) -> bool {
136        matches!(self, ReplayMode::VerifyPre | ReplayMode::VerifyBoth)
137    }
138
139    /// Returns `true` if this mode requires post-state verification after
140    /// applying each mutation.
141    pub fn verify_post(&self) -> bool {
142        matches!(self, ReplayMode::VerifyPost | ReplayMode::VerifyBoth)
143    }
144}
145
146/// Engine for replaying recorded mutations.
147pub struct ReplayEngine<'a> {
148    log: &'a TxLog,
149    mode: ReplayMode,
150    state_store: InMemoryStateStore,
151}
152
153impl<'a> ReplayEngine<'a> {
154    /// Create a new replay engine.
155    pub fn new(log: &'a TxLog) -> Self {
156        Self {
157            log,
158            mode: ReplayMode::default(),
159            state_store: InMemoryStateStore::new(),
160        }
161    }
162
163    /// Set the replay mode.
164    pub fn with_mode(mut self, mode: ReplayMode) -> Self {
165        self.mode = mode;
166        self
167    }
168
169    /// Replay all mutations in the log to a single file.
170    ///
171    /// This is the simplest replay mode - applies all mutations sequentially.
172    pub fn replay_all(&mut self, file: &mut PureFile) -> Result<ReplayResult, ReplayError> {
173        let mut result = ReplayResult::new();
174
175        for entry in self.log.entries() {
176            match &entry.action {
177                TxAction::MutationApplied {
178                    mutation_type,
179                    mutation_data,
180                    pre_state,
181                    post_state,
182                    ..
183                } => {
184                    // Try to replay this mutation
185                    match self.replay_mutation_entry(
186                        entry,
187                        mutation_type,
188                        mutation_data,
189                        pre_state.as_ref(),
190                        post_state.as_ref(),
191                        file,
192                    ) {
193                        Ok(changes) => {
194                            result.mutations_applied += 1;
195                            result.total_changes += changes;
196                        }
197                        Err(ReplayError::UnreplayableMutation(t)) => {
198                            result.skipped += 1;
199                            result.warnings.push(format!(
200                                "Skipped unreplayable mutation: {} (entry #{})",
201                                t, entry.id
202                            ));
203                        }
204                        Err(ReplayError::MissingMutationData(_)) => {
205                            result.skipped += 1;
206                            result.warnings.push(format!(
207                                "Skipped mutation without data (entry #{})",
208                                entry.id
209                            ));
210                        }
211                        Err(e) => return Err(e),
212                    }
213                }
214                _ => {
215                    // Skip non-mutation entries
216                }
217            }
218        }
219
220        Ok(result)
221    }
222
223    /// Replay mutations for a specific file path.
224    ///
225    /// Only applies mutations that target the given file.
226    pub fn replay_file(
227        &mut self,
228        target_path: &Path,
229        file: &mut PureFile,
230    ) -> Result<ReplayResult, ReplayError> {
231        let mut result = ReplayResult::new();
232
233        for entry in self.log.entries() {
234            if let TxAction::MutationApplied {
235                mutation_type,
236                mutation_data,
237                file_path,
238                pre_state,
239                post_state,
240                ..
241            } = &entry.action
242            {
243                // Check if this mutation targets our file
244                let matches = file_path.as_ref().map(|p| p == target_path).unwrap_or(true); // If no path specified, assume it matches
245
246                if !matches {
247                    continue;
248                }
249
250                match self.replay_mutation_entry(
251                    entry,
252                    mutation_type,
253                    mutation_data,
254                    pre_state.as_ref(),
255                    post_state.as_ref(),
256                    file,
257                ) {
258                    Ok(changes) => {
259                        result.mutations_applied += 1;
260                        result.total_changes += changes;
261                    }
262                    Err(ReplayError::UnreplayableMutation(t)) => {
263                        result.skipped += 1;
264                        result.warnings.push(format!(
265                            "Skipped unreplayable mutation: {} (entry #{})",
266                            t, entry.id
267                        ));
268                    }
269                    Err(ReplayError::MissingMutationData(_)) => {
270                        result.skipped += 1;
271                        result.warnings.push(format!(
272                            "Skipped mutation without data (entry #{})",
273                            entry.id
274                        ));
275                    }
276                    Err(e) => return Err(e),
277                }
278            }
279        }
280
281        Ok(result)
282    }
283
284    /// Replay a single mutation entry.
285    fn replay_mutation_entry(
286        &mut self,
287        entry: &TxEntry,
288        mutation_type: &str,
289        mutation_data: &Option<serde_json::Value>,
290        pre_state: Option<&StateRef>,
291        post_state: Option<&StateRef>,
292        file: &mut PureFile,
293    ) -> Result<usize, ReplayError> {
294        // Verify pre-state if required
295        if self.mode.verify_pre() {
296            if let Some(expected_pre) = pre_state {
297                let current = self.state_store.store(
298                    &file
299                        .to_source()
300                        .map_err(|e| ReplayError::MutationFailed(e.to_string()))?,
301                );
302                if &current != expected_pre {
303                    return Err(ReplayError::PreStateMismatch {
304                        id: entry.id,
305                        expected: expected_pre.short().to_string(),
306                        actual: current.short().to_string(),
307                    });
308                }
309            }
310        }
311
312        // Get mutation data
313        let data = mutation_data
314            .as_ref()
315            .ok_or(ReplayError::MissingMutationData(entry.id))?;
316
317        // Parse to SerializableMutation
318        let serializable: SerializableMutation = serde_json::from_value(data.clone())
319            .map_err(|e| ReplayError::ParseError(e.to_string()))?;
320
321        // Convert to Mutation
322        let mutation = serializable
323            .to_mutation()
324            .ok_or_else(|| ReplayError::UnreplayableMutation(mutation_type.to_string()))?;
325
326        // Apply mutation
327        // TODO: Mutation::apply was removed. Replay functionality is currently broken.
328        // Migration to ASTRegApply is required for replay to work again.
329        // let result = mutation.apply(file);
330        let _mutation = mutation; // suppress unused warning
331        let changes = 0usize; // placeholder until migration
332
333        // Verify post-state if required
334        if self.mode.verify_post() {
335            if let Some(expected_post) = post_state {
336                let current = self.state_store.store(
337                    &file
338                        .to_source()
339                        .map_err(|e| ReplayError::MutationFailed(e.to_string()))?,
340                );
341                if &current != expected_post {
342                    return Err(ReplayError::PostStateMismatch {
343                        id: entry.id,
344                        expected: expected_post.short().to_string(),
345                        actual: current.short().to_string(),
346                    });
347                }
348            }
349        }
350
351        Ok(changes)
352    }
353
354    /// Get statistics about replayable mutations in the log.
355    pub fn analyze(&self) -> ReplayAnalysis {
356        let mut analysis = ReplayAnalysis::default();
357
358        for entry in self.log.entries() {
359            if let TxAction::MutationApplied {
360                mutation_type,
361                mutation_data,
362                pre_state,
363                post_state,
364                ..
365            } = &entry.action
366            {
367                analysis.total_mutations += 1;
368
369                // Check if data is present
370                if mutation_data.is_none() {
371                    analysis.missing_data += 1;
372                    analysis
373                        .unreplayable_types
374                        .push(format!("{} (entry #{}, no data)", mutation_type, entry.id));
375                    continue;
376                }
377
378                // Check if state refs are present
379                if pre_state.is_some() {
380                    analysis.with_pre_state += 1;
381                }
382                if post_state.is_some() {
383                    analysis.with_post_state += 1;
384                }
385
386                // Try to parse and check if replayable
387                if let Some(data) = mutation_data {
388                    match serde_json::from_value::<SerializableMutation>(data.clone()) {
389                        Ok(sm) => {
390                            if sm.to_mutation().is_some() {
391                                analysis.replayable += 1;
392                            } else {
393                                analysis.unreplayable_types.push(format!(
394                                    "{} (entry #{}, Generic)",
395                                    mutation_type, entry.id
396                                ));
397                            }
398                        }
399                        Err(_) => {
400                            analysis.parse_errors += 1;
401                            analysis.unreplayable_types.push(format!(
402                                "{} (entry #{}, parse error)",
403                                mutation_type, entry.id
404                            ));
405                        }
406                    }
407                }
408            }
409        }
410
411        analysis
412    }
413}
414
415/// Analysis of replay capability for a log.
416#[derive(Debug, Default)]
417pub struct ReplayAnalysis {
418    /// Total mutation entries in the log.
419    pub total_mutations: usize,
420    /// Mutations that can be replayed.
421    pub replayable: usize,
422    /// Mutations missing mutation_data.
423    pub missing_data: usize,
424    /// Mutations with parse errors.
425    pub parse_errors: usize,
426    /// Mutations with pre_state recorded.
427    pub with_pre_state: usize,
428    /// Mutations with post_state recorded.
429    pub with_post_state: usize,
430    /// Types that cannot be replayed (with reasons).
431    pub unreplayable_types: Vec<String>,
432}
433
434impl ReplayAnalysis {
435    /// Percentage of mutations that can be replayed.
436    pub fn replayable_percentage(&self) -> f64 {
437        if self.total_mutations == 0 {
438            100.0
439        } else {
440            (self.replayable as f64 / self.total_mutations as f64) * 100.0
441        }
442    }
443
444    /// Check if full replay is possible.
445    pub fn is_fully_replayable(&self) -> bool {
446        self.replayable == self.total_mutations
447    }
448
449    /// Check if verified replay is possible (all states recorded).
450    pub fn can_verify(&self) -> bool {
451        self.with_pre_state == self.total_mutations && self.with_post_state == self.total_mutations
452    }
453}
454
455#[cfg(test)]
456mod tests {
457    use super::*;
458    use crate::txlog::TxAction;
459    use ryo_analysis::{SymbolId, SymbolKind, SymbolPath, SymbolRegistry};
460    use ryo_mutations::RenameMutation;
461
462    /// Create a dummy SymbolId for testing purposes using SymbolRegistry
463    fn dummy_symbol_id() -> SymbolId {
464        let mut registry = SymbolRegistry::new();
465        let path = SymbolPath::parse("test_crate::test_dummy").unwrap();
466        registry.register(path, SymbolKind::Function).unwrap()
467    }
468
469    fn create_test_log_with_mutation() -> TxLog {
470        let mut log = TxLog::new();
471
472        // Add a replayable mutation
473        let mutation = RenameMutation {
474            symbol_id: dummy_symbol_id(),
475            to: "bar".to_string(),
476        };
477        use ryo_mutations::ToSerializable;
478        let serializable = mutation.to_serializable();
479
480        log.log(TxAction::MutationApplied {
481            mutation_type: "Rename".to_string(),
482            target: "foo -> bar".to_string(),
483            changes: 1,
484            mutation_data: Some(serializable.to_json()),
485            file_path: None,
486            pre_state: None,
487            post_state: None,
488            affected_symbols: vec![],
489        });
490
491        log
492    }
493
494    #[test]
495    fn test_replay_engine_analyze() {
496        let log = create_test_log_with_mutation();
497        let engine = ReplayEngine::new(&log);
498        let analysis = engine.analyze();
499
500        assert_eq!(analysis.total_mutations, 1);
501        assert_eq!(analysis.replayable, 1);
502        assert!(analysis.is_fully_replayable());
503    }
504
505    #[test]
506    #[ignore = "V1 path disabled - Mutation::apply removed, needs V2 migration"]
507    fn test_replay_simple_mutation() {
508        let log = create_test_log_with_mutation();
509        let mut engine = ReplayEngine::new(&log);
510
511        // Create a file with 'foo' that can be renamed
512        let mut file = PureFile::from_source("fn foo() {}").unwrap();
513
514        let result = engine.replay_all(&mut file).unwrap();
515
516        assert_eq!(result.mutations_applied, 1);
517        assert!(result.total_changes > 0);
518
519        // Verify the rename happened
520        let source = file.to_source().unwrap();
521        assert!(source.contains("bar"));
522        assert!(!source.contains("foo"));
523    }
524
525    #[test]
526    fn test_analyze_missing_data() {
527        let mut log = TxLog::new();
528
529        // Add mutation without data
530        log.log(TxAction::MutationApplied {
531            mutation_type: "Rename".to_string(),
532            target: "foo -> bar".to_string(),
533            changes: 1,
534            mutation_data: None, // Missing!
535            file_path: None,
536            pre_state: None,
537            post_state: None,
538            affected_symbols: vec![],
539        });
540
541        let engine = ReplayEngine::new(&log);
542        let analysis = engine.analyze();
543
544        assert_eq!(analysis.total_mutations, 1);
545        assert_eq!(analysis.missing_data, 1);
546        assert_eq!(analysis.replayable, 0);
547        assert!(!analysis.is_fully_replayable());
548    }
549
550    // =========================================================================
551    // 検証テスト: 現場でのReplay可能性を明確化
552    // =========================================================================
553
554    /// 検証1: 複数のMutationタイプのReplay可能性
555    #[test]
556    fn test_verify_multiple_mutation_types() {
557        use ryo_mutations::{AddDeriveMutation, AddFunctionMutation, ToSerializable};
558
559        let mut log = TxLog::new();
560
561        // 1. Rename - Replayable
562        let rename = RenameMutation {
563            symbol_id: dummy_symbol_id(),
564            to: "new_name".to_string(),
565        };
566        log.log(TxAction::MutationApplied {
567            mutation_type: "Rename".to_string(),
568            target: "old_name -> new_name".to_string(),
569            changes: 1,
570            mutation_data: Some(rename.to_serializable().to_json()),
571            file_path: None,
572            pre_state: None,
573            post_state: None,
574            affected_symbols: vec![],
575        });
576
577        // 2. AddFunction - NOT Replayable (SymbolId cannot be deserialized)
578        let add_fn = AddFunctionMutation {
579            parent: dummy_symbol_id(),
580            name: "new_fn".to_string(),
581            params: vec![("x".to_string(), "i32".to_string())],
582            return_type: Some("i32".to_string()),
583            body: "x + 1".to_string(),
584            is_pub: true,
585        };
586        log.log(TxAction::MutationApplied {
587            mutation_type: "AddFunction".to_string(),
588            target: "new_fn".to_string(),
589            changes: 1,
590            mutation_data: Some(add_fn.to_serializable().to_json()),
591            file_path: None,
592            pre_state: None,
593            post_state: None,
594            affected_symbols: vec![],
595        });
596
597        // 3. AddDerive - Replayable
598        let add_derive = AddDeriveMutation {
599            symbol_id: dummy_symbol_id(),
600            derives: vec!["Debug".to_string(), "Clone".to_string()],
601        };
602        log.log(TxAction::MutationApplied {
603            mutation_type: "AddDerive".to_string(),
604            target: "MyStruct".to_string(),
605            changes: 1,
606            mutation_data: Some(add_derive.to_serializable().to_json()),
607            file_path: None,
608            pre_state: None,
609            post_state: None,
610            affected_symbols: vec![],
611        });
612
613        // 4. mutation_dataなし(従来のlog_mutation経由) - NOT Replayable
614        log.log(TxAction::MutationApplied {
615            mutation_type: "Rename".to_string(),
616            target: "legacy call".to_string(),
617            changes: 1,
618            mutation_data: None, // ← 問題点:従来のAPIはここがNone
619            file_path: None,
620            pre_state: None,
621            post_state: None,
622            affected_symbols: vec![],
623        });
624
625        let engine = ReplayEngine::new(&log);
626        let analysis = engine.analyze();
627
628        // 結果確認
629        // Note: Only Rename is replayable (uses SymbolId::parse)
630        // - AddFunction: NOT replayable (SymbolId cannot be deserialized, returns None)
631        // - AddDerive: NOT replayable (SymbolId cannot be deserialized, returns None)
632        // - Legacy: NOT replayable (no mutation_data)
633        assert_eq!(analysis.total_mutations, 4);
634        assert_eq!(analysis.replayable, 1); // Only Rename is replayable
635        assert_eq!(analysis.missing_data, 1); // mutation_dataなし
636        assert!(!analysis.is_fully_replayable()); // 完全Replayは不可
637
638        // Replay可能率: 1/4 = 25%
639        assert!((analysis.replayable_percentage() - 25.0).abs() < 0.1);
640    }
641
642    /// 検証2: Generic mutationはReplay不可能
643    #[test]
644    fn test_verify_generic_mutation_not_replayable() {
645        use ryo_mutations::SerializableMutation;
646
647        let mut log = TxLog::new();
648
649        // Generic mutationをシミュレート
650        let generic = SerializableMutation::Generic {
651            mutation_type: "CustomMutation".to_string(),
652            description: "Some custom operation".to_string(),
653        };
654
655        log.log(TxAction::MutationApplied {
656            mutation_type: "CustomMutation".to_string(),
657            target: "custom".to_string(),
658            changes: 1,
659            mutation_data: Some(generic.to_json()),
660            file_path: None,
661            pre_state: None,
662            post_state: None,
663            affected_symbols: vec![],
664        });
665
666        let engine = ReplayEngine::new(&log);
667        let analysis = engine.analyze();
668
669        assert_eq!(analysis.total_mutations, 1);
670        assert_eq!(analysis.replayable, 0); // Generic → to_mutation() returns None
671        assert!(!analysis.unreplayable_types.is_empty());
672    }
673
674    /// 検証3: 状態検証付きReplay
675    #[test]
676    fn test_verify_replay_with_state_verification() {
677        use crate::storage::InMemoryStateStore;
678        use ryo_mutations::ToSerializable;
679
680        let mut state_store = InMemoryStateStore::new();
681
682        // 初期状態を記録
683        let initial_source = "fn foo() { println!(\"hello\"); }";
684        let pre_state = state_store.store(initial_source);
685
686        // Mutation適用後の期待状態
687        let expected_source = "fn bar() { println!(\"hello\"); }";
688        let post_state = state_store.store(expected_source);
689
690        // ログ作成(状態付き)
691        let mut log = TxLog::new();
692        let rename = RenameMutation {
693            symbol_id: dummy_symbol_id(),
694            to: "bar".to_string(),
695        };
696
697        log.log(TxAction::MutationApplied {
698            mutation_type: "Rename".to_string(),
699            target: "foo -> bar".to_string(),
700            changes: 1,
701            mutation_data: Some(rename.to_serializable().to_json()),
702            file_path: Some(std::path::PathBuf::from("test.rs")),
703            pre_state: Some(pre_state.clone()),
704            post_state: Some(post_state.clone()),
705            affected_symbols: vec![],
706        });
707
708        let engine = ReplayEngine::new(&log);
709        let analysis = engine.analyze();
710
711        // 状態検証可能
712        assert_eq!(analysis.with_pre_state, 1);
713        assert_eq!(analysis.with_post_state, 1);
714        assert!(analysis.can_verify());
715    }
716
717    /// 検証4: 実際のReplay実行(複数mutation連続適用)
718    #[test]
719    #[ignore = "V1 path disabled - Mutation::apply removed, needs V2 migration"]
720    fn test_verify_sequential_replay() {
721        use ryo_mutations::ToSerializable;
722
723        let mut log = TxLog::new();
724
725        // 1. foo → bar
726        let rename1 = RenameMutation {
727            symbol_id: dummy_symbol_id(),
728            to: "bar".to_string(),
729        };
730        log.log(TxAction::MutationApplied {
731            mutation_type: "Rename".to_string(),
732            target: "foo -> bar".to_string(),
733            changes: 1,
734            mutation_data: Some(rename1.to_serializable().to_json()),
735            file_path: None,
736            pre_state: None,
737            post_state: None,
738            affected_symbols: vec![],
739        });
740
741        // 2. bar → baz
742        let rename2 = RenameMutation {
743            symbol_id: dummy_symbol_id(),
744            to: "baz".to_string(),
745        };
746        log.log(TxAction::MutationApplied {
747            mutation_type: "Rename".to_string(),
748            target: "bar -> baz".to_string(),
749            changes: 1,
750            mutation_data: Some(rename2.to_serializable().to_json()),
751            file_path: None,
752            pre_state: None,
753            post_state: None,
754            affected_symbols: vec![],
755        });
756
757        // Replay実行
758        let mut engine = ReplayEngine::new(&log);
759        let mut file = PureFile::from_source("fn foo() { foo(); }").unwrap();
760
761        let result = engine.replay_all(&mut file).unwrap();
762
763        // 検証
764        assert_eq!(result.mutations_applied, 2);
765        let source = file.to_source().unwrap();
766        assert!(source.contains("baz"));
767        assert!(!source.contains("foo"));
768        assert!(!source.contains("bar"));
769    }
770
771    /// 検証5: 現場問題の再現 - log_mutation()経由のログはReplay不可能
772    #[test]
773    fn test_verify_current_logger_issue() {
774        use crate::txlog::TxLogger;
775        use std::path::PathBuf;
776        use std::thread;
777        use std::time::Duration;
778
779        // 現行のTxLoggerを使用
780        let logger = TxLogger::start(PathBuf::from("/test"), 10);
781
782        // 従来のlog_mutation() - mutation_dataなし
783        logger.log_mutation("Rename", "foo -> bar", 5);
784
785        thread::sleep(Duration::from_millis(10));
786        let log = logger.finish();
787
788        let engine = ReplayEngine::new(&log);
789        let analysis = engine.analyze();
790
791        // 問題点: log_mutation()はmutation_dataを記録しない
792        assert_eq!(analysis.total_mutations, 1);
793        assert_eq!(analysis.missing_data, 1);
794        assert_eq!(analysis.replayable, 0);
795
796        // これが修正すべき箇所
797        // → log_mutation()にMutation自体を渡すAPIが必要
798    }
799
800    /// 検証6: 新API record_mutation()はReplay可能
801    #[test]
802    fn test_verify_new_record_mutation_api() {
803        use crate::txlog::TxLogger;
804        use std::path::PathBuf;
805        use std::thread;
806        use std::time::Duration;
807
808        let logger = TxLogger::start(PathBuf::from("/test"), 10);
809
810        // 新しいrecord_mutation() API - Replayable!
811        let mutation = RenameMutation {
812            symbol_id: dummy_symbol_id(),
813            to: "bar".to_string(),
814        };
815        logger.record_mutation(&mutation, 5);
816
817        thread::sleep(Duration::from_millis(10));
818        let log = logger.finish();
819
820        let engine = ReplayEngine::new(&log);
821        let analysis = engine.analyze();
822
823        // 新APIはmutation_dataを自動記録する
824        assert_eq!(analysis.total_mutations, 1);
825        assert_eq!(analysis.missing_data, 0);
826        assert_eq!(analysis.replayable, 1);
827        assert!(analysis.is_fully_replayable());
828    }
829
830    /// 検証7: 新旧API混在時の分析
831    #[test]
832    fn test_verify_mixed_api_usage() {
833        use crate::txlog::TxLogger;
834        use std::path::PathBuf;
835        use std::thread;
836        use std::time::Duration;
837
838        let logger = TxLogger::start(PathBuf::from("/test"), 10);
839
840        // 旧API (NOT replayable)
841        logger.log_mutation("Rename", "legacy", 1);
842
843        // 新API (REPLAYABLE)
844        let mutation1 = RenameMutation {
845            symbol_id: dummy_symbol_id(),
846            to: "bar".to_string(),
847        };
848        logger.record_mutation(&mutation1, 2);
849
850        // 新API with file path (REPLAYABLE)
851        let mutation2 = RenameMutation {
852            symbol_id: dummy_symbol_id(),
853            to: "qux".to_string(),
854        };
855        logger.record_mutation_for_file(&mutation2, 3, "src/lib.rs");
856
857        thread::sleep(Duration::from_millis(10));
858        let log = logger.finish();
859
860        let engine = ReplayEngine::new(&log);
861        let analysis = engine.analyze();
862
863        // 結果: 3 mutations, 2 replayable, 1 missing data
864        assert_eq!(analysis.total_mutations, 3);
865        assert_eq!(analysis.replayable, 2);
866        assert_eq!(analysis.missing_data, 1);
867        assert!(!analysis.is_fully_replayable());
868
869        // Replay可能率: 66.67%
870        assert!((analysis.replayable_percentage() - 66.67).abs() < 1.0);
871    }
872}