Skip to main content

clawft_kernel/
mesh_runtime.rs

1//! Mesh runtime orchestrator for WeftOS node-to-node communication (K6).
2//!
3//! The [`MeshRuntime`] wires together transport connections, serialization
4//! via [`MeshIpcEnvelope`], and the local [`A2ARouter`] so that a
5//! `RemoteNode` message target actually delivers across the network.
6
7use 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
23/// Control topic carrying signed node facts between peers (card
24/// mesh-placement-03). Consumed by the runtime: handed to the installed
25/// [`PeerControlSink`], never routed to local subscribers.
26pub const FACTS_TOPIC: &str = "mesh.node_facts";
27
28/// Control topic carrying signed artifact revocations (card
29/// mesh-placement-25). Handled like [`FACTS_TOPIC`].
30pub const REVOKE_TOPIC: &str = "mesh.artifact.revoke";
31
32/// Control topic carrying signed Seed bindings (ADR-106). Handled like
33/// [`FACTS_TOPIC`].
34pub const COG_BINDING_TOPIC: &str = "mesh.cog.binding";
35
36/// Control topic carrying signed checkout grants and operator approvals
37/// (ADR-106). Handled like [`FACTS_TOPIC`].
38pub const COG_GRANT_TOPIC: &str = "mesh.cog.grant";
39
40/// Control topic carrying licence catch-up sync requests and responses
41/// (ADR-106 section 5.5). Handled like [`FACTS_TOPIC`].
42pub const COG_SYNC_TOPIC: &str = "mesh.cog.sync";
43
44/// Control topic carrying inference adverts and forwarded requests between
45/// peers (card mesh-placement-19). Handled like [`FACTS_TOPIC`]; see
46/// `infer_proxy::hub`.
47pub const INFER_TOPIC: &str = "mesh.infer";
48
49/// Control topic carrying liveness ping/pong between verified peers
50/// (see [`crate::mesh_liveness`]). Handled like [`FACTS_TOPIC`].
51pub const PING_TOPIC: &str = "mesh.ping";
52
53/// Control topics the runtime consumes instead of routing locally.
54const 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
64/// Receiver of a runtime control topic (one of `CONTROL_TOPICS`).
65///
66/// The sink decides what to trust: it is handed the connection's
67/// authenticated identity ([`PeerCtx`]) and returns payloads to send back
68/// to the same peer (for example a re-send request).
69pub trait PeerControlSink: Send + Sync + 'static {
70    /// Handle one control payload from `ctx.peer_id`; return replies.
71    ///
72    /// `conn` identifies the connection the frame arrived on (0 when the
73    /// caller has none): one connection can claim many source ids while
74    /// unverified, so per-connection limits key on this, not on `ctx`.
75    fn on_peer_control(
76        &self,
77        ctx: &PeerCtx,
78        conn: u64,
79        payload: &serde_json::Value,
80    ) -> Vec<serde_json::Value>;
81}
82
83/// A handle to a connected peer, holding the sender half of an mpsc
84/// channel whose receiver is read by a background write loop.
85pub struct PeerConnection {
86    /// Remote node identifier.
87    pub node_id: String,
88    /// When the connection was established.
89    pub connected_at: chrono::DateTime<chrono::Utc>,
90    /// Sender for outbound serialized messages.
91    pub sender: tokio::sync::mpsc::Sender<Vec<u8>>,
92    /// True when this route was registered by a connection whose node id
93    /// admission verified. A verified route cannot be taken over by an
94    /// unverified connection claiming the same id.
95    pub verified: bool,
96    /// The admitted class of the connection (`Node`, `Leaf`, ...); `Legacy`
97    /// for a route no admission verdict classed.
98    pub class: crate::mesh_admit::PeerClass,
99    /// Live-route accounting for the serving connection (None for routes
100    /// added without one). Dropping the route, by removal or replacement,
101    /// decrements it.
102    _tally: Option<RouteGuard>,
103}
104
105/// What [`MeshRuntime::peer_details`] reports for one connected peer.
106#[derive(Debug, Clone)]
107pub struct PeerDetail {
108    /// Remote node identifier.
109    pub node_id: String,
110    /// Admitted class of the connection.
111    pub class: crate::mesh_admit::PeerClass,
112    /// Admission verified the node id.
113    pub verified: bool,
114    /// Verified and class `node` (see [`MeshRuntime::peer_licensed`]).
115    pub licensed: bool,
116    /// When the connection was established.
117    pub connected_at: chrono::DateTime<chrono::Utc>,
118    /// Heartbeat tracker's view, when discovery is attached and tracks it.
119    pub heartbeat: Option<crate::mesh_heartbeat::HeartbeatState>,
120    /// Last counted liveness pong from this peer (real liveness), when the
121    /// liveness service runs and the peer has answered.
122    pub last_seen: Option<std::time::SystemTime>,
123    /// Smoothed ping round-trip time in milliseconds, same condition.
124    pub rtt_ms: Option<f64>,
125    /// Pings that timed out since the last counted pong.
126    pub missed_pongs: Option<u32>,
127    /// Load the peer attached to its last pong (peer-claimed, unsigned).
128    pub load: Option<crate::mesh_load::LoadSample>,
129}
130
131/// Count of live routes registered by one serving connection. Lets the
132/// connection learn in O(1), instead of scanning the peer map, that its
133/// routes were removed (`disconnect_peer`, revocation) or replaced.
134#[derive(Clone, Default)]
135pub struct RouteTally(Arc<std::sync::atomic::AtomicUsize>);
136
137impl RouteTally {
138    /// Routes currently pointing at this connection.
139    pub fn live(&self) -> usize {
140        self.0.load(std::sync::atomic::Ordering::Acquire)
141    }
142}
143
144/// Held by a `PeerConnection`; decrements its tally when the route drops.
145struct 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
160/// Discovery state for mesh peer lookup and health tracking.
161pub struct DiscoveryState {
162    /// Kademlia routing table for peer lookup.
163    pub kademlia: Mutex<KademliaTable>,
164    /// Known peer addresses: node_id -> socket addr string.
165    pub peer_addresses: DashMap<String, String>,
166    /// Heartbeat tracker for failure detection.
167    pub heartbeat: Mutex<HeartbeatTracker>,
168}
169
170/// Admission material owned by this runtime. Nested seed dials must use this
171/// gate after verifying a reciprocal hello; a configured node-id is not proof.
172pub 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
179/// The mesh runtime orchestrates transport, connections, and message bridging.
180///
181/// It maintains a set of active peer connections (keyed by node ID) and
182/// provides the plumbing to:
183/// 1. Send a [`MeshIpcEnvelope`] to a connected peer.
184/// 2. Receive an envelope from a peer and inject it into the local
185///    [`A2ARouter`].
186pub struct MeshRuntime {
187    /// Installed ADR-103 leaf certificate and replay gate.
188    leaf_ingress: std::sync::OnceLock<Arc<crate::mesh_leaf::LeafIngress>>,
189    authentication: std::sync::OnceLock<MeshAuthentication>,
190    /// Local node identifier.
191    node_id: String,
192    /// Active peer connections: node_id -> PeerConnection.
193    peers: DashMap<String, PeerConnection>,
194    /// Reference to the local A2A router for injecting remote messages.
195    local_router: Option<Arc<dyn LocalDelivery>>,
196    /// Optional discovery state (Kademlia + heartbeat).
197    discovery: Option<DiscoveryState>,
198    /// Mesh time synchronization state.
199    clock: std::sync::Mutex<MeshClockSync>,
200    /// Late-bound chain manager. When set (via
201    /// [`set_chain_manager`]), every successful `handle_incoming`
202    /// appends a `peer.envelope` event to the ExoChain so mesh
203    /// activity is auditable via `weaver chain local`.
204    #[cfg(feature = "exochain")]
205    chain_manager: std::sync::OnceLock<Arc<crate::chain::ChainManager>>,
206    /// Peer topic subscription registry: topic → set of peer node IDs.
207    ///
208    /// Populated when a peer sends a `mesh.subscribe` control envelope.
209    /// Consulted by the A2A router's Topic handler to forward published
210    /// messages to remote nodes that subscribed to the same topic.
211    mesh_subscriptions: DashMap<String, Vec<String>>,
212    /// Assessment mesh transport (WEFT-117 / K6.6). When set, inbound
213    /// `FrameType::AssessmentSync` frames are demuxed into the transport
214    /// instead of being treated as MeshIpcEnvelope JSON.
215    assessment_transport: std::sync::OnceLock<Arc<AssessmentTransport>>,
216    /// Peer join/leave/health event bus (WEFT-120).
217    ///
218    /// `ClusterService` / `ClusterMembership` subscribe so cluster
219    /// membership tracks live mesh state.
220    peer_events: MeshPeerEventBus,
221    /// Sinks for control topics (`mesh.node_facts`, `mesh.artifact.revoke`).
222    control_sinks: DashMap<String, Arc<dyn PeerControlSink>>,
223    /// Connection ids for control-topic limits, keyed by outbound channel.
224    conn_ids: Mutex<Vec<(tokio::sync::mpsc::WeakSender<Vec<u8>>, u64)>>,
225    conn_seq: std::sync::atomic::AtomicU64,
226    /// Whether admission is `enforce`. Only then does a peer whose node id
227    /// was not verified stop counting as a cluster member; under `observe`
228    /// and `off` (the shipped default is `observe`) every peer counts, as
229    /// before. Set by the owner of the admission mode and updated when the
230    /// mode changes.
231    enforcing: std::sync::atomic::AtomicBool,
232    /// Liveness ping/pong service, once started ([`MeshRuntime::start_liveness`]).
233    liveness: std::sync::OnceLock<Arc<crate::mesh_liveness::Liveness>>,
234}
235
236impl MeshRuntime {
237    /// Install the service-owned leaf gate before the listener starts.
238    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    /// Start the liveness ping/pong between verified peers (idempotent).
254    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    /// The liveness service, when started.
262    pub fn liveness(&self) -> Option<&Arc<crate::mesh_liveness::Liveness>> {
263        self.liveness.get()
264    }
265
266    /// Say whether admission is `enforce` (see the field). Takes effect for
267    /// the peer events emitted after the call.
268    pub fn set_enforcing(&self, enforcing: bool) {
269        self.enforcing.store(enforcing, std::sync::atomic::Ordering::SeqCst);
270    }
271
272    /// Whether admission is `enforce`.
273    pub fn enforcing(&self) -> bool {
274        self.enforcing.load(std::sync::atomic::Ordering::SeqCst)
275    }
276
277    /// Whether a connection counts as a cluster member: admission verified
278    /// its node id, or admission is not enforcing.
279    fn counts_as_member(&self, verified: bool) -> bool {
280        verified || !self.enforcing()
281    }
282
283    /// Create a new mesh runtime for the given local node.
284    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    /// Create a mesh runtime with discovery state initialized.
307    ///
308    /// The `kademlia_id` is used as the local key in the Kademlia routing
309    /// table. Heartbeat tracking starts with default configuration.
310    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    /// Stable id of the connection behind `outbound` (1-based; 0 means none).
337    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    /// Install the sink for the control topic `topic` (first call wins).
352    /// Only the topics in `CONTROL_TOPICS` are control topics.
353    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    /// Subscribe to live mesh peer membership / health events (WEFT-120).
358    pub fn subscribe_peer_events(&self) -> tokio::sync::broadcast::Receiver<MeshPeerEvent> {
359        self.peer_events.subscribe()
360    }
361
362    /// Shared peer-event bus (for boot wiring and tests).
363    pub fn peer_event_bus(&self) -> &MeshPeerEventBus {
364        &self.peer_events
365    }
366
367    /// Publish a peer event (used by external discovery / tests).
368    pub fn emit_peer_event(&self, event: MeshPeerEvent) {
369        self.peer_events.emit(event);
370    }
371
372    /// Attach the assessment transport used for AssessmentSync demux (WEFT-117).
373    ///
374    /// Idempotent (first call wins). Safe after the runtime is wrapped in
375    /// [`Arc`] so boot can wire the transport after mesh listener spawn.
376    pub fn set_assessment_transport(&self, transport: Arc<AssessmentTransport>) {
377        let _ = self.assessment_transport.set(transport);
378    }
379
380    /// Shared assessment transport, if registered.
381    pub fn assessment_transport(&self) -> Option<Arc<AssessmentTransport>> {
382        self.assessment_transport.get().cloned()
383    }
384
385    /// Broadcast raw encoded frame bytes to every connected peer.
386    ///
387    /// Used by the assessment gossip/diff push loop. Failures on individual
388    /// peers are logged and skipped (best-effort fan-out).
389    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    /// Drain pending assessment broadcast (diff/gossip) and push to all peers.
405    ///
406    /// Returns the number of peers that received the frame, or 0 if nothing
407    /// was pending / transport not wired.
408    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    /// Drive one assessment gossip tick using the published local report.
419    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    /// Return this node's identifier.
431    pub fn node_id(&self) -> &str {
432        &self.node_id
433    }
434
435    /// Attach the local A2A router for incoming message injection.
436    pub fn set_local_router(&mut self, router: Arc<A2ARouter>) {
437        self.local_router = Some(router);
438    }
439
440    /// Attach any [`LocalDelivery`] sink for inbound messages (a mesh
441    /// service delivers to tenants instead of an in-kernel router).
442    pub fn set_local_delivery(&mut self, delivery: Arc<dyn LocalDelivery>) {
443        self.local_router = Some(delivery);
444    }
445
446    // ── Peer topic subscription registry ─────────────────────────
447
448    /// Register a remote peer as a subscriber for the given topic.
449    ///
450    /// Called from [`handle_incoming`] when a `mesh.subscribe` control
451    /// envelope is received. De-duplicates: if the peer is already
452    /// registered for this topic, the call is a no-op.
453    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    /// Remove a peer's subscription for the given topic.
465    ///
466    /// Called when a peer disconnects or sends an explicit unsubscribe.
467    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    /// Remove all topic subscriptions for the given peer.
474    ///
475    /// Called when `disconnect_peer` is invoked so stale entries are
476    /// cleaned up and future publishes don't attempt dead channels.
477    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    /// Return the list of peer node IDs subscribed to the given topic.
484    ///
485    /// Used by the A2A router's Topic handler to forward published
486    /// messages to remote nodes.
487    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    /// Attach the ExoChain manager for auditable peer-envelope logging.
495    ///
496    /// Idempotent (first call wins; subsequent calls are no-ops). Uses
497    /// interior mutability so this can be called after the runtime has
498    /// been wrapped in [`Arc`], which matches the boot-sequence ordering
499    /// where the chain manager is constructed after the mesh listener.
500    #[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    /// Register a peer connection using an already-established channel.
506    ///
507    /// This is the low-level entry point used after a TCP (or other
508    /// transport) connection has been set up and the node-ID exchange
509    /// has completed. Higher-level helpers like `connect_peer` build
510    /// on top of this.
511    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    /// True when `node_id` is routed through some channel other than `tx`.
516    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    /// [`add_peer`](Self::add_peer) that counts the route in `tally`, so the
521    /// owning connection can tell in O(1) when it is removed or replaced.
522    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    /// Register the route of a connection whose node id admission just
532    /// authenticated, before any envelope arrives, so the peer joins the
533    /// cluster when its handshake completes rather than on its first
534    /// application frame. `verified` is the admission verdict (enforce);
535    /// returns false when refused (see [`register_peer`](Self::register_peer)).
536    /// Test-only: production registers with the admitted class
537    /// ([`register_authenticated_as`](Self::register_authenticated_as)).
538    #[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    /// [`register_authenticated`](Self::register_authenticated) with the
550    /// admitted class of the connection, so outbound decisions that need a
551    /// full node ([`peer_licensed`](Self::peer_licensed)) can tell a leaf.
552    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    /// Register or refresh a route. Returns false when refused: an
564    /// unverified connection may not replace a verified peer's route.
565    ///
566    /// Re-registering the *same* channel (every inbound envelope does) is
567    /// a no-op; only a genuinely new channel replaces the route and emits
568    /// `Recovered`.
569    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        // Decide and write under the entry (shard) lock so an unverified
607        // registration can never overwrite a verified one in a race.
608        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                    // Unauthenticated claims can displace each other; a
619                    // legitimate reconnect looks identical, so this is
620                    // logged, not refused (ADR-103 A10: observe is not
621                    // protection).
622                    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        // WEFT-120: reconnected peers emit Recovered; first connect → Joined.
634        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    /// Remove every route that still points at `tx` (a closed connection's
652    /// outbound channel). Routes since replaced by a newer connection are
653    /// left alone; `Left` is emitted only for routes actually removed.
654    /// Returns the number removed.
655    /// True when some route currently sends through `tx`'s channel.
656    /// O(peers): diagnostics and tests only; connections use [`RouteTally`].
657    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                // Admitted routes lose their subscriptions with the
672                // connection. Unadmitted (legacy) peers keep theirs, as
673                // before: a leaf that reconnects must not have to
674                // re-subscribe.
675                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    /// Send a [`MeshIpcEnvelope`] to a connected peer.
686    ///
687    /// Serializes the envelope to JSON bytes and pushes them into the
688    /// peer's outbound channel. Returns an error if the peer is not
689    /// connected or the channel is closed/full.
690    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    /// Build and send a message to a remote node.
710    ///
711    /// Wraps a [`KernelMessage`] in a [`MeshIpcEnvelope`] with the
712    /// correct source/dest and sends it to the named peer.
713    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    /// Handle incoming raw bytes from a peer.
719    ///
720    /// First attempts AssessmentSync demux via the registered
721    /// [`AssessmentTransport`] (WEFT-117). Non-assessment traffic is
722    /// deserialized as a [`MeshIpcEnvelope`] and injected into the
723    /// local A2A router. The message target is unwrapped from
724    /// `RemoteNode` if present so that the local router delivers to
725    /// the correct local process.
726    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    /// Handle incoming bytes while auto-registering the sending peer.
737    ///
738    /// Used by the mesh accept loop: the kernel doesn't know the peer's
739    /// node ID until the first envelope arrives, so we opportunistically
740    /// register `envelope.source_node → outbound` before dispatching.
741    /// Always (re)registers the channel: on reconnect the node_id is
742    /// unchanged but the TCP connection — and thus the outbound channel
743    /// — is new, and a stale entry would route every push to the dead
744    /// channel of the previous connection. This is what wires
745    /// `A2ARouter` topic forwarding to inbound leaf subscribers (they
746    /// send `mesh.subscribe`, then the kernel can call `send_to_peer`
747    /// back over this connection).
748    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    /// [`handle_incoming_from`](Self::handle_incoming_from) with the
757    /// connection's authenticated identity.
758    ///
759    /// With `peer = Some(ctx)` and `ctx.node_verified`, the route is
760    /// registered under `ctx.peer_id` (never under the envelope's claim),
761    /// an envelope whose `source_node` differs is rejected, and the
762    /// route cannot later be taken over by an unverified connection. With
763    /// `None` (or an unverified ctx) behaviour is the pre-admission one:
764    /// the peer is registered under its claimed `source_node`, with a
765    /// warning the first time that id is seen.
766    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    /// [`handle_incoming_peer`](Self::handle_incoming_peer) that also counts
776    /// the routes it registers in `tally` (the serving connection's O(1)
777    /// "is my route still there" signal).
778    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        // AssessmentSync frames do not carry MeshIpcEnvelope source_node
786        // for auto-registration. Demux first; reply (if any) goes on the
787        // same outbound channel. Peer registration for assessment-only
788        // peers happens when the first IPC envelope arrives, or via
789        // explicit add_peer (seed / test harness).
790        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            // No authenticated identity: the claim is all there is.
812            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        // Always (re)register the peer's outbound channel. A leaf that
821        // reconnects (daemon restart, wifi blip, socket idle-timeout)
822        // keeps the same node_id but gets a fresh channel — the stale
823        // entry must be replaced or `send_to_peer` delivers into the
824        // dead channel of the dropped connection and the peer never
825        // receives anything again.
826        let conn = self.conn_id(&outbound);
827        // The class rides with the route so a re-registered leaf (after
828        // `remove_dead_peers` dropped it) stays a leaf, never a node.
829        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    /// Demux AssessmentSync frames into the registered transport.
841    ///
842    /// Returns `Ok(true)` when the bytes were assessment traffic (handled),
843    /// `Ok(false)` when the caller should continue with IPC envelope path.
844    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                        // Prefer routing the reply to the known peer by source_node.
867                        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        // Auditable record: append a peer.envelope event to the local
891        // chain before routing. Captures source_node (leaf identity),
892        // dest_node, envelope_id, and the resolved topic/service when
893        // known. The chain append is best-effort — a failure here must
894        // not block message delivery.
895        #[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        // Unwrap the RemoteNode wrapper so the local router sees the
920        // inner target (Process, Service, Topic, etc.).
921        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        // Intercept mesh.subscribe control envelopes before local routing.
928        //
929        // A peer sends `Topic("mesh.subscribe")` with a JSON payload of the
930        // form `{"topic": "<topic-name>"}` to register interest. We record
931        // the subscription and consume the message — it does not propagate
932        // to the local router or any local subscribers. This is handled
933        // before the local-router lookup: a subscribe is consumed here and
934        // never routed, so it must succeed even on a runtime with no router
935        // attached (e.g. a pure leaf-push relay, or before the kernel wires
936        // its router in).
937        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                // Same admitted-identity path as delivery: the subscriber
944                // is `ctx.peer_id`, and a tenant-aware sink may veto.
945                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            // Malformed subscribe — drop silently rather than routing to
960            // local subscribers who have no idea what to do with it.
961            warn!(
962                from = %envelope.source_node,
963                "received malformed mesh.subscribe envelope (missing 'topic' field)"
964            );
965            return Ok(());
966        }
967
968        // Control topics are handed to their sink with the connection's
969        // authenticated identity and never routed.
970        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        // Everything else routes to the local kernel router.
990        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    /// The connection id and verified flag of the current route to `node_id`
998    /// (None when not connected). Lets a cache of per-connection facts check
999    /// it still describes the connection in use.
1000    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    /// Number of currently connected peers.
1006    pub fn peer_count(&self) -> usize {
1007        self.peers.len()
1008    }
1009
1010    /// True when `node_id` is routed through a connection whose node id
1011    /// admission verified.
1012    pub fn peer_verified(&self, node_id: &str) -> bool {
1013        self.peers.get(node_id).is_some_and(|p| p.verified)
1014    }
1015
1016    /// True when `node_id` is routed through a verified connection whose
1017    /// admitted class is `node`: the outbound twin of
1018    /// `mesh_artifact_tunnel::licensed_peer`. A verified leaf is not.
1019    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    /// List the node IDs of all connected peers.
1026    pub fn peer_ids(&self) -> Vec<String> {
1027        self.peers.iter().map(|entry| entry.key().clone()).collect()
1028    }
1029
1030    /// Per-peer connection detail, sorted by node id (observability only).
1031    ///
1032    /// Round-trip time is deliberately absent: nothing in the runtime measures
1033    /// it (`PeerMetrics` is never fed), and a made-up zero would read as a
1034    /// perfect link.
1035    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    /// Disconnect a peer, dropping its send channel.
1073    ///
1074    /// Also cleans up any topic subscriptions the peer registered so
1075    /// future publishes don't attempt to send to a closed channel.
1076    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    /// Disconnect every connected peer (used by mesh service stop / shutdown).
1089    ///
1090    /// Returns the number of peers that were disconnected.
1091    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    // ── Discovery integration ─────────────────────────────────────
1101
1102    /// Register a peer's network address for future connection.
1103    ///
1104    /// Stored in the discovery state's address map. If discovery is not
1105    /// initialized this is a no-op.
1106    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    /// Return known peers from the discovery address map.
1114    ///
1115    /// Each entry is `(node_id, address)`. Returns an empty vec if
1116    /// discovery is not initialized.
1117    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    /// Record a heartbeat from the given peer (marks it alive).
1129    ///
1130    /// If the peer is not yet tracked by the heartbeat system it will
1131    /// be added automatically.
1132    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            // Emit health transitions for cluster membership (WEFT-120).
1141            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    // ── Time synchronization ──────────────────────────────────────
1167
1168    /// Get the current mesh-synchronized time in microseconds since epoch.
1169    ///
1170    /// If time sync is active (synced from authority), returns the
1171    /// authority-aligned time. Otherwise returns local system time.
1172    pub fn mesh_time_us(&self) -> u64 {
1173        self.clock.lock().unwrap().mesh_time_us()
1174    }
1175
1176    /// Get the clock uncertainty estimate in microseconds.
1177    pub fn clock_uncertainty_us(&self) -> u64 {
1178        self.clock.lock().unwrap().uncertainty_us
1179    }
1180
1181    /// Get the current clock source quality.
1182    pub fn clock_source(&self) -> ClockSource {
1183        self.clock.lock().unwrap().local_source
1184    }
1185
1186    /// Set the local clock source (e.g., after NTP sync is confirmed).
1187    pub fn set_clock_source(&self, source: ClockSource) {
1188        self.clock.lock().unwrap().local_source = source;
1189    }
1190
1191    /// Process a time sync sample from a peer's heartbeat.
1192    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    /// Check if this node is the time authority.
1201    pub fn is_time_authority(&self) -> bool {
1202        self.clock.lock().unwrap().is_authority(&self.node_id)
1203    }
1204
1205    /// Return node IDs of peers that heartbeat considers suspect or dead.
1206    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    /// Disconnect peers that the heartbeat tracker considers dead.
1223    ///
1224    /// Emits [`MeshPeerEvent::Unreachable`] before the connection is
1225    /// dropped so cluster membership can mark partition/failure
1226    /// separately from a graceful [`MeshPeerEvent::Left`].
1227    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                // Disconnect without a second Left event: remove peer map
1238                // entry and topics, but treat Unreachable as the bus signal.
1239                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    /// Mutable access to discovery state (for tests and setup code).
1249    pub fn discovery_mut(&mut self) -> Option<&mut DiscoveryState> {
1250        self.discovery.as_mut()
1251    }
1252
1253    /// Shared access to discovery state.
1254    pub fn discovery(&self) -> Option<&DiscoveryState> {
1255        self.discovery.as_ref()
1256    }
1257
1258    // ── Chain sync (WEFT-105 / K6.4) ──────────────────────────────
1259
1260    /// Build a serialized [`ChainSyncRequest`] for sending to a peer.
1261    ///
1262    /// The request asks for chain events starting after `from_seq`.
1263    /// When a chain manager is attached, `after_hash` is filled from the
1264    /// local tip at `from_seq` (or the head hash when `from_seq` is the tip).
1265    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    /// Build a chain sync **response** from the local chain (server side).
1277    ///
1278    /// Returns `None` when no chain manager is attached.
1279    #[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    /// Handle an incoming chain sync response by replaying remote events
1309    /// into the local [`ChainManager`] via [`ChainManager::append_signed`].
1310    ///
1311    /// Returns the number of events **newly applied** (idempotent skips
1312    /// are not counted). When no chain manager is attached, returns the
1313    /// number of events in the payload without applying them (legacy /
1314    /// transport-only callers). See [`Self::handle_chain_sync_outcome`] for
1315    /// the typed form, which also reports a stop at an authority event.
1316    ///
1317    /// Merge policy (ADR-089): linear catch-up only. A hash mismatch at
1318    /// the same sequence surfaces as a mesh error (fork) rather than
1319    /// silent overwrite — multi-parent merge commits are a later layer.
1320    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    /// [`Self::handle_chain_sync_response`] with a typed, non-fatal stop:
1325    /// replication halts cleanly at the first authority-bearing event
1326    /// (a reserved source or kind, see `chain::RESERVED_SOURCES`) and says
1327    /// where, instead of failing the batch. Verified replication of authority
1328    /// events is future work; skipping one would leave a sequence gap.
1329    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            // Prefer the event at `from_seq` if present.
1384            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/// Where chain replication stopped at an authority-bearing event.
1404#[derive(Debug, Clone, PartialEq, Eq)]
1405pub struct StoppedAtAuthorityEvent {
1406    /// Sequence of the refused event.
1407    pub sequence: u64,
1408    /// Its source.
1409    pub source: String,
1410}
1411
1412/// Result of applying a chain sync batch.
1413#[derive(Debug, Clone, PartialEq, Eq)]
1414pub struct ChainSyncOutcome {
1415    /// Events newly applied.
1416    pub applied: usize,
1417    /// Set when replication halted at an authority event (not an error).
1418    pub stopped: Option<StoppedAtAuthorityEvent>,
1419}
1420
1421// ── Tests ───────────────────────────────────────────────────────
1422
1423#[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    /// Helper: build a minimal A2ARouter with one registered process and return
1435    /// (router, pid, inbox_receiver).
1436    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 1: empty peer list on creation ─────────────────────
1460
1461    #[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 2: add mock peer, verify peer_count ────────────────
1470
1471    #[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    // ── WEFT-120: peer-event emission ─────────────────────────────
1481
1482    #[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(); // Joined
1504        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        // Re-register same node_id (reconnect) without disconnecting first
1523        // keeps was_known=true → Recovered.
1524        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(); // Joined
1545
1546        // Force peer to Dead via zero suspect timeout + two misses.
1547        {
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"); // Alive → Suspect
1557            hb.record_miss("dying"); // Suspect + zero timeout → Dead
1558            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        // Unverified only exists under enforce; observe and off count everyone.
1584        rt.set_enforcing(true);
1585        membership.spawn_mesh_peer_listener(rt.subscribe_peer_events());
1586
1587        // A legacy route (`add_peer`: a seed, a leaf, a test harness) is a
1588        // claimed id, held as Unverified; an admitted one is Active.
1589        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        // Reconnect after partition.
1613        let (tx2, _rx2) = tokio::sync::mpsc::channel(16);
1614        // Still in peers map from first add — re-add emits Recovered.
1615        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    // ── Test 3: send envelope to peer (mock channel) ────────────
1628
1629    #[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    // ── Test 4: receive envelope, inject into local router ──────
1649
1650    #[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        // Build an envelope targeting a local process via RemoteNode wrapper
1658        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 5: disconnect peer ─────────────────────────────────
1678
1679    #[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    // ── Test 6: round-trip (serialize -> deserialize -> inject) ──
1691
1692    #[tokio::test]
1693    async fn round_trip_send_receive() {
1694        let (router, pid, mut inbox_rx) = make_router_with_process();
1695
1696        // "Node A" side: build the runtime and a mock peer channel
1697        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        // Send from A to B
1702        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        // "Node B" side: receive the bytes and inject
1713        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 7: node ID exchange metadata ───────────────────────
1726
1727    #[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        // connected_at should be very recent (within 1 second)
1736        let age = chrono::Utc::now() - entry.connected_at;
1737        assert!(age.num_seconds() < 2);
1738    }
1739
1740    // ── Test 8: multiple peers ──────────────────────────────────
1741
1742    #[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    // ── Test 9: send to unknown peer returns error ──────────────
1757
1758    #[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    // ── Test 10: handle_incoming with malformed data ────────────
1770
1771    #[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    // ── Test 11: handle_incoming without router errors ──────────
1783
1784    #[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 12: disconnect nonexistent peer is safe ────────────
1797
1798    #[test]
1799    fn disconnect_nonexistent_peer_is_noop() {
1800        let rt = MeshRuntime::new("local".into());
1801        rt.disconnect_peer("ghost"); // should not panic
1802        assert_eq!(rt.peer_count(), 0);
1803    }
1804
1805    // ── Test 13: two-node message exchange ──────────────────────
1806
1807    #[tokio::test]
1808    async fn two_nodes_exchange_messages() {
1809        // Create two MeshRuntime instances with different node IDs.
1810        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        // Create channels to simulate network transport.
1817        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        // Each node knows the other as a peer.
1821        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        // Node A sends a message targeting a process on node B.
1825        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        // Simulate the wire: read from a_to_b channel, inject into rt_b.
1836        let wire_bytes = a_to_b_rx.recv().await.unwrap();
1837        rt_b.handle_incoming(&wire_bytes).await.unwrap();
1838
1839        // The message should be in node B's local inbox.
1840        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        // Node B replies back to node A (we just verify send succeeds).
1847        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 14: discover_peers returns registered addresses ─────
1857
1858    #[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 15: heartbeat tracking detects suspect peer ─────────
1891
1892    #[test]
1893    fn heartbeat_tracking_detects_dead_peer() {
1894        let mut rt = MeshRuntime::with_discovery("local".into(), [0u8; 32]);
1895
1896        // Configure heartbeat with zero suspect timeout for instant transition.
1897        {
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        // Record heartbeat for healthy peer.
1909        rt.record_heartbeat("healthy");
1910
1911        // Simulate misses for dying peer -> suspect -> dead.
1912        {
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 16: remove_dead_peers cleans connections ────────────
1925
1926    #[test]
1927    fn remove_dead_peers_cleans_connections() {
1928        let mut rt = MeshRuntime::with_discovery("local".into(), [0u8; 32]);
1929
1930        // Add a peer connection.
1931        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        // Force the peer into Dead state.
1936        {
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 17: Kademlia routing finds closest peers ────────────
1956
1957    #[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        // Insert peers with different XOR distances from the local key [0;32].
1964        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        // Find the 3 closest to the local key.
1981        let closest = kad.find_closest(&[0u8; 32], 3);
1982        assert_eq!(closest.len(), 3);
1983        // The closest by XOR distance to [0;32] should be the ones with
1984        // the smallest first byte, which maps to node_id strings that
1985        // start with the smallest byte values.
1986    }
1987
1988    // ── Test 18: with_discovery initializes state ────────────────
1989
1990    #[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 19: discover_peers empty without discovery ──────────
2002
2003    #[test]
2004    fn discover_peers_empty_without_discovery() {
2005        let rt = MeshRuntime::new("plain-node".into());
2006        assert!(rt.discover_peers().is_empty());
2007        // These should be safe no-ops.
2008        rt.register_peer_address("x", "y");
2009        rt.record_heartbeat("x");
2010        assert!(rt.check_peer_health().is_empty());
2011    }
2012
2013    // ── Test 20: chain sync request round-trip ───────────────────
2014
2015    #[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 21: chain sync response handling (no chain attached) ─
2027
2028    #[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        // Without a chain manager, events are counted but not applied.
2045        let count = rt.handle_chain_sync_response(&data).unwrap();
2046        assert_eq!(count, 2);
2047    }
2048
2049    // ── Test 22: chain sync response malformed errors ────────────
2050
2051    #[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    // ── Test 22b: two-node chain convergence via append_signed ───
2059
2060    /// WEFT-105: source node is ahead; sink shares genesis and catches up
2061    /// via chain sync response → both tips converge.
2062    #[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        // Sink starts from the same cluster genesis (shared identity).
2074        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        // Second sync is idempotent — zero new applies.
2096        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    /// Replication stops cleanly (typed, non-fatal) at the first authority
2103    /// event; what came before is applied, nothing after is.
2104    #[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    // ── Test 23: inbound peer auto-registered, topic forwarded ───
2133
2134    /// End-to-end: a leaf peer sends a `mesh.subscribe` envelope over
2135    /// an inbound connection; the kernel auto-registers the peer, records
2136    /// the subscription, and then forwards a locally-published topic
2137    /// message back through the peer's outbound channel.
2138    #[tokio::test]
2139    async fn subscribe_then_topic_forwards_to_inbound_peer() {
2140        let (router, pid, _inbox) = make_router_with_process();
2141
2142        // Wire a mesh runtime to the router so the Topic handler's
2143        // forwarder can see it.
2144        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        // Inbound peer's outbound channel (what the accept loop would drain).
2150        let (out_tx, mut out_rx) = tokio::sync::mpsc::channel::<Vec<u8>>(16);
2151
2152        // Leaf sends mesh.subscribe to register interest in topic "push.leaf-x".
2153        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        // Peer should now be registered in both the peer table and the
2166        // subscription registry.
2167        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        // Locally publish to the subscribed topic. The A2A router's Topic
2177        // handler should forward to the mesh peer via send_to_peer.
2178        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        // The outbound drain should see the forwarded envelope.
2186        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        // Disconnect should also clear the subscription, so a follow-up
2205        // publish has nowhere to go.
2206        rt.disconnect_peer("leaf-x");
2207        assert!(rt.peers_for_topic("push.leaf-x").is_empty());
2208    }
2209
2210    // ── Test 24: reconnecting peer re-registers its outbound channel ─
2211
2212    /// Regression: a leaf that reconnects keeps the same `source_node`
2213    /// but gets a fresh outbound channel. `handle_incoming_from` must
2214    /// replace the stale entry — otherwise `send_to_peer` delivers into
2215    /// the dead channel of the dropped connection and the leaf never
2216    /// receives anything again.
2217    #[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        // First connection.
2230        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        // Reconnect: same node_id, brand-new channel (old connection dropped).
2234        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        // A push must land on the NEW channel, not the stale one.
2238        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    // ── WEFT-117: AssessmentTransport demux + two-node diff push ─
2257
2258    /// Two mock peers: node A pushes a FindingDiff through MeshRuntime
2259    /// demux; node B's AssessmentTransport updates peer state without
2260    /// requiring a full report exchange.
2261    #[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        // Routers not required for assessment demux path.
2291        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        // Mock peer channels: A → B and B → A
2297        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        // A queues FindingDiff and drains via push_pending_assessment.
2329        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        // B demuxes AssessmentSync in the mesh event loop.
2337        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        // JSON IPC still works when assessment transport is wired
2348        // (non-assessment traffic falls through).
2349        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        // Silence unused warning for b_to_a_rx if nothing was replied.
2365        let _ = b_to_a_rx.try_recv();
2366    }
2367}
2368
2369#[cfg(test)]
2370#[path = "mesh_runtime_bind_tests.rs"]
2371mod bind_tests;