1use std::collections::HashMap;
21use std::sync::RwLock;
22
23use serde::{Deserialize, Serialize};
24
25#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
34pub enum NodeCapability {
35 VectorSearch {
37 index_size: u64,
39 dimensions: u32,
41 },
42 TensorLogic {
44 rule_count: u64,
46 },
47 GradientSync {
49 model_size_bytes: u64,
51 },
52 BlockStorage {
54 stored_bytes: u64,
56 },
57 ContentRouting,
59}
60
61impl NodeCapability {
62 pub fn name(&self) -> &str {
67 match self {
68 NodeCapability::VectorSearch { .. } => "vector_search",
69 NodeCapability::TensorLogic { .. } => "tensor_logic",
70 NodeCapability::GradientSync { .. } => "gradient_sync",
71 NodeCapability::BlockStorage { .. } => "block_storage",
72 NodeCapability::ContentRouting => "content_routing",
73 }
74 }
75}
76
77#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
81pub struct NodeCapabilities {
82 pub peer_id: String,
84 pub capabilities: Vec<NodeCapability>,
86 pub protocol_version: String,
88 pub announced_at: u64,
91 pub ttl_secs: u64,
93}
94
95impl NodeCapabilities {
96 pub const DEFAULT_TTL_SECS: u64 = 300;
98
99 pub fn new(peer_id: &str, capabilities: Vec<NodeCapability>, protocol_version: &str) -> Self {
104 let announced_at = std::time::SystemTime::now()
105 .duration_since(std::time::UNIX_EPOCH)
106 .map(|d| d.as_millis() as u64)
107 .unwrap_or(0);
108
109 Self {
110 peer_id: peer_id.to_owned(),
111 capabilities,
112 protocol_version: protocol_version.to_owned(),
113 announced_at,
114 ttl_secs: Self::DEFAULT_TTL_SECS,
115 }
116 }
117
118 pub fn has_capability(&self, name: &str) -> bool {
120 self.capabilities.iter().any(|c| c.name() == name)
121 }
122
123 pub fn is_expired(&self, now_ms: u64) -> bool {
127 now_ms
128 > self
129 .announced_at
130 .saturating_add(self.ttl_secs.saturating_mul(1_000))
131 }
132
133 pub fn get_capability(&self, name: &str) -> Option<&NodeCapability> {
136 self.capabilities.iter().find(|c| c.name() == name)
137 }
138}
139
140#[derive(Debug, Default)]
165pub struct NodeCapabilityRegistry {
166 entries: RwLock<HashMap<String, NodeCapabilities>>,
167}
168
169impl NodeCapabilityRegistry {
170 pub fn new() -> Self {
172 Self::default()
173 }
174
175 pub fn register(&self, caps: NodeCapabilities) {
178 match self.entries.write() {
179 Ok(mut guard) => {
180 guard.insert(caps.peer_id.clone(), caps);
181 }
182 Err(poisoned) => {
183 let mut guard = poisoned.into_inner();
186 guard.insert(caps.peer_id.clone(), caps);
187 }
188 }
189 }
190
191 pub fn unregister(&self, peer_id: &str) {
195 match self.entries.write() {
196 Ok(mut guard) => {
197 guard.remove(peer_id);
198 }
199 Err(poisoned) => {
200 let mut guard = poisoned.into_inner();
201 guard.remove(peer_id);
202 }
203 }
204 }
205
206 pub fn get(&self, peer_id: &str) -> Option<NodeCapabilities> {
209 match self.entries.read() {
210 Ok(guard) => guard.get(peer_id).cloned(),
211 Err(poisoned) => poisoned.into_inner().get(peer_id).cloned(),
212 }
213 }
214
215 pub fn find_by_capability(&self, name: &str) -> Vec<NodeCapabilities> {
221 let now_ms = current_time_ms();
222 match self.entries.read() {
223 Ok(guard) => guard
224 .values()
225 .filter(|nc| !nc.is_expired(now_ms) && nc.has_capability(name))
226 .cloned()
227 .collect(),
228 Err(poisoned) => poisoned
229 .into_inner()
230 .values()
231 .filter(|nc| !nc.is_expired(now_ms) && nc.has_capability(name))
232 .cloned()
233 .collect(),
234 }
235 }
236
237 pub fn evict_expired(&self, now_ms: u64) -> usize {
240 match self.entries.write() {
241 Ok(mut guard) => {
242 let before = guard.len();
243 guard.retain(|_, nc| !nc.is_expired(now_ms));
244 before - guard.len()
245 }
246 Err(poisoned) => {
247 let mut guard = poisoned.into_inner();
248 let before = guard.len();
249 guard.retain(|_, nc| !nc.is_expired(now_ms));
250 before - guard.len()
251 }
252 }
253 }
254
255 pub fn peer_count(&self) -> usize {
258 match self.entries.read() {
259 Ok(guard) => guard.len(),
260 Err(poisoned) => poisoned.into_inner().len(),
261 }
262 }
263
264 pub fn capability_histogram(&self) -> HashMap<String, usize> {
267 match self.entries.read() {
268 Ok(guard) => build_histogram(guard.values()),
269 Err(poisoned) => build_histogram(poisoned.into_inner().values()),
270 }
271 }
272}
273
274#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
283pub enum Capability {
284 Bitswap {
286 version: u8,
288 },
289 TensorSwap {
291 version: u8,
293 },
294 Kademlia,
296 GossipSub {
298 topics: Vec<String>,
300 },
301 ContentAddressing,
303 Custom {
305 name: String,
307 version: u8,
309 },
310}
311
312#[derive(Debug, Clone, PartialEq, Eq)]
320pub struct CapabilityAdvertisement {
321 pub peer_id: String,
323 pub capabilities: Vec<Capability>,
325 pub advertised_at_tick: u64,
327 pub expires_at_tick: u64,
329}
330
331impl CapabilityAdvertisement {
332 #[inline]
336 pub fn is_valid(&self, current_tick: u64) -> bool {
337 current_tick < self.expires_at_tick
338 }
339
340 #[inline]
343 pub fn has_capability(&self, cap: &Capability) -> bool {
344 self.capabilities.contains(cap)
345 }
346}
347
348#[derive(Debug, Clone, PartialEq, Eq)]
352pub struct CapabilityRegistryStats {
353 pub total_peers: usize,
356 pub active_peers: usize,
358 pub total_capabilities_registered: usize,
361 pub expired_count: usize,
363}
364
365#[derive(Debug, Default)]
394pub struct PeerCapabilityRegistry {
395 advertisements: HashMap<String, CapabilityAdvertisement>,
397}
398
399impl PeerCapabilityRegistry {
400 pub fn new() -> Self {
402 Self::default()
403 }
404
405 pub fn advertise(
409 &mut self,
410 peer_id: String,
411 capabilities: Vec<Capability>,
412 current_tick: u64,
413 ttl_ticks: u64,
414 ) {
415 let advert = CapabilityAdvertisement {
416 peer_id: peer_id.clone(),
417 capabilities,
418 advertised_at_tick: current_tick,
419 expires_at_tick: current_tick.saturating_add(ttl_ticks),
420 };
421 self.advertisements.insert(peer_id, advert);
422 }
423
424 pub fn peers_with_capability(&self, cap: &Capability, current_tick: u64) -> Vec<&str> {
429 let mut peers: Vec<&str> = self
430 .advertisements
431 .values()
432 .filter(|advert| advert.is_valid(current_tick) && advert.has_capability(cap))
433 .map(|advert| advert.peer_id.as_str())
434 .collect();
435 peers.sort_unstable();
436 peers
437 }
438
439 pub fn peer_capabilities(&self, peer_id: &str, current_tick: u64) -> Option<&[Capability]> {
446 self.advertisements
447 .get(peer_id)
448 .filter(|advert| advert.is_valid(current_tick))
449 .map(|advert| advert.capabilities.as_slice())
450 }
451
452 pub fn remove_peer(&mut self, peer_id: &str) -> bool {
457 self.advertisements.remove(peer_id).is_some()
458 }
459
460 pub fn evict_expired(&mut self, current_tick: u64) -> usize {
464 let before = self.advertisements.len();
465 self.advertisements
466 .retain(|_, advert| advert.is_valid(current_tick));
467 before - self.advertisements.len()
468 }
469
470 pub fn stats(&self, current_tick: u64) -> CapabilityRegistryStats {
472 let total_peers = self.advertisements.len();
473 let mut active_peers = 0usize;
474 let mut expired_count = 0usize;
475 let mut total_capabilities_registered = 0usize;
476
477 for advert in self.advertisements.values() {
478 total_capabilities_registered =
479 total_capabilities_registered.saturating_add(advert.capabilities.len());
480 if advert.is_valid(current_tick) {
481 active_peers = active_peers.saturating_add(1);
482 } else {
483 expired_count = expired_count.saturating_add(1);
484 }
485 }
486
487 CapabilityRegistryStats {
488 total_peers,
489 active_peers,
490 total_capabilities_registered,
491 expired_count,
492 }
493 }
494}
495
496fn current_time_ms() -> u64 {
499 std::time::SystemTime::now()
500 .duration_since(std::time::UNIX_EPOCH)
501 .map(|d| d.as_millis() as u64)
502 .unwrap_or(0)
503}
504
505fn build_histogram<'a, I>(iter: I) -> HashMap<String, usize>
506where
507 I: Iterator<Item = &'a NodeCapabilities>,
508{
509 let mut map: HashMap<String, usize> = HashMap::new();
510 for nc in iter {
511 for cap in &nc.capabilities {
512 *map.entry(cap.name().to_owned()).or_insert(0) += 1;
513 }
514 }
515 map
516}
517
518#[cfg(test)]
521mod tests {
522 use super::*;
523
524 fn make_node_caps(peer_id: &str, caps: Vec<NodeCapability>) -> NodeCapabilities {
529 NodeCapabilities::new(peer_id, caps, "0.2.0")
530 }
531
532 fn make_expired_node_caps(peer_id: &str, caps: Vec<NodeCapability>) -> NodeCapabilities {
535 let mut nc = make_node_caps(peer_id, caps);
536 nc.announced_at = 0;
537 nc.ttl_secs = 1;
538 nc
539 }
540
541 #[test]
544 fn test_register_and_get() {
545 let registry = NodeCapabilityRegistry::new();
546 let caps = make_node_caps("peer-a", vec![NodeCapability::ContentRouting]);
547 registry.register(caps.clone());
548
549 let retrieved = registry.get("peer-a").expect("should be present");
550 assert_eq!(retrieved.peer_id, "peer-a");
551 assert_eq!(retrieved.protocol_version, "0.2.0");
552 }
553
554 #[test]
557 fn test_get_unknown_peer_returns_none() {
558 let registry = NodeCapabilityRegistry::new();
559 assert!(registry.get("nobody").is_none());
560 }
561
562 #[test]
565 fn test_has_capability_true() {
566 let caps = make_node_caps(
567 "peer-b",
568 vec![NodeCapability::VectorSearch {
569 index_size: 1000,
570 dimensions: 128,
571 }],
572 );
573 assert!(caps.has_capability("vector_search"));
574 }
575
576 #[test]
579 fn test_has_capability_false() {
580 let caps = make_node_caps("peer-c", vec![NodeCapability::ContentRouting]);
581 assert!(!caps.has_capability("vector_search"));
582 }
583
584 #[test]
587 fn test_find_by_capability_correct_peers() {
588 let registry = NodeCapabilityRegistry::new();
589
590 registry.register(make_node_caps(
591 "peer-1",
592 vec![NodeCapability::BlockStorage { stored_bytes: 512 }],
593 ));
594 registry.register(make_node_caps(
595 "peer-2",
596 vec![
597 NodeCapability::BlockStorage { stored_bytes: 1024 },
598 NodeCapability::ContentRouting,
599 ],
600 ));
601 registry.register(make_node_caps(
602 "peer-3",
603 vec![NodeCapability::ContentRouting],
604 ));
605
606 let found = registry.find_by_capability("block_storage");
607 let ids: Vec<&str> = found.iter().map(|nc| nc.peer_id.as_str()).collect();
608 assert_eq!(ids.len(), 2);
609 assert!(ids.contains(&"peer-1"));
610 assert!(ids.contains(&"peer-2"));
611 }
612
613 #[test]
616 fn test_find_by_capability_excludes_expired() {
617 let registry = NodeCapabilityRegistry::new();
618
619 registry.register(make_node_caps(
620 "fresh",
621 vec![NodeCapability::ContentRouting],
622 ));
623 registry.register(make_expired_node_caps(
624 "stale",
625 vec![NodeCapability::ContentRouting],
626 ));
627
628 let found = registry.find_by_capability("content_routing");
629 assert_eq!(found.len(), 1);
630 assert_eq!(found[0].peer_id, "fresh");
631 }
632
633 #[test]
636 fn test_is_expired_true() {
637 let mut nc = make_node_caps("x", vec![]);
638 nc.announced_at = 1_000_000; nc.ttl_secs = 60;
640 let now_ms = 1_000_000 + 70_000;
641 assert!(nc.is_expired(now_ms));
642 }
643
644 #[test]
647 fn test_is_expired_false() {
648 let mut nc = make_node_caps("y", vec![]);
649 nc.announced_at = 1_000_000;
650 nc.ttl_secs = 300;
651 let now_ms = 1_000_000 + 10_000;
652 assert!(!nc.is_expired(now_ms));
653 }
654
655 #[test]
658 fn test_evict_expired_removes_stale() {
659 let registry = NodeCapabilityRegistry::new();
660
661 registry.register(make_node_caps(
662 "alive",
663 vec![NodeCapability::ContentRouting],
664 ));
665 registry.register(make_expired_node_caps(
666 "dead-1",
667 vec![NodeCapability::ContentRouting],
668 ));
669 registry.register(make_expired_node_caps(
670 "dead-2",
671 vec![NodeCapability::ContentRouting],
672 ));
673
674 let evicted = registry.evict_expired(current_time_ms());
675 assert_eq!(evicted, 2);
676 assert_eq!(registry.peer_count(), 1);
677 }
678
679 #[test]
682 fn test_evict_expired_keeps_fresh() {
683 let registry = NodeCapabilityRegistry::new();
684
685 registry.register(make_node_caps(
686 "fresh-a",
687 vec![NodeCapability::TensorLogic { rule_count: 10 }],
688 ));
689 registry.register(make_node_caps(
690 "fresh-b",
691 vec![NodeCapability::TensorLogic { rule_count: 20 }],
692 ));
693
694 let evicted = registry.evict_expired(current_time_ms());
695 assert_eq!(evicted, 0);
696 assert_eq!(registry.peer_count(), 2);
697 }
698
699 #[test]
702 fn test_capability_histogram() {
703 let registry = NodeCapabilityRegistry::new();
704
705 registry.register(make_node_caps(
706 "h1",
707 vec![
708 NodeCapability::ContentRouting,
709 NodeCapability::BlockStorage { stored_bytes: 0 },
710 ],
711 ));
712 registry.register(make_node_caps("h2", vec![NodeCapability::ContentRouting]));
713 registry.register(make_node_caps(
714 "h3",
715 vec![NodeCapability::VectorSearch {
716 index_size: 50,
717 dimensions: 64,
718 }],
719 ));
720
721 let hist = registry.capability_histogram();
722 assert_eq!(*hist.get("content_routing").unwrap_or(&0), 2);
723 assert_eq!(*hist.get("block_storage").unwrap_or(&0), 1);
724 assert_eq!(*hist.get("vector_search").unwrap_or(&0), 1);
725 assert_eq!(*hist.get("tensor_logic").unwrap_or(&0), 0);
726 }
727
728 #[test]
731 fn test_get_capability_returns_correct_variant() {
732 let caps = make_node_caps(
733 "peer-v",
734 vec![
735 NodeCapability::VectorSearch {
736 index_size: 2048,
737 dimensions: 256,
738 },
739 NodeCapability::ContentRouting,
740 ],
741 );
742
743 let found = caps
744 .get_capability("vector_search")
745 .expect("should be found");
746 match found {
747 NodeCapability::VectorSearch {
748 index_size,
749 dimensions,
750 } => {
751 assert_eq!(*index_size, 2048);
752 assert_eq!(*dimensions, 256);
753 }
754 other => panic!("wrong variant: {other:?}"),
755 }
756 }
757
758 #[test]
761 fn test_get_capability_absent() {
762 let caps = make_node_caps("peer-z", vec![NodeCapability::ContentRouting]);
763 assert!(caps.get_capability("gradient_sync").is_none());
764 }
765
766 #[test]
769 fn test_unregister_removes_peer() {
770 let registry = NodeCapabilityRegistry::new();
771 registry.register(make_node_caps(
772 "to-remove",
773 vec![NodeCapability::ContentRouting],
774 ));
775 assert_eq!(registry.peer_count(), 1);
776
777 registry.unregister("to-remove");
778 assert_eq!(registry.peer_count(), 0);
779 assert!(registry.get("to-remove").is_none());
780 }
781
782 #[test]
785 fn test_unregister_unknown_is_noop() {
786 let registry = NodeCapabilityRegistry::new();
787 registry.unregister("ghost"); assert_eq!(registry.peer_count(), 0);
789 }
790
791 #[test]
794 fn test_register_replaces_existing() {
795 let registry = NodeCapabilityRegistry::new();
796
797 registry.register(make_node_caps(
798 "peer-r",
799 vec![NodeCapability::ContentRouting],
800 ));
801 registry.register(make_node_caps(
802 "peer-r",
803 vec![NodeCapability::BlockStorage { stored_bytes: 9999 }],
804 ));
805
806 assert_eq!(registry.peer_count(), 1);
807 let nc = registry.get("peer-r").expect("peer-r should be present");
808 assert!(!nc.has_capability("content_routing"));
809 assert!(nc.has_capability("block_storage"));
810 }
811
812 #[test]
815 fn test_gradient_sync_name() {
816 let cap = NodeCapability::GradientSync {
817 model_size_bytes: 1_000_000,
818 };
819 assert_eq!(cap.name(), "gradient_sync");
820 }
821
822 #[test]
825 fn test_serde_round_trip() {
826 let original = make_node_caps(
827 "peer-serde",
828 vec![
829 NodeCapability::TensorLogic { rule_count: 42 },
830 NodeCapability::GradientSync {
831 model_size_bytes: 1024,
832 },
833 ],
834 );
835
836 let json = serde_json::to_string(&original).expect("serialise");
837 let decoded: NodeCapabilities = serde_json::from_str(&json).expect("deserialise");
838 assert_eq!(original, decoded);
839 }
840
841 fn make_registry() -> PeerCapabilityRegistry {
848 PeerCapabilityRegistry::new()
849 }
850
851 #[test]
854 fn test_pcr_advertise_creates_entry() {
855 let mut reg = make_registry();
856 reg.advertise("peer-a".to_string(), vec![Capability::Kademlia], 0, 100);
857 let caps = reg
858 .peer_capabilities("peer-a", 0)
859 .expect("should have capabilities");
860 assert_eq!(caps, &[Capability::Kademlia]);
861 }
862
863 #[test]
866 fn test_pcr_advertise_overwrites_existing() {
867 let mut reg = make_registry();
868 reg.advertise("peer-b".to_string(), vec![Capability::Kademlia], 0, 100);
869 reg.advertise(
870 "peer-b".to_string(),
871 vec![Capability::Bitswap { version: 2 }],
872 10,
873 200,
874 );
875 let caps = reg
876 .peer_capabilities("peer-b", 10)
877 .expect("should have capabilities");
878 assert_eq!(caps.len(), 1);
879 assert!(caps.contains(&Capability::Bitswap { version: 2 }));
880 assert!(!caps.contains(&Capability::Kademlia));
881 }
882
883 #[test]
886 fn test_pcr_peers_with_capability_sorted() {
887 let mut reg = make_registry();
888 reg.advertise("peer-c".to_string(), vec![Capability::Kademlia], 0, 100);
889 reg.advertise("peer-a".to_string(), vec![Capability::Kademlia], 0, 100);
890 reg.advertise("peer-b".to_string(), vec![Capability::Kademlia], 0, 100);
891 reg.advertise(
892 "peer-d".to_string(),
893 vec![Capability::ContentAddressing],
894 0,
895 100,
896 );
897
898 let peers = reg.peers_with_capability(&Capability::Kademlia, 0);
899 assert_eq!(peers, vec!["peer-a", "peer-b", "peer-c"]);
900 }
901
902 #[test]
905 fn test_pcr_peers_with_capability_ignores_expired() {
906 let mut reg = make_registry();
907 reg.advertise("fresh".to_string(), vec![Capability::Kademlia], 0, 100);
908 reg.advertise("stale".to_string(), vec![Capability::Kademlia], 0, 10);
910
911 let peers = reg.peers_with_capability(&Capability::Kademlia, 20);
912 assert_eq!(peers, vec!["fresh"]);
913 }
914
915 #[test]
918 fn test_pcr_peer_capabilities_none_for_expired() {
919 let mut reg = make_registry();
920 reg.advertise("peer-exp".to_string(), vec![Capability::Kademlia], 0, 5);
922 assert!(reg.peer_capabilities("peer-exp", 10).is_none());
923 }
924
925 #[test]
928 fn test_pcr_peer_capabilities_some_for_valid() {
929 let mut reg = make_registry();
930 reg.advertise(
931 "peer-ok".to_string(),
932 vec![Capability::ContentAddressing],
933 0,
934 100,
935 );
936 let caps = reg
937 .peer_capabilities("peer-ok", 50)
938 .expect("should be valid");
939 assert_eq!(caps, &[Capability::ContentAddressing]);
940 }
941
942 #[test]
945 fn test_pcr_peer_capabilities_none_for_unknown() {
946 let reg = make_registry();
947 assert!(reg.peer_capabilities("ghost", 0).is_none());
948 }
949
950 #[test]
953 fn test_pcr_remove_peer_returns_true() {
954 let mut reg = make_registry();
955 reg.advertise("peer-del".to_string(), vec![Capability::Kademlia], 0, 100);
956 assert!(reg.remove_peer("peer-del"));
957 assert!(reg.peer_capabilities("peer-del", 0).is_none());
958 }
959
960 #[test]
963 fn test_pcr_remove_peer_returns_false() {
964 let mut reg = make_registry();
965 assert!(!reg.remove_peer("nobody"));
966 }
967
968 #[test]
971 fn test_pcr_evict_expired_removes_expired() {
972 let mut reg = make_registry();
973 reg.advertise("alive".to_string(), vec![Capability::Kademlia], 0, 100);
974 reg.advertise("dead-1".to_string(), vec![Capability::Kademlia], 0, 5);
975 reg.advertise("dead-2".to_string(), vec![Capability::Kademlia], 0, 3);
976
977 let evicted = reg.evict_expired(10);
978 assert_eq!(evicted, 2);
979 assert!(reg.peer_capabilities("alive", 10).is_some());
981 assert!(reg.peer_capabilities("dead-1", 10).is_none());
982 assert!(reg.peer_capabilities("dead-2", 10).is_none());
983 }
984
985 #[test]
988 fn test_pcr_evict_expired_zero_when_none_expired() {
989 let mut reg = make_registry();
990 reg.advertise("p1".to_string(), vec![Capability::Kademlia], 0, 100);
991 reg.advertise("p2".to_string(), vec![Capability::Kademlia], 0, 200);
992 assert_eq!(reg.evict_expired(0), 0);
993 }
994
995 #[test]
998 fn test_pcr_stats_active_vs_total() {
999 let mut reg = make_registry();
1000 reg.advertise("active-1".to_string(), vec![Capability::Kademlia], 0, 100);
1001 reg.advertise(
1002 "active-2".to_string(),
1003 vec![Capability::ContentAddressing],
1004 0,
1005 100,
1006 );
1007 reg.advertise("expired-1".to_string(), vec![Capability::Kademlia], 0, 5);
1009
1010 let stats = reg.stats(50);
1011 assert_eq!(stats.total_peers, 3);
1012 assert_eq!(stats.active_peers, 2);
1013 assert_eq!(stats.expired_count, 1);
1014 assert_eq!(stats.total_capabilities_registered, 3); }
1016
1017 #[test]
1020 fn test_pcr_gossipsub_topic_matching_exact() {
1021 let mut reg = make_registry();
1022 let topics_a = vec!["news".to_string(), "sports".to_string()];
1023 let topics_b = vec!["weather".to_string()];
1024
1025 reg.advertise(
1026 "peer-gs-a".to_string(),
1027 vec![Capability::GossipSub {
1028 topics: topics_a.clone(),
1029 }],
1030 0,
1031 100,
1032 );
1033 reg.advertise(
1034 "peer-gs-b".to_string(),
1035 vec![Capability::GossipSub {
1036 topics: topics_b.clone(),
1037 }],
1038 0,
1039 100,
1040 );
1041
1042 let peers_news = reg.peers_with_capability(
1044 &Capability::GossipSub {
1045 topics: topics_a.clone(),
1046 },
1047 0,
1048 );
1049 assert_eq!(peers_news, vec!["peer-gs-a"]);
1050
1051 let peers_weather = reg.peers_with_capability(
1052 &Capability::GossipSub {
1053 topics: topics_b.clone(),
1054 },
1055 0,
1056 );
1057 assert_eq!(peers_weather, vec!["peer-gs-b"]);
1058 }
1059
1060 #[test]
1063 fn test_pcr_custom_capability_matching() {
1064 let mut reg = make_registry();
1065 reg.advertise(
1066 "peer-custom".to_string(),
1067 vec![Capability::Custom {
1068 name: "my-proto".to_string(),
1069 version: 3,
1070 }],
1071 0,
1072 100,
1073 );
1074
1075 let peers = reg.peers_with_capability(
1077 &Capability::Custom {
1078 name: "my-proto".to_string(),
1079 version: 3,
1080 },
1081 0,
1082 );
1083 assert_eq!(peers, vec!["peer-custom"]);
1084
1085 let peers_v2 = reg.peers_with_capability(
1087 &Capability::Custom {
1088 name: "my-proto".to_string(),
1089 version: 2,
1090 },
1091 0,
1092 );
1093 assert!(peers_v2.is_empty());
1094
1095 let peers_other = reg.peers_with_capability(
1097 &Capability::Custom {
1098 name: "other-proto".to_string(),
1099 version: 3,
1100 },
1101 0,
1102 );
1103 assert!(peers_other.is_empty());
1104 }
1105
1106 #[test]
1109 fn test_pcr_tensorswap_version_matching() {
1110 let mut reg = make_registry();
1111 reg.advertise(
1112 "peer-ts-v1".to_string(),
1113 vec![Capability::TensorSwap { version: 1 }],
1114 0,
1115 100,
1116 );
1117 reg.advertise(
1118 "peer-ts-v2".to_string(),
1119 vec![Capability::TensorSwap { version: 2 }],
1120 0,
1121 100,
1122 );
1123
1124 let v1_peers = reg.peers_with_capability(&Capability::TensorSwap { version: 1 }, 0);
1125 assert_eq!(v1_peers, vec!["peer-ts-v1"]);
1126
1127 let v2_peers = reg.peers_with_capability(&Capability::TensorSwap { version: 2 }, 0);
1128 assert_eq!(v2_peers, vec!["peer-ts-v2"]);
1129 }
1130
1131 #[test]
1134 fn test_pcr_bitswap_exact_version() {
1135 let mut reg = make_registry();
1136 reg.advertise(
1137 "peer-bs".to_string(),
1138 vec![Capability::Bitswap { version: 1 }],
1139 0,
1140 100,
1141 );
1142
1143 assert_eq!(
1145 reg.peers_with_capability(&Capability::Bitswap { version: 1 }, 0),
1146 vec!["peer-bs"]
1147 );
1148
1149 assert!(reg
1151 .peers_with_capability(&Capability::Bitswap { version: 2 }, 0)
1152 .is_empty());
1153 }
1154
1155 #[test]
1158 fn test_pcr_stats_total_caps_includes_expired() {
1159 let mut reg = make_registry();
1160 reg.advertise(
1162 "active".to_string(),
1163 vec![Capability::Kademlia, Capability::ContentAddressing],
1164 0,
1165 100,
1166 );
1167 reg.advertise(
1169 "expired".to_string(),
1170 vec![Capability::Bitswap { version: 1 }],
1171 0,
1172 5,
1173 );
1174
1175 let stats = reg.stats(50);
1176 assert_eq!(stats.total_capabilities_registered, 3); assert_eq!(stats.active_peers, 1);
1178 assert_eq!(stats.expired_count, 1);
1179 }
1180
1181 #[test]
1184 fn test_pcr_multiple_capabilities_per_peer() {
1185 let mut reg = make_registry();
1186 reg.advertise(
1187 "multi".to_string(),
1188 vec![
1189 Capability::Kademlia,
1190 Capability::Bitswap { version: 1 },
1191 Capability::ContentAddressing,
1192 ],
1193 0,
1194 100,
1195 );
1196
1197 assert_eq!(
1198 reg.peers_with_capability(&Capability::Kademlia, 0),
1199 vec!["multi"]
1200 );
1201 assert_eq!(
1202 reg.peers_with_capability(&Capability::Bitswap { version: 1 }, 0),
1203 vec!["multi"]
1204 );
1205 assert_eq!(
1206 reg.peers_with_capability(&Capability::ContentAddressing, 0),
1207 vec!["multi"]
1208 );
1209 }
1210
1211 #[test]
1214 fn test_pcr_advert_valid_at_boundary() {
1215 let mut reg = make_registry();
1216 reg.advertise("peer-bnd".to_string(), vec![Capability::Kademlia], 0, 10);
1218
1219 assert!(reg.peer_capabilities("peer-bnd", 9).is_some());
1221 assert!(reg.peer_capabilities("peer-bnd", 10).is_none());
1223 }
1224
1225 #[test]
1228 fn test_pcr_empty_registry_stats() {
1229 let reg = make_registry();
1230 let stats = reg.stats(0);
1231 assert_eq!(stats.total_peers, 0);
1232 assert_eq!(stats.active_peers, 0);
1233 assert_eq!(stats.expired_count, 0);
1234 assert_eq!(stats.total_capabilities_registered, 0);
1235 }
1236
1237 #[test]
1240 fn test_pcr_peers_with_capability_empty_registry() {
1241 let reg = make_registry();
1242 assert!(reg
1243 .peers_with_capability(&Capability::Kademlia, 0)
1244 .is_empty());
1245 }
1246
1247 #[test]
1250 fn test_advert_is_valid_boundary() {
1251 let advert = CapabilityAdvertisement {
1252 peer_id: "p".to_string(),
1253 capabilities: vec![],
1254 advertised_at_tick: 0,
1255 expires_at_tick: 50,
1256 };
1257 assert!(advert.is_valid(49));
1258 assert!(!advert.is_valid(50));
1259 assert!(!advert.is_valid(51));
1260 }
1261
1262 #[test]
1265 fn test_advert_has_capability() {
1266 let advert = CapabilityAdvertisement {
1267 peer_id: "p".to_string(),
1268 capabilities: vec![Capability::Kademlia, Capability::ContentAddressing],
1269 advertised_at_tick: 0,
1270 expires_at_tick: 100,
1271 };
1272 assert!(advert.has_capability(&Capability::Kademlia));
1273 assert!(advert.has_capability(&Capability::ContentAddressing));
1274 assert!(!advert.has_capability(&Capability::Bitswap { version: 1 }));
1275 }
1276
1277 #[test]
1280 fn test_pcr_evict_then_re_advertise() {
1281 let mut reg = make_registry();
1282 reg.advertise("peer-evict".to_string(), vec![Capability::Kademlia], 0, 5);
1283
1284 let evicted = reg.evict_expired(10);
1286 assert_eq!(evicted, 1);
1287
1288 reg.advertise(
1290 "peer-evict".to_string(),
1291 vec![Capability::ContentAddressing],
1292 10,
1293 50,
1294 );
1295 let caps = reg
1296 .peer_capabilities("peer-evict", 10)
1297 .expect("should be present after re-advertise");
1298 assert!(caps.contains(&Capability::ContentAddressing));
1299 }
1300
1301 #[test]
1304 fn test_pcr_peers_with_capability_all_expired() {
1305 let mut reg = make_registry();
1306 reg.advertise("peer-x".to_string(), vec![Capability::Kademlia], 0, 1);
1307 reg.advertise("peer-y".to_string(), vec![Capability::Kademlia], 0, 2);
1308
1309 let peers = reg.peers_with_capability(&Capability::Kademlia, 100);
1310 assert!(peers.is_empty());
1311 }
1312}