1use crate::trace::replay::{CompactTaskId, ReplayEvent, ReplayTrace, TraceMetadata};
35use serde::Serialize;
36use std::fmt;
37
38#[derive(Debug, Clone, PartialEq, Eq, Default)]
44pub enum ReplayMode {
45 #[default]
47 Run,
48 Step,
50 RunTo(Breakpoint),
52}
53
54#[derive(Debug, Clone, PartialEq, Eq)]
56pub enum Breakpoint {
57 Tick(u64),
59 Task(CompactTaskId),
61 EventIndex(usize),
63}
64
65#[derive(Debug)]
71pub struct DivergenceError {
72 pub index: usize,
74 pub expected: Option<ReplayEvent>,
76 pub actual: ReplayEvent,
78 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#[derive(Debug, thiserror::Error)]
100pub enum ReplayError {
101 #[error("{0}")]
103 Divergence(#[from] DivergenceError),
104
105 #[error("trace ended unexpectedly at event {index}, expected more events")]
107 UnexpectedEnd {
108 index: usize,
110 },
111
112 #[error("invalid trace metadata: {0}")]
114 InvalidMetadata(String),
115
116 #[error("trace version mismatch: expected {expected}, found {found}")]
118 VersionMismatch {
119 expected: u32,
121 found: u32,
123 },
124}
125
126#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
128pub struct BrowserReplayReport {
129 pub trace_id: String,
131 pub schema_version: u32,
133 pub seed: u64,
135 pub event_count: usize,
137 pub replayed_events: usize,
139 pub completed: bool,
141 pub divergence_index: Option<usize>,
143 pub divergence_context: Option<String>,
145 pub minimization_prefix_len: Option<usize>,
147 pub minimization_reduction_pct: Option<u64>,
149 pub artifact_pointer: Option<String>,
151 pub rerun_commands: Vec<String>,
153}
154
155impl BrowserReplayReport {
156 pub fn to_json_pretty(&self) -> Result<String, serde_json::Error> {
162 serde_json::to_string_pretty(self)
163 }
164}
165
166#[derive(Debug)]
176pub struct TraceReplayer {
177 trace: ReplayTrace,
179 current_index: usize,
181 mode: ReplayMode,
183 at_breakpoint: bool,
185 completed: bool,
187}
188
189impl TraceReplayer {
190 #[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 #[must_use]
204 pub fn metadata(&self) -> &TraceMetadata {
205 &self.trace.metadata
206 }
207
208 #[must_use]
210 pub fn event_count(&self) -> usize {
211 self.trace.len()
212 }
213
214 #[must_use]
216 pub fn current_index(&self) -> usize {
217 self.current_index
218 }
219
220 #[must_use]
222 pub fn is_completed(&self) -> bool {
223 self.completed
224 }
225
226 #[must_use]
228 pub fn at_breakpoint(&self) -> bool {
229 self.at_breakpoint
230 }
231
232 pub fn set_mode(&mut self, mode: ReplayMode) {
234 self.mode = mode;
235 self.at_breakpoint = false;
236 }
237
238 #[must_use]
240 pub fn mode(&self) -> &ReplayMode {
241 &self.mode
242 }
243
244 #[must_use]
246 pub fn peek(&self) -> Option<&ReplayEvent> {
247 self.trace.events.get(self.current_index)
248 }
249
250 #[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 self.at_breakpoint = should_break;
274
275 self.trace.events.get(event_index)
276 }
277
278 pub fn reset(&mut self) {
280 self.current_index = 0;
281 self.completed = false;
282 self.at_breakpoint = false;
283 }
284
285 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 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 pub fn verify_and_advance(&mut self, actual: &ReplayEvent) -> Result<(), ReplayError> {
328 self.verify(actual)?;
329 self.next();
330 Ok(())
331 }
332
333 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 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 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 #[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 #[must_use]
401 pub fn into_trace(self) -> ReplayTrace {
402 self.trace
403 }
404
405 #[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
456fn events_match(expected: &ReplayEvent, actual: &ReplayEvent) -> bool {
465 use std::mem::discriminant;
466
467 if discriminant(expected) != discriminant(actual) {
469 return false;
470 }
471
472 expected == actual
475}
476
477fn 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
534pub trait EventSource {
544 fn next_event(&mut self) -> Option<ReplayEvent>;
546
547 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#[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 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 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 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 let count = replayer.run().unwrap();
686 assert_eq!(count, 2); 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 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 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 replayer.next();
729 replayer.next();
730 assert_eq!(replayer.current_index(), 2);
731
732 replayer.seek(0).unwrap();
734 assert_eq!(replayer.current_index(), 0);
735 assert!(!replayer.is_completed());
736
737 replayer.next();
739 replayer.next();
740 replayer.next();
741 assert!(replayer.is_completed());
742
743 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 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 replayer.next();
879 assert!(replayer.is_completed());
880
881 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 }
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 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 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 #[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}