1use std::collections::{HashMap, HashSet, VecDeque};
11
12#[derive(Debug, Clone, PartialEq)]
18pub enum TracerError {
19 NodeNotFound(String),
21 CycleDetected {
23 path: Vec<String>,
25 },
26 QueryTooExpensive(usize),
28 InvalidStrength(f64),
30 MaxDepthExceeded,
32}
33
34impl std::fmt::Display for TracerError {
35 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36 match self {
37 Self::NodeNotFound(id) => write!(f, "node not found: {id}"),
38 Self::CycleDetected { path } => write!(f, "cycle detected: {}", path.join(" -> ")),
39 Self::QueryTooExpensive(n) => write!(f, "query too expensive: {n} nodes"),
40 Self::InvalidStrength(s) => write!(f, "invalid strength {s}: must be in [0.0, 1.0]"),
41 Self::MaxDepthExceeded => write!(f, "max depth exceeded"),
42 }
43 }
44}
45
46impl std::error::Error for TracerError {}
47
48#[derive(Debug, Clone, PartialEq, Eq, Hash)]
54pub enum CausalRelation {
55 DirectCause,
57 IndirectCause,
59 Enables,
61 Inhibits,
63 Correlates,
65 Precedes,
67}
68
69#[derive(Debug, Clone)]
75pub struct CausalNode {
76 pub id: String,
78 pub event_type: String,
80 pub timestamp: u64,
82 pub attributes: Vec<(String, String)>,
84 pub confidence: f64,
86}
87
88impl CausalNode {
89 pub fn new(
91 id: impl Into<String>,
92 event_type: impl Into<String>,
93 timestamp: u64,
94 confidence: f64,
95 ) -> Self {
96 Self {
97 id: id.into(),
98 event_type: event_type.into(),
99 timestamp,
100 attributes: Vec::new(),
101 confidence,
102 }
103 }
104
105 pub fn with_attribute(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
107 self.attributes.push((key.into(), value.into()));
108 self
109 }
110}
111
112#[derive(Debug, Clone)]
114pub struct CausalEdge {
115 pub from_id: String,
117 pub to_id: String,
119 pub relation: CausalRelation,
121 pub strength: f64,
123 pub delay_us: u64,
125}
126
127impl CausalEdge {
128 pub fn new(
130 from_id: impl Into<String>,
131 to_id: impl Into<String>,
132 relation: CausalRelation,
133 strength: f64,
134 delay_us: u64,
135 ) -> Self {
136 Self {
137 from_id: from_id.into(),
138 to_id: to_id.into(),
139 relation,
140 strength,
141 delay_us,
142 }
143 }
144}
145
146#[derive(Debug, Clone)]
152pub struct CausalChain {
153 pub nodes: Vec<CausalNode>,
155 pub edges: Vec<CausalEdge>,
157 pub root_id: String,
159 pub leaf_ids: Vec<String>,
161 pub chain_confidence: f64,
163 pub depth: usize,
165}
166
167#[derive(Debug, Clone)]
173pub struct TraceQuery {
174 pub root_event_id: Option<String>,
176 pub event_types: Vec<String>,
178 pub max_depth: usize,
180 pub min_strength: f64,
182 pub time_window_us: Option<(u64, u64)>,
184 pub include_relations: Vec<CausalRelation>,
186}
187
188impl Default for TraceQuery {
189 fn default() -> Self {
190 Self {
191 root_event_id: None,
192 event_types: Vec::new(),
193 max_depth: 16,
194 min_strength: 0.0,
195 time_window_us: None,
196 include_relations: Vec::new(),
197 }
198 }
199}
200
201#[derive(Debug, Clone)]
207pub struct TracerConfig {
208 pub max_chain_depth: usize,
210 pub min_edge_strength: f64,
212 pub max_nodes: usize,
214 pub enable_cycle_detection: bool,
216 pub confidence_threshold: f64,
218}
219
220impl Default for TracerConfig {
221 fn default() -> Self {
222 Self {
223 max_chain_depth: 32,
224 min_edge_strength: 0.0,
225 max_nodes: 100_000,
226 enable_cycle_detection: true,
227 confidence_threshold: 0.0,
228 }
229 }
230}
231
232#[derive(Debug, Clone, Default)]
238pub struct TracerStats {
239 pub nodes_tracked: usize,
241 pub edges_tracked: usize,
243 pub chains_traced: usize,
245 pub avg_chain_depth: f64,
247 pub cycles_detected: usize,
249}
250
251#[derive(Debug, Clone)]
257struct EdgeRef {
258 to: String,
259 edge_index: usize,
260}
261
262pub struct CausalChainTracer {
272 config: TracerConfig,
273 nodes: HashMap<String, CausalNode>,
274 edges: Vec<CausalEdge>,
275 adj_out: HashMap<String, Vec<EdgeRef>>,
277 adj_in: HashMap<String, Vec<String>>,
279 stats: TracerStats,
280}
281
282impl CausalChainTracer {
283 pub fn new(config: TracerConfig) -> Self {
289 Self {
290 config,
291 nodes: HashMap::new(),
292 edges: Vec::new(),
293 adj_out: HashMap::new(),
294 adj_in: HashMap::new(),
295 stats: TracerStats::default(),
296 }
297 }
298
299 pub fn with_defaults() -> Self {
301 Self::new(TracerConfig::default())
302 }
303
304 pub fn add_node(&mut self, node: CausalNode) -> Result<(), TracerError> {
312 if self.nodes.len() >= self.config.max_nodes && !self.nodes.contains_key(&node.id) {
313 return Err(TracerError::QueryTooExpensive(self.nodes.len()));
314 }
315 self.adj_out.entry(node.id.clone()).or_default();
317 self.adj_in.entry(node.id.clone()).or_default();
318 self.nodes.insert(node.id.clone(), node);
319 self.stats.nodes_tracked = self.nodes.len();
320 Ok(())
321 }
322
323 pub fn add_edge(&mut self, edge: CausalEdge) -> Result<(), TracerError> {
330 if !self.nodes.contains_key(&edge.from_id) {
331 return Err(TracerError::NodeNotFound(edge.from_id.clone()));
332 }
333 if !self.nodes.contains_key(&edge.to_id) {
334 return Err(TracerError::NodeNotFound(edge.to_id.clone()));
335 }
336 if edge.strength < 0.0 || edge.strength > 1.0 {
337 return Err(TracerError::InvalidStrength(edge.strength));
338 }
339
340 if self.config.enable_cycle_detection {
341 if let Some(cycle_path) = self.find_cycle_path(&edge.to_id, &edge.from_id) {
343 self.stats.cycles_detected += 1;
344 let mut full_path = vec![edge.from_id.clone()];
346 full_path.extend(cycle_path);
347 return Err(TracerError::CycleDetected { path: full_path });
348 }
349 }
350
351 let edge_index = self.edges.len();
352 let from = edge.from_id.clone();
353 let to = edge.to_id.clone();
354
355 self.adj_out.entry(from.clone()).or_default().push(EdgeRef {
356 to: to.clone(),
357 edge_index,
358 });
359
360 self.adj_in.entry(to).or_default().push(from);
361
362 self.edges.push(edge);
363 self.stats.edges_tracked = self.edges.len();
364 Ok(())
365 }
366
367 pub fn remove_node(&mut self, id: &str) -> Result<(), TracerError> {
369 if !self.nodes.contains_key(id) {
370 return Err(TracerError::NodeNotFound(id.to_string()));
371 }
372
373 let edge_indices_to_remove: HashSet<usize> = self
375 .edges
376 .iter()
377 .enumerate()
378 .filter(|(_, e)| e.from_id == id || e.to_id == id)
379 .map(|(i, _)| i)
380 .collect();
381
382 let mut new_edges: Vec<CausalEdge> = Vec::new();
384 let mut old_to_new: HashMap<usize, usize> = HashMap::new();
385 for (old_idx, edge) in self.edges.drain(..).enumerate() {
386 if !edge_indices_to_remove.contains(&old_idx) {
387 old_to_new.insert(old_idx, new_edges.len());
388 new_edges.push(edge);
389 }
390 }
391 self.edges = new_edges;
392
393 self.adj_out.clear();
395 self.adj_in.clear();
396 self.nodes.remove(id);
397
398 for nid in self.nodes.keys() {
399 self.adj_out.entry(nid.clone()).or_default();
400 self.adj_in.entry(nid.clone()).or_default();
401 }
402 for (i, edge) in self.edges.iter().enumerate() {
403 self.adj_out
404 .entry(edge.from_id.clone())
405 .or_default()
406 .push(EdgeRef {
407 to: edge.to_id.clone(),
408 edge_index: i,
409 });
410 self.adj_in
411 .entry(edge.to_id.clone())
412 .or_default()
413 .push(edge.from_id.clone());
414 }
415
416 self.stats.nodes_tracked = self.nodes.len();
417 self.stats.edges_tracked = self.edges.len();
418 Ok(())
419 }
420
421 pub fn trace(&mut self, query: &TraceQuery) -> Result<Vec<CausalChain>, TracerError> {
431 let roots = self.resolve_roots(query)?;
432 let max_depth = query.max_depth.min(self.config.max_chain_depth);
433
434 let mut all_chains: Vec<CausalChain> = Vec::new();
435
436 for root_id in &roots {
437 let root_node = self
439 .nodes
440 .get(root_id)
441 .ok_or_else(|| TracerError::NodeNotFound(root_id.clone()))?;
442
443 if !self.node_passes_query(root_node, query) {
444 continue;
445 }
446
447 let mut stack: Vec<(String, Vec<String>, Vec<usize>, f64)> =
449 vec![(root_id.clone(), vec![root_id.clone()], Vec::new(), 1.0_f64)];
450
451 while let Some((current_id, node_path, edge_path, confidence)) = stack.pop() {
452 let depth = node_path.len() - 1;
453 let out_edges = self.adj_out.get(¤t_id).cloned().unwrap_or_default();
454
455 let valid_next: Vec<(String, usize, f64)> = out_edges
457 .iter()
458 .filter_map(|eref| {
459 let edge = &self.edges[eref.edge_index];
460 if edge.strength < query.min_strength {
462 return None;
463 }
464 if !query.include_relations.is_empty()
466 && !query.include_relations.contains(&edge.relation)
467 {
468 return None;
469 }
470 let target = self.nodes.get(&eref.to)?;
472 if !self.node_passes_query(target, query) {
473 return None;
474 }
475 if node_path.contains(&eref.to) {
477 return None;
478 }
479 Some((eref.to.clone(), eref.edge_index, edge.strength))
480 })
481 .collect();
482
483 let is_leaf = valid_next.is_empty() || depth >= max_depth;
484
485 if is_leaf {
486 if confidence >= self.config.confidence_threshold {
488 let chain = self.build_chain(&node_path, &edge_path, confidence)?;
489 all_chains.push(chain);
490 }
491 } else {
492 for (next_id, edge_idx, strength) in valid_next {
493 let mut new_node_path = node_path.clone();
494 new_node_path.push(next_id.clone());
495 let mut new_edge_path = edge_path.clone();
496 new_edge_path.push(edge_idx);
497 let new_confidence = confidence * strength;
498 stack.push((next_id, new_node_path, new_edge_path, new_confidence));
499 }
500 }
501 }
502 }
503
504 self.stats.chains_traced += all_chains.len();
506 if !all_chains.is_empty() {
507 let total_depth: usize = all_chains.iter().map(|c| c.depth).sum();
508 self.stats.avg_chain_depth = total_depth as f64 / all_chains.len() as f64;
509 }
510
511 Ok(all_chains)
512 }
513
514 pub fn shortest_path(&self, from: &str, to: &str) -> Result<Option<Vec<String>>, TracerError> {
520 if !self.nodes.contains_key(from) {
521 return Err(TracerError::NodeNotFound(from.to_string()));
522 }
523 if !self.nodes.contains_key(to) {
524 return Err(TracerError::NodeNotFound(to.to_string()));
525 }
526 if from == to {
527 return Ok(Some(vec![from.to_string()]));
528 }
529
530 let mut visited: HashSet<String> = HashSet::new();
532 let mut queue: VecDeque<(String, Vec<String>)> = VecDeque::new();
534 queue.push_back((from.to_string(), vec![from.to_string()]));
535 visited.insert(from.to_string());
536
537 while let Some((current, path)) = queue.pop_front() {
538 let out_edges = match self.adj_out.get(¤t) {
539 Some(v) => v.clone(),
540 None => continue,
541 };
542 for eref in &out_edges {
543 if visited.contains(&eref.to) {
544 continue;
545 }
546 let mut new_path = path.clone();
547 new_path.push(eref.to.clone());
548 if eref.to == to {
549 return Ok(Some(new_path));
550 }
551 visited.insert(eref.to.clone());
552 queue.push_back((eref.to.clone(), new_path));
553 }
554 }
555
556 Ok(None)
557 }
558
559 pub fn strongest_path(&self, from: &str, to: &str) -> Result<Option<CausalChain>, TracerError> {
566 if !self.nodes.contains_key(from) {
567 return Err(TracerError::NodeNotFound(from.to_string()));
568 }
569 if !self.nodes.contains_key(to) {
570 return Err(TracerError::NodeNotFound(to.to_string()));
571 }
572
573 if from == to {
574 let node = self
575 .nodes
576 .get(from)
577 .ok_or_else(|| TracerError::NodeNotFound(from.to_string()))?
578 .clone();
579 return Ok(Some(CausalChain {
580 nodes: vec![node],
581 edges: Vec::new(),
582 root_id: from.to_string(),
583 leaf_ids: vec![from.to_string()],
584 chain_confidence: 1.0,
585 depth: 0,
586 }));
587 }
588
589 let mut dist: HashMap<String, (f64, Option<String>, Option<usize>)> = HashMap::new();
592 use std::cmp::Reverse;
594 use std::collections::BinaryHeap;
595
596 #[derive(PartialEq)]
598 struct OrdF64(f64);
599 impl Eq for OrdF64 {}
600 impl PartialOrd for OrdF64 {
601 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
602 Some(self.cmp(other))
603 }
604 }
605 impl Ord for OrdF64 {
606 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
607 self.0
608 .partial_cmp(&other.0)
609 .unwrap_or(std::cmp::Ordering::Equal)
610 }
611 }
612
613 let mut heap: BinaryHeap<(Reverse<OrdF64>, String)> = BinaryHeap::new();
615
616 dist.insert(from.to_string(), (0.0, None, None));
617 heap.push((Reverse(OrdF64(0.0)), from.to_string()));
618
619 while let Some((Reverse(OrdF64(cost)), current)) = heap.pop() {
620 let recorded_cost = dist.get(¤t).map(|e| e.0).unwrap_or(f64::INFINITY);
622 if cost > recorded_cost + f64::EPSILON {
623 continue;
624 }
625
626 if current == to {
627 break;
628 }
629
630 let out_edges = match self.adj_out.get(¤t) {
631 Some(v) => v.clone(),
632 None => continue,
633 };
634
635 for eref in &out_edges {
636 let edge = &self.edges[eref.edge_index];
637 if edge.strength <= 0.0 {
638 continue;
640 }
641 let new_cost = cost + (-edge.strength.ln());
642 let existing = dist.get(&eref.to).map(|e| e.0).unwrap_or(f64::INFINITY);
643 if new_cost < existing - f64::EPSILON {
644 dist.insert(
645 eref.to.clone(),
646 (new_cost, Some(current.clone()), Some(eref.edge_index)),
647 );
648 heap.push((Reverse(OrdF64(new_cost)), eref.to.clone()));
649 }
650 }
651 }
652
653 if !dist.contains_key(to) || dist[to].1.is_none() && to != from {
655 return Ok(None);
656 }
657
658 let mut node_ids: Vec<String> = Vec::new();
659 let mut edge_indices: Vec<usize> = Vec::new();
660 let mut cursor = to.to_string();
661
662 loop {
663 node_ids.push(cursor.clone());
664 let entry = match dist.get(&cursor) {
665 Some(e) => e.clone(),
666 None => break,
667 };
668 if let Some(ei) = entry.2 {
669 edge_indices.push(ei);
670 }
671 match entry.1 {
672 Some(pred) => cursor = pred,
673 None => break,
674 }
675 }
676
677 node_ids.reverse();
678 edge_indices.reverse();
679
680 let chain = self.build_chain(
681 &node_ids,
682 &edge_indices,
683 self.product_of_edges(&edge_indices),
684 )?;
685 Ok(Some(chain))
686 }
687
688 pub fn root_causes(&self, event_id: &str) -> Result<Vec<CausalNode>, TracerError> {
694 if !self.nodes.contains_key(event_id) {
695 return Err(TracerError::NodeNotFound(event_id.to_string()));
696 }
697
698 let mut visited: HashSet<String> = HashSet::new();
700 let mut queue: VecDeque<String> = VecDeque::new();
701 queue.push_back(event_id.to_string());
702 visited.insert(event_id.to_string());
703
704 while let Some(current) = queue.pop_front() {
705 let parents = self.adj_in.get(¤t).cloned().unwrap_or_default();
706 for parent in parents {
707 if !visited.contains(&parent) {
708 visited.insert(parent.clone());
709 queue.push_back(parent);
710 }
711 }
712 }
713
714 let mut roots: Vec<CausalNode> = Vec::new();
716 for node_id in &visited {
717 if node_id == event_id {
718 continue;
719 }
720 let has_incoming = self
721 .adj_in
722 .get(node_id)
723 .map(|v| !v.is_empty())
724 .unwrap_or(false);
725 if !has_incoming {
726 if let Some(n) = self.nodes.get(node_id) {
727 roots.push(n.clone());
728 }
729 }
730 }
731 Ok(roots)
732 }
733
734 pub fn downstream_effects(
736 &self,
737 event_id: &str,
738 depth: usize,
739 ) -> Result<Vec<CausalNode>, TracerError> {
740 if !self.nodes.contains_key(event_id) {
741 return Err(TracerError::NodeNotFound(event_id.to_string()));
742 }
743
744 let mut visited: HashSet<String> = HashSet::new();
745 let mut queue: VecDeque<(String, usize)> = VecDeque::new();
746 queue.push_back((event_id.to_string(), 0));
747 visited.insert(event_id.to_string());
748
749 while let Some((current, d)) = queue.pop_front() {
750 if d >= depth {
751 continue;
752 }
753 let out_edges = self.adj_out.get(¤t).cloned().unwrap_or_default();
754 for eref in &out_edges {
755 if !visited.contains(&eref.to) {
756 visited.insert(eref.to.clone());
757 queue.push_back((eref.to.clone(), d + 1));
758 }
759 }
760 }
761
762 let mut effects: Vec<CausalNode> = visited
764 .iter()
765 .filter(|id| id.as_str() != event_id)
766 .filter_map(|id| self.nodes.get(id).cloned())
767 .collect();
768 effects.sort_by_key(|a| a.timestamp);
769 Ok(effects)
770 }
771
772 pub fn stats(&self) -> TracerStats {
774 self.stats.clone()
775 }
776
777 fn find_cycle_path(&self, start: &str, target: &str) -> Option<Vec<String>> {
784 if start == target {
785 return Some(vec![start.to_string()]);
786 }
787
788 let mut visited: HashSet<String> = HashSet::new();
789 let mut path: Vec<String> = Vec::new();
790 self.dfs_find(start, target, &mut visited, &mut path)
791 }
792
793 fn dfs_find(
795 &self,
796 current: &str,
797 target: &str,
798 visited: &mut HashSet<String>,
799 path: &mut Vec<String>,
800 ) -> Option<Vec<String>> {
801 if visited.contains(current) {
802 return None;
803 }
804 visited.insert(current.to_string());
805 path.push(current.to_string());
806
807 if current == target {
808 return Some(path.clone());
809 }
810
811 let out_edges = match self.adj_out.get(current) {
812 Some(v) => v.clone(),
813 None => {
814 path.pop();
815 return None;
816 }
817 };
818
819 for eref in &out_edges {
820 if let Some(found) = self.dfs_find(&eref.to, target, visited, path) {
821 return Some(found);
822 }
823 }
824
825 path.pop();
826 None
827 }
828
829 fn resolve_roots(&self, query: &TraceQuery) -> Result<Vec<String>, TracerError> {
831 match &query.root_event_id {
832 Some(id) => {
833 if !self.nodes.contains_key(id) {
834 return Err(TracerError::NodeNotFound(id.clone()));
835 }
836 Ok(vec![id.clone()])
837 }
838 None => {
839 let roots: Vec<String> = self
841 .nodes
842 .keys()
843 .filter(|id| self.adj_in.get(*id).map(|v| v.is_empty()).unwrap_or(true))
844 .cloned()
845 .collect();
846 Ok(roots)
847 }
848 }
849 }
850
851 fn node_passes_query(&self, node: &CausalNode, query: &TraceQuery) -> bool {
853 if !query.event_types.is_empty() && !query.event_types.contains(&node.event_type) {
854 return false;
855 }
856 if let Some((start, end)) = query.time_window_us {
857 if node.timestamp < start || node.timestamp > end {
858 return false;
859 }
860 }
861 true
862 }
863
864 fn product_of_edges(&self, edge_indices: &[usize]) -> f64 {
866 edge_indices
867 .iter()
868 .fold(1.0_f64, |acc, &i| acc * self.edges[i].strength)
869 }
870
871 fn build_chain(
873 &self,
874 node_ids: &[String],
875 edge_indices: &[usize],
876 confidence: f64,
877 ) -> Result<CausalChain, TracerError> {
878 let mut nodes: Vec<CausalNode> = Vec::with_capacity(node_ids.len());
879 for id in node_ids {
880 let node = self
881 .nodes
882 .get(id)
883 .ok_or_else(|| TracerError::NodeNotFound(id.clone()))?
884 .clone();
885 nodes.push(node);
886 }
887
888 let mut edges: Vec<CausalEdge> = Vec::with_capacity(edge_indices.len());
889 for &i in edge_indices {
890 edges.push(self.edges[i].clone());
891 }
892
893 let root_id = node_ids.first().cloned().unwrap_or_default();
894 let leaf_ids = vec![node_ids.last().cloned().unwrap_or_default()];
895 let depth = node_ids.len().saturating_sub(1);
896
897 Ok(CausalChain {
898 nodes,
899 edges,
900 root_id,
901 leaf_ids,
902 chain_confidence: confidence,
903 depth,
904 })
905 }
906}
907
908pub fn xorshift64(state: &mut u64) -> u64 {
914 let mut x = *state;
915 x ^= x << 13;
916 x ^= x >> 7;
917 x ^= x << 17;
918 *state = x;
919 x
920}
921
922#[cfg(test)]
927mod tests {
928 use super::*;
929
930 fn make_tracer() -> CausalChainTracer {
935 CausalChainTracer::new(TracerConfig {
936 max_chain_depth: 16,
937 min_edge_strength: 0.0,
938 max_nodes: 10_000,
939 enable_cycle_detection: true,
940 confidence_threshold: 0.0,
941 })
942 }
943
944 fn node(id: &str, etype: &str, ts: u64) -> CausalNode {
945 CausalNode::new(id, etype, ts, 1.0)
946 }
947
948 fn edge(from: &str, to: &str, rel: CausalRelation, strength: f64) -> CausalEdge {
949 CausalEdge::new(from, to, rel, strength, 0)
950 }
951
952 #[test]
956 fn test_add_node_basic() {
957 let mut t = make_tracer();
958 let n = node("A", "login", 1000);
959 assert!(t.add_node(n).is_ok());
960 assert_eq!(t.stats().nodes_tracked, 1);
961 }
962
963 #[test]
967 fn test_add_node_duplicate_overwrite() {
968 let mut t = make_tracer();
969 t.add_node(node("A", "login", 100))
970 .expect("test setup: add_node failed");
971 t.add_node(node("A", "logout", 200))
972 .expect("test setup: add_node failed");
973 assert_eq!(t.stats().nodes_tracked, 1);
975 }
976
977 #[test]
981 fn test_add_node_max_nodes() {
982 let mut t = CausalChainTracer::new(TracerConfig {
983 max_nodes: 2,
984 ..Default::default()
985 });
986 t.add_node(node("A", "x", 0))
987 .expect("test setup: add_node failed");
988 t.add_node(node("B", "x", 0))
989 .expect("test setup: add_node failed");
990 let err = t.add_node(node("C", "x", 0)).unwrap_err();
991 assert!(matches!(err, TracerError::QueryTooExpensive(_)));
992 }
993
994 #[test]
998 fn test_add_edge_basic() {
999 let mut t = make_tracer();
1000 t.add_node(node("A", "e", 0))
1001 .expect("test setup: add_node failed");
1002 t.add_node(node("B", "e", 1))
1003 .expect("test setup: add_node failed");
1004 assert!(t
1005 .add_edge(edge("A", "B", CausalRelation::DirectCause, 0.8))
1006 .is_ok());
1007 assert_eq!(t.stats().edges_tracked, 1);
1008 }
1009
1010 #[test]
1014 fn test_add_edge_missing_from() {
1015 let mut t = make_tracer();
1016 t.add_node(node("B", "e", 0))
1017 .expect("test setup: add_node failed");
1018 let err = t
1019 .add_edge(edge("X", "B", CausalRelation::Enables, 0.5))
1020 .unwrap_err();
1021 assert!(matches!(err, TracerError::NodeNotFound(_)));
1022 }
1023
1024 #[test]
1028 fn test_add_edge_missing_to() {
1029 let mut t = make_tracer();
1030 t.add_node(node("A", "e", 0))
1031 .expect("test setup: add_node failed");
1032 let err = t
1033 .add_edge(edge("A", "Y", CausalRelation::Enables, 0.5))
1034 .unwrap_err();
1035 assert!(matches!(err, TracerError::NodeNotFound(_)));
1036 }
1037
1038 #[test]
1042 fn test_add_edge_strength_too_high() {
1043 let mut t = make_tracer();
1044 t.add_node(node("A", "e", 0))
1045 .expect("test setup: add_node failed");
1046 t.add_node(node("B", "e", 1))
1047 .expect("test setup: add_node failed");
1048 let err = t
1049 .add_edge(edge("A", "B", CausalRelation::Precedes, 1.5))
1050 .unwrap_err();
1051 assert!(matches!(err, TracerError::InvalidStrength(_)));
1052 }
1053
1054 #[test]
1058 fn test_add_edge_strength_negative() {
1059 let mut t = make_tracer();
1060 t.add_node(node("A", "e", 0))
1061 .expect("test setup: add_node failed");
1062 t.add_node(node("B", "e", 1))
1063 .expect("test setup: add_node failed");
1064 let err = t
1065 .add_edge(edge("A", "B", CausalRelation::Precedes, -0.1))
1066 .unwrap_err();
1067 assert!(matches!(err, TracerError::InvalidStrength(_)));
1068 }
1069
1070 #[test]
1074 fn test_add_edge_boundary_strengths() {
1075 let mut t = make_tracer();
1076 t.add_node(node("A", "e", 0))
1077 .expect("test setup: add_node failed");
1078 t.add_node(node("B", "e", 1))
1079 .expect("test setup: add_node failed");
1080 t.add_node(node("C", "e", 2))
1081 .expect("test setup: add_node failed");
1082 assert!(t
1083 .add_edge(edge("A", "B", CausalRelation::Precedes, 0.0))
1084 .is_ok());
1085 assert!(t
1086 .add_edge(edge("B", "C", CausalRelation::Precedes, 1.0))
1087 .is_ok());
1088 }
1089
1090 #[test]
1094 fn test_cycle_direct() {
1095 let mut t = make_tracer();
1096 t.add_node(node("A", "e", 0))
1097 .expect("test setup: add_node failed");
1098 t.add_node(node("B", "e", 1))
1099 .expect("test setup: add_node failed");
1100 t.add_edge(edge("A", "B", CausalRelation::DirectCause, 0.9))
1101 .expect("test setup: add_edge failed");
1102 let err = t
1103 .add_edge(edge("B", "A", CausalRelation::DirectCause, 0.9))
1104 .unwrap_err();
1105 assert!(matches!(err, TracerError::CycleDetected { .. }));
1106 }
1107
1108 #[test]
1112 fn test_cycle_indirect() {
1113 let mut t = make_tracer();
1114 for id in ["A", "B", "C"] {
1115 t.add_node(node(id, "e", 0))
1116 .expect("test setup: add_node failed");
1117 }
1118 t.add_edge(edge("A", "B", CausalRelation::DirectCause, 0.8))
1119 .expect("test setup: add_edge failed");
1120 t.add_edge(edge("B", "C", CausalRelation::DirectCause, 0.8))
1121 .expect("test setup: add_edge failed");
1122 let err = t
1123 .add_edge(edge("C", "A", CausalRelation::DirectCause, 0.8))
1124 .unwrap_err();
1125 assert!(matches!(err, TracerError::CycleDetected { .. }));
1126 }
1127
1128 #[test]
1132 fn test_cycle_self_loop() {
1133 let mut t = make_tracer();
1134 t.add_node(node("A", "e", 0))
1135 .expect("test setup: add_node failed");
1136 let err = t
1137 .add_edge(edge("A", "A", CausalRelation::DirectCause, 0.5))
1138 .unwrap_err();
1139 assert!(matches!(err, TracerError::CycleDetected { .. }));
1140 }
1141
1142 #[test]
1146 fn test_cycle_detection_disabled() {
1147 let mut t = CausalChainTracer::new(TracerConfig {
1148 enable_cycle_detection: false,
1149 ..Default::default()
1150 });
1151 t.add_node(node("A", "e", 0))
1152 .expect("test setup: add_node failed");
1153 t.add_node(node("B", "e", 1))
1154 .expect("test setup: add_node failed");
1155 t.add_edge(edge("A", "B", CausalRelation::DirectCause, 0.9))
1156 .expect("test setup: add_edge failed");
1157 assert!(t
1159 .add_edge(edge("B", "A", CausalRelation::DirectCause, 0.9))
1160 .is_ok());
1161 }
1162
1163 #[test]
1167 fn test_trace_simple_chain() {
1168 let mut t = make_tracer();
1169 t.add_node(node("A", "e", 0))
1170 .expect("test setup: add_node failed");
1171 t.add_node(node("B", "e", 1))
1172 .expect("test setup: add_node failed");
1173 t.add_edge(edge("A", "B", CausalRelation::DirectCause, 0.9))
1174 .expect("test setup: add_edge failed");
1175 let q = TraceQuery {
1176 root_event_id: Some("A".to_string()),
1177 max_depth: 4,
1178 ..Default::default()
1179 };
1180 let chains = t.trace(&q).expect("test setup: trace failed");
1181 assert_eq!(chains.len(), 1);
1182 assert_eq!(chains[0].nodes.len(), 2);
1183 assert!((chains[0].chain_confidence - 0.9).abs() < 1e-9);
1184 }
1185
1186 #[test]
1190 fn test_trace_confidence_product() {
1191 let mut t = make_tracer();
1192 for (id, ts) in [("A", 0u64), ("B", 1), ("C", 2)] {
1193 t.add_node(node(id, "e", ts))
1194 .expect("test setup: add_node failed");
1195 }
1196 t.add_edge(edge("A", "B", CausalRelation::DirectCause, 0.8))
1197 .expect("test setup: add_edge failed");
1198 t.add_edge(edge("B", "C", CausalRelation::DirectCause, 0.5))
1199 .expect("test setup: add_edge failed");
1200 let q = TraceQuery {
1201 root_event_id: Some("A".to_string()),
1202 max_depth: 4,
1203 ..Default::default()
1204 };
1205 let chains = t.trace(&q).expect("test setup: trace failed");
1206 assert_eq!(chains.len(), 1);
1207 assert!((chains[0].chain_confidence - 0.4).abs() < 1e-9);
1208 }
1209
1210 #[test]
1214 fn test_trace_max_depth() {
1215 let mut t = make_tracer();
1216 for (id, ts) in [("A", 0u64), ("B", 1), ("C", 2), ("D", 3)] {
1217 t.add_node(node(id, "e", ts))
1218 .expect("test setup: add_node failed");
1219 }
1220 t.add_edge(edge("A", "B", CausalRelation::DirectCause, 0.9))
1221 .expect("test setup: add_edge failed");
1222 t.add_edge(edge("B", "C", CausalRelation::DirectCause, 0.9))
1223 .expect("test setup: add_edge failed");
1224 t.add_edge(edge("C", "D", CausalRelation::DirectCause, 0.9))
1225 .expect("test setup: add_edge failed");
1226
1227 let q = TraceQuery {
1228 root_event_id: Some("A".to_string()),
1229 max_depth: 1,
1230 ..Default::default()
1231 };
1232 let chains = t.trace(&q).expect("test setup: trace failed");
1233 assert_eq!(chains.len(), 1);
1235 assert_eq!(chains[0].depth, 1);
1236 }
1237
1238 #[test]
1242 fn test_trace_min_strength_filter() {
1243 let mut t = make_tracer();
1244 t.add_node(node("A", "e", 0))
1245 .expect("test setup: add_node failed");
1246 t.add_node(node("B", "e", 1))
1247 .expect("test setup: add_node failed");
1248 t.add_node(node("C", "e", 2))
1249 .expect("test setup: add_node failed");
1250 t.add_edge(edge("A", "B", CausalRelation::DirectCause, 0.3))
1251 .expect("test setup: add_edge failed");
1252 t.add_edge(edge("A", "C", CausalRelation::DirectCause, 0.9))
1253 .expect("test setup: add_edge failed");
1254 let q = TraceQuery {
1255 root_event_id: Some("A".to_string()),
1256 min_strength: 0.5,
1257 max_depth: 4,
1258 ..Default::default()
1259 };
1260 let chains = t.trace(&q).expect("test setup: trace failed");
1261 assert_eq!(chains.len(), 1);
1263 assert_eq!(chains[0].leaf_ids[0], "C");
1264 }
1265
1266 #[test]
1270 fn test_trace_event_type_filter() {
1271 let mut t = make_tracer();
1272 t.add_node(node("A", "request", 0))
1273 .expect("test setup: add_node failed");
1274 t.add_node(node("B", "response", 1))
1275 .expect("test setup: add_node failed");
1276 t.add_node(node("C", "error", 2))
1277 .expect("test setup: add_node failed");
1278 t.add_edge(edge("A", "B", CausalRelation::DirectCause, 0.9))
1279 .expect("test setup: add_edge failed");
1280 t.add_edge(edge("A", "C", CausalRelation::DirectCause, 0.9))
1281 .expect("test setup: add_edge failed");
1282
1283 let q = TraceQuery {
1284 root_event_id: Some("A".to_string()),
1285 event_types: vec!["request".to_string(), "response".to_string()],
1286 max_depth: 4,
1287 ..Default::default()
1288 };
1289 let chains = t.trace(&q).expect("test setup: trace failed");
1290 assert_eq!(chains.len(), 1);
1292 assert!(chains[0].nodes.iter().all(|n| n.event_type != "error"));
1293 }
1294
1295 #[test]
1299 fn test_trace_time_window_filter() {
1300 let mut t = make_tracer();
1301 t.add_node(node("A", "e", 0))
1302 .expect("test setup: add_node failed");
1303 t.add_node(node("B", "e", 100))
1304 .expect("test setup: add_node failed");
1305 t.add_node(node("C", "e", 500))
1306 .expect("test setup: add_node failed");
1307 t.add_edge(edge("A", "B", CausalRelation::DirectCause, 0.9))
1308 .expect("test setup: add_edge failed");
1309 t.add_edge(edge("A", "C", CausalRelation::DirectCause, 0.9))
1310 .expect("test setup: add_edge failed");
1311
1312 let q = TraceQuery {
1313 root_event_id: Some("A".to_string()),
1314 time_window_us: Some((0, 200)),
1315 max_depth: 4,
1316 ..Default::default()
1317 };
1318 let chains = t.trace(&q).expect("test setup: trace failed");
1319 assert!(chains
1321 .iter()
1322 .all(|c| !c.leaf_ids.contains(&"C".to_string())));
1323 }
1324
1325 #[test]
1329 fn test_trace_relation_filter() {
1330 let mut t = make_tracer();
1331 t.add_node(node("A", "e", 0))
1332 .expect("test setup: add_node failed");
1333 t.add_node(node("B", "e", 1))
1334 .expect("test setup: add_node failed");
1335 t.add_node(node("C", "e", 2))
1336 .expect("test setup: add_node failed");
1337 t.add_edge(edge("A", "B", CausalRelation::DirectCause, 0.9))
1338 .expect("test setup: add_edge failed");
1339 t.add_edge(edge("A", "C", CausalRelation::Correlates, 0.9))
1340 .expect("test setup: add_edge failed");
1341
1342 let q = TraceQuery {
1343 root_event_id: Some("A".to_string()),
1344 include_relations: vec![CausalRelation::DirectCause],
1345 max_depth: 4,
1346 ..Default::default()
1347 };
1348 let chains = t.trace(&q).expect("test setup: trace failed");
1349 assert!(chains
1351 .iter()
1352 .all(|c| !c.leaf_ids.contains(&"C".to_string())));
1353 }
1354
1355 #[test]
1359 fn test_trace_confidence_threshold() {
1360 let mut t = CausalChainTracer::new(TracerConfig {
1361 confidence_threshold: 0.5,
1362 ..Default::default()
1363 });
1364 t.add_node(node("A", "e", 0))
1365 .expect("test setup: add_node failed");
1366 t.add_node(node("B", "e", 1))
1367 .expect("test setup: add_node failed");
1368 t.add_node(node("C", "e", 2))
1369 .expect("test setup: add_node failed");
1370 t.add_edge(edge("A", "B", CausalRelation::DirectCause, 0.9))
1371 .expect("test setup: add_edge failed");
1372 t.add_edge(edge("A", "C", CausalRelation::DirectCause, 0.3))
1374 .expect("test setup: add_edge failed");
1375 let q = TraceQuery {
1376 root_event_id: Some("A".to_string()),
1377 max_depth: 4,
1378 ..Default::default()
1379 };
1380 let chains = t.trace(&q).expect("test setup: trace failed");
1381 assert!(chains.iter().all(|c| c.chain_confidence >= 0.5));
1382 }
1383
1384 #[test]
1388 fn test_trace_branching() {
1389 let mut t = make_tracer();
1390 for id in ["A", "B", "C", "D"] {
1392 t.add_node(node(id, "e", 0))
1393 .expect("test setup: add_node failed");
1394 }
1395 t.add_edge(edge("A", "B", CausalRelation::DirectCause, 0.8))
1396 .expect("test setup: add_edge failed");
1397 t.add_edge(edge("A", "C", CausalRelation::DirectCause, 0.7))
1398 .expect("test setup: add_edge failed");
1399 t.add_edge(edge("B", "D", CausalRelation::DirectCause, 0.9))
1400 .expect("test setup: add_edge failed");
1401 t.add_edge(edge("C", "D", CausalRelation::DirectCause, 0.6))
1402 .expect("test setup: add_edge failed");
1403
1404 let q = TraceQuery {
1405 root_event_id: Some("A".to_string()),
1406 max_depth: 8,
1407 ..Default::default()
1408 };
1409 let chains = t.trace(&q).expect("test setup: trace failed");
1410 assert_eq!(chains.len(), 2);
1412 }
1413
1414 #[test]
1418 fn test_trace_empty_graph() {
1419 let mut t = make_tracer();
1420 let q = TraceQuery::default();
1421 let chains = t.trace(&q).expect("test setup: trace failed");
1422 assert!(chains.is_empty());
1423 }
1424
1425 #[test]
1429 fn test_trace_root_not_found() {
1430 let mut t = make_tracer();
1431 let q = TraceQuery {
1432 root_event_id: Some("MISSING".to_string()),
1433 ..Default::default()
1434 };
1435 let err = t.trace(&q).unwrap_err();
1436 assert!(matches!(err, TracerError::NodeNotFound(_)));
1437 }
1438
1439 #[test]
1443 fn test_shortest_path_direct() {
1444 let mut t = make_tracer();
1445 t.add_node(node("A", "e", 0))
1446 .expect("test setup: add_node failed");
1447 t.add_node(node("B", "e", 1))
1448 .expect("test setup: add_node failed");
1449 t.add_edge(edge("A", "B", CausalRelation::DirectCause, 0.9))
1450 .expect("test setup: add_edge failed");
1451 let path = t
1452 .shortest_path("A", "B")
1453 .expect("test setup: shortest_path failed")
1454 .expect("test setup: expected Some path");
1455 assert_eq!(path, vec!["A", "B"]);
1456 }
1457
1458 #[test]
1462 fn test_shortest_path_multi_hop() {
1463 let mut t = make_tracer();
1464 for id in ["A", "B", "C"] {
1465 t.add_node(node(id, "e", 0))
1466 .expect("test setup: add_node failed");
1467 }
1468 t.add_edge(edge("A", "B", CausalRelation::DirectCause, 0.8))
1469 .expect("test setup: add_edge failed");
1470 t.add_edge(edge("B", "C", CausalRelation::DirectCause, 0.8))
1471 .expect("test setup: add_edge failed");
1472 let path = t
1473 .shortest_path("A", "C")
1474 .expect("test setup: shortest_path failed")
1475 .expect("test setup: expected Some path");
1476 assert_eq!(path.len(), 3);
1477 }
1478
1479 #[test]
1483 fn test_shortest_path_no_path() {
1484 let mut t = make_tracer();
1485 t.add_node(node("A", "e", 0))
1486 .expect("test setup: add_node failed");
1487 t.add_node(node("B", "e", 1))
1488 .expect("test setup: add_node failed");
1489 let result = t
1490 .shortest_path("A", "B")
1491 .expect("test setup: shortest_path failed");
1492 assert!(result.is_none());
1493 }
1494
1495 #[test]
1499 fn test_shortest_path_same_node() {
1500 let mut t = make_tracer();
1501 t.add_node(node("A", "e", 0))
1502 .expect("test setup: add_node failed");
1503 let path = t
1504 .shortest_path("A", "A")
1505 .expect("test setup: shortest_path failed")
1506 .expect("test setup: expected Some path");
1507 assert_eq!(path, vec!["A"]);
1508 }
1509
1510 #[test]
1514 fn test_shortest_path_missing_node() {
1515 let t = make_tracer();
1516 let err = t.shortest_path("X", "Y").unwrap_err();
1517 assert!(matches!(err, TracerError::NodeNotFound(_)));
1518 }
1519
1520 #[test]
1524 fn test_shortest_path_is_shortest() {
1525 let mut t = make_tracer();
1526 for id in ["A", "B", "C", "D"] {
1528 t.add_node(node(id, "e", 0))
1529 .expect("test setup: add_node failed");
1530 }
1531 t.add_edge(edge("A", "B", CausalRelation::DirectCause, 0.9))
1532 .expect("test setup: add_edge failed");
1533 t.add_edge(edge("B", "D", CausalRelation::DirectCause, 0.9))
1534 .expect("test setup: add_edge failed");
1535 t.add_edge(edge("A", "C", CausalRelation::DirectCause, 0.9))
1536 .expect("test setup: add_edge failed");
1537 t.add_edge(edge("C", "D", CausalRelation::DirectCause, 0.9))
1538 .expect("test setup: add_edge failed");
1539 let path = t
1540 .shortest_path("A", "D")
1541 .expect("test setup: shortest_path failed")
1542 .expect("test setup: expected Some path");
1543 assert_eq!(path.len(), 3);
1544 }
1545
1546 #[test]
1550 fn test_strongest_path_simple() {
1551 let mut t = make_tracer();
1552 t.add_node(node("A", "e", 0))
1553 .expect("test setup: add_node failed");
1554 t.add_node(node("B", "e", 1))
1555 .expect("test setup: add_node failed");
1556 t.add_edge(edge("A", "B", CausalRelation::DirectCause, 0.7))
1557 .expect("test setup: add_edge failed");
1558 let chain = t
1559 .strongest_path("A", "B")
1560 .expect("test setup: strongest_path failed")
1561 .expect("test setup: expected Some chain");
1562 assert!((chain.chain_confidence - 0.7).abs() < 1e-9);
1563 }
1564
1565 #[test]
1569 fn test_strongest_path_picks_higher() {
1570 let mut t = make_tracer();
1571 for id in ["A", "B", "C", "D"] {
1574 t.add_node(node(id, "e", 0))
1575 .expect("test setup: add_node failed");
1576 }
1577 t.add_edge(edge("A", "B", CausalRelation::DirectCause, 0.9))
1578 .expect("test setup: add_edge failed");
1579 t.add_edge(edge("B", "D", CausalRelation::DirectCause, 0.9))
1580 .expect("test setup: add_edge failed");
1581 t.add_edge(edge("A", "C", CausalRelation::DirectCause, 0.5))
1582 .expect("test setup: add_edge failed");
1583 t.add_edge(edge("C", "D", CausalRelation::DirectCause, 0.5))
1584 .expect("test setup: add_edge failed");
1585 let chain = t
1586 .strongest_path("A", "D")
1587 .expect("test setup: strongest_path failed")
1588 .expect("test setup: expected Some chain");
1589 assert!(chain.chain_confidence > 0.8);
1590 }
1591
1592 #[test]
1596 fn test_strongest_path_same_node() {
1597 let mut t = make_tracer();
1598 t.add_node(node("A", "e", 0))
1599 .expect("test setup: add_node failed");
1600 let chain = t
1601 .strongest_path("A", "A")
1602 .expect("test setup: strongest_path failed")
1603 .expect("test setup: expected Some chain");
1604 assert!((chain.chain_confidence - 1.0).abs() < 1e-9);
1605 assert_eq!(chain.depth, 0);
1606 }
1607
1608 #[test]
1612 fn test_strongest_path_no_path() {
1613 let mut t = make_tracer();
1614 t.add_node(node("A", "e", 0))
1615 .expect("test setup: add_node failed");
1616 t.add_node(node("B", "e", 1))
1617 .expect("test setup: add_node failed");
1618 let result = t
1619 .strongest_path("A", "B")
1620 .expect("test setup: strongest_path failed");
1621 assert!(result.is_none());
1622 }
1623
1624 #[test]
1628 fn test_strongest_path_missing() {
1629 let t = make_tracer();
1630 let err = t.strongest_path("X", "Y").unwrap_err();
1631 assert!(matches!(err, TracerError::NodeNotFound(_)));
1632 }
1633
1634 #[test]
1638 fn test_root_causes_single() {
1639 let mut t = make_tracer();
1640 for id in ["A", "B", "C"] {
1641 t.add_node(node(id, "e", 0))
1642 .expect("test setup: add_node failed");
1643 }
1644 t.add_edge(edge("A", "B", CausalRelation::DirectCause, 0.9))
1645 .expect("test setup: add_edge failed");
1646 t.add_edge(edge("B", "C", CausalRelation::DirectCause, 0.9))
1647 .expect("test setup: add_edge failed");
1648 let roots = t.root_causes("C").expect("test setup: root_causes failed");
1649 assert_eq!(roots.len(), 1);
1650 assert_eq!(roots[0].id, "A");
1651 }
1652
1653 #[test]
1657 fn test_root_causes_multiple() {
1658 let mut t = make_tracer();
1659 for id in ["A", "B", "C"] {
1660 t.add_node(node(id, "e", 0))
1661 .expect("test setup: add_node failed");
1662 }
1663 t.add_edge(edge("A", "C", CausalRelation::DirectCause, 0.9))
1664 .expect("test setup: add_edge failed");
1665 t.add_edge(edge("B", "C", CausalRelation::DirectCause, 0.9))
1666 .expect("test setup: add_edge failed");
1667 let mut roots = t.root_causes("C").expect("test setup: root_causes failed");
1668 roots.sort_by(|a, b| a.id.cmp(&b.id));
1669 assert_eq!(roots.len(), 2);
1670 assert_eq!(roots[0].id, "A");
1671 assert_eq!(roots[1].id, "B");
1672 }
1673
1674 #[test]
1678 fn test_root_causes_no_ancestors() {
1679 let mut t = make_tracer();
1680 t.add_node(node("A", "e", 0))
1681 .expect("test setup: add_node failed");
1682 let roots = t.root_causes("A").expect("test setup: root_causes failed");
1683 assert!(roots.is_empty());
1685 }
1686
1687 #[test]
1691 fn test_root_causes_missing() {
1692 let t = make_tracer();
1693 let err = t.root_causes("MISSING").unwrap_err();
1694 assert!(matches!(err, TracerError::NodeNotFound(_)));
1695 }
1696
1697 #[test]
1701 fn test_downstream_effects_depth1() {
1702 let mut t = make_tracer();
1703 for id in ["A", "B", "C", "D"] {
1704 t.add_node(node(id, "e", 0))
1705 .expect("test setup: add_node failed");
1706 }
1707 t.add_edge(edge("A", "B", CausalRelation::DirectCause, 0.9))
1708 .expect("test setup: add_edge failed");
1709 t.add_edge(edge("B", "C", CausalRelation::DirectCause, 0.9))
1710 .expect("test setup: add_edge failed");
1711 t.add_edge(edge("B", "D", CausalRelation::DirectCause, 0.9))
1712 .expect("test setup: add_edge failed");
1713 let effects = t
1714 .downstream_effects("A", 1)
1715 .expect("test setup: downstream_effects failed");
1716 let ids: Vec<&str> = effects.iter().map(|n| n.id.as_str()).collect();
1717 assert!(ids.contains(&"B"));
1718 assert!(!ids.contains(&"C"));
1719 }
1720
1721 #[test]
1725 fn test_downstream_effects_deep() {
1726 let mut t = make_tracer();
1727 for (id, ts) in [("A", 0u64), ("B", 1), ("C", 2), ("D", 3)] {
1728 t.add_node(node(id, "e", ts))
1729 .expect("test setup: add_node failed");
1730 }
1731 t.add_edge(edge("A", "B", CausalRelation::DirectCause, 0.9))
1732 .expect("test setup: add_edge failed");
1733 t.add_edge(edge("B", "C", CausalRelation::DirectCause, 0.9))
1734 .expect("test setup: add_edge failed");
1735 t.add_edge(edge("C", "D", CausalRelation::DirectCause, 0.9))
1736 .expect("test setup: add_edge failed");
1737 let effects = t
1738 .downstream_effects("A", 10)
1739 .expect("test setup: downstream_effects failed");
1740 assert_eq!(effects.len(), 3);
1741 }
1742
1743 #[test]
1747 fn test_downstream_effects_missing() {
1748 let t = make_tracer();
1749 let err = t.downstream_effects("MISSING", 5).unwrap_err();
1750 assert!(matches!(err, TracerError::NodeNotFound(_)));
1751 }
1752
1753 #[test]
1757 fn test_downstream_effects_zero_depth() {
1758 let mut t = make_tracer();
1759 t.add_node(node("A", "e", 0))
1760 .expect("test setup: add_node failed");
1761 t.add_node(node("B", "e", 1))
1762 .expect("test setup: add_node failed");
1763 t.add_edge(edge("A", "B", CausalRelation::DirectCause, 0.9))
1764 .expect("test setup: add_edge failed");
1765 let effects = t
1766 .downstream_effects("A", 0)
1767 .expect("test setup: downstream_effects failed");
1768 assert!(effects.is_empty());
1770 }
1771
1772 #[test]
1776 fn test_remove_node_basic() {
1777 let mut t = make_tracer();
1778 t.add_node(node("A", "e", 0))
1779 .expect("test setup: add_node failed");
1780 t.add_node(node("B", "e", 1))
1781 .expect("test setup: add_node failed");
1782 t.add_edge(edge("A", "B", CausalRelation::DirectCause, 0.8))
1783 .expect("test setup: add_edge failed");
1784 t.remove_node("A").expect("test setup: remove_node failed");
1785 assert_eq!(t.stats().nodes_tracked, 1);
1786 assert_eq!(t.stats().edges_tracked, 0);
1787 }
1788
1789 #[test]
1793 fn test_remove_node_removes_edges() {
1794 let mut t = make_tracer();
1795 for id in ["A", "B", "C"] {
1796 t.add_node(node(id, "e", 0))
1797 .expect("test setup: add_node failed");
1798 }
1799 t.add_edge(edge("A", "B", CausalRelation::DirectCause, 0.9))
1800 .expect("test setup: add_edge failed");
1801 t.add_edge(edge("B", "C", CausalRelation::DirectCause, 0.9))
1802 .expect("test setup: add_edge failed");
1803 t.remove_node("B").expect("test setup: remove_node failed");
1804 assert_eq!(t.stats().edges_tracked, 0);
1805 }
1806
1807 #[test]
1811 fn test_remove_node_missing() {
1812 let mut t = make_tracer();
1813 let err = t.remove_node("MISSING").unwrap_err();
1814 assert!(matches!(err, TracerError::NodeNotFound(_)));
1815 }
1816
1817 #[test]
1821 fn test_stats_basic() {
1822 let mut t = make_tracer();
1823 t.add_node(node("A", "e", 0))
1824 .expect("test setup: add_node failed");
1825 t.add_node(node("B", "e", 1))
1826 .expect("test setup: add_node failed");
1827 t.add_edge(edge("A", "B", CausalRelation::DirectCause, 0.9))
1828 .expect("test setup: add_edge failed");
1829 let s = t.stats();
1830 assert_eq!(s.nodes_tracked, 2);
1831 assert_eq!(s.edges_tracked, 1);
1832 }
1833
1834 #[test]
1838 fn test_stats_cycles_detected() {
1839 let mut t = make_tracer();
1840 t.add_node(node("A", "e", 0))
1841 .expect("test setup: add_node failed");
1842 t.add_node(node("B", "e", 1))
1843 .expect("test setup: add_node failed");
1844 t.add_edge(edge("A", "B", CausalRelation::DirectCause, 0.9))
1845 .expect("test setup: add_edge failed");
1846 let _ = t.add_edge(edge("B", "A", CausalRelation::DirectCause, 0.9));
1847 assert_eq!(t.stats().cycles_detected, 1);
1848 }
1849
1850 #[test]
1854 fn test_xorshift64_deterministic() {
1855 let mut state = 12345u64;
1856 let v1 = xorshift64(&mut state);
1857 let mut state2 = 12345u64;
1858 let v2 = xorshift64(&mut state2);
1859 assert_eq!(v1, v2);
1860 assert_ne!(v1, 0);
1861 }
1862
1863 #[test]
1867 fn test_large_graph_stress() {
1868 let mut t = make_tracer();
1869 let mut rng = 999_999u64;
1870 let n = 50usize;
1871
1872 for i in 0..n {
1873 t.add_node(node(&i.to_string(), "stress", xorshift64(&mut rng) % 1000))
1874 .expect("test setup: add_node failed");
1875 }
1876
1877 let mut added = 0usize;
1879 for i in 0..n {
1880 for j in (i + 1)..n {
1881 let r = xorshift64(&mut rng);
1882 if r.is_multiple_of(5) {
1883 let strength = (r % 100) as f64 / 100.0;
1884 let _ = t.add_edge(edge(
1885 &i.to_string(),
1886 &j.to_string(),
1887 CausalRelation::DirectCause,
1888 strength,
1889 ));
1890 added += 1;
1891 }
1892 }
1893 }
1894
1895 assert!(added > 0, "expected some edges");
1896 let q = TraceQuery {
1897 root_event_id: Some("0".to_string()),
1898 max_depth: 5,
1899 ..Default::default()
1900 };
1901 let chains = t.trace(&q).expect("test setup: trace failed");
1902 assert!(chains.len() <= 10_000);
1904 }
1905
1906 #[test]
1910 fn test_trace_no_relation_filter() {
1911 let mut t = make_tracer();
1912 t.add_node(node("A", "e", 0))
1913 .expect("test setup: add_node failed");
1914 t.add_node(node("B", "e", 1))
1915 .expect("test setup: add_node failed");
1916 t.add_node(node("C", "e", 2))
1917 .expect("test setup: add_node failed");
1918 t.add_edge(edge("A", "B", CausalRelation::Inhibits, 0.9))
1919 .expect("test setup: add_edge failed");
1920 t.add_edge(edge("A", "C", CausalRelation::IndirectCause, 0.9))
1921 .expect("test setup: add_edge failed");
1922 let q = TraceQuery {
1923 root_event_id: Some("A".to_string()),
1924 include_relations: vec![], max_depth: 4,
1926 ..Default::default()
1927 };
1928 let chains = t.trace(&q).expect("test setup: trace failed");
1929 assert_eq!(chains.len(), 2);
1930 }
1931
1932 #[test]
1936 fn test_causal_node_with_attribute() {
1937 let n = CausalNode::new("A", "login", 0, 1.0)
1938 .with_attribute("ip", "192.168.1.1")
1939 .with_attribute("user", "bob");
1940 assert_eq!(n.attributes.len(), 2);
1941 assert_eq!(
1942 n.attributes[0],
1943 ("ip".to_string(), "192.168.1.1".to_string())
1944 );
1945 }
1946
1947 #[test]
1951 fn test_trace_implicit_roots() {
1952 let mut t = make_tracer();
1953 t.add_node(node("R1", "e", 0))
1955 .expect("test setup: add_node failed");
1956 t.add_node(node("R2", "e", 0))
1957 .expect("test setup: add_node failed");
1958 t.add_node(node("X", "e", 1))
1959 .expect("test setup: add_node failed");
1960 t.add_edge(edge("R1", "X", CausalRelation::DirectCause, 0.8))
1961 .expect("test setup: add_edge failed");
1962 t.add_edge(edge("R2", "X", CausalRelation::DirectCause, 0.7))
1963 .expect("test setup: add_edge failed");
1964
1965 let q = TraceQuery {
1966 root_event_id: None,
1967 max_depth: 4,
1968 ..Default::default()
1969 };
1970 let chains = t.trace(&q).expect("test setup: trace failed");
1971 assert_eq!(chains.len(), 2);
1973 }
1974
1975 #[test]
1979 fn test_tracer_error_display() {
1980 let e = TracerError::NodeNotFound("ABC".to_string());
1981 assert!(e.to_string().contains("ABC"));
1982 let e2 = TracerError::InvalidStrength(2.0);
1983 assert!(e2.to_string().contains("2"));
1984 let e3 = TracerError::CycleDetected {
1985 path: vec!["A".to_string(), "B".to_string()],
1986 };
1987 assert!(e3.to_string().contains("A -> B"));
1988 }
1989
1990 #[test]
1994 fn test_chains_traced_counter() {
1995 let mut t = make_tracer();
1996 t.add_node(node("A", "e", 0))
1997 .expect("test setup: add_node failed");
1998 t.add_node(node("B", "e", 1))
1999 .expect("test setup: add_node failed");
2000 t.add_edge(edge("A", "B", CausalRelation::DirectCause, 0.9))
2001 .expect("test setup: add_edge failed");
2002 let q = TraceQuery {
2003 root_event_id: Some("A".to_string()),
2004 max_depth: 4,
2005 ..Default::default()
2006 };
2007 t.trace(&q).expect("test setup: trace failed");
2008 t.trace(&q).expect("test setup: trace failed");
2009 assert_eq!(t.stats().chains_traced, 2);
2010 }
2011}