Skip to main content

peat_mesh/
mesh.rs

1//! PeatMesh facade — unified entry point for the mesh networking library.
2//!
3//! Provides [`PeatMesh`] as the single entry point that composes transport,
4//! topology, routing, hierarchy, and (optionally) the HTTP/WS broker into a
5//! cohesive mesh networking stack.
6//!
7//! # Lock ordering
8//!
9//! `PeatMesh` contains three sync `RwLock`s:
10//!
11//! | Lock | Type | Protects |
12//! |------|------|----------|
13//! | `state` | `std::sync::RwLock<MeshState>` | Lifecycle state machine |
14//! | `discovery` | `std::sync::RwLock<Option<Box<dyn DiscoveryStrategy>>>` | Discovery strategy |
15//! | `started_at` | `std::sync::RwLock<Option<Instant>>` | Start timestamp |
16//! | `cancellation_token` | `std::sync::RwLock<CancellationToken>` | Shutdown signalling |
17//!
18//! **Required acquisition order (when multiple locks are needed):**
19//!
20//! 1. `state`
21//! 2. `cancellation_token`
22//! 3. `discovery`
23//! 4. `started_at`
24//!
25//! In practice, `start()` and `stop()` are the only methods that acquire more
26//! than one of these locks, and they follow this order. All other accessors
27//! acquire a single lock at a time.
28//!
29//! **Invariant:** none of these locks should be held while calling into
30//! subsystem locks (e.g., `TransportManager` or `SyncChannel`). The facade
31//! reads its own state, releases the guard, then delegates to the subsystem.
32
33use crate::config::MeshConfig;
34use crate::hierarchy::HierarchyStrategy;
35use crate::routing::MeshRouter;
36use crate::transport::{MeshTransport, NodeId, TransportError, TransportManager};
37use std::fmt;
38use std::sync::{Arc, RwLock};
39use std::time::Instant;
40use tokio::sync::broadcast;
41use tokio_util::sync::CancellationToken;
42
43// ─── Lifecycle state ─────────────────────────────────────────────────────────
44
45/// Lifecycle state of the mesh.
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum MeshState {
48    /// Mesh created but not yet started.
49    Created,
50    /// Mesh is in the process of starting.
51    Starting,
52    /// Mesh is running and accepting connections.
53    Running,
54    /// Mesh is in the process of stopping.
55    Stopping,
56    /// Mesh has been stopped.
57    Stopped,
58}
59
60impl fmt::Display for MeshState {
61    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62        match self {
63            MeshState::Created => write!(f, "created"),
64            MeshState::Starting => write!(f, "starting"),
65            MeshState::Running => write!(f, "running"),
66            MeshState::Stopping => write!(f, "stopping"),
67            MeshState::Stopped => write!(f, "stopped"),
68        }
69    }
70}
71
72// ─── Error type ──────────────────────────────────────────────────────────────
73
74/// Unified error type for mesh operations.
75#[derive(Debug)]
76pub enum MeshError {
77    /// Operation requires the mesh to be running.
78    NotRunning,
79    /// Mesh is already running or starting.
80    AlreadyRunning,
81    /// Invalid configuration.
82    InvalidConfig(String),
83    /// Underlying transport error.
84    Transport(TransportError),
85    /// Catch-all for other errors.
86    Other(String),
87}
88
89impl fmt::Display for MeshError {
90    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91        match self {
92            MeshError::NotRunning => write!(f, "mesh is not running"),
93            MeshError::AlreadyRunning => write!(f, "mesh is already running"),
94            MeshError::InvalidConfig(msg) => write!(f, "invalid configuration: {}", msg),
95            MeshError::Transport(err) => write!(f, "transport error: {}", err),
96            MeshError::Other(msg) => write!(f, "{}", msg),
97        }
98    }
99}
100
101impl std::error::Error for MeshError {
102    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
103        match self {
104            MeshError::Transport(err) => Some(err),
105            _ => None,
106        }
107    }
108}
109
110impl From<TransportError> for MeshError {
111    fn from(err: TransportError) -> Self {
112        MeshError::Transport(err)
113    }
114}
115
116// ─── Events ──────────────────────────────────────────────────────────────────
117
118/// Mesh-wide events broadcast to subscribers.
119#[derive(Debug, Clone)]
120pub enum PeatMeshEvent {
121    /// Mesh lifecycle state changed.
122    StateChanged(MeshState),
123    /// A new peer joined the mesh.
124    PeerJoined(NodeId),
125    /// A peer left the mesh.
126    PeerLeft(NodeId),
127    /// Topology changed.
128    TopologyChanged(Box<crate::topology::TopologyEvent>),
129}
130
131// ─── Status snapshot ─────────────────────────────────────────────────────────
132
133/// Point-in-time snapshot of mesh status.
134#[derive(Debug, Clone)]
135pub struct MeshStatus {
136    /// Current lifecycle state.
137    pub state: MeshState,
138    /// Number of connected peers.
139    pub peer_count: usize,
140    /// This node's identifier.
141    pub node_id: String,
142    /// Time since the mesh was started.
143    pub uptime: std::time::Duration,
144}
145
146// ─── PeatMesh facade ────────────────────────────────────────────────────────
147
148const EVENT_CHANNEL_CAPACITY: usize = 256;
149
150/// Unified mesh facade composing all subsystems.
151///
152/// Create with [`PeatMesh::new`] for simple use or [`PeatMeshBuilder`] for
153/// advanced construction with pre-configured subsystems.
154pub struct PeatMesh {
155    config: MeshConfig,
156    node_id: String,
157    state: RwLock<MeshState>,
158    transport: Option<Arc<dyn MeshTransport>>,
159    transport_manager: Option<TransportManager>,
160    hierarchy: Option<Arc<dyn HierarchyStrategy>>,
161    router: Option<MeshRouter>,
162    // ── QoS ──
163    bandwidth: Option<crate::qos::BandwidthAllocation>,
164    preemption: Option<crate::qos::PreemptionController>,
165    // ── Security ──
166    device_keypair: Option<crate::security::DeviceKeypair>,
167    formation_key: Option<crate::security::FormationKey>,
168    // ── Discovery ──
169    discovery: RwLock<Option<Box<dyn crate::discovery::DiscoveryStrategy>>>,
170    // ── Beacon ──
171    beacon_broadcaster: Option<crate::beacon::BeaconBroadcaster>,
172    beacon_observer: Option<Arc<crate::beacon::BeaconObserver>>,
173    beacon_janitor: Option<crate::beacon::BeaconJanitor>,
174    // ── Topology ──
175    topology_manager: Option<crate::topology::TopologyManager>,
176    event_tx: broadcast::Sender<PeatMeshEvent>,
177    #[cfg(feature = "broker")]
178    broker_event_tx: broadcast::Sender<crate::broker::state::MeshEvent>,
179    started_at: RwLock<Option<Instant>>,
180    /// Token for signalling graceful shutdown to background tasks.
181    /// Wrapped in `RwLock` so `start()` can replace a cancelled token
182    /// when restarting a previously-stopped mesh.
183    cancellation_token: RwLock<CancellationToken>,
184}
185
186impl PeatMesh {
187    /// Create a new PeatMesh with the given configuration.
188    ///
189    /// If `config.node_id` is `None`, a UUID v4 is generated automatically.
190    pub fn new(config: MeshConfig) -> Self {
191        let node_id = config
192            .node_id
193            .clone()
194            .unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
195        let (event_tx, _) = broadcast::channel(EVENT_CHANNEL_CAPACITY);
196        #[cfg(feature = "broker")]
197        let (broker_event_tx, _) = broadcast::channel(EVENT_CHANNEL_CAPACITY);
198        Self {
199            config,
200            node_id,
201            state: RwLock::new(MeshState::Created),
202            transport: None,
203            transport_manager: None,
204            hierarchy: None,
205            router: None,
206            bandwidth: None,
207            preemption: None,
208            device_keypair: None,
209            formation_key: None,
210            discovery: RwLock::new(None),
211            beacon_broadcaster: None,
212            beacon_observer: None,
213            beacon_janitor: None,
214            topology_manager: None,
215            event_tx,
216            #[cfg(feature = "broker")]
217            broker_event_tx,
218            started_at: RwLock::new(None),
219            cancellation_token: RwLock::new(CancellationToken::new()),
220        }
221    }
222
223    /// Start the mesh (Created/Stopped → Starting → Running).
224    pub fn start(&self) -> Result<(), MeshError> {
225        let mut state = self.state.write().unwrap_or_else(|e| e.into_inner());
226        match *state {
227            MeshState::Created | MeshState::Stopped => {}
228            MeshState::Running | MeshState::Starting | MeshState::Stopping => {
229                return Err(MeshError::AlreadyRunning);
230            }
231        }
232
233        *state = MeshState::Starting;
234        let _ = self
235            .event_tx
236            .send(PeatMeshEvent::StateChanged(MeshState::Starting));
237
238        // Reset the cancellation token so a previously-stopped mesh can
239        // restart with a fresh, uncancelled token.
240        *self
241            .cancellation_token
242            .write()
243            .unwrap_or_else(|e| e.into_inner()) = CancellationToken::new();
244
245        *state = MeshState::Running;
246        *self.started_at.write().unwrap_or_else(|e| e.into_inner()) = Some(Instant::now());
247        let _ = self
248            .event_tx
249            .send(PeatMeshEvent::StateChanged(MeshState::Running));
250
251        #[cfg(feature = "broker")]
252        self.emit_broker_event(crate::broker::state::MeshEvent::TopologyChanged {
253            new_role: "standalone".to_string(),
254            peer_count: 0,
255        });
256
257        Ok(())
258    }
259
260    /// Stop the mesh (Running → Stopping → Stopped).
261    ///
262    /// Cancels the shared [`CancellationToken`] so that background tasks
263    /// (topology builder, sync channels, etc.) observe the signal and exit
264    /// promptly instead of waiting for their next interval or timeout.
265    pub fn stop(&self) -> Result<(), MeshError> {
266        let mut state = self.state.write().unwrap_or_else(|e| e.into_inner());
267        match *state {
268            MeshState::Running => {}
269            _ => return Err(MeshError::NotRunning),
270        }
271
272        *state = MeshState::Stopping;
273        let _ = self
274            .event_tx
275            .send(PeatMeshEvent::StateChanged(MeshState::Stopping));
276
277        // Signal all background tasks to shut down.
278        self.cancellation_token
279            .read()
280            .unwrap_or_else(|e| e.into_inner())
281            .cancel();
282
283        *state = MeshState::Stopped;
284        let _ = self
285            .event_tx
286            .send(PeatMeshEvent::StateChanged(MeshState::Stopped));
287
288        #[cfg(feature = "broker")]
289        self.emit_broker_event(crate::broker::state::MeshEvent::TopologyChanged {
290            new_role: "stopped".to_string(),
291            peer_count: 0,
292        });
293
294        Ok(())
295    }
296
297    /// Get the current lifecycle state.
298    pub fn state(&self) -> MeshState {
299        *self.state.read().unwrap_or_else(|e| e.into_inner())
300    }
301
302    /// Get a point-in-time status snapshot.
303    pub fn status(&self) -> MeshStatus {
304        let state = *self.state.read().unwrap_or_else(|e| e.into_inner());
305        let uptime = self
306            .started_at
307            .read()
308            .unwrap()
309            .map(|t| t.elapsed())
310            .unwrap_or_default();
311        let peer_count = self.transport.as_ref().map(|t| t.peer_count()).unwrap_or(0);
312
313        MeshStatus {
314            state,
315            peer_count,
316            node_id: self.node_id.clone(),
317            uptime,
318        }
319    }
320
321    /// Get the mesh configuration.
322    pub fn config(&self) -> &MeshConfig {
323        &self.config
324    }
325
326    /// Get the node ID.
327    pub fn node_id(&self) -> &str {
328        &self.node_id
329    }
330
331    /// Subscribe to mesh-wide events.
332    pub fn subscribe_events(&self) -> broadcast::Receiver<PeatMeshEvent> {
333        self.event_tx.subscribe()
334    }
335
336    /// Obtain a child cancellation token for a background task.
337    ///
338    /// The returned token is cancelled when [`stop()`](Self::stop) is called.
339    /// Each call returns a new child so that individual subsystems can also
340    /// cancel their own children independently.
341    pub fn child_token(&self) -> CancellationToken {
342        self.cancellation_token
343            .read()
344            .unwrap_or_else(|e| e.into_inner())
345            .child_token()
346    }
347
348    /// Set the transport layer.
349    pub fn set_transport(&mut self, transport: Arc<dyn MeshTransport>) {
350        self.transport = Some(transport);
351    }
352
353    /// Set the multi-transport manager for PACE-based transport selection.
354    pub fn set_transport_manager(&mut self, tm: TransportManager) {
355        self.transport_manager = Some(tm);
356    }
357
358    /// Get a reference to the transport manager, if set.
359    pub fn transport_manager(&self) -> Option<&TransportManager> {
360        self.transport_manager.as_ref()
361    }
362
363    /// Set the hierarchy strategy.
364    pub fn set_hierarchy(&mut self, hierarchy: Arc<dyn HierarchyStrategy>) {
365        self.hierarchy = Some(hierarchy);
366    }
367
368    /// Get a reference to the transport, if set.
369    pub fn transport(&self) -> Option<&Arc<dyn MeshTransport>> {
370        self.transport.as_ref()
371    }
372
373    /// Get a reference to the hierarchy strategy, if set.
374    pub fn hierarchy(&self) -> Option<&Arc<dyn HierarchyStrategy>> {
375        self.hierarchy.as_ref()
376    }
377
378    /// Get a reference to the router, if set.
379    pub fn router(&self) -> Option<&MeshRouter> {
380        self.router.as_ref()
381    }
382
383    // ── QoS policies ────────────────────────────────────────────
384
385    /// Set the bandwidth allocation policy.
386    pub fn set_bandwidth(&mut self, bw: crate::qos::BandwidthAllocation) {
387        self.bandwidth = Some(bw);
388    }
389
390    /// Get a reference to the bandwidth allocation, if set.
391    pub fn bandwidth(&self) -> Option<&crate::qos::BandwidthAllocation> {
392        self.bandwidth.as_ref()
393    }
394
395    /// Set the preemption controller.
396    pub fn set_preemption(&mut self, pc: crate::qos::PreemptionController) {
397        self.preemption = Some(pc);
398    }
399
400    /// Get a reference to the preemption controller, if set.
401    pub fn preemption(&self) -> Option<&crate::qos::PreemptionController> {
402        self.preemption.as_ref()
403    }
404
405    // ── Security primitives ─────────────────────────────────────
406
407    /// Set the device keypair (Ed25519).
408    pub fn set_device_keypair(&mut self, kp: crate::security::DeviceKeypair) {
409        self.device_keypair = Some(kp);
410    }
411
412    /// Get a reference to the device keypair, if set.
413    pub fn device_keypair(&self) -> Option<&crate::security::DeviceKeypair> {
414        self.device_keypair.as_ref()
415    }
416
417    /// Set the formation key (HMAC-SHA256).
418    pub fn set_formation_key(&mut self, fk: crate::security::FormationKey) {
419        self.formation_key = Some(fk);
420    }
421
422    /// Get a reference to the formation key, if set.
423    pub fn formation_key(&self) -> Option<&crate::security::FormationKey> {
424        self.formation_key.as_ref()
425    }
426
427    // ── Discovery ───────────────────────────────────────────────
428
429    /// Set the discovery strategy.
430    ///
431    /// Takes `&self` (not `&mut self`) because the field uses interior
432    /// mutability (`RwLock`) — `DiscoveryStrategy::start()` requires
433    /// `&mut self`.
434    pub fn set_discovery(&self, strategy: Box<dyn crate::discovery::DiscoveryStrategy>) {
435        *self.discovery.write().unwrap_or_else(|e| e.into_inner()) = Some(strategy);
436    }
437
438    /// Get a reference to the discovery RwLock.
439    pub fn discovery(&self) -> &RwLock<Option<Box<dyn crate::discovery::DiscoveryStrategy>>> {
440        &self.discovery
441    }
442
443    // ── Beacon ──────────────────────────────────────────────────
444
445    /// Set the beacon broadcaster.
446    pub fn set_beacon_broadcaster(&mut self, bb: crate::beacon::BeaconBroadcaster) {
447        self.beacon_broadcaster = Some(bb);
448    }
449
450    /// Get a reference to the beacon broadcaster, if set.
451    pub fn beacon_broadcaster(&self) -> Option<&crate::beacon::BeaconBroadcaster> {
452        self.beacon_broadcaster.as_ref()
453    }
454
455    /// Set the beacon observer (Arc-wrapped for sharing with TopologyBuilder).
456    pub fn set_beacon_observer(&mut self, bo: Arc<crate::beacon::BeaconObserver>) {
457        self.beacon_observer = Some(bo);
458    }
459
460    /// Get a reference to the beacon observer, if set.
461    pub fn beacon_observer(&self) -> Option<&Arc<crate::beacon::BeaconObserver>> {
462        self.beacon_observer.as_ref()
463    }
464
465    /// Set the beacon janitor.
466    pub fn set_beacon_janitor(&mut self, bj: crate::beacon::BeaconJanitor) {
467        self.beacon_janitor = Some(bj);
468    }
469
470    /// Get a reference to the beacon janitor, if set.
471    pub fn beacon_janitor(&self) -> Option<&crate::beacon::BeaconJanitor> {
472        self.beacon_janitor.as_ref()
473    }
474
475    // ── Topology ────────────────────────────────────────────────
476
477    /// Set the topology manager.
478    pub fn set_topology_manager(&mut self, tm: crate::topology::TopologyManager) {
479        self.topology_manager = Some(tm);
480    }
481
482    /// Get a reference to the topology manager, if set.
483    pub fn topology_manager(&self) -> Option<&crate::topology::TopologyManager> {
484        self.topology_manager.as_ref()
485    }
486
487    /// Emit a broker event for WebSocket subscribers.
488    #[cfg(feature = "broker")]
489    pub fn emit_mesh_event(&self, event: crate::broker::state::MeshEvent) {
490        let _ = self.broker_event_tx.send(event);
491    }
492
493    #[cfg(feature = "broker")]
494    fn emit_broker_event(&self, event: crate::broker::state::MeshEvent) {
495        let _ = self.broker_event_tx.send(event);
496    }
497}
498
499// ─── Feature-gated MeshBrokerState impl ──────────────────────────────────────
500
501#[cfg(feature = "broker")]
502#[async_trait::async_trait]
503impl crate::broker::state::MeshBrokerState for PeatMesh {
504    fn node_info(&self) -> crate::broker::state::MeshNodeInfo {
505        let uptime = self
506            .started_at
507            .read()
508            .unwrap()
509            .map(|t| t.elapsed().as_secs())
510            .unwrap_or(0);
511        crate::broker::state::MeshNodeInfo {
512            node_id: self.node_id.clone(),
513            uptime_secs: uptime,
514            version: env!("CARGO_PKG_VERSION").to_string(),
515        }
516    }
517
518    async fn list_peers(&self) -> Vec<crate::broker::state::PeerSummary> {
519        let Some(transport) = &self.transport else {
520            return vec![];
521        };
522        transport
523            .connected_peers()
524            .into_iter()
525            .map(|peer_id| {
526                let health = transport.get_peer_health(&peer_id);
527                crate::broker::state::PeerSummary {
528                    id: peer_id.to_string(),
529                    connected: true,
530                    state: health
531                        .as_ref()
532                        .map(|h| h.state.to_string())
533                        .unwrap_or_else(|| "unknown".to_string()),
534                    rtt_ms: health.map(|h| h.rtt_ms as u64),
535                }
536            })
537            .collect()
538    }
539
540    async fn get_peer(&self, id: &str) -> Option<crate::broker::state::PeerSummary> {
541        let transport = self.transport.as_ref()?;
542        let node_id = NodeId::new(id.to_string());
543        if transport.is_connected(&node_id) {
544            let health = transport.get_peer_health(&node_id);
545            Some(crate::broker::state::PeerSummary {
546                id: id.to_string(),
547                connected: true,
548                state: health
549                    .as_ref()
550                    .map(|h| h.state.to_string())
551                    .unwrap_or_else(|| "unknown".to_string()),
552                rtt_ms: health.map(|h| h.rtt_ms as u64),
553            })
554        } else {
555            None
556        }
557    }
558
559    fn topology(&self) -> crate::broker::state::TopologySummary {
560        let peer_count = self.transport.as_ref().map(|t| t.peer_count()).unwrap_or(0);
561        crate::broker::state::TopologySummary {
562            peer_count,
563            role: "standalone".to_string(),
564            hierarchy_level: 0,
565        }
566    }
567
568    fn subscribe_events(&self) -> broadcast::Receiver<crate::broker::state::MeshEvent> {
569        self.broker_event_tx.subscribe()
570    }
571}
572
573// ─── Builder ─────────────────────────────────────────────────────────────────
574
575/// Builder for constructing a [`PeatMesh`] with pre-configured subsystems.
576pub struct PeatMeshBuilder {
577    config: MeshConfig,
578    transport: Option<Arc<dyn MeshTransport>>,
579    transport_manager: Option<TransportManager>,
580    hierarchy: Option<Arc<dyn HierarchyStrategy>>,
581    router: Option<MeshRouter>,
582    bandwidth: Option<crate::qos::BandwidthAllocation>,
583    preemption: Option<crate::qos::PreemptionController>,
584    device_keypair: Option<crate::security::DeviceKeypair>,
585    formation_key: Option<crate::security::FormationKey>,
586    discovery: Option<Box<dyn crate::discovery::DiscoveryStrategy>>,
587    beacon_broadcaster: Option<crate::beacon::BeaconBroadcaster>,
588    beacon_observer: Option<Arc<crate::beacon::BeaconObserver>>,
589    beacon_janitor: Option<crate::beacon::BeaconJanitor>,
590    topology_manager: Option<crate::topology::TopologyManager>,
591}
592
593impl PeatMeshBuilder {
594    /// Create a new builder with the given configuration.
595    pub fn new(config: MeshConfig) -> Self {
596        Self {
597            config,
598            transport: None,
599            transport_manager: None,
600            hierarchy: None,
601            router: None,
602            bandwidth: None,
603            preemption: None,
604            device_keypair: None,
605            formation_key: None,
606            discovery: None,
607            beacon_broadcaster: None,
608            beacon_observer: None,
609            beacon_janitor: None,
610            topology_manager: None,
611        }
612    }
613
614    /// Set a single transport layer.
615    pub fn with_transport(mut self, transport: Arc<dyn MeshTransport>) -> Self {
616        self.transport = Some(transport);
617        self
618    }
619
620    /// Set the multi-transport manager for PACE-based transport selection.
621    pub fn with_transport_manager(mut self, tm: TransportManager) -> Self {
622        self.transport_manager = Some(tm);
623        self
624    }
625
626    /// Set the hierarchy strategy.
627    pub fn with_hierarchy(mut self, hierarchy: Arc<dyn HierarchyStrategy>) -> Self {
628        self.hierarchy = Some(hierarchy);
629        self
630    }
631
632    /// Set the router.
633    pub fn with_router(mut self, router: MeshRouter) -> Self {
634        self.router = Some(router);
635        self
636    }
637
638    /// Set the bandwidth allocation policy.
639    pub fn with_bandwidth(mut self, bw: crate::qos::BandwidthAllocation) -> Self {
640        self.bandwidth = Some(bw);
641        self
642    }
643
644    /// Set the preemption controller.
645    pub fn with_preemption(mut self, pc: crate::qos::PreemptionController) -> Self {
646        self.preemption = Some(pc);
647        self
648    }
649
650    /// Set the device keypair.
651    pub fn with_device_keypair(mut self, kp: crate::security::DeviceKeypair) -> Self {
652        self.device_keypair = Some(kp);
653        self
654    }
655
656    /// Derive a deterministic device keypair from a seed and context.
657    ///
658    /// Convenience wrapper around [`crate::security::DeviceKeypair::from_seed`].
659    pub fn with_device_keypair_from_seed(
660        mut self,
661        seed: &[u8],
662        context: &str,
663    ) -> Result<Self, MeshError> {
664        let kp = crate::security::DeviceKeypair::from_seed(seed, context)
665            .map_err(|e| MeshError::InvalidConfig(e.to_string()))?;
666        self.device_keypair = Some(kp);
667        Ok(self)
668    }
669
670    /// Set the formation key.
671    pub fn with_formation_key(mut self, fk: crate::security::FormationKey) -> Self {
672        self.formation_key = Some(fk);
673        self
674    }
675
676    /// Set the discovery strategy.
677    pub fn with_discovery(
678        mut self,
679        strategy: Box<dyn crate::discovery::DiscoveryStrategy>,
680    ) -> Self {
681        self.discovery = Some(strategy);
682        self
683    }
684
685    /// Set the beacon broadcaster.
686    pub fn with_beacon_broadcaster(mut self, bb: crate::beacon::BeaconBroadcaster) -> Self {
687        self.beacon_broadcaster = Some(bb);
688        self
689    }
690
691    /// Set the beacon observer.
692    pub fn with_beacon_observer(mut self, bo: Arc<crate::beacon::BeaconObserver>) -> Self {
693        self.beacon_observer = Some(bo);
694        self
695    }
696
697    /// Set the beacon janitor.
698    pub fn with_beacon_janitor(mut self, bj: crate::beacon::BeaconJanitor) -> Self {
699        self.beacon_janitor = Some(bj);
700        self
701    }
702
703    /// Set the topology manager.
704    pub fn with_topology_manager(mut self, tm: crate::topology::TopologyManager) -> Self {
705        self.topology_manager = Some(tm);
706        self
707    }
708
709    /// Build the [`PeatMesh`] instance.
710    pub fn build(self) -> PeatMesh {
711        let node_id = self
712            .config
713            .node_id
714            .clone()
715            .unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
716        let (event_tx, _) = broadcast::channel(EVENT_CHANNEL_CAPACITY);
717        #[cfg(feature = "broker")]
718        let (broker_event_tx, _) = broadcast::channel(EVENT_CHANNEL_CAPACITY);
719
720        PeatMesh {
721            config: self.config,
722            node_id,
723            state: RwLock::new(MeshState::Created),
724            transport: self.transport,
725            transport_manager: self.transport_manager,
726            hierarchy: self.hierarchy,
727            router: self.router,
728            bandwidth: self.bandwidth,
729            preemption: self.preemption,
730            device_keypair: self.device_keypair,
731            formation_key: self.formation_key,
732            discovery: RwLock::new(self.discovery),
733            beacon_broadcaster: self.beacon_broadcaster,
734            beacon_observer: self.beacon_observer,
735            beacon_janitor: self.beacon_janitor,
736            topology_manager: self.topology_manager,
737            event_tx,
738            #[cfg(feature = "broker")]
739            broker_event_tx,
740            started_at: RwLock::new(None),
741            cancellation_token: RwLock::new(CancellationToken::new()),
742        }
743    }
744}
745
746// ─── Tests ───────────────────────────────────────────────────────────────────
747
748#[cfg(test)]
749mod tests {
750    use super::*;
751    use crate::config::MeshDiscoveryConfig;
752    use crate::transport::PeerEventReceiver;
753    use async_trait::async_trait;
754    use std::time::Duration;
755
756    // ── Mock transport for testing ───────────────────────────────
757
758    struct MockTransport {
759        peers: Vec<NodeId>,
760    }
761
762    impl MockTransport {
763        fn new(peers: Vec<NodeId>) -> Self {
764            Self { peers }
765        }
766
767        fn empty() -> Self {
768            Self { peers: vec![] }
769        }
770    }
771
772    #[async_trait]
773    impl MeshTransport for MockTransport {
774        async fn start(&self) -> crate::transport::Result<()> {
775            Ok(())
776        }
777        async fn stop(&self) -> crate::transport::Result<()> {
778            Ok(())
779        }
780        async fn connect(
781            &self,
782            _peer_id: &NodeId,
783        ) -> crate::transport::Result<Box<dyn crate::transport::MeshConnection>> {
784            Err(TransportError::NotStarted)
785        }
786        async fn disconnect(&self, _peer_id: &NodeId) -> crate::transport::Result<()> {
787            Ok(())
788        }
789        fn get_connection(
790            &self,
791            _peer_id: &NodeId,
792        ) -> Option<Box<dyn crate::transport::MeshConnection>> {
793            None
794        }
795        fn peer_count(&self) -> usize {
796            self.peers.len()
797        }
798        fn connected_peers(&self) -> Vec<NodeId> {
799            self.peers.clone()
800        }
801        fn subscribe_peer_events(&self) -> PeerEventReceiver {
802            let (_tx, rx) = tokio::sync::mpsc::channel(1);
803            rx
804        }
805    }
806
807    // ── PeatMesh::new ────────────────────────────────────────────
808
809    #[test]
810    fn test_new_with_default_config() {
811        let mesh = PeatMesh::new(MeshConfig::default());
812        assert_eq!(mesh.state(), MeshState::Created);
813        assert!(!mesh.node_id().is_empty());
814    }
815
816    #[test]
817    fn test_new_with_explicit_node_id() {
818        let cfg = MeshConfig {
819            node_id: Some("my-node".to_string()),
820            ..Default::default()
821        };
822        let mesh = PeatMesh::new(cfg);
823        assert_eq!(mesh.node_id(), "my-node");
824    }
825
826    #[test]
827    fn test_new_auto_generates_uuid_node_id() {
828        let mesh = PeatMesh::new(MeshConfig::default());
829        // UUID v4 format: 8-4-4-4-12 hex digits
830        assert_eq!(mesh.node_id().len(), 36);
831        assert_eq!(mesh.node_id().chars().filter(|&c| c == '-').count(), 4);
832    }
833
834    // ── Lifecycle: start / stop ──────────────────────────────────
835
836    #[test]
837    fn test_start_transitions_to_running() {
838        let mesh = PeatMesh::new(MeshConfig::default());
839        assert!(mesh.start().is_ok());
840        assert_eq!(mesh.state(), MeshState::Running);
841    }
842
843    #[test]
844    fn test_start_when_already_running_returns_error() {
845        let mesh = PeatMesh::new(MeshConfig::default());
846        mesh.start().unwrap();
847        let err = mesh.start().unwrap_err();
848        assert!(matches!(err, MeshError::AlreadyRunning));
849    }
850
851    #[test]
852    fn test_stop_transitions_to_stopped() {
853        let mesh = PeatMesh::new(MeshConfig::default());
854        mesh.start().unwrap();
855        assert!(mesh.stop().is_ok());
856        assert_eq!(mesh.state(), MeshState::Stopped);
857    }
858
859    #[test]
860    fn test_stop_when_not_running_returns_error() {
861        let mesh = PeatMesh::new(MeshConfig::default());
862        let err = mesh.stop().unwrap_err();
863        assert!(matches!(err, MeshError::NotRunning));
864    }
865
866    #[test]
867    fn test_restart_after_stop() {
868        let mesh = PeatMesh::new(MeshConfig::default());
869        mesh.start().unwrap();
870        mesh.stop().unwrap();
871        assert!(mesh.start().is_ok());
872        assert_eq!(mesh.state(), MeshState::Running);
873    }
874
875    #[test]
876    fn test_stop_when_created_returns_error() {
877        let mesh = PeatMesh::new(MeshConfig::default());
878        assert!(matches!(mesh.stop().unwrap_err(), MeshError::NotRunning));
879    }
880
881    #[test]
882    fn test_stop_when_already_stopped_returns_error() {
883        let mesh = PeatMesh::new(MeshConfig::default());
884        mesh.start().unwrap();
885        mesh.stop().unwrap();
886        assert!(matches!(mesh.stop().unwrap_err(), MeshError::NotRunning));
887    }
888
889    // ── Status ───────────────────────────────────────────────────
890
891    #[test]
892    fn test_status_before_start() {
893        let cfg = MeshConfig {
894            node_id: Some("status-node".to_string()),
895            ..Default::default()
896        };
897        let mesh = PeatMesh::new(cfg);
898        let status = mesh.status();
899        assert_eq!(status.state, MeshState::Created);
900        assert_eq!(status.peer_count, 0);
901        assert_eq!(status.node_id, "status-node");
902        assert_eq!(status.uptime, Duration::ZERO);
903    }
904
905    #[test]
906    fn test_status_while_running() {
907        let mesh = PeatMesh::new(MeshConfig {
908            node_id: Some("running-node".to_string()),
909            ..Default::default()
910        });
911        mesh.start().unwrap();
912        let status = mesh.status();
913        assert_eq!(status.state, MeshState::Running);
914        assert_eq!(status.node_id, "running-node");
915        // Uptime should be non-zero (or at least zero on a very fast machine)
916        assert!(status.uptime <= Duration::from_secs(1));
917    }
918
919    #[test]
920    fn test_status_peer_count_with_transport() {
921        let peers = vec![NodeId::new("p1".into()), NodeId::new("p2".into())];
922        let mut mesh = PeatMesh::new(MeshConfig::default());
923        mesh.set_transport(Arc::new(MockTransport::new(peers)));
924        let status = mesh.status();
925        assert_eq!(status.peer_count, 2);
926    }
927
928    // ── Config accessor ──────────────────────────────────────────
929
930    #[test]
931    fn test_config_accessor() {
932        let cfg = MeshConfig {
933            node_id: Some("cfg-test".to_string()),
934            discovery: MeshDiscoveryConfig {
935                mdns_enabled: false,
936                ..Default::default()
937            },
938            ..Default::default()
939        };
940        let mesh = PeatMesh::new(cfg);
941        assert_eq!(mesh.config().node_id.as_deref(), Some("cfg-test"));
942        assert!(!mesh.config().discovery.mdns_enabled);
943    }
944
945    // ── Event subscription ───────────────────────────────────────
946
947    #[test]
948    fn test_subscribe_events_receives_state_changes() {
949        let mesh = PeatMesh::new(MeshConfig::default());
950        let mut rx = mesh.subscribe_events();
951
952        mesh.start().unwrap();
953
954        // Should receive Starting then Running
955        let evt1 = rx.try_recv().unwrap();
956        assert!(matches!(
957            evt1,
958            PeatMeshEvent::StateChanged(MeshState::Starting)
959        ));
960        let evt2 = rx.try_recv().unwrap();
961        assert!(matches!(
962            evt2,
963            PeatMeshEvent::StateChanged(MeshState::Running)
964        ));
965    }
966
967    #[test]
968    fn test_subscribe_events_receives_stop_events() {
969        let mesh = PeatMesh::new(MeshConfig::default());
970        let mut rx = mesh.subscribe_events();
971
972        mesh.start().unwrap();
973        // Drain start events
974        let _ = rx.try_recv();
975        let _ = rx.try_recv();
976
977        mesh.stop().unwrap();
978
979        let evt1 = rx.try_recv().unwrap();
980        assert!(matches!(
981            evt1,
982            PeatMeshEvent::StateChanged(MeshState::Stopping)
983        ));
984        let evt2 = rx.try_recv().unwrap();
985        assert!(matches!(
986            evt2,
987            PeatMeshEvent::StateChanged(MeshState::Stopped)
988        ));
989    }
990
991    #[test]
992    fn test_multiple_subscribers() {
993        let mesh = PeatMesh::new(MeshConfig::default());
994        let mut rx1 = mesh.subscribe_events();
995        let mut rx2 = mesh.subscribe_events();
996
997        mesh.start().unwrap();
998
999        // Both receivers should get events
1000        assert!(rx1.try_recv().is_ok());
1001        assert!(rx2.try_recv().is_ok());
1002    }
1003
1004    // ── set_transport / set_hierarchy ────────────────────────────
1005
1006    #[test]
1007    fn test_set_transport() {
1008        let mut mesh = PeatMesh::new(MeshConfig::default());
1009        assert!(mesh.transport().is_none());
1010
1011        mesh.set_transport(Arc::new(MockTransport::empty()));
1012        assert!(mesh.transport().is_some());
1013    }
1014
1015    #[test]
1016    fn test_set_hierarchy() {
1017        use crate::beacon::HierarchyLevel;
1018        use crate::hierarchy::{NodeRole, StaticHierarchyStrategy};
1019
1020        let mut mesh = PeatMesh::new(MeshConfig::default());
1021        assert!(mesh.hierarchy().is_none());
1022
1023        let strategy = StaticHierarchyStrategy {
1024            assigned_level: HierarchyLevel::Platoon,
1025            assigned_role: NodeRole::Leader,
1026        };
1027        mesh.set_hierarchy(Arc::new(strategy));
1028        assert!(mesh.hierarchy().is_some());
1029    }
1030
1031    #[test]
1032    fn test_router_initially_none() {
1033        let mesh = PeatMesh::new(MeshConfig::default());
1034        assert!(mesh.router().is_none());
1035    }
1036
1037    // ── MeshState ────────────────────────────────────────────────
1038
1039    #[test]
1040    fn test_mesh_state_display() {
1041        assert_eq!(MeshState::Created.to_string(), "created");
1042        assert_eq!(MeshState::Starting.to_string(), "starting");
1043        assert_eq!(MeshState::Running.to_string(), "running");
1044        assert_eq!(MeshState::Stopping.to_string(), "stopping");
1045        assert_eq!(MeshState::Stopped.to_string(), "stopped");
1046    }
1047
1048    #[test]
1049    fn test_mesh_state_equality() {
1050        assert_eq!(MeshState::Created, MeshState::Created);
1051        assert_ne!(MeshState::Created, MeshState::Running);
1052    }
1053
1054    #[test]
1055    fn test_mesh_state_clone_copy() {
1056        let s = MeshState::Running;
1057        let copied = s;
1058        // Verify Copy semantics: original is still usable after copy
1059        assert_eq!(s, copied);
1060    }
1061
1062    #[test]
1063    fn test_mesh_state_debug() {
1064        let debug = format!("{:?}", MeshState::Running);
1065        assert!(debug.contains("Running"));
1066    }
1067
1068    // ── MeshError ────────────────────────────────────────────────
1069
1070    #[test]
1071    fn test_mesh_error_display_not_running() {
1072        let err = MeshError::NotRunning;
1073        assert_eq!(err.to_string(), "mesh is not running");
1074    }
1075
1076    #[test]
1077    fn test_mesh_error_display_already_running() {
1078        let err = MeshError::AlreadyRunning;
1079        assert_eq!(err.to_string(), "mesh is already running");
1080    }
1081
1082    #[test]
1083    fn test_mesh_error_display_invalid_config() {
1084        let err = MeshError::InvalidConfig("bad value".to_string());
1085        assert_eq!(err.to_string(), "invalid configuration: bad value");
1086    }
1087
1088    #[test]
1089    fn test_mesh_error_display_transport() {
1090        let terr = TransportError::NotStarted;
1091        let err = MeshError::Transport(terr);
1092        assert!(err.to_string().contains("Transport not started"));
1093    }
1094
1095    #[test]
1096    fn test_mesh_error_display_other() {
1097        let err = MeshError::Other("something went wrong".to_string());
1098        assert_eq!(err.to_string(), "something went wrong");
1099    }
1100
1101    #[test]
1102    fn test_mesh_error_source_transport() {
1103        use std::error::Error;
1104        let terr = TransportError::ConnectionFailed("timeout".into());
1105        let err = MeshError::Transport(terr);
1106        assert!(err.source().is_some());
1107    }
1108
1109    #[test]
1110    fn test_mesh_error_source_none_for_others() {
1111        use std::error::Error;
1112        assert!(MeshError::NotRunning.source().is_none());
1113        assert!(MeshError::AlreadyRunning.source().is_none());
1114        assert!(MeshError::InvalidConfig("x".into()).source().is_none());
1115        assert!(MeshError::Other("x".into()).source().is_none());
1116    }
1117
1118    #[test]
1119    fn test_mesh_error_from_transport_error() {
1120        let terr = TransportError::NotStarted;
1121        let err: MeshError = terr.into();
1122        assert!(matches!(err, MeshError::Transport(_)));
1123    }
1124
1125    #[test]
1126    fn test_mesh_error_debug() {
1127        let err = MeshError::NotRunning;
1128        let debug = format!("{:?}", err);
1129        assert!(debug.contains("NotRunning"));
1130    }
1131
1132    // ── PeatMeshEvent ────────────────────────────────────────────
1133
1134    #[test]
1135    fn test_event_state_changed() {
1136        let evt = PeatMeshEvent::StateChanged(MeshState::Running);
1137        let debug = format!("{:?}", evt);
1138        assert!(debug.contains("Running"));
1139    }
1140
1141    #[test]
1142    fn test_event_peer_joined() {
1143        let evt = PeatMeshEvent::PeerJoined(NodeId::new("peer-1".into()));
1144        let cloned = evt.clone();
1145        let debug = format!("{:?}", cloned);
1146        assert!(debug.contains("peer-1"));
1147    }
1148
1149    #[test]
1150    fn test_event_peer_left() {
1151        let evt = PeatMeshEvent::PeerLeft(NodeId::new("peer-2".into()));
1152        let cloned = evt.clone();
1153        let debug = format!("{:?}", cloned);
1154        assert!(debug.contains("peer-2"));
1155    }
1156
1157    #[test]
1158    fn test_event_topology_changed() {
1159        let topo_evt = crate::topology::TopologyEvent::PeerLost {
1160            lost_peer_id: "gone".to_string(),
1161        };
1162        let evt = PeatMeshEvent::TopologyChanged(Box::new(topo_evt));
1163        let cloned = evt.clone();
1164        let debug = format!("{:?}", cloned);
1165        assert!(debug.contains("gone"));
1166    }
1167
1168    // ── MeshStatus ───────────────────────────────────────────────
1169
1170    #[test]
1171    fn test_mesh_status_debug() {
1172        let status = MeshStatus {
1173            state: MeshState::Running,
1174            peer_count: 5,
1175            node_id: "n1".to_string(),
1176            uptime: Duration::from_secs(120),
1177        };
1178        let debug = format!("{:?}", status);
1179        assert!(debug.contains("Running"));
1180        assert!(debug.contains("n1"));
1181    }
1182
1183    #[test]
1184    fn test_mesh_status_clone() {
1185        let status = MeshStatus {
1186            state: MeshState::Stopped,
1187            peer_count: 0,
1188            node_id: "n2".to_string(),
1189            uptime: Duration::ZERO,
1190        };
1191        let cloned = status.clone();
1192        assert_eq!(cloned.state, MeshState::Stopped);
1193        assert_eq!(cloned.node_id, "n2");
1194    }
1195
1196    // ── PeatMeshBuilder ──────────────────────────────────────────
1197
1198    #[test]
1199    fn test_builder_minimal() {
1200        let mesh = PeatMeshBuilder::new(MeshConfig::default()).build();
1201        assert_eq!(mesh.state(), MeshState::Created);
1202        assert!(mesh.transport().is_none());
1203        assert!(mesh.hierarchy().is_none());
1204        assert!(mesh.router().is_none());
1205    }
1206
1207    #[test]
1208    fn test_builder_with_node_id() {
1209        let cfg = MeshConfig {
1210            node_id: Some("builder-node".to_string()),
1211            ..Default::default()
1212        };
1213        let mesh = PeatMeshBuilder::new(cfg).build();
1214        assert_eq!(mesh.node_id(), "builder-node");
1215    }
1216
1217    #[test]
1218    fn test_builder_with_transport() {
1219        let mesh = PeatMeshBuilder::new(MeshConfig::default())
1220            .with_transport(Arc::new(MockTransport::empty()))
1221            .build();
1222        assert!(mesh.transport().is_some());
1223    }
1224
1225    #[test]
1226    fn test_builder_with_hierarchy() {
1227        use crate::beacon::HierarchyLevel;
1228        use crate::hierarchy::{NodeRole, StaticHierarchyStrategy};
1229
1230        let strategy = StaticHierarchyStrategy {
1231            assigned_level: HierarchyLevel::Squad,
1232            assigned_role: NodeRole::Member,
1233        };
1234        let mesh = PeatMeshBuilder::new(MeshConfig::default())
1235            .with_hierarchy(Arc::new(strategy))
1236            .build();
1237        assert!(mesh.hierarchy().is_some());
1238    }
1239
1240    #[test]
1241    fn test_builder_with_router() {
1242        let router = MeshRouter::with_node_id("test");
1243        let mesh = PeatMeshBuilder::new(MeshConfig::default())
1244            .with_router(router)
1245            .build();
1246        assert!(mesh.router().is_some());
1247    }
1248
1249    #[test]
1250    fn test_builder_all_subsystems() {
1251        use crate::beacon::HierarchyLevel;
1252        use crate::hierarchy::{NodeRole, StaticHierarchyStrategy};
1253
1254        let strategy = StaticHierarchyStrategy {
1255            assigned_level: HierarchyLevel::Platoon,
1256            assigned_role: NodeRole::Leader,
1257        };
1258        let peers = vec![NodeId::new("p1".into())];
1259        let router = MeshRouter::with_node_id("full");
1260
1261        let mesh = PeatMeshBuilder::new(MeshConfig {
1262            node_id: Some("full-node".to_string()),
1263            ..Default::default()
1264        })
1265        .with_transport(Arc::new(MockTransport::new(peers)))
1266        .with_hierarchy(Arc::new(strategy))
1267        .with_router(router)
1268        .build();
1269
1270        assert_eq!(mesh.node_id(), "full-node");
1271        assert!(mesh.transport().is_some());
1272        assert!(mesh.hierarchy().is_some());
1273        assert!(mesh.router().is_some());
1274        assert_eq!(mesh.status().peer_count, 1);
1275    }
1276
1277    #[test]
1278    fn test_builder_lifecycle() {
1279        let mesh = PeatMeshBuilder::new(MeshConfig::default()).build();
1280        assert!(mesh.start().is_ok());
1281        assert_eq!(mesh.state(), MeshState::Running);
1282        assert!(mesh.stop().is_ok());
1283        assert_eq!(mesh.state(), MeshState::Stopped);
1284    }
1285
1286    // ── TransportManager integration ──────────────────────────────
1287
1288    #[test]
1289    fn test_transport_manager_initially_none() {
1290        let mesh = PeatMesh::new(MeshConfig::default());
1291        assert!(mesh.transport_manager().is_none());
1292    }
1293
1294    #[test]
1295    fn test_set_transport_manager() {
1296        use crate::transport::TransportManagerConfig;
1297        let mut mesh = PeatMesh::new(MeshConfig::default());
1298        let tm = TransportManager::new(TransportManagerConfig::default());
1299        mesh.set_transport_manager(tm);
1300        assert!(mesh.transport_manager().is_some());
1301    }
1302
1303    #[test]
1304    fn test_builder_with_transport_manager() {
1305        use crate::transport::TransportManagerConfig;
1306        let tm = TransportManager::new(TransportManagerConfig::default());
1307        let mesh = PeatMeshBuilder::new(MeshConfig::default())
1308            .with_transport_manager(tm)
1309            .build();
1310        assert!(mesh.transport_manager().is_some());
1311    }
1312
1313    #[test]
1314    fn test_builder_full_with_transport_manager() {
1315        use crate::beacon::HierarchyLevel;
1316        use crate::hierarchy::{NodeRole, StaticHierarchyStrategy};
1317        use crate::transport::TransportManagerConfig;
1318
1319        let strategy = StaticHierarchyStrategy {
1320            assigned_level: HierarchyLevel::Platoon,
1321            assigned_role: NodeRole::Leader,
1322        };
1323        let peers = vec![NodeId::new("p1".into())];
1324        let router = MeshRouter::with_node_id("full");
1325        let tm = TransportManager::new(TransportManagerConfig::default());
1326
1327        let mesh = PeatMeshBuilder::new(MeshConfig {
1328            node_id: Some("full-tm-node".to_string()),
1329            ..Default::default()
1330        })
1331        .with_transport(Arc::new(MockTransport::new(peers)))
1332        .with_transport_manager(tm)
1333        .with_hierarchy(Arc::new(strategy))
1334        .with_router(router)
1335        .build();
1336
1337        assert_eq!(mesh.node_id(), "full-tm-node");
1338        assert!(mesh.transport().is_some());
1339        assert!(mesh.transport_manager().is_some());
1340        assert!(mesh.hierarchy().is_some());
1341        assert!(mesh.router().is_some());
1342    }
1343
1344    // ── Gap 5: QoS policies ────────────────────────────────────────
1345
1346    #[test]
1347    fn test_bandwidth_initially_none() {
1348        let mesh = PeatMesh::new(MeshConfig::default());
1349        assert!(mesh.bandwidth().is_none());
1350    }
1351
1352    #[test]
1353    fn test_set_bandwidth() {
1354        let mut mesh = PeatMesh::new(MeshConfig::default());
1355        mesh.set_bandwidth(crate::qos::BandwidthAllocation::new(1_000_000));
1356        assert!(mesh.bandwidth().is_some());
1357    }
1358
1359    #[test]
1360    fn test_preemption_initially_none() {
1361        let mesh = PeatMesh::new(MeshConfig::default());
1362        assert!(mesh.preemption().is_none());
1363    }
1364
1365    #[test]
1366    fn test_set_preemption() {
1367        let mut mesh = PeatMesh::new(MeshConfig::default());
1368        mesh.set_preemption(crate::qos::PreemptionController::new());
1369        assert!(mesh.preemption().is_some());
1370    }
1371
1372    #[test]
1373    fn test_builder_with_bandwidth() {
1374        let mesh = PeatMeshBuilder::new(MeshConfig::default())
1375            .with_bandwidth(crate::qos::BandwidthAllocation::default_tactical())
1376            .build();
1377        assert!(mesh.bandwidth().is_some());
1378    }
1379
1380    #[test]
1381    fn test_builder_with_preemption() {
1382        let mesh = PeatMeshBuilder::new(MeshConfig::default())
1383            .with_preemption(crate::qos::PreemptionController::new())
1384            .build();
1385        assert!(mesh.preemption().is_some());
1386    }
1387
1388    // ── Gap 6: Security primitives ─────────────────────────────────
1389
1390    #[test]
1391    fn test_device_keypair_initially_none() {
1392        let mesh = PeatMesh::new(MeshConfig::default());
1393        assert!(mesh.device_keypair().is_none());
1394    }
1395
1396    #[test]
1397    fn test_set_device_keypair() {
1398        let mut mesh = PeatMesh::new(MeshConfig::default());
1399        mesh.set_device_keypair(crate::security::DeviceKeypair::generate());
1400        assert!(mesh.device_keypair().is_some());
1401    }
1402
1403    #[test]
1404    fn test_formation_key_initially_none() {
1405        let mesh = PeatMesh::new(MeshConfig::default());
1406        assert!(mesh.formation_key().is_none());
1407    }
1408
1409    #[test]
1410    fn test_set_formation_key() {
1411        let mut mesh = PeatMesh::new(MeshConfig::default());
1412        mesh.set_formation_key(crate::security::FormationKey::new(
1413            "test-formation",
1414            &[0u8; 32],
1415        ));
1416        assert!(mesh.formation_key().is_some());
1417    }
1418
1419    #[test]
1420    fn test_builder_with_device_keypair() {
1421        let mesh = PeatMeshBuilder::new(MeshConfig::default())
1422            .with_device_keypair(crate::security::DeviceKeypair::generate())
1423            .build();
1424        assert!(mesh.device_keypair().is_some());
1425    }
1426
1427    #[test]
1428    fn test_builder_with_device_keypair_from_seed() {
1429        let mesh = PeatMeshBuilder::new(MeshConfig::default())
1430            .with_device_keypair_from_seed(b"k8s-secret", "pod-1")
1431            .unwrap()
1432            .build();
1433        assert!(mesh.device_keypair().is_some());
1434
1435        // Same seed+context should produce the same device ID
1436        let mesh2 = PeatMeshBuilder::new(MeshConfig::default())
1437            .with_device_keypair_from_seed(b"k8s-secret", "pod-1")
1438            .unwrap()
1439            .build();
1440        assert_eq!(
1441            mesh.device_keypair().unwrap().device_id(),
1442            mesh2.device_keypair().unwrap().device_id()
1443        );
1444    }
1445
1446    #[test]
1447    fn test_builder_with_formation_key() {
1448        let mesh = PeatMeshBuilder::new(MeshConfig::default())
1449            .with_formation_key(crate::security::FormationKey::new("f1", &[1u8; 32]))
1450            .build();
1451        assert!(mesh.formation_key().is_some());
1452    }
1453
1454    // ── Gap 3: Discovery strategy ──────────────────────────────────
1455
1456    #[test]
1457    fn test_discovery_initially_none() {
1458        let mesh = PeatMesh::new(MeshConfig::default());
1459        assert!(mesh
1460            .discovery()
1461            .read()
1462            .unwrap_or_else(|e| e.into_inner())
1463            .is_none());
1464    }
1465
1466    #[test]
1467    fn test_set_discovery() {
1468        let mesh = PeatMesh::new(MeshConfig::default());
1469        let strategy = crate::discovery::HybridDiscovery::new();
1470        mesh.set_discovery(Box::new(strategy));
1471        assert!(mesh
1472            .discovery()
1473            .read()
1474            .unwrap_or_else(|e| e.into_inner())
1475            .is_some());
1476    }
1477
1478    #[test]
1479    fn test_builder_with_discovery() {
1480        let strategy = crate::discovery::HybridDiscovery::new();
1481        let mesh = PeatMeshBuilder::new(MeshConfig::default())
1482            .with_discovery(Box::new(strategy))
1483            .build();
1484        assert!(mesh
1485            .discovery()
1486            .read()
1487            .unwrap_or_else(|e| e.into_inner())
1488            .is_some());
1489    }
1490
1491    // ── Gap 2: Beacon system ───────────────────────────────────────
1492
1493    fn mock_storage() -> Arc<dyn crate::beacon::BeaconStorage> {
1494        Arc::new(crate::beacon::MockBeaconStorage::new())
1495    }
1496
1497    #[test]
1498    fn test_beacon_broadcaster_initially_none() {
1499        let mesh = PeatMesh::new(MeshConfig::default());
1500        assert!(mesh.beacon_broadcaster().is_none());
1501    }
1502
1503    #[test]
1504    fn test_set_beacon_broadcaster() {
1505        use crate::beacon::{BeaconBroadcaster, GeoPosition, HierarchyLevel};
1506
1507        let mut mesh = PeatMesh::new(MeshConfig::default());
1508        let bb = BeaconBroadcaster::new(
1509            mock_storage(),
1510            "test-node".to_string(),
1511            GeoPosition {
1512                lat: 0.0,
1513                lon: 0.0,
1514                alt: None,
1515            },
1516            HierarchyLevel::Squad,
1517            None,
1518            Duration::from_secs(5),
1519        );
1520        mesh.set_beacon_broadcaster(bb);
1521        assert!(mesh.beacon_broadcaster().is_some());
1522    }
1523
1524    #[test]
1525    fn test_beacon_observer_initially_none() {
1526        let mesh = PeatMesh::new(MeshConfig::default());
1527        assert!(mesh.beacon_observer().is_none());
1528    }
1529
1530    #[test]
1531    fn test_set_beacon_observer() {
1532        use crate::beacon::BeaconObserver;
1533
1534        let mut mesh = PeatMesh::new(MeshConfig::default());
1535        let bo = Arc::new(BeaconObserver::new(mock_storage(), "s00000".to_string()));
1536        mesh.set_beacon_observer(bo);
1537        assert!(mesh.beacon_observer().is_some());
1538    }
1539
1540    #[test]
1541    fn test_beacon_janitor_initially_none() {
1542        let mesh = PeatMesh::new(MeshConfig::default());
1543        assert!(mesh.beacon_janitor().is_none());
1544    }
1545
1546    #[test]
1547    fn test_set_beacon_janitor() {
1548        use crate::beacon::BeaconJanitor;
1549        use std::collections::HashMap;
1550
1551        let mut mesh = PeatMesh::new(MeshConfig::default());
1552        let nearby = Arc::new(tokio::sync::RwLock::new(HashMap::new()));
1553        let bj = BeaconJanitor::new(nearby, Duration::from_secs(60), Duration::from_secs(10));
1554        mesh.set_beacon_janitor(bj);
1555        assert!(mesh.beacon_janitor().is_some());
1556    }
1557
1558    #[test]
1559    fn test_builder_with_beacon_broadcaster() {
1560        use crate::beacon::{BeaconBroadcaster, GeoPosition, HierarchyLevel};
1561
1562        let bb = BeaconBroadcaster::new(
1563            mock_storage(),
1564            "builder-node".to_string(),
1565            GeoPosition {
1566                lat: 1.0,
1567                lon: 2.0,
1568                alt: None,
1569            },
1570            HierarchyLevel::Platoon,
1571            None,
1572            Duration::from_secs(5),
1573        );
1574        let mesh = PeatMeshBuilder::new(MeshConfig::default())
1575            .with_beacon_broadcaster(bb)
1576            .build();
1577        assert!(mesh.beacon_broadcaster().is_some());
1578    }
1579
1580    #[test]
1581    fn test_builder_with_beacon_observer() {
1582        use crate::beacon::BeaconObserver;
1583
1584        let bo = Arc::new(BeaconObserver::new(mock_storage(), "s00000".to_string()));
1585        let mesh = PeatMeshBuilder::new(MeshConfig::default())
1586            .with_beacon_observer(bo)
1587            .build();
1588        assert!(mesh.beacon_observer().is_some());
1589    }
1590
1591    #[test]
1592    fn test_builder_with_beacon_janitor() {
1593        use crate::beacon::BeaconJanitor;
1594        use std::collections::HashMap;
1595
1596        let nearby = Arc::new(tokio::sync::RwLock::new(HashMap::new()));
1597        let bj = BeaconJanitor::new(nearby, Duration::from_secs(60), Duration::from_secs(10));
1598        let mesh = PeatMeshBuilder::new(MeshConfig::default())
1599            .with_beacon_janitor(bj)
1600            .build();
1601        assert!(mesh.beacon_janitor().is_some());
1602    }
1603
1604    // ── Gap 1: Topology manager ────────────────────────────────────
1605
1606    #[test]
1607    fn test_topology_manager_initially_none() {
1608        let mesh = PeatMesh::new(MeshConfig::default());
1609        assert!(mesh.topology_manager().is_none());
1610    }
1611
1612    #[test]
1613    fn test_set_topology_manager() {
1614        use crate::beacon::{BeaconObserver, GeoPosition, HierarchyLevel};
1615        use crate::topology::{TopologyBuilder, TopologyConfig, TopologyManager};
1616
1617        let mut mesh = PeatMesh::new(MeshConfig::default());
1618        let observer = Arc::new(BeaconObserver::new(mock_storage(), "s00000".to_string()));
1619        let builder = TopologyBuilder::new(
1620            TopologyConfig::default(),
1621            "topo-node".to_string(),
1622            GeoPosition {
1623                lat: 0.0,
1624                lon: 0.0,
1625                alt: None,
1626            },
1627            HierarchyLevel::Squad,
1628            None,
1629            observer,
1630        );
1631        let transport: Arc<dyn MeshTransport> = Arc::new(MockTransport::empty());
1632        let tm = TopologyManager::new(builder, transport);
1633        mesh.set_topology_manager(tm);
1634        assert!(mesh.topology_manager().is_some());
1635    }
1636
1637    #[test]
1638    fn test_builder_with_topology_manager() {
1639        use crate::beacon::{BeaconObserver, GeoPosition, HierarchyLevel};
1640        use crate::topology::{TopologyBuilder, TopologyConfig, TopologyManager};
1641
1642        let observer = Arc::new(BeaconObserver::new(mock_storage(), "s00000".to_string()));
1643        let builder = TopologyBuilder::new(
1644            TopologyConfig::default(),
1645            "topo-builder-node".to_string(),
1646            GeoPosition {
1647                lat: 0.0,
1648                lon: 0.0,
1649                alt: None,
1650            },
1651            HierarchyLevel::Squad,
1652            None,
1653            observer,
1654        );
1655        let transport: Arc<dyn MeshTransport> = Arc::new(MockTransport::empty());
1656        let tm = TopologyManager::new(builder, transport);
1657        let mesh = PeatMeshBuilder::new(MeshConfig::default())
1658            .with_topology_manager(tm)
1659            .build();
1660        assert!(mesh.topology_manager().is_some());
1661    }
1662
1663    // ── Cancellation token ──────────────────────────────────────────
1664
1665    #[test]
1666    fn test_child_token_not_cancelled_while_running() {
1667        let mesh = PeatMesh::new(MeshConfig::default());
1668        mesh.start().unwrap();
1669        let token = mesh.child_token();
1670        assert!(!token.is_cancelled());
1671    }
1672
1673    #[test]
1674    fn test_child_token_cancelled_after_stop() {
1675        let mesh = PeatMesh::new(MeshConfig::default());
1676        mesh.start().unwrap();
1677        let token = mesh.child_token();
1678        mesh.stop().unwrap();
1679        assert!(token.is_cancelled());
1680    }
1681
1682    #[test]
1683    fn test_child_token_reset_on_restart() {
1684        let mesh = PeatMesh::new(MeshConfig::default());
1685        mesh.start().unwrap();
1686        let first_token = mesh.child_token();
1687        mesh.stop().unwrap();
1688        assert!(first_token.is_cancelled());
1689
1690        // Re-start: new token should be fresh (uncancelled)
1691        mesh.start().unwrap();
1692        let second_token = mesh.child_token();
1693        assert!(!second_token.is_cancelled());
1694
1695        // First token remains cancelled (it belonged to the old lifecycle)
1696        assert!(first_token.is_cancelled());
1697    }
1698}
1699
1700// ─── Broker feature tests ────────────────────────────────────────────────────
1701
1702#[cfg(all(test, feature = "broker"))]
1703mod broker_tests {
1704    use super::*;
1705    use crate::broker::state::MeshBrokerState;
1706    use crate::config::MeshConfig;
1707
1708    #[test]
1709    fn test_broker_node_info() {
1710        let mesh = PeatMesh::new(MeshConfig {
1711            node_id: Some("broker-node".to_string()),
1712            ..Default::default()
1713        });
1714        let info = mesh.node_info();
1715        assert_eq!(info.node_id, "broker-node");
1716        assert_eq!(info.uptime_secs, 0);
1717        assert!(!info.version.is_empty());
1718    }
1719
1720    #[test]
1721    fn test_broker_node_info_with_uptime() {
1722        let mesh = PeatMesh::new(MeshConfig {
1723            node_id: Some("uptime-node".to_string()),
1724            ..Default::default()
1725        });
1726        mesh.start().unwrap();
1727        let info = mesh.node_info();
1728        assert_eq!(info.node_id, "uptime-node");
1729        // uptime_secs might be 0 on a fast machine, that's OK
1730    }
1731
1732    #[tokio::test]
1733    async fn test_broker_list_peers_no_transport() {
1734        let mesh = PeatMesh::new(MeshConfig::default());
1735        let peers = mesh.list_peers().await;
1736        assert!(peers.is_empty());
1737    }
1738
1739    #[tokio::test]
1740    async fn test_broker_get_peer_no_transport() {
1741        let mesh = PeatMesh::new(MeshConfig::default());
1742        let peer = mesh.get_peer("unknown").await;
1743        assert!(peer.is_none());
1744    }
1745
1746    #[test]
1747    fn test_broker_topology() {
1748        let mesh = PeatMesh::new(MeshConfig::default());
1749        let topo = mesh.topology();
1750        assert_eq!(topo.peer_count, 0);
1751        assert_eq!(topo.role, "standalone");
1752        assert_eq!(topo.hierarchy_level, 0);
1753    }
1754
1755    #[test]
1756    fn test_broker_subscribe_events() {
1757        let mesh = PeatMesh::new(MeshConfig::default());
1758        let _rx = MeshBrokerState::subscribe_events(&mesh);
1759        // Receiver is valid (won't panic)
1760    }
1761
1762    #[test]
1763    fn test_broker_event_bridge() {
1764        use crate::broker::state::MeshEvent;
1765
1766        let mesh = PeatMesh::new(MeshConfig::default());
1767        let mut rx = MeshBrokerState::subscribe_events(&mesh);
1768
1769        // Emit a broker event via the public API
1770        mesh.emit_mesh_event(MeshEvent::PeerConnected {
1771            peer_id: "test-peer".into(),
1772        });
1773
1774        // Receiver should get it
1775        let event = rx.try_recv().unwrap();
1776        assert!(matches!(
1777            event,
1778            MeshEvent::PeerConnected { ref peer_id } if peer_id == "test-peer"
1779        ));
1780    }
1781
1782    #[test]
1783    fn test_broker_event_bridge_start_emits_topology() {
1784        use crate::broker::state::MeshEvent;
1785
1786        let mesh = PeatMesh::new(MeshConfig::default());
1787        let mut rx = MeshBrokerState::subscribe_events(&mesh);
1788
1789        mesh.start().unwrap();
1790
1791        let event = rx.try_recv().unwrap();
1792        assert!(matches!(
1793            event,
1794            MeshEvent::TopologyChanged { ref new_role, peer_count: 0 } if new_role == "standalone"
1795        ));
1796    }
1797
1798    #[test]
1799    fn test_broker_event_bridge_stop_emits_topology() {
1800        use crate::broker::state::MeshEvent;
1801
1802        let mesh = PeatMesh::new(MeshConfig::default());
1803        mesh.start().unwrap();
1804
1805        let mut rx = MeshBrokerState::subscribe_events(&mesh);
1806        mesh.stop().unwrap();
1807
1808        let event = rx.try_recv().unwrap();
1809        assert!(matches!(
1810            event,
1811            MeshEvent::TopologyChanged { ref new_role, peer_count: 0 } if new_role == "stopped"
1812        ));
1813    }
1814}