1use std::collections::{HashMap, HashSet, VecDeque};
40
41fn xorshift64(state: &mut u64) -> u64 {
49 let mut x = *state;
50 x ^= x << 13;
51 x ^= x >> 7;
52 x ^= x << 17;
53 *state = x;
54 x
55}
56
57#[derive(Debug, Clone, PartialEq)]
63pub enum GraphError {
64 PeerNotFound(String),
66 EdgeNotFound { from: String, to: String },
68 SelfLoop(String),
70 InvalidWeight(f64),
72 MaxPeersExceeded,
74}
75
76impl std::fmt::Display for GraphError {
77 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78 match self {
79 GraphError::PeerNotFound(id) => write!(f, "peer not found: {id}"),
80 GraphError::EdgeNotFound { from, to } => {
81 write!(f, "edge not found: {from} → {to}")
82 }
83 GraphError::SelfLoop(id) => write!(f, "self-loop not allowed for peer {id}"),
84 GraphError::InvalidWeight(w) => {
85 write!(f, "invalid edge weight {w}; must be in [0.0, 1.0]")
86 }
87 GraphError::MaxPeersExceeded => write!(f, "maximum peer count exceeded"),
88 }
89 }
90}
91
92impl std::error::Error for GraphError {}
93
94#[derive(Debug, Clone)]
100pub struct TrustEdge {
101 pub from_peer: String,
103 pub to_peer: String,
105 pub weight: f64,
107 pub created_at: u64,
109 pub updated_at: u64,
111 pub interaction_count: u64,
113}
114
115#[derive(Debug, Clone)]
117pub struct ReputationScore {
118 pub peer_id: String,
120 pub direct_score: f64,
122 pub propagated_score: f64,
124 pub combined_score: f64,
126 pub confidence: f64,
128 pub percentile: f64,
130}
131
132#[derive(Debug, Clone)]
134pub enum ReputationEvent {
135 PositiveInteraction { peer_id: String, magnitude: f64 },
137 NegativeInteraction { peer_id: String, magnitude: f64 },
139 EdgeAdded {
141 from: String,
142 to: String,
143 weight: f64,
144 },
145 EdgeRemoved { from: String, to: String },
147 ScoreDecayed {
149 peer_id: String,
150 old_score: f64,
151 new_score: f64,
152 },
153}
154
155#[derive(Debug, Clone)]
157pub struct GraphConfig {
158 pub trust_decay_factor: f64,
161 pub propagation_depth: u8,
163 pub propagation_damping: f64,
165 pub min_edge_weight: f64,
167 pub max_peers: usize,
169 pub combination_weight_direct: f64,
171 pub combination_weight_propagated: f64,
173}
174
175impl Default for GraphConfig {
176 fn default() -> Self {
177 Self {
178 trust_decay_factor: 0.99,
179 propagation_depth: 3,
180 propagation_damping: 0.5,
181 min_edge_weight: 0.01,
182 max_peers: 10_000,
183 combination_weight_direct: 0.6,
184 combination_weight_propagated: 0.4,
185 }
186 }
187}
188
189#[derive(Debug, Clone)]
191pub struct GraphStats {
192 pub peer_count: usize,
194 pub edge_count: usize,
196 pub avg_out_degree: f64,
198 pub avg_reputation: f64,
200 pub top_peers: Vec<String>,
202 pub isolated_peers: usize,
204}
205
206#[derive(Debug, Clone)]
211struct PeerState {
212 peer_id: String,
213 direct_score: f64,
214 propagated_score: f64,
215 interaction_count: u64,
217}
218
219impl PeerState {
220 fn new(peer_id: String) -> Self {
221 Self {
222 peer_id,
223 direct_score: 0.5, propagated_score: 0.0,
225 interaction_count: 0,
226 }
227 }
228}
229
230pub struct PeerReputationGraph {
240 config: GraphConfig,
241 peers: HashMap<String, PeerState>,
243 edges: HashMap<(String, String), TrustEdge>,
245 out_adj: HashMap<String, HashSet<String>>,
247 in_adj: HashMap<String, HashSet<String>>,
249 clock: u64,
251 rng_state: u64,
253}
254
255impl PeerReputationGraph {
256 pub fn new(config: GraphConfig) -> Self {
258 Self {
259 config,
260 peers: HashMap::new(),
261 edges: HashMap::new(),
262 out_adj: HashMap::new(),
263 in_adj: HashMap::new(),
264 clock: 1,
265 rng_state: 0xdeadbeef_cafebabe,
266 }
267 }
268
269 pub fn add_peer(&mut self, peer_id: String) -> Result<(), GraphError> {
278 if self.peers.contains_key(&peer_id) {
279 return Ok(());
280 }
281 if self.peers.len() >= self.config.max_peers {
282 return Err(GraphError::MaxPeersExceeded);
283 }
284 self.out_adj.entry(peer_id.clone()).or_default();
285 self.in_adj.entry(peer_id.clone()).or_default();
286 self.peers.insert(peer_id.clone(), PeerState::new(peer_id));
287 self.tick();
288 Ok(())
289 }
290
291 pub fn remove_peer(&mut self, peer_id: &str) -> Result<(), GraphError> {
293 if !self.peers.contains_key(peer_id) {
294 return Err(GraphError::PeerNotFound(peer_id.to_string()));
295 }
296
297 let out_peers: Vec<String> = self
299 .out_adj
300 .get(peer_id)
301 .map(|s| s.iter().cloned().collect())
302 .unwrap_or_default();
303 let in_peers: Vec<String> = self
304 .in_adj
305 .get(peer_id)
306 .map(|s| s.iter().cloned().collect())
307 .unwrap_or_default();
308
309 for to in &out_peers {
310 self.edges.remove(&(peer_id.to_string(), to.clone()));
311 if let Some(set) = self.in_adj.get_mut(to) {
312 set.remove(peer_id);
313 }
314 }
315 for from in &in_peers {
316 self.edges.remove(&(from.clone(), peer_id.to_string()));
317 if let Some(set) = self.out_adj.get_mut(from) {
318 set.remove(peer_id);
319 }
320 }
321
322 self.out_adj.remove(peer_id);
323 self.in_adj.remove(peer_id);
324 self.peers.remove(peer_id);
325 self.tick();
326 Ok(())
327 }
328
329 pub fn add_edge(&mut self, from: &str, to: &str, weight: f64) -> Result<(), GraphError> {
341 if from == to {
342 return Err(GraphError::SelfLoop(from.to_string()));
343 }
344 if !(0.0..=1.0).contains(&weight) {
345 return Err(GraphError::InvalidWeight(weight));
346 }
347 if !self.peers.contains_key(from) {
348 return Err(GraphError::PeerNotFound(from.to_string()));
349 }
350 if !self.peers.contains_key(to) {
351 return Err(GraphError::PeerNotFound(to.to_string()));
352 }
353
354 let now = self.tick();
355 let key = (from.to_string(), to.to_string());
356
357 if let Some(edge) = self.edges.get_mut(&key) {
358 edge.weight = weight;
359 edge.updated_at = now;
360 edge.interaction_count += 1;
361 } else {
362 self.edges.insert(
363 key.clone(),
364 TrustEdge {
365 from_peer: from.to_string(),
366 to_peer: to.to_string(),
367 weight,
368 created_at: now,
369 updated_at: now,
370 interaction_count: 1,
371 },
372 );
373 self.out_adj
374 .entry(from.to_string())
375 .or_default()
376 .insert(to.to_string());
377 self.in_adj
378 .entry(to.to_string())
379 .or_default()
380 .insert(from.to_string());
381 }
382 Ok(())
383 }
384
385 pub fn remove_edge(&mut self, from: &str, to: &str) -> Result<(), GraphError> {
387 let key = (from.to_string(), to.to_string());
388 if self.edges.remove(&key).is_none() {
389 return Err(GraphError::EdgeNotFound {
390 from: from.to_string(),
391 to: to.to_string(),
392 });
393 }
394 if let Some(set) = self.out_adj.get_mut(from) {
395 set.remove(to);
396 }
397 if let Some(set) = self.in_adj.get_mut(to) {
398 set.remove(from);
399 }
400 self.tick();
401 Ok(())
402 }
403
404 pub fn record_interaction(
415 &mut self,
416 peer_id: &str,
417 positive: bool,
418 magnitude: f64,
419 ) -> Result<ReputationEvent, GraphError> {
420 const ALPHA: f64 = 0.1;
421
422 let state = self
423 .peers
424 .get_mut(peer_id)
425 .ok_or_else(|| GraphError::PeerNotFound(peer_id.to_string()))?;
426
427 let delta = if positive { magnitude } else { -magnitude };
428
429 let target = (state.direct_score + delta).clamp(0.0, 1.0);
432 let ema = state.direct_score + ALPHA * (target - state.direct_score);
433
434 state.direct_score = ema.clamp(0.0, 1.0);
435 state.interaction_count += 1;
436
437 let event = if positive {
438 ReputationEvent::PositiveInteraction {
439 peer_id: peer_id.to_string(),
440 magnitude,
441 }
442 } else {
443 ReputationEvent::NegativeInteraction {
444 peer_id: peer_id.to_string(),
445 magnitude,
446 }
447 };
448 self.tick();
449 Ok(event)
450 }
451
452 pub fn propagate_trust(&mut self) -> usize {
473 let direct_scores: HashMap<String, f64> = self
475 .peers
476 .iter()
477 .map(|(id, s)| (id.clone(), s.direct_score))
478 .collect();
479
480 let mut propagated: HashMap<String, f64> =
482 self.peers.keys().map(|id| (id.clone(), 0.0_f64)).collect();
483
484 let depth = self.config.propagation_depth as usize;
485 let damping = self.config.propagation_damping;
486
487 for (src_id, &src_direct) in &direct_scores {
489 if src_direct == 0.0 {
490 continue;
491 }
492
493 let mut queue: VecDeque<(String, f64, usize)> = VecDeque::new();
495 queue.push_back((src_id.clone(), src_direct, 0));
496
497 let mut visited: HashSet<String> = HashSet::new();
498 visited.insert(src_id.clone());
499
500 while let Some((node, trust, hop)) = queue.pop_front() {
501 if hop >= depth {
502 continue;
503 }
504
505 let neighbors: Vec<(String, f64)> = self
506 .out_adj
507 .get(&node)
508 .map(|set| {
509 set.iter()
510 .filter_map(|nb| {
511 let key = (node.clone(), nb.clone());
512 self.edges.get(&key).map(|e| (nb.clone(), e.weight))
513 })
514 .collect()
515 })
516 .unwrap_or_default();
517
518 for (nb, edge_weight) in neighbors {
519 if visited.contains(&nb) {
520 continue;
521 }
522 visited.insert(nb.clone());
523
524 let contribution = edge_weight * trust * damping.powi((hop + 1) as i32);
525 if let Some(acc) = propagated.get_mut(&nb) {
526 *acc += contribution;
527 }
528
529 queue.push_back((nb, trust * edge_weight, hop + 1));
530 }
531 }
532 }
533
534 let mut updated = 0_usize;
536 for (id, new_prop) in propagated {
537 if let Some(state) = self.peers.get_mut(&id) {
538 let clamped = new_prop.clamp(0.0, 1.0);
539 if (clamped - state.propagated_score).abs() > f64::EPSILON {
540 state.propagated_score = clamped;
541 updated += 1;
542 }
543 }
544 }
545 updated
546 }
547
548 pub fn decay_scores(&mut self) -> Vec<ReputationEvent> {
559 let factor = self.config.trust_decay_factor;
560 let min_weight = self.config.min_edge_weight;
561
562 let mut events = Vec::new();
563
564 for state in self.peers.values_mut() {
565 let old = state.direct_score;
566 let new = (old * factor).clamp(0.0, 1.0);
567 if (new - old).abs() > 0.001 {
568 events.push(ReputationEvent::ScoreDecayed {
569 peer_id: state.peer_id.clone(),
570 old_score: old,
571 new_score: new,
572 });
573 }
574 state.direct_score = new;
575 }
576
577 let to_prune: Vec<(String, String)> = self
579 .edges
580 .iter()
581 .filter(|(_, e)| e.weight < min_weight)
582 .map(|(k, _)| (k.0.clone(), k.1.clone()))
583 .collect();
584
585 for (from, to) in to_prune {
586 let _ = self.remove_edge(&from, &to);
588 }
589
590 self.tick();
591 events
592 }
593
594 pub fn reputation(&self, peer_id: &str) -> Result<ReputationScore, GraphError> {
604 let state = self
605 .peers
606 .get(peer_id)
607 .ok_or_else(|| GraphError::PeerNotFound(peer_id.to_string()))?;
608
609 let wd = self.config.combination_weight_direct;
610 let wp = self.config.combination_weight_propagated;
611 let total_weight = wd + wp;
612
613 let combined = if total_weight > 0.0 {
614 (wd * state.direct_score + wp * state.propagated_score) / total_weight
615 } else {
616 state.direct_score
617 };
618
619 let confidence = (state.interaction_count as f64 / 10.0).min(1.0);
620
621 let combined_clamped = combined.clamp(0.0, 1.0);
623 let percentile = self.percentile_of(peer_id, combined_clamped);
624
625 Ok(ReputationScore {
626 peer_id: peer_id.to_string(),
627 direct_score: state.direct_score,
628 propagated_score: state.propagated_score,
629 combined_score: combined_clamped,
630 confidence,
631 percentile,
632 })
633 }
634
635 pub fn top_peers(&self, n: usize) -> Vec<ReputationScore> {
637 let mut scores: Vec<ReputationScore> = self
638 .peers
639 .keys()
640 .filter_map(|id| self.reputation(id).ok())
641 .collect();
642
643 scores.sort_by(|a, b| {
644 b.combined_score
645 .partial_cmp(&a.combined_score)
646 .unwrap_or(std::cmp::Ordering::Equal)
647 });
648
649 scores.truncate(n);
650 scores
651 }
652
653 pub fn peers_trusting(&self, peer_id: &str) -> Vec<String> {
659 self.in_adj
660 .get(peer_id)
661 .map(|set| set.iter().cloned().collect())
662 .unwrap_or_default()
663 }
664
665 pub fn peers_trusted_by(&self, peer_id: &str) -> Vec<String> {
667 self.out_adj
668 .get(peer_id)
669 .map(|set| set.iter().cloned().collect())
670 .unwrap_or_default()
671 }
672
673 pub fn stats(&self) -> GraphStats {
675 let peer_count = self.peers.len();
676 let edge_count = self.edges.len();
677
678 let avg_out_degree = if peer_count > 0 {
679 edge_count as f64 / peer_count as f64
680 } else {
681 0.0
682 };
683
684 let avg_reputation = if peer_count > 0 {
685 let sum: f64 = self
686 .peers
687 .keys()
688 .filter_map(|id| self.reputation(id).ok())
689 .map(|r| r.combined_score)
690 .sum();
691 sum / peer_count as f64
692 } else {
693 0.0
694 };
695
696 let mut top = self.top_peers(5);
697 top.sort_by(|a, b| {
699 b.combined_score
700 .partial_cmp(&a.combined_score)
701 .unwrap_or(std::cmp::Ordering::Equal)
702 .then_with(|| a.peer_id.cmp(&b.peer_id))
703 });
704 let top_peers = top.into_iter().map(|r| r.peer_id).collect();
705
706 let isolated_peers = self
707 .peers
708 .keys()
709 .filter(|id| {
710 self.out_adj.get(*id).map(|s| s.is_empty()).unwrap_or(true)
711 && self.in_adj.get(*id).map(|s| s.is_empty()).unwrap_or(true)
712 })
713 .count();
714
715 GraphStats {
716 peer_count,
717 edge_count,
718 avg_out_degree,
719 avg_reputation,
720 top_peers,
721 isolated_peers,
722 }
723 }
724
725 pub fn prune_isolated(&mut self) -> usize {
730 let to_remove: Vec<String> = self
731 .peers
732 .iter()
733 .filter(|(id, state)| {
734 let out_empty = self.out_adj.get(*id).map(|s| s.is_empty()).unwrap_or(true);
735 let in_empty = self.in_adj.get(*id).map(|s| s.is_empty()).unwrap_or(true);
736 if !out_empty || !in_empty {
737 return false;
738 }
739 let wd = self.config.combination_weight_direct;
740 let wp = self.config.combination_weight_propagated;
741 let total = wd + wp;
742 let combined = if total > 0.0 {
743 (wd * state.direct_score + wp * state.propagated_score) / total
744 } else {
745 state.direct_score
746 };
747 combined < 0.1
748 })
749 .map(|(id, _)| id.clone())
750 .collect();
751
752 let count = to_remove.len();
753 for id in to_remove {
754 let _ = self.remove_peer(&id);
756 }
757 count
758 }
759
760 pub fn all_edges(&self) -> Vec<TrustEdge> {
766 self.edges.values().cloned().collect()
767 }
768
769 pub fn edge(&self, from: &str, to: &str) -> Option<&TrustEdge> {
771 self.edges.get(&(from.to_string(), to.to_string()))
772 }
773
774 pub fn contains_peer(&self, peer_id: &str) -> bool {
776 self.peers.contains_key(peer_id)
777 }
778
779 pub fn peer_count(&self) -> usize {
781 self.peers.len()
782 }
783
784 pub fn edge_count(&self) -> usize {
786 self.edges.len()
787 }
788
789 fn tick(&mut self) -> u64 {
795 self.clock += 1;
796 let _ = xorshift64(&mut self.rng_state);
798 self.clock
799 }
800
801 fn percentile_of(&self, peer_id: &str, combined: f64) -> f64 {
803 if self.peers.len() <= 1 {
804 return 0.5;
805 }
806 let below = self
807 .peers
808 .iter()
809 .filter(|(id, state)| {
810 if id.as_str() == peer_id {
811 return false;
812 }
813 let wd = self.config.combination_weight_direct;
814 let wp = self.config.combination_weight_propagated;
815 let total = wd + wp;
816 let other_combined = if total > 0.0 {
817 (wd * state.direct_score + wp * state.propagated_score) / total
818 } else {
819 state.direct_score
820 };
821 other_combined < combined
822 })
823 .count();
824 let n = self.peers.len() - 1; if n == 0 {
826 0.5
827 } else {
828 below as f64 / n as f64
829 }
830 }
831}
832
833#[cfg(test)]
838mod tests {
839 use super::*;
840
841 fn make_graph() -> PeerReputationGraph {
842 PeerReputationGraph::new(GraphConfig::default())
843 }
844
845 #[test]
848 fn test_add_peer_basic() {
849 let mut g = make_graph();
850 g.add_peer("alice".to_string())
851 .expect("test: add_peer alice should succeed");
852 assert!(g.contains_peer("alice"));
853 }
854
855 #[test]
856 fn test_add_peer_idempotent() {
857 let mut g = make_graph();
858 g.add_peer("alice".to_string())
859 .expect("test: add_peer alice should succeed");
860 g.add_peer("alice".to_string())
862 .expect("test: re-adding alice should succeed");
863 assert_eq!(g.peer_count(), 1);
864 }
865
866 #[test]
867 fn test_add_peer_max_exceeded() {
868 let mut g = PeerReputationGraph::new(GraphConfig {
869 max_peers: 2,
870 ..Default::default()
871 });
872 g.add_peer("a".to_string())
873 .expect("test: add_peer a should succeed");
874 g.add_peer("b".to_string())
875 .expect("test: add_peer b should succeed");
876 let err = g
877 .add_peer("c".to_string())
878 .expect_err("test: expected error when exceeding max peers");
879 assert_eq!(err, GraphError::MaxPeersExceeded);
880 }
881
882 #[test]
883 fn test_remove_peer_basic() {
884 let mut g = make_graph();
885 g.add_peer("alice".to_string())
886 .expect("test: add_peer alice should succeed");
887 g.remove_peer("alice")
888 .expect("test: remove_peer alice should succeed");
889 assert!(!g.contains_peer("alice"));
890 }
891
892 #[test]
893 fn test_remove_peer_not_found() {
894 let mut g = make_graph();
895 let err = g
896 .remove_peer("ghost")
897 .expect_err("test: expected error removing nonexistent peer");
898 assert!(matches!(err, GraphError::PeerNotFound(_)));
899 }
900
901 #[test]
902 fn test_remove_peer_removes_edges() {
903 let mut g = make_graph();
904 g.add_peer("a".to_string())
905 .expect("test: add_peer a should succeed");
906 g.add_peer("b".to_string())
907 .expect("test: add_peer b should succeed");
908 g.add_peer("c".to_string())
909 .expect("test: add_peer c should succeed");
910 g.add_edge("a", "b", 0.8)
911 .expect("test: add_edge a->b should succeed");
912 g.add_edge("c", "a", 0.6)
913 .expect("test: add_edge c->a should succeed");
914 g.remove_peer("a")
915 .expect("test: remove_peer a should succeed");
916 assert_eq!(g.edge_count(), 0);
917 assert!(g.peers_trusting("b").is_empty());
918 assert!(g.peers_trusted_by("c").is_empty());
919 }
920
921 #[test]
924 fn test_add_edge_basic() {
925 let mut g = make_graph();
926 g.add_peer("a".to_string())
927 .expect("test: add_peer a should succeed");
928 g.add_peer("b".to_string())
929 .expect("test: add_peer b should succeed");
930 g.add_edge("a", "b", 0.7)
931 .expect("test: add_edge a->b should succeed");
932 assert_eq!(g.edge_count(), 1);
933 assert_eq!(
934 g.edge("a", "b")
935 .expect("test: edge a->b should exist")
936 .weight,
937 0.7
938 );
939 }
940
941 #[test]
942 fn test_add_edge_update_existing() {
943 let mut g = make_graph();
944 g.add_peer("a".to_string())
945 .expect("test: add_peer a should succeed");
946 g.add_peer("b".to_string())
947 .expect("test: add_peer b should succeed");
948 g.add_edge("a", "b", 0.5)
949 .expect("test: add_edge a->b 0.5 should succeed");
950 g.add_edge("a", "b", 0.9)
951 .expect("test: update edge a->b to 0.9 should succeed");
952 assert_eq!(g.edge_count(), 1);
953 assert_eq!(
954 g.edge("a", "b")
955 .expect("test: edge a->b should exist")
956 .weight,
957 0.9
958 );
959 assert_eq!(
960 g.edge("a", "b")
961 .expect("test: edge a->b should exist")
962 .interaction_count,
963 2
964 );
965 }
966
967 #[test]
968 fn test_add_edge_self_loop() {
969 let mut g = make_graph();
970 g.add_peer("a".to_string())
971 .expect("test: add_peer a should succeed");
972 let err = g
973 .add_edge("a", "a", 0.5)
974 .expect_err("test: expected error for self-loop edge");
975 assert!(matches!(err, GraphError::SelfLoop(_)));
976 }
977
978 #[test]
979 fn test_add_edge_invalid_weight_negative() {
980 let mut g = make_graph();
981 g.add_peer("a".to_string())
982 .expect("test: add_peer a should succeed");
983 g.add_peer("b".to_string())
984 .expect("test: add_peer b should succeed");
985 let err = g
986 .add_edge("a", "b", -0.1)
987 .expect_err("test: expected error for negative weight");
988 assert!(matches!(err, GraphError::InvalidWeight(_)));
989 }
990
991 #[test]
992 fn test_add_edge_invalid_weight_above_one() {
993 let mut g = make_graph();
994 g.add_peer("a".to_string())
995 .expect("test: add_peer a should succeed");
996 g.add_peer("b".to_string())
997 .expect("test: add_peer b should succeed");
998 let err = g
999 .add_edge("a", "b", 1.5)
1000 .expect_err("test: expected error for weight above 1.0");
1001 assert!(matches!(err, GraphError::InvalidWeight(_)));
1002 }
1003
1004 #[test]
1005 fn test_add_edge_peer_not_found() {
1006 let mut g = make_graph();
1007 g.add_peer("a".to_string())
1008 .expect("test: add_peer a should succeed");
1009 let err = g
1010 .add_edge("a", "ghost", 0.5)
1011 .expect_err("test: expected error for nonexistent target peer");
1012 assert!(matches!(err, GraphError::PeerNotFound(_)));
1013 }
1014
1015 #[test]
1016 fn test_remove_edge_basic() {
1017 let mut g = make_graph();
1018 g.add_peer("a".to_string())
1019 .expect("test: add_peer a should succeed");
1020 g.add_peer("b".to_string())
1021 .expect("test: add_peer b should succeed");
1022 g.add_edge("a", "b", 0.5)
1023 .expect("test: add_edge a->b should succeed");
1024 g.remove_edge("a", "b")
1025 .expect("test: remove_edge a->b should succeed");
1026 assert_eq!(g.edge_count(), 0);
1027 }
1028
1029 #[test]
1030 fn test_remove_edge_not_found() {
1031 let mut g = make_graph();
1032 g.add_peer("a".to_string())
1033 .expect("test: add_peer a should succeed");
1034 g.add_peer("b".to_string())
1035 .expect("test: add_peer b should succeed");
1036 let err = g
1037 .remove_edge("a", "b")
1038 .expect_err("test: expected error removing nonexistent edge");
1039 assert!(matches!(err, GraphError::EdgeNotFound { .. }));
1040 }
1041
1042 #[test]
1045 fn test_positive_interaction_increases_score() {
1046 let mut g = make_graph();
1047 g.add_peer("a".to_string())
1048 .expect("test: add_peer a should succeed");
1049 let initial = g
1050 .reputation("a")
1051 .expect("test: reputation for a should exist")
1052 .direct_score;
1053 let event = g
1054 .record_interaction("a", true, 1.0)
1055 .expect("test: record positive interaction for a should succeed");
1056 let after = g
1057 .reputation("a")
1058 .expect("test: reputation for a should exist")
1059 .direct_score;
1060 assert!(after >= initial);
1061 assert!(matches!(event, ReputationEvent::PositiveInteraction { .. }));
1062 }
1063
1064 #[test]
1065 fn test_negative_interaction_decreases_score() {
1066 let mut g = make_graph();
1067 g.add_peer("a".to_string())
1068 .expect("test: add_peer a should succeed");
1069 let initial = g
1070 .reputation("a")
1071 .expect("test: reputation for a should exist")
1072 .direct_score;
1073 let event = g
1074 .record_interaction("a", false, 1.0)
1075 .expect("test: record negative interaction for a should succeed");
1076 let after = g
1077 .reputation("a")
1078 .expect("test: reputation for a should exist")
1079 .direct_score;
1080 assert!(after <= initial);
1081 assert!(matches!(event, ReputationEvent::NegativeInteraction { .. }));
1082 }
1083
1084 #[test]
1085 fn test_interaction_clamps_score() {
1086 let mut g = make_graph();
1087 g.add_peer("a".to_string())
1088 .expect("test: add_peer a should succeed");
1089 for _ in 0..100 {
1091 g.record_interaction("a", true, 10.0)
1092 .expect("test: record large positive interaction should succeed");
1093 }
1094 let score = g
1095 .reputation("a")
1096 .expect("test: reputation for a should exist")
1097 .direct_score;
1098 assert!(score <= 1.0);
1099 for _ in 0..100 {
1101 g.record_interaction("a", false, 10.0)
1102 .expect("test: record large negative interaction should succeed");
1103 }
1104 let score = g
1105 .reputation("a")
1106 .expect("test: reputation for a should exist")
1107 .direct_score;
1108 assert!(score >= 0.0);
1109 }
1110
1111 #[test]
1112 fn test_interaction_peer_not_found() {
1113 let mut g = make_graph();
1114 let err = g
1115 .record_interaction("ghost", true, 0.5)
1116 .expect_err("test: expected error for nonexistent peer interaction");
1117 assert!(matches!(err, GraphError::PeerNotFound(_)));
1118 }
1119
1120 #[test]
1121 fn test_interaction_count_increments() {
1122 let mut g = make_graph();
1123 g.add_peer("a".to_string())
1124 .expect("test: add_peer a should succeed");
1125 g.record_interaction("a", true, 0.5)
1126 .expect("test: record positive interaction should succeed");
1127 g.record_interaction("a", false, 0.2)
1128 .expect("test: record negative interaction should succeed");
1129 let score = g
1130 .reputation("a")
1131 .expect("test: reputation for a should exist");
1132 assert!(score.confidence > 0.0);
1133 }
1134
1135 #[test]
1138 fn test_propagate_trust_basic() {
1139 let mut g = make_graph();
1140 g.add_peer("a".to_string())
1141 .expect("test: add_peer a should succeed");
1142 g.add_peer("b".to_string())
1143 .expect("test: add_peer b should succeed");
1144 g.add_edge("a", "b", 1.0)
1145 .expect("test: add_edge a->b should succeed");
1146 for _ in 0..50 {
1148 g.record_interaction("a", true, 1.0)
1149 .expect("test: record positive interaction for a should succeed");
1150 }
1151 let updated = g.propagate_trust();
1152 assert!(updated > 0);
1153 let b_score = g
1154 .reputation("b")
1155 .expect("test: reputation for b should exist");
1156 assert!(b_score.propagated_score > 0.0);
1157 }
1158
1159 #[test]
1160 fn test_propagate_trust_no_edges() {
1161 let mut g = make_graph();
1162 g.add_peer("a".to_string())
1163 .expect("test: add_peer a should succeed");
1164 g.add_peer("b".to_string())
1165 .expect("test: add_peer b should succeed");
1166 g.propagate_trust();
1168 assert_eq!(
1169 g.reputation("b")
1170 .expect("test: reputation for b should exist")
1171 .propagated_score,
1172 0.0
1173 );
1174 }
1175
1176 #[test]
1177 fn test_propagate_trust_depth_limit() {
1178 let mut g = PeerReputationGraph::new(GraphConfig {
1184 propagation_depth: 1,
1185 propagation_damping: 0.5,
1186 ..Default::default()
1187 });
1188 g.add_peer("a".to_string())
1189 .expect("test: add_peer a should succeed");
1190 g.add_peer("b".to_string())
1191 .expect("test: add_peer b should succeed");
1192 g.add_peer("c".to_string())
1193 .expect("test: add_peer c should succeed");
1194 g.add_edge("a", "b", 1.0)
1195 .expect("test: add_edge a->b should succeed");
1196 g.add_edge("b", "c", 1.0)
1197 .expect("test: add_edge b->c should succeed");
1198 for _ in 0..50 {
1199 g.record_interaction("a", true, 1.0)
1200 .expect("test: record positive interaction for a should succeed");
1201 }
1202 g.propagate_trust();
1203 assert!(
1205 g.reputation("b")
1206 .expect("test: reputation for b should exist")
1207 .propagated_score
1208 > 0.0
1209 );
1210 let b_prop = g
1215 .reputation("b")
1216 .expect("test: reputation for b should exist")
1217 .propagated_score;
1218 let c_prop = g
1219 .reputation("c")
1220 .expect("test: reputation for c should exist")
1221 .propagated_score;
1222 assert!(b_prop >= c_prop);
1224 }
1225
1226 #[test]
1227 fn test_propagate_trust_multi_hop() {
1228 let mut g = PeerReputationGraph::new(GraphConfig {
1230 propagation_depth: 2,
1231 propagation_damping: 0.5,
1232 ..Default::default()
1233 });
1234 g.add_peer("a".to_string())
1235 .expect("test: add_peer a should succeed");
1236 g.add_peer("b".to_string())
1237 .expect("test: add_peer b should succeed");
1238 g.add_peer("c".to_string())
1239 .expect("test: add_peer c should succeed");
1240 g.add_edge("a", "b", 1.0)
1241 .expect("test: add_edge a->b should succeed");
1242 g.add_edge("b", "c", 1.0)
1243 .expect("test: add_edge b->c should succeed");
1244 for _ in 0..50 {
1245 g.record_interaction("a", true, 1.0)
1246 .expect("test: record positive interaction for a should succeed");
1247 g.record_interaction("b", true, 1.0)
1248 .expect("test: record positive interaction for b should succeed");
1249 }
1250 g.propagate_trust();
1251 assert!(
1252 g.reputation("c")
1253 .expect("test: reputation for c should exist")
1254 .propagated_score
1255 > 0.0
1256 );
1257 }
1258
1259 #[test]
1260 fn test_propagate_returns_count() {
1261 let mut g = make_graph();
1262 g.add_peer("a".to_string())
1263 .expect("test: add_peer a should succeed");
1264 g.add_peer("b".to_string())
1265 .expect("test: add_peer b should succeed");
1266 g.add_edge("a", "b", 1.0)
1267 .expect("test: add_edge a->b should succeed");
1268 for _ in 0..20 {
1269 g.record_interaction("a", true, 1.0)
1270 .expect("test: record positive interaction for a should succeed");
1271 }
1272 let count = g.propagate_trust();
1273 assert!(count >= 1);
1274 }
1275
1276 #[test]
1279 fn test_decay_scores_reduces_score() {
1280 let mut g = make_graph();
1281 g.add_peer("a".to_string())
1282 .expect("test: add_peer a should succeed");
1283 for _ in 0..50 {
1284 g.record_interaction("a", true, 1.0)
1285 .expect("test: record positive interaction for a should succeed");
1286 }
1287 let before = g
1288 .reputation("a")
1289 .expect("test: reputation for a should exist")
1290 .direct_score;
1291 g.decay_scores();
1292 let after = g
1293 .reputation("a")
1294 .expect("test: reputation for a after decay should exist")
1295 .direct_score;
1296 assert!(after <= before);
1297 }
1298
1299 #[test]
1300 fn test_decay_emits_events() {
1301 let mut g = PeerReputationGraph::new(GraphConfig {
1302 trust_decay_factor: 0.5, ..Default::default()
1304 });
1305 g.add_peer("a".to_string())
1306 .expect("test: add_peer a should succeed");
1307 for _ in 0..50 {
1308 g.record_interaction("a", true, 1.0)
1309 .expect("test: record positive interaction for a should succeed");
1310 }
1311 let events = g.decay_scores();
1312 assert!(!events.is_empty());
1313 assert!(events
1314 .iter()
1315 .any(|e| matches!(e, ReputationEvent::ScoreDecayed { .. })));
1316 }
1317
1318 #[test]
1319 fn test_decay_prunes_weak_edges() {
1320 let mut g = PeerReputationGraph::new(GraphConfig {
1321 min_edge_weight: 0.5,
1322 ..Default::default()
1323 });
1324 g.add_peer("a".to_string())
1325 .expect("test: add_peer a should succeed");
1326 g.add_peer("b".to_string())
1327 .expect("test: add_peer b should succeed");
1328 g.add_edge("a", "b", 0.3)
1329 .expect("test: add weak edge a->b should succeed"); g.decay_scores();
1331 assert_eq!(g.edge_count(), 0, "weak edge should be pruned");
1332 }
1333
1334 #[test]
1335 fn test_decay_keeps_strong_edges() {
1336 let mut g = PeerReputationGraph::new(GraphConfig {
1337 min_edge_weight: 0.1,
1338 ..Default::default()
1339 });
1340 g.add_peer("a".to_string())
1341 .expect("test: add_peer a should succeed");
1342 g.add_peer("b".to_string())
1343 .expect("test: add_peer b should succeed");
1344 g.add_edge("a", "b", 0.9)
1345 .expect("test: add strong edge a->b should succeed"); g.decay_scores();
1347 assert_eq!(g.edge_count(), 1, "strong edge should remain");
1348 }
1349
1350 #[test]
1353 fn test_reputation_not_found() {
1354 let g = make_graph();
1355 let err = g
1356 .reputation("ghost")
1357 .expect_err("test: expected error for nonexistent peer reputation");
1358 assert!(matches!(err, GraphError::PeerNotFound(_)));
1359 }
1360
1361 #[test]
1362 fn test_reputation_combined_score_range() {
1363 let mut g = make_graph();
1364 g.add_peer("a".to_string())
1365 .expect("test: add_peer a should succeed");
1366 for _ in 0..20 {
1367 g.record_interaction("a", true, 0.5)
1368 .expect("test: record positive interaction for a should succeed");
1369 }
1370 let r = g
1371 .reputation("a")
1372 .expect("test: reputation for a should exist");
1373 assert!((0.0..=1.0).contains(&r.combined_score));
1374 }
1375
1376 #[test]
1377 fn test_reputation_confidence_grows() {
1378 let mut g = make_graph();
1379 g.add_peer("a".to_string())
1380 .expect("test: add_peer a should succeed");
1381 let c0 = g
1382 .reputation("a")
1383 .expect("test: reputation for a should exist")
1384 .confidence;
1385 for _ in 0..10 {
1386 g.record_interaction("a", true, 0.5)
1387 .expect("test: record positive interaction should succeed");
1388 }
1389 let c10 = g
1390 .reputation("a")
1391 .expect("test: reputation for a should exist")
1392 .confidence;
1393 assert!(c10 > c0);
1394 }
1395
1396 #[test]
1397 fn test_reputation_confidence_caps_at_one() {
1398 let mut g = make_graph();
1399 g.add_peer("a".to_string())
1400 .expect("test: add_peer a should succeed");
1401 for _ in 0..200 {
1402 g.record_interaction("a", true, 0.3)
1403 .expect("test: record positive interaction should succeed");
1404 }
1405 let c = g
1406 .reputation("a")
1407 .expect("test: reputation for a should exist")
1408 .confidence;
1409 assert!(c <= 1.0);
1410 }
1411
1412 #[test]
1415 fn test_top_peers_empty() {
1416 let g = make_graph();
1417 assert!(g.top_peers(5).is_empty());
1418 }
1419
1420 #[test]
1421 fn test_top_peers_order() {
1422 let mut g = make_graph();
1423 for name in &["a", "b", "c"] {
1424 g.add_peer(name.to_string())
1425 .expect("test: add_peer should succeed");
1426 }
1427 for _ in 0..20 {
1428 g.record_interaction("a", true, 1.0)
1429 .expect("test: record positive interaction for a should succeed");
1430 }
1431 let top = g.top_peers(3);
1432 assert!(!top.is_empty());
1433 assert_eq!(top[0].peer_id, "a");
1434 }
1435
1436 #[test]
1437 fn test_top_peers_limit() {
1438 let mut g = make_graph();
1439 for i in 0..10 {
1440 g.add_peer(format!("peer{i}"))
1441 .expect("test: add_peer should succeed");
1442 }
1443 assert!(g.top_peers(5).len() <= 5);
1444 }
1445
1446 #[test]
1447 fn test_top_peers_fewer_than_n() {
1448 let mut g = make_graph();
1449 g.add_peer("a".to_string())
1450 .expect("test: add_peer a should succeed");
1451 let top = g.top_peers(10);
1452 assert_eq!(top.len(), 1);
1453 }
1454
1455 #[test]
1458 fn test_peers_trusting() {
1459 let mut g = make_graph();
1460 g.add_peer("a".to_string())
1461 .expect("test: add_peer a should succeed");
1462 g.add_peer("b".to_string())
1463 .expect("test: add_peer b should succeed");
1464 g.add_peer("c".to_string())
1465 .expect("test: add_peer c should succeed");
1466 g.add_edge("b", "a", 0.5)
1467 .expect("test: add_edge b->a should succeed");
1468 g.add_edge("c", "a", 0.7)
1469 .expect("test: add_edge c->a should succeed");
1470 let trusting = g.peers_trusting("a");
1471 assert!(trusting.contains(&"b".to_string()));
1472 assert!(trusting.contains(&"c".to_string()));
1473 }
1474
1475 #[test]
1476 fn test_peers_trusted_by() {
1477 let mut g = make_graph();
1478 g.add_peer("a".to_string())
1479 .expect("test: add_peer a should succeed");
1480 g.add_peer("b".to_string())
1481 .expect("test: add_peer b should succeed");
1482 g.add_peer("c".to_string())
1483 .expect("test: add_peer c should succeed");
1484 g.add_edge("a", "b", 0.5)
1485 .expect("test: add_edge a->b should succeed");
1486 g.add_edge("a", "c", 0.7)
1487 .expect("test: add_edge a->c should succeed");
1488 let trusted = g.peers_trusted_by("a");
1489 assert!(trusted.contains(&"b".to_string()));
1490 assert!(trusted.contains(&"c".to_string()));
1491 }
1492
1493 #[test]
1494 fn test_peers_trusting_empty() {
1495 let mut g = make_graph();
1496 g.add_peer("loner".to_string())
1497 .expect("test: add_peer loner should succeed");
1498 assert!(g.peers_trusting("loner").is_empty());
1499 }
1500
1501 #[test]
1504 fn test_stats_empty() {
1505 let g = make_graph();
1506 let s = g.stats();
1507 assert_eq!(s.peer_count, 0);
1508 assert_eq!(s.edge_count, 0);
1509 assert_eq!(s.avg_out_degree, 0.0);
1510 assert_eq!(s.isolated_peers, 0);
1511 }
1512
1513 #[test]
1514 fn test_stats_peer_and_edge_count() {
1515 let mut g = make_graph();
1516 g.add_peer("a".to_string())
1517 .expect("test: add_peer a should succeed");
1518 g.add_peer("b".to_string())
1519 .expect("test: add_peer b should succeed");
1520 g.add_edge("a", "b", 0.5)
1521 .expect("test: add_edge a->b should succeed");
1522 let s = g.stats();
1523 assert_eq!(s.peer_count, 2);
1524 assert_eq!(s.edge_count, 1);
1525 }
1526
1527 #[test]
1528 fn test_stats_isolated_peers() {
1529 let mut g = make_graph();
1530 g.add_peer("a".to_string())
1531 .expect("test: add_peer a should succeed");
1532 g.add_peer("b".to_string())
1533 .expect("test: add_peer b should succeed");
1534 for _ in 0..50 {
1537 g.record_interaction("b", false, 1.0)
1538 .expect("test: record negative interaction for b should succeed");
1539 }
1540 let s = g.stats();
1541 assert_eq!(s.isolated_peers, 2);
1543 }
1544
1545 #[test]
1546 fn test_stats_top_peers_max_five() {
1547 let mut g = make_graph();
1548 for i in 0..10 {
1549 g.add_peer(format!("p{i}"))
1550 .expect("test: add_peer should succeed");
1551 }
1552 let s = g.stats();
1553 assert!(s.top_peers.len() <= 5);
1554 }
1555
1556 #[test]
1559 fn test_prune_isolated_removes_low_score_no_edge() {
1560 let mut g = PeerReputationGraph::new(GraphConfig {
1561 trust_decay_factor: 0.5,
1562 ..Default::default()
1563 });
1564 g.add_peer("a".to_string())
1565 .expect("test: add_peer a should succeed");
1566 g.add_peer("b".to_string())
1567 .expect("test: add_peer b should succeed");
1568 for _ in 0..50 {
1570 g.record_interaction("b", false, 1.0)
1571 .expect("test: record negative interaction for b should succeed");
1572 }
1573 let pruned = g.prune_isolated();
1575 assert!(pruned >= 1);
1576 assert!(!g.contains_peer("b"));
1577 }
1578
1579 #[test]
1580 fn test_prune_isolated_keeps_high_score() {
1581 let mut g = make_graph();
1582 g.add_peer("a".to_string())
1583 .expect("test: add_peer a should succeed");
1584 for _ in 0..50 {
1585 g.record_interaction("a", true, 1.0)
1586 .expect("test: record positive interaction for a should succeed");
1587 }
1588 let pruned = g.prune_isolated();
1589 assert_eq!(pruned, 0);
1591 assert!(g.contains_peer("a"));
1592 }
1593
1594 #[test]
1595 fn test_prune_isolated_keeps_connected_peers() {
1596 let mut g = make_graph();
1597 g.add_peer("a".to_string())
1598 .expect("test: add_peer a should succeed");
1599 g.add_peer("b".to_string())
1600 .expect("test: add_peer b should succeed");
1601 g.add_edge("a", "b", 0.8)
1602 .expect("test: add_edge a->b should succeed");
1603 for _ in 0..100 {
1605 g.record_interaction("a", false, 1.0)
1606 .expect("test: record negative interaction for a should succeed");
1607 }
1608 let pruned = g.prune_isolated();
1609 assert_eq!(pruned, 0);
1610 assert!(g.contains_peer("a"));
1611 }
1612
1613 #[test]
1616 fn test_graph_error_display_peer_not_found() {
1617 let err = GraphError::PeerNotFound("x".to_string());
1618 assert!(err.to_string().contains("x"));
1619 }
1620
1621 #[test]
1622 fn test_graph_error_display_edge_not_found() {
1623 let err = GraphError::EdgeNotFound {
1624 from: "a".to_string(),
1625 to: "b".to_string(),
1626 };
1627 let s = err.to_string();
1628 assert!(s.contains("a"));
1629 assert!(s.contains("b"));
1630 }
1631
1632 #[test]
1633 fn test_graph_error_display_self_loop() {
1634 let err = GraphError::SelfLoop("a".to_string());
1635 assert!(err.to_string().contains("a"));
1636 }
1637
1638 #[test]
1639 fn test_graph_error_display_invalid_weight() {
1640 let err = GraphError::InvalidWeight(2.5);
1641 assert!(err.to_string().contains("2.5"));
1642 }
1643
1644 #[test]
1645 fn test_graph_error_max_peers() {
1646 assert_eq!(
1647 GraphError::MaxPeersExceeded.to_string(),
1648 "maximum peer count exceeded"
1649 );
1650 }
1651
1652 #[test]
1655 fn test_edge_interaction_count() {
1656 let mut g = make_graph();
1657 g.add_peer("a".to_string())
1658 .expect("test: add_peer a should succeed");
1659 g.add_peer("b".to_string())
1660 .expect("test: add_peer b should succeed");
1661 g.add_edge("a", "b", 0.5)
1662 .expect("test: add_edge a->b 0.5 should succeed");
1663 g.add_edge("a", "b", 0.6)
1664 .expect("test: update edge a->b to 0.6 should succeed");
1665 g.add_edge("a", "b", 0.7)
1666 .expect("test: update edge a->b to 0.7 should succeed");
1667 assert_eq!(
1668 g.edge("a", "b")
1669 .expect("test: edge a->b should exist")
1670 .interaction_count,
1671 3
1672 );
1673 }
1674
1675 #[test]
1678 fn test_graph_config_defaults() {
1679 let cfg = GraphConfig::default();
1680 assert!((cfg.trust_decay_factor - 0.99).abs() < 1e-9);
1681 assert_eq!(cfg.propagation_depth, 3);
1682 assert!((cfg.propagation_damping - 0.5).abs() < 1e-9);
1683 assert_eq!(cfg.max_peers, 10_000);
1684 }
1685
1686 #[test]
1689 fn test_xorshift64_produces_nonzero() {
1690 let mut state = 12345u64;
1691 let v = xorshift64(&mut state);
1692 assert_ne!(v, 0);
1693 assert_ne!(state, 12345);
1694 }
1695
1696 #[test]
1697 fn test_xorshift64_sequence_unique() {
1698 let mut state = 0xdeadbeef_u64;
1699 let a = xorshift64(&mut state);
1700 let b = xorshift64(&mut state);
1701 let c = xorshift64(&mut state);
1702 assert_ne!(a, b);
1704 assert_ne!(b, c);
1705 }
1706
1707 #[test]
1710 fn test_combined_score_weights() {
1711 let cfg = GraphConfig {
1712 combination_weight_direct: 1.0,
1713 combination_weight_propagated: 0.0,
1714 ..Default::default()
1715 };
1716 let mut g = PeerReputationGraph::new(cfg);
1717 g.add_peer("a".to_string())
1718 .expect("test: add_peer a should succeed");
1719 for _ in 0..50 {
1720 g.record_interaction("a", true, 1.0)
1721 .expect("test: record positive interaction for a should succeed");
1722 }
1723 g.propagate_trust();
1724 let r = g
1725 .reputation("a")
1726 .expect("test: reputation for a should exist");
1727 assert!((r.combined_score - r.direct_score).abs() < 1e-9);
1729 }
1730
1731 #[test]
1734 fn test_percentile_single_peer() {
1735 let mut g = make_graph();
1736 g.add_peer("a".to_string())
1737 .expect("test: add_peer a should succeed");
1738 let r = g
1739 .reputation("a")
1740 .expect("test: reputation for a should exist");
1741 assert_eq!(r.percentile, 0.5);
1742 }
1743
1744 #[test]
1745 fn test_percentile_ordering() {
1746 let mut g = make_graph();
1747 g.add_peer("low".to_string())
1748 .expect("test: add_peer low should succeed");
1749 g.add_peer("high".to_string())
1750 .expect("test: add_peer high should succeed");
1751 for _ in 0..50 {
1753 g.record_interaction("high", true, 1.0)
1754 .expect("test: record positive interaction for high should succeed");
1755 g.record_interaction("low", false, 1.0)
1756 .expect("test: record negative interaction for low should succeed");
1757 }
1758 let low_pct = g
1759 .reputation("low")
1760 .expect("test: reputation for low should exist")
1761 .percentile;
1762 let high_pct = g
1763 .reputation("high")
1764 .expect("test: reputation for high should exist")
1765 .percentile;
1766 assert!(high_pct > low_pct);
1767 }
1768
1769 #[test]
1772 fn test_all_edges() {
1773 let mut g = make_graph();
1774 g.add_peer("a".to_string())
1775 .expect("test: add_peer a should succeed");
1776 g.add_peer("b".to_string())
1777 .expect("test: add_peer b should succeed");
1778 g.add_peer("c".to_string())
1779 .expect("test: add_peer c should succeed");
1780 g.add_edge("a", "b", 0.5)
1781 .expect("test: add_edge a->b should succeed");
1782 g.add_edge("b", "c", 0.3)
1783 .expect("test: add_edge b->c should succeed");
1784 let edges = g.all_edges();
1785 assert_eq!(edges.len(), 2);
1786 }
1787
1788 #[test]
1791 fn test_propagation_damping_reduces_with_hops() {
1792 let cfg = GraphConfig {
1797 propagation_depth: 3,
1798 propagation_damping: 0.5,
1799 ..Default::default()
1800 };
1801 let mut g = PeerReputationGraph::new(cfg);
1802 g.add_peer("a".to_string())
1803 .expect("test: add_peer a should succeed");
1804 g.add_peer("b".to_string())
1805 .expect("test: add_peer b should succeed");
1806 g.add_peer("c".to_string())
1807 .expect("test: add_peer c should succeed");
1808 g.add_peer("d".to_string())
1809 .expect("test: add_peer d should succeed");
1810 g.add_edge("a", "b", 1.0)
1811 .expect("test: add_edge a->b should succeed");
1812 g.add_edge("b", "c", 1.0)
1813 .expect("test: add_edge b->c should succeed");
1814 g.add_edge("c", "d", 1.0)
1815 .expect("test: add_edge c->d should succeed");
1816 for _ in 0..100 {
1818 g.record_interaction("a", true, 1.0)
1819 .expect("test: record positive interaction for a should succeed");
1820 }
1821 for _ in 0..100 {
1823 g.record_interaction("b", false, 1.0)
1824 .expect("test: record negative interaction for b should succeed");
1825 g.record_interaction("c", false, 1.0)
1826 .expect("test: record negative interaction for c should succeed");
1827 g.record_interaction("d", false, 1.0)
1828 .expect("test: record negative interaction for d should succeed");
1829 }
1830 g.propagate_trust();
1831 let b_prop = g
1834 .reputation("b")
1835 .expect("test: reputation for b should exist")
1836 .propagated_score;
1837 let d_prop = g
1838 .reputation("d")
1839 .expect("test: reputation for d should exist")
1840 .propagated_score;
1841 assert!(b_prop > d_prop, "b_prop={b_prop}, d_prop={d_prop}");
1842 }
1843}