1use serde::{Deserialize, Serialize};
17
18#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
26pub enum ChronoshiftError {
27 CheckpointNotFound { timeline: String, tick: u64 },
29 DigestChainBroken {
31 tick: u64,
32 seq: u64,
33 expected: String,
34 actual: String,
35 },
36 DeterminismViolation {
39 tick: u64,
40 source_hash: String,
41 replay_hash: String,
42 },
43 ForkLimitExceeded { max: u32, current: u32 },
45 ReplayOverflow { max: usize, attempted: usize },
47 TickOrderViolation { prev_tick: u64, curr_tick: u64 },
49 ReplayRangeInvalid { from: u64, to: u64 },
51 TimelineNotFound { timeline: String },
53 TimelineExpired { timeline: String, expired_at: u64 },
55 ComputeCapExceeded { cap_pct: u32 },
57}
58
59impl core::fmt::Display for ChronoshiftError {
60 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
61 match self {
62 Self::CheckpointNotFound { timeline, tick } => {
63 write!(f, "checkpoint_not_found: timeline={timeline}, tick={tick}")
64 }
65 Self::DigestChainBroken {
66 tick,
67 seq,
68 expected,
69 actual,
70 } => {
71 write!(
72 f,
73 "digest_chain_broken: tick={tick}, seq={seq}, expected={expected}, actual={actual}"
74 )
75 }
76 Self::DeterminismViolation {
77 tick,
78 source_hash,
79 replay_hash,
80 } => {
81 write!(
82 f,
83 "determinism_violation: tick={tick}, source={source_hash}, replay={replay_hash}"
84 )
85 }
86 Self::ForkLimitExceeded { max, current } => {
87 write!(f, "fork_limit_exceeded: max={max}, current={current}")
88 }
89 Self::ReplayOverflow { max, attempted } => {
90 write!(f, "replay_overflow: max={max}, attempted={attempted}")
91 }
92 Self::TickOrderViolation { prev_tick, curr_tick } => {
93 write!(f, "tick_order_violation: prev_tick={prev_tick}, curr_tick={curr_tick}")
94 }
95 Self::ReplayRangeInvalid { from, to } => {
96 write!(f, "replay_range_invalid: from={from}, to={to}")
97 }
98 Self::TimelineNotFound { timeline } => {
99 write!(f, "timeline_not_found: {timeline}")
100 }
101 Self::TimelineExpired { timeline, expired_at } => {
102 write!(f, "timeline_expired: {timeline}, expired_at={expired_at}")
103 }
104 Self::ComputeCapExceeded { cap_pct } => {
105 write!(f, "compute_cap_exceeded: cap_pct={cap_pct}")
106 }
107 }
108 }
109}
110
111#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
118pub struct CanonEventSnapshot {
119 pub event_id: String,
120 pub timeline_id: String,
121 pub event_type: String,
122 pub scope_key: String,
123 pub actor_id: String,
124 pub target_id: String,
125 pub payload: String,
126 pub tick: u64,
127 pub seq: u64,
128 pub digest: String,
129 pub prev_event_id: String,
130}
131
132#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
134pub struct StateSnapshot {
135 pub timeline_id: String,
136 pub tick: u64,
137 pub entity_count: u64,
138 pub event_count: u64,
139 pub scope_chain_heads: Vec<(String, String)>,
141 pub state_hash: String,
142}
143
144#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
146pub struct CheckpointSnapshot {
147 pub timeline_id: String,
148 pub tick: u64,
149 pub event_hash: String,
150 pub state_hash: String,
151}
152
153#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
159pub enum ForkVisibilityLevel {
160 Private,
161 Shared,
162 Public,
163}
164
165#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
167pub struct ForkSpec {
168 pub parent_timeline: String,
169 pub fork_tick: u64,
170 pub name: String,
171 pub visibility: ForkVisibilityLevel,
172 pub compute_cap_pct: u32,
174 pub ttl_ticks: u64,
176}
177
178pub struct CheckpointBuilder {
184 timeline_id: String,
185 tick: u64,
186}
187
188impl CheckpointBuilder {
189 pub fn new(timeline_id: &str, tick: u64) -> Self {
191 Self {
192 timeline_id: timeline_id.to_string(),
193 tick,
194 }
195 }
196
197 pub fn compute_event_hash(events: &[CanonEventSnapshot]) -> String {
203 let mut hasher = blake3::Hasher::new_derive_key("dreamwell.checkpoint.events.v1");
204 for event in events {
205 hasher.update(event.digest.as_bytes());
206 hasher.update(b"|");
207 }
208 hasher.finalize().to_hex().to_string()
209 }
210
211 pub fn compute_state_hash(state: &StateSnapshot) -> Result<String, String> {
213 let serialized = serde_json::to_string(state).map_err(|e| format!("state_hash_serialization_failed:{}", e))?;
214 Ok(blake3::hash(serialized.as_bytes()).to_hex().to_string())
215 }
216
217 pub fn build(&self, events: &[CanonEventSnapshot], state: &StateSnapshot) -> Result<CheckpointSnapshot, String> {
219 let event_hash = Self::compute_event_hash(events);
220 let state_hash = Self::compute_state_hash(state)?;
221 Ok(CheckpointSnapshot {
222 timeline_id: self.timeline_id.clone(),
223 tick: self.tick,
224 event_hash,
225 state_hash,
226 })
227 }
228}
229
230pub const MAX_REPLAY_EVENTS: usize = 1_000_000;
241
242pub struct ReplayEngine {
243 source_timeline: String,
244 target_timeline: String,
245 from_tick: u64,
246 to_tick: u64,
247 events: Vec<CanonEventSnapshot>,
248}
249
250impl ReplayEngine {
251 pub fn new(source: &str, target: &str, from_tick: u64, to_tick: u64) -> Self {
253 Self {
254 source_timeline: source.to_string(),
255 target_timeline: target.to_string(),
256 from_tick,
257 to_tick,
258 events: Vec::new(),
259 }
260 }
261
262 pub fn source_timeline(&self) -> &str {
264 &self.source_timeline
265 }
266
267 pub fn target_timeline(&self) -> &str {
269 &self.target_timeline
270 }
271
272 pub fn from_tick(&self) -> u64 {
274 self.from_tick
275 }
276
277 pub fn to_tick(&self) -> u64 {
279 self.to_tick
280 }
281
282 pub fn events(&self) -> &[CanonEventSnapshot] {
284 &self.events
285 }
286
287 pub fn add_event(&mut self, event: CanonEventSnapshot) -> Result<(), ChronoshiftError> {
291 if self.events.len() >= MAX_REPLAY_EVENTS {
292 return Err(ChronoshiftError::ReplayOverflow {
293 max: MAX_REPLAY_EVENTS,
294 attempted: self.events.len() + 1,
295 });
296 }
297 self.events.push(event);
298 Ok(())
299 }
300
301 pub fn validate_chain(&self) -> Result<(), ChronoshiftError> {
310 if self.events.is_empty() {
311 return Ok(());
312 }
313
314 for i in 1..self.events.len() {
315 let prev = &self.events[i - 1];
316 let curr = &self.events[i];
317
318 if curr.tick < prev.tick {
320 return Err(ChronoshiftError::TickOrderViolation {
321 prev_tick: prev.tick,
322 curr_tick: curr.tick,
323 });
324 }
325
326 if curr.prev_event_id != prev.event_id {
328 return Err(ChronoshiftError::DigestChainBroken {
329 tick: curr.tick,
330 seq: curr.seq,
331 expected: prev.event_id.clone(),
332 actual: curr.prev_event_id.clone(),
333 });
334 }
335
336 let recomputed = recompute_digest(curr);
338 if recomputed != curr.digest {
339 return Err(ChronoshiftError::DigestChainBroken {
340 tick: curr.tick,
341 seq: curr.seq,
342 expected: recomputed,
343 actual: curr.digest.clone(),
344 });
345 }
346 }
347
348 Ok(())
349 }
350
351 pub fn replay_tick(&self, tick: u64) -> Vec<CanonEventSnapshot> {
353 let mut tick_events: Vec<CanonEventSnapshot> = self.events.iter().filter(|e| e.tick == tick).cloned().collect();
354 tick_events.sort_by_key(|e| e.seq);
355 tick_events
356 }
357
358 pub fn replay_range(&self, from: u64, to: u64) -> Vec<CanonEventSnapshot> {
360 if from > to {
361 return Vec::new(); }
363 let mut range_events: Vec<CanonEventSnapshot> = self
364 .events
365 .iter()
366 .filter(|e| e.tick >= from && e.tick <= to)
367 .cloned()
368 .collect();
369 range_events.sort_by(|a, b| a.tick.cmp(&b.tick).then(a.seq.cmp(&b.seq)));
370 range_events
371 }
372
373 pub fn compute_determinism_hash(&self) -> String {
377 let mut sorted = self.events.clone();
378 sorted.sort_by(|a, b| a.tick.cmp(&b.tick).then(a.seq.cmp(&b.seq)));
379
380 let mut hasher = blake3::Hasher::new();
381 for event in &sorted {
382 hasher.update(event.event_id.as_bytes());
383 hasher.update(event.event_type.as_bytes());
384 hasher.update(&event.tick.to_le_bytes());
385 hasher.update(&event.seq.to_le_bytes());
386 hasher.update(event.digest.as_bytes());
387 }
388 hasher.finalize().to_hex().to_string()
389 }
390}
391
392#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
398pub struct EventMismatch {
399 pub tick: u64,
400 pub seq: u64,
401 pub source_digest: String,
402 pub target_digest: String,
403 pub source_event_type: String,
404 pub target_event_type: String,
405}
406
407#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
409pub struct TimelineDiff {
410 pub source: String,
411 pub target: String,
412 pub from_tick: u64,
413 pub to_tick: u64,
414 pub source_event_count: u64,
415 pub target_event_count: u64,
416 pub divergence_tick: Option<u64>,
418 pub mismatched_events: Vec<EventMismatch>,
420 pub determinism_verified: bool,
422}
423
424#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
434pub struct ProofBundle {
435 pub proof_id: String,
436 pub timeline_id: String,
437 pub from_tick: u64,
438 pub to_tick: u64,
439 pub event_count: u64,
440 pub root_event_id: String,
441 pub final_event_id: String,
442 pub root_digest: String,
443 pub final_digest: String,
444 pub state_hash_start: String,
445 pub state_hash_end: String,
446 pub merkle_root: String,
447 pub created_at_tick: u64,
448}
449
450impl ProofBundle {
451 pub fn compute_merkle_root(events: &[CanonEventSnapshot]) -> String {
453 let leaves: Vec<String> = events.iter().map(|e| e.digest.clone()).collect();
454 compute_merkle_tree(&leaves)
455 }
456
457 pub fn verify_chain(events: &[CanonEventSnapshot]) -> Result<(), ChronoshiftError> {
461 if events.is_empty() {
462 return Ok(());
463 }
464
465 for i in 1..events.len() {
466 let prev = &events[i - 1];
467 let curr = &events[i];
468
469 if curr.prev_event_id != prev.event_id {
470 return Err(ChronoshiftError::DigestChainBroken {
471 tick: curr.tick,
472 seq: curr.seq,
473 expected: prev.event_id.clone(),
474 actual: curr.prev_event_id.clone(),
475 });
476 }
477
478 let recomputed = recompute_digest(curr);
479 if recomputed != curr.digest {
480 return Err(ChronoshiftError::DigestChainBroken {
481 tick: curr.tick,
482 seq: curr.seq,
483 expected: recomputed,
484 actual: curr.digest.clone(),
485 });
486 }
487 }
488
489 Ok(())
490 }
491}
492
493pub fn compute_merkle_tree(leaves: &[String]) -> String {
506 if leaves.is_empty() {
507 return blake3::hash(b"").to_hex().to_string();
508 }
509
510 let mut layer: Vec<blake3::Hash> = leaves.iter().map(|leaf| blake3::hash(leaf.as_bytes())).collect();
512
513 while layer.len() > 1 {
514 let mut next_layer = Vec::with_capacity(layer.len().div_ceil(2));
515
516 let mut i = 0;
517 while i + 1 < layer.len() {
518 let mut hasher = blake3::Hasher::new();
519 hasher.update(layer[i].as_bytes());
520 hasher.update(layer[i + 1].as_bytes());
521 next_layer.push(hasher.finalize());
522 i += 2;
523 }
524
525 if i < layer.len() {
527 let mut hasher = blake3::Hasher::new();
528 hasher.update(layer[i].as_bytes());
529 hasher.update(layer[i].as_bytes());
530 next_layer.push(hasher.finalize());
531 }
532
533 layer = next_layer;
534 }
535
536 layer[0].to_hex().to_string()
537}
538
539fn recompute_digest(event: &CanonEventSnapshot) -> String {
548 crate::canon::compute_event_digest(
549 &event.event_id,
550 &event.event_type,
551 event.tick,
552 event.seq,
553 &event.timeline_id,
554 &event.scope_key,
555 &event.actor_id,
556 &event.prev_event_id,
557 )
558}
559
560pub fn diff_timelines(
567 source: &[CanonEventSnapshot],
568 target: &[CanonEventSnapshot],
569 from_tick: u64,
570 to_tick: u64,
571) -> TimelineDiff {
572 let source_filtered: Vec<&CanonEventSnapshot> = source
573 .iter()
574 .filter(|e| e.tick >= from_tick && e.tick <= to_tick)
575 .collect();
576 let target_filtered: Vec<&CanonEventSnapshot> = target
577 .iter()
578 .filter(|e| e.tick >= from_tick && e.tick <= to_tick)
579 .collect();
580
581 let source_timeline = source_filtered
582 .first()
583 .map(|e| e.timeline_id.clone())
584 .unwrap_or_default();
585 let target_timeline = target_filtered
586 .first()
587 .map(|e| e.timeline_id.clone())
588 .unwrap_or_default();
589
590 let mut target_map: std::collections::HashMap<(u64, u64), &CanonEventSnapshot> = std::collections::HashMap::new();
592 for event in &target_filtered {
593 target_map.insert((event.tick, event.seq), event);
594 }
595
596 let mut mismatched_events = Vec::new();
597 let mut divergence_tick: Option<u64> = None;
598
599 for src_event in &source_filtered {
600 let key = (src_event.tick, src_event.seq);
601 if let Some(tgt_event) = target_map.get(&key) {
602 if src_event.digest != tgt_event.digest {
603 mismatched_events.push(EventMismatch {
604 tick: src_event.tick,
605 seq: src_event.seq,
606 source_digest: src_event.digest.clone(),
607 target_digest: tgt_event.digest.clone(),
608 source_event_type: src_event.event_type.clone(),
609 target_event_type: tgt_event.event_type.clone(),
610 });
611 if divergence_tick.is_none() || src_event.tick < divergence_tick.unwrap() {
612 divergence_tick = Some(src_event.tick);
613 }
614 }
615 } else {
616 mismatched_events.push(EventMismatch {
618 tick: src_event.tick,
619 seq: src_event.seq,
620 source_digest: src_event.digest.clone(),
621 target_digest: String::new(),
622 source_event_type: src_event.event_type.clone(),
623 target_event_type: String::new(),
624 });
625 if divergence_tick.is_none() || src_event.tick < divergence_tick.unwrap() {
626 divergence_tick = Some(src_event.tick);
627 }
628 }
629 }
630
631 let mut source_map: std::collections::HashMap<(u64, u64), &CanonEventSnapshot> = std::collections::HashMap::new();
633 for event in &source_filtered {
634 source_map.insert((event.tick, event.seq), event);
635 }
636 for tgt_event in &target_filtered {
637 let key = (tgt_event.tick, tgt_event.seq);
638 if !source_map.contains_key(&key) {
639 mismatched_events.push(EventMismatch {
640 tick: tgt_event.tick,
641 seq: tgt_event.seq,
642 source_digest: String::new(),
643 target_digest: tgt_event.digest.clone(),
644 source_event_type: String::new(),
645 target_event_type: tgt_event.event_type.clone(),
646 });
647 if divergence_tick.is_none() || tgt_event.tick < divergence_tick.unwrap() {
648 divergence_tick = Some(tgt_event.tick);
649 }
650 }
651 }
652
653 mismatched_events.sort_by(|a, b| a.tick.cmp(&b.tick).then(a.seq.cmp(&b.seq)));
655
656 let determinism_verified = mismatched_events.is_empty();
657
658 TimelineDiff {
659 source: source_timeline,
660 target: target_timeline,
661 from_tick,
662 to_tick,
663 source_event_count: source_filtered.len() as u64,
664 target_event_count: target_filtered.len() as u64,
665 divergence_tick,
666 mismatched_events,
667 determinism_verified,
668 }
669}
670
671pub fn verify_determinism(original: &[CanonEventSnapshot], replay: &[CanonEventSnapshot]) -> bool {
676 if original.len() != replay.len() {
677 return false;
678 }
679 original.iter().zip(replay.iter()).all(|(o, r)| o.digest == r.digest)
680}
681
682pub fn compute_tick_hash(events: &[CanonEventSnapshot], tick: u64) -> String {
687 let mut tick_events: Vec<&CanonEventSnapshot> = events.iter().filter(|e| e.tick == tick).collect();
688 tick_events.sort_by_key(|e| e.seq);
689
690 let mut hasher = blake3::Hasher::new_derive_key("dreamwell.chronoshift.tick.v1");
691 for event in &tick_events {
692 hasher.update(event.digest.as_bytes());
693 hasher.update(b"|");
694 }
695
696 hasher.finalize().to_hex().to_string()
697}
698
699#[cfg(test)]
704mod tests {
705 use super::*;
706
707 fn make_event(
708 id: &str,
709 timeline: &str,
710 event_type: &str,
711 tick: u64,
712 seq: u64,
713 prev_id: &str,
714 ) -> CanonEventSnapshot {
715 let scope_key = format!("tl:{}/lvl:world/id:test", timeline);
716 let digest =
717 crate::canon::compute_event_digest(id, event_type, tick, seq, timeline, &scope_key, "actor_1", prev_id);
718 CanonEventSnapshot {
719 event_id: id.to_string(),
720 timeline_id: timeline.to_string(),
721 event_type: event_type.to_string(),
722 scope_key,
723 actor_id: "actor_1".to_string(),
724 target_id: "target_1".to_string(),
725 payload: "{}".to_string(),
726 tick,
727 seq,
728 digest,
729 prev_event_id: prev_id.to_string(),
730 }
731 }
732
733 fn make_chain(timeline: &str, count: usize) -> Vec<CanonEventSnapshot> {
734 let mut events = Vec::with_capacity(count);
735 let mut prev_id = String::new();
736 for i in 0..count {
737 let id = format!("evt:{timeline}:{}:{:06}", i / 3, i % 3);
738 let tick = (i / 3) as u64;
739 let seq = (i % 3) as u64;
740 let event = make_event(&id, timeline, "test_event", tick, seq, &prev_id);
741 prev_id = id;
742 events.push(event);
743 }
744 events
745 }
746
747 #[test]
748 fn test_checkpoint_builder_event_hash_deterministic() {
749 let events = make_chain("tl_sacred", 5);
750 let hash_a = CheckpointBuilder::compute_event_hash(&events);
751 let hash_b = CheckpointBuilder::compute_event_hash(&events);
752 assert_eq!(hash_a, hash_b);
753 assert_eq!(hash_a.len(), 64);
754 }
755
756 #[test]
757 fn test_checkpoint_builder_state_hash_uses_blake3() {
758 let state = StateSnapshot {
759 timeline_id: "tl_sacred".to_string(),
760 tick: 10,
761 entity_count: 100,
762 event_count: 50,
763 scope_chain_heads: vec![("scope_a".to_string(), "abc123".to_string())],
764 state_hash: String::new(),
765 };
766 let hash = CheckpointBuilder::compute_state_hash(&state).unwrap();
767 assert_eq!(hash.len(), 64);
769 }
770
771 #[test]
772 fn test_checkpoint_builder_build() {
773 let events = make_chain("tl_sacred", 3);
774 let state = StateSnapshot {
775 timeline_id: "tl_sacred".to_string(),
776 tick: 1,
777 entity_count: 10,
778 event_count: 3,
779 scope_chain_heads: vec![],
780 state_hash: String::new(),
781 };
782 let builder = CheckpointBuilder::new("tl_sacred", 1);
783 let cp = builder.build(&events, &state).unwrap();
784 assert_eq!(cp.timeline_id, "tl_sacred");
785 assert_eq!(cp.tick, 1);
786 assert!(!cp.event_hash.is_empty());
787 assert!(!cp.state_hash.is_empty());
788 }
789
790 #[test]
791 fn test_replay_engine_validate_chain_ok() {
792 let events = make_chain("tl_a", 6);
793 let mut engine = ReplayEngine::new("tl_a", "tl_b", 0, 2);
794 for e in events {
795 engine.add_event(e).unwrap();
796 }
797 assert!(engine.validate_chain().is_ok());
798 }
799
800 #[test]
801 fn test_replay_engine_validate_chain_broken() {
802 let mut events = make_chain("tl_a", 4);
803 events[2].prev_event_id = "wrong_id".to_string();
805 let mut engine = ReplayEngine::new("tl_a", "tl_b", 0, 1);
806 for e in events {
807 engine.add_event(e).unwrap();
808 }
809 let result = engine.validate_chain();
810 assert!(result.is_err());
811 match result.unwrap_err() {
812 ChronoshiftError::DigestChainBroken { tick, .. } => {
813 assert_eq!(tick, 0);
814 }
815 other => panic!("unexpected error: {other:?}"),
816 }
817 }
818
819 #[test]
820 fn test_replay_engine_replay_tick() {
821 let events = make_chain("tl_a", 9);
822 let mut engine = ReplayEngine::new("tl_a", "tl_b", 0, 3);
823 for e in events {
824 engine.add_event(e).unwrap();
825 }
826 let tick_1 = engine.replay_tick(1);
827 assert_eq!(tick_1.len(), 3);
828 for e in &tick_1 {
829 assert_eq!(e.tick, 1);
830 }
831 assert!(tick_1[0].seq <= tick_1[1].seq);
833 assert!(tick_1[1].seq <= tick_1[2].seq);
834 }
835
836 #[test]
837 fn test_replay_engine_replay_range() {
838 let events = make_chain("tl_a", 9);
839 let mut engine = ReplayEngine::new("tl_a", "tl_b", 0, 3);
840 for e in events {
841 engine.add_event(e).unwrap();
842 }
843 let range = engine.replay_range(0, 1);
844 assert_eq!(range.len(), 6);
845 for e in &range {
846 assert!(e.tick <= 1);
847 }
848 }
849
850 #[test]
851 fn test_replay_engine_determinism_hash() {
852 let events = make_chain("tl_a", 5);
853 let mut engine = ReplayEngine::new("tl_a", "tl_b", 0, 2);
854 for e in events.clone() {
855 engine.add_event(e).unwrap();
856 }
857 let hash_a = engine.compute_determinism_hash();
858 let hash_b = engine.compute_determinism_hash();
859 assert_eq!(hash_a, hash_b);
860 assert_eq!(hash_a.len(), 64); }
862
863 #[test]
864 fn test_replay_engine_accessors() {
865 let engine = ReplayEngine::new("src", "tgt", 10, 20);
866 assert_eq!(engine.source_timeline(), "src");
867 assert_eq!(engine.target_timeline(), "tgt");
868 assert_eq!(engine.from_tick(), 10);
869 assert_eq!(engine.to_tick(), 20);
870 assert!(engine.events().is_empty());
871 }
872
873 #[test]
874 fn test_diff_timelines_identical() {
875 let events = make_chain("tl_a", 6);
876 let diff = diff_timelines(&events, &events, 0, 2);
877 assert!(diff.determinism_verified);
878 assert!(diff.divergence_tick.is_none());
879 assert!(diff.mismatched_events.is_empty());
880 assert_eq!(diff.source_event_count, 6);
881 assert_eq!(diff.target_event_count, 6);
882 }
883
884 #[test]
885 fn test_diff_timelines_divergent() {
886 let source = make_chain("tl_a", 6);
887 let mut target = make_chain("tl_a", 6);
888 target[3].digest = "0000000000000000".to_string();
890 let diff = diff_timelines(&source, &target, 0, 2);
891 assert!(!diff.determinism_verified);
892 assert_eq!(diff.divergence_tick, Some(1));
893 assert!(!diff.mismatched_events.is_empty());
894 }
895
896 #[test]
897 fn test_diff_timelines_different_lengths() {
898 let source = make_chain("tl_a", 6);
899 let target = make_chain("tl_b", 3);
900 let diff = diff_timelines(&source, &target, 0, 2);
901 assert!(!diff.determinism_verified);
902 assert_eq!(diff.source_event_count, 6);
903 assert_eq!(diff.target_event_count, 3);
904 }
905
906 #[test]
907 fn test_verify_determinism_true() {
908 let events = make_chain("tl_a", 5);
909 assert!(verify_determinism(&events, &events));
910 }
911
912 #[test]
913 fn test_verify_determinism_false_different_digests() {
914 let original = make_chain("tl_a", 3);
915 let mut replay = original.clone();
916 replay[1].digest = "ffffffffffffffff".to_string();
917 assert!(!verify_determinism(&original, &replay));
918 }
919
920 #[test]
921 fn test_verify_determinism_false_different_lengths() {
922 let original = make_chain("tl_a", 5);
923 let replay = make_chain("tl_a", 3);
924 assert!(!verify_determinism(&original, &replay));
925 }
926
927 #[test]
928 fn test_compute_tick_hash_deterministic() {
929 let events = make_chain("tl_a", 9);
930 let hash_a = compute_tick_hash(&events, 1);
931 let hash_b = compute_tick_hash(&events, 1);
932 assert_eq!(hash_a, hash_b);
933 assert_eq!(hash_a.len(), 64);
934 }
935
936 #[test]
937 fn test_compute_tick_hash_different_ticks() {
938 let events = make_chain("tl_a", 9);
939 let hash_0 = compute_tick_hash(&events, 0);
940 let hash_1 = compute_tick_hash(&events, 1);
941 assert_ne!(hash_0, hash_1);
942 }
943
944 #[test]
945 fn test_compute_tick_hash_empty_tick() {
946 let events = make_chain("tl_a", 3);
947 let hash = compute_tick_hash(&events, 999);
948 assert_eq!(hash.len(), 64);
950 }
951
952 #[test]
953 fn test_merkle_tree_single_leaf() {
954 let leaves = vec!["abc123".to_string()];
955 let root = compute_merkle_tree(&leaves);
956 let expected = blake3::hash(b"abc123").to_hex().to_string();
957 assert_eq!(root, expected);
958 }
959
960 #[test]
961 fn test_merkle_tree_two_leaves() {
962 let leaves = vec!["aaa".to_string(), "bbb".to_string()];
963 let root = compute_merkle_tree(&leaves);
964
965 let left = blake3::hash(b"aaa");
966 let right = blake3::hash(b"bbb");
967 let mut hasher = blake3::Hasher::new();
968 hasher.update(left.as_bytes());
969 hasher.update(right.as_bytes());
970 let expected = hasher.finalize().to_hex().to_string();
971 assert_eq!(root, expected);
972 }
973
974 #[test]
975 fn test_merkle_tree_odd_leaves() {
976 let leaves = vec!["a".to_string(), "b".to_string(), "c".to_string()];
977 let root = compute_merkle_tree(&leaves);
978 assert_eq!(root.len(), 64);
979 }
980
981 #[test]
982 fn test_merkle_tree_empty() {
983 let root = compute_merkle_tree(&[]);
984 let expected = blake3::hash(b"").to_hex().to_string();
985 assert_eq!(root, expected);
986 }
987
988 #[test]
989 fn test_merkle_tree_deterministic() {
990 let leaves: Vec<String> = (0..8).map(|i| format!("leaf_{i}")).collect();
991 let root_a = compute_merkle_tree(&leaves);
992 let root_b = compute_merkle_tree(&leaves);
993 assert_eq!(root_a, root_b);
994 }
995
996 #[test]
997 fn test_proof_bundle_compute_merkle_root() {
998 let events = make_chain("tl_a", 4);
999 let root = ProofBundle::compute_merkle_root(&events);
1000 assert_eq!(root.len(), 64);
1001 }
1002
1003 #[test]
1004 fn test_proof_bundle_verify_chain_ok() {
1005 let events = make_chain("tl_a", 5);
1006 assert!(ProofBundle::verify_chain(&events).is_ok());
1007 }
1008
1009 #[test]
1010 fn test_proof_bundle_verify_chain_broken() {
1011 let mut events = make_chain("tl_a", 4);
1012 events[1].digest = "0000000000000000".to_string();
1013 assert!(ProofBundle::verify_chain(&events).is_err());
1014 }
1015
1016 #[test]
1017 fn test_fork_spec_roundtrip() {
1018 let spec = ForkSpec {
1019 parent_timeline: "tl_sacred".to_string(),
1020 fork_tick: 100,
1021 name: "what_if_war".to_string(),
1022 visibility: ForkVisibilityLevel::Shared,
1023 compute_cap_pct: 25,
1024 ttl_ticks: 500,
1025 };
1026 let json = serde_json::to_string(&spec).unwrap();
1027 let deserialized: ForkSpec = serde_json::from_str(&json).unwrap();
1028 assert_eq!(spec, deserialized);
1029 }
1030
1031 #[test]
1032 fn test_chronoshift_error_display() {
1033 let err = ChronoshiftError::CheckpointNotFound {
1034 timeline: "tl_a".to_string(),
1035 tick: 42,
1036 };
1037 let msg = format!("{err}");
1038 assert!(msg.contains("checkpoint_not_found"));
1039 assert!(msg.contains("tl_a"));
1040 assert!(msg.contains("42"));
1041 }
1042
1043 #[test]
1044 fn test_state_snapshot_serialization() {
1045 let state = StateSnapshot {
1046 timeline_id: "tl_sacred".to_string(),
1047 tick: 50,
1048 entity_count: 1000,
1049 event_count: 500,
1050 scope_chain_heads: vec![
1051 ("world:ayora".to_string(), "abc123".to_string()),
1052 ("realm:verdant".to_string(), "def456".to_string()),
1053 ],
1054 state_hash: "deadbeef".to_string(),
1055 };
1056 let json = serde_json::to_string(&state).unwrap();
1057 let deserialized: StateSnapshot = serde_json::from_str(&json).unwrap();
1058 assert_eq!(state, deserialized);
1059 }
1060
1061 #[test]
1062 fn test_replay_engine_empty_chain_validates() {
1063 let engine = ReplayEngine::new("a", "b", 0, 0);
1064 assert!(engine.validate_chain().is_ok());
1065 }
1066
1067 #[test]
1068 fn test_replay_engine_single_event_validates() {
1069 let events = make_chain("tl_a", 1);
1070 let mut engine = ReplayEngine::new("tl_a", "tl_b", 0, 0);
1071 engine.add_event(events[0].clone()).unwrap();
1072 assert!(engine.validate_chain().is_ok());
1073 }
1074
1075 #[test]
1076 fn test_replay_engine_overflow() {
1077 let mut engine = ReplayEngine::new("a", "b", 0, 0);
1078 engine.events = Vec::with_capacity(0);
1081 engine
1082 .events
1083 .resize(MAX_REPLAY_EVENTS, make_event("e", "t", "x", 0, 0, ""));
1084 let result = engine.add_event(make_event("overflow", "t", "x", 1, 0, "e"));
1085 assert!(result.is_err());
1086 match result.unwrap_err() {
1087 ChronoshiftError::ReplayOverflow { max, .. } => {
1088 assert_eq!(max, MAX_REPLAY_EVENTS);
1089 }
1090 other => panic!("unexpected error: {other:?}"),
1091 }
1092 }
1093
1094 #[test]
1095 fn test_validate_chain_tick_order_violation() {
1096 let e0 = make_event("evt:tl_a:10:000000", "tl_a", "test_event", 10, 0, "");
1098 let e1 = make_event("evt:tl_a:5:000000", "tl_a", "test_event", 5, 0, "evt:tl_a:10:000000");
1099 let mut engine = ReplayEngine::new("tl_a", "tl_b", 0, 20);
1100 engine.add_event(e0).unwrap();
1101 engine.add_event(e1).unwrap();
1102 let result = engine.validate_chain();
1103 assert!(result.is_err());
1104 match result.unwrap_err() {
1105 ChronoshiftError::TickOrderViolation { prev_tick, curr_tick } => {
1106 assert_eq!(prev_tick, 10);
1107 assert_eq!(curr_tick, 5);
1108 }
1109 other => panic!("unexpected error: {other:?}"),
1110 }
1111 }
1112}