1use std::collections::{HashMap, VecDeque};
68use std::fmt;
69
70#[derive(Clone, Debug, PartialEq, Eq, Hash)]
79pub struct CapabilityVersion {
80 pub major: u16,
82 pub minor: u16,
84 pub patch: u16,
86}
87
88impl CapabilityVersion {
89 #[inline]
93 pub fn is_compatible_with(&self, required: &CapabilityVersion) -> bool {
94 self.major == required.major && self.minor >= required.minor
95 }
96}
97
98impl fmt::Display for CapabilityVersion {
99 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
100 write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
101 }
102}
103
104impl Default for CapabilityVersion {
105 fn default() -> Self {
106 Self {
107 major: 1,
108 minor: 0,
109 patch: 0,
110 }
111 }
112}
113
114#[derive(Clone, Debug, PartialEq)]
123pub struct PeerCapability {
124 pub name: String,
126 pub version: CapabilityVersion,
128 pub required: bool,
130 pub metadata: HashMap<String, String>,
132}
133
134impl PeerCapability {
135 pub fn required(name: impl Into<String>, version: CapabilityVersion) -> Self {
137 Self {
138 name: name.into(),
139 version,
140 required: true,
141 metadata: HashMap::new(),
142 }
143 }
144
145 pub fn optional(name: impl Into<String>, version: CapabilityVersion) -> Self {
147 Self {
148 name: name.into(),
149 version,
150 required: false,
151 metadata: HashMap::new(),
152 }
153 }
154
155 pub fn with_metadata(
157 name: impl Into<String>,
158 version: CapabilityVersion,
159 required: bool,
160 metadata: HashMap<String, String>,
161 ) -> Self {
162 Self {
163 name: name.into(),
164 version,
165 required,
166 metadata,
167 }
168 }
169}
170
171#[derive(Clone, Debug, PartialEq)]
177pub struct NegotiationOffer {
178 pub peer_id: String,
180 pub capabilities: Vec<PeerCapability>,
182 pub protocol_version: u16,
184 pub timestamp: u64,
186}
187
188impl NegotiationOffer {
189 fn capability_map(&self) -> HashMap<&str, &PeerCapability> {
191 self.capabilities
192 .iter()
193 .map(|c| (c.name.as_str(), c))
194 .collect()
195 }
196}
197
198#[derive(Clone, Debug, PartialEq)]
204pub enum NegotiationResult {
205 Accepted {
207 agreed_capabilities: Vec<String>,
209 rejected_optional: Vec<String>,
211 },
212 Rejected {
214 reason: String,
216 missing_required: Vec<String>,
218 },
219}
220
221impl NegotiationResult {
222 #[inline]
224 pub fn is_accepted(&self) -> bool {
225 matches!(self, NegotiationResult::Accepted { .. })
226 }
227
228 #[inline]
230 pub fn is_rejected(&self) -> bool {
231 matches!(self, NegotiationResult::Rejected { .. })
232 }
233
234 pub fn agreed_capabilities(&self) -> &[String] {
236 match self {
237 NegotiationResult::Accepted {
238 agreed_capabilities,
239 ..
240 } => agreed_capabilities,
241 NegotiationResult::Rejected { .. } => &[],
242 }
243 }
244
245 pub fn rejection_reason(&self) -> Option<&str> {
247 match self {
248 NegotiationResult::Rejected { reason, .. } => Some(reason.as_str()),
249 NegotiationResult::Accepted { .. } => None,
250 }
251 }
252}
253
254#[derive(Clone, Debug, Default, PartialEq)]
260pub enum CapabilityPolicy {
261 RequireAll,
264
265 RequireSubset(Vec<String>),
268
269 #[default]
272 BestEffort,
273}
274
275#[derive(Clone, Debug)]
281pub struct NegotiatorConfig {
282 pub local_capabilities: Vec<PeerCapability>,
284 pub policy: CapabilityPolicy,
286 pub min_protocol_version: u16,
288 pub negotiation_timeout_ms: u64,
291}
292
293impl Default for NegotiatorConfig {
294 fn default() -> Self {
295 Self {
296 local_capabilities: Vec::new(),
297 policy: CapabilityPolicy::BestEffort,
298 min_protocol_version: 1,
299 negotiation_timeout_ms: 5_000,
300 }
301 }
302}
303
304impl NegotiatorConfig {
305 pub fn new(
307 local_capabilities: Vec<PeerCapability>,
308 policy: CapabilityPolicy,
309 min_protocol_version: u16,
310 negotiation_timeout_ms: u64,
311 ) -> Self {
312 Self {
313 local_capabilities,
314 policy,
315 min_protocol_version,
316 negotiation_timeout_ms,
317 }
318 }
319}
320
321#[derive(Clone, Debug)]
327pub struct NegotiationRecord {
328 pub peer_id: String,
330 pub result: NegotiationResult,
332 pub negotiated_at: u64,
334 pub latency_ms: u64,
336}
337
338#[derive(Clone, Debug, PartialEq)]
344pub struct NegotiatorStats {
345 pub total_negotiations: u64,
347 pub total_accepted: u64,
349 pub total_rejected: u64,
351 pub accept_rate: f64,
354 pub capabilities_advertised: usize,
356}
357
358#[derive(Debug)]
375pub struct PeerCapabilityNegotiator {
376 pub config: NegotiatorConfig,
378 pub negotiation_history: VecDeque<NegotiationRecord>,
380 pub max_history: usize,
382 pub total_negotiations: u64,
384 pub total_accepted: u64,
386 pub total_rejected: u64,
388}
389
390impl PeerCapabilityNegotiator {
391 pub fn new(config: NegotiatorConfig, max_history: usize) -> Self {
399 Self {
400 config,
401 negotiation_history: VecDeque::new(),
402 max_history,
403 total_negotiations: 0,
404 total_accepted: 0,
405 total_rejected: 0,
406 }
407 }
408
409 pub fn build_offer(&self, peer_id: String, now: u64) -> NegotiationOffer {
416 NegotiationOffer {
417 peer_id,
418 capabilities: self.config.local_capabilities.clone(),
419 protocol_version: self.config.min_protocol_version,
420 timestamp: now,
421 }
422 }
423
424 pub fn evaluate_offer(&self, offer: &NegotiationOffer) -> NegotiationResult {
436 if offer.protocol_version < self.config.min_protocol_version {
438 return NegotiationResult::Rejected {
439 reason: "protocol version too low".to_string(),
440 missing_required: Vec::new(),
441 };
442 }
443
444 let offer_map = offer.capability_map();
445
446 let missing_required: Vec<String> = self
448 .config
449 .local_capabilities
450 .iter()
451 .filter(|cap| cap.required)
452 .filter(|cap| {
453 offer_map
454 .get(cap.name.as_str())
455 .map(|peer_cap| !peer_cap.version.is_compatible_with(&cap.version))
456 .unwrap_or(true) })
458 .map(|cap| cap.name.clone())
459 .collect();
460
461 match &self.config.policy {
463 CapabilityPolicy::RequireAll => {
464 if !missing_required.is_empty() {
465 return NegotiationResult::Rejected {
466 reason: "missing required capabilities".to_string(),
467 missing_required,
468 };
469 }
470 let agreed = self.compute_agreed(&offer_map);
471 let rejected_optional = self.compute_rejected_optional(&offer_map);
472 NegotiationResult::Accepted {
473 agreed_capabilities: agreed,
474 rejected_optional,
475 }
476 }
477
478 CapabilityPolicy::RequireSubset(required_names) => {
479 let missing_subset: Vec<String> = required_names
480 .iter()
481 .filter(|name| {
482 let local_cap = self
484 .config
485 .local_capabilities
486 .iter()
487 .find(|c| &c.name == *name);
488 match local_cap {
489 None => false, Some(local) => offer_map
491 .get(local.name.as_str())
492 .map(|peer_cap| {
493 !peer_cap.version.is_compatible_with(&local.version)
494 })
495 .unwrap_or(true),
496 }
497 })
498 .cloned()
499 .collect();
500
501 if !missing_subset.is_empty() {
502 return NegotiationResult::Rejected {
503 reason: "missing required subset capabilities".to_string(),
504 missing_required: missing_subset,
505 };
506 }
507 let agreed = self.compute_agreed(&offer_map);
508 let rejected_optional = self.compute_rejected_optional(&offer_map);
509 NegotiationResult::Accepted {
510 agreed_capabilities: agreed,
511 rejected_optional,
512 }
513 }
514
515 CapabilityPolicy::BestEffort => {
516 let agreed = self.compute_agreed(&offer_map);
518 let mut rejected_optional = self.compute_rejected_optional(&offer_map);
520 for name in &missing_required {
521 if !rejected_optional.contains(name) {
522 rejected_optional.push(name.clone());
523 }
524 }
525 NegotiationResult::Accepted {
526 agreed_capabilities: agreed,
527 rejected_optional,
528 }
529 }
530 }
531 }
532
533 pub fn negotiate(
550 &mut self,
551 peer_id: String,
552 offer: NegotiationOffer,
553 start_ms: u64,
554 end_ms: u64,
555 ) -> &NegotiationResult {
556 let result = self.evaluate_offer(&offer);
557 let latency_ms = end_ms.saturating_sub(start_ms);
558
559 self.total_negotiations += 1;
561 if result.is_accepted() {
562 self.total_accepted += 1;
563 } else {
564 self.total_rejected += 1;
565 }
566
567 let record = NegotiationRecord {
569 peer_id,
570 result,
571 negotiated_at: end_ms,
572 latency_ms,
573 };
574
575 if self.max_history > 0 {
576 if self.negotiation_history.len() >= self.max_history {
577 self.negotiation_history.pop_front();
578 }
579 self.negotiation_history.push_back(record);
580 &self.negotiation_history.back().expect("just pushed").result
582 } else {
583 self.negotiation_history.clear();
592 self.negotiation_history.push_back(record);
593 &self.negotiation_history[0].result
594 }
595 }
596
597 pub fn is_capability_supported(&self, name: &str, version: &CapabilityVersion) -> bool {
602 self.config
603 .local_capabilities
604 .iter()
605 .any(|cap| cap.name == name && cap.version.is_compatible_with(version))
606 }
607
608 pub fn common_capabilities(&self, offer: &NegotiationOffer) -> Vec<String> {
611 let offer_map = offer.capability_map();
612 self.compute_agreed(&offer_map)
613 }
614
615 pub fn history_for_peer(&self, peer_id: &str) -> Vec<&NegotiationRecord> {
619 self.negotiation_history
620 .iter()
621 .filter(|r| r.peer_id == peer_id)
622 .collect()
623 }
624
625 pub fn negotiator_stats(&self) -> NegotiatorStats {
627 let accept_rate = if self.total_negotiations == 0 {
628 0.0
629 } else {
630 self.total_accepted as f64 / self.total_negotiations as f64
631 };
632 NegotiatorStats {
633 total_negotiations: self.total_negotiations,
634 total_accepted: self.total_accepted,
635 total_rejected: self.total_rejected,
636 accept_rate,
637 capabilities_advertised: self.config.local_capabilities.len(),
638 }
639 }
640
641 fn compute_agreed<'a>(&self, offer_map: &HashMap<&'a str, &'a PeerCapability>) -> Vec<String> {
645 self.config
646 .local_capabilities
647 .iter()
648 .filter(|local_cap| {
649 offer_map
650 .get(local_cap.name.as_str())
651 .map(|peer_cap| peer_cap.version.is_compatible_with(&local_cap.version))
652 .unwrap_or(false)
653 })
654 .map(|cap| cap.name.clone())
655 .collect()
656 }
657
658 fn compute_rejected_optional<'a>(
660 &self,
661 offer_map: &HashMap<&'a str, &'a PeerCapability>,
662 ) -> Vec<String> {
663 self.config
664 .local_capabilities
665 .iter()
666 .filter(|cap| !cap.required)
667 .filter(|cap| {
668 offer_map
669 .get(cap.name.as_str())
670 .map(|peer_cap| !peer_cap.version.is_compatible_with(&cap.version))
671 .unwrap_or(true)
672 })
673 .map(|cap| cap.name.clone())
674 .collect()
675 }
676}
677
678#[cfg(test)]
683mod tests {
684 use crate::peer_capability_negotiator::{
685 CapabilityPolicy, CapabilityVersion, NegotiationOffer, NegotiationResult, NegotiatorConfig,
686 NegotiatorStats, PeerCapability, PeerCapabilityNegotiator,
687 };
688 use std::collections::HashMap;
689
690 fn ver(major: u16, minor: u16, patch: u16) -> CapabilityVersion {
693 CapabilityVersion {
694 major,
695 minor,
696 patch,
697 }
698 }
699
700 fn cap_req(name: &str, major: u16, minor: u16) -> PeerCapability {
701 PeerCapability::required(name, ver(major, minor, 0))
702 }
703
704 fn cap_opt(name: &str, major: u16, minor: u16) -> PeerCapability {
705 PeerCapability::optional(name, ver(major, minor, 0))
706 }
707
708 fn offer(peer_id: &str, caps: Vec<PeerCapability>, proto: u16) -> NegotiationOffer {
709 NegotiationOffer {
710 peer_id: peer_id.to_string(),
711 capabilities: caps,
712 protocol_version: proto,
713 timestamp: 1_000,
714 }
715 }
716
717 fn make_negotiator(
718 caps: Vec<PeerCapability>,
719 policy: CapabilityPolicy,
720 min_proto: u16,
721 ) -> PeerCapabilityNegotiator {
722 PeerCapabilityNegotiator::new(NegotiatorConfig::new(caps, policy, min_proto, 5_000), 64)
723 }
724
725 #[test]
728 fn test_version_compatible_same_major_higher_minor() {
729 let v = ver(2, 3, 0);
730 let req = ver(2, 1, 0);
731 assert!(v.is_compatible_with(&req));
732 }
733
734 #[test]
735 fn test_version_compatible_same_exact() {
736 let v = ver(1, 0, 0);
737 assert!(v.is_compatible_with(&v));
738 }
739
740 #[test]
741 fn test_version_incompatible_major_mismatch() {
742 let v = ver(2, 5, 0);
743 let req = ver(1, 5, 0);
744 assert!(!v.is_compatible_with(&req));
745 }
746
747 #[test]
748 fn test_version_incompatible_lower_minor() {
749 let v = ver(1, 0, 0);
750 let req = ver(1, 1, 0);
751 assert!(!v.is_compatible_with(&req));
752 }
753
754 #[test]
755 fn test_version_display() {
756 assert_eq!(ver(3, 14, 159).to_string(), "3.14.159");
757 }
758
759 #[test]
760 fn test_version_default() {
761 let d = CapabilityVersion::default();
762 assert_eq!(d.major, 1);
763 assert_eq!(d.minor, 0);
764 assert_eq!(d.patch, 0);
765 }
766
767 #[test]
768 fn test_version_patch_irrelevant_for_compat() {
769 let v = ver(1, 2, 0);
771 let req = ver(1, 2, 99);
772 assert!(v.is_compatible_with(&req));
773 }
774
775 #[test]
778 fn test_peer_capability_required_constructor() {
779 let cap = PeerCapability::required("graphsync", ver(1, 0, 0));
780 assert!(cap.required);
781 assert_eq!(cap.name, "graphsync");
782 assert!(cap.metadata.is_empty());
783 }
784
785 #[test]
786 fn test_peer_capability_optional_constructor() {
787 let cap = PeerCapability::optional("relay", ver(2, 0, 0));
788 assert!(!cap.required);
789 }
790
791 #[test]
792 fn test_peer_capability_with_metadata() {
793 let mut meta = HashMap::new();
794 meta.insert("max_block_size".to_string(), "1048576".to_string());
795 let cap = PeerCapability::with_metadata("bitswap", ver(2, 0, 0), true, meta.clone());
796 assert_eq!(cap.metadata, meta);
797 }
798
799 #[test]
802 fn test_result_is_accepted() {
803 let r = NegotiationResult::Accepted {
804 agreed_capabilities: vec!["bitswap".to_string()],
805 rejected_optional: vec![],
806 };
807 assert!(r.is_accepted());
808 assert!(!r.is_rejected());
809 }
810
811 #[test]
812 fn test_result_is_rejected() {
813 let r = NegotiationResult::Rejected {
814 reason: "test".to_string(),
815 missing_required: vec![],
816 };
817 assert!(r.is_rejected());
818 assert!(!r.is_accepted());
819 }
820
821 #[test]
822 fn test_result_agreed_capabilities_accepted() {
823 let names = vec!["a".to_string(), "b".to_string()];
824 let r = NegotiationResult::Accepted {
825 agreed_capabilities: names.clone(),
826 rejected_optional: vec![],
827 };
828 assert_eq!(r.agreed_capabilities(), names.as_slice());
829 }
830
831 #[test]
832 fn test_result_agreed_capabilities_rejected_empty() {
833 let r = NegotiationResult::Rejected {
834 reason: "no".to_string(),
835 missing_required: vec![],
836 };
837 assert!(r.agreed_capabilities().is_empty());
838 }
839
840 #[test]
841 fn test_result_rejection_reason() {
842 let r = NegotiationResult::Rejected {
843 reason: "protocol version too low".to_string(),
844 missing_required: vec![],
845 };
846 assert_eq!(r.rejection_reason(), Some("protocol version too low"));
847 }
848
849 #[test]
852 fn test_evaluate_rejects_low_protocol_version() {
853 let neg = make_negotiator(vec![], CapabilityPolicy::RequireAll, 3);
854 let result = neg.evaluate_offer(&offer("peer1", vec![], 2));
855 assert!(result.is_rejected());
856 assert_eq!(result.rejection_reason(), Some("protocol version too low"));
857 }
858
859 #[test]
860 fn test_evaluate_accepts_exact_protocol_version() {
861 let neg = make_negotiator(vec![], CapabilityPolicy::RequireAll, 2);
862 let result = neg.evaluate_offer(&offer("peer1", vec![], 2));
863 assert!(result.is_accepted());
864 }
865
866 #[test]
867 fn test_evaluate_accepts_higher_protocol_version() {
868 let neg = make_negotiator(vec![], CapabilityPolicy::RequireAll, 1);
869 let result = neg.evaluate_offer(&offer("peer1", vec![], 5));
870 assert!(result.is_accepted());
871 }
872
873 #[test]
876 fn test_require_all_rejects_missing_required() {
877 let neg = make_negotiator(
878 vec![cap_req("bitswap", 2, 0)],
879 CapabilityPolicy::RequireAll,
880 1,
881 );
882 let result = neg.evaluate_offer(&offer("peer", vec![], 1));
883 assert!(result.is_rejected());
884 match result {
885 NegotiationResult::Rejected {
886 missing_required, ..
887 } => {
888 assert!(missing_required.contains(&"bitswap".to_string()));
889 }
890 _ => panic!("expected Rejected"),
891 }
892 }
893
894 #[test]
895 fn test_require_all_rejects_version_incompatible() {
896 let neg = make_negotiator(
897 vec![cap_req("bitswap", 2, 3)],
898 CapabilityPolicy::RequireAll,
899 1,
900 );
901 let result = neg.evaluate_offer(&offer("peer", vec![cap_opt("bitswap", 2, 1)], 1));
903 assert!(result.is_rejected());
904 }
905
906 #[test]
907 fn test_require_all_accepts_compatible_versions() {
908 let neg = make_negotiator(
909 vec![cap_req("bitswap", 2, 0)],
910 CapabilityPolicy::RequireAll,
911 1,
912 );
913 let result = neg.evaluate_offer(&offer("peer", vec![cap_opt("bitswap", 2, 1)], 1));
915 assert!(result.is_accepted());
916 }
917
918 #[test]
919 fn test_require_all_agreed_capabilities_correct() {
920 let neg = make_negotiator(
921 vec![cap_req("bitswap", 2, 0), cap_opt("relay", 1, 0)],
922 CapabilityPolicy::RequireAll,
923 1,
924 );
925 let peer_caps = vec![cap_opt("bitswap", 2, 2), cap_opt("relay", 1, 1)];
926 let result = neg.evaluate_offer(&offer("peer", peer_caps, 1));
927 match result {
928 NegotiationResult::Accepted {
929 agreed_capabilities,
930 ..
931 } => {
932 assert!(agreed_capabilities.contains(&"bitswap".to_string()));
933 assert!(agreed_capabilities.contains(&"relay".to_string()));
934 }
935 _ => panic!("expected Accepted"),
936 }
937 }
938
939 #[test]
940 fn test_require_all_rejected_optional_populated() {
941 let neg = make_negotiator(
942 vec![
943 cap_req("bitswap", 2, 0),
944 cap_opt("graphsync", 1, 0), ],
946 CapabilityPolicy::RequireAll,
947 1,
948 );
949 let result = neg.evaluate_offer(&offer("peer", vec![cap_opt("bitswap", 2, 0)], 1));
950 match result {
951 NegotiationResult::Accepted {
952 rejected_optional, ..
953 } => {
954 assert!(rejected_optional.contains(&"graphsync".to_string()));
955 }
956 _ => panic!("expected Accepted"),
957 }
958 }
959
960 #[test]
963 fn test_require_subset_rejects_missing_subset_member() {
964 let neg = make_negotiator(
965 vec![cap_req("bitswap", 2, 0), cap_req("graphsync", 1, 0)],
966 CapabilityPolicy::RequireSubset(vec!["bitswap".to_string()]),
967 1,
968 );
969 let result = neg.evaluate_offer(&offer("peer", vec![cap_opt("graphsync", 1, 0)], 1));
971 assert!(result.is_rejected());
972 }
973
974 #[test]
975 fn test_require_subset_accepts_when_subset_present() {
976 let neg = make_negotiator(
977 vec![cap_req("bitswap", 2, 0), cap_req("graphsync", 1, 0)],
978 CapabilityPolicy::RequireSubset(vec!["bitswap".to_string()]),
979 1,
980 );
981 let result = neg.evaluate_offer(&offer("peer", vec![cap_opt("bitswap", 2, 1)], 1));
983 assert!(result.is_accepted());
984 }
985
986 #[test]
987 fn test_require_subset_rejects_version_incompatible() {
988 let neg = make_negotiator(
989 vec![cap_req("bitswap", 2, 5)],
990 CapabilityPolicy::RequireSubset(vec!["bitswap".to_string()]),
991 1,
992 );
993 let result = neg.evaluate_offer(&offer("peer", vec![cap_opt("bitswap", 2, 3)], 1));
995 assert!(result.is_rejected());
996 }
997
998 #[test]
999 fn test_require_subset_unknown_subset_name_ignored() {
1000 let neg = make_negotiator(
1002 vec![cap_req("bitswap", 2, 0)],
1003 CapabilityPolicy::RequireSubset(vec!["unknown".to_string()]),
1004 1,
1005 );
1006 let result = neg.evaluate_offer(&offer("peer", vec![], 1));
1007 assert!(result.is_accepted());
1009 }
1010
1011 #[test]
1014 fn test_best_effort_always_accepted() {
1015 let neg = make_negotiator(
1016 vec![cap_req("bitswap", 2, 0)],
1017 CapabilityPolicy::BestEffort,
1018 1,
1019 );
1020 let result = neg.evaluate_offer(&offer("peer", vec![], 1));
1022 assert!(result.is_accepted());
1023 }
1024
1025 #[test]
1026 fn test_best_effort_missing_required_in_rejected_optional() {
1027 let neg = make_negotiator(
1028 vec![cap_req("bitswap", 2, 0)],
1029 CapabilityPolicy::BestEffort,
1030 1,
1031 );
1032 let result = neg.evaluate_offer(&offer("peer", vec![], 1));
1033 match result {
1034 NegotiationResult::Accepted {
1035 rejected_optional, ..
1036 } => {
1037 assert!(rejected_optional.contains(&"bitswap".to_string()));
1038 }
1039 _ => panic!("expected Accepted"),
1040 }
1041 }
1042
1043 #[test]
1044 fn test_best_effort_agreed_when_present() {
1045 let neg = make_negotiator(
1046 vec![cap_req("bitswap", 2, 0)],
1047 CapabilityPolicy::BestEffort,
1048 1,
1049 );
1050 let result = neg.evaluate_offer(&offer("peer", vec![cap_opt("bitswap", 2, 1)], 1));
1051 match result {
1052 NegotiationResult::Accepted {
1053 agreed_capabilities,
1054 rejected_optional,
1055 } => {
1056 assert!(agreed_capabilities.contains(&"bitswap".to_string()));
1057 assert!(rejected_optional.is_empty());
1058 }
1059 _ => panic!("expected Accepted"),
1060 }
1061 }
1062
1063 #[test]
1066 fn test_build_offer_contains_local_capabilities() {
1067 let neg = make_negotiator(
1068 vec![cap_req("bitswap", 2, 0), cap_opt("relay", 1, 0)],
1069 CapabilityPolicy::BestEffort,
1070 1,
1071 );
1072 let o = neg.build_offer("local-peer".to_string(), 42_000);
1073 assert_eq!(o.peer_id, "local-peer");
1074 assert_eq!(o.timestamp, 42_000);
1075 assert_eq!(o.capabilities.len(), 2);
1076 assert_eq!(o.protocol_version, 1);
1077 }
1078
1079 #[test]
1080 fn test_build_offer_empty_capabilities() {
1081 let neg = make_negotiator(vec![], CapabilityPolicy::BestEffort, 2);
1082 let o = neg.build_offer("me".to_string(), 0);
1083 assert!(o.capabilities.is_empty());
1084 assert_eq!(o.protocol_version, 2);
1085 }
1086
1087 #[test]
1090 fn test_negotiate_increments_total() {
1091 let mut neg = make_negotiator(vec![], CapabilityPolicy::BestEffort, 1);
1092 neg.negotiate("p1".to_string(), offer("p1", vec![], 1), 0, 10);
1093 assert_eq!(neg.total_negotiations, 1);
1094 }
1095
1096 #[test]
1097 fn test_negotiate_increments_accepted_counter() {
1098 let mut neg = make_negotiator(vec![], CapabilityPolicy::BestEffort, 1);
1099 neg.negotiate("p1".to_string(), offer("p1", vec![], 1), 0, 10);
1100 assert_eq!(neg.total_accepted, 1);
1101 assert_eq!(neg.total_rejected, 0);
1102 }
1103
1104 #[test]
1105 fn test_negotiate_increments_rejected_counter() {
1106 let mut neg = make_negotiator(vec![], CapabilityPolicy::RequireAll, 5);
1107 neg.negotiate("p1".to_string(), offer("p1", vec![], 1), 0, 10);
1108 assert_eq!(neg.total_rejected, 1);
1109 assert_eq!(neg.total_accepted, 0);
1110 }
1111
1112 #[test]
1113 fn test_negotiate_result_stored_in_history() {
1114 let mut neg = make_negotiator(vec![], CapabilityPolicy::BestEffort, 1);
1115 neg.negotiate("p1".to_string(), offer("p1", vec![], 1), 100, 150);
1116 assert_eq!(neg.negotiation_history.len(), 1);
1117 let rec = &neg.negotiation_history[0];
1118 assert_eq!(rec.peer_id, "p1");
1119 assert_eq!(rec.latency_ms, 50);
1120 assert_eq!(rec.negotiated_at, 150);
1121 }
1122
1123 #[test]
1124 fn test_negotiate_evicts_oldest_when_full() {
1125 let config = NegotiatorConfig::new(vec![], CapabilityPolicy::BestEffort, 1, 5_000);
1126 let mut neg = PeerCapabilityNegotiator::new(config, 2);
1127 neg.negotiate("p1".to_string(), offer("p1", vec![], 1), 0, 1);
1128 neg.negotiate("p2".to_string(), offer("p2", vec![], 1), 1, 2);
1129 neg.negotiate("p3".to_string(), offer("p3", vec![], 1), 2, 3);
1130 assert_eq!(neg.negotiation_history.len(), 2);
1131 assert_eq!(neg.negotiation_history[0].peer_id, "p2");
1133 assert_eq!(neg.negotiation_history[1].peer_id, "p3");
1134 }
1135
1136 #[test]
1137 fn test_negotiate_returns_correct_result_ref() {
1138 let mut neg = make_negotiator(vec![], CapabilityPolicy::BestEffort, 1);
1139 let result = neg.negotiate("p".to_string(), offer("p", vec![], 1), 0, 5);
1140 assert!(result.is_accepted());
1141 }
1142
1143 #[test]
1146 fn test_is_capability_supported_true() {
1147 let neg = make_negotiator(
1148 vec![cap_req("bitswap", 2, 3)],
1149 CapabilityPolicy::BestEffort,
1150 1,
1151 );
1152 assert!(neg.is_capability_supported("bitswap", &ver(2, 1, 0)));
1154 }
1155
1156 #[test]
1157 fn test_is_capability_supported_false_wrong_name() {
1158 let neg = make_negotiator(
1159 vec![cap_req("bitswap", 2, 0)],
1160 CapabilityPolicy::BestEffort,
1161 1,
1162 );
1163 assert!(!neg.is_capability_supported("graphsync", &ver(1, 0, 0)));
1164 }
1165
1166 #[test]
1167 fn test_is_capability_supported_false_incompatible_version() {
1168 let neg = make_negotiator(
1169 vec![cap_req("bitswap", 2, 0)],
1170 CapabilityPolicy::BestEffort,
1171 1,
1172 );
1173 assert!(!neg.is_capability_supported("bitswap", &ver(2, 5, 0)));
1175 }
1176
1177 #[test]
1180 fn test_common_capabilities_intersection() {
1181 let neg = make_negotiator(
1182 vec![cap_req("bitswap", 2, 0), cap_opt("relay", 1, 0)],
1183 CapabilityPolicy::BestEffort,
1184 1,
1185 );
1186 let peer_caps = vec![cap_opt("bitswap", 2, 1), cap_opt("graphsync", 1, 0)];
1187 let common = neg.common_capabilities(&offer("peer", peer_caps, 1));
1188 assert!(common.contains(&"bitswap".to_string()));
1189 assert!(!common.contains(&"relay".to_string()));
1190 assert!(!common.contains(&"graphsync".to_string()));
1191 }
1192
1193 #[test]
1194 fn test_common_capabilities_empty_when_no_overlap() {
1195 let neg = make_negotiator(
1196 vec![cap_req("bitswap", 2, 0)],
1197 CapabilityPolicy::BestEffort,
1198 1,
1199 );
1200 let common = neg.common_capabilities(&offer("peer", vec![], 1));
1201 assert!(common.is_empty());
1202 }
1203
1204 #[test]
1207 fn test_history_for_peer_filtered() {
1208 let mut neg = make_negotiator(vec![], CapabilityPolicy::BestEffort, 1);
1209 neg.negotiate("alice".to_string(), offer("alice", vec![], 1), 0, 1);
1210 neg.negotiate("bob".to_string(), offer("bob", vec![], 1), 1, 2);
1211 neg.negotiate("alice".to_string(), offer("alice", vec![], 1), 2, 3);
1212
1213 let alice_history = neg.history_for_peer("alice");
1214 assert_eq!(alice_history.len(), 2);
1215 assert!(alice_history.iter().all(|r| r.peer_id == "alice"));
1216 }
1217
1218 #[test]
1219 fn test_history_for_peer_empty_for_unknown() {
1220 let neg = make_negotiator(vec![], CapabilityPolicy::BestEffort, 1);
1221 assert!(neg.history_for_peer("nobody").is_empty());
1222 }
1223
1224 #[test]
1227 fn test_stats_zero_accept_rate_when_no_negotiations() {
1228 let neg = make_negotiator(vec![], CapabilityPolicy::BestEffort, 1);
1229 let stats = neg.negotiator_stats();
1230 assert_eq!(stats.total_negotiations, 0);
1231 assert_eq!(stats.accept_rate, 0.0);
1232 }
1233
1234 #[test]
1235 fn test_stats_accept_rate_calculation() {
1236 let mut neg = make_negotiator(vec![], CapabilityPolicy::BestEffort, 1);
1237 neg.negotiate("p1".to_string(), offer("p1", vec![], 1), 0, 1);
1239 neg.negotiate("p2".to_string(), offer("p2", vec![], 1), 1, 2);
1240 neg.negotiate("p3".to_string(), offer("p3", vec![], 0), 2, 3);
1242 let stats = neg.negotiator_stats();
1243 assert_eq!(stats.total_negotiations, 3);
1244 assert_eq!(stats.total_accepted, 2);
1245 assert_eq!(stats.total_rejected, 1);
1246 let expected = 2.0 / 3.0;
1247 assert!((stats.accept_rate - expected).abs() < 1e-10);
1248 }
1249
1250 #[test]
1251 fn test_stats_capabilities_advertised_count() {
1252 let neg = make_negotiator(
1253 vec![cap_req("a", 1, 0), cap_opt("b", 1, 0), cap_req("c", 1, 0)],
1254 CapabilityPolicy::BestEffort,
1255 1,
1256 );
1257 assert_eq!(neg.negotiator_stats().capabilities_advertised, 3);
1258 }
1259
1260 #[test]
1261 fn test_stats_is_clone() {
1262 let neg = make_negotiator(vec![], CapabilityPolicy::BestEffort, 1);
1263 let stats: NegotiatorStats = neg.negotiator_stats();
1264 let _cloned = stats.clone();
1265 }
1266
1267 #[test]
1270 fn test_config_default_values() {
1271 let cfg = NegotiatorConfig::default();
1272 assert!(cfg.local_capabilities.is_empty());
1273 assert_eq!(cfg.policy, CapabilityPolicy::BestEffort);
1274 assert_eq!(cfg.min_protocol_version, 1);
1275 assert_eq!(cfg.negotiation_timeout_ms, 5_000);
1276 }
1277
1278 #[test]
1281 fn test_policy_default_is_best_effort() {
1282 assert_eq!(CapabilityPolicy::default(), CapabilityPolicy::BestEffort);
1283 }
1284
1285 #[test]
1288 fn test_negotiate_max_history_zero() {
1289 let config = NegotiatorConfig::new(vec![], CapabilityPolicy::BestEffort, 1, 5_000);
1290 let mut neg = PeerCapabilityNegotiator::new(config, 0);
1291 let result = neg.negotiate("peer".to_string(), offer("peer", vec![], 1), 0, 1);
1293 assert!(result.is_accepted());
1294 assert_eq!(neg.total_negotiations, 1);
1295 neg.negotiate("peer".to_string(), offer("peer", vec![], 1), 1, 2);
1297 assert_eq!(neg.negotiation_history.len(), 1);
1298 }
1299
1300 #[test]
1301 fn test_multiple_agreed_capabilities_deduplication_not_needed() {
1302 let neg = make_negotiator(
1305 vec![cap_req("bitswap", 2, 0)],
1306 CapabilityPolicy::RequireAll,
1307 1,
1308 );
1309 let peer_caps = vec![
1310 cap_opt("bitswap", 2, 1),
1311 cap_opt("bitswap", 2, 2), ];
1313 let result = neg.evaluate_offer(&offer("peer", peer_caps, 1));
1314 assert!(result.is_accepted());
1316 }
1317
1318 #[test]
1319 fn test_latency_stored_in_record() {
1320 let mut neg = make_negotiator(vec![], CapabilityPolicy::BestEffort, 1);
1321 neg.negotiate("p".to_string(), offer("p", vec![], 1), 1_000, 1_075);
1322 assert_eq!(neg.negotiation_history[0].latency_ms, 75);
1323 }
1324
1325 #[test]
1326 fn test_saturation_on_end_before_start() {
1327 let mut neg = make_negotiator(vec![], CapabilityPolicy::BestEffort, 1);
1329 neg.negotiate("p".to_string(), offer("p", vec![], 1), 1_000, 500);
1330 assert_eq!(neg.negotiation_history[0].latency_ms, 0);
1331 }
1332
1333 #[test]
1334 fn test_mixed_required_optional_require_all() {
1335 let neg = make_negotiator(
1337 vec![
1338 cap_req("bitswap", 2, 0),
1339 cap_opt("graphsync", 1, 0),
1340 cap_opt("relay", 1, 0),
1341 ],
1342 CapabilityPolicy::RequireAll,
1343 1,
1344 );
1345 let peer_caps = vec![cap_opt("bitswap", 2, 0), cap_opt("graphsync", 1, 1)];
1346 let result = neg.evaluate_offer(&offer("peer", peer_caps, 1));
1347 match result {
1348 NegotiationResult::Accepted {
1349 agreed_capabilities,
1350 rejected_optional,
1351 } => {
1352 assert!(agreed_capabilities.contains(&"bitswap".to_string()));
1353 assert!(agreed_capabilities.contains(&"graphsync".to_string()));
1354 assert!(rejected_optional.contains(&"relay".to_string()));
1355 }
1356 _ => panic!("expected Accepted"),
1357 }
1358 }
1359}