1use crate::constants::*;
8use crate::relay::{RelayInfo, RelayManager, RelayRole, RelayTransition};
9use offline_protocol_core::Message;
10use std::collections::hash_map::DefaultHasher;
11use std::collections::HashMap;
12use std::hash::{Hash, Hasher};
13use std::time::{Duration, Instant};
14
15#[derive(Debug, Clone)]
17pub struct NeighborInfo {
18 pub peer_id: String,
20
21 pub rssi: i16,
23
24 pub hops_to_destination: Option<u8>,
26
27 pub link_quality: u8,
29
30 pub relay_info: Option<RelayInfo>,
32}
33
34#[derive(Debug, Clone)]
36pub struct GossipConfig {
37 pub enabled: bool,
39
40 pub target_fanout: usize,
43
44 pub min_probability: f32,
47
48 pub small_network_threshold: usize,
51
52 pub priority_bypass: bool,
54}
55
56impl Default for GossipConfig {
57 fn default() -> Self {
58 Self {
59 enabled: true,
60 target_fanout: 4,
61 min_probability: 0.15,
62 small_network_threshold: 10,
63 priority_bypass: true,
64 }
65 }
66}
67
68#[derive(Debug, Clone)]
70pub struct GradientRoutingConfig {
71 pub enabled: bool,
73 pub max_routes_per_destination: usize,
75 pub route_ttl_secs: u64,
77 pub max_routing_table_size: usize,
79}
80
81impl Default for GradientRoutingConfig {
82 fn default() -> Self {
83 Self {
84 enabled: true,
85 max_routes_per_destination: 3,
86 route_ttl_secs: 300, max_routing_table_size: 1000,
88 }
89 }
90}
91
92#[derive(Debug, Clone)]
96pub struct RouteEntry {
97 pub next_hop: String,
99 pub hop_count: u8,
101 pub last_seen: Instant,
103 pub quality: f32,
105 pub sequence_number: u32,
108}
109
110#[derive(Debug)]
113pub struct GradientRoutingTable {
114 config: GradientRoutingConfig,
115 routes: HashMap<String, Vec<RouteEntry>>,
117 neighbor_destinations: HashMap<String, Vec<String>>,
119}
120
121impl GradientRoutingTable {
122 pub fn new() -> Self {
124 Self::with_config(GradientRoutingConfig::default())
125 }
126
127 pub fn with_config(config: GradientRoutingConfig) -> Self {
129 Self {
130 config,
131 routes: HashMap::new(),
132 neighbor_destinations: HashMap::new(),
133 }
134 }
135
136 pub fn learn_route(
140 &mut self,
141 destination: &str,
142 next_hop: &str,
143 hop_count: u8,
144 quality: f32,
145 sequence_number: u32,
146 ) {
147 if !self.config.enabled {
148 return;
149 }
150
151 let now = Instant::now();
152
153 let routes = self.routes.entry(destination.to_string()).or_default();
155
156 if let Some(existing) = routes.iter_mut().find(|r| r.next_hop == next_hop) {
158 let accept = seq_is_newer(sequence_number, existing.sequence_number)
160 || (sequence_number == existing.sequence_number && hop_count <= existing.hop_count);
161 if accept {
162 existing.hop_count = hop_count;
163 existing.last_seen = now;
164 existing.quality = quality;
165 existing.sequence_number = sequence_number;
166 }
167 return;
168 }
169
170 if routes.len() >= self.config.max_routes_per_destination {
172 routes.sort_by(|a, b| {
174 seq_cmp(b.sequence_number, a.sequence_number)
175 .then_with(|| {
176 b.quality
177 .partial_cmp(&a.quality)
178 .unwrap_or(std::cmp::Ordering::Equal)
179 })
180 .then_with(|| a.hop_count.cmp(&b.hop_count))
181 });
182 if let Some(evicted) = routes.pop() {
183 if let Some(dests) = self.neighbor_destinations.get_mut(&evicted.next_hop) {
185 dests.retain(|d| d != destination);
186 if dests.is_empty() {
187 self.neighbor_destinations.remove(&evicted.next_hop);
188 }
189 }
190 }
191 }
192
193 routes.push(RouteEntry {
194 next_hop: next_hop.to_string(),
195 hop_count,
196 last_seen: now,
197 quality,
198 sequence_number,
199 });
200
201 let dests = self
203 .neighbor_destinations
204 .entry(next_hop.to_string())
205 .or_default();
206 if !dests.contains(&destination.to_string()) {
207 dests.push(destination.to_string());
208 }
209
210 self.enforce_size_limit();
212 }
213
214 pub fn get_route(&self, destination: &str) -> Option<&RouteEntry> {
216 if !self.config.enabled {
217 return None;
218 }
219
220 let routes = self.routes.get(destination)?;
221 let ttl = Duration::from_secs(self.config.route_ttl_secs);
222 let now = Instant::now();
223
224 routes
227 .iter()
228 .filter(|r| now.duration_since(r.last_seen) < ttl)
229 .max_by(|a, b| {
230 seq_cmp(a.sequence_number, b.sequence_number)
231 .then_with(|| {
232 a.quality
233 .partial_cmp(&b.quality)
234 .unwrap_or(std::cmp::Ordering::Equal)
235 })
236 .then_with(|| b.hop_count.cmp(&a.hop_count))
237 })
238 }
239
240 pub fn get_routes(&self, destination: &str) -> Vec<&RouteEntry> {
242 if !self.config.enabled {
243 return Vec::new();
244 }
245
246 let Some(routes) = self.routes.get(destination) else {
247 return Vec::new();
248 };
249
250 let ttl = Duration::from_secs(self.config.route_ttl_secs);
251 let now = Instant::now();
252
253 routes
254 .iter()
255 .filter(|r| now.duration_since(r.last_seen) < ttl)
256 .collect()
257 }
258
259 pub fn has_route(&self, destination: &str) -> bool {
261 self.get_route(destination).is_some()
262 }
263
264 pub fn remove_neighbor(&mut self, neighbor: &str) {
266 for routes in self.routes.values_mut() {
268 routes.retain(|r| r.next_hop != neighbor);
269 }
270
271 self.routes.retain(|_, v| !v.is_empty());
273
274 self.neighbor_destinations.remove(neighbor);
276 }
277
278 pub fn cleanup_expired(&mut self) {
280 let ttl = Duration::from_secs(self.config.route_ttl_secs);
281 let now = Instant::now();
282
283 for routes in self.routes.values_mut() {
284 routes.retain(|r| now.duration_since(r.last_seen) < ttl);
285 }
286
287 self.routes.retain(|_, v| !v.is_empty());
288
289 self.neighbor_destinations.clear();
291 for (dest, routes) in &self.routes {
292 for route in routes {
293 self.neighbor_destinations
294 .entry(route.next_hop.clone())
295 .or_default()
296 .push(dest.clone());
297 }
298 }
299 }
300
301 pub fn destination_count(&self) -> usize {
303 self.routes.len()
304 }
305
306 pub fn route_count(&self) -> usize {
308 self.routes.values().map(|v| v.len()).sum()
309 }
310
311 fn enforce_size_limit(&mut self) {
316 let route_ttl_secs = self.config.route_ttl_secs as f64;
317 while self.route_count() > self.config.max_routing_table_size {
318 let worst = self
320 .routes
321 .iter()
322 .flat_map(|(dest, routes)| routes.iter().map(move |r| (dest.clone(), r)))
323 .min_by(|(_, a), (_, b)| {
324 let score_a = composite_eviction_score(a, route_ttl_secs);
325 let score_b = composite_eviction_score(b, route_ttl_secs);
326 score_a
327 .partial_cmp(&score_b)
328 .unwrap_or(std::cmp::Ordering::Equal)
329 });
330
331 if let Some((dest, route)) = worst {
332 let next_hop = route.next_hop.clone();
333 if let Some(routes) = self.routes.get_mut(&dest) {
334 routes.retain(|r| r.next_hop != next_hop);
335 if routes.is_empty() {
336 self.routes.remove(&dest);
337 }
338 }
339 if let Some(dests) = self.neighbor_destinations.get_mut(&next_hop) {
341 dests.retain(|d| d != &dest);
342 if dests.is_empty() {
343 self.neighbor_destinations.remove(&next_hop);
344 }
345 }
346 } else {
347 break;
348 }
349 }
350 }
351}
352
353fn seq_is_newer(new: u32, old: u32) -> bool {
356 new != old && new.wrapping_sub(old) < (1u32 << 31)
358}
359
360fn seq_cmp(a: u32, b: u32) -> std::cmp::Ordering {
372 if a == b {
373 std::cmp::Ordering::Equal
374 } else if seq_is_newer(a, b) {
375 std::cmp::Ordering::Greater
376 } else {
377 std::cmp::Ordering::Less
378 }
379}
380
381fn composite_eviction_score(route: &RouteEntry, route_ttl_secs: f64) -> f64 {
384 let age_secs = route.last_seen.elapsed().as_secs_f64();
385 let recency_factor = 1.0 - (age_secs / route_ttl_secs).min(1.0);
386 route.quality as f64 * recency_factor
387}
388
389impl Default for GradientRoutingTable {
390 fn default() -> Self {
391 Self::new()
392 }
393}
394
395#[derive(Debug, Clone)]
397pub struct PathConfig {
398 pub forward_to_top_k: usize,
400
401 pub max_congestion_level: f32,
403
404 pub gossip: GossipConfig,
406
407 pub gradient_routing: GradientRoutingConfig,
409}
410
411impl Default for PathConfig {
412 fn default() -> Self {
413 Self {
414 forward_to_top_k: DEFAULT_FORWARD_TO_TOP_K,
415 max_congestion_level: DEFAULT_MAX_CONGESTION_LEVEL,
416 gossip: GossipConfig::default(),
417 gradient_routing: GradientRoutingConfig::default(),
418 }
419 }
420}
421
422#[derive(Debug, Clone)]
424pub struct PathScore {
425 pub signal: f32,
427
428 pub proximity: f32,
430
431 pub capacity: f32,
433
434 pub energy: f32,
436
437 pub total: f32,
439}
440
441#[derive(Debug, Clone)]
443pub struct ForwardingDecision {
444 pub peers: Vec<String>,
446 pub gossip_applied: bool,
448 pub probability: f32,
450 pub dropped_count: usize,
452}
453
454pub struct PathSelector {
456 config: PathConfig,
457 relay_manager: RelayManager,
458 local_device_id: String,
460 routing_table: GradientRoutingTable,
462}
463
464impl PathSelector {
465 pub fn new() -> Self {
467 Self::with_config(PathConfig::default(), RelayManager::new())
468 }
469
470 pub fn with_config(config: PathConfig, relay_manager: RelayManager) -> Self {
472 let routing_table = GradientRoutingTable::with_config(config.gradient_routing.clone());
473 Self {
474 config,
475 relay_manager,
476 local_device_id: String::new(),
477 routing_table,
478 }
479 }
480
481 pub fn set_local_device_id(&mut self, device_id: impl Into<String>) {
483 self.local_device_id = device_id.into();
484 }
485
486 pub fn current_relay_role(&self) -> RelayRole {
492 self.relay_manager.current_role()
493 }
494
495 pub fn evaluate_relay_transition(
501 &mut self,
502 connection_count: usize,
503 battery_level: u8,
504 is_charging: bool,
505 ) -> Option<RelayTransition> {
506 self.relay_manager
507 .evaluate_transition(connection_count, battery_level, is_charging)
508 }
509
510 pub fn compute_forwarding_probability(&self, visible_peer_count: usize) -> f32 {
515 let gossip = &self.config.gossip;
516
517 if !gossip.enabled || visible_peer_count <= gossip.small_network_threshold {
518 return 1.0;
519 }
520
521 let raw_probability = gossip.target_fanout as f32 / visible_peer_count as f32;
522 raw_probability.max(gossip.min_probability).min(1.0)
523 }
524
525 fn should_forward_to_peer(&self, message_id: &str, peer_id: &str, probability: f32) -> bool {
531 if probability >= 1.0 {
532 return true;
533 }
534 if probability <= 0.0 {
535 return false;
536 }
537
538 let mut hasher = DefaultHasher::new();
540 message_id.hash(&mut hasher);
541 peer_id.hash(&mut hasher);
542 self.local_device_id.hash(&mut hasher);
543 let hash = hasher.finish();
544
545 let hash_probability = (hash as f64 / u64::MAX as f64) as f32;
547
548 hash_probability < probability
549 }
550
551 pub fn select_paths_with_gossip(
564 &self,
565 message: &Message,
566 neighbors: &[NeighborInfo],
567 total_visible_peers: usize,
568 ) -> ForwardingDecision {
569 if neighbors.is_empty() {
570 return ForwardingDecision {
571 peers: Vec::new(),
572 gossip_applied: false,
573 probability: 1.0,
574 dropped_count: 0,
575 };
576 }
577
578 let bypass_gossip = self.config.gossip.priority_bypass
580 && matches!(
581 message.priority,
582 offline_protocol_core::MessagePriority::High
583 | offline_protocol_core::MessagePriority::Critical
584 );
585
586 let probability = if bypass_gossip {
588 1.0
589 } else {
590 self.compute_forwarding_probability(total_visible_peers.max(neighbors.len()))
591 };
592
593 let gossip_applied = probability < 1.0;
594
595 let mut scored_neighbors: Vec<(String, PathScore)> = neighbors
597 .iter()
598 .map(|neighbor| {
599 let score = self.calculate_path_score(message, neighbor);
600 (neighbor.peer_id.clone(), score)
601 })
602 .collect();
603
604 scored_neighbors.retain(|(peer_id, _)| {
606 if let Some(neighbor) = neighbors.iter().find(|n| n.peer_id == *peer_id) {
607 if let Some(relay_info) = &neighbor.relay_info {
608 return relay_info.congestion_level <= self.config.max_congestion_level;
609 }
610 }
611 true
612 });
613
614 scored_neighbors.sort_by(|a, b| {
616 b.1.total
617 .partial_cmp(&a.1.total)
618 .unwrap_or(std::cmp::Ordering::Equal)
619 });
620
621 let candidates: Vec<String> = scored_neighbors
623 .into_iter()
624 .take(self.config.forward_to_top_k)
625 .map(|(peer_id, _)| peer_id)
626 .collect();
627
628 let initial_count = candidates.len();
630 let message_id = message.id.as_str();
631 let peers: Vec<String> = if gossip_applied {
632 candidates
633 .into_iter()
634 .filter(|peer_id| {
635 self.should_forward_to_peer(&message_id, peer_id.as_str(), probability)
636 })
637 .collect()
638 } else {
639 candidates
640 };
641
642 let dropped_count = initial_count.saturating_sub(peers.len());
643
644 ForwardingDecision {
645 peers,
646 gossip_applied,
647 probability,
648 dropped_count,
649 }
650 }
651
652 pub fn select_paths(&self, message: &Message, neighbors: &[NeighborInfo]) -> Vec<String> {
666 if neighbors.is_empty() {
667 return Vec::new();
668 }
669
670 let mut scored_neighbors: Vec<(String, PathScore)> = neighbors
672 .iter()
673 .map(|neighbor| {
674 let score = self.calculate_path_score(message, neighbor);
675 (neighbor.peer_id.clone(), score)
676 })
677 .collect();
678
679 scored_neighbors.retain(|(peer_id, _)| {
681 if let Some(neighbor) = neighbors.iter().find(|n| n.peer_id == *peer_id) {
682 if let Some(relay_info) = &neighbor.relay_info {
683 return relay_info.congestion_level <= self.config.max_congestion_level;
684 }
685 }
686 true
687 });
688
689 scored_neighbors.sort_by(|a, b| {
691 b.1.total
692 .partial_cmp(&a.1.total)
693 .unwrap_or(std::cmp::Ordering::Equal)
694 });
695
696 scored_neighbors
698 .into_iter()
699 .take(self.config.forward_to_top_k)
700 .map(|(peer_id, _)| peer_id)
701 .collect()
702 }
703
704 fn calculate_path_score(&self, message: &Message, neighbor: &NeighborInfo) -> PathScore {
706 let signal_score = self.calculate_signal_score(neighbor);
707 let proximity_score = self.calculate_proximity_score(message, neighbor);
708 let capacity_score = self.calculate_capacity_score(neighbor);
709 let energy_score = self.calculate_energy_score(neighbor);
710
711 let total = (signal_score * SIGNAL_WEIGHT)
712 + (proximity_score * PROXIMITY_WEIGHT)
713 + (capacity_score * CAPACITY_WEIGHT)
714 + (energy_score * ENERGY_WEIGHT);
715
716 PathScore {
717 signal: signal_score,
718 proximity: proximity_score,
719 capacity: capacity_score,
720 energy: energy_score,
721 total,
722 }
723 }
724
725 fn calculate_signal_score(&self, neighbor: &NeighborInfo) -> f32 {
727 let rssi = neighbor.rssi;
728
729 if rssi >= EXCELLENT_RSSI_THRESHOLD {
730 EXCELLENT_SIGNAL_SCORE
731 } else if rssi >= GOOD_RSSI_THRESHOLD {
732 GOOD_SIGNAL_BASE
733 + ((rssi - GOOD_RSSI_THRESHOLD) as f32 * 30.0
734 / (EXCELLENT_RSSI_THRESHOLD - GOOD_RSSI_THRESHOLD) as f32)
735 } else if rssi >= FAIR_RSSI_THRESHOLD {
736 FAIR_SIGNAL_BASE
737 + ((rssi - FAIR_RSSI_THRESHOLD) as f32 * 30.0
738 / (GOOD_RSSI_THRESHOLD - FAIR_RSSI_THRESHOLD) as f32)
739 } else {
740 ((rssi - POOR_RSSI_MAX).max(0) as f32 * FAIR_SIGNAL_BASE
741 / (FAIR_RSSI_THRESHOLD - POOR_RSSI_MAX) as f32)
742 .max(0.0)
743 }
744 }
745
746 fn calculate_proximity_score(&self, message: &Message, neighbor: &NeighborInfo) -> f32 {
748 if let Some(hops) = neighbor.hops_to_destination {
749 let remaining_ttl = message.ttl.value();
751
752 if hops == 0 {
753 100.0
755 } else {
756 let score = if hops < remaining_ttl {
758 100.0 - (hops as f32 / remaining_ttl as f32 * 50.0)
759 } else {
760 20.0
762 };
763 score.max(0.0)
764 }
765 } else {
766 neighbor.link_quality as f32
768 }
769 }
770
771 fn calculate_capacity_score(&self, neighbor: &NeighborInfo) -> f32 {
773 if let Some(relay_info) = &neighbor.relay_info {
774 let relay_score = self.relay_manager.calculate_relay_score(relay_info);
775 relay_score.min(EXCELLENT_SIGNAL_SCORE)
776 } else {
777 NON_RELAY_BASIC_CAPACITY
778 }
779 }
780
781 fn calculate_energy_score(&self, neighbor: &NeighborInfo) -> f32 {
783 if let Some(relay_info) = &neighbor.relay_info {
784 if relay_info.is_charging {
785 CHARGING_BATTERY_SCORE
786 } else {
787 relay_info.battery_level as f32
788 }
789 } else {
790 NON_RELAY_ASSUMED_BATTERY
791 }
792 }
793
794 pub fn select_best_path(
796 &self,
797 message: &Message,
798 neighbors: &[NeighborInfo],
799 ) -> Option<String> {
800 self.select_paths(message, neighbors).into_iter().next()
801 }
802
803 pub fn should_route_around_congestion(&self, neighbor: &NeighborInfo) -> bool {
805 if let Some(relay_info) = &neighbor.relay_info {
806 relay_info.congestion_level > self.config.max_congestion_level
807 } else {
808 false
809 }
810 }
811
812 pub fn config(&self) -> &PathConfig {
814 &self.config
815 }
816
817 pub fn learn_route_from_message(
823 &mut self,
824 message: &Message,
825 from_neighbor: &str,
826 quality: f32,
827 ) {
828 let sender = message.sender.as_str();
829 let hop_count = message.hop_count.value();
830 self.routing_table.learn_route(
831 sender,
832 from_neighbor,
833 hop_count,
834 quality,
835 0, );
837 }
838
839 pub fn get_route_to(&self, destination: &str) -> Option<&RouteEntry> {
841 self.routing_table.get_route(destination)
842 }
843
844 pub fn has_route_to(&self, destination: &str) -> bool {
846 self.routing_table.has_route(destination)
847 }
848
849 pub fn select_directed_path(
852 &self,
853 message: &Message,
854 neighbors: &[NeighborInfo],
855 ) -> Option<String> {
856 let recipient = message.recipient.as_str();
857
858 for neighbor in neighbors {
860 if neighbor.peer_id == recipient {
861 return Some(neighbor.peer_id.clone());
862 }
863 }
864
865 if let Some(route) = self.routing_table.get_route(recipient) {
867 if neighbors.iter().any(|n| n.peer_id == route.next_hop) {
869 return Some(route.next_hop.clone());
870 }
871 }
872
873 self.select_best_path(message, neighbors)
875 }
876
877 pub fn remove_neighbor_routes(&mut self, neighbor: &str) {
879 self.routing_table.remove_neighbor(neighbor);
880 }
881
882 pub fn cleanup_routes(&mut self) {
884 self.routing_table.cleanup_expired();
885 }
886
887 pub fn routing_stats(&self) -> (usize, usize) {
889 (
890 self.routing_table.destination_count(),
891 self.routing_table.route_count(),
892 )
893 }
894
895 pub fn routing_table_mut(&mut self) -> &mut GradientRoutingTable {
897 &mut self.routing_table
898 }
899}
900
901impl Default for PathSelector {
902 fn default() -> Self {
903 Self::new()
904 }
905}
906
907#[cfg(test)]
908mod tests {
909 use super::*;
910 use crate::relay::{RelayInfo, RelayRole};
911 use offline_protocol_core::{AppId, UserId};
912
913 fn create_test_message() -> Message {
914 Message::new(
915 UserId::new("alice").unwrap(),
916 UserId::new("bob").unwrap(),
917 AppId::new("test").unwrap(),
918 "Test message",
919 )
920 }
921
922 fn create_neighbor(id: &str, rssi: i16, hops: Option<u8>, congestion: f32) -> NeighborInfo {
923 NeighborInfo {
924 peer_id: id.to_string(),
925 rssi,
926 hops_to_destination: hops,
927 link_quality: 80,
928 relay_info: Some(RelayInfo {
929 connection_count: 5,
930 battery_level: 70,
931 is_charging: false,
932 role: RelayRole::Relay,
933 link_quality: 80,
934 queue_depth: 10,
935 congestion_level: congestion,
936 }),
937 }
938 }
939
940 #[test]
941 fn test_path_selection_basic() {
942 let selector = PathSelector::new();
943 let message = create_test_message();
944
945 let neighbors = vec![
946 create_neighbor("peer1", -60, Some(2), 0.2),
947 create_neighbor("peer2", -70, Some(3), 0.3),
948 create_neighbor("peer3", -55, Some(1), 0.1),
949 ];
950
951 let paths = selector.select_paths(&message, &neighbors);
952
953 assert!(!paths.is_empty());
955 assert!(paths.len() <= 3);
956
957 assert_eq!(paths[0], "peer3");
959 }
960
961 #[test]
962 fn test_congestion_filtering() {
963 let selector = PathSelector::new();
964 let message = create_test_message();
965
966 let neighbors = vec![
967 create_neighbor("peer1", -60, Some(2), 0.9), create_neighbor("peer2", -65, Some(3), 0.3),
969 ];
970
971 let paths = selector.select_paths(&message, &neighbors);
972
973 assert!(!paths.contains(&"peer1".to_string()));
975 }
976
977 #[test]
978 fn test_direct_destination_preferred() {
979 let selector = PathSelector::new();
980 let message = create_test_message();
981
982 let neighbors = vec![
983 create_neighbor("peer1", -70, Some(5), 0.3),
984 create_neighbor("peer2", -70, Some(0), 0.1), ];
986
987 let paths = selector.select_paths(&message, &neighbors);
988
989 assert_eq!(paths[0], "peer2");
991 }
992
993 #[test]
994 fn test_signal_score_calculation() {
995 let selector = PathSelector::new();
996
997 let excellent = create_neighbor("peer1", -40, None, 0.1);
998 let score = selector.calculate_signal_score(&excellent);
999 assert_eq!(score, 100.0);
1000
1001 let poor = create_neighbor("peer2", -90, None, 0.1);
1002 let score = selector.calculate_signal_score(&poor);
1003 assert!(score < 40.0);
1004 }
1005
1006 #[test]
1007 fn test_empty_neighbors() {
1008 let selector = PathSelector::new();
1009 let message = create_test_message();
1010
1011 let paths = selector.select_paths(&message, &[]);
1012 assert!(paths.is_empty());
1013 }
1014
1015 #[test]
1016 fn test_select_best_path() {
1017 let selector = PathSelector::new();
1018 let message = create_test_message();
1019
1020 let neighbors = vec![
1021 create_neighbor("peer1", -60, Some(2), 0.2),
1022 create_neighbor("peer2", -55, Some(1), 0.1),
1023 ];
1024
1025 let best = selector.select_best_path(&message, &neighbors);
1026 assert_eq!(best, Some("peer2".to_string()));
1027 }
1028
1029 #[test]
1030 fn test_route_around_congestion() {
1031 let selector = PathSelector::new();
1032
1033 let congested = create_neighbor("peer1", -60, Some(2), 0.9);
1034 assert!(selector.should_route_around_congestion(&congested));
1035
1036 let normal = create_neighbor("peer2", -60, Some(2), 0.3);
1037 assert!(!selector.should_route_around_congestion(&normal));
1038 }
1039
1040 #[test]
1041 fn test_gossip_probability_small_network() {
1042 let selector = PathSelector::new();
1043
1044 assert_eq!(selector.compute_forwarding_probability(5), 1.0);
1046 assert_eq!(selector.compute_forwarding_probability(10), 1.0);
1047 }
1048
1049 #[test]
1050 fn test_gossip_probability_large_network() {
1051 let selector = PathSelector::new();
1052
1053 let prob = selector.compute_forwarding_probability(40);
1057 assert!((prob - 0.15).abs() < 0.01);
1058
1059 let prob = selector.compute_forwarding_probability(20);
1061 assert!((prob - 0.2).abs() < 0.01);
1062
1063 let prob = selector.compute_forwarding_probability(100);
1065 assert!((prob - 0.15).abs() < 0.01);
1066 }
1067
1068 #[test]
1069 fn test_gossip_disabled() {
1070 let config = PathConfig {
1071 gossip: GossipConfig {
1072 enabled: false,
1073 ..Default::default()
1074 },
1075 ..Default::default()
1076 };
1077 let selector = PathSelector::with_config(config, RelayManager::new());
1078
1079 assert_eq!(selector.compute_forwarding_probability(1000), 1.0);
1081 }
1082
1083 #[test]
1084 fn test_gossip_forwarding_deterministic() {
1085 let mut selector = PathSelector::new();
1086 selector.set_local_device_id("device1");
1087
1088 let message = create_test_message();
1089 let neighbors = vec![
1090 create_neighbor("peer1", -60, Some(2), 0.2),
1091 create_neighbor("peer2", -60, Some(2), 0.2),
1092 create_neighbor("peer3", -60, Some(2), 0.2),
1093 ];
1094
1095 let decision1 = selector.select_paths_with_gossip(&message, &neighbors, 50);
1097 let decision2 = selector.select_paths_with_gossip(&message, &neighbors, 50);
1098
1099 assert_eq!(decision1.peers, decision2.peers);
1100 assert!(decision1.gossip_applied);
1101 }
1102
1103 #[test]
1104 fn test_gossip_priority_bypass() {
1105 use offline_protocol_core::MessagePriority;
1106
1107 let mut selector = PathSelector::new();
1108 selector.set_local_device_id("device1");
1109
1110 let high_priority_message = Message::builder(
1111 UserId::new("alice").unwrap(),
1112 UserId::new("bob").unwrap(),
1113 AppId::new("test").unwrap(),
1114 )
1115 .content("Test")
1116 .priority(MessagePriority::High)
1117 .build();
1118
1119 let neighbors = vec![
1120 create_neighbor("peer1", -60, Some(2), 0.2),
1121 create_neighbor("peer2", -60, Some(2), 0.2),
1122 ];
1123
1124 let decision = selector.select_paths_with_gossip(&high_priority_message, &neighbors, 100);
1126 assert!(!decision.gossip_applied);
1127 assert_eq!(decision.probability, 1.0);
1128 }
1129
1130 #[test]
1131 fn test_gossip_reduces_forwarding_count() {
1132 let mut selector = PathSelector::new();
1133 selector.set_local_device_id("test-device");
1134
1135 let neighbors: Vec<NeighborInfo> = (0..20)
1136 .map(|i| create_neighbor(&format!("peer{}", i), -60, Some(2), 0.2))
1137 .collect();
1138
1139 let message = create_test_message();
1140
1141 let decision = selector.select_paths_with_gossip(&message, &neighbors, 100);
1144
1145 assert!(decision.gossip_applied);
1147 assert!((decision.probability - 0.15).abs() < 0.01);
1149 }
1150
1151 #[test]
1154 fn test_dsdv_prefer_higher_sequence_number() {
1155 let mut table = GradientRoutingTable::new();
1156 let dest = "alice";
1157
1158 table.learn_route(dest, "peer_a", 2, 0.9, 1);
1159 table.learn_route(dest, "peer_b", 3, 0.8, 2);
1160
1161 let route = table.get_route(dest).expect("should have route");
1162 assert_eq!(route.next_hop, "peer_b");
1163 assert_eq!(route.sequence_number, 2);
1164 }
1165
1166 #[test]
1167 fn test_dsdv_reject_stale_update_same_next_hop() {
1168 let mut table = GradientRoutingTable::new();
1169 let dest = "bob";
1170
1171 table.learn_route(dest, "peer_a", 2, 0.9, 2);
1172 table.learn_route(dest, "peer_a", 5, 0.5, 1); let route = table.get_route(dest).expect("should have route");
1175 assert_eq!(route.next_hop, "peer_a");
1176 assert_eq!(route.hop_count, 2);
1177 assert_eq!(route.sequence_number, 2);
1178 }
1179
1180 #[test]
1181 fn test_dsdv_accept_same_sequence_better_hop_count() {
1182 let mut table = GradientRoutingTable::new();
1183 let dest = "carol";
1184
1185 table.learn_route(dest, "peer_a", 4, 0.7, 2);
1186 table.learn_route(dest, "peer_a", 2, 0.8, 2); let route = table.get_route(dest).expect("should have route");
1189 assert_eq!(route.hop_count, 2);
1190 assert_eq!(route.sequence_number, 2);
1191 }
1192
1193 #[test]
1194 fn test_global_eviction_prefers_low_quality_routes() {
1195 let config = GradientRoutingConfig {
1196 max_routing_table_size: 3,
1197 max_routes_per_destination: 3,
1198 route_ttl_secs: 300,
1199 ..Default::default()
1200 };
1201 let mut table = GradientRoutingTable::with_config(config);
1202
1203 table.learn_route("alice", "hop_good", 1, 0.9, 0);
1205 table.learn_route("bob", "hop_low1", 3, 0.1, 0);
1207 table.learn_route("carol", "hop_low2", 4, 0.1, 0);
1208
1209 assert_eq!(table.route_count(), 3);
1210
1211 table.learn_route("dave", "hop_new", 2, 0.5, 0);
1213 assert_eq!(table.route_count(), 3);
1214
1215 assert!(
1217 table.has_route("alice"),
1218 "High-quality route to alice should survive eviction"
1219 );
1220 assert!(table.has_route("dave"), "Newly inserted route should exist");
1222 }
1223
1224 #[test]
1225 fn test_dsdv_eviction_removes_worst_route() {
1226 let config = GradientRoutingConfig {
1227 max_routes_per_destination: 2,
1228 ..Default::default()
1229 };
1230 let mut table = GradientRoutingTable::with_config(config);
1231 let dest = "dest";
1232
1233 table.learn_route(dest, "hop1", 1, 0.9, 10);
1234 table.learn_route(dest, "hop2", 2, 0.8, 10);
1235 table.learn_route(dest, "hop3", 3, 0.7, 5);
1237
1238 let route = table.get_route(dest).expect("should have route");
1239 assert_eq!(route.next_hop, "hop1");
1240 let routes = table.get_routes(dest);
1241 assert_eq!(routes.len(), 2);
1242 assert!(routes.iter().any(|r| r.next_hop == "hop1"));
1243 assert!(routes.iter().any(|r| r.next_hop == "hop3"));
1244 assert!(
1245 !routes.iter().any(|r| r.next_hop == "hop2"),
1246 "hop2 (worst of original two) should be evicted"
1247 );
1248 }
1249
1250 #[test]
1253 fn test_seq_is_newer_basic() {
1254 assert!(seq_is_newer(2, 1));
1255 assert!(seq_is_newer(100, 50));
1256 assert!(!seq_is_newer(1, 2));
1257 assert!(!seq_is_newer(5, 5)); }
1259
1260 #[test]
1261 fn test_seq_is_newer_wrapping() {
1262 assert!(seq_is_newer(0, u32::MAX));
1264 assert!(seq_is_newer(1, u32::MAX));
1265 assert!(seq_is_newer(5, u32::MAX - 2));
1266 assert!(!seq_is_newer(u32::MAX, 0));
1268 assert!(!seq_is_newer(u32::MAX - 2, 5));
1269 }
1270
1271 #[test]
1272 fn test_dsdv_wrapping_sequence_update() {
1273 let mut table = GradientRoutingTable::new();
1274 let dest = "alice";
1275
1276 table.learn_route(dest, "peer_a", 2, 0.9, u32::MAX - 1);
1278
1279 table.learn_route(dest, "peer_a", 2, 0.9, 0);
1281
1282 let route = table.get_route(dest).expect("should have route");
1283 assert_eq!(
1284 route.sequence_number, 0,
1285 "Wrapped sequence should be accepted"
1286 );
1287 }
1288
1289 #[test]
1290 fn test_seq_cmp_basic_and_wrapping() {
1291 use std::cmp::Ordering;
1292 assert_eq!(seq_cmp(5, 5), Ordering::Equal);
1293 assert_eq!(seq_cmp(10, 5), Ordering::Greater);
1294 assert_eq!(seq_cmp(5, 10), Ordering::Less);
1295 assert_eq!(seq_cmp(0, u32::MAX), Ordering::Greater);
1297 assert_eq!(seq_cmp(u32::MAX, 0), Ordering::Less);
1298 }
1299
1300 #[test]
1301 fn test_get_route_prefers_wrapped_sequence() {
1302 let mut table = GradientRoutingTable::new();
1303 let dest = "alice";
1304
1305 table.learn_route(dest, "peer_old", 2, 0.9, u32::MAX - 1);
1307 table.learn_route(dest, "peer_new", 3, 0.7, 1);
1308
1309 let route = table.get_route(dest).expect("should have route");
1310 assert_eq!(
1311 route.next_hop, "peer_new",
1312 "get_route must prefer wrapped (newer) sequence number"
1313 );
1314 assert_eq!(route.sequence_number, 1);
1315 }
1316
1317 #[test]
1318 fn test_per_dest_eviction_respects_wrapping() {
1319 let config = GradientRoutingConfig {
1320 max_routes_per_destination: 2,
1321 ..Default::default()
1322 };
1323 let mut table = GradientRoutingTable::with_config(config);
1324 let dest = "bob";
1325
1326 table.learn_route(dest, "hop_old", 2, 0.8, u32::MAX - 5);
1328 table.learn_route(dest, "hop_mid", 2, 0.8, u32::MAX);
1329
1330 table.learn_route(dest, "hop_new", 2, 0.8, 2);
1332
1333 let routes = table.get_routes(dest);
1334 assert_eq!(routes.len(), 2);
1335 assert!(
1336 routes.iter().any(|r| r.next_hop == "hop_new"),
1337 "Wrapped-sequence route must survive"
1338 );
1339 assert!(
1340 routes.iter().any(|r| r.next_hop == "hop_mid"),
1341 "Second-best sequence route must survive"
1342 );
1343 assert!(
1344 !routes.iter().any(|r| r.next_hop == "hop_old"),
1345 "Oldest sequence route should be evicted"
1346 );
1347 }
1348
1349 #[test]
1350 fn test_seq_is_newer_half_space_boundary() {
1351 let half = 1u32 << 31;
1354 assert!(
1355 !seq_is_newer(half, 0),
1356 "Half-space distance is ambiguous, should not be newer"
1357 );
1358 assert!(!seq_is_newer(0, half), "Reverse half-space also not newer");
1359 }
1360}