1use crate::audit::{AuditStore, ExecutionTrace, TraceEvent, TraceEventKind};
8use crate::types::{CostEstimate, TokenUsage};
9use chrono::{DateTime, Utc};
10use serde::{Deserialize, Serialize};
11use std::collections::HashSet;
12use uuid::Uuid;
13
14#[derive(Debug, Clone, thiserror::Error)]
20pub enum ReplayError {
21 #[error("trace not found: {0}")]
22 TraceNotFound(Uuid),
23 #[error("position {position} out of bounds (total: {total})")]
24 OutOfBounds { position: usize, total: usize },
25 #[error("bookmark index {0} out of bounds")]
26 BookmarkNotFound(usize),
27 #[error("empty trace: no events to replay")]
28 EmptyTrace,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct Bookmark {
38 pub position: usize,
39 pub label: String,
40 pub created_at: DateTime<Utc>,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct ReplaySnapshot {
50 pub trace_id: Uuid,
51 pub position: usize,
52 pub total_events: usize,
53 pub progress_pct: f64,
55 pub current_event: Option<TraceEvent>,
56 pub elapsed_from_start: Option<u64>,
58 pub cumulative_usage: TokenUsage,
59 pub cumulative_cost: CostEstimate,
60 pub tools_executed_so_far: Vec<String>,
61 pub errors_so_far: usize,
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct TimelineEntry {
71 pub sequence: usize,
72 pub timestamp: DateTime<Utc>,
73 pub elapsed_ms: u64,
75 pub description: String,
76 pub is_current: bool,
78 pub is_bookmarked: bool,
80}
81
82pub struct ReplayEngine {
89 trace: ExecutionTrace,
90 position: usize,
92 bookmarks: Vec<Bookmark>,
93}
94
95impl ReplayEngine {
96 pub fn new(trace: ExecutionTrace) -> Self {
98 Self {
99 trace,
100 position: 0,
101 bookmarks: Vec::new(),
102 }
103 }
104
105 pub fn from_store(store: &AuditStore, trace_id: Uuid) -> Result<Self, ReplayError> {
107 let trace = store
108 .get_trace(trace_id)
109 .ok_or(ReplayError::TraceNotFound(trace_id))?;
110 Ok(Self::new(trace.clone()))
111 }
112
113 pub fn position(&self) -> usize {
115 self.position
116 }
117
118 pub fn total_events(&self) -> usize {
120 self.trace.events.len()
121 }
122
123 pub fn is_at_start(&self) -> bool {
125 self.position == 0
126 }
127
128 pub fn is_at_end(&self) -> bool {
130 self.trace.events.is_empty() || self.position >= self.trace.events.len() - 1
131 }
132
133 pub fn step_forward(&mut self) -> Option<&TraceEvent> {
136 if self.position + 1 < self.trace.events.len() {
137 self.position += 1;
138 self.trace.events.get(self.position)
139 } else {
140 None
141 }
142 }
143
144 pub fn step_backward(&mut self) -> Option<&TraceEvent> {
147 if self.position > 0 {
148 self.position -= 1;
149 self.trace.events.get(self.position)
150 } else {
151 None
152 }
153 }
154
155 pub fn seek(&mut self, position: usize) -> Result<&TraceEvent, ReplayError> {
157 if position >= self.trace.events.len() {
158 return Err(ReplayError::OutOfBounds {
159 position,
160 total: self.trace.events.len(),
161 });
162 }
163 self.position = position;
164 Ok(&self.trace.events[self.position])
165 }
166
167 pub fn rewind(&mut self) {
169 self.position = 0;
170 }
171
172 pub fn fast_forward(&mut self) {
174 if !self.trace.events.is_empty() {
175 self.position = self.trace.events.len() - 1;
176 }
177 }
178
179 pub fn current_event(&self) -> Option<&TraceEvent> {
181 self.trace.events.get(self.position)
182 }
183
184 pub fn snapshot(&self) -> ReplaySnapshot {
186 let total_events = self.trace.events.len();
187 let current_event = self.trace.events.get(self.position).cloned();
188
189 let elapsed_from_start = current_event.as_ref().map(|e| {
190 (e.timestamp - self.trace.started_at)
191 .num_milliseconds()
192 .max(0) as u64
193 });
194
195 let progress_pct = if total_events == 0 {
196 0.0
197 } else if total_events == 1 {
198 100.0
199 } else {
200 (self.position as f64 / (total_events - 1) as f64) * 100.0
201 };
202
203 let end = if total_events == 0 {
204 0
205 } else {
206 self.position + 1
207 };
208
209 let tools_executed_so_far: Vec<String> = self
210 .trace
211 .events
212 .iter()
213 .take(end)
214 .filter_map(|e| {
215 if let TraceEventKind::ToolExecuted { ref tool, .. } = e.kind {
216 Some(tool.clone())
217 } else {
218 None
219 }
220 })
221 .collect();
222
223 let errors_so_far = self
224 .trace
225 .events
226 .iter()
227 .take(end)
228 .filter(|e| matches!(&e.kind, TraceEventKind::Error { .. }))
229 .count();
230
231 ReplaySnapshot {
232 trace_id: self.trace.trace_id,
233 position: self.position,
234 total_events,
235 progress_pct,
236 current_event,
237 elapsed_from_start,
238 cumulative_usage: self.cumulative_usage(),
239 cumulative_cost: self.cumulative_cost(),
240 tools_executed_so_far,
241 errors_so_far,
242 }
243 }
244
245 pub fn describe_current(&self) -> String {
247 match self.current_event() {
248 Some(event) => format!(
249 "[{}/{}] {}",
250 self.position + 1,
251 self.total_events(),
252 describe_event(&event.kind)
253 ),
254 None => "No events".to_string(),
255 }
256 }
257
258 pub fn timeline(&self) -> Vec<TimelineEntry> {
260 let bookmark_positions: HashSet<usize> =
261 self.bookmarks.iter().map(|b| b.position).collect();
262
263 self.trace
264 .events
265 .iter()
266 .map(|event| {
267 let elapsed_ms = (event.timestamp - self.trace.started_at)
268 .num_milliseconds()
269 .max(0) as u64;
270
271 TimelineEntry {
272 sequence: event.sequence,
273 timestamp: event.timestamp,
274 elapsed_ms,
275 description: describe_event(&event.kind),
276 is_current: event.sequence == self.position,
277 is_bookmarked: bookmark_positions.contains(&event.sequence),
278 }
279 })
280 .collect()
281 }
282
283 pub fn add_bookmark(&mut self, label: impl Into<String>) {
285 self.bookmarks.push(Bookmark {
286 position: self.position,
287 label: label.into(),
288 created_at: Utc::now(),
289 });
290 }
291
292 pub fn bookmarks(&self) -> &[Bookmark] {
294 &self.bookmarks
295 }
296
297 pub fn goto_bookmark(&mut self, index: usize) -> Result<&TraceEvent, ReplayError> {
299 let position = self
300 .bookmarks
301 .get(index)
302 .ok_or(ReplayError::BookmarkNotFound(index))?
303 .position;
304 self.seek(position)
305 }
306
307 pub fn trace(&self) -> &ExecutionTrace {
309 &self.trace
310 }
311
312 pub fn skip_to_next_tool_event(&mut self) -> Option<&TraceEvent> {
315 let start = self.position + 1;
316 for i in start..self.trace.events.len() {
317 match &self.trace.events[i].kind {
318 TraceEventKind::ToolRequested { .. }
319 | TraceEventKind::ToolApproved { .. }
320 | TraceEventKind::ToolDenied { .. }
321 | TraceEventKind::ToolExecuted { .. } => {
322 self.position = i;
323 return self.trace.events.get(i);
324 }
325 _ => continue,
326 }
327 }
328 None
329 }
330
331 pub fn cumulative_usage(&self) -> TokenUsage {
333 let mut usage = TokenUsage::default();
334 let end = self.position + 1;
335 for event in self.trace.events.iter().take(end) {
336 if let TraceEventKind::LlmCall {
337 input_tokens,
338 output_tokens,
339 ..
340 } = &event.kind
341 {
342 usage.input_tokens += input_tokens;
343 usage.output_tokens += output_tokens;
344 }
345 }
346 usage
347 }
348
349 pub fn cumulative_cost(&self) -> CostEstimate {
354 let mut estimate = CostEstimate::default();
355 let end = self.position + 1;
356 for event in self.trace.events.iter().take(end) {
357 if let TraceEventKind::LlmCall {
358 cost,
359 input_tokens,
360 output_tokens,
361 ..
362 } = &event.kind
363 {
364 let total_tokens = input_tokens + output_tokens;
365 if total_tokens > 0 {
366 estimate.input_cost += cost * (*input_tokens as f64 / total_tokens as f64);
367 estimate.output_cost += cost * (*output_tokens as f64 / total_tokens as f64);
368 }
369 }
370 }
371 estimate
372 }
373}
374
375pub fn describe_event(kind: &TraceEventKind) -> String {
381 match kind {
382 TraceEventKind::TaskStarted { goal, .. } => {
383 format!("Task started: {}", goal)
384 }
385 TraceEventKind::TaskCompleted {
386 success,
387 iterations,
388 ..
389 } => {
390 format!(
391 "Task {} after {} iterations",
392 if *success {
393 "completed successfully"
394 } else {
395 "failed"
396 },
397 iterations
398 )
399 }
400 TraceEventKind::ToolRequested {
401 tool, risk_level, ..
402 } => {
403 format!("Tool requested: {} (risk: {:?})", tool, risk_level)
404 }
405 TraceEventKind::ToolApproved { tool } => {
406 format!("Tool approved: {}", tool)
407 }
408 TraceEventKind::ToolDenied { tool, reason } => {
409 format!("Tool denied: {} - {}", tool, reason)
410 }
411 TraceEventKind::ApprovalRequested { tool, .. } => {
412 format!("Approval requested for: {}", tool)
413 }
414 TraceEventKind::ApprovalDecision { tool, approved } => {
415 format!(
416 "Approval {}: {}",
417 if *approved { "granted" } else { "rejected" },
418 tool
419 )
420 }
421 TraceEventKind::ToolExecuted {
422 tool,
423 success,
424 duration_ms,
425 ..
426 } => {
427 format!(
428 "Tool executed: {} ({}, {}ms)",
429 tool,
430 if *success { "ok" } else { "failed" },
431 duration_ms
432 )
433 }
434 TraceEventKind::LlmCall {
435 model,
436 input_tokens,
437 output_tokens,
438 ..
439 } => {
440 format!(
441 "LLM call: {} ({}/{} tokens)",
442 model, input_tokens, output_tokens
443 )
444 }
445 TraceEventKind::StatusChange { from, to } => {
446 format!("Status: {} -> {}", from, to)
447 }
448 TraceEventKind::Error { message } => {
449 format!("Error: {}", message)
450 }
451 }
452}
453
454pub struct ReplaySession {
460 engines: Vec<ReplayEngine>,
461 active_index: Option<usize>,
462}
463
464impl ReplaySession {
465 pub fn new() -> Self {
467 Self {
468 engines: Vec::new(),
469 active_index: None,
470 }
471 }
472
473 pub fn add_replay(&mut self, trace: ExecutionTrace) -> usize {
476 let index = self.engines.len();
477 self.engines.push(ReplayEngine::new(trace));
478 if self.active_index.is_none() {
479 self.active_index = Some(index);
480 }
481 index
482 }
483
484 pub fn set_active(&mut self, index: usize) -> Result<(), ReplayError> {
486 if index >= self.engines.len() {
487 return Err(ReplayError::OutOfBounds {
488 position: index,
489 total: self.engines.len(),
490 });
491 }
492 self.active_index = Some(index);
493 Ok(())
494 }
495
496 pub fn active(&self) -> Option<&ReplayEngine> {
498 self.active_index.and_then(|i| self.engines.get(i))
499 }
500
501 pub fn active_mut(&mut self) -> Option<&mut ReplayEngine> {
503 self.active_index.and_then(|i| self.engines.get_mut(i))
504 }
505
506 pub fn list_replays(&self) -> Vec<ReplaySummary> {
508 self.engines
509 .iter()
510 .enumerate()
511 .map(|(i, engine)| ReplaySummary {
512 index: i,
513 trace_id: engine.trace().trace_id,
514 goal: engine.trace().goal.clone(),
515 event_count: engine.total_events(),
516 is_active: self.active_index == Some(i),
517 })
518 .collect()
519 }
520
521 pub fn len(&self) -> usize {
523 self.engines.len()
524 }
525
526 pub fn is_empty(&self) -> bool {
528 self.engines.is_empty()
529 }
530}
531
532impl Default for ReplaySession {
533 fn default() -> Self {
534 Self::new()
535 }
536}
537
538#[derive(Debug, Clone, Serialize, Deserialize)]
540pub struct ReplaySummary {
541 pub index: usize,
542 pub trace_id: Uuid,
543 pub goal: String,
544 pub event_count: usize,
545 pub is_active: bool,
546}
547
548#[cfg(test)]
553mod tests {
554 use super::*;
555 use crate::audit::{AuditStore, ExecutionTrace, TraceEventKind};
556 use crate::types::RiskLevel;
557 use uuid::Uuid;
558
559 fn sample_trace() -> ExecutionTrace {
572 let session_id = Uuid::new_v4();
573 let task_id = Uuid::new_v4();
574 let mut trace = ExecutionTrace::new(session_id, task_id, "test task");
575
576 trace.push_event(TraceEventKind::ToolRequested {
578 tool: "file_read".into(),
579 risk_level: RiskLevel::ReadOnly,
580 args_summary: "path=/src/main.rs".into(),
581 });
582 trace.push_event(TraceEventKind::ToolApproved {
583 tool: "file_read".into(),
584 });
585 trace.push_event(TraceEventKind::ToolExecuted {
586 tool: "file_read".into(),
587 success: true,
588 duration_ms: 42,
589 output_preview: "fn main() {...}".into(),
590 });
591 trace.push_event(TraceEventKind::LlmCall {
592 model: "gpt-4".into(),
593 input_tokens: 500,
594 output_tokens: 200,
595 cost: 0.021,
596 });
597 trace.push_event(TraceEventKind::ToolRequested {
598 tool: "file_write".into(),
599 risk_level: RiskLevel::Write,
600 args_summary: "path=/src/lib.rs".into(),
601 });
602 trace.push_event(TraceEventKind::ToolDenied {
603 tool: "file_write".into(),
604 reason: "path denied".into(),
605 });
606 trace.push_event(TraceEventKind::Error {
607 message: "write denied".into(),
608 });
609
610 trace.iterations = 3;
612 trace.complete(true);
613 trace
614 }
615
616 #[test]
620 fn test_replay_engine_new() {
621 let trace = sample_trace();
622 let engine = ReplayEngine::new(trace);
623 assert_eq!(engine.position(), 0);
624 assert_eq!(engine.total_events(), 9);
625 assert!(engine.bookmarks().is_empty());
626 }
627
628 #[test]
632 fn test_replay_engine_step_forward() {
633 let mut engine = ReplayEngine::new(sample_trace());
634 assert_eq!(engine.position(), 0);
635
636 let event = engine.step_forward().unwrap();
637 assert_eq!(event.sequence, 1);
638 assert_eq!(engine.position(), 1);
639
640 let event = engine.step_forward().unwrap();
641 assert_eq!(event.sequence, 2);
642 assert_eq!(engine.position(), 2);
643 }
644
645 #[test]
649 fn test_replay_engine_step_backward() {
650 let mut engine = ReplayEngine::new(sample_trace());
651 engine.seek(3).unwrap();
652 assert_eq!(engine.position(), 3);
653
654 let event = engine.step_backward().unwrap();
655 assert_eq!(event.sequence, 2);
656 assert_eq!(engine.position(), 2);
657
658 let event = engine.step_backward().unwrap();
659 assert_eq!(event.sequence, 1);
660 assert_eq!(engine.position(), 1);
661 }
662
663 #[test]
667 fn test_replay_engine_at_boundaries() {
668 let trace = sample_trace();
669 let total = trace.events.len();
670 let mut engine = ReplayEngine::new(trace);
671
672 assert!(engine.is_at_start());
674 assert!(!engine.is_at_end());
675
676 assert!(engine.step_backward().is_none());
678 assert_eq!(engine.position(), 0);
679
680 engine.fast_forward();
682 assert_eq!(engine.position(), total - 1);
683 assert!(!engine.is_at_start());
684 assert!(engine.is_at_end());
685
686 assert!(engine.step_forward().is_none());
688 assert_eq!(engine.position(), total - 1);
689 }
690
691 #[test]
695 fn test_replay_engine_seek() {
696 let mut engine = ReplayEngine::new(sample_trace());
697 let event = engine.seek(4).unwrap();
698 assert_eq!(event.sequence, 4);
699 assert_eq!(engine.position(), 4);
700
701 let event = engine.seek(0).unwrap();
702 assert_eq!(event.sequence, 0);
703 assert_eq!(engine.position(), 0);
704
705 let event = engine.seek(8).unwrap();
706 assert_eq!(event.sequence, 8);
707 assert_eq!(engine.position(), 8);
708 }
709
710 #[test]
714 fn test_replay_engine_seek_out_of_bounds() {
715 let mut engine = ReplayEngine::new(sample_trace());
716 let result = engine.seek(100);
717 assert!(matches!(
718 result,
719 Err(ReplayError::OutOfBounds {
720 position: 100,
721 total: 9
722 })
723 ));
724
725 let result = engine.seek(9);
727 assert!(matches!(result, Err(ReplayError::OutOfBounds { .. })));
728 }
729
730 #[test]
734 fn test_replay_engine_rewind() {
735 let mut engine = ReplayEngine::new(sample_trace());
736 engine.seek(5).unwrap();
737 assert_eq!(engine.position(), 5);
738
739 engine.rewind();
740 assert_eq!(engine.position(), 0);
741 assert!(engine.is_at_start());
742 }
743
744 #[test]
748 fn test_replay_engine_fast_forward() {
749 let trace = sample_trace();
750 let total = trace.events.len();
751 let mut engine = ReplayEngine::new(trace);
752 assert_eq!(engine.position(), 0);
753
754 engine.fast_forward();
755 assert_eq!(engine.position(), total - 1);
756 assert!(engine.is_at_end());
757 }
758
759 #[test]
763 fn test_replay_engine_current_event() {
764 let engine = ReplayEngine::new(sample_trace());
765 let event = engine.current_event().unwrap();
766 assert_eq!(event.sequence, 0);
767 assert!(matches!(
768 &event.kind,
769 TraceEventKind::TaskStarted { goal, .. } if goal == "test task"
770 ));
771 }
772
773 #[test]
777 fn test_replay_engine_snapshot() {
778 let mut engine = ReplayEngine::new(sample_trace());
779
780 let snap = engine.snapshot();
782 assert_eq!(snap.trace_id, engine.trace().trace_id);
783 assert_eq!(snap.position, 0);
784 assert_eq!(snap.total_events, 9);
785 assert!(snap.current_event.is_some());
786 assert!(snap.elapsed_from_start.is_some());
787 assert!(snap.tools_executed_so_far.is_empty());
788 assert_eq!(snap.errors_so_far, 0);
789
790 engine.seek(3).unwrap();
792 let snap = engine.snapshot();
793 assert_eq!(snap.position, 3);
794 assert_eq!(snap.tools_executed_so_far.len(), 1);
795 assert_eq!(snap.tools_executed_so_far[0], "file_read");
796
797 engine.seek(7).unwrap();
799 let snap = engine.snapshot();
800 assert_eq!(snap.errors_so_far, 1);
801 }
802
803 #[test]
807 fn test_replay_engine_describe_current() {
808 let mut engine = ReplayEngine::new(sample_trace());
809 let desc = engine.describe_current();
810 assert!(desc.contains("[1/9]"));
811 assert!(desc.contains("Task started"));
812 assert!(desc.contains("test task"));
813
814 engine.seek(4).unwrap();
815 let desc = engine.describe_current();
816 assert!(desc.contains("[5/9]"));
817 assert!(desc.contains("LLM call"));
818 assert!(desc.contains("gpt-4"));
819 }
820
821 #[test]
825 fn test_replay_engine_timeline() {
826 let trace = sample_trace();
827 let total = trace.events.len();
828 let mut engine = ReplayEngine::new(trace);
829
830 let timeline = engine.timeline();
831 assert_eq!(timeline.len(), total);
832
833 assert!(timeline[0].is_current);
835 assert!(!timeline[1].is_current);
836
837 engine.step_forward();
839 let timeline = engine.timeline();
840 assert!(!timeline[0].is_current);
841 assert!(timeline[1].is_current);
842
843 for entry in &timeline {
845 assert!(!entry.description.is_empty());
846 }
847 }
848
849 #[test]
853 fn test_replay_engine_bookmarks() {
854 let mut engine = ReplayEngine::new(sample_trace());
855
856 engine.add_bookmark("start");
857 engine.step_forward();
858 engine.step_forward();
859 engine.add_bookmark("after two steps");
860
861 assert_eq!(engine.bookmarks().len(), 2);
862 assert_eq!(engine.bookmarks()[0].position, 0);
863 assert_eq!(engine.bookmarks()[0].label, "start");
864 assert_eq!(engine.bookmarks()[1].position, 2);
865 assert_eq!(engine.bookmarks()[1].label, "after two steps");
866
867 let event = engine.goto_bookmark(0).unwrap();
869 assert_eq!(event.sequence, 0);
870 assert_eq!(engine.position(), 0);
871
872 let timeline = engine.timeline();
874 assert!(timeline[0].is_bookmarked);
875 assert!(!timeline[1].is_bookmarked);
876 assert!(timeline[2].is_bookmarked);
877 }
878
879 #[test]
883 fn test_replay_engine_bookmark_out_of_bounds() {
884 let mut engine = ReplayEngine::new(sample_trace());
885 let result = engine.goto_bookmark(0);
886 assert!(matches!(result, Err(ReplayError::BookmarkNotFound(0))));
887
888 engine.add_bookmark("only one");
889 let result = engine.goto_bookmark(5);
890 assert!(matches!(result, Err(ReplayError::BookmarkNotFound(5))));
891 }
892
893 #[test]
897 fn test_replay_engine_skip_to_tool() {
898 let mut engine = ReplayEngine::new(sample_trace());
899
900 let event = engine.skip_to_next_tool_event().unwrap();
902 assert_eq!(event.sequence, 1);
903 assert!(matches!(
904 &event.kind,
905 TraceEventKind::ToolRequested { tool, .. } if tool == "file_read"
906 ));
907 assert_eq!(engine.position(), 1);
908
909 let event = engine.skip_to_next_tool_event().unwrap();
911 assert_eq!(event.sequence, 2);
912 assert!(matches!(
913 &event.kind,
914 TraceEventKind::ToolApproved { tool } if tool == "file_read"
915 ));
916
917 let event = engine.skip_to_next_tool_event().unwrap();
919 assert_eq!(event.sequence, 3);
920
921 let event = engine.skip_to_next_tool_event().unwrap();
923 assert_eq!(event.sequence, 5);
924
925 let event = engine.skip_to_next_tool_event().unwrap();
927 assert_eq!(event.sequence, 6);
928
929 assert!(engine.skip_to_next_tool_event().is_none());
931 }
932
933 #[test]
937 fn test_replay_engine_cumulative_usage() {
938 let mut engine = ReplayEngine::new(sample_trace());
939
940 let usage = engine.cumulative_usage();
942 assert_eq!(usage.input_tokens, 0);
943 assert_eq!(usage.output_tokens, 0);
944
945 engine.seek(4).unwrap();
947 let usage = engine.cumulative_usage();
948 assert_eq!(usage.input_tokens, 500);
949 assert_eq!(usage.output_tokens, 200);
950 assert_eq!(usage.total(), 700);
951
952 engine.fast_forward();
954 let usage = engine.cumulative_usage();
955 assert_eq!(usage.input_tokens, 500);
956 assert_eq!(usage.output_tokens, 200);
957 }
958
959 #[test]
963 fn test_replay_engine_cumulative_cost() {
964 let mut engine = ReplayEngine::new(sample_trace());
965
966 let cost = engine.cumulative_cost();
968 assert!((cost.total() - 0.0).abs() < f64::EPSILON);
969
970 engine.seek(4).unwrap();
972 let cost = engine.cumulative_cost();
973 assert!((cost.total() - 0.021).abs() < 0.001);
974 assert!(cost.input_cost > 0.0);
976 assert!(cost.output_cost > 0.0);
977
978 engine.fast_forward();
980 let cost = engine.cumulative_cost();
981 assert!((cost.total() - 0.021).abs() < 0.001);
982 }
983
984 #[test]
988 fn test_replay_engine_from_store() {
989 let trace = sample_trace();
990 let trace_id = trace.trace_id;
991 let mut store = AuditStore::new();
992 store.add_trace(trace);
993
994 let engine = ReplayEngine::from_store(&store, trace_id).unwrap();
995 assert_eq!(engine.trace().trace_id, trace_id);
996 assert_eq!(engine.position(), 0);
997 assert_eq!(engine.total_events(), 9);
998 }
999
1000 #[test]
1004 fn test_replay_engine_from_store_not_found() {
1005 let store = AuditStore::new();
1006 let missing_id = Uuid::new_v4();
1007 let result = ReplayEngine::from_store(&store, missing_id);
1008 assert!(matches!(result, Err(ReplayError::TraceNotFound(id)) if id == missing_id));
1009 }
1010
1011 #[test]
1015 fn test_replay_session_new() {
1016 let session = ReplaySession::new();
1017 assert!(session.is_empty());
1018 assert_eq!(session.len(), 0);
1019 assert!(session.active().is_none());
1020 assert!(session.list_replays().is_empty());
1021 }
1022
1023 #[test]
1027 fn test_replay_session_add_and_activate() {
1028 let mut session = ReplaySession::new();
1029
1030 let idx0 = session.add_replay(sample_trace());
1031 assert_eq!(idx0, 0);
1032 assert_eq!(session.len(), 1);
1033 assert!(!session.is_empty());
1034
1035 assert!(session.active().is_some());
1037 assert_eq!(session.active().unwrap().position(), 0);
1038
1039 let idx1 = session.add_replay(sample_trace());
1040 assert_eq!(idx1, 1);
1041 assert_eq!(session.len(), 2);
1042
1043 let summaries = session.list_replays();
1045 assert!(summaries[0].is_active);
1046 assert!(!summaries[1].is_active);
1047
1048 session.set_active(1).unwrap();
1050 let summaries = session.list_replays();
1051 assert!(!summaries[0].is_active);
1052 assert!(summaries[1].is_active);
1053
1054 assert!(session.set_active(10).is_err());
1056 }
1057
1058 #[test]
1062 fn test_replay_session_list() {
1063 let mut session = ReplaySession::new();
1064
1065 let t1 = sample_trace();
1066 let id1 = t1.trace_id;
1067 session.add_replay(t1);
1068
1069 let t2 = sample_trace();
1070 let id2 = t2.trace_id;
1071 session.add_replay(t2);
1072
1073 let list = session.list_replays();
1074 assert_eq!(list.len(), 2);
1075 assert_eq!(list[0].index, 0);
1076 assert_eq!(list[0].trace_id, id1);
1077 assert_eq!(list[0].goal, "test task");
1078 assert_eq!(list[0].event_count, 9);
1079 assert!(list[0].is_active);
1080
1081 assert_eq!(list[1].index, 1);
1082 assert_eq!(list[1].trace_id, id2);
1083 assert!(!list[1].is_active);
1084 }
1085
1086 #[test]
1090 fn test_describe_event_variants() {
1091 let desc = describe_event(&TraceEventKind::TaskStarted {
1093 task_id: Uuid::new_v4(),
1094 goal: "refactor auth".into(),
1095 });
1096 assert!(desc.contains("Task started"));
1097 assert!(desc.contains("refactor auth"));
1098
1099 let desc = describe_event(&TraceEventKind::TaskCompleted {
1101 task_id: Uuid::new_v4(),
1102 success: true,
1103 iterations: 5,
1104 });
1105 assert!(desc.contains("completed successfully"));
1106 assert!(desc.contains("5 iterations"));
1107
1108 let desc = describe_event(&TraceEventKind::TaskCompleted {
1110 task_id: Uuid::new_v4(),
1111 success: false,
1112 iterations: 3,
1113 });
1114 assert!(desc.contains("failed"));
1115 assert!(desc.contains("3 iterations"));
1116
1117 let desc = describe_event(&TraceEventKind::ToolRequested {
1119 tool: "file_read".into(),
1120 risk_level: RiskLevel::ReadOnly,
1121 args_summary: "".into(),
1122 });
1123 assert!(desc.contains("Tool requested"));
1124 assert!(desc.contains("file_read"));
1125 assert!(desc.contains("ReadOnly"));
1126
1127 let desc = describe_event(&TraceEventKind::ToolApproved {
1129 tool: "shell_exec".into(),
1130 });
1131 assert!(desc.contains("Tool approved"));
1132 assert!(desc.contains("shell_exec"));
1133
1134 let desc = describe_event(&TraceEventKind::ToolDenied {
1136 tool: "file_write".into(),
1137 reason: "not allowed".into(),
1138 });
1139 assert!(desc.contains("Tool denied"));
1140 assert!(desc.contains("file_write"));
1141 assert!(desc.contains("not allowed"));
1142
1143 let desc = describe_event(&TraceEventKind::ApprovalRequested {
1145 tool: "deploy".into(),
1146 context: "production".into(),
1147 });
1148 assert!(desc.contains("Approval requested for"));
1149 assert!(desc.contains("deploy"));
1150
1151 let desc = describe_event(&TraceEventKind::ApprovalDecision {
1153 tool: "deploy".into(),
1154 approved: true,
1155 });
1156 assert!(desc.contains("granted"));
1157 assert!(desc.contains("deploy"));
1158
1159 let desc = describe_event(&TraceEventKind::ApprovalDecision {
1161 tool: "deploy".into(),
1162 approved: false,
1163 });
1164 assert!(desc.contains("rejected"));
1165 assert!(desc.contains("deploy"));
1166
1167 let desc = describe_event(&TraceEventKind::ToolExecuted {
1169 tool: "grep".into(),
1170 success: true,
1171 duration_ms: 42,
1172 output_preview: "match found".into(),
1173 });
1174 assert!(desc.contains("Tool executed"));
1175 assert!(desc.contains("grep"));
1176 assert!(desc.contains("ok"));
1177 assert!(desc.contains("42ms"));
1178
1179 let desc = describe_event(&TraceEventKind::ToolExecuted {
1181 tool: "grep".into(),
1182 success: false,
1183 duration_ms: 100,
1184 output_preview: "".into(),
1185 });
1186 assert!(desc.contains("failed"));
1187 assert!(desc.contains("100ms"));
1188
1189 let desc = describe_event(&TraceEventKind::LlmCall {
1191 model: "gpt-4".into(),
1192 input_tokens: 1000,
1193 output_tokens: 500,
1194 cost: 0.05,
1195 });
1196 assert!(desc.contains("LLM call"));
1197 assert!(desc.contains("gpt-4"));
1198 assert!(desc.contains("1000/500 tokens"));
1199
1200 let desc = describe_event(&TraceEventKind::StatusChange {
1202 from: "idle".into(),
1203 to: "thinking".into(),
1204 });
1205 assert!(desc.contains("Status"));
1206 assert!(desc.contains("idle"));
1207 assert!(desc.contains("thinking"));
1208
1209 let desc = describe_event(&TraceEventKind::Error {
1211 message: "something failed".into(),
1212 });
1213 assert!(desc.contains("Error"));
1214 assert!(desc.contains("something failed"));
1215 }
1216
1217 #[test]
1221 fn test_replay_error_display() {
1222 let err = ReplayError::TraceNotFound(Uuid::nil());
1223 assert!(err.to_string().contains("trace not found"));
1224
1225 let err = ReplayError::OutOfBounds {
1226 position: 10,
1227 total: 5,
1228 };
1229 let msg = err.to_string();
1230 assert!(msg.contains("10"));
1231 assert!(msg.contains("5"));
1232
1233 let err = ReplayError::BookmarkNotFound(3);
1234 assert!(err.to_string().contains("3"));
1235
1236 let err = ReplayError::EmptyTrace;
1237 assert!(err.to_string().contains("empty trace"));
1238 }
1239
1240 #[test]
1244 fn test_replay_snapshot_progress() {
1245 let mut engine = ReplayEngine::new(sample_trace());
1246
1247 let snap = engine.snapshot();
1249 assert!((snap.progress_pct - 0.0).abs() < f64::EPSILON);
1250
1251 engine.seek(4).unwrap();
1253 let snap = engine.snapshot();
1254 assert!((snap.progress_pct - 50.0).abs() < f64::EPSILON);
1255
1256 engine.fast_forward();
1258 let snap = engine.snapshot();
1259 assert!((snap.progress_pct - 100.0).abs() < f64::EPSILON);
1260 }
1261
1262 #[test]
1266 fn test_replay_session_default() {
1267 let session = ReplaySession::default();
1268 assert!(session.is_empty());
1269 assert_eq!(session.len(), 0);
1270 assert!(session.active().is_none());
1271 }
1272
1273 #[test]
1278 fn test_replay_session_active_mut() {
1279 let mut session = ReplaySession::new();
1280 session.add_replay(sample_trace());
1281
1282 let engine = session.active_mut().unwrap();
1284 engine.step_forward();
1285 assert_eq!(engine.position(), 1);
1286
1287 assert_eq!(session.active().unwrap().position(), 1);
1289 }
1290
1291 #[test]
1292 fn test_replay_engine_empty_trace() {
1293 let mut trace = ExecutionTrace::new(Uuid::new_v4(), Uuid::new_v4(), "empty");
1295 trace.events.clear();
1296
1297 let mut engine = ReplayEngine::new(trace);
1298 assert_eq!(engine.position(), 0);
1299 assert_eq!(engine.total_events(), 0);
1300 assert!(engine.is_at_start());
1301 assert!(engine.is_at_end());
1302 assert!(engine.current_event().is_none());
1303 assert!(engine.step_forward().is_none());
1304 assert!(engine.step_backward().is_none());
1305 assert!(engine.skip_to_next_tool_event().is_none());
1306 assert_eq!(engine.describe_current(), "No events");
1307
1308 let snap = engine.snapshot();
1309 assert_eq!(snap.total_events, 0);
1310 assert!((snap.progress_pct - 0.0).abs() < f64::EPSILON);
1311 assert!(snap.current_event.is_none());
1312 assert!(snap.elapsed_from_start.is_none());
1313
1314 let timeline = engine.timeline();
1315 assert!(timeline.is_empty());
1316 }
1317
1318 #[test]
1319 fn test_replay_engine_walk_all_events() {
1320 let mut engine = ReplayEngine::new(sample_trace());
1321 let total = engine.total_events();
1322 let mut count = 1; while engine.step_forward().is_some() {
1325 count += 1;
1326 }
1327
1328 assert_eq!(count, total);
1329 assert!(engine.is_at_end());
1330
1331 count = 1;
1333 while engine.step_backward().is_some() {
1334 count += 1;
1335 }
1336
1337 assert_eq!(count, total);
1338 assert!(engine.is_at_start());
1339 }
1340
1341 #[test]
1342 fn test_replay_snapshot_serialization() {
1343 let engine = ReplayEngine::new(sample_trace());
1344 let snap = engine.snapshot();
1345
1346 let json = serde_json::to_string(&snap).unwrap();
1347 let restored: ReplaySnapshot = serde_json::from_str(&json).unwrap();
1348
1349 assert_eq!(restored.trace_id, snap.trace_id);
1350 assert_eq!(restored.position, snap.position);
1351 assert_eq!(restored.total_events, snap.total_events);
1352 assert_eq!(restored.errors_so_far, snap.errors_so_far);
1353 }
1354
1355 #[test]
1356 fn test_timeline_entry_serialization() {
1357 let engine = ReplayEngine::new(sample_trace());
1358 let timeline = engine.timeline();
1359
1360 let json = serde_json::to_string(&timeline).unwrap();
1361 let restored: Vec<TimelineEntry> = serde_json::from_str(&json).unwrap();
1362 assert_eq!(restored.len(), timeline.len());
1363 assert_eq!(restored[0].sequence, 0);
1364 }
1365
1366 #[test]
1367 fn test_bookmark_serialization() {
1368 let bookmark = Bookmark {
1369 position: 5,
1370 label: "important point".into(),
1371 created_at: Utc::now(),
1372 };
1373
1374 let json = serde_json::to_string(&bookmark).unwrap();
1375 let restored: Bookmark = serde_json::from_str(&json).unwrap();
1376 assert_eq!(restored.position, 5);
1377 assert_eq!(restored.label, "important point");
1378 }
1379
1380 #[test]
1381 fn test_replay_summary_serialization() {
1382 let summary = ReplaySummary {
1383 index: 0,
1384 trace_id: Uuid::new_v4(),
1385 goal: "test goal".into(),
1386 event_count: 42,
1387 is_active: true,
1388 };
1389
1390 let json = serde_json::to_string(&summary).unwrap();
1391 let restored: ReplaySummary = serde_json::from_str(&json).unwrap();
1392 assert_eq!(restored.index, 0);
1393 assert_eq!(restored.goal, "test goal");
1394 assert_eq!(restored.event_count, 42);
1395 assert!(restored.is_active);
1396 }
1397}