Skip to main content

ipfrs_network/
protocol_version.rs

1//! Protocol versioning and compatibility negotiation for P2P connections.
2//!
3//! This module provides:
4//! - Semantic version representation for protocols (`ProtocolVersion`)
5//! - Compatibility classification between local and remote protocol versions
6//! - Protocol descriptor with feature advertisement
7//! - Multi-protocol version negotiation (`ProtocolVersionManager`)
8//! - Statistics tracking for negotiation outcomes
9//!
10//! ## Design Rationale
11//!
12//! Compatibility is evaluated against a per-protocol `min_compatible` floor.
13//! A remote peer speaking version `R` is:
14//!
15//! - **FullyCompatible**   — `R == local`
16//! - **BackwardCompatible** — `R < local` but `R >= min_compatible` (we speak down to them)
17//! - **ForwardCompatible**  — `R > local` (they are newer; we use our feature set)
18//! - **Incompatible**       — `R < min_compatible` (wire format too old to speak safely)
19//!
20//! Feature negotiation produces the intersection of advertised feature sets.
21//! Features present locally but absent from the intersection are recorded in
22//! `NegotiationResult::dropped_features`.
23
24use std::collections::HashMap;
25use std::fmt;
26
27// ────────────────────────────────────────────────────────────────────────────
28// ProtocolVersion
29// ────────────────────────────────────────────────────────────────────────────
30
31/// A semantic version triple for a protocol (`major.minor.patch`).
32///
33/// Ordering follows standard semver lexicographic comparison: major is most
34/// significant, then minor, then patch.
35#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
36pub struct ProtocolVersion {
37    pub major: u32,
38    pub minor: u32,
39    pub patch: u32,
40}
41
42impl ProtocolVersion {
43    /// Construct a new version triple.
44    pub fn new(major: u32, minor: u32, patch: u32) -> Self {
45        Self {
46            major,
47            minor,
48            patch,
49        }
50    }
51
52    /// Parse a dotted version string such as `"1.2.3"`.
53    ///
54    /// Returns `None` if the string does not contain exactly three
55    /// dot-separated numeric components.
56    pub fn parse(s: &str) -> Option<Self> {
57        let parts: Vec<&str> = s.split('.').collect();
58        if parts.len() != 3 {
59            return None;
60        }
61        let major = parts[0].parse::<u32>().ok()?;
62        let minor = parts[1].parse::<u32>().ok()?;
63        let patch = parts[2].parse::<u32>().ok()?;
64        Some(Self::new(major, minor, patch))
65    }
66
67    /// Determine the `CompatibilityLevel` of `self` (the local version) with
68    /// respect to `other` (the remote version), given `min_compatible` as the
69    /// oldest version we are willing to speak with.
70    ///
71    /// Rules (evaluated in priority order):
72    ///
73    /// 1. `other < min_compatible` → `Incompatible`
74    /// 2. `other == self`          → `FullyCompatible`
75    /// 3. `other < self`           → `BackwardCompatible` (remote is older)
76    /// 4. `other > self`           → `ForwardCompatible`  (remote is newer)
77    pub fn is_compatible_with(&self, other: &Self, min_compatible: &Self) -> CompatibilityLevel {
78        if other < min_compatible {
79            return CompatibilityLevel::Incompatible;
80        }
81        if other == self {
82            CompatibilityLevel::FullyCompatible
83        } else if other < self {
84            CompatibilityLevel::BackwardCompatible
85        } else {
86            CompatibilityLevel::ForwardCompatible
87        }
88    }
89
90    /// Return the canonical string representation `"major.minor.patch"`.
91    pub fn to_string_repr(&self) -> String {
92        format!("{}.{}.{}", self.major, self.minor, self.patch)
93    }
94}
95
96impl fmt::Display for ProtocolVersion {
97    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98        write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
99    }
100}
101
102// ────────────────────────────────────────────────────────────────────────────
103// CompatibilityLevel
104// ────────────────────────────────────────────────────────────────────────────
105
106/// Classification of the compatibility relationship between two protocol
107/// version endpoints.
108#[derive(Debug, Clone, PartialEq, Eq)]
109pub enum CompatibilityLevel {
110    /// Both peers run the identical version — full feature parity guaranteed.
111    FullyCompatible,
112    /// The remote peer is older than us but at or above the minimum floor.
113    /// We adapt downward to serve them.
114    BackwardCompatible,
115    /// The remote peer is newer than us.  We can still communicate using our
116    /// own (older) feature set, but some of their features will be unavailable.
117    ForwardCompatible,
118    /// The remote version is below our `min_compatible` floor.
119    /// Communication is not safe and negotiation should be rejected.
120    Incompatible,
121}
122
123// ────────────────────────────────────────────────────────────────────────────
124// ProtocolDescriptor
125// ────────────────────────────────────────────────────────────────────────────
126
127/// Full description of a protocol as advertised by one side of a connection.
128///
129/// A `ProtocolDescriptor` captures both the current version and the oldest
130/// version that the advertising peer can still speak (`min_compatible`), plus
131/// the list of optional features it supports.
132#[derive(Debug, Clone)]
133pub struct ProtocolDescriptor {
134    /// Human-readable protocol name, e.g. `"ipfrs/bitswap"`.
135    pub name: String,
136    /// The version this peer is currently running.
137    pub version: ProtocolVersion,
138    /// The oldest version this peer can still inter-operate with.
139    pub min_compatible: ProtocolVersion,
140    /// Optional feature strings this peer advertises.
141    pub features: Vec<String>,
142}
143
144impl ProtocolDescriptor {
145    /// Convenience constructor.
146    pub fn new(
147        name: impl Into<String>,
148        version: ProtocolVersion,
149        min_compatible: ProtocolVersion,
150        features: Vec<String>,
151    ) -> Self {
152        Self {
153            name: name.into(),
154            version,
155            min_compatible,
156            features,
157        }
158    }
159}
160
161// ────────────────────────────────────────────────────────────────────────────
162// NegotiationResult
163// ────────────────────────────────────────────────────────────────────────────
164
165/// The outcome of a successful protocol version negotiation between two peers.
166///
167/// When negotiation fails entirely (e.g. unknown protocol or incompatible
168/// version), `ProtocolVersionManager::negotiate` returns `None`.
169#[derive(Debug, Clone)]
170pub struct NegotiationResult {
171    /// The protocol name that was negotiated.
172    pub protocol: String,
173    /// The version that both sides agreed to operate at.
174    ///
175    /// This is the *lower* of the two versions (local min or remote version)
176    /// ensuring backward compatibility.
177    pub agreed_version: ProtocolVersion,
178    /// Overall compatibility classification.
179    pub compatibility: CompatibilityLevel,
180    /// Features present on both sides — the effective feature set.
181    pub common_features: Vec<String>,
182    /// Features present locally but absent on the remote — unavailable for
183    /// this session.
184    pub dropped_features: Vec<String>,
185}
186
187// ────────────────────────────────────────────────────────────────────────────
188// VersionStats
189// ────────────────────────────────────────────────────────────────────────────
190
191/// Running statistics for all negotiation attempts handled by a
192/// `ProtocolVersionManager`.
193#[derive(Debug, Clone, Default)]
194pub struct VersionStats {
195    /// Total negotiation attempts (success + failure).
196    pub negotiations: u64,
197    /// Negotiations that produced a `NegotiationResult`.
198    pub successful: u64,
199    /// Negotiations that failed (unknown protocol or incompatible versions).
200    pub failed: u64,
201    /// Successful negotiations that were `BackwardCompatible`.
202    pub backward_compat: u64,
203    /// Successful negotiations that were `ForwardCompatible`.
204    pub forward_compat: u64,
205}
206
207// ────────────────────────────────────────────────────────────────────────────
208// ProtocolVersionManager
209// ────────────────────────────────────────────────────────────────────────────
210
211/// Central registry and negotiation engine for protocol versions.
212///
213/// Peers register their local `ProtocolDescriptor`s and then call `negotiate`
214/// when a remote peer's descriptor arrives.  The manager updates internal
215/// statistics for every attempt.
216pub struct ProtocolVersionManager {
217    supported: HashMap<String, ProtocolDescriptor>,
218    stats: VersionStats,
219}
220
221impl ProtocolVersionManager {
222    /// Create an empty manager with no registered protocols.
223    pub fn new() -> Self {
224        Self {
225            supported: HashMap::new(),
226            stats: VersionStats::default(),
227        }
228    }
229
230    /// Register a local `ProtocolDescriptor`.
231    ///
232    /// Returns `true` if this is a new registration, `false` if a descriptor
233    /// with the same name already existed and was overwritten.
234    pub fn register(&mut self, descriptor: ProtocolDescriptor) -> bool {
235        let is_new = !self.supported.contains_key(&descriptor.name);
236        self.supported.insert(descriptor.name.clone(), descriptor);
237        is_new
238    }
239
240    /// Attempt to negotiate with a remote peer's `ProtocolDescriptor`.
241    ///
242    /// Returns `Some(NegotiationResult)` on success, `None` when:
243    /// - The protocol is not registered locally, or
244    /// - The versions are `Incompatible` in both directions.
245    ///
246    /// **Compatibility check flow:**
247    ///
248    /// 1. We check the remote version against our local descriptor's
249    ///    `min_compatible` floor.
250    /// 2. We *also* check our local version against the remote descriptor's
251    ///    `min_compatible` floor — we must be acceptable to them too.
252    /// 3. If both checks pass, a `NegotiationResult` is produced.
253    ///
254    /// **Agreed version selection:**
255    /// The session runs at `min(local.version, remote.version)` — the older of
256    /// the two — ensuring that neither side uses features unavailable to the
257    /// other.
258    pub fn negotiate(
259        &mut self,
260        protocol: &str,
261        remote: &ProtocolDescriptor,
262    ) -> Option<NegotiationResult> {
263        self.stats.negotiations += 1;
264
265        let local = match self.supported.get(protocol) {
266            Some(d) => d.clone(),
267            None => {
268                self.stats.failed += 1;
269                return None;
270            }
271        };
272
273        // Check remote version against our minimum floor.
274        let compat_local_pov = local
275            .version
276            .is_compatible_with(&remote.version, &local.min_compatible);
277
278        if compat_local_pov == CompatibilityLevel::Incompatible {
279            self.stats.failed += 1;
280            return None;
281        }
282
283        // Check our version against the remote's minimum floor.
284        let compat_remote_pov = remote
285            .version
286            .is_compatible_with(&local.version, &remote.min_compatible);
287
288        if compat_remote_pov == CompatibilityLevel::Incompatible {
289            self.stats.failed += 1;
290            return None;
291        }
292
293        // Choose the lower version for the session.
294        let agreed_version = if local.version <= remote.version {
295            local.version.clone()
296        } else {
297            remote.version.clone()
298        };
299
300        let common_features = Self::find_common_features(&local.features, &remote.features);
301
302        let dropped_features: Vec<String> = local
303            .features
304            .iter()
305            .filter(|f| !common_features.contains(f))
306            .cloned()
307            .collect();
308
309        // Update directional stats.
310        match &compat_local_pov {
311            CompatibilityLevel::BackwardCompatible => self.stats.backward_compat += 1,
312            CompatibilityLevel::ForwardCompatible => self.stats.forward_compat += 1,
313            _ => {}
314        }
315        self.stats.successful += 1;
316
317        Some(NegotiationResult {
318            protocol: protocol.to_owned(),
319            agreed_version,
320            compatibility: compat_local_pov,
321            common_features,
322            dropped_features,
323        })
324    }
325
326    /// Compute the intersection of two feature sets, preserving the order of
327    /// the local side.
328    pub fn find_common_features(local: &[String], remote: &[String]) -> Vec<String> {
329        let remote_set: std::collections::HashSet<&String> = remote.iter().collect();
330        local
331            .iter()
332            .filter(|f| remote_set.contains(f))
333            .cloned()
334            .collect()
335    }
336
337    /// Look up the registered descriptor for a protocol by name.
338    pub fn get_descriptor(&self, protocol: &str) -> Option<&ProtocolDescriptor> {
339        self.supported.get(protocol)
340    }
341
342    /// Return `true` if a descriptor for `protocol` has been registered.
343    pub fn is_registered(&self, protocol: &str) -> bool {
344        self.supported.contains_key(protocol)
345    }
346
347    /// Return all registered protocol names as string slices.
348    pub fn supported_protocols(&self) -> Vec<&str> {
349        self.supported.keys().map(String::as_str).collect()
350    }
351
352    /// Read-only view of the accumulated negotiation statistics.
353    pub fn stats(&self) -> &VersionStats {
354        &self.stats
355    }
356}
357
358impl Default for ProtocolVersionManager {
359    fn default() -> Self {
360        Self::new()
361    }
362}
363
364// ────────────────────────────────────────────────────────────────────────────
365// Tests
366// ────────────────────────────────────────────────────────────────────────────
367
368#[cfg(test)]
369mod tests {
370    use super::*;
371
372    // ── ProtocolVersion construction & display ────────────────────────────
373
374    #[test]
375    fn test_version_new_stores_components() {
376        let v = ProtocolVersion::new(1, 2, 3);
377        assert_eq!(v.major, 1);
378        assert_eq!(v.minor, 2);
379        assert_eq!(v.patch, 3);
380    }
381
382    #[test]
383    fn test_version_display() {
384        let v = ProtocolVersion::new(2, 0, 1);
385        assert_eq!(format!("{}", v), "2.0.1");
386    }
387
388    #[test]
389    fn test_version_to_string_repr() {
390        let v = ProtocolVersion::new(0, 10, 99);
391        assert_eq!(v.to_string_repr(), "0.10.99");
392    }
393
394    #[test]
395    fn test_version_roundtrip() {
396        let original = ProtocolVersion::new(3, 14, 7);
397        let s = original.to_string_repr();
398        let parsed = ProtocolVersion::parse(&s).expect("roundtrip must succeed");
399        assert_eq!(original, parsed);
400    }
401
402    // ── ProtocolVersion parsing ───────────────────────────────────────────
403
404    #[test]
405    fn test_parse_valid() {
406        let v = ProtocolVersion::parse("1.2.3").expect("valid parse");
407        assert_eq!(v, ProtocolVersion::new(1, 2, 3));
408    }
409
410    #[test]
411    fn test_parse_zeros() {
412        let v = ProtocolVersion::parse("0.0.0").expect("valid zeros");
413        assert_eq!(v, ProtocolVersion::new(0, 0, 0));
414    }
415
416    #[test]
417    fn test_parse_invalid_not_enough_parts() {
418        assert!(ProtocolVersion::parse("1.2").is_none());
419    }
420
421    #[test]
422    fn test_parse_invalid_too_many_parts() {
423        assert!(ProtocolVersion::parse("1.2.3.4").is_none());
424    }
425
426    #[test]
427    fn test_parse_invalid_non_numeric() {
428        assert!(ProtocolVersion::parse("a.b.c").is_none());
429    }
430
431    #[test]
432    fn test_parse_empty_string() {
433        assert!(ProtocolVersion::parse("").is_none());
434    }
435
436    // ── ProtocolVersion ordering ──────────────────────────────────────────
437
438    #[test]
439    fn test_version_ordering_major_dominates() {
440        assert!(ProtocolVersion::new(2, 0, 0) > ProtocolVersion::new(1, 9, 9));
441    }
442
443    #[test]
444    fn test_version_ordering_minor_secondary() {
445        assert!(ProtocolVersion::new(1, 3, 0) > ProtocolVersion::new(1, 2, 99));
446    }
447
448    #[test]
449    fn test_version_ordering_patch_tertiary() {
450        assert!(ProtocolVersion::new(1, 2, 4) > ProtocolVersion::new(1, 2, 3));
451    }
452
453    #[test]
454    fn test_version_equality() {
455        assert_eq!(ProtocolVersion::new(1, 2, 3), ProtocolVersion::new(1, 2, 3));
456    }
457
458    // ── Compatibility levels ──────────────────────────────────────────────
459
460    #[test]
461    fn test_compat_fully_compatible() {
462        let local = ProtocolVersion::new(1, 2, 3);
463        let remote = ProtocolVersion::new(1, 2, 3);
464        let min = ProtocolVersion::new(1, 0, 0);
465        assert_eq!(
466            local.is_compatible_with(&remote, &min),
467            CompatibilityLevel::FullyCompatible
468        );
469    }
470
471    #[test]
472    fn test_compat_backward_compatible_remote_older() {
473        let local = ProtocolVersion::new(2, 0, 0);
474        let remote = ProtocolVersion::new(1, 5, 0); // older than local
475        let min = ProtocolVersion::new(1, 0, 0);
476        assert_eq!(
477            local.is_compatible_with(&remote, &min),
478            CompatibilityLevel::BackwardCompatible
479        );
480    }
481
482    #[test]
483    fn test_compat_forward_compatible_remote_newer() {
484        let local = ProtocolVersion::new(1, 0, 0);
485        let remote = ProtocolVersion::new(2, 0, 0); // newer than local
486        let min = ProtocolVersion::new(1, 0, 0);
487        assert_eq!(
488            local.is_compatible_with(&remote, &min),
489            CompatibilityLevel::ForwardCompatible
490        );
491    }
492
493    #[test]
494    fn test_compat_incompatible_below_min() {
495        let local = ProtocolVersion::new(2, 0, 0);
496        let remote = ProtocolVersion::new(0, 5, 0); // below min floor
497        let min = ProtocolVersion::new(1, 0, 0);
498        assert_eq!(
499            local.is_compatible_with(&remote, &min),
500            CompatibilityLevel::Incompatible
501        );
502    }
503
504    #[test]
505    fn test_compat_exactly_at_min_floor() {
506        let local = ProtocolVersion::new(2, 0, 0);
507        let remote = ProtocolVersion::new(1, 0, 0); // exactly at min
508        let min = ProtocolVersion::new(1, 0, 0);
509        // remote == min, but remote < local → BackwardCompatible
510        assert_eq!(
511            local.is_compatible_with(&remote, &min),
512            CompatibilityLevel::BackwardCompatible
513        );
514    }
515
516    // ── find_common_features ──────────────────────────────────────────────
517
518    #[test]
519    fn test_common_features_full_intersection() {
520        let local = vec!["a".to_owned(), "b".to_owned(), "c".to_owned()];
521        let remote = vec!["b".to_owned(), "a".to_owned(), "c".to_owned()];
522        let common = ProtocolVersionManager::find_common_features(&local, &remote);
523        // Order follows local
524        assert_eq!(common, vec!["a", "b", "c"]);
525    }
526
527    #[test]
528    fn test_common_features_partial_intersection() {
529        let local = vec!["x".to_owned(), "y".to_owned(), "z".to_owned()];
530        let remote = vec!["y".to_owned(), "w".to_owned()];
531        let common = ProtocolVersionManager::find_common_features(&local, &remote);
532        assert_eq!(common, vec!["y"]);
533    }
534
535    #[test]
536    fn test_common_features_empty_intersection() {
537        let local = vec!["a".to_owned()];
538        let remote = vec!["b".to_owned()];
539        let common = ProtocolVersionManager::find_common_features(&local, &remote);
540        assert!(common.is_empty());
541    }
542
543    #[test]
544    fn test_common_features_empty_inputs() {
545        let common = ProtocolVersionManager::find_common_features(&[], &[]);
546        assert!(common.is_empty());
547    }
548
549    // ── ProtocolVersionManager registration ──────────────────────────────
550
551    #[test]
552    fn test_register_new_returns_true() {
553        let mut mgr = ProtocolVersionManager::new();
554        let desc = ProtocolDescriptor::new(
555            "proto/a",
556            ProtocolVersion::new(1, 0, 0),
557            ProtocolVersion::new(1, 0, 0),
558            vec![],
559        );
560        assert!(mgr.register(desc));
561    }
562
563    #[test]
564    fn test_register_duplicate_returns_false() {
565        let mut mgr = ProtocolVersionManager::new();
566        let mk = || {
567            ProtocolDescriptor::new(
568                "proto/a",
569                ProtocolVersion::new(1, 0, 0),
570                ProtocolVersion::new(1, 0, 0),
571                vec![],
572            )
573        };
574        mgr.register(mk());
575        assert!(!mgr.register(mk()));
576    }
577
578    #[test]
579    fn test_is_registered() {
580        let mut mgr = ProtocolVersionManager::new();
581        assert!(!mgr.is_registered("proto/b"));
582        let desc = ProtocolDescriptor::new(
583            "proto/b",
584            ProtocolVersion::new(1, 0, 0),
585            ProtocolVersion::new(1, 0, 0),
586            vec![],
587        );
588        mgr.register(desc);
589        assert!(mgr.is_registered("proto/b"));
590    }
591
592    #[test]
593    fn test_get_descriptor() {
594        let mut mgr = ProtocolVersionManager::new();
595        let desc = ProtocolDescriptor::new(
596            "proto/c",
597            ProtocolVersion::new(2, 1, 0),
598            ProtocolVersion::new(1, 0, 0),
599            vec!["compression".to_owned()],
600        );
601        mgr.register(desc);
602        let retrieved = mgr.get_descriptor("proto/c").expect("must exist");
603        assert_eq!(retrieved.name, "proto/c");
604        assert_eq!(retrieved.version, ProtocolVersion::new(2, 1, 0));
605    }
606
607    #[test]
608    fn test_supported_protocols_lists_names() {
609        let mut mgr = ProtocolVersionManager::new();
610        mgr.register(ProtocolDescriptor::new(
611            "p1",
612            ProtocolVersion::new(1, 0, 0),
613            ProtocolVersion::new(1, 0, 0),
614            vec![],
615        ));
616        mgr.register(ProtocolDescriptor::new(
617            "p2",
618            ProtocolVersion::new(1, 0, 0),
619            ProtocolVersion::new(1, 0, 0),
620            vec![],
621        ));
622        let mut names = mgr.supported_protocols();
623        names.sort();
624        assert_eq!(names, vec!["p1", "p2"]);
625    }
626
627    // ── Negotiation ───────────────────────────────────────────────────────
628
629    fn make_mgr_with(
630        name: &str,
631        local_ver: ProtocolVersion,
632        min: ProtocolVersion,
633        features: Vec<String>,
634    ) -> ProtocolVersionManager {
635        let mut mgr = ProtocolVersionManager::new();
636        mgr.register(ProtocolDescriptor::new(name, local_ver, min, features));
637        mgr
638    }
639
640    #[test]
641    fn test_negotiate_unknown_protocol_returns_none() {
642        let mut mgr = ProtocolVersionManager::new();
643        let remote = ProtocolDescriptor::new(
644            "unknown",
645            ProtocolVersion::new(1, 0, 0),
646            ProtocolVersion::new(1, 0, 0),
647            vec![],
648        );
649        assert!(mgr.negotiate("unknown", &remote).is_none());
650        assert_eq!(mgr.stats().failed, 1);
651        assert_eq!(mgr.stats().negotiations, 1);
652    }
653
654    #[test]
655    fn test_negotiate_fully_compatible() {
656        let mut mgr = make_mgr_with(
657            "proto/x",
658            ProtocolVersion::new(1, 0, 0),
659            ProtocolVersion::new(1, 0, 0),
660            vec!["feat-a".to_owned()],
661        );
662        let remote = ProtocolDescriptor::new(
663            "proto/x",
664            ProtocolVersion::new(1, 0, 0),
665            ProtocolVersion::new(1, 0, 0),
666            vec!["feat-a".to_owned()],
667        );
668        let result = mgr.negotiate("proto/x", &remote).expect("should succeed");
669        assert_eq!(result.compatibility, CompatibilityLevel::FullyCompatible);
670        assert_eq!(result.agreed_version, ProtocolVersion::new(1, 0, 0));
671        assert_eq!(result.common_features, vec!["feat-a"]);
672        assert!(result.dropped_features.is_empty());
673    }
674
675    #[test]
676    fn test_negotiate_backward_compat_remote_older() {
677        let mut mgr = make_mgr_with(
678            "proto/y",
679            ProtocolVersion::new(2, 0, 0),
680            ProtocolVersion::new(1, 0, 0),
681            vec!["new-feat".to_owned(), "old-feat".to_owned()],
682        );
683        let remote = ProtocolDescriptor::new(
684            "proto/y",
685            ProtocolVersion::new(1, 5, 0), // older than local 2.0.0
686            ProtocolVersion::new(1, 0, 0),
687            vec!["old-feat".to_owned()],
688        );
689        let result = mgr.negotiate("proto/y", &remote).expect("should succeed");
690        assert_eq!(result.compatibility, CompatibilityLevel::BackwardCompatible);
691        // agreed at the remote (lower) version
692        assert_eq!(result.agreed_version, ProtocolVersion::new(1, 5, 0));
693        assert_eq!(result.common_features, vec!["old-feat"]);
694        assert_eq!(result.dropped_features, vec!["new-feat"]);
695        assert_eq!(mgr.stats().backward_compat, 1);
696    }
697
698    #[test]
699    fn test_negotiate_forward_compat_remote_newer() {
700        let mut mgr = make_mgr_with(
701            "proto/z",
702            ProtocolVersion::new(1, 0, 0),
703            ProtocolVersion::new(1, 0, 0),
704            vec!["base".to_owned()],
705        );
706        let remote = ProtocolDescriptor::new(
707            "proto/z",
708            ProtocolVersion::new(2, 0, 0), // newer than local 1.0.0
709            ProtocolVersion::new(1, 0, 0),
710            vec!["base".to_owned(), "advanced".to_owned()],
711        );
712        let result = mgr.negotiate("proto/z", &remote).expect("should succeed");
713        assert_eq!(result.compatibility, CompatibilityLevel::ForwardCompatible);
714        // agreed at local (lower) version
715        assert_eq!(result.agreed_version, ProtocolVersion::new(1, 0, 0));
716        assert_eq!(result.common_features, vec!["base"]);
717        assert_eq!(mgr.stats().forward_compat, 1);
718    }
719
720    #[test]
721    fn test_negotiate_incompatible_remote_below_local_min() {
722        let mut mgr = make_mgr_with(
723            "proto/q",
724            ProtocolVersion::new(3, 0, 0),
725            ProtocolVersion::new(2, 0, 0), // min floor is 2.0.0
726            vec![],
727        );
728        let remote = ProtocolDescriptor::new(
729            "proto/q",
730            ProtocolVersion::new(1, 9, 9), // below our min
731            ProtocolVersion::new(1, 0, 0),
732            vec![],
733        );
734        assert!(mgr.negotiate("proto/q", &remote).is_none());
735        assert_eq!(mgr.stats().failed, 1);
736    }
737
738    #[test]
739    fn test_negotiate_incompatible_local_below_remote_min() {
740        // Remote requires at least 3.0.0; local only runs 1.0.0.
741        let mut mgr = make_mgr_with(
742            "proto/r",
743            ProtocolVersion::new(1, 0, 0),
744            ProtocolVersion::new(1, 0, 0),
745            vec![],
746        );
747        let remote = ProtocolDescriptor::new(
748            "proto/r",
749            ProtocolVersion::new(3, 0, 0),
750            ProtocolVersion::new(3, 0, 0), // remote won't talk < 3.0.0
751            vec![],
752        );
753        assert!(mgr.negotiate("proto/r", &remote).is_none());
754        assert_eq!(mgr.stats().failed, 1);
755    }
756
757    #[test]
758    fn test_negotiate_stats_accumulate() {
759        let mut mgr = make_mgr_with(
760            "proto/s",
761            ProtocolVersion::new(1, 0, 0),
762            ProtocolVersion::new(1, 0, 0),
763            vec![],
764        );
765        let good_remote = ProtocolDescriptor::new(
766            "proto/s",
767            ProtocolVersion::new(1, 0, 0),
768            ProtocolVersion::new(1, 0, 0),
769            vec![],
770        );
771        mgr.negotiate("proto/s", &good_remote);
772        mgr.negotiate("proto/s", &good_remote);
773        mgr.negotiate("unknown", &good_remote);
774
775        let s = mgr.stats();
776        assert_eq!(s.negotiations, 3);
777        assert_eq!(s.successful, 2);
778        assert_eq!(s.failed, 1);
779    }
780
781    #[test]
782    fn test_negotiate_feature_dropping() {
783        let mut mgr = make_mgr_with(
784            "proto/t",
785            ProtocolVersion::new(1, 0, 0),
786            ProtocolVersion::new(1, 0, 0),
787            vec!["alpha".to_owned(), "beta".to_owned(), "gamma".to_owned()],
788        );
789        let remote = ProtocolDescriptor::new(
790            "proto/t",
791            ProtocolVersion::new(1, 0, 0),
792            ProtocolVersion::new(1, 0, 0),
793            vec!["alpha".to_owned(), "gamma".to_owned()], // beta missing
794        );
795        let result = mgr.negotiate("proto/t", &remote).expect("success");
796        assert_eq!(result.common_features, vec!["alpha", "gamma"]);
797        assert_eq!(result.dropped_features, vec!["beta"]);
798    }
799
800    #[test]
801    fn test_protocol_name_in_result() {
802        let mut mgr = make_mgr_with(
803            "ipfrs/custom",
804            ProtocolVersion::new(1, 0, 0),
805            ProtocolVersion::new(1, 0, 0),
806            vec![],
807        );
808        let remote = ProtocolDescriptor::new(
809            "ipfrs/custom",
810            ProtocolVersion::new(1, 0, 0),
811            ProtocolVersion::new(1, 0, 0),
812            vec![],
813        );
814        let result = mgr.negotiate("ipfrs/custom", &remote).expect("success");
815        assert_eq!(result.protocol, "ipfrs/custom");
816    }
817
818    #[test]
819    fn test_default_manager_has_no_protocols() {
820        let mgr = ProtocolVersionManager::default();
821        assert!(mgr.supported_protocols().is_empty());
822        assert_eq!(mgr.stats().negotiations, 0);
823    }
824}