Skip to main content

ipfrs_network/
connection_tracker.rs

1//! Peer Connection Tracker
2//!
3//! Tracks the full lifecycle of peer connections: establishment, protocol negotiation,
4//! failure, graceful close, and per-peer statistics.
5//!
6//! # Overview
7//!
8//! `PeerConnectionTracker` maintains a registry of all known peers and their current
9//! connection state, aggregated statistics, and a bounded event log of the 500 most
10//! recent connection events.
11//!
12//! # Example
13//!
14//! ```
15//! use ipfrs_network::connection_tracker::{
16//!     ConnectionEvent, PeerConnectionTracker,
17//! };
18//!
19//! let mut tracker = PeerConnectionTracker::new();
20//!
21//! tracker.record_event(ConnectionEvent::Connected {
22//!     peer_id: "peer-1".to_string(),
23//!     address: "/ip4/127.0.0.1/tcp/4001".to_string(),
24//!     at_secs: 1_000,
25//! });
26//!
27//! let peers = tracker.connected_peers();
28//! assert_eq!(peers.len(), 1);
29//! ```
30
31use std::collections::{HashMap, VecDeque};
32
33// ---------------------------------------------------------------------------
34// Constants
35// ---------------------------------------------------------------------------
36
37/// Maximum number of events retained in the event log.
38const MAX_EVENT_LOG: usize = 500;
39
40// ---------------------------------------------------------------------------
41// ConnectionEvent
42// ---------------------------------------------------------------------------
43
44/// An event that describes a state change or observation on a peer connection.
45#[derive(Clone, Debug)]
46pub enum ConnectionEvent {
47    /// A peer successfully established a connection.
48    Connected {
49        peer_id: String,
50        address: String,
51        at_secs: u64,
52    },
53    /// A peer's connection was terminated.
54    Disconnected {
55        peer_id: String,
56        reason: String,
57        at_secs: u64,
58    },
59    /// A protocol was negotiated with a peer.
60    ProtocolNegotiated {
61        peer_id: String,
62        protocol: String,
63        at_secs: u64,
64    },
65    /// A ping round-trip succeeded.
66    PingSuccess {
67        peer_id: String,
68        rtt_ms: f64,
69        at_secs: u64,
70    },
71    /// A ping attempt failed.
72    PingFailed { peer_id: String, at_secs: u64 },
73    /// The address associated with a peer changed.
74    AddressChanged {
75        peer_id: String,
76        new_address: String,
77        at_secs: u64,
78    },
79}
80
81impl ConnectionEvent {
82    /// Returns the `peer_id` carried by any variant.
83    pub fn peer_id(&self) -> &str {
84        match self {
85            ConnectionEvent::Connected { peer_id, .. } => peer_id,
86            ConnectionEvent::Disconnected { peer_id, .. } => peer_id,
87            ConnectionEvent::ProtocolNegotiated { peer_id, .. } => peer_id,
88            ConnectionEvent::PingSuccess { peer_id, .. } => peer_id,
89            ConnectionEvent::PingFailed { peer_id, .. } => peer_id,
90            ConnectionEvent::AddressChanged { peer_id, .. } => peer_id,
91        }
92    }
93}
94
95// ---------------------------------------------------------------------------
96// ConnectionState
97// ---------------------------------------------------------------------------
98
99/// The current state of a peer's connection.
100#[derive(Clone, Debug, PartialEq, Eq)]
101pub enum ConnectionState {
102    /// The connection has been fully established.
103    Connected,
104    /// The connection has been terminated.
105    Disconnected,
106    /// A connection attempt is in progress.
107    Connecting,
108}
109
110// ---------------------------------------------------------------------------
111// PeerConnectionInfo
112// ---------------------------------------------------------------------------
113
114/// All tracked information for a single peer.
115#[derive(Clone, Debug)]
116pub struct PeerConnectionInfo {
117    /// Unique peer identifier.
118    pub peer_id: String,
119    /// Most recent address used by the peer.
120    pub address: String,
121    /// Current connection state.
122    pub state: ConnectionState,
123    /// Unix timestamp (seconds) when the connection was first established.
124    pub connected_at_secs: Option<u64>,
125    /// Unix timestamp (seconds) when the connection was last terminated.
126    pub disconnected_at_secs: Option<u64>,
127    /// Protocols negotiated with this peer.
128    pub protocols: Vec<String>,
129    /// Total number of ping attempts (successes + failures).
130    pub ping_count: u64,
131    /// Number of failed ping attempts.
132    pub ping_failures: u64,
133    /// Running average round-trip time across all successful pings (milliseconds).
134    pub avg_rtt_ms: f64,
135}
136
137impl PeerConnectionInfo {
138    /// Create a new, empty `PeerConnectionInfo` for the given peer.
139    fn new(peer_id: String, address: String) -> Self {
140        Self {
141            peer_id,
142            address,
143            state: ConnectionState::Connecting,
144            connected_at_secs: None,
145            disconnected_at_secs: None,
146            protocols: Vec::new(),
147            ping_count: 0,
148            ping_failures: 0,
149            avg_rtt_ms: 0.0,
150        }
151    }
152
153    /// Seconds the peer has been connected, measured from `now_secs`.
154    ///
155    /// Returns `0` if the peer is not currently connected.
156    pub fn uptime_secs(&self, now_secs: u64) -> u64 {
157        if self.state != ConnectionState::Connected {
158            return 0;
159        }
160        match self.connected_at_secs {
161            Some(connected_at) if now_secs >= connected_at => now_secs - connected_at,
162            _ => 0,
163        }
164    }
165
166    /// Fraction of pings that succeeded: `1.0 - (failures / total.max(1))`.
167    pub fn reliability(&self) -> f64 {
168        1.0 - (self.ping_failures as f64 / self.ping_count.max(1) as f64)
169    }
170}
171
172// ---------------------------------------------------------------------------
173// TrackerStats
174// ---------------------------------------------------------------------------
175
176/// Aggregated statistics across all tracked peers.
177#[derive(Clone, Debug, Default)]
178pub struct TrackerStats {
179    /// Total number of peers that have ever connected (cumulative).
180    pub total_connections: u64,
181    /// Number of peers currently in the `Connected` state.
182    pub current_connections: usize,
183    /// Total number of disconnection events recorded.
184    pub total_disconnections: u64,
185    /// Total number of ping attempts (successes + failures) recorded.
186    pub total_pings: u64,
187    /// Total number of failed ping attempts recorded.
188    pub total_ping_failures: u64,
189}
190
191// ---------------------------------------------------------------------------
192// PeerConnectionTracker
193// ---------------------------------------------------------------------------
194
195/// Tracks the lifecycle of all peer connections with per-peer statistics.
196pub struct PeerConnectionTracker {
197    /// Per-peer connection info, keyed by peer ID.
198    pub peers: HashMap<String, PeerConnectionInfo>,
199    /// Bounded ring-buffer of recent events (last `MAX_EVENT_LOG`).
200    pub event_log: VecDeque<ConnectionEvent>,
201    /// Aggregated statistics across all events.
202    pub stats: TrackerStats,
203}
204
205impl PeerConnectionTracker {
206    /// Create a new, empty tracker.
207    pub fn new() -> Self {
208        Self {
209            peers: HashMap::new(),
210            event_log: VecDeque::with_capacity(MAX_EVENT_LOG + 1),
211            stats: TrackerStats::default(),
212        }
213    }
214
215    /// Record a `ConnectionEvent`, updating per-peer state and the event log.
216    pub fn record_event(&mut self, event: ConnectionEvent) {
217        // ------------------------------------------------------------------
218        // 1. Update peer state
219        // ------------------------------------------------------------------
220        match &event {
221            ConnectionEvent::Connected {
222                peer_id,
223                address,
224                at_secs,
225            } => {
226                let entry = self
227                    .peers
228                    .entry(peer_id.clone())
229                    .or_insert_with(|| PeerConnectionInfo::new(peer_id.clone(), address.clone()));
230                entry.state = ConnectionState::Connected;
231                entry.address = address.clone();
232                entry.connected_at_secs = Some(*at_secs);
233                entry.disconnected_at_secs = None;
234
235                self.stats.total_connections += 1;
236                self.stats.current_connections = self.count_connected();
237            }
238
239            ConnectionEvent::Disconnected {
240                peer_id, at_secs, ..
241            } => {
242                if let Some(entry) = self.peers.get_mut(peer_id) {
243                    entry.state = ConnectionState::Disconnected;
244                    entry.disconnected_at_secs = Some(*at_secs);
245                }
246                self.stats.total_disconnections += 1;
247                self.stats.current_connections = self.count_connected();
248            }
249
250            ConnectionEvent::ProtocolNegotiated {
251                peer_id, protocol, ..
252            } => {
253                if let Some(entry) = self.peers.get_mut(peer_id) {
254                    if !entry.protocols.contains(protocol) {
255                        entry.protocols.push(protocol.clone());
256                    }
257                }
258            }
259
260            ConnectionEvent::PingSuccess {
261                peer_id, rtt_ms, ..
262            } => {
263                if let Some(entry) = self.peers.get_mut(peer_id) {
264                    entry.ping_count += 1;
265                    let n = entry.ping_count as f64;
266                    entry.avg_rtt_ms = (entry.avg_rtt_ms * (n - 1.0) + rtt_ms) / n;
267                }
268                self.stats.total_pings += 1;
269            }
270
271            ConnectionEvent::PingFailed { peer_id, .. } => {
272                if let Some(entry) = self.peers.get_mut(peer_id) {
273                    entry.ping_count += 1;
274                    entry.ping_failures += 1;
275                }
276                self.stats.total_pings += 1;
277                self.stats.total_ping_failures += 1;
278            }
279
280            ConnectionEvent::AddressChanged {
281                peer_id,
282                new_address,
283                ..
284            } => {
285                if let Some(entry) = self.peers.get_mut(peer_id) {
286                    entry.address = new_address.clone();
287                }
288            }
289        }
290
291        // ------------------------------------------------------------------
292        // 2. Append to event log, trimming to MAX_EVENT_LOG
293        // ------------------------------------------------------------------
294        self.event_log.push_back(event);
295        while self.event_log.len() > MAX_EVENT_LOG {
296            self.event_log.pop_front();
297        }
298    }
299
300    /// Return references to all peers currently in the `Connected` state.
301    pub fn connected_peers(&self) -> Vec<&PeerConnectionInfo> {
302        self.peers
303            .values()
304            .filter(|p| p.state == ConnectionState::Connected)
305            .collect()
306    }
307
308    /// Look up a single peer by ID.
309    pub fn peer_info(&self, peer_id: &str) -> Option<&PeerConnectionInfo> {
310        self.peers.get(peer_id)
311    }
312
313    /// Remove a peer from the registry entirely.
314    ///
315    /// Returns `true` if the peer was present, `false` otherwise.
316    pub fn remove_peer(&mut self, peer_id: &str) -> bool {
317        let removed = self.peers.remove(peer_id).is_some();
318        if removed {
319            self.stats.current_connections = self.count_connected();
320        }
321        removed
322    }
323
324    /// Return peers whose `reliability()` is strictly below `threshold`.
325    pub fn unreliable_peers(&self, threshold: f64) -> Vec<&PeerConnectionInfo> {
326        self.peers
327            .values()
328            .filter(|p| p.reliability() < threshold)
329            .collect()
330    }
331
332    /// Return the top-`n` peers sorted by `uptime_secs(now_secs)` in descending order.
333    pub fn top_peers_by_uptime(&self, n: usize, now_secs: u64) -> Vec<&PeerConnectionInfo> {
334        let mut peers: Vec<&PeerConnectionInfo> = self.peers.values().collect();
335        peers.sort_by_key(|b| std::cmp::Reverse(b.uptime_secs(now_secs)));
336        peers.truncate(n);
337        peers
338    }
339
340    /// Return a reference to the current aggregated statistics.
341    pub fn stats(&self) -> &TrackerStats {
342        &self.stats
343    }
344
345    /// Return the last `count` events from the event log.
346    pub fn recent_events(&self, count: usize) -> Vec<&ConnectionEvent> {
347        let len = self.event_log.len();
348        let skip = len.saturating_sub(count);
349        self.event_log.iter().skip(skip).collect()
350    }
351
352    // ------------------------------------------------------------------
353    // Internal helpers
354    // ------------------------------------------------------------------
355
356    fn count_connected(&self) -> usize {
357        self.peers
358            .values()
359            .filter(|p| p.state == ConnectionState::Connected)
360            .count()
361    }
362}
363
364impl Default for PeerConnectionTracker {
365    fn default() -> Self {
366        Self::new()
367    }
368}
369
370// ---------------------------------------------------------------------------
371// Tests
372// ---------------------------------------------------------------------------
373
374#[cfg(test)]
375mod tests {
376    use super::*;
377
378    // -----------------------------------------------------------------------
379    // Helper
380    // -----------------------------------------------------------------------
381
382    fn connected_event(peer_id: &str, addr: &str, at: u64) -> ConnectionEvent {
383        ConnectionEvent::Connected {
384            peer_id: peer_id.to_string(),
385            address: addr.to_string(),
386            at_secs: at,
387        }
388    }
389
390    fn disconnected_event(peer_id: &str, reason: &str, at: u64) -> ConnectionEvent {
391        ConnectionEvent::Disconnected {
392            peer_id: peer_id.to_string(),
393            reason: reason.to_string(),
394            at_secs: at,
395        }
396    }
397
398    fn ping_success(peer_id: &str, rtt: f64, at: u64) -> ConnectionEvent {
399        ConnectionEvent::PingSuccess {
400            peer_id: peer_id.to_string(),
401            rtt_ms: rtt,
402            at_secs: at,
403        }
404    }
405
406    fn ping_failed(peer_id: &str, at: u64) -> ConnectionEvent {
407        ConnectionEvent::PingFailed {
408            peer_id: peer_id.to_string(),
409            at_secs: at,
410        }
411    }
412
413    // -----------------------------------------------------------------------
414    // 1. new() produces empty state
415    // -----------------------------------------------------------------------
416    #[test]
417    fn test_new_empty_state() {
418        let tracker = PeerConnectionTracker::new();
419        assert!(tracker.peers.is_empty());
420        assert!(tracker.event_log.is_empty());
421        assert_eq!(tracker.stats.total_connections, 0);
422        assert_eq!(tracker.stats.current_connections, 0);
423    }
424
425    // -----------------------------------------------------------------------
426    // 2. record Connected updates peer state
427    // -----------------------------------------------------------------------
428    #[test]
429    fn test_record_connected_updates_state() {
430        let mut tracker = PeerConnectionTracker::new();
431        tracker.record_event(connected_event("peer-1", "/ip4/1.2.3.4/tcp/4001", 1000));
432
433        let info = tracker.peer_info("peer-1").expect("peer should exist");
434        assert_eq!(info.state, ConnectionState::Connected);
435        assert_eq!(info.address, "/ip4/1.2.3.4/tcp/4001");
436        assert_eq!(info.connected_at_secs, Some(1000));
437        assert_eq!(tracker.stats.total_connections, 1);
438        assert_eq!(tracker.stats.current_connections, 1);
439    }
440
441    // -----------------------------------------------------------------------
442    // 3. record Disconnected updates state and timestamp
443    // -----------------------------------------------------------------------
444    #[test]
445    fn test_record_disconnected_updates_state() {
446        let mut tracker = PeerConnectionTracker::new();
447        tracker.record_event(connected_event("peer-1", "/ip4/1.2.3.4/tcp/4001", 1000));
448        tracker.record_event(disconnected_event("peer-1", "timeout", 2000));
449
450        let info = tracker.peer_info("peer-1").expect("peer should exist");
451        assert_eq!(info.state, ConnectionState::Disconnected);
452        assert_eq!(info.disconnected_at_secs, Some(2000));
453        assert_eq!(tracker.stats.total_disconnections, 1);
454        assert_eq!(tracker.stats.current_connections, 0);
455    }
456
457    // -----------------------------------------------------------------------
458    // 4. record ProtocolNegotiated appends to protocols (no duplicates)
459    // -----------------------------------------------------------------------
460    #[test]
461    fn test_protocol_negotiated_appends() {
462        let mut tracker = PeerConnectionTracker::new();
463        tracker.record_event(connected_event("peer-1", "/ip4/1.2.3.4/tcp/4001", 1000));
464        tracker.record_event(ConnectionEvent::ProtocolNegotiated {
465            peer_id: "peer-1".to_string(),
466            protocol: "/ipfs/kad/1.0.0".to_string(),
467            at_secs: 1001,
468        });
469        tracker.record_event(ConnectionEvent::ProtocolNegotiated {
470            peer_id: "peer-1".to_string(),
471            protocol: "/ipfs/bitswap/1.2.0".to_string(),
472            at_secs: 1002,
473        });
474        // Duplicate – must not be added twice
475        tracker.record_event(ConnectionEvent::ProtocolNegotiated {
476            peer_id: "peer-1".to_string(),
477            protocol: "/ipfs/kad/1.0.0".to_string(),
478            at_secs: 1003,
479        });
480
481        let info = tracker.peer_info("peer-1").expect("peer should exist");
482        assert_eq!(info.protocols.len(), 2);
483        assert!(info.protocols.contains(&"/ipfs/kad/1.0.0".to_string()));
484        assert!(info.protocols.contains(&"/ipfs/bitswap/1.2.0".to_string()));
485    }
486
487    // -----------------------------------------------------------------------
488    // 5. record PingSuccess updates avg_rtt_ms correctly (3 pings)
489    // -----------------------------------------------------------------------
490    #[test]
491    fn test_ping_success_avg_rtt() {
492        let mut tracker = PeerConnectionTracker::new();
493        tracker.record_event(connected_event("peer-1", "/ip4/1.2.3.4/tcp/4001", 1000));
494
495        tracker.record_event(ping_success("peer-1", 10.0, 1001));
496        tracker.record_event(ping_success("peer-1", 20.0, 1002));
497        tracker.record_event(ping_success("peer-1", 30.0, 1003));
498
499        let info = tracker.peer_info("peer-1").expect("peer should exist");
500        assert_eq!(info.ping_count, 3);
501        // Expected: (10 + 20 + 30) / 3 = 20.0
502        let expected = 20.0_f64;
503        assert!(
504            (info.avg_rtt_ms - expected).abs() < 1e-9,
505            "avg_rtt_ms = {} expected {}",
506            info.avg_rtt_ms,
507            expected
508        );
509    }
510
511    // -----------------------------------------------------------------------
512    // 6. record PingFailed increments ping_failures
513    // -----------------------------------------------------------------------
514    #[test]
515    fn test_ping_failed_increments_failures() {
516        let mut tracker = PeerConnectionTracker::new();
517        tracker.record_event(connected_event("peer-1", "/ip4/1.2.3.4/tcp/4001", 1000));
518        tracker.record_event(ping_failed("peer-1", 1001));
519        tracker.record_event(ping_failed("peer-1", 1002));
520
521        let info = tracker.peer_info("peer-1").expect("peer should exist");
522        assert_eq!(info.ping_count, 2);
523        assert_eq!(info.ping_failures, 2);
524        assert_eq!(tracker.stats.total_ping_failures, 2);
525    }
526
527    // -----------------------------------------------------------------------
528    // 7. record AddressChanged updates address
529    // -----------------------------------------------------------------------
530    #[test]
531    fn test_address_changed_updates_address() {
532        let mut tracker = PeerConnectionTracker::new();
533        tracker.record_event(connected_event("peer-1", "/ip4/1.2.3.4/tcp/4001", 1000));
534        tracker.record_event(ConnectionEvent::AddressChanged {
535            peer_id: "peer-1".to_string(),
536            new_address: "/ip4/5.6.7.8/tcp/4001".to_string(),
537            at_secs: 1010,
538        });
539
540        let info = tracker.peer_info("peer-1").expect("peer should exist");
541        assert_eq!(info.address, "/ip4/5.6.7.8/tcp/4001");
542    }
543
544    // -----------------------------------------------------------------------
545    // 8. event_log is bounded at MAX_EVENT_LOG (500)
546    // -----------------------------------------------------------------------
547    #[test]
548    fn test_event_log_bounded_at_500() {
549        let mut tracker = PeerConnectionTracker::new();
550        // Record 600 events – only the last 500 should be retained.
551        for i in 0u64..600 {
552            tracker.record_event(ping_success("peer-1", i as f64, i));
553        }
554        assert_eq!(tracker.event_log.len(), 500);
555    }
556
557    // -----------------------------------------------------------------------
558    // 9. connected_peers filters correctly
559    // -----------------------------------------------------------------------
560    #[test]
561    fn test_connected_peers_filters() {
562        let mut tracker = PeerConnectionTracker::new();
563        tracker.record_event(connected_event("peer-1", "/ip4/1.2.3.4/tcp/4001", 1000));
564        tracker.record_event(connected_event("peer-2", "/ip4/2.2.2.2/tcp/4001", 1000));
565        tracker.record_event(disconnected_event("peer-2", "bye", 2000));
566
567        let connected = tracker.connected_peers();
568        assert_eq!(connected.len(), 1);
569        assert_eq!(connected[0].peer_id, "peer-1");
570    }
571
572    // -----------------------------------------------------------------------
573    // 10. peer_info returns Some / None
574    // -----------------------------------------------------------------------
575    #[test]
576    fn test_peer_info_some_none() {
577        let mut tracker = PeerConnectionTracker::new();
578        tracker.record_event(connected_event("peer-1", "/ip4/1.2.3.4/tcp/4001", 1000));
579
580        assert!(tracker.peer_info("peer-1").is_some());
581        assert!(tracker.peer_info("unknown").is_none());
582    }
583
584    // -----------------------------------------------------------------------
585    // 11. remove_peer returns true / false
586    // -----------------------------------------------------------------------
587    #[test]
588    fn test_remove_peer_true_false() {
589        let mut tracker = PeerConnectionTracker::new();
590        tracker.record_event(connected_event("peer-1", "/ip4/1.2.3.4/tcp/4001", 1000));
591
592        assert!(tracker.remove_peer("peer-1"));
593        assert!(!tracker.remove_peer("peer-1")); // already removed
594        assert!(!tracker.remove_peer("ghost")); // never existed
595    }
596
597    // -----------------------------------------------------------------------
598    // 12. unreliable_peers threshold filter
599    // -----------------------------------------------------------------------
600    #[test]
601    fn test_unreliable_peers_threshold() {
602        let mut tracker = PeerConnectionTracker::new();
603        // peer-1: 1 success, 1 failure → reliability = 0.5
604        tracker.record_event(connected_event("peer-1", "/ip4/1.1.1.1/tcp/4001", 1000));
605        tracker.record_event(ping_success("peer-1", 10.0, 1001));
606        tracker.record_event(ping_failed("peer-1", 1002));
607
608        // peer-2: 2 successes → reliability = 1.0
609        tracker.record_event(connected_event("peer-2", "/ip4/2.2.2.2/tcp/4001", 1000));
610        tracker.record_event(ping_success("peer-2", 10.0, 1001));
611        tracker.record_event(ping_success("peer-2", 10.0, 1002));
612
613        let unreliable = tracker.unreliable_peers(0.8);
614        assert_eq!(unreliable.len(), 1);
615        assert_eq!(unreliable[0].peer_id, "peer-1");
616    }
617
618    // -----------------------------------------------------------------------
619    // 13. top_peers_by_uptime sorted descending
620    // -----------------------------------------------------------------------
621    #[test]
622    fn test_top_peers_by_uptime_sorted() {
623        let mut tracker = PeerConnectionTracker::new();
624        // peer-1 connected at t=100, peer-2 at t=200, peer-3 at t=300
625        tracker.record_event(connected_event("peer-1", "/ip4/1.1.1.1/tcp/4001", 100));
626        tracker.record_event(connected_event("peer-2", "/ip4/2.2.2.2/tcp/4001", 200));
627        tracker.record_event(connected_event("peer-3", "/ip4/3.3.3.3/tcp/4001", 300));
628
629        let now = 1000u64;
630        let top = tracker.top_peers_by_uptime(3, now);
631
632        // peer-1 uptime=900, peer-2 uptime=800, peer-3 uptime=700
633        assert_eq!(top.len(), 3);
634        assert_eq!(top[0].peer_id, "peer-1");
635        assert_eq!(top[1].peer_id, "peer-2");
636        assert_eq!(top[2].peer_id, "peer-3");
637    }
638
639    // -----------------------------------------------------------------------
640    // 14. top_peers_by_uptime respects n limit
641    // -----------------------------------------------------------------------
642    #[test]
643    fn test_top_peers_by_uptime_limit() {
644        let mut tracker = PeerConnectionTracker::new();
645        for i in 0u64..10 {
646            tracker.record_event(connected_event(
647                &format!("peer-{i}"),
648                "/ip4/1.1.1.1/tcp/4001",
649                i * 10,
650            ));
651        }
652        let top = tracker.top_peers_by_uptime(3, 1000);
653        assert_eq!(top.len(), 3);
654    }
655
656    // -----------------------------------------------------------------------
657    // 15. reliability() calculation
658    // -----------------------------------------------------------------------
659    #[test]
660    fn test_reliability_calculation() {
661        let mut tracker = PeerConnectionTracker::new();
662        tracker.record_event(connected_event("peer-1", "/ip4/1.2.3.4/tcp/4001", 1000));
663
664        // No pings yet: reliability = 1.0 - (0 / max(0,1)) = 1.0
665        let info = tracker.peer_info("peer-1").expect("peer should exist");
666        assert!((info.reliability() - 1.0).abs() < 1e-9);
667
668        // 3 successes, 1 failure → ping_count=4, failures=1, reliability = 0.75
669        tracker.record_event(ping_success("peer-1", 10.0, 1001));
670        tracker.record_event(ping_success("peer-1", 10.0, 1002));
671        tracker.record_event(ping_success("peer-1", 10.0, 1003));
672        tracker.record_event(ping_failed("peer-1", 1004));
673
674        let info = tracker.peer_info("peer-1").expect("peer should exist");
675        let expected = 1.0 - (1.0 / 4.0);
676        assert!(
677            (info.reliability() - expected).abs() < 1e-9,
678            "got {}",
679            info.reliability()
680        );
681    }
682
683    // -----------------------------------------------------------------------
684    // 16. uptime_secs when connected / not connected
685    // -----------------------------------------------------------------------
686    #[test]
687    fn test_uptime_secs_connected_and_not() {
688        let mut tracker = PeerConnectionTracker::new();
689        tracker.record_event(connected_event("peer-1", "/ip4/1.2.3.4/tcp/4001", 500));
690
691        let info = tracker.peer_info("peer-1").expect("peer should exist");
692        assert_eq!(info.uptime_secs(1000), 500);
693
694        tracker.record_event(disconnected_event("peer-1", "bye", 800));
695        let info = tracker.peer_info("peer-1").expect("peer should exist");
696        assert_eq!(info.uptime_secs(1000), 0);
697    }
698
699    // -----------------------------------------------------------------------
700    // 17. stats() totals updated correctly
701    // -----------------------------------------------------------------------
702    #[test]
703    fn test_stats_totals_updated() {
704        let mut tracker = PeerConnectionTracker::new();
705        tracker.record_event(connected_event("peer-1", "/ip4/1.1.1.1/tcp/4001", 1000));
706        tracker.record_event(connected_event("peer-2", "/ip4/2.2.2.2/tcp/4001", 1001));
707        tracker.record_event(ping_success("peer-1", 15.0, 1002));
708        tracker.record_event(ping_failed("peer-2", 1003));
709        tracker.record_event(disconnected_event("peer-1", "timeout", 2000));
710
711        let s = tracker.stats();
712        assert_eq!(s.total_connections, 2);
713        assert_eq!(s.current_connections, 1); // peer-2 still connected
714        assert_eq!(s.total_disconnections, 1);
715        assert_eq!(s.total_pings, 2);
716        assert_eq!(s.total_ping_failures, 1);
717    }
718
719    // -----------------------------------------------------------------------
720    // 18. recent_events returns last N
721    // -----------------------------------------------------------------------
722    #[test]
723    fn test_recent_events_last_n() {
724        let mut tracker = PeerConnectionTracker::new();
725        tracker.record_event(connected_event("peer-1", "/ip4/1.2.3.4/tcp/4001", 1000));
726        tracker.record_event(ping_success("peer-1", 10.0, 1001));
727        tracker.record_event(ping_success("peer-1", 20.0, 1002));
728        tracker.record_event(ping_failed("peer-1", 1003));
729        tracker.record_event(disconnected_event("peer-1", "done", 1004));
730
731        // Last 3 events
732        let recent = tracker.recent_events(3);
733        assert_eq!(recent.len(), 3);
734        // The last event should be Disconnected
735        assert!(matches!(recent[2], ConnectionEvent::Disconnected { .. }));
736        // The second-to-last should be PingFailed
737        assert!(matches!(recent[1], ConnectionEvent::PingFailed { .. }));
738
739        // Requesting more than available returns all
740        let all = tracker.recent_events(100);
741        assert_eq!(all.len(), 5);
742    }
743}