1use std::sync::{Arc, Mutex};
8
9use dashmap::DashMap;
10use tracing::{debug, warn};
11
12use crate::a2a::A2ARouter;
13use crate::error::{KernelError, KernelResult};
14use crate::ipc::{KernelMessage, MessagePayload, MessageTarget};
15use crate::mesh_assess::AssessmentTransport;
16use crate::mesh_chain::{ChainSyncRequest, ChainSyncResponse};
17use crate::mesh_discovery::{MeshPeerEvent, MeshPeerEventBus};
18use crate::mesh_heartbeat::{ClockSource, HeartbeatConfig, HeartbeatTracker, MeshClockSync};
19use crate::mesh_delivery::{LocalDelivery, PeerCtx};
20use crate::mesh_ipc::MeshIpcEnvelope;
21use crate::mesh_kad::KademliaTable;
22
23pub const FACTS_TOPIC: &str = "mesh.node_facts";
27
28pub const REVOKE_TOPIC: &str = "mesh.artifact.revoke";
31
32pub const COG_BINDING_TOPIC: &str = "mesh.cog.binding";
35
36pub const COG_GRANT_TOPIC: &str = "mesh.cog.grant";
39
40pub const COG_SYNC_TOPIC: &str = "mesh.cog.sync";
43
44pub const INFER_TOPIC: &str = "mesh.infer";
48
49pub const PING_TOPIC: &str = "mesh.ping";
52
53const CONTROL_TOPICS: [&str; 7] = [
55 FACTS_TOPIC,
56 REVOKE_TOPIC,
57 COG_BINDING_TOPIC,
58 COG_GRANT_TOPIC,
59 COG_SYNC_TOPIC,
60 INFER_TOPIC,
61 PING_TOPIC,
62];
63
64pub trait PeerControlSink: Send + Sync + 'static {
70 fn on_peer_control(
76 &self,
77 ctx: &PeerCtx,
78 conn: u64,
79 payload: &serde_json::Value,
80 ) -> Vec<serde_json::Value>;
81}
82
83pub struct PeerConnection {
86 pub node_id: String,
88 pub connected_at: chrono::DateTime<chrono::Utc>,
90 pub sender: tokio::sync::mpsc::Sender<Vec<u8>>,
92 pub verified: bool,
96 pub class: crate::mesh_admit::PeerClass,
99 _tally: Option<RouteGuard>,
103}
104
105#[derive(Debug, Clone)]
107pub struct PeerDetail {
108 pub node_id: String,
110 pub class: crate::mesh_admit::PeerClass,
112 pub verified: bool,
114 pub licensed: bool,
116 pub connected_at: chrono::DateTime<chrono::Utc>,
118 pub heartbeat: Option<crate::mesh_heartbeat::HeartbeatState>,
120 pub last_seen: Option<std::time::SystemTime>,
123 pub rtt_ms: Option<f64>,
125 pub missed_pongs: Option<u32>,
127 pub load: Option<crate::mesh_load::LoadSample>,
129}
130
131#[derive(Clone, Default)]
135pub struct RouteTally(Arc<std::sync::atomic::AtomicUsize>);
136
137impl RouteTally {
138 pub fn live(&self) -> usize {
140 self.0.load(std::sync::atomic::Ordering::Acquire)
141 }
142}
143
144struct RouteGuard(Arc<std::sync::atomic::AtomicUsize>);
146
147impl RouteGuard {
148 fn new(t: &RouteTally) -> Self {
149 t.0.fetch_add(1, std::sync::atomic::Ordering::AcqRel);
150 Self(Arc::clone(&t.0))
151 }
152}
153
154impl Drop for RouteGuard {
155 fn drop(&mut self) {
156 self.0.fetch_sub(1, std::sync::atomic::Ordering::AcqRel);
157 }
158}
159
160pub struct DiscoveryState {
162 pub kademlia: Mutex<KademliaTable>,
164 pub peer_addresses: DashMap<String, String>,
166 pub heartbeat: Mutex<HeartbeatTracker>,
168}
169
170pub struct MeshAuthentication {
173 pub gate: Arc<dyn crate::mesh_admit::AdmissionGate>,
174 pub identity: Arc<crate::mesh_admit::DialIdentity>,
175 pub noise_static: [u8; 32],
176 pub require_authenticated_seeds: bool,
177}
178
179pub struct MeshRuntime {
187 leaf_ingress: std::sync::OnceLock<Arc<crate::mesh_leaf::LeafIngress>>,
189 authentication: std::sync::OnceLock<MeshAuthentication>,
190 node_id: String,
192 peers: DashMap<String, PeerConnection>,
194 local_router: Option<Arc<dyn LocalDelivery>>,
196 discovery: Option<DiscoveryState>,
198 clock: std::sync::Mutex<MeshClockSync>,
200 #[cfg(feature = "exochain")]
205 chain_manager: std::sync::OnceLock<Arc<crate::chain::ChainManager>>,
206 mesh_subscriptions: DashMap<String, Vec<String>>,
212 assessment_transport: std::sync::OnceLock<Arc<AssessmentTransport>>,
216 peer_events: MeshPeerEventBus,
221 control_sinks: DashMap<String, Arc<dyn PeerControlSink>>,
223 conn_ids: Mutex<Vec<(tokio::sync::mpsc::WeakSender<Vec<u8>>, u64)>>,
225 conn_seq: std::sync::atomic::AtomicU64,
226 enforcing: std::sync::atomic::AtomicBool,
232 liveness: std::sync::OnceLock<Arc<crate::mesh_liveness::Liveness>>,
234}
235
236impl MeshRuntime {
237 pub fn set_leaf_ingress(&self, ingress: Arc<crate::mesh_leaf::LeafIngress>) -> Result<(), Arc<crate::mesh_leaf::LeafIngress>> {
239 self.leaf_ingress.set(ingress)
240 }
241
242 pub fn leaf_ingress(&self) -> Option<&Arc<crate::mesh_leaf::LeafIngress>> {
243 self.leaf_ingress.get()
244 }
245 pub fn set_authentication(&self, auth: MeshAuthentication) -> bool {
246 self.authentication.set(auth).is_ok()
247 }
248
249 pub fn authentication(&self) -> Option<&MeshAuthentication> {
250 self.authentication.get()
251 }
252
253 pub fn start_liveness(self: &Arc<Self>, cfg: crate::mesh_liveness::LivenessConfig) {
255 let lv = crate::mesh_liveness::Liveness::new(self, cfg);
256 if self.liveness.set(lv.clone()).is_ok() {
257 lv.start();
258 }
259 }
260
261 pub fn liveness(&self) -> Option<&Arc<crate::mesh_liveness::Liveness>> {
263 self.liveness.get()
264 }
265
266 pub fn set_enforcing(&self, enforcing: bool) {
269 self.enforcing.store(enforcing, std::sync::atomic::Ordering::SeqCst);
270 }
271
272 pub fn enforcing(&self) -> bool {
274 self.enforcing.load(std::sync::atomic::Ordering::SeqCst)
275 }
276
277 fn counts_as_member(&self, verified: bool) -> bool {
280 verified || !self.enforcing()
281 }
282
283 pub fn new(node_id: String) -> Self {
285 Self {
286 leaf_ingress: std::sync::OnceLock::new(),
287 authentication: std::sync::OnceLock::new(),
288 node_id,
289 peers: DashMap::new(),
290 local_router: None,
291 discovery: None,
292 clock: std::sync::Mutex::new(MeshClockSync::new(ClockSource::Local)),
293 #[cfg(feature = "exochain")]
294 chain_manager: std::sync::OnceLock::new(),
295 mesh_subscriptions: DashMap::new(),
296 assessment_transport: std::sync::OnceLock::new(),
297 peer_events: MeshPeerEventBus::new(),
298 control_sinks: DashMap::new(),
299 conn_ids: Mutex::new(Vec::new()),
300 conn_seq: std::sync::atomic::AtomicU64::new(0),
301 enforcing: std::sync::atomic::AtomicBool::new(false),
302 liveness: std::sync::OnceLock::new(),
303 }
304 }
305
306 pub fn with_discovery(node_id: String, kademlia_id: [u8; 32]) -> Self {
311 Self {
312 leaf_ingress: std::sync::OnceLock::new(),
313 authentication: std::sync::OnceLock::new(),
314 node_id,
315 peers: DashMap::new(),
316 local_router: None,
317 discovery: Some(DiscoveryState {
318 kademlia: Mutex::new(KademliaTable::new(kademlia_id)),
319 peer_addresses: DashMap::new(),
320 heartbeat: Mutex::new(HeartbeatTracker::new(HeartbeatConfig::default())),
321 }),
322 clock: std::sync::Mutex::new(MeshClockSync::new(ClockSource::Local)),
323 #[cfg(feature = "exochain")]
324 chain_manager: std::sync::OnceLock::new(),
325 mesh_subscriptions: DashMap::new(),
326 assessment_transport: std::sync::OnceLock::new(),
327 peer_events: MeshPeerEventBus::new(),
328 control_sinks: DashMap::new(),
329 conn_ids: Mutex::new(Vec::new()),
330 conn_seq: std::sync::atomic::AtomicU64::new(0),
331 enforcing: std::sync::atomic::AtomicBool::new(false),
332 liveness: std::sync::OnceLock::new(),
333 }
334 }
335
336 fn conn_id(&self, outbound: &tokio::sync::mpsc::Sender<Vec<u8>>) -> u64 {
338 let mut ids = self.conn_ids.lock().unwrap_or_else(|p| p.into_inner());
339 ids.retain(|(w, _)| w.upgrade().is_some());
340 if let Some((_, id)) = ids
341 .iter()
342 .find(|(w, _)| w.upgrade().is_some_and(|t| t.same_channel(outbound)))
343 {
344 return *id;
345 }
346 let id = self.conn_seq.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1;
347 ids.push((outbound.downgrade(), id));
348 id
349 }
350
351 pub fn set_control_sink(&self, topic: &str, sink: Arc<dyn PeerControlSink>) {
354 self.control_sinks.entry(topic.to_string()).or_insert(sink);
355 }
356
357 pub fn subscribe_peer_events(&self) -> tokio::sync::broadcast::Receiver<MeshPeerEvent> {
359 self.peer_events.subscribe()
360 }
361
362 pub fn peer_event_bus(&self) -> &MeshPeerEventBus {
364 &self.peer_events
365 }
366
367 pub fn emit_peer_event(&self, event: MeshPeerEvent) {
369 self.peer_events.emit(event);
370 }
371
372 pub fn set_assessment_transport(&self, transport: Arc<AssessmentTransport>) {
377 let _ = self.assessment_transport.set(transport);
378 }
379
380 pub fn assessment_transport(&self) -> Option<Arc<AssessmentTransport>> {
382 self.assessment_transport.get().cloned()
383 }
384
385 pub async fn broadcast_raw(&self, data: &[u8]) -> usize {
390 let mut sent = 0usize;
391 for entry in self.peers.iter() {
392 let peer_id = entry.key().clone();
393 let sender = entry.sender.clone();
394 match sender.send(data.to_vec()).await {
395 Ok(()) => sent += 1,
396 Err(_) => {
397 warn!(peer = %peer_id, "assessment broadcast: peer send channel closed");
398 }
399 }
400 }
401 sent
402 }
403
404 pub async fn push_pending_assessment(&self) -> usize {
409 let Some(transport) = self.assessment_transport.get() else {
410 return 0;
411 };
412 let Some(bytes) = transport.drain_pending() else {
413 return 0;
414 };
415 self.broadcast_raw(&bytes).await
416 }
417
418 pub async fn assessment_gossip_tick(&self) -> usize {
420 let Some(transport) = self.assessment_transport.get() else {
421 return 0;
422 };
423 let report = transport.latest_report();
424 let Some(bytes) = transport.gossip_tick(report.as_ref()) else {
425 return 0;
426 };
427 self.broadcast_raw(&bytes).await
428 }
429
430 pub fn node_id(&self) -> &str {
432 &self.node_id
433 }
434
435 pub fn set_local_router(&mut self, router: Arc<A2ARouter>) {
437 self.local_router = Some(router);
438 }
439
440 pub fn set_local_delivery(&mut self, delivery: Arc<dyn LocalDelivery>) {
443 self.local_router = Some(delivery);
444 }
445
446 pub fn register_peer_topic(&self, topic: &str, peer_node_id: &str) {
454 let mut entry = self
455 .mesh_subscriptions
456 .entry(topic.to_string())
457 .or_default();
458 if !entry.contains(&peer_node_id.to_string()) {
459 entry.push(peer_node_id.to_string());
460 debug!(topic, peer = %peer_node_id, "registered peer topic subscription");
461 }
462 }
463
464 pub fn unregister_peer_topic(&self, topic: &str, peer_node_id: &str) {
468 if let Some(mut subs) = self.mesh_subscriptions.get_mut(topic) {
469 subs.retain(|id| id != peer_node_id);
470 }
471 }
472
473 pub fn unregister_all_peer_topics(&self, peer_node_id: &str) {
478 for mut entry in self.mesh_subscriptions.iter_mut() {
479 entry.retain(|id| id != peer_node_id);
480 }
481 }
482
483 pub fn peers_for_topic(&self, topic: &str) -> Vec<String> {
488 self.mesh_subscriptions
489 .get(topic)
490 .map(|subs| subs.clone())
491 .unwrap_or_default()
492 }
493
494 #[cfg(feature = "exochain")]
501 pub fn set_chain_manager(&self, cm: Arc<crate::chain::ChainManager>) {
502 let _ = self.chain_manager.set(cm);
503 }
504
505 pub fn add_peer(&self, node_id: String, sender: tokio::sync::mpsc::Sender<Vec<u8>>) {
512 self.register_peer(node_id, sender, false, None);
513 }
514
515 pub fn route_is_foreign(&self, node_id: &str, tx: &tokio::sync::mpsc::Sender<Vec<u8>>) -> bool {
517 self.peers.get(node_id).is_some_and(|p| !p.sender.same_channel(tx))
518 }
519
520 pub fn add_peer_tallied(
523 &self,
524 node_id: String,
525 sender: tokio::sync::mpsc::Sender<Vec<u8>>,
526 tally: &RouteTally,
527 ) {
528 self.register_peer(node_id, sender, false, Some(tally));
529 }
530
531 #[cfg(test)]
539 pub fn register_authenticated(
540 &self,
541 node_id: String,
542 sender: tokio::sync::mpsc::Sender<Vec<u8>>,
543 verified: bool,
544 tally: &RouteTally,
545 ) -> bool {
546 self.register_authenticated_as(node_id, sender, verified, crate::mesh_admit::PeerClass::Node, tally)
547 }
548
549 pub fn register_authenticated_as(
553 &self,
554 node_id: String,
555 sender: tokio::sync::mpsc::Sender<Vec<u8>>,
556 verified: bool,
557 class: crate::mesh_admit::PeerClass,
558 tally: &RouteTally,
559 ) -> bool {
560 self.register_peer_classed(node_id, sender, verified, class, Some(tally))
561 }
562
563 fn register_peer(
570 &self,
571 node_id: String,
572 sender: tokio::sync::mpsc::Sender<Vec<u8>>,
573 verified: bool,
574 tally: Option<&RouteTally>,
575 ) -> bool {
576 let class = if verified {
577 crate::mesh_admit::PeerClass::Node
578 } else {
579 crate::mesh_admit::PeerClass::Legacy
580 };
581 self.register_peer_classed(node_id, sender, verified, class, tally)
582 }
583
584 fn register_peer_classed(
585 &self,
586 node_id: String,
587 sender: tokio::sync::mpsc::Sender<Vec<u8>>,
588 verified: bool,
589 class: crate::mesh_admit::PeerClass,
590 tally: Option<&RouteTally>,
591 ) -> bool {
592 use dashmap::mapref::entry::Entry;
593 debug!(peer = %node_id, "adding peer connection");
594 let address = self
595 .discovery
596 .as_ref()
597 .and_then(|d| d.peer_addresses.get(&node_id).map(|a| a.value().clone()));
598 let conn = PeerConnection {
599 node_id: node_id.clone(),
600 connected_at: chrono::Utc::now(),
601 sender,
602 verified,
603 class,
604 _tally: tally.map(RouteGuard::new),
605 };
606 let was_known = match self.peers.entry(node_id.clone()) {
609 Entry::Occupied(mut o) => {
610 if o.get().sender.same_channel(&conn.sender) {
611 return true;
612 }
613 if o.get().verified && !verified {
614 warn!(peer = %node_id, "refusing route takeover of an admitted peer by an unverified connection");
615 return false;
616 }
617 if !verified {
618 warn!(peer = %node_id,
623 "unverified connection replaced an unverified route; the previous connection will be closed");
624 }
625 o.insert(conn);
626 true
627 }
628 Entry::Vacant(v) => {
629 v.insert(conn);
630 false
631 }
632 };
633 if was_known {
635 self.peer_events.emit(MeshPeerEvent::Recovered {
636 node_id,
637 address,
638 verified: self.counts_as_member(verified),
639 });
640 } else {
641 self.peer_events.emit(MeshPeerEvent::Joined {
642 node_id,
643 address,
644 platform: None,
645 verified: self.counts_as_member(verified),
646 });
647 }
648 true
649 }
650
651 pub fn routes_via(&self, tx: &tokio::sync::mpsc::Sender<Vec<u8>>) -> bool {
658 self.peers.iter().any(|e| e.sender.same_channel(tx))
659 }
660
661 pub fn disconnect_channel(&self, tx: &tokio::sync::mpsc::Sender<Vec<u8>>) -> usize {
662 let ids: Vec<String> = self
663 .peers
664 .iter()
665 .filter(|e| e.sender.same_channel(tx))
666 .map(|e| e.key().clone())
667 .collect();
668 let mut n = 0;
669 for id in ids {
670 if let Some((_, conn)) = self.peers.remove_if(&id, |_, p| p.sender.same_channel(tx)) {
671 if conn.verified {
676 self.unregister_all_peer_topics(&id);
677 }
678 self.peer_events.emit(MeshPeerEvent::Left { node_id: id });
679 n += 1;
680 }
681 }
682 n
683 }
684
685 pub async fn send_to_peer(&self, node_id: &str, envelope: MeshIpcEnvelope) -> KernelResult<()> {
691 let peer = self
692 .peers
693 .get(node_id)
694 .ok_or_else(|| KernelError::Mesh(format!("peer not connected: {node_id}")))?;
695
696 let bytes = envelope
697 .to_bytes()
698 .map_err(|e| KernelError::Mesh(format!("serialization error: {e}")))?;
699
700 peer.sender
701 .send(bytes)
702 .await
703 .map_err(|_| KernelError::Mesh(format!("send channel closed for peer {node_id}")))?;
704
705 debug!(peer = %node_id, "sent envelope to peer");
706 Ok(())
707 }
708
709 pub async fn route_to_remote(&self, node_id: &str, message: KernelMessage) -> KernelResult<()> {
714 let envelope = MeshIpcEnvelope::new(self.node_id.clone(), node_id.to_string(), message);
715 self.send_to_peer(node_id, envelope).await
716 }
717
718 pub async fn handle_incoming(&self, data: &[u8]) -> KernelResult<()> {
727 if self.try_handle_assessment(data, None).await? {
728 return Ok(());
729 }
730 let envelope = MeshIpcEnvelope::from_bytes(data)
731 .map_err(|e| KernelError::Mesh(format!("deserialization error: {e}")))?;
732 let ctx = PeerCtx::unauthenticated(envelope.source_node.clone());
733 self.handle_envelope(envelope, &ctx, 0).await
734 }
735
736 pub async fn handle_incoming_from(
749 &self,
750 data: &[u8],
751 outbound: tokio::sync::mpsc::Sender<Vec<u8>>,
752 ) -> KernelResult<()> {
753 self.handle_incoming_peer(data, outbound, None).await
754 }
755
756 pub async fn handle_incoming_peer(
767 &self,
768 data: &[u8],
769 outbound: tokio::sync::mpsc::Sender<Vec<u8>>,
770 peer: Option<&PeerCtx>,
771 ) -> KernelResult<()> {
772 self.handle_incoming_tallied(data, outbound, peer, None).await
773 }
774
775 pub async fn handle_incoming_tallied(
779 &self,
780 data: &[u8],
781 outbound: tokio::sync::mpsc::Sender<Vec<u8>>,
782 peer: Option<&PeerCtx>,
783 tally: Option<&RouteTally>,
784 ) -> KernelResult<()> {
785 if self
791 .try_handle_assessment(data, Some(outbound.clone()))
792 .await?
793 {
794 return Ok(());
795 }
796
797 let envelope = MeshIpcEnvelope::from_bytes(data)
798 .map_err(|e| KernelError::Mesh(format!("deserialization error: {e}")))?;
799
800 let mut ctx = match peer {
801 Some(c) => c.clone(),
802 None => PeerCtx::unauthenticated(envelope.source_node.clone()),
803 };
804 if ctx.node_verified && envelope.source_node != ctx.peer_id {
805 return Err(KernelError::Mesh(format!(
806 "source_node {} does not match admitted node {}",
807 envelope.source_node, ctx.peer_id
808 )));
809 }
810 if !ctx.node_verified {
811 ctx.peer_id = envelope.source_node.clone();
813 if !self.peers.contains_key(&ctx.peer_id) {
814 warn!(peer = %ctx.peer_id,
815 "registering peer under an unauthenticated source_node claim");
816 }
817 }
818 ctx.src_scope = envelope.src_scope.clone();
819
820 let conn = self.conn_id(&outbound);
827 let class = if ctx.node_verified { ctx.class } else { crate::mesh_admit::PeerClass::Legacy };
830 if !self.register_peer_classed(ctx.peer_id.clone(), outbound, ctx.node_verified, class, tally) {
831 return Err(KernelError::Mesh(format!(
832 "route for {} belongs to an admitted peer",
833 ctx.peer_id
834 )));
835 }
836
837 self.handle_envelope(envelope, &ctx, conn).await
838 }
839
840 async fn try_handle_assessment(
845 &self,
846 data: &[u8],
847 reply_tx: Option<tokio::sync::mpsc::Sender<Vec<u8>>>,
848 ) -> KernelResult<bool> {
849 let Some(transport) = self.assessment_transport.get() else {
850 return Ok(false);
851 };
852 match transport.try_handle_raw(data)? {
853 Some(maybe_reply) => {
854 if let Some(reply) = maybe_reply {
855 if let Some(tx) = reply_tx {
856 let _ = tx.send(reply).await;
857 } else if let Some(source) = AssessmentTransport::try_extract_payload(data)
858 .ok()
859 .flatten()
860 .and_then(|p| {
861 crate::mesh_assess::AssessmentEnvelope::from_bytes(&p)
862 .ok()
863 .map(|e| e.source_node)
864 })
865 {
866 if let Some(peer) = self.peers.get(&source) {
868 let _ = peer.sender.send(reply).await;
869 }
870 }
871 }
872 Ok(true)
873 }
874 None => Ok(false),
875 }
876 }
877
878 async fn handle_envelope(
879 &self,
880 envelope: MeshIpcEnvelope,
881 ctx: &PeerCtx,
882 conn: u64,
883 ) -> KernelResult<()> {
884 debug!(
885 from_node = %envelope.source_node,
886 dest_node = %envelope.dest_node,
887 "received mesh envelope"
888 );
889
890 #[cfg(feature = "exochain")]
896 if let Some(cm) = self.chain_manager.get() {
897 let topic = match &envelope.message.target {
898 MessageTarget::Topic(t) => Some(t.clone()),
899 MessageTarget::Service(s) => Some(format!("service:{s}")),
900 MessageTarget::ServiceMethod { service, method } => {
901 Some(format!("service:{service}#{method}"))
902 }
903 MessageTarget::RemoteNode { target, .. } => match target.as_ref() {
904 MessageTarget::Topic(t) => Some(t.clone()),
905 _ => None,
906 },
907 _ => None,
908 };
909 let payload = serde_json::json!({
910 "source_node": envelope.source_node,
911 "dest_node": envelope.dest_node,
912 "envelope_id": envelope.envelope_id,
913 "topic": topic,
914 "hop_count": envelope.hop_count,
915 });
916 let _ = cm.append("mesh", "peer.envelope", Some(payload));
917 }
918
919 let dest_scope = envelope.dest_scope;
922 let mut message = envelope.message;
923 if let MessageTarget::RemoteNode { target, .. } = message.target {
924 message.target = *target;
925 }
926
927 if let MessageTarget::Topic(ref ctrl_topic) = message.target
938 && ctrl_topic == "mesh.subscribe"
939 {
940 if let MessagePayload::Json(ref payload) = message.payload
941 && let Some(topic) = payload.get("topic").and_then(|v| v.as_str())
942 {
943 if let Some(router) = self.local_router.as_ref()
946 && !router
947 .authorize_subscribe(ctx, topic, dest_scope.as_ref())
948 .await
949 {
950 warn!(from = %ctx.peer_id, topic, "mesh.subscribe refused by local delivery");
951 if ctx.node_verified && ctx.class == crate::mesh_admit::PeerClass::Leaf {
952 return Err(KernelError::Mesh("certified leaf subscription scope refused".into()));
953 }
954 return Ok(());
955 }
956 self.register_peer_topic(topic, &ctx.peer_id);
957 return Ok(());
958 }
959 warn!(
962 from = %envelope.source_node,
963 "received malformed mesh.subscribe envelope (missing 'topic' field)"
964 );
965 return Ok(());
966 }
967
968 if let MessageTarget::Topic(ref t) = message.target
971 && CONTROL_TOPICS.contains(&t.as_str())
972 {
973 let sink = self.control_sinks.get(t.as_str()).map(|s| s.clone());
974 if let (Some(sink), MessagePayload::Json(payload)) = (sink, &message.payload) {
975 for reply in sink.on_peer_control(ctx, conn, payload) {
976 let msg = KernelMessage::new(
977 0,
978 MessageTarget::Topic(t.clone()),
979 MessagePayload::Json(reply),
980 );
981 if let Err(e) = self.route_to_remote(&ctx.peer_id, msg).await {
982 warn!(peer = %ctx.peer_id, topic = %t, error = %e, "control reply failed");
983 }
984 }
985 }
986 return Ok(());
987 }
988
989 let router = self
991 .local_router
992 .as_ref()
993 .ok_or_else(|| KernelError::Mesh("no local router attached to mesh runtime".into()))?;
994 router.deliver(ctx, dest_scope.as_ref(), message).await
995 }
996
997 pub fn peer_route(&self, node_id: &str) -> Option<(u64, bool)> {
1001 let p = self.peers.get(node_id)?;
1002 Some((self.conn_id(&p.sender), p.verified))
1003 }
1004
1005 pub fn peer_count(&self) -> usize {
1007 self.peers.len()
1008 }
1009
1010 pub fn peer_verified(&self, node_id: &str) -> bool {
1013 self.peers.get(node_id).is_some_and(|p| p.verified)
1014 }
1015
1016 pub fn peer_licensed(&self, node_id: &str) -> bool {
1020 self.peers
1021 .get(node_id)
1022 .is_some_and(|p| p.verified && p.class == crate::mesh_admit::PeerClass::Node)
1023 }
1024
1025 pub fn peer_ids(&self) -> Vec<String> {
1027 self.peers.iter().map(|entry| entry.key().clone()).collect()
1028 }
1029
1030 pub fn peer_details(&self) -> Vec<PeerDetail> {
1036 let mut out: Vec<PeerDetail> = self
1037 .peers
1038 .iter()
1039 .map(|e| {
1040 let p = e.value();
1041 PeerDetail {
1042 node_id: e.key().clone(),
1043 class: p.class,
1044 verified: p.verified,
1045 licensed: p.verified && p.class == crate::mesh_admit::PeerClass::Node,
1046 connected_at: p.connected_at,
1047 heartbeat: self
1048 .discovery
1049 .as_ref()
1050 .and_then(|d| d.heartbeat.lock().ok()?.peer_state(e.key())),
1051 last_seen: None,
1052 rtt_ms: None,
1053 missed_pongs: None,
1054 load: None,
1055 }
1056 })
1057 .collect();
1058 out.sort_by(|a, b| a.node_id.cmp(&b.node_id));
1059 if let Some(lv) = self.liveness.get() {
1060 for d in &mut out {
1061 if let Some(l) = lv.peer(&d.node_id) {
1062 d.last_seen = Some(l.last_seen);
1063 d.rtt_ms = Some(l.rtt_ms);
1064 d.missed_pongs = Some(l.missed);
1065 d.load = l.load;
1066 }
1067 }
1068 }
1069 out
1070 }
1071
1072 pub fn disconnect_peer(&self, node_id: &str) {
1077 if self.peers.remove(node_id).is_some() {
1078 self.unregister_all_peer_topics(node_id);
1079 debug!(peer = %node_id, "disconnected peer");
1080 self.peer_events.emit(MeshPeerEvent::Left {
1081 node_id: node_id.to_owned(),
1082 });
1083 } else {
1084 warn!(peer = %node_id, "disconnect_peer: peer not found");
1085 }
1086 }
1087
1088 pub fn disconnect_all_peers(&self) -> usize {
1092 let ids: Vec<String> = self.peer_ids();
1093 let n = ids.len();
1094 for id in &ids {
1095 self.disconnect_peer(id);
1096 }
1097 n
1098 }
1099
1100 pub fn register_peer_address(&self, node_id: &str, addr: &str) {
1107 if let Some(ref disc) = self.discovery {
1108 disc.peer_addresses
1109 .insert(node_id.to_string(), addr.to_string());
1110 }
1111 }
1112
1113 pub fn discover_peers(&self) -> Vec<(String, String)> {
1118 match self.discovery.as_ref() {
1119 Some(disc) => disc
1120 .peer_addresses
1121 .iter()
1122 .map(|entry| (entry.key().clone(), entry.value().clone()))
1123 .collect(),
1124 None => Vec::new(),
1125 }
1126 }
1127
1128 pub fn record_heartbeat(&self, node_id: &str) {
1133 if let Some(ref disc) = self.discovery {
1134 let mut hb = disc.heartbeat.lock().unwrap();
1135 let prior = hb.peer_state(node_id);
1136 if prior.is_none() {
1137 hb.add_peer(node_id.to_string());
1138 }
1139 hb.record_alive(node_id);
1140 use crate::mesh_heartbeat::HeartbeatState;
1142 match prior {
1143 Some(HeartbeatState::Suspect) | Some(HeartbeatState::Dead) => {
1144 drop(hb);
1145 self.peer_events.emit(MeshPeerEvent::Recovered {
1146 node_id: node_id.to_owned(),
1147 address: disc
1148 .peer_addresses
1149 .get(node_id)
1150 .map(|a| a.value().clone()),
1151 verified: self.counts_as_member(
1152 self.peers.get(node_id).is_some_and(|p| p.verified),
1153 ),
1154 });
1155 }
1156 _ => {
1157 drop(hb);
1158 self.peer_events.emit(MeshPeerEvent::Alive {
1159 node_id: node_id.to_owned(),
1160 });
1161 }
1162 }
1163 }
1164 }
1165
1166 pub fn mesh_time_us(&self) -> u64 {
1173 self.clock.lock().unwrap().mesh_time_us()
1174 }
1175
1176 pub fn clock_uncertainty_us(&self) -> u64 {
1178 self.clock.lock().unwrap().uncertainty_us
1179 }
1180
1181 pub fn clock_source(&self) -> ClockSource {
1183 self.clock.lock().unwrap().local_source
1184 }
1185
1186 pub fn set_clock_source(&self, source: ClockSource) {
1188 self.clock.lock().unwrap().local_source = source;
1189 }
1190
1191 pub fn sync_clock_from_peer(&self, peer_id: &str, peer_time_us: u64, peer_source: ClockSource) {
1193 let local_time = crate::mesh_heartbeat::system_time_us();
1194 self.clock
1195 .lock()
1196 .unwrap()
1197 .process_sync(peer_id, peer_time_us, peer_source, local_time);
1198 }
1199
1200 pub fn is_time_authority(&self) -> bool {
1202 self.clock.lock().unwrap().is_authority(&self.node_id)
1203 }
1204
1205 pub fn check_peer_health(&self) -> Vec<String> {
1207 match self.discovery.as_ref() {
1208 Some(disc) => {
1209 let hb = disc.heartbeat.lock().unwrap();
1210 let mut unhealthy: Vec<String> = hb
1211 .suspect_peers()
1212 .into_iter()
1213 .map(|s| s.to_string())
1214 .collect();
1215 unhealthy.extend(hb.dead_peers().into_iter().map(|s| s.to_string()));
1216 unhealthy
1217 }
1218 None => Vec::new(),
1219 }
1220 }
1221
1222 pub fn remove_dead_peers(&self) {
1228 if let Some(ref disc) = self.discovery {
1229 let dead: Vec<String> = {
1230 let hb = disc.heartbeat.lock().unwrap();
1231 hb.dead_peers().into_iter().map(|s| s.to_string()).collect()
1232 };
1233 for node_id in &dead {
1234 self.peer_events.emit(MeshPeerEvent::Unreachable {
1235 node_id: node_id.clone(),
1236 });
1237 if self.peers.remove(node_id.as_str()).is_some() {
1240 self.unregister_all_peer_topics(node_id);
1241 debug!(peer = %node_id, "removed dead peer (unreachable)");
1242 }
1243 disc.peer_addresses.remove(node_id.as_str());
1244 }
1245 }
1246 }
1247
1248 pub fn discovery_mut(&mut self) -> Option<&mut DiscoveryState> {
1250 self.discovery.as_mut()
1251 }
1252
1253 pub fn discovery(&self) -> Option<&DiscoveryState> {
1255 self.discovery.as_ref()
1256 }
1257
1258 pub fn build_chain_sync_request(&self, from_seq: u64) -> Vec<u8> {
1266 let (after_hash, chain_id) = self.chain_tip_meta(from_seq);
1267 let req = ChainSyncRequest {
1268 chain_id,
1269 after_sequence: from_seq,
1270 after_hash,
1271 max_events: 256,
1272 };
1273 serde_json::to_vec(&req).unwrap_or_default()
1274 }
1275
1276 #[cfg(feature = "exochain")]
1280 pub fn build_chain_sync_response(
1281 &self,
1282 after_sequence: u64,
1283 max_events: u32,
1284 ) -> Option<ChainSyncResponse> {
1285 let cm = self.chain_manager.get()?;
1286 let mut events = cm.tail_from(after_sequence);
1287 let has_more = events.len() > max_events as usize;
1288 if has_more {
1289 events.truncate(max_events as usize);
1290 }
1291 let json_events: Vec<serde_json::Value> = events
1292 .iter()
1293 .filter_map(|e| serde_json::to_value(e).ok())
1294 .collect();
1295 Some(ChainSyncResponse {
1296 chain_id: cm.chain_id(),
1297 events: json_events,
1298 has_more,
1299 tip_sequence: cm.head_sequence(),
1300 tip_hash: cm
1301 .head_hash()
1302 .iter()
1303 .map(|b| format!("{b:02x}"))
1304 .collect(),
1305 })
1306 }
1307
1308 pub fn handle_chain_sync_response(&self, data: &[u8]) -> KernelResult<usize> {
1321 self.handle_chain_sync_outcome(data).map(|o| o.applied)
1322 }
1323
1324 pub fn handle_chain_sync_outcome(&self, data: &[u8]) -> KernelResult<ChainSyncOutcome> {
1330 let resp: ChainSyncResponse = serde_json::from_slice(data)
1331 .map_err(|e| KernelError::Mesh(format!("chain sync deserialize error: {e}")))?;
1332
1333 #[cfg(feature = "exochain")]
1334 {
1335 if let Some(cm) = self.chain_manager.get() {
1336 let mut applied = 0usize;
1337 let mut stopped = None;
1338 for value in &resp.events {
1339 let event: crate::chain::ChainEvent = serde_json::from_value(value.clone())
1340 .map_err(|e| {
1341 KernelError::Mesh(format!("chain sync event decode: {e}"))
1342 })?;
1343 let seq = event.sequence;
1344 match cm.append_signed(event) {
1345 Ok(_) => applied += 1,
1346 Err(crate::chain::AppendSignedError::AlreadyPresent { .. }) => {}
1347 Err(crate::chain::AppendSignedError::ReservedSource { event_source }) => {
1348 warn!(seq, source = %event_source, "chain sync stopped at an authority event");
1349 stopped = Some(StoppedAtAuthorityEvent { sequence: seq, source: event_source });
1350 break;
1351 }
1352 Err(e) => {
1353 return Err(KernelError::Mesh(format!(
1354 "chain sync append_signed: {e}"
1355 )));
1356 }
1357 }
1358 }
1359 debug!(
1360 applied,
1361 batch = resp.events.len(),
1362 tip = resp.tip_sequence,
1363 "chain sync response applied"
1364 );
1365 return Ok(ChainSyncOutcome { applied, stopped });
1366 }
1367 }
1368
1369 Ok(ChainSyncOutcome { applied: resp.events.len(), stopped: None })
1370 }
1371
1372 #[cfg(feature = "exochain")]
1373 fn chain_tip_meta(&self, from_seq: u64) -> (String, u32) {
1374 if let Some(cm) = self.chain_manager.get() {
1375 let hash = if from_seq == 0 && cm.head_sequence() == 0 {
1376 cm.head_hash()
1377 } else {
1378 cm.tail_from(from_seq.saturating_sub(1))
1379 .first()
1380 .map(|e| e.prev_hash)
1381 .unwrap_or_else(|| cm.head_hash())
1382 };
1383 let hash = cm
1385 .tail(0)
1386 .into_iter()
1387 .find(|e| e.sequence == from_seq)
1388 .map(|e| e.hash)
1389 .unwrap_or(hash);
1390 let hex: String = hash.iter().map(|b| format!("{b:02x}")).collect();
1391 (hex, cm.chain_id())
1392 } else {
1393 (String::new(), 0)
1394 }
1395 }
1396
1397 #[cfg(not(feature = "exochain"))]
1398 fn chain_tip_meta(&self, _from_seq: u64) -> (String, u32) {
1399 (String::new(), 0)
1400 }
1401}
1402
1403#[derive(Debug, Clone, PartialEq, Eq)]
1405pub struct StoppedAtAuthorityEvent {
1406 pub sequence: u64,
1408 pub source: String,
1410}
1411
1412#[derive(Debug, Clone, PartialEq, Eq)]
1414pub struct ChainSyncOutcome {
1415 pub applied: usize,
1417 pub stopped: Option<StoppedAtAuthorityEvent>,
1419}
1420
1421#[cfg(test)]
1424mod tests {
1425 use super::*;
1426 use crate::capability::{AgentCapabilities, CapabilityChecker};
1427 use crate::ipc::{KernelMessage, MessagePayload, MessageTarget};
1428 use crate::mesh_heartbeat::HeartbeatState;
1429 use crate::mesh_ipc::MeshIpcEnvelope;
1430 use crate::process::{ProcessEntry, ProcessState, ProcessTable, ResourceUsage};
1431 use crate::topic::TopicRouter;
1432 use tokio_util::sync::CancellationToken;
1433
1434 fn make_router_with_process() -> (
1437 Arc<A2ARouter>,
1438 crate::process::Pid,
1439 tokio::sync::mpsc::Receiver<KernelMessage>,
1440 ) {
1441 let table = Arc::new(ProcessTable::new(64));
1442 let entry = ProcessEntry {
1443 pid: 0,
1444 agent_id: "test-agent".into(),
1445 state: ProcessState::Running,
1446 capabilities: AgentCapabilities::default(),
1447 resource_usage: ResourceUsage::default(),
1448 cancel_token: CancellationToken::new(),
1449 parent_pid: None,
1450 };
1451 let pid = table.insert(entry).unwrap();
1452 let checker = Arc::new(CapabilityChecker::new(table.clone()));
1453 let topics = Arc::new(TopicRouter::new(table.clone()));
1454 let router = Arc::new(A2ARouter::new(table, checker, topics));
1455 let rx = router.create_inbox(pid);
1456 (router, pid, rx)
1457 }
1458
1459 #[test]
1462 fn new_runtime_has_no_peers() {
1463 let rt = MeshRuntime::new("node-local".into());
1464 assert_eq!(rt.peer_count(), 0);
1465 assert!(rt.peer_ids().is_empty());
1466 assert_eq!(rt.node_id(), "node-local");
1467 }
1468
1469 #[test]
1472 fn add_peer_increases_count() {
1473 let rt = MeshRuntime::new("local".into());
1474 let (tx, _rx) = tokio::sync::mpsc::channel(16);
1475 rt.add_peer("peer-1".into(), tx);
1476 assert_eq!(rt.peer_count(), 1);
1477 assert_eq!(rt.peer_ids(), vec!["peer-1".to_string()]);
1478 }
1479
1480 #[tokio::test]
1483 async fn add_peer_emits_joined_event() {
1484 use crate::mesh_discovery::MeshPeerEvent;
1485 let rt = MeshRuntime::new("local".into());
1486 let mut rx = rt.subscribe_peer_events();
1487 let (tx, _rx) = tokio::sync::mpsc::channel(16);
1488 rt.add_peer("peer-j".into(), tx);
1489 let ev = rx.recv().await.unwrap();
1490 match ev {
1491 MeshPeerEvent::Joined { node_id, .. } => assert_eq!(node_id, "peer-j"),
1492 other => panic!("expected Joined, got {other:?}"),
1493 }
1494 }
1495
1496 #[tokio::test]
1497 async fn disconnect_peer_emits_left_event() {
1498 use crate::mesh_discovery::MeshPeerEvent;
1499 let rt = MeshRuntime::new("local".into());
1500 let mut rx = rt.subscribe_peer_events();
1501 let (tx, _rx) = tokio::sync::mpsc::channel(16);
1502 rt.add_peer("peer-l".into(), tx);
1503 let _ = rx.recv().await.unwrap(); rt.disconnect_peer("peer-l");
1505 let ev = rx.recv().await.unwrap();
1506 assert_eq!(
1507 ev,
1508 MeshPeerEvent::Left {
1509 node_id: "peer-l".into()
1510 }
1511 );
1512 }
1513
1514 #[tokio::test]
1515 async fn re_add_peer_emits_recovered() {
1516 use crate::mesh_discovery::MeshPeerEvent;
1517 let rt = MeshRuntime::new("local".into());
1518 let mut rx = rt.subscribe_peer_events();
1519 let (tx1, _rx1) = tokio::sync::mpsc::channel(16);
1520 rt.add_peer("peer-r".into(), tx1);
1521 let _ = rx.recv().await.unwrap();
1522 let (tx2, _rx2) = tokio::sync::mpsc::channel(16);
1525 rt.add_peer("peer-r".into(), tx2);
1526 let ev = rx.recv().await.unwrap();
1527 match ev {
1528 MeshPeerEvent::Recovered { node_id, .. } => assert_eq!(node_id, "peer-r"),
1529 other => panic!("expected Recovered, got {other:?}"),
1530 }
1531 }
1532
1533 #[tokio::test]
1534 async fn remove_dead_peers_emits_unreachable() {
1535 use crate::mesh_discovery::MeshPeerEvent;
1536 use crate::mesh_heartbeat::{HeartbeatConfig, HeartbeatState};
1537 use std::time::Duration;
1538
1539 let kad = [0u8; 32];
1540 let mut rt = MeshRuntime::with_discovery("local".into(), kad);
1541 let mut rx = rt.subscribe_peer_events();
1542 let (tx, _rx) = tokio::sync::mpsc::channel(16);
1543 rt.add_peer("dying".into(), tx);
1544 let _ = rx.recv().await.unwrap(); {
1548 let disc = rt.discovery_mut().unwrap();
1549 *disc.heartbeat.lock().unwrap() =
1550 crate::mesh_heartbeat::HeartbeatTracker::new(HeartbeatConfig {
1551 suspect_timeout: Duration::ZERO,
1552 ..Default::default()
1553 });
1554 let mut hb = disc.heartbeat.lock().unwrap();
1555 hb.add_peer("dying".into());
1556 hb.record_miss("dying"); hb.record_miss("dying"); assert_eq!(hb.peer_state("dying"), Some(HeartbeatState::Dead));
1559 }
1560
1561 rt.remove_dead_peers();
1562 let ev = rx.recv().await.unwrap();
1563 assert_eq!(
1564 ev,
1565 MeshPeerEvent::Unreachable {
1566 node_id: "dying".into()
1567 }
1568 );
1569 assert_eq!(rt.peer_count(), 0);
1570 }
1571
1572 #[tokio::test]
1573 async fn mesh_runtime_events_drive_cluster_membership() {
1574 use crate::cluster::{ClusterConfig, ClusterMembership, NodeState};
1575 use crate::mesh_discovery::MeshPeerEvent;
1576 use std::sync::Arc;
1577
1578 let membership = Arc::new(
1579 ClusterMembership::new(ClusterConfig::default())
1580 .with_min_peer_interval(std::time::Duration::ZERO),
1581 );
1582 let rt = MeshRuntime::new("local".into());
1583 rt.set_enforcing(true);
1585 membership.spawn_mesh_peer_listener(rt.subscribe_peer_events());
1586
1587 let (utx, _urx) = tokio::sync::mpsc::channel(16);
1590 rt.add_peer("legacy-peer".into(), utx);
1591 let (tx, _rx) = tokio::sync::mpsc::channel(16);
1592 assert!(rt.register_authenticated("mesh-peer".into(), tx, true, &RouteTally::default()));
1593 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1594 assert_eq!(
1595 membership.get_peer("legacy-peer").unwrap().state,
1596 NodeState::Unverified
1597 );
1598 assert_eq!(
1599 membership.get_peer("mesh-peer").unwrap().state,
1600 NodeState::Active
1601 );
1602
1603 rt.emit_peer_event(MeshPeerEvent::Unreachable {
1604 node_id: "mesh-peer".into(),
1605 });
1606 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1607 assert_eq!(
1608 membership.get_peer("mesh-peer").unwrap().state,
1609 NodeState::Unreachable
1610 );
1611
1612 let (tx2, _rx2) = tokio::sync::mpsc::channel(16);
1614 assert!(rt.register_authenticated("mesh-peer".into(), tx2, true, &RouteTally::default()));
1616 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1617 assert_eq!(
1618 membership.get_peer("mesh-peer").unwrap().state,
1619 NodeState::Active
1620 );
1621
1622 rt.disconnect_peer("mesh-peer");
1623 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1624 assert!(membership.get_peer("mesh-peer").is_none());
1625 }
1626
1627 #[tokio::test]
1630 async fn send_to_peer_delivers_serialized_bytes() {
1631 let rt = MeshRuntime::new("local".into());
1632 let (tx, mut rx) = tokio::sync::mpsc::channel(16);
1633 rt.add_peer("peer-1".into(), tx);
1634
1635 let msg = KernelMessage::text(0, MessageTarget::Broadcast, "hello");
1636 let envelope = MeshIpcEnvelope::new("local".into(), "peer-1".into(), msg);
1637 let expected_id = envelope.envelope_id.clone();
1638
1639 rt.send_to_peer("peer-1", envelope).await.unwrap();
1640
1641 let received = rx.recv().await.unwrap();
1642 let decoded = MeshIpcEnvelope::from_bytes(&received).unwrap();
1643 assert_eq!(decoded.envelope_id, expected_id);
1644 assert_eq!(decoded.source_node, "local");
1645 assert_eq!(decoded.dest_node, "peer-1");
1646 }
1647
1648 #[tokio::test]
1651 async fn handle_incoming_injects_into_local_router() {
1652 let (router, pid, mut inbox_rx) = make_router_with_process();
1653
1654 let mut rt = MeshRuntime::new("node-b".into());
1655 rt.set_local_router(router);
1656
1657 let inner_target = MessageTarget::Process(pid);
1659 let remote_target = MessageTarget::RemoteNode {
1660 node_id: "node-b".into(),
1661 target: Box::new(inner_target),
1662 };
1663 let msg = KernelMessage::text(pid, remote_target, "from-remote");
1664 let envelope = MeshIpcEnvelope::new("node-a".into(), "node-b".into(), msg);
1665 let bytes = envelope.to_bytes().unwrap();
1666
1667 rt.handle_incoming(&bytes).await.unwrap();
1668
1669 let delivered = inbox_rx.try_recv().unwrap();
1670 assert!(matches!(delivered.target, MessageTarget::Process(p) if p == pid));
1671 match &delivered.payload {
1672 MessagePayload::Text(s) => assert_eq!(s, "from-remote"),
1673 other => panic!("expected Text payload, got: {other:?}"),
1674 }
1675 }
1676
1677 #[test]
1680 fn disconnect_peer_removes_it() {
1681 let rt = MeshRuntime::new("local".into());
1682 let (tx, _rx) = tokio::sync::mpsc::channel(16);
1683 rt.add_peer("peer-1".into(), tx);
1684 assert_eq!(rt.peer_count(), 1);
1685
1686 rt.disconnect_peer("peer-1");
1687 assert_eq!(rt.peer_count(), 0);
1688 }
1689
1690 #[tokio::test]
1693 async fn round_trip_send_receive() {
1694 let (router, pid, mut inbox_rx) = make_router_with_process();
1695
1696 let rt_a = MeshRuntime::new("node-a".into());
1698 let (tx, mut peer_rx) = tokio::sync::mpsc::channel(16);
1699 rt_a.add_peer("node-b".into(), tx);
1700
1701 let msg = KernelMessage::text(
1703 pid,
1704 MessageTarget::RemoteNode {
1705 node_id: "node-b".into(),
1706 target: Box::new(MessageTarget::Process(pid)),
1707 },
1708 "round-trip",
1709 );
1710 rt_a.route_to_remote("node-b", msg).await.unwrap();
1711
1712 let wire_bytes = peer_rx.recv().await.unwrap();
1714 let mut rt_b = MeshRuntime::new("node-b".into());
1715 rt_b.set_local_router(router);
1716 rt_b.handle_incoming(&wire_bytes).await.unwrap();
1717
1718 let delivered = inbox_rx.try_recv().unwrap();
1719 match &delivered.payload {
1720 MessagePayload::Text(s) => assert_eq!(s, "round-trip"),
1721 other => panic!("expected Text payload, got: {other:?}"),
1722 }
1723 }
1724
1725 #[test]
1728 fn peer_connection_stores_node_id_and_timestamp() {
1729 let rt = MeshRuntime::new("local".into());
1730 let (tx, _rx) = tokio::sync::mpsc::channel(16);
1731 rt.add_peer("remote-42".into(), tx);
1732
1733 let entry = rt.peers.get("remote-42").unwrap();
1734 assert_eq!(entry.node_id, "remote-42");
1735 let age = chrono::Utc::now() - entry.connected_at;
1737 assert!(age.num_seconds() < 2);
1738 }
1739
1740 #[test]
1743 fn multiple_peers_connected() {
1744 let rt = MeshRuntime::new("local".into());
1745 for i in 0..5 {
1746 let (tx, _rx) = tokio::sync::mpsc::channel(16);
1747 rt.add_peer(format!("peer-{i}"), tx);
1748 }
1749 assert_eq!(rt.peer_count(), 5);
1750
1751 let mut ids = rt.peer_ids();
1752 ids.sort();
1753 assert_eq!(ids, vec!["peer-0", "peer-1", "peer-2", "peer-3", "peer-4"]);
1754 }
1755
1756 #[tokio::test]
1759 async fn send_to_unknown_peer_errors() {
1760 let rt = MeshRuntime::new("local".into());
1761 let msg = KernelMessage::text(0, MessageTarget::Broadcast, "hi");
1762 let envelope = MeshIpcEnvelope::new("local".into(), "ghost".into(), msg);
1763
1764 let err = rt.send_to_peer("ghost", envelope).await.unwrap_err();
1765 let msg = err.to_string();
1766 assert!(msg.contains("peer not connected"), "got: {msg}");
1767 }
1768
1769 #[tokio::test]
1772 async fn handle_incoming_malformed_data_errors() {
1773 let mut rt = MeshRuntime::new("local".into());
1774 let (router, _pid, _rx) = make_router_with_process();
1775 rt.set_local_router(router);
1776
1777 let err = rt.handle_incoming(b"not valid json").await.unwrap_err();
1778 let msg = err.to_string();
1779 assert!(msg.contains("deserialization"), "got: {msg}");
1780 }
1781
1782 #[tokio::test]
1785 async fn handle_incoming_without_router_errors() {
1786 let rt = MeshRuntime::new("local".into());
1787 let msg = KernelMessage::text(0, MessageTarget::Broadcast, "hi");
1788 let envelope = MeshIpcEnvelope::new("a".into(), "local".into(), msg);
1789 let bytes = envelope.to_bytes().unwrap();
1790
1791 let err = rt.handle_incoming(&bytes).await.unwrap_err();
1792 let msg = err.to_string();
1793 assert!(msg.contains("no local router"), "got: {msg}");
1794 }
1795
1796 #[test]
1799 fn disconnect_nonexistent_peer_is_noop() {
1800 let rt = MeshRuntime::new("local".into());
1801 rt.disconnect_peer("ghost"); assert_eq!(rt.peer_count(), 0);
1803 }
1804
1805 #[tokio::test]
1808 async fn two_nodes_exchange_messages() {
1809 let (router_b, pid_b, mut inbox_b) = make_router_with_process();
1811
1812 let rt_a = MeshRuntime::new("node-a".into());
1813 let mut rt_b = MeshRuntime::new("node-b".into());
1814 rt_b.set_local_router(router_b);
1815
1816 let (a_to_b_tx, mut a_to_b_rx) = tokio::sync::mpsc::channel::<Vec<u8>>(64);
1818 let (b_to_a_tx, mut b_to_a_rx) = tokio::sync::mpsc::channel::<Vec<u8>>(64);
1819
1820 rt_a.add_peer("node-b".into(), a_to_b_tx);
1822 rt_b.add_peer("node-a".into(), b_to_a_tx);
1823
1824 let msg_a = KernelMessage::text(
1826 pid_b,
1827 MessageTarget::RemoteNode {
1828 node_id: "node-b".into(),
1829 target: Box::new(MessageTarget::Process(pid_b)),
1830 },
1831 "hello-from-a",
1832 );
1833 rt_a.route_to_remote("node-b", msg_a).await.unwrap();
1834
1835 let wire_bytes = a_to_b_rx.recv().await.unwrap();
1837 rt_b.handle_incoming(&wire_bytes).await.unwrap();
1838
1839 let delivered = inbox_b.try_recv().unwrap();
1841 match &delivered.payload {
1842 MessagePayload::Text(s) => assert_eq!(s, "hello-from-a"),
1843 other => panic!("expected Text, got: {other:?}"),
1844 }
1845
1846 let msg_b = KernelMessage::text(0, MessageTarget::Broadcast, "reply-from-b");
1848 rt_b.route_to_remote("node-a", msg_b).await.unwrap();
1849
1850 let reply_bytes = b_to_a_rx.recv().await.unwrap();
1851 let reply_env = MeshIpcEnvelope::from_bytes(&reply_bytes).unwrap();
1852 assert_eq!(reply_env.source_node, "node-b");
1853 assert_eq!(reply_env.dest_node, "node-a");
1854 }
1855
1856 #[test]
1859 fn discover_peers_returns_registered_addresses() {
1860 let rt = MeshRuntime::with_discovery("node-x".into(), [0u8; 32]);
1861 rt.register_peer_address("peer-1", "10.0.0.1:9489");
1862 rt.register_peer_address("peer-2", "10.0.0.2:9489");
1863 rt.register_peer_address("peer-3", "10.0.0.3:9489");
1864
1865 let mut peers = rt.discover_peers();
1866 peers.sort_by(|a, b| a.0.cmp(&b.0));
1867 assert_eq!(peers.len(), 3);
1868 assert_eq!(peers[0], ("peer-1".into(), "10.0.0.1:9489".into()));
1869 assert_eq!(peers[1], ("peer-2".into(), "10.0.0.2:9489".into()));
1870 assert_eq!(peers[2], ("peer-3".into(), "10.0.0.3:9489".into()));
1871 }
1872
1873 #[test]
1874 fn peer_details_report_class_verified_licensed_and_heartbeat() {
1875 let rt = MeshRuntime::with_discovery("local".into(), [0u8; 32]);
1876 let tally = RouteTally::default();
1877 let (tx, _rx) = tokio::sync::mpsc::channel(1);
1878 let (tx2, _rx2) = tokio::sync::mpsc::channel(1);
1879 rt.register_authenticated_as("b-leaf".into(), tx, true, crate::mesh_admit::PeerClass::Leaf, &tally);
1880 rt.register_authenticated_as("a-node".into(), tx2, true, crate::mesh_admit::PeerClass::Node, &tally);
1881 rt.record_heartbeat("a-node");
1882 let d = rt.peer_details();
1883 assert_eq!(d.iter().map(|p| p.node_id.as_str()).collect::<Vec<_>>(), ["a-node", "b-leaf"]);
1884 assert!(d[0].licensed && d[0].verified);
1885 assert_eq!(d[0].heartbeat, Some(crate::mesh_heartbeat::HeartbeatState::Alive));
1886 assert!(!d[1].licensed && d[1].verified && d[1].class.as_str() == "leaf");
1887 assert_eq!(d[1].heartbeat, None);
1888 }
1889
1890 #[test]
1893 fn heartbeat_tracking_detects_dead_peer() {
1894 let mut rt = MeshRuntime::with_discovery("local".into(), [0u8; 32]);
1895
1896 {
1898 let disc = rt.discovery_mut().unwrap();
1899 let mut hb = disc.heartbeat.lock().unwrap();
1900 *hb = HeartbeatTracker::new(crate::mesh_heartbeat::HeartbeatConfig {
1901 suspect_timeout: std::time::Duration::from_secs(0),
1902 ..crate::mesh_heartbeat::HeartbeatConfig::default()
1903 });
1904 hb.add_peer("healthy".into());
1905 hb.add_peer("dying".into());
1906 }
1907
1908 rt.record_heartbeat("healthy");
1910
1911 {
1913 let disc = rt.discovery_mut().unwrap();
1914 let mut hb = disc.heartbeat.lock().unwrap();
1915 hb.record_miss("dying");
1916 hb.record_miss("dying");
1917 }
1918
1919 let unhealthy = rt.check_peer_health();
1920 assert!(unhealthy.contains(&"dying".to_string()));
1921 assert!(!unhealthy.contains(&"healthy".to_string()));
1922 }
1923
1924 #[test]
1927 fn remove_dead_peers_cleans_connections() {
1928 let mut rt = MeshRuntime::with_discovery("local".into(), [0u8; 32]);
1929
1930 let (tx, _rx) = tokio::sync::mpsc::channel(16);
1932 rt.add_peer("dead-peer".into(), tx);
1933 rt.register_peer_address("dead-peer", "10.0.0.99:9489");
1934
1935 {
1937 let disc = rt.discovery_mut().unwrap();
1938 let mut hb = disc.heartbeat.lock().unwrap();
1939 *hb = HeartbeatTracker::new(crate::mesh_heartbeat::HeartbeatConfig {
1940 suspect_timeout: std::time::Duration::from_secs(0),
1941 ..crate::mesh_heartbeat::HeartbeatConfig::default()
1942 });
1943 hb.add_peer("dead-peer".into());
1944 hb.record_miss("dead-peer");
1945 hb.record_miss("dead-peer");
1946 assert_eq!(hb.peer_state("dead-peer"), Some(HeartbeatState::Dead));
1947 }
1948
1949 assert_eq!(rt.peer_count(), 1);
1950 rt.remove_dead_peers();
1951 assert_eq!(rt.peer_count(), 0);
1952 assert!(rt.discover_peers().is_empty());
1953 }
1954
1955 #[test]
1958 fn kademlia_routing_finds_closest_peers() {
1959 let mut rt = MeshRuntime::with_discovery("local".into(), [0u8; 32]);
1960
1961 let disc = rt.discovery_mut().unwrap();
1962 let mut kad = disc.kademlia.lock().unwrap();
1963 for i in 1..=5u8 {
1965 let mut peer_key = [0u8; 32];
1966 peer_key[0] = i;
1967 kad.add_peer(
1968 peer_key,
1969 crate::mesh_kad::DhtEntry {
1970 key: format!("peer-{i}"),
1971 node_id: format!("peer-{i}"),
1972 address: format!("10.0.0.{i}:9489"),
1973 platform: "linux".into(),
1974 last_seen: 1000,
1975 governance_genesis_prefix: "0000000000000000".into(),
1976 },
1977 );
1978 }
1979
1980 let closest = kad.find_closest(&[0u8; 32], 3);
1982 assert_eq!(closest.len(), 3);
1983 }
1987
1988 #[test]
1991 fn with_discovery_initializes_state() {
1992 let rt = MeshRuntime::with_discovery("disc-node".into(), [0xAB; 32]);
1993 assert_eq!(rt.node_id(), "disc-node");
1994 assert!(rt.discovery().is_some());
1995
1996 let disc = rt.discovery().unwrap();
1997 assert_eq!(*disc.kademlia.lock().unwrap().local_key(), [0xAB; 32]);
1998 assert_eq!(disc.heartbeat.lock().unwrap().peer_count(), 0);
1999 }
2000
2001 #[test]
2004 fn discover_peers_empty_without_discovery() {
2005 let rt = MeshRuntime::new("plain-node".into());
2006 assert!(rt.discover_peers().is_empty());
2007 rt.register_peer_address("x", "y");
2009 rt.record_heartbeat("x");
2010 assert!(rt.check_peer_health().is_empty());
2011 }
2012
2013 #[test]
2016 fn chain_sync_request_round_trip() {
2017 let rt = MeshRuntime::new("sync-node".into());
2018 let bytes = rt.build_chain_sync_request(42);
2019
2020 let req: crate::mesh_chain::ChainSyncRequest = serde_json::from_slice(&bytes).unwrap();
2021 assert_eq!(req.chain_id, 0);
2022 assert_eq!(req.after_sequence, 42);
2023 assert_eq!(req.max_events, 256);
2024 }
2025
2026 #[test]
2029 fn chain_sync_response_handling() {
2030 let rt = MeshRuntime::new("sync-node".into());
2031
2032 let resp = crate::mesh_chain::ChainSyncResponse {
2033 chain_id: 0,
2034 events: vec![
2035 serde_json::json!({"type": "write"}),
2036 serde_json::json!({"type": "delete"}),
2037 ],
2038 has_more: false,
2039 tip_sequence: 100,
2040 tip_hash: "abc".into(),
2041 };
2042 let data = serde_json::to_vec(&resp).unwrap();
2043
2044 let count = rt.handle_chain_sync_response(&data).unwrap();
2046 assert_eq!(count, 2);
2047 }
2048
2049 #[test]
2052 fn chain_sync_response_malformed_errors() {
2053 let rt = MeshRuntime::new("sync-node".into());
2054 let err = rt.handle_chain_sync_response(b"bad json").unwrap_err();
2055 assert!(err.to_string().contains("chain sync deserialize"));
2056 }
2057
2058 #[cfg(feature = "exochain")]
2063 #[test]
2064 fn two_node_chain_sync_convergence() {
2065 use crate::chain::ChainManager;
2066 use std::sync::Arc;
2067
2068 let source_cm = Arc::new(ChainManager::new(0, 1000));
2069 source_cm.append("mesh", "peer.join", Some(serde_json::json!({"id": "a"})));
2070 source_cm.append("mesh", "peer.join", Some(serde_json::json!({"id": "b"})));
2071 source_cm.append("kernel", "boot", None);
2072
2073 let genesis = source_cm.tail(0)[0].clone();
2075 let sink_cm = Arc::new(ChainManager::from_events(vec![genesis], 1000).unwrap());
2076 assert!(sink_cm.head_sequence() < source_cm.head_sequence());
2077
2078 let source_rt = MeshRuntime::new("source".into());
2079 source_rt.set_chain_manager(source_cm.clone());
2080 let sink_rt = MeshRuntime::new("sink".into());
2081 sink_rt.set_chain_manager(sink_cm.clone());
2082
2083 let after = sink_cm.head_sequence();
2084 let resp = source_rt
2085 .build_chain_sync_response(after, 256)
2086 .expect("source has chain");
2087 assert!(!resp.events.is_empty());
2088 let data = serde_json::to_vec(&resp).unwrap();
2089
2090 let applied = sink_rt.handle_chain_sync_response(&data).unwrap();
2091 assert_eq!(applied, 3);
2092 assert_eq!(sink_cm.head_sequence(), source_cm.head_sequence());
2093 assert_eq!(sink_cm.head_hash(), source_cm.head_hash());
2094
2095 let resp2 = source_rt
2097 .build_chain_sync_response(sink_cm.head_sequence(), 256)
2098 .unwrap();
2099 assert!(resp2.events.is_empty());
2100 }
2101
2102 #[cfg(feature = "exochain")]
2105 #[test]
2106 fn chain_sync_stops_at_the_first_authority_event() {
2107 use crate::chain::ChainManager;
2108 use std::sync::Arc;
2109
2110 let src = Arc::new(ChainManager::new(0, 1000));
2111 src.append("mesh", "peer.join", None);
2112 let forged = src.append("auth.token", "auth.token.issued", Some(serde_json::json!({})));
2113 src.append("mesh", "peer.join", None);
2114 let genesis = src.tail(0)[0].clone();
2115 let sink = Arc::new(ChainManager::from_events(vec![genesis], 1000).unwrap());
2116 let src_rt = MeshRuntime::new("s".into());
2117 src_rt.set_chain_manager(src.clone());
2118 let sink_rt = MeshRuntime::new("k".into());
2119 sink_rt.set_chain_manager(sink.clone());
2120 let resp = src_rt.build_chain_sync_response(sink.head_sequence(), 256).unwrap();
2121 let data = serde_json::to_vec(&resp).unwrap();
2122 let out = sink_rt.handle_chain_sync_outcome(&data).unwrap();
2123 assert_eq!(out.applied, 1);
2124 assert_eq!(
2125 out.stopped,
2126 Some(StoppedAtAuthorityEvent { sequence: forged.sequence, source: "auth.token".into() })
2127 );
2128 assert!(sink.tail(0).iter().all(|e| e.source != "auth.token"));
2129 assert_eq!(sink_rt.handle_chain_sync_response(&data).unwrap(), 0);
2130 }
2131
2132 #[tokio::test]
2139 async fn subscribe_then_topic_forwards_to_inbound_peer() {
2140 let (router, pid, _inbox) = make_router_with_process();
2141
2142 let mut rt = MeshRuntime::new("kernel".into());
2145 rt.set_local_router(router.clone());
2146 let rt = Arc::new(rt);
2147 router.set_mesh_runtime(rt.clone());
2148
2149 let (out_tx, mut out_rx) = tokio::sync::mpsc::channel::<Vec<u8>>(16);
2151
2152 let sub_msg = KernelMessage::new(
2154 pid,
2155 MessageTarget::Topic("mesh.subscribe".into()),
2156 MessagePayload::Json(serde_json::json!({"topic": "push.leaf-x"})),
2157 );
2158 let sub_env = MeshIpcEnvelope::new("leaf-x".into(), "kernel".into(), sub_msg);
2159 let sub_bytes = sub_env.to_bytes().unwrap();
2160
2161 rt.handle_incoming_from(&sub_bytes, out_tx.clone())
2162 .await
2163 .unwrap();
2164
2165 assert!(
2168 rt.peer_ids().contains(&"leaf-x".to_string()),
2169 "inbound peer should be auto-registered by source_node"
2170 );
2171 assert_eq!(
2172 rt.peers_for_topic("push.leaf-x"),
2173 vec!["leaf-x".to_string()]
2174 );
2175
2176 let push = KernelMessage::new(
2179 pid,
2180 MessageTarget::Topic("push.leaf-x".into()),
2181 MessagePayload::Text("hello-leaf".into()),
2182 );
2183 router.send(push).await.unwrap();
2184
2185 let forwarded_bytes =
2187 tokio::time::timeout(std::time::Duration::from_millis(500), out_rx.recv())
2188 .await
2189 .expect("forward should arrive within timeout")
2190 .expect("channel should not be closed");
2191
2192 let forwarded = MeshIpcEnvelope::from_bytes(&forwarded_bytes).unwrap();
2193 assert_eq!(forwarded.source_node, "kernel");
2194 assert_eq!(forwarded.dest_node, "leaf-x");
2195 match &forwarded.message.target {
2196 MessageTarget::Topic(t) => assert_eq!(t, "push.leaf-x"),
2197 other => panic!("expected Topic target, got {other:?}"),
2198 }
2199 match &forwarded.message.payload {
2200 MessagePayload::Text(s) => assert_eq!(s, "hello-leaf"),
2201 other => panic!("expected Text payload, got {other:?}"),
2202 }
2203
2204 rt.disconnect_peer("leaf-x");
2207 assert!(rt.peers_for_topic("push.leaf-x").is_empty());
2208 }
2209
2210 #[tokio::test]
2218 async fn reconnecting_peer_replaces_stale_outbound_channel() {
2219 let rt = MeshRuntime::new("kernel".into());
2220
2221 let sub_msg = KernelMessage::new(
2222 0,
2223 MessageTarget::Topic("mesh.subscribe".into()),
2224 MessagePayload::Json(serde_json::json!({"topic": "push.leaf-y"})),
2225 );
2226 let sub_env = MeshIpcEnvelope::new("leaf-y".into(), "kernel".into(), sub_msg);
2227 let sub_bytes = sub_env.to_bytes().unwrap();
2228
2229 let (old_tx, mut old_rx) = tokio::sync::mpsc::channel::<Vec<u8>>(16);
2231 rt.handle_incoming_from(&sub_bytes, old_tx).await.unwrap();
2232
2233 let (new_tx, mut new_rx) = tokio::sync::mpsc::channel::<Vec<u8>>(16);
2235 rt.handle_incoming_from(&sub_bytes, new_tx).await.unwrap();
2236
2237 let push = KernelMessage::new(
2239 0,
2240 MessageTarget::Topic("push.leaf-y".into()),
2241 MessagePayload::Text("after-reconnect".into()),
2242 );
2243 let env = MeshIpcEnvelope::new("kernel".into(), "leaf-y".into(), push);
2244 rt.send_to_peer("leaf-y", env).await.unwrap();
2245
2246 assert!(
2247 new_rx.try_recv().is_ok(),
2248 "push should be delivered on the reconnected channel"
2249 );
2250 assert!(
2251 old_rx.try_recv().is_err(),
2252 "stale channel from the dropped connection must not receive"
2253 );
2254 }
2255
2256 #[tokio::test]
2262 async fn assessment_transport_demux_and_diff_push_two_nodes() {
2263 use crate::assessment::analyzer::diff_reports;
2264 use crate::assessment::mesh::MeshCoordinator;
2265 use crate::assessment::{AssessmentReport, AssessmentSummary, Finding};
2266 use crate::mesh_assess::AssessmentTransport;
2267 use chrono::Utc;
2268
2269 fn report_with(findings: Vec<Finding>) -> AssessmentReport {
2270 AssessmentReport {
2271 timestamp: Utc::now(),
2272 scope: "full".into(),
2273 project: "/tmp/a".into(),
2274 files_scanned: 10,
2275 summary: AssessmentSummary {
2276 total_files: 10,
2277 coherence_score: 0.9,
2278 ..Default::default()
2279 },
2280 findings,
2281 analyzers_run: vec!["complexity".into()],
2282 }
2283 }
2284
2285 let coord_a = Arc::new(MeshCoordinator::new("node-a".into(), "proj-a".into()));
2286 let coord_b = Arc::new(MeshCoordinator::new("node-b".into(), "proj-b".into()));
2287 let transport_a = Arc::new(AssessmentTransport::new(coord_a.clone()));
2288 let transport_b = Arc::new(AssessmentTransport::new(coord_b.clone()));
2289
2290 let rt_a = Arc::new(MeshRuntime::new("node-a".into()));
2292 let rt_b = Arc::new(MeshRuntime::new("node-b".into()));
2293 rt_a.set_assessment_transport(transport_a.clone());
2294 rt_b.set_assessment_transport(transport_b.clone());
2295
2296 let (a_to_b_tx, mut a_to_b_rx) = tokio::sync::mpsc::channel::<Vec<u8>>(8);
2298 let (b_to_a_tx, mut b_to_a_rx) = tokio::sync::mpsc::channel::<Vec<u8>>(8);
2299 rt_a.add_peer("node-b".into(), a_to_b_tx);
2300 rt_b.add_peer("node-a".into(), b_to_a_tx.clone());
2301
2302 let prev = report_with(vec![Finding {
2303 severity: "warning".into(),
2304 category: "size".into(),
2305 file: "a.rs".into(),
2306 line: Some(1),
2307 message: "old".into(),
2308 }]);
2309 let curr = report_with(vec![
2310 Finding {
2311 severity: "warning".into(),
2312 category: "size".into(),
2313 file: "a.rs".into(),
2314 line: Some(1),
2315 message: "old".into(),
2316 },
2317 Finding {
2318 severity: "warning".into(),
2319 category: "size".into(),
2320 file: "b.rs".into(),
2321 line: Some(2),
2322 message: "new finding".into(),
2323 },
2324 ]);
2325 let diff = diff_reports(&curr, &prev);
2326 assert_eq!(diff.findings_new.len(), 1);
2327
2328 coord_a.set_pending_broadcast(coord_a.build_finding_diff(&curr, &diff));
2330 transport_a.publish_report(curr.clone());
2331 let sent = rt_a.push_pending_assessment().await;
2332 assert_eq!(sent, 1, "diff should reach mock peer B channel");
2333
2334 let frame_bytes = a_to_b_rx.try_recv().expect("B should receive encoded frame");
2335
2336 rt_b.handle_incoming_from(&frame_bytes, b_to_a_tx.clone())
2338 .await
2339 .expect("assessment demux must succeed");
2340
2341 let peers = transport_b.peer_states();
2342 assert_eq!(peers.len(), 1);
2343 assert_eq!(peers[0].node_id, "node-a");
2344 assert_eq!(peers[0].project_name, "proj-a");
2345 assert_eq!(peers[0].finding_count, 2);
2346
2347 let ipc = MeshIpcEnvelope::new(
2350 "node-a".into(),
2351 "node-b".into(),
2352 KernelMessage::new(
2353 0,
2354 MessageTarget::Topic("mesh.subscribe".into()),
2355 MessagePayload::Json(serde_json::json!({"topic": "t1"})),
2356 ),
2357 );
2358 let ipc_bytes = ipc.to_bytes().unwrap();
2359 rt_b.handle_incoming_from(&ipc_bytes, b_to_a_tx)
2360 .await
2361 .unwrap();
2362 assert!(rt_b.peers_for_topic("t1").contains(&"node-a".to_string()));
2363
2364 let _ = b_to_a_rx.try_recv();
2366 }
2367}
2368
2369#[cfg(test)]
2370#[path = "mesh_runtime_bind_tests.rs"]
2371mod bind_tests;