Skip to main content

asupersync/trace/
replayer.rs

1//! Trace replayer for deterministic replay.
2//!
3//! The [`TraceReplayer`] feeds recorded decisions back into the Lab runtime
4//! to achieve exact execution replay. This is useful for:
5//!
6//! - Debugging non-deterministic issues
7//! - Reproducing test failures
8//! - Verifying execution determinism
9//!
10//! # Usage
11//!
12//! ```ignore
13//! use asupersync::trace::replayer::{TraceReplayer, ReplayMode};
14//! use asupersync::trace::file::TraceReader;
15//!
16//! // Load a trace file
17//! let reader = TraceReader::open("trace.bin")?;
18//!
19//! // Create a replayer
20//! let mut replayer = TraceReplayer::new(reader)?;
21//!
22//! // Replay step by step
23//! replayer.set_mode(ReplayMode::Step);
24//! while let Some(event) = replayer.step()? {
25//!     println!("Replayed: {:?}", event);
26//! }
27//! ```
28//!
29//! # Divergence Detection
30//!
31//! The replayer detects when actual execution diverges from the recorded trace.
32//! This indicates either a bug in the code or trace corruption.
33
34use crate::trace::replay::{CompactTaskId, ReplayEvent, ReplayTrace, TraceMetadata};
35use serde::Serialize;
36use std::fmt;
37
38// =============================================================================
39// Replay Mode
40// =============================================================================
41
42/// The replay execution mode.
43#[derive(Debug, Clone, PartialEq, Eq, Default)]
44pub enum ReplayMode {
45    /// Run to completion without stopping.
46    #[default]
47    Run,
48    /// Stop after each event.
49    Step,
50    /// Run until a specific breakpoint is reached.
51    RunTo(Breakpoint),
52}
53
54/// A breakpoint that stops replay execution.
55#[derive(Debug, Clone, PartialEq, Eq)]
56pub enum Breakpoint {
57    /// Stop at a specific tick (step number).
58    Tick(u64),
59    /// Stop when a specific task is scheduled.
60    Task(CompactTaskId),
61    /// Stop at a specific event index.
62    EventIndex(usize),
63}
64
65// =============================================================================
66// Divergence Error
67// =============================================================================
68
69/// Error indicating that execution diverged from the recorded trace.
70#[derive(Debug)]
71pub struct DivergenceError {
72    /// The event index where divergence occurred.
73    pub index: usize,
74    /// The expected event from the trace, or `None` when the trace is exhausted.
75    pub expected: Option<ReplayEvent>,
76    /// The actual event that occurred.
77    pub actual: ReplayEvent,
78    /// Additional context about the divergence.
79    pub context: String,
80}
81
82impl fmt::Display for DivergenceError {
83    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84        let expected = self.expected.as_ref().map_or_else(
85            || "<trace_exhausted>".to_string(),
86            |event| format!("{event:?}"),
87        );
88        write!(
89            f,
90            "Replay divergence at event {}: expected {}, got {:?}. {}",
91            self.index, expected, self.actual, self.context
92        )
93    }
94}
95
96impl std::error::Error for DivergenceError {}
97
98/// Errors that can occur during replay.
99#[derive(Debug, thiserror::Error)]
100pub enum ReplayError {
101    /// Execution diverged from the trace.
102    #[error("{0}")]
103    Divergence(#[from] DivergenceError),
104
105    /// Trace ended unexpectedly.
106    #[error("trace ended unexpectedly at event {index}, expected more events")]
107    UnexpectedEnd {
108        /// The event index where the trace ended.
109        index: usize,
110    },
111
112    /// Invalid trace metadata.
113    #[error("invalid trace metadata: {0}")]
114    InvalidMetadata(String),
115
116    /// Version mismatch.
117    #[error("trace version mismatch: expected {expected}, found {found}")]
118    VersionMismatch {
119        /// Expected version.
120        expected: u32,
121        /// Found version.
122        found: u32,
123    },
124}
125
126/// Structured replay report for browser-incident and CI repro workflows.
127#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
128pub struct BrowserReplayReport {
129    /// Trace identifier used by diagnostics and artifacts.
130    pub trace_id: String,
131    /// Replay schema version.
132    pub schema_version: u32,
133    /// Recording seed.
134    pub seed: u64,
135    /// Total events in trace.
136    pub event_count: usize,
137    /// Events consumed by replayer.
138    pub replayed_events: usize,
139    /// True when replay consumed all events.
140    pub completed: bool,
141    /// Optional divergence index.
142    pub divergence_index: Option<usize>,
143    /// Optional divergence summary.
144    pub divergence_context: Option<String>,
145    /// Minimal replay prefix length for divergence reproduction.
146    pub minimization_prefix_len: Option<usize>,
147    /// Percentage reduction from full trace size for the minimal prefix.
148    pub minimization_reduction_pct: Option<u64>,
149    /// Optional artifact pointer for persisted report/replay payload.
150    pub artifact_pointer: Option<String>,
151    /// Deterministic rerun command bundle.
152    pub rerun_commands: Vec<String>,
153}
154
155impl BrowserReplayReport {
156    /// Serializes report to pretty JSON.
157    ///
158    /// # Errors
159    ///
160    /// Returns an error when JSON serialization fails.
161    pub fn to_json_pretty(&self) -> Result<String, serde_json::Error> {
162        serde_json::to_string_pretty(self)
163    }
164}
165
166// =============================================================================
167// Trace Replayer
168// =============================================================================
169
170/// Replays a recorded trace, feeding decisions back to the runtime.
171///
172/// The replayer maintains a cursor into the trace and provides the next
173/// expected event for each replay step. It can detect divergence when
174/// actual execution doesn't match the recorded trace.
175#[derive(Debug)]
176pub struct TraceReplayer {
177    /// The trace being replayed.
178    trace: ReplayTrace,
179    /// Current position in the trace.
180    current_index: usize,
181    /// Current replay mode.
182    mode: ReplayMode,
183    /// Whether we've hit a breakpoint.
184    at_breakpoint: bool,
185    /// Whether replay has completed.
186    completed: bool,
187}
188
189impl TraceReplayer {
190    /// Creates a new replayer from a trace.
191    #[must_use]
192    pub fn new(trace: ReplayTrace) -> Self {
193        Self {
194            trace,
195            current_index: 0,
196            mode: ReplayMode::Run,
197            at_breakpoint: false,
198            completed: false,
199        }
200    }
201
202    /// Returns the trace metadata.
203    #[must_use]
204    pub fn metadata(&self) -> &TraceMetadata {
205        &self.trace.metadata
206    }
207
208    /// Returns the total number of events in the trace.
209    #[must_use]
210    pub fn event_count(&self) -> usize {
211        self.trace.len()
212    }
213
214    /// Returns the current event index.
215    #[must_use]
216    pub fn current_index(&self) -> usize {
217        self.current_index
218    }
219
220    /// Returns true if replay has completed.
221    #[must_use]
222    pub fn is_completed(&self) -> bool {
223        self.completed
224    }
225
226    /// Returns true if we're at a breakpoint.
227    #[must_use]
228    pub fn at_breakpoint(&self) -> bool {
229        self.at_breakpoint
230    }
231
232    /// Sets the replay mode.
233    pub fn set_mode(&mut self, mode: ReplayMode) {
234        self.mode = mode;
235        self.at_breakpoint = false;
236    }
237
238    /// Returns the current replay mode.
239    #[must_use]
240    pub fn mode(&self) -> &ReplayMode {
241        &self.mode
242    }
243
244    /// Returns the next expected event without advancing.
245    #[must_use]
246    pub fn peek(&self) -> Option<&ReplayEvent> {
247        self.trace.events.get(self.current_index)
248    }
249
250    /// Advances to the next event and returns it.
251    ///
252    /// Returns `None` if the trace has been fully replayed.
253    #[allow(clippy::should_implement_trait)]
254    pub fn next(&mut self) -> Option<&ReplayEvent> {
255        if self.completed {
256            return None;
257        }
258
259        let event_index = self.current_index;
260        let Some(event) = self.trace.events.get(event_index) else {
261            self.completed = true;
262            return None;
263        };
264        let should_break = self.check_breakpoint(event, event_index + 1);
265
266        self.current_index = event_index + 1;
267
268        if self.current_index >= self.trace.len() {
269            self.completed = true;
270        }
271
272        // Check for breakpoint
273        self.at_breakpoint = should_break;
274
275        self.trace.events.get(event_index)
276    }
277
278    /// Resets the replayer to the beginning of the trace.
279    pub fn reset(&mut self) {
280        self.current_index = 0;
281        self.completed = false;
282        self.at_breakpoint = false;
283    }
284
285    /// Seeks to a specific event index.
286    ///
287    /// Returns an error if the index is out of bounds.
288    pub fn seek(&mut self, index: usize) -> Result<(), ReplayError> {
289        if index >= self.trace.len() {
290            return Err(ReplayError::UnexpectedEnd { index });
291        }
292        self.current_index = index;
293        self.completed = false;
294        self.at_breakpoint = false;
295        Ok(())
296    }
297
298    /// Verifies that an actual event matches the expected recorded event.
299    ///
300    /// Returns an error with divergence details if they don't match.
301    pub fn verify(&self, actual: &ReplayEvent) -> Result<(), DivergenceError> {
302        let Some(expected) = self.peek() else {
303            return Err(DivergenceError {
304                index: self.current_index,
305                expected: None,
306                actual: actual.clone(),
307                context: "Trace ended but execution continued".to_string(),
308            });
309        };
310
311        if !events_match(expected, actual) {
312            return Err(DivergenceError {
313                index: self.current_index,
314                expected: Some(expected.clone()),
315                actual: actual.clone(),
316                context: divergence_context(expected, actual),
317            });
318        }
319
320        Ok(())
321    }
322
323    /// Verifies and advances to the next event.
324    ///
325    /// This is the main replay loop method: verify the actual event matches,
326    /// then advance.
327    pub fn verify_and_advance(&mut self, actual: &ReplayEvent) -> Result<(), ReplayError> {
328        self.verify(actual)?;
329        self.next();
330        Ok(())
331    }
332
333    /// Checks if we should stop at a breakpoint.
334    fn check_breakpoint(&self, event: &ReplayEvent, next_index: usize) -> bool {
335        if let ReplayMode::RunTo(ref breakpoint) = self.mode {
336            match breakpoint {
337                Breakpoint::EventIndex(idx) => next_index == *idx + 1,
338                Breakpoint::Tick(tick) => {
339                    if let ReplayEvent::TaskScheduled { at_tick, .. } = event {
340                        *at_tick >= *tick
341                    } else {
342                        false
343                    }
344                }
345                Breakpoint::Task(task_id) => {
346                    if let ReplayEvent::TaskScheduled { task, .. } = event {
347                        task == task_id
348                    } else {
349                        false
350                    }
351                }
352            }
353        } else {
354            matches!(self.mode, ReplayMode::Step)
355        }
356    }
357
358    /// Steps forward, respecting the current mode.
359    ///
360    /// In Step mode, advances one event and stops.
361    /// In Run mode, advances all events until completion.
362    /// In RunTo mode, advances until the breakpoint is reached.
363    pub fn step(&mut self) -> Result<Option<&ReplayEvent>, ReplayError> {
364        if self.completed {
365            return Ok(None);
366        }
367
368        self.at_breakpoint = false;
369        let event = self.next();
370
371        Ok(event)
372    }
373
374    /// Continues execution until completion or breakpoint.
375    ///
376    /// Returns the number of events processed.
377    pub fn run(&mut self) -> Result<usize, ReplayError> {
378        let mut count = 0;
379
380        while !self.completed && !self.at_breakpoint {
381            if self.next().is_some() {
382                count += 1;
383            }
384        }
385
386        Ok(count)
387    }
388
389    /// Returns all remaining events without consuming them.
390    #[must_use]
391    pub fn remaining_events(&self) -> &[ReplayEvent] {
392        if self.current_index >= self.trace.len() {
393            &[]
394        } else {
395            &self.trace.events[self.current_index..]
396        }
397    }
398
399    /// Consumes the replayer and returns the underlying trace.
400    #[must_use]
401    pub fn into_trace(self) -> ReplayTrace {
402        self.trace
403    }
404
405    /// Builds a structured replay report for CI/browser incident artifacts.
406    #[must_use]
407    pub fn browser_replay_report(
408        &self,
409        trace_id: impl Into<String>,
410        artifact_pointer: Option<impl Into<String>>,
411        rerun_commands: Vec<String>,
412        divergence: Option<&DivergenceError>,
413    ) -> BrowserReplayReport {
414        let (
415            divergence_index,
416            divergence_context,
417            minimization_prefix_len,
418            minimization_reduction_pct,
419        ) = divergence.map_or((None, None, None, None), |divergence| {
420            let prefix =
421                crate::trace::divergence::minimal_divergent_prefix(&self.trace, divergence.index);
422            let reduction_pct = minimization_reduction_pct(self.trace.len(), prefix.len());
423            (
424                Some(divergence.index),
425                Some(divergence.context.clone()),
426                Some(prefix.len()),
427                Some(reduction_pct),
428            )
429        });
430
431        BrowserReplayReport {
432            trace_id: trace_id.into(),
433            schema_version: self.trace.metadata.version,
434            seed: self.trace.metadata.seed,
435            event_count: self.trace.len(),
436            replayed_events: self.current_index,
437            completed: self.completed,
438            divergence_index,
439            divergence_context,
440            minimization_prefix_len,
441            minimization_reduction_pct,
442            artifact_pointer: artifact_pointer.map(Into::into),
443            rerun_commands,
444        }
445    }
446}
447
448fn minimization_reduction_pct(total: usize, prefix: usize) -> u64 {
449    if total == 0 || prefix >= total {
450        return 0;
451    }
452    let reduced = total - prefix;
453    ((reduced * 100) / total) as u64
454}
455
456// =============================================================================
457// Event Matching
458// =============================================================================
459
460/// Checks if two replay events match (allowing for minor differences).
461///
462/// Some fields may vary slightly between recording and replay due to
463/// timing or other factors. This function defines what constitutes a "match".
464fn events_match(expected: &ReplayEvent, actual: &ReplayEvent) -> bool {
465    use std::mem::discriminant;
466
467    // First check if they're the same variant
468    if discriminant(expected) != discriminant(actual) {
469        return false;
470    }
471
472    // For most events, exact equality is required
473    // In the future, we might want to be more lenient for certain fields
474    expected == actual
475}
476
477/// Generates helpful context for a divergence error.
478fn divergence_context(expected: &ReplayEvent, actual: &ReplayEvent) -> String {
479    match (expected, actual) {
480        (
481            ReplayEvent::TaskScheduled {
482                task: expected_task,
483                at_tick: expected_tick,
484            },
485            ReplayEvent::TaskScheduled {
486                task: actual_task,
487                at_tick: actual_tick,
488            },
489        ) => {
490            if expected_task == actual_task {
491                format!(
492                    "Task scheduled at different tick: expected {expected_tick}, got {actual_tick}"
493                )
494            } else {
495                format!(
496                    "Different task scheduled: expected {expected_task:?}, got {actual_task:?} (at tick {actual_tick})"
497                )
498            }
499        }
500        (
501            ReplayEvent::TimeAdvanced {
502                from_nanos: e_from,
503                to_nanos: e_to,
504            },
505            ReplayEvent::TimeAdvanced {
506                from_nanos: a_from,
507                to_nanos: a_to,
508            },
509        ) => {
510            format!(
511                "Time advanced differently: expected {e_from}ns -> {e_to}ns, got {a_from}ns -> {a_to}ns"
512            )
513        }
514        (
515            ReplayEvent::TaskCompleted {
516                task: e_task,
517                outcome: e_out,
518            },
519            ReplayEvent::TaskCompleted {
520                task: a_task,
521                outcome: a_out,
522            },
523        ) => {
524            if e_task == a_task {
525                format!("Different outcome: expected {e_out}, got {a_out}")
526            } else {
527                format!("Different task completed: expected {e_task:?}, got {a_task:?}")
528            }
529        }
530        _ => "Events have same type but different values".to_string(),
531    }
532}
533
534// =============================================================================
535// Event Source Trait
536// =============================================================================
537
538/// Trait for types that can provide replay events.
539///
540/// This allows the replayer to work with different event sources:
541/// - In-memory traces (`ReplayTrace`)
542/// - Streaming file readers (`TraceReader`)
543pub trait EventSource {
544    /// Returns the next event, or `None` if exhausted.
545    fn next_event(&mut self) -> Option<ReplayEvent>;
546
547    /// Returns the metadata for this trace.
548    fn metadata(&self) -> &TraceMetadata;
549}
550
551impl EventSource for ReplayTrace {
552    fn next_event(&mut self) -> Option<ReplayEvent> {
553        if self.cursor < self.events.len() {
554            let event = self.events[self.cursor].clone();
555            self.cursor += 1;
556            Some(event)
557        } else {
558            None
559        }
560    }
561
562    fn metadata(&self) -> &TraceMetadata {
563        &self.metadata
564    }
565}
566
567// =============================================================================
568// Tests
569// =============================================================================
570
571#[cfg(test)]
572mod tests {
573    use super::*;
574    use crate::trace::replay::TraceMetadata;
575
576    fn make_trace(events: Vec<ReplayEvent>) -> ReplayTrace {
577        ReplayTrace {
578            metadata: TraceMetadata::new(42),
579            events,
580            cursor: 0,
581        }
582    }
583
584    #[test]
585    fn basic_replay() {
586        let events = vec![
587            ReplayEvent::RngSeed { seed: 42 },
588            ReplayEvent::TaskScheduled {
589                task: CompactTaskId(1),
590                at_tick: 0,
591            },
592            ReplayEvent::TaskCompleted {
593                task: CompactTaskId(1),
594                outcome: 0,
595            },
596        ];
597
598        let mut replayer = TraceReplayer::new(make_trace(events));
599
600        assert_eq!(replayer.event_count(), 3);
601        assert_eq!(replayer.current_index(), 0);
602        assert!(!replayer.is_completed());
603
604        // Advance through events
605        let e1 = replayer.next().cloned();
606        assert!(matches!(e1, Some(ReplayEvent::RngSeed { seed: 42 })));
607
608        let e2 = replayer.next().cloned();
609        assert!(matches!(e2, Some(ReplayEvent::TaskScheduled { .. })));
610
611        let e3 = replayer.next().cloned();
612        assert!(matches!(e3, Some(ReplayEvent::TaskCompleted { .. })));
613
614        assert!(replayer.is_completed());
615        assert!(replayer.next().is_none());
616    }
617
618    #[test]
619    fn step_mode() {
620        let events = vec![
621            ReplayEvent::RngSeed { seed: 42 },
622            ReplayEvent::TaskScheduled {
623                task: CompactTaskId(1),
624                at_tick: 0,
625            },
626        ];
627
628        let mut replayer = TraceReplayer::new(make_trace(events));
629        replayer.set_mode(ReplayMode::Step);
630
631        // Each step should stop
632        replayer.step().unwrap();
633        assert!(replayer.at_breakpoint());
634
635        replayer.step().unwrap();
636        assert!(replayer.at_breakpoint());
637        assert!(replayer.is_completed());
638    }
639
640    #[test]
641    fn breakpoint_at_tick() {
642        let events = vec![
643            ReplayEvent::TaskScheduled {
644                task: CompactTaskId(1),
645                at_tick: 0,
646            },
647            ReplayEvent::TaskScheduled {
648                task: CompactTaskId(2),
649                at_tick: 5,
650            },
651            ReplayEvent::TaskScheduled {
652                task: CompactTaskId(3),
653                at_tick: 10,
654            },
655        ];
656
657        let mut replayer = TraceReplayer::new(make_trace(events));
658        replayer.set_mode(ReplayMode::RunTo(Breakpoint::Tick(5)));
659
660        // Run should stop at tick 5
661        let count = replayer.run().unwrap();
662        assert_eq!(count, 2);
663        assert!(replayer.at_breakpoint());
664        assert!(!replayer.is_completed());
665    }
666
667    #[test]
668    fn breakpoint_at_event_index() {
669        let events = vec![
670            ReplayEvent::RngSeed { seed: 42 },
671            ReplayEvent::TaskScheduled {
672                task: CompactTaskId(1),
673                at_tick: 0,
674            },
675            ReplayEvent::TaskCompleted {
676                task: CompactTaskId(1),
677                outcome: 0,
678            },
679        ];
680
681        let mut replayer = TraceReplayer::new(make_trace(events));
682        replayer.set_mode(ReplayMode::RunTo(Breakpoint::EventIndex(1)));
683
684        // Run should stop at event index 1
685        let count = replayer.run().unwrap();
686        assert_eq!(count, 2); // Processed events 0 and 1
687        assert!(replayer.at_breakpoint());
688    }
689
690    #[test]
691    fn verify_matching_events() {
692        let events = vec![ReplayEvent::RngSeed { seed: 42 }];
693        let replayer = TraceReplayer::new(make_trace(events));
694
695        // Matching event should succeed
696        let actual = ReplayEvent::RngSeed { seed: 42 };
697        assert!(replayer.verify(&actual).is_ok());
698    }
699
700    #[test]
701    fn verify_mismatching_events() {
702        let events = vec![ReplayEvent::RngSeed { seed: 42 }];
703        let replayer = TraceReplayer::new(make_trace(events));
704
705        // Different event should fail
706        let actual = ReplayEvent::RngSeed { seed: 99 };
707        let err = replayer.verify(&actual).unwrap_err();
708        assert_eq!(err.index, 0);
709    }
710
711    #[test]
712    fn seek_and_reset() {
713        let events = vec![
714            ReplayEvent::RngSeed { seed: 42 },
715            ReplayEvent::TaskScheduled {
716                task: CompactTaskId(1),
717                at_tick: 0,
718            },
719            ReplayEvent::TaskCompleted {
720                task: CompactTaskId(1),
721                outcome: 0,
722            },
723        ];
724
725        let mut replayer = TraceReplayer::new(make_trace(events));
726
727        // Advance partway
728        replayer.next();
729        replayer.next();
730        assert_eq!(replayer.current_index(), 2);
731
732        // Seek to beginning
733        replayer.seek(0).unwrap();
734        assert_eq!(replayer.current_index(), 0);
735        assert!(!replayer.is_completed());
736
737        // Advance to end
738        replayer.next();
739        replayer.next();
740        replayer.next();
741        assert!(replayer.is_completed());
742
743        // Reset
744        replayer.reset();
745        assert_eq!(replayer.current_index(), 0);
746        assert!(!replayer.is_completed());
747    }
748
749    #[test]
750    fn verify_and_advance() {
751        let events = vec![
752            ReplayEvent::RngSeed { seed: 42 },
753            ReplayEvent::TaskScheduled {
754                task: CompactTaskId(1),
755                at_tick: 0,
756            },
757        ];
758
759        let mut replayer = TraceReplayer::new(make_trace(events));
760
761        // Verify and advance with matching events
762        let actual1 = ReplayEvent::RngSeed { seed: 42 };
763        assert!(replayer.verify_and_advance(&actual1).is_ok());
764        assert_eq!(replayer.current_index(), 1);
765
766        let actual2 = ReplayEvent::TaskScheduled {
767            task: CompactTaskId(1),
768            at_tick: 0,
769        };
770        assert!(replayer.verify_and_advance(&actual2).is_ok());
771        assert!(replayer.is_completed());
772    }
773
774    #[test]
775    fn divergence_error_formatting() {
776        let expected = ReplayEvent::TaskScheduled {
777            task: CompactTaskId(1),
778            at_tick: 0,
779        };
780        let actual = ReplayEvent::TaskScheduled {
781            task: CompactTaskId(2),
782            at_tick: 0,
783        };
784
785        let err = DivergenceError {
786            index: 5,
787            expected: Some(expected.clone()),
788            actual: actual.clone(),
789            context: divergence_context(&expected, &actual),
790        };
791
792        let msg = format!("{err}");
793        assert!(msg.contains("event 5"));
794        assert!(msg.contains("Different task scheduled"));
795    }
796
797    #[test]
798    fn divergence_error_formatting_handles_trace_exhausted() {
799        let err = DivergenceError {
800            index: 7,
801            expected: None,
802            actual: ReplayEvent::RngSeed { seed: 99 },
803            context: "Trace ended but execution continued".to_string(),
804        };
805
806        let msg = format!("{err}");
807        assert!(msg.contains("<trace_exhausted>"));
808        assert!(msg.contains("event 7"));
809    }
810
811    #[test]
812    fn remaining_events() {
813        let events = vec![
814            ReplayEvent::RngSeed { seed: 42 },
815            ReplayEvent::TaskScheduled {
816                task: CompactTaskId(1),
817                at_tick: 0,
818            },
819            ReplayEvent::TaskCompleted {
820                task: CompactTaskId(1),
821                outcome: 0,
822            },
823        ];
824
825        let mut replayer = TraceReplayer::new(make_trace(events));
826
827        assert_eq!(replayer.remaining_events().len(), 3);
828
829        replayer.next();
830        assert_eq!(replayer.remaining_events().len(), 2);
831
832        replayer.next();
833        replayer.next();
834        assert_eq!(replayer.remaining_events().len(), 0);
835    }
836
837    #[test]
838    fn breakpoint_at_task() {
839        let events = vec![
840            ReplayEvent::TaskScheduled {
841                task: CompactTaskId(1),
842                at_tick: 0,
843            },
844            ReplayEvent::TaskScheduled {
845                task: CompactTaskId(2),
846                at_tick: 1,
847            },
848            ReplayEvent::TaskScheduled {
849                task: CompactTaskId(3),
850                at_tick: 2,
851            },
852        ];
853
854        let mut replayer = TraceReplayer::new(make_trace(events));
855        replayer.set_mode(ReplayMode::RunTo(Breakpoint::Task(CompactTaskId(2))));
856
857        let count = replayer.run().unwrap();
858        assert_eq!(count, 2);
859        assert!(replayer.at_breakpoint());
860        assert!(!replayer.is_completed());
861    }
862
863    #[test]
864    fn seek_out_of_bounds_returns_error() {
865        let events = vec![ReplayEvent::RngSeed { seed: 42 }];
866        let mut replayer = TraceReplayer::new(make_trace(events));
867
868        let err = replayer.seek(5).unwrap_err();
869        assert!(matches!(err, ReplayError::UnexpectedEnd { index: 5 }));
870    }
871
872    #[test]
873    fn verify_past_end_of_trace() {
874        let events = vec![ReplayEvent::RngSeed { seed: 42 }];
875        let mut replayer = TraceReplayer::new(make_trace(events));
876
877        // Consume the only event
878        replayer.next();
879        assert!(replayer.is_completed());
880
881        // Verify past end should produce divergence
882        let actual = ReplayEvent::RngSeed { seed: 99 };
883        let err = replayer.verify(&actual).unwrap_err();
884        assert!(err.expected.is_none());
885        assert_eq!(err.index, 1);
886        assert!(err.context.contains("Trace ended"));
887        assert!(format!("{err}").contains("<trace_exhausted>"));
888    }
889
890    #[test]
891    fn run_mode_completes_all_events() {
892        let events = vec![
893            ReplayEvent::RngSeed { seed: 1 },
894            ReplayEvent::RngSeed { seed: 2 },
895            ReplayEvent::RngSeed { seed: 3 },
896        ];
897
898        let mut replayer = TraceReplayer::new(make_trace(events));
899        replayer.set_mode(ReplayMode::Run);
900
901        let count = replayer.run().unwrap();
902        assert_eq!(count, 3);
903        assert!(replayer.is_completed());
904    }
905
906    #[test]
907    fn empty_trace_properties() {
908        let replayer = TraceReplayer::new(make_trace(vec![]));
909
910        assert_eq!(replayer.event_count(), 0);
911        assert!(replayer.remaining_events().is_empty());
912        assert!(replayer.peek().is_none());
913        // Note: run() on an empty trace does not terminate because
914        // next() returns None via early `?` without setting completed.
915        // This is a known edge case (empty traces are not typical).
916    }
917
918    #[test]
919    fn set_mode_clears_breakpoint_flag() {
920        let events = vec![
921            ReplayEvent::RngSeed { seed: 1 },
922            ReplayEvent::RngSeed { seed: 2 },
923        ];
924
925        let mut replayer = TraceReplayer::new(make_trace(events));
926        replayer.set_mode(ReplayMode::Step);
927
928        replayer.step().unwrap();
929        assert!(replayer.at_breakpoint());
930
931        // Changing mode clears the breakpoint flag
932        replayer.set_mode(ReplayMode::Run);
933        assert!(!replayer.at_breakpoint());
934    }
935
936    #[test]
937    fn into_trace_returns_original() {
938        let events = vec![ReplayEvent::RngSeed { seed: 42 }];
939        let trace = make_trace(events.clone());
940        let seed = trace.metadata.seed;
941
942        let replayer = TraceReplayer::new(trace);
943        let recovered = replayer.into_trace();
944
945        assert_eq!(recovered.metadata.seed, seed);
946        assert_eq!(recovered.events, events);
947    }
948
949    #[test]
950    fn metadata_accessible_from_replayer() {
951        let events = vec![ReplayEvent::RngSeed { seed: 99 }];
952        let trace = make_trace(events);
953        let replayer = TraceReplayer::new(trace);
954
955        assert_eq!(replayer.metadata().seed, 42);
956        assert_eq!(
957            replayer.metadata().version,
958            crate::trace::replay::REPLAY_SCHEMA_VERSION
959        );
960    }
961
962    #[test]
963    fn event_source_trait_cursor_advances() {
964        let events = vec![
965            ReplayEvent::RngSeed { seed: 1 },
966            ReplayEvent::RngSeed { seed: 2 },
967            ReplayEvent::RngSeed { seed: 3 },
968        ];
969        let mut trace = make_trace(events);
970
971        let e1 = trace.next_event().expect("event 1");
972        assert!(matches!(e1, ReplayEvent::RngSeed { seed: 1 }));
973
974        let e2 = trace.next_event().expect("event 2");
975        assert!(matches!(e2, ReplayEvent::RngSeed { seed: 2 }));
976
977        let e3 = trace.next_event().expect("event 3");
978        assert!(matches!(e3, ReplayEvent::RngSeed { seed: 3 }));
979
980        assert!(trace.next_event().is_none());
981    }
982
983    #[test]
984    fn divergence_context_time_advanced() {
985        let expected = ReplayEvent::TimeAdvanced {
986            from_nanos: 0,
987            to_nanos: 1000,
988        };
989        let actual = ReplayEvent::TimeAdvanced {
990            from_nanos: 0,
991            to_nanos: 2000,
992        };
993
994        let ctx = divergence_context(&expected, &actual);
995        assert!(ctx.contains("Time advanced differently"));
996    }
997
998    #[test]
999    fn divergence_context_task_completed_same_task() {
1000        let expected = ReplayEvent::TaskCompleted {
1001            task: CompactTaskId(1),
1002            outcome: 0,
1003        };
1004        let actual = ReplayEvent::TaskCompleted {
1005            task: CompactTaskId(1),
1006            outcome: 2,
1007        };
1008
1009        let ctx = divergence_context(&expected, &actual);
1010        assert!(ctx.contains("Different outcome"));
1011    }
1012
1013    #[test]
1014    fn divergence_context_different_variant_types() {
1015        let expected = ReplayEvent::RngSeed { seed: 1 };
1016        let actual = ReplayEvent::RngValue { value: 2 };
1017
1018        // events_match checks discriminant first
1019        assert!(!events_match(&expected, &actual));
1020    }
1021
1022    #[test]
1023    fn peek_does_not_advance() {
1024        let events = vec![
1025            ReplayEvent::RngSeed { seed: 42 },
1026            ReplayEvent::RngSeed { seed: 99 },
1027        ];
1028        let replayer = TraceReplayer::new(make_trace(events));
1029
1030        let e1 = replayer.peek().cloned();
1031        let e2 = replayer.peek().cloned();
1032        assert_eq!(e1, e2);
1033        assert_eq!(replayer.current_index(), 0);
1034    }
1035
1036    // =========================================================================
1037    // Wave 59 – pure data-type trait coverage
1038    // =========================================================================
1039
1040    #[test]
1041    fn replay_mode_debug_clone_eq_default() {
1042        let mode = ReplayMode::default();
1043        let dbg = format!("{mode:?}");
1044        assert!(dbg.contains("Run"), "{dbg}");
1045        let cloned = mode.clone();
1046        assert_eq!(mode, cloned);
1047        assert_ne!(mode, ReplayMode::Step);
1048    }
1049
1050    #[test]
1051    fn breakpoint_debug_clone_eq() {
1052        let bp = Breakpoint::Tick(42);
1053        let dbg = format!("{bp:?}");
1054        assert!(dbg.contains("Tick"), "{dbg}");
1055        let cloned = bp.clone();
1056        assert_eq!(bp, cloned);
1057        assert_ne!(bp, Breakpoint::EventIndex(7));
1058    }
1059
1060    #[test]
1061    fn browser_replay_report_without_divergence_records_completion() {
1062        let events = vec![
1063            ReplayEvent::RngSeed { seed: 42 },
1064            ReplayEvent::TaskScheduled {
1065                task: CompactTaskId(1),
1066                at_tick: 0,
1067            },
1068        ];
1069        let mut replayer = TraceReplayer::new(make_trace(events.clone()));
1070        for event in &events {
1071            replayer.verify_and_advance(event).expect("self-consistent");
1072        }
1073
1074        let report = replayer.browser_replay_report(
1075            "trace-browser-ok",
1076            Some("artifacts/replay/browser-ok.json"),
1077            vec!["asupersync lab replay --seed 42".to_string()],
1078            None,
1079        );
1080        assert_eq!(report.trace_id, "trace-browser-ok");
1081        assert_eq!(report.event_count, 2);
1082        assert_eq!(report.replayed_events, 2);
1083        assert!(report.completed);
1084        assert!(report.divergence_index.is_none());
1085        assert!(report.minimization_prefix_len.is_none());
1086        assert_eq!(
1087            report.artifact_pointer,
1088            Some("artifacts/replay/browser-ok.json".to_string())
1089        );
1090
1091        let json = report.to_json_pretty().expect("serialize report");
1092        assert!(json.contains("trace-browser-ok"));
1093        assert!(json.contains("rerun_commands"));
1094    }
1095
1096    #[test]
1097    fn browser_replay_report_with_divergence_includes_minimization_hint() {
1098        let events = vec![
1099            ReplayEvent::RngSeed { seed: 42 },
1100            ReplayEvent::TaskScheduled {
1101                task: CompactTaskId(1),
1102                at_tick: 0,
1103            },
1104            ReplayEvent::TaskCompleted {
1105                task: CompactTaskId(1),
1106                outcome: 0,
1107            },
1108        ];
1109        let replayer = TraceReplayer::new(make_trace(events));
1110        let bad = ReplayEvent::RngSeed { seed: 999 };
1111        let divergence = replayer.verify(&bad).expect_err("expected divergence");
1112        let report = replayer.browser_replay_report(
1113            "trace-browser-div",
1114            None::<&str>,
1115            vec!["asupersync lab replay --seed 42 --window-start 0 --window-events 1".to_string()],
1116            Some(&divergence),
1117        );
1118        assert_eq!(report.divergence_index, Some(0));
1119        assert!(report.divergence_context.is_some());
1120        assert_eq!(report.minimization_prefix_len, Some(1));
1121        assert_eq!(report.minimization_reduction_pct, Some(66));
1122        assert!(!report.rerun_commands.is_empty());
1123    }
1124}