Skip to main content

clawft_kernel/
a2a.rs

1//! Agent-to-agent IPC protocol.
2//!
3//! The [`A2ARouter`] provides direct PID-to-PID messaging with
4//! capability-checked routing, per-agent inboxes, and request-response
5//! patterns with timeout support. It integrates with the
6//! [`TopicRouter`] for pub/sub delivery.
7//!
8//! # Message Flow
9//!
10//! ```text
11//! Agent A (PID 1)       A2ARouter          Agent B (PID 7)
12//!      |                    |                    |
13//!      |-- send(msg) ------>|                    |
14//!      |                    |-- check_scope ---->|
15//!      |                    |<-- Ok -------------|
16//!      |                    |-- inbox.send(7) -->|
17//!      |                    |                    |-- recv msg
18//! ```
19
20use std::sync::Arc;
21use std::time::{Duration, Instant};
22
23use dashmap::DashMap;
24use tokio::sync::{mpsc, oneshot};
25use tracing::{debug, warn};
26
27use crate::capability::CapabilityChecker;
28use crate::error::{KernelError, KernelResult};
29use crate::ipc::{KernelMessage, MessageTarget};
30use crate::process::{Pid, ProcessState, ProcessTable};
31use crate::service::ServiceRegistry;
32use crate::topic::{SubscriberSink, TopicRouter};
33
34#[cfg(feature = "exochain")]
35use crate::chain::ChainManager;
36
37#[cfg(feature = "mesh")]
38use crate::mesh_ipc::MeshIpcEnvelope;
39#[cfg(feature = "mesh")]
40use crate::mesh_runtime::MeshRuntime;
41
42/// Default inbox channel capacity per agent.
43const DEFAULT_INBOX_CAPACITY: usize = 1024;
44
45/// Maximum serialized message size (16 MiB) -- prevents DoS via oversized payloads.
46///
47/// This mirrors [`crate::mesh::MAX_MESSAGE_SIZE`]. Enforcement happens at the
48/// mesh boundary in `mesh_framing.rs` where raw bytes are received from remote
49/// nodes. Within a single kernel, messages travel as typed `KernelMessage`
50/// structs over `mpsc` channels, so the size limit is not checked on the
51/// in-process hot path.
52#[allow(dead_code)]
53const MAX_A2A_MESSAGE_SIZE: usize = 16 * 1024 * 1024;
54
55/// A pending request awaiting a correlated response.
56struct PendingRequest {
57    /// Sender to deliver the response on.
58    response_tx: oneshot::Sender<KernelMessage>,
59    /// When the request was sent (for timeout tracking / diagnostics).
60    #[allow(dead_code)]
61    sent_at: Instant,
62}
63
64/// Carries messages for remote nodes when the mesh runs outside this kernel
65/// (service mode, ADR-103 P3-U): the router hands every
66/// [`MessageTarget::RemoteNode`] message to it instead of a [`MeshRuntime`].
67#[cfg(feature = "mesh")]
68#[async_trait::async_trait]
69pub trait RemoteForwarder: Send + Sync + 'static {
70    /// Forward `msg` to `node_id`.
71    async fn forward(&self, node_id: &str, msg: KernelMessage) -> KernelResult<()>;
72}
73
74/// Agent-to-agent message router.
75///
76/// Manages per-agent inboxes (bounded `mpsc` channels), validates
77/// IPC scope through the capability checker, and routes messages
78/// to their targets (direct PID, topic, broadcast, service).
79pub struct A2ARouter {
80    /// Process table for state validation.
81    process_table: Arc<ProcessTable>,
82
83    /// Capability checker for IPC scope enforcement.
84    capability_checker: Arc<CapabilityChecker>,
85
86    /// Topic router for pub/sub delivery.
87    topic_router: Arc<TopicRouter>,
88
89    /// Service registry for service-based routing (D1, D19, K2.1).
90    service_registry: Option<Arc<ServiceRegistry>>,
91
92    /// Per-agent inboxes: PID -> sender half of inbox channel.
93    inboxes: DashMap<Pid, mpsc::Sender<KernelMessage>>,
94
95    /// Pending request-response tracking: request_id -> PendingRequest.
96    pending_requests: DashMap<String, PendingRequest>,
97
98    /// Optional routing-time gate backend (C4 dual-layer governance).
99    ///
100    /// Uses `OnceLock` to support post-construction wiring from boot
101    /// (the governance gate is created after the A2ARouter is wrapped
102    /// in `Arc`). `with_gate()` and `set_gate()` both target this field.
103    #[cfg(feature = "exochain")]
104    gate: std::sync::OnceLock<Arc<dyn crate::gate::GateBackend>>,
105
106    /// Dead letter queue for undeliverable messages (os-patterns).
107    #[cfg(feature = "os-patterns")]
108    dead_letter_queue: std::sync::OnceLock<Arc<crate::dead_letter::DeadLetterQueue>>,
109
110    /// Optional mesh runtime for cross-node message delivery (K6).
111    #[cfg(feature = "mesh")]
112    mesh_runtime: std::sync::OnceLock<Arc<MeshRuntime>>,
113
114    /// Remote-node forwarder used instead of `mesh_runtime` in service mode.
115    #[cfg(feature = "mesh")]
116    remote_forwarder: std::sync::OnceLock<Arc<dyn RemoteForwarder>>,
117}
118
119impl A2ARouter {
120    /// Create a new A2A router.
121    pub fn new(
122        process_table: Arc<ProcessTable>,
123        capability_checker: Arc<CapabilityChecker>,
124        topic_router: Arc<TopicRouter>,
125    ) -> Self {
126        Self {
127            process_table,
128            capability_checker,
129            topic_router,
130            service_registry: None,
131            inboxes: DashMap::new(),
132            pending_requests: DashMap::new(),
133            #[cfg(feature = "exochain")]
134            gate: std::sync::OnceLock::new(),
135            #[cfg(feature = "os-patterns")]
136            dead_letter_queue: std::sync::OnceLock::new(),
137            #[cfg(feature = "mesh")]
138            mesh_runtime: std::sync::OnceLock::new(),
139            #[cfg(feature = "mesh")]
140            remote_forwarder: std::sync::OnceLock::new(),
141        }
142    }
143
144    /// Attach a service registry for service-based routing (D1, D19).
145    pub fn with_service_registry(mut self, registry: Arc<ServiceRegistry>) -> Self {
146        self.service_registry = Some(registry);
147        self
148    }
149
150    /// Attach a routing-time gate for dual-layer governance (C4).
151    ///
152    /// When set, every message routed through `send()` is checked against
153    /// the gate *before* inbox delivery. A `Deny` decision blocks the
154    /// message; `Defer` still delivers (the handler-time gate decides).
155    #[cfg(feature = "exochain")]
156    pub fn with_gate(self, gate: Arc<dyn crate::gate::GateBackend>) -> Self {
157        let _ = self.gate.set(gate);
158        self
159    }
160
161    /// Set the routing-time gate after construction (for boot wiring).
162    ///
163    /// This allows the governance gate to be attached after the router
164    /// is already wrapped in an `Arc`, since the `OnceLock` provides
165    /// interior mutability for the first (and only) write.
166    #[cfg(feature = "exochain")]
167    pub fn set_gate(&self, gate: Arc<dyn crate::gate::GateBackend>) {
168        let _ = self.gate.set(gate);
169    }
170
171    /// Set the dead letter queue after construction (for boot wiring).
172    ///
173    /// Like `set_gate()`, uses `OnceLock` for interior mutability so
174    /// the DLQ can be attached after the router is wrapped in `Arc`.
175    #[cfg(feature = "os-patterns")]
176    pub fn set_dead_letter_queue(&self, dlq: Arc<crate::dead_letter::DeadLetterQueue>) {
177        let _ = self.dead_letter_queue.set(dlq);
178    }
179
180    /// Get the dead letter queue (if configured).
181    #[cfg(feature = "os-patterns")]
182    pub fn dead_letter_queue(&self) -> Option<&Arc<crate::dead_letter::DeadLetterQueue>> {
183        self.dead_letter_queue.get()
184    }
185
186    /// Attach the mesh runtime for cross-node message delivery (K6).
187    ///
188    /// Uses `OnceLock` so the runtime can be attached after the router
189    /// is already wrapped in `Arc`.
190    #[cfg(feature = "mesh")]
191    pub fn set_mesh_runtime(&self, runtime: Arc<MeshRuntime>) {
192        let _ = self.mesh_runtime.set(runtime);
193    }
194
195    /// Attach the forwarder that carries remote-node messages when the mesh
196    /// is owned by the machine mesh service (P3-U). Ignored once set.
197    #[cfg(feature = "mesh")]
198    pub fn set_remote_forwarder(&self, forwarder: Arc<dyn RemoteForwarder>) {
199        let _ = self.remote_forwarder.set(forwarder);
200    }
201
202    /// Get the mesh runtime (if configured).
203    #[cfg(feature = "mesh")]
204    pub fn mesh_runtime(&self) -> Option<&Arc<MeshRuntime>> {
205        self.mesh_runtime.get()
206    }
207
208    /// Get the service registry (if configured).
209    pub fn service_registry(&self) -> Option<&Arc<ServiceRegistry>> {
210        self.service_registry.as_ref()
211    }
212
213    /// Create an inbox for a process.
214    ///
215    /// Returns the receiver half that the agent should poll for
216    /// incoming messages. The sender half is stored internally
217    /// for routing.
218    ///
219    /// If an inbox already exists for this PID, the old one is
220    /// replaced (existing messages are lost).
221    pub fn create_inbox(&self, pid: Pid) -> mpsc::Receiver<KernelMessage> {
222        let (tx, rx) = mpsc::channel(DEFAULT_INBOX_CAPACITY);
223        self.inboxes.insert(pid, tx);
224        debug!(pid, "created inbox");
225        rx
226    }
227
228    /// Remove an inbox (used during process cleanup).
229    pub fn remove_inbox(&self, pid: Pid) {
230        self.inboxes.remove(&pid);
231        debug!(pid, "removed inbox");
232    }
233
234    /// Send a message, routing it to the appropriate target.
235    ///
236    /// Validates that the sender exists and is running, checks
237    /// IPC scope via the capability checker, then delivers the
238    /// message to the target.
239    ///
240    /// # Routing
241    ///
242    /// - `Process(pid)`: delivers directly to the target's inbox
243    /// - `Topic(name)`: publishes to all topic subscribers
244    /// - `Broadcast`: delivers to all inboxes except the sender
245    /// - `Service(name)`: logs a warning (service routing is a
246    ///   future extension)
247    /// - `Kernel`: logs a warning (kernel messages are internal)
248    ///
249    /// # Errors
250    ///
251    /// Returns `KernelError::ProcessNotFound` if the sender PID
252    /// is not in the process table, or `KernelError::CapabilityDenied`
253    /// if the sender's IPC scope does not permit the target.
254    pub async fn send(&self, msg: KernelMessage) -> KernelResult<()> {
255        let from = msg.from;
256
257        // Validate sender exists and is running
258        let sender = self
259            .process_table
260            .get(from)
261            .ok_or(KernelError::ProcessNotFound { pid: from })?;
262
263        if !matches!(
264            sender.state,
265            ProcessState::Running | ProcessState::Suspended
266        ) {
267            return Err(KernelError::Ipc(format!(
268                "sender PID {from} is not running (state: {})",
269                sender.state
270            )));
271        }
272
273        // C4: Routing-time gate check (first layer of dual-layer governance).
274        // A Deny blocks the message before it reaches any inbox. Defer
275        // still delivers — the handler-time gate makes the final call.
276        #[cfg(feature = "exochain")]
277        if let Some(gate) = self.gate.get() {
278            let action = match &msg.payload {
279                crate::ipc::MessagePayload::ToolCall { name, .. } => format!("tool.{name}"),
280                crate::ipc::MessagePayload::Signal(_) => "ipc.signal".to_string(),
281                _ => "ipc.send".to_string(),
282            };
283            use crate::governance::{GateEffectKind, with_effect_context};
284            let context = with_effect_context(
285                serde_json::json!({
286                    "from": from,
287                    "target": format!("{:?}", msg.target),
288                    "layer": "routing",
289                }),
290                &GateEffectKind::A2aRouting.effect_vector(),
291            );
292            match gate.check(&from.to_string(), &action, &context) {
293                crate::gate::GateDecision::Deny { reason, .. } => {
294                    return Err(KernelError::CapabilityDenied {
295                        pid: from,
296                        action,
297                        reason: format!("routing gate denied: {reason}"),
298                    });
299                }
300                crate::gate::GateDecision::Defer { .. }
301                | crate::gate::GateDecision::Permit { .. } => {
302                    // Permitted or deferred — continue to delivery.
303                }
304            }
305        }
306
307        // Route based on target
308        match &msg.target {
309            MessageTarget::Process(target_pid) => {
310                // Check IPC scope
311                self.capability_checker
312                    .check_ipc_target(from, *target_pid)?;
313
314                self.deliver_to_inbox(*target_pid, msg).await
315            }
316            MessageTarget::Topic(topic) => {
317                // WEFT-150: enforce IpcScope topic grants on publish
318                // (mirrors check_ipc_target for Process). Kernel PID 0
319                // defaults to IpcScope::All; Restricted / Topic scopes
320                // and browser Restricted are blocked from non-public
321                // topics (mesh.leaf.* is not public).
322                self.capability_checker.check_ipc_topic(from, topic)?;
323
324                let sinks = self.topic_router.live_sinks(topic);
325                let mut delivered = 0u32;
326
327                // Serialize the message once for external streaming
328                // subscribers; only build this if at least one
329                // external sink exists so the fast path is unaffected.
330                let mut external_line: Option<Vec<u8>> = None;
331                if sinks
332                    .iter()
333                    .any(|(_, s)| matches!(s, SubscriberSink::ExternalStream(_)))
334                    && let Ok(mut bytes) = serde_json::to_vec(&msg)
335                {
336                    bytes.push(b'\n');
337                    external_line = Some(bytes);
338                }
339
340                for (_id, sink) in &sinks {
341                    match sink {
342                        SubscriberSink::PidInbox(sub_pid) => {
343                            if *sub_pid != from {
344                                let msg_clone = msg.clone();
345                                if self.deliver_to_inbox(*sub_pid, msg_clone).await.is_ok() {
346                                    delivered += 1;
347                                }
348                            }
349                        }
350                        SubscriberSink::ExternalStream(tx) => {
351                            if let Some(line) = external_line.as_ref() {
352                                // Best-effort delivery; a full or closed
353                                // channel drops the message for that
354                                // client (the daemon will prune on the
355                                // next publish via live_sinks).
356                                match tx.try_send(line.clone()) {
357                                    Ok(()) => {
358                                        delivered += 1;
359                                    }
360                                    Err(e) => {
361                                        warn!(topic, error = %e, "external stream drop");
362                                    }
363                                }
364                            }
365                        }
366                    }
367                }
368
369                // Forward to mesh peers that registered a subscription for
370                // this topic via a `mesh.subscribe` control envelope.
371                #[cfg(feature = "mesh")]
372                if let Some(runtime) = self.mesh_runtime.get() {
373                    let peer_ids = runtime.peers_for_topic(topic);
374                    let mut mesh_delivered = 0u32;
375                    for peer_id in &peer_ids {
376                        let envelope = MeshIpcEnvelope::new(
377                            runtime.node_id().to_string(),
378                            peer_id.clone(),
379                            msg.clone(),
380                        );
381                        match runtime.send_to_peer(peer_id, envelope).await {
382                            Ok(()) => mesh_delivered += 1,
383                            Err(e) => {
384                                warn!(
385                                    topic,
386                                    peer = %peer_id,
387                                    error = %e,
388                                    "failed to forward topic to mesh peer"
389                                );
390                            }
391                        }
392                    }
393                    if mesh_delivered > 0 {
394                        debug!(from, topic, mesh_delivered, "forwarded topic to mesh peers");
395                    }
396                }
397
398                debug!(from, topic, delivered, "published to topic");
399                Ok(())
400            }
401            MessageTarget::Broadcast => {
402                let mut delivered = 0u32;
403                let pids: Vec<Pid> = self.inboxes.iter().map(|entry| *entry.key()).collect();
404
405                for pid in pids {
406                    if pid != from {
407                        // Check IPC scope for each target
408                        if self.capability_checker.check_ipc_target(from, pid).is_ok() {
409                            let msg_clone = msg.clone();
410                            if self.deliver_to_inbox(pid, msg_clone).await.is_ok() {
411                                delivered += 1;
412                            }
413                        }
414                    }
415                }
416                debug!(from, delivered, "broadcast sent");
417                Ok(())
418            }
419            MessageTarget::Service(name) => {
420                let name = name.clone();
421                self.route_to_service(from, &name, msg).await
422            }
423            MessageTarget::ServiceMethod { service, .. } => {
424                let service_name = service.clone();
425                self.route_to_service(from, &service_name, msg).await
426            }
427            MessageTarget::Kernel => {
428                debug!(from, "kernel message routing not yet implemented");
429                Ok(())
430            }
431            MessageTarget::RemoteNode { node_id, .. } => {
432                #[cfg(feature = "mesh")]
433                {
434                    if let Some(forwarder) = self.remote_forwarder.get() {
435                        let node_id = node_id.clone();
436                        debug!(from, %node_id, "routing message to remote node via mesh service");
437                        return forwarder.forward(&node_id, msg).await;
438                    }
439                    if let Some(runtime) = self.mesh_runtime.get() {
440                        let node_id = node_id.clone();
441                        debug!(from, %node_id, "routing message to remote node via mesh");
442                        let envelope = MeshIpcEnvelope::new(
443                            runtime.node_id().to_string(),
444                            node_id.clone(),
445                            msg,
446                        );
447                        runtime.send_to_peer(&node_id, envelope).await.map_err(|e| {
448                            KernelError::Mesh(format!(
449                                "failed to send to remote node '{node_id}': {e}"
450                            ))
451                        })
452                    } else {
453                        debug!(from, %node_id, "remote node routing: no mesh runtime attached");
454                        Err(KernelError::Mesh(format!(
455                            "remote routing to node '{node_id}' not yet implemented"
456                        )))
457                    }
458                }
459                #[cfg(not(feature = "mesh"))]
460                {
461                    debug!(from, %node_id, "remote node routing not available (mesh feature disabled)");
462                    Err(KernelError::Mesh(format!(
463                        "remote routing to node '{node_id}' not yet implemented"
464                    )))
465                }
466            }
467        }
468    }
469
470    /// Deliver a message to a specific PID's inbox.
471    ///
472    /// If the inbox does not exist or is full, the message is routed to
473    /// the dead letter queue (when os-patterns is enabled) and an error
474    /// is returned.
475    async fn deliver_to_inbox(&self, pid: Pid, msg: KernelMessage) -> KernelResult<()> {
476        // Clone the sender so we release the DashMap read lock before
477        // any potential remove() call (which needs a write lock on the
478        // same shard — holding both would deadlock).
479        let tx = match self.inboxes.get(&pid) {
480            Some(tx) => tx.clone(),
481            None => {
482                warn!(pid, "no inbox for PID, dead-lettering");
483                #[cfg(feature = "os-patterns")]
484                if let Some(dlq) = self.dead_letter_queue.get() {
485                    dlq.intake(
486                        msg,
487                        crate::dead_letter::DeadLetterReason::TargetNotFound { pid },
488                    );
489                }
490                return Err(KernelError::Ipc(format!("no inbox for PID {pid}")));
491            }
492        };
493
494        match tx.try_send(msg) {
495            Ok(()) => {
496                debug!(pid, "message delivered to inbox");
497                Ok(())
498            }
499            Err(mpsc::error::TrySendError::Full(msg)) => {
500                let _rejected_msg = msg;
501                warn!(pid, "inbox full, dead-lettering");
502                #[cfg(feature = "os-patterns")]
503                if let Some(dlq) = self.dead_letter_queue.get() {
504                    dlq.intake(
505                        rejected_msg,
506                        crate::dead_letter::DeadLetterReason::InboxFull { pid },
507                    );
508                }
509                Err(KernelError::Ipc(format!("inbox full for PID {pid}")))
510            }
511            Err(mpsc::error::TrySendError::Closed(msg)) => {
512                let _rejected_msg = msg;
513                warn!(pid, "inbox closed, removing and dead-lettering");
514                self.inboxes.remove(&pid);
515                #[cfg(feature = "os-patterns")]
516                if let Some(dlq) = self.dead_letter_queue.get() {
517                    dlq.intake(
518                        rejected_msg,
519                        crate::dead_letter::DeadLetterReason::AgentExited { pid },
520                    );
521                }
522                Err(KernelError::Ipc(format!("inbox closed for PID {pid}")))
523            }
524        }
525    }
526
527    /// Route a message to a named service via the ServiceRegistry.
528    ///
529    /// Resolves the service name to an owning agent PID, then delivers
530    /// the message to that agent's inbox.
531    async fn route_to_service(
532        &self,
533        from: Pid,
534        service_name: &str,
535        msg: KernelMessage,
536    ) -> KernelResult<()> {
537        let registry = self.service_registry.as_ref().ok_or_else(|| {
538            KernelError::Ipc(format!(
539                "no service registry configured; cannot route to service '{service_name}'"
540            ))
541        })?;
542
543        let target_pid = registry
544            .resolve_target(service_name)
545            .ok_or_else(|| KernelError::Ipc(format!("service not found: '{service_name}'")))?;
546
547        // Check IPC scope
548        self.capability_checker.check_ipc_target(from, target_pid)?;
549
550        self.deliver_to_inbox(target_pid, msg).await
551    }
552
553    /// Send a message with chain-event logging.
554    ///
555    /// This mirrors `KernelIpc::send_checked` but for the A2ARouter:
556    /// every routed message is logged as an `ipc.send` chain event with
557    /// sender, target, payload type, and message ID — forming a
558    /// tamper-evident IPC audit trail in the exochain.
559    ///
560    /// When the `exochain` feature is disabled this is equivalent to
561    /// a plain `send()`.
562    #[cfg(feature = "exochain")]
563    pub async fn send_checked(
564        &self,
565        msg: KernelMessage,
566        chain: Option<&ChainManager>,
567    ) -> KernelResult<()> {
568        // Log the IPC event before delivery so the chain records intent
569        // even if the inbox is full or closed.
570        if let Some(cm) = chain {
571            cm.append(
572                "ipc",
573                "ipc.send",
574                Some(serde_json::json!({
575                    "from": msg.from,
576                    "target": format!("{:?}", msg.target),
577                    "payload_type": msg.payload.type_name(),
578                    "msg_id": msg.id,
579                })),
580            );
581        }
582        self.send(msg).await
583    }
584
585    /// Get the topic router.
586    pub fn topic_router(&self) -> &Arc<TopicRouter> {
587        &self.topic_router
588    }
589
590    /// Get the number of active inboxes.
591    pub fn inbox_count(&self) -> usize {
592        self.inboxes.len()
593    }
594
595    /// Check whether a PID has an inbox.
596    pub fn has_inbox(&self, pid: Pid) -> bool {
597        self.inboxes.contains_key(&pid)
598    }
599
600    /// Send a request and wait for a correlated response with timeout.
601    ///
602    /// The request message is sent normally, but its `id` is registered
603    /// as a pending request. When a response arrives with a matching
604    /// `correlation_id`, it is delivered to the returned future instead
605    /// of the sender's inbox.
606    ///
607    /// # Errors
608    ///
609    /// Returns `KernelError::Timeout` if no response arrives within the
610    /// specified duration, or `KernelError::Ipc` if the response channel
611    /// is closed before a response arrives.
612    pub async fn request(
613        &self,
614        msg: KernelMessage,
615        timeout: Duration,
616    ) -> KernelResult<KernelMessage> {
617        let request_id = msg.id.clone();
618        let (tx, rx) = oneshot::channel();
619
620        // Register pending request before sending so there is no race.
621        self.pending_requests.insert(
622            request_id.clone(),
623            PendingRequest {
624                response_tx: tx,
625                sent_at: Instant::now(),
626            },
627        );
628
629        // Send the message; clean up on failure.
630        if let Err(e) = self.send(msg).await {
631            self.pending_requests.remove(&request_id);
632            return Err(e);
633        }
634
635        // Wait for response with timeout.
636        match tokio::time::timeout(timeout, rx).await {
637            Ok(Ok(response)) => Ok(response),
638            Ok(Err(_)) => {
639                self.pending_requests.remove(&request_id);
640                Err(KernelError::Ipc("response channel closed".into()))
641            }
642            Err(_) => {
643                self.pending_requests.remove(&request_id);
644                Err(KernelError::Timeout {
645                    operation: format!("request {request_id}"),
646                    duration_ms: timeout.as_millis() as u64,
647                })
648            }
649        }
650    }
651
652    /// Try to complete a pending request with a correlated response.
653    ///
654    /// If the message has a `correlation_id` that matches a pending
655    /// request, the response is delivered to the waiting future and
656    /// `true` is returned. Otherwise returns `false`.
657    pub fn try_complete_request(&self, msg: KernelMessage) -> bool {
658        if let Some(ref corr_id) = msg.correlation_id
659            && let Some((_, pending)) = self.pending_requests.remove(corr_id)
660        {
661            let _ = pending.response_tx.send(msg);
662            return true;
663        }
664        false
665    }
666
667    /// Get the number of pending requests.
668    pub fn pending_request_count(&self) -> usize {
669        self.pending_requests.len()
670    }
671}
672
673#[cfg(test)]
674mod tests {
675    use super::*;
676    use crate::capability::AgentCapabilities;
677    use crate::ipc::MessagePayload;
678    use crate::process::{ProcessEntry, ResourceUsage};
679    use tokio_util::sync::CancellationToken;
680
681    fn setup_router(
682        agent_count: usize,
683    ) -> (A2ARouter, Vec<Pid>, Vec<mpsc::Receiver<KernelMessage>>) {
684        let table = Arc::new(ProcessTable::new(64));
685        let mut pids = Vec::new();
686
687        for i in 0..agent_count {
688            let entry = ProcessEntry {
689                pid: 0,
690                agent_id: format!("agent-{i}"),
691                state: ProcessState::Running,
692                capabilities: AgentCapabilities::default(),
693                resource_usage: ResourceUsage::default(),
694                cancel_token: CancellationToken::new(),
695                parent_pid: None,
696            };
697            let pid = table.insert(entry).unwrap();
698            pids.push(pid);
699        }
700
701        let checker = Arc::new(CapabilityChecker::new(table.clone()));
702        let topic_router = Arc::new(TopicRouter::new(table.clone()));
703        let router = A2ARouter::new(table, checker, topic_router);
704
705        let mut receivers = Vec::new();
706        for &pid in &pids {
707            let rx = router.create_inbox(pid);
708            receivers.push(rx);
709        }
710
711        (router, pids, receivers)
712    }
713
714    #[tokio::test]
715    async fn direct_message_delivery() {
716        let (router, pids, mut receivers) = setup_router(2);
717
718        let msg = KernelMessage::text(pids[0], MessageTarget::Process(pids[1]), "hello");
719        router.send(msg).await.unwrap();
720
721        let received = receivers[1].try_recv().unwrap();
722        assert_eq!(received.from, pids[0]);
723        assert!(matches!(
724            received.payload,
725            MessagePayload::Text(ref t) if t == "hello"
726        ));
727    }
728
729    #[tokio::test]
730    async fn message_to_self_works() {
731        let (router, pids, mut receivers) = setup_router(1);
732
733        let msg = KernelMessage::text(pids[0], MessageTarget::Process(pids[0]), "self-msg");
734        router.send(msg).await.unwrap();
735
736        let received = receivers[0].try_recv().unwrap();
737        assert!(matches!(
738            received.payload,
739            MessagePayload::Text(ref t) if t == "self-msg"
740        ));
741    }
742
743    #[tokio::test]
744    async fn broadcast_delivers_to_all_except_sender() {
745        let (router, pids, mut receivers) = setup_router(3);
746
747        let msg = KernelMessage::text(pids[0], MessageTarget::Broadcast, "broadcast");
748        router.send(msg).await.unwrap();
749
750        // Sender should not receive
751        assert!(receivers[0].try_recv().is_err());
752
753        // Others should receive
754        let r1 = receivers[1].try_recv().unwrap();
755        assert!(matches!(
756            r1.payload,
757            MessagePayload::Text(ref t) if t == "broadcast"
758        ));
759        let r2 = receivers[2].try_recv().unwrap();
760        assert!(matches!(
761            r2.payload,
762            MessagePayload::Text(ref t) if t == "broadcast"
763        ));
764    }
765
766    #[tokio::test]
767    async fn topic_publish_delivers_to_subscribers() {
768        let (router, pids, mut receivers) = setup_router(3);
769
770        // Subscribe pids[1] and pids[2] to "build"
771        router.topic_router().subscribe(pids[1], "build");
772        router.topic_router().subscribe(pids[2], "build");
773
774        let msg = KernelMessage::text(pids[0], MessageTarget::Topic("build".into()), "build done");
775        router.send(msg).await.unwrap();
776
777        // Sender not subscribed, should not receive
778        assert!(receivers[0].try_recv().is_err());
779
780        // Subscribers should receive
781        assert!(receivers[1].try_recv().is_ok());
782        assert!(receivers[2].try_recv().is_ok());
783    }
784
785    #[tokio::test]
786    async fn topic_publish_delivers_to_external_stream() {
787        let (router, pids, _receivers) = setup_router(1);
788
789        let (tx, mut rx) = mpsc::channel::<Vec<u8>>(8);
790        let _id = router
791            .topic_router()
792            .subscribe_sink("events", SubscriberSink::ExternalStream(tx));
793
794        let msg = KernelMessage::text(pids[0], MessageTarget::Topic("events".into()), "hi");
795        router.send(msg).await.unwrap();
796
797        // External stream receives a JSON line terminated by '\n'.
798        let line = rx
799            .try_recv()
800            .expect("external stream should receive message");
801        assert!(line.ends_with(b"\n"));
802        let trimmed = std::str::from_utf8(&line[..line.len() - 1]).unwrap();
803        let parsed: serde_json::Value = serde_json::from_str(trimmed).unwrap();
804        assert_eq!(parsed["from"], pids[0]);
805    }
806
807    #[tokio::test]
808    async fn topic_publish_fans_out_to_pid_and_external() {
809        let (router, pids, mut receivers) = setup_router(2);
810        router.topic_router().subscribe(pids[1], "events");
811
812        let (tx, mut rx) = mpsc::channel::<Vec<u8>>(8);
813        router
814            .topic_router()
815            .subscribe_sink("events", SubscriberSink::ExternalStream(tx));
816
817        let msg = KernelMessage::text(pids[0], MessageTarget::Topic("events".into()), "both");
818        router.send(msg).await.unwrap();
819
820        assert!(receivers[1].try_recv().is_ok(), "pid subscriber missed");
821        assert!(rx.try_recv().is_ok(), "external subscriber missed");
822    }
823
824    #[tokio::test]
825    async fn topic_publish_excludes_sender_if_subscribed() {
826        let (router, pids, mut receivers) = setup_router(2);
827
828        // Both subscribe
829        router.topic_router().subscribe(pids[0], "build");
830        router.topic_router().subscribe(pids[1], "build");
831
832        let msg = KernelMessage::text(pids[0], MessageTarget::Topic("build".into()), "done");
833        router.send(msg).await.unwrap();
834
835        // Sender should not receive their own publish
836        assert!(receivers[0].try_recv().is_err());
837        // Other subscriber should receive
838        assert!(receivers[1].try_recv().is_ok());
839    }
840
841    #[tokio::test]
842    async fn send_from_nonexistent_pid_fails() {
843        let (router, _pids, _receivers) = setup_router(1);
844
845        let msg = KernelMessage::text(999, MessageTarget::Process(1), "hello");
846        let result = router.send(msg).await;
847        assert!(result.is_err());
848    }
849
850    #[tokio::test]
851    async fn send_to_pid_without_inbox_fails() {
852        let table = Arc::new(ProcessTable::new(64));
853
854        // Create sender (running)
855        let sender_entry = ProcessEntry {
856            pid: 0,
857            agent_id: "sender".to_owned(),
858            state: ProcessState::Running,
859            capabilities: AgentCapabilities::default(),
860            resource_usage: ResourceUsage::default(),
861            cancel_token: CancellationToken::new(),
862            parent_pid: None,
863        };
864        let sender_pid = table.insert(sender_entry).unwrap();
865
866        // Create target (running) but don't create inbox
867        let target_entry = ProcessEntry {
868            pid: 0,
869            agent_id: "target".to_owned(),
870            state: ProcessState::Running,
871            capabilities: AgentCapabilities::default(),
872            resource_usage: ResourceUsage::default(),
873            cancel_token: CancellationToken::new(),
874            parent_pid: None,
875        };
876        let target_pid = table.insert(target_entry).unwrap();
877
878        let checker = Arc::new(CapabilityChecker::new(table.clone()));
879        let topic_router = Arc::new(TopicRouter::new(table.clone()));
880        let router = A2ARouter::new(table, checker, topic_router);
881
882        // Create inbox only for sender
883        let _rx = router.create_inbox(sender_pid);
884
885        let msg = KernelMessage::text(sender_pid, MessageTarget::Process(target_pid), "hello");
886        let result = router.send(msg).await;
887        assert!(result.is_err());
888    }
889
890    #[tokio::test]
891    async fn ipc_scope_restricts_messaging() {
892        let table = Arc::new(ProcessTable::new(64));
893
894        // Create sender with restricted IPC scope
895        use crate::capability::IpcScope;
896        let sender_entry = ProcessEntry {
897            pid: 0,
898            agent_id: "restricted".to_owned(),
899            state: ProcessState::Running,
900            capabilities: AgentCapabilities {
901                ipc_scope: IpcScope::Restricted(vec![]), // No allowed PIDs
902                ..Default::default()
903            },
904            resource_usage: ResourceUsage::default(),
905            cancel_token: CancellationToken::new(),
906            parent_pid: None,
907        };
908        let sender_pid = table.insert(sender_entry).unwrap();
909
910        // Create target
911        let target_entry = ProcessEntry {
912            pid: 0,
913            agent_id: "target".to_owned(),
914            state: ProcessState::Running,
915            capabilities: AgentCapabilities::default(),
916            resource_usage: ResourceUsage::default(),
917            cancel_token: CancellationToken::new(),
918            parent_pid: None,
919        };
920        let target_pid = table.insert(target_entry).unwrap();
921
922        let checker = Arc::new(CapabilityChecker::new(table.clone()));
923        let topic_router = Arc::new(TopicRouter::new(table.clone()));
924        let router = A2ARouter::new(table, checker, topic_router);
925        let _rx1 = router.create_inbox(sender_pid);
926        let _rx2 = router.create_inbox(target_pid);
927
928        let msg = KernelMessage::text(sender_pid, MessageTarget::Process(target_pid), "blocked");
929        let result = router.send(msg).await;
930        assert!(result.is_err());
931    }
932
933    #[tokio::test]
934    async fn ipc_scope_none_blocks_all() {
935        let table = Arc::new(ProcessTable::new(64));
936
937        use crate::capability::IpcScope;
938        let sender_entry = ProcessEntry {
939            pid: 0,
940            agent_id: "no-ipc".to_owned(),
941            state: ProcessState::Running,
942            capabilities: AgentCapabilities {
943                can_ipc: false,
944                ipc_scope: IpcScope::None,
945                ..Default::default()
946            },
947            resource_usage: ResourceUsage::default(),
948            cancel_token: CancellationToken::new(),
949            parent_pid: None,
950        };
951        let sender_pid = table.insert(sender_entry).unwrap();
952
953        let target_entry = ProcessEntry {
954            pid: 0,
955            agent_id: "target".to_owned(),
956            state: ProcessState::Running,
957            capabilities: AgentCapabilities::default(),
958            resource_usage: ResourceUsage::default(),
959            cancel_token: CancellationToken::new(),
960            parent_pid: None,
961        };
962        let target_pid = table.insert(target_entry).unwrap();
963
964        let checker = Arc::new(CapabilityChecker::new(table.clone()));
965        let topic_router = Arc::new(TopicRouter::new(table.clone()));
966        let router = A2ARouter::new(table, checker, topic_router);
967        let _rx1 = router.create_inbox(sender_pid);
968        let _rx2 = router.create_inbox(target_pid);
969
970        let msg = KernelMessage::text(sender_pid, MessageTarget::Process(target_pid), "blocked");
971        let result = router.send(msg).await;
972        assert!(result.is_err());
973    }
974
975    #[test]
976    fn create_and_remove_inbox() {
977        let table = Arc::new(ProcessTable::new(64));
978        let checker = Arc::new(CapabilityChecker::new(table.clone()));
979        let topic_router = Arc::new(TopicRouter::new(table.clone()));
980        let router = A2ARouter::new(table, checker, topic_router);
981
982        let _rx = router.create_inbox(42);
983        assert!(router.has_inbox(42));
984        assert_eq!(router.inbox_count(), 1);
985
986        router.remove_inbox(42);
987        assert!(!router.has_inbox(42));
988        assert_eq!(router.inbox_count(), 0);
989    }
990
991    #[tokio::test]
992    async fn tool_call_message_routes() {
993        let (router, pids, mut receivers) = setup_router(2);
994
995        let msg = KernelMessage::tool_call(
996            pids[0],
997            MessageTarget::Process(pids[1]),
998            "read_file",
999            serde_json::json!({"path": "/test"}),
1000        );
1001        router.send(msg).await.unwrap();
1002
1003        let received = receivers[1].try_recv().unwrap();
1004        assert!(matches!(
1005            received.payload,
1006            MessagePayload::ToolCall { ref name, .. } if name == "read_file"
1007        ));
1008    }
1009
1010    #[tokio::test]
1011    async fn tool_result_message_routes() {
1012        let (router, pids, mut receivers) = setup_router(2);
1013
1014        let msg = KernelMessage::tool_result(
1015            pids[1],
1016            MessageTarget::Process(pids[0]),
1017            "call-1",
1018            serde_json::json!({"content": "data"}),
1019        );
1020        router.send(msg).await.unwrap();
1021
1022        let received = receivers[0].try_recv().unwrap();
1023        assert!(matches!(
1024            received.payload,
1025            MessagePayload::ToolResult { ref call_id, .. } if call_id == "call-1"
1026        ));
1027    }
1028
1029    #[cfg(feature = "exochain")]
1030    #[tokio::test]
1031    async fn send_checked_logs_chain_event() {
1032        let (router, pids, mut receivers) = setup_router(2);
1033
1034        let chain = crate::chain::ChainManager::new(0, 1000);
1035        let initial_seq = chain.sequence();
1036
1037        let msg = KernelMessage::text(pids[0], MessageTarget::Process(pids[1]), "audited");
1038        router.send_checked(msg, Some(&chain)).await.unwrap();
1039
1040        // Message should still be delivered
1041        let received = receivers[1].try_recv().unwrap();
1042        assert!(matches!(
1043            received.payload,
1044            MessagePayload::Text(ref t) if t == "audited"
1045        ));
1046
1047        // Chain should have a new ipc.send event
1048        assert_eq!(chain.sequence(), initial_seq + 1);
1049        let events = chain.tail(1);
1050        assert_eq!(events[0].kind, "ipc.send");
1051        assert_eq!(events[0].source, "ipc");
1052        let payload = events[0].payload.as_ref().unwrap();
1053        assert_eq!(payload["from"], pids[0]);
1054        assert_eq!(payload["payload_type"], "text");
1055    }
1056
1057    #[cfg(feature = "exochain")]
1058    #[tokio::test]
1059    async fn send_checked_without_chain_still_delivers() {
1060        let (router, pids, mut receivers) = setup_router(2);
1061
1062        let msg = KernelMessage::text(pids[0], MessageTarget::Process(pids[1]), "no-chain");
1063        router.send_checked(msg, None).await.unwrap();
1064
1065        let received = receivers[1].try_recv().unwrap();
1066        assert!(matches!(
1067            received.payload,
1068            MessagePayload::Text(ref t) if t == "no-chain"
1069        ));
1070    }
1071
1072    /// WEFT-150: Restricted IpcScope cannot publish to leaf-push topics.
1073    #[tokio::test]
1074    async fn restricted_scope_cannot_publish_leaf_push_topic() {
1075        use crate::capability::IpcScope;
1076        use crate::process::ProcessTable;
1077
1078        let table = Arc::new(ProcessTable::new(16));
1079        let entry = ProcessEntry {
1080            pid: 0,
1081            agent_id: "browser-like".into(),
1082            state: ProcessState::Running,
1083            capabilities: AgentCapabilities {
1084                ipc_scope: IpcScope::Restricted(vec![]),
1085                ..AgentCapabilities::default()
1086            },
1087            resource_usage: ResourceUsage::default(),
1088            cancel_token: CancellationToken::new(),
1089            parent_pid: None,
1090        };
1091        let pid = table.insert(entry).unwrap();
1092        let checker = Arc::new(CapabilityChecker::new(table.clone()));
1093        let topic_router = Arc::new(TopicRouter::new(table.clone()));
1094        let router = A2ARouter::new(table, checker, topic_router);
1095        router.create_inbox(pid);
1096
1097        let leaf_topic = "mesh.leaf.deadbeef.push";
1098        let msg = KernelMessage::text(pid, MessageTarget::Topic(leaf_topic.into()), "leaf");
1099        let err = router
1100            .send(msg)
1101            .await
1102            .expect_err("Restricted must not publish mesh.leaf.*.push");
1103        assert!(
1104            matches!(err, KernelError::CapabilityDenied { .. }),
1105            "expected CapabilityDenied, got {err:?}"
1106        );
1107
1108        // Public topics remain allowed under Restricted.
1109        let ok_msg =
1110            KernelMessage::text(pid, MessageTarget::Topic("public.events".into()), "ok");
1111        router
1112            .send(ok_msg)
1113            .await
1114            .expect("Restricted may publish public.*");
1115    }
1116
1117    /// WEFT-150: default (IpcScope::All) agents can publish leaf-push topics.
1118    #[tokio::test]
1119    async fn all_scope_can_publish_leaf_push_topic() {
1120        let (router, pids, _receivers) = setup_router(1);
1121        let msg = KernelMessage::text(
1122            pids[0],
1123            MessageTarget::Topic("mesh.leaf.abc123.push".into()),
1124            "scene",
1125        );
1126        router
1127            .send(msg)
1128            .await
1129            .expect("IpcScope::All may publish leaf-push topics");
1130    }
1131
1132    #[tokio::test]
1133    async fn rvf_payload_routes() {
1134        let (router, pids, mut receivers) = setup_router(2);
1135
1136        let msg = KernelMessage::new(
1137            pids[0],
1138            MessageTarget::Process(pids[1]),
1139            MessagePayload::Rvf {
1140                segment_type: 0x40,
1141                data: vec![0xCA, 0xFE],
1142            },
1143        );
1144        router.send(msg).await.unwrap();
1145
1146        let received = receivers[1].try_recv().unwrap();
1147        assert!(matches!(
1148            received.payload,
1149            MessagePayload::Rvf {
1150                segment_type: 0x40,
1151                ..
1152            }
1153        ));
1154    }
1155
1156    #[tokio::test]
1157    async fn closed_inbox_auto_removed() {
1158        let (router, pids, receivers) = setup_router(2);
1159
1160        // Drop receiver for pids[1] — closes the inbox channel
1161        drop(receivers);
1162
1163        assert!(router.has_inbox(pids[1]));
1164
1165        // Sending should fail with "inbox closed" and auto-remove the entry
1166        let msg = KernelMessage::text(pids[0], MessageTarget::Process(pids[1]), "gone");
1167        let result = router.send(msg).await;
1168        assert!(result.is_err());
1169        let err_msg = format!("{}", result.unwrap_err());
1170        assert!(
1171            err_msg.contains("inbox closed"),
1172            "expected 'inbox closed', got: {err_msg}"
1173        );
1174
1175        // Inbox should have been removed automatically
1176        assert!(!router.has_inbox(pids[1]));
1177    }
1178
1179    #[tokio::test]
1180    async fn inbox_overflow_returns_error() {
1181        // Create a router with 2 agents, but use a small inbox
1182        // We can't change DEFAULT_INBOX_CAPACITY, so we fill a normal inbox.
1183        // Instead, create a custom channel with capacity 2 for a targeted test.
1184        let table = Arc::new(ProcessTable::new(64));
1185
1186        let sender_entry = ProcessEntry {
1187            pid: 0,
1188            agent_id: "sender".to_owned(),
1189            state: ProcessState::Running,
1190            capabilities: AgentCapabilities::default(),
1191            resource_usage: ResourceUsage::default(),
1192            cancel_token: CancellationToken::new(),
1193            parent_pid: None,
1194        };
1195        let sender_pid = table.insert(sender_entry).unwrap();
1196
1197        let target_entry = ProcessEntry {
1198            pid: 0,
1199            agent_id: "target".to_owned(),
1200            state: ProcessState::Running,
1201            capabilities: AgentCapabilities::default(),
1202            resource_usage: ResourceUsage::default(),
1203            cancel_token: CancellationToken::new(),
1204            parent_pid: None,
1205        };
1206        let target_pid = table.insert(target_entry).unwrap();
1207
1208        let checker = Arc::new(CapabilityChecker::new(table.clone()));
1209        let topic_router = Arc::new(TopicRouter::new(table.clone()));
1210        let router = A2ARouter::new(table, checker, topic_router);
1211
1212        // Create sender inbox normally
1213        let _rx_sender = router.create_inbox(sender_pid);
1214
1215        // Manually insert a tiny-capacity channel for target (capacity=2)
1216        let (tx, _rx_target) = mpsc::channel(2);
1217        router.inboxes.insert(target_pid, tx);
1218
1219        // Fill it up
1220        let m1 = KernelMessage::text(sender_pid, MessageTarget::Process(target_pid), "msg1");
1221        let m2 = KernelMessage::text(sender_pid, MessageTarget::Process(target_pid), "msg2");
1222        router.send(m1).await.unwrap();
1223        router.send(m2).await.unwrap();
1224
1225        // Third message should fail — inbox full
1226        let m3 = KernelMessage::text(sender_pid, MessageTarget::Process(target_pid), "overflow");
1227        let result = router.send(m3).await;
1228        assert!(result.is_err());
1229        let err_msg = format!("{}", result.unwrap_err());
1230        assert!(
1231            err_msg.contains("inbox full"),
1232            "expected 'inbox full', got: {err_msg}"
1233        );
1234
1235        // Inbox should still exist (not removed on full, only on closed)
1236        assert!(router.has_inbox(target_pid));
1237    }
1238
1239    #[tokio::test]
1240    async fn concurrent_sends_to_same_pid() {
1241        let (router, pids, mut receivers) = setup_router(4);
1242        let router = Arc::new(router);
1243        let target = pids[0];
1244
1245        // 3 senders each send a message to the same target concurrently
1246        let mut handles = Vec::new();
1247        for &sender_pid in &pids[1..] {
1248            let r = Arc::clone(&router);
1249            let msg = KernelMessage::text(
1250                sender_pid,
1251                MessageTarget::Process(target),
1252                format!("from-{sender_pid}"),
1253            );
1254            handles.push(tokio::spawn(async move { r.send(msg).await }));
1255        }
1256
1257        // All sends should succeed
1258        for h in handles {
1259            h.await.unwrap().unwrap();
1260        }
1261
1262        // Target should have received all 3 messages
1263        let mut received = Vec::new();
1264        while let Ok(msg) = receivers[0].try_recv() {
1265            if let MessagePayload::Text(t) = &msg.payload {
1266                received.push(t.clone());
1267            }
1268        }
1269        assert_eq!(received.len(), 3);
1270        // All senders represented (order may vary)
1271        for &sender_pid in &pids[1..] {
1272            assert!(
1273                received.iter().any(|t| t == &format!("from-{sender_pid}")),
1274                "missing message from PID {sender_pid}"
1275            );
1276        }
1277    }
1278
1279    #[tokio::test]
1280    async fn send_from_non_running_process_fails() {
1281        let table = Arc::new(ProcessTable::new(64));
1282
1283        // Create sender in Exited state
1284        let sender_entry = ProcessEntry {
1285            pid: 0,
1286            agent_id: "exited-sender".to_owned(),
1287            state: ProcessState::Exited(0),
1288            capabilities: AgentCapabilities::default(),
1289            resource_usage: ResourceUsage::default(),
1290            cancel_token: CancellationToken::new(),
1291            parent_pid: None,
1292        };
1293        let sender_pid = table.insert(sender_entry).unwrap();
1294
1295        // Create target in Running state
1296        let target_entry = ProcessEntry {
1297            pid: 0,
1298            agent_id: "target".to_owned(),
1299            state: ProcessState::Running,
1300            capabilities: AgentCapabilities::default(),
1301            resource_usage: ResourceUsage::default(),
1302            cancel_token: CancellationToken::new(),
1303            parent_pid: None,
1304        };
1305        let target_pid = table.insert(target_entry).unwrap();
1306
1307        let checker = Arc::new(CapabilityChecker::new(table.clone()));
1308        let topic_router = Arc::new(TopicRouter::new(table.clone()));
1309        let router = A2ARouter::new(table, checker, topic_router);
1310        let _rx1 = router.create_inbox(sender_pid);
1311        let _rx2 = router.create_inbox(target_pid);
1312
1313        let msg = KernelMessage::text(sender_pid, MessageTarget::Process(target_pid), "from-dead");
1314        let result = router.send(msg).await;
1315        assert!(result.is_err(), "send from non-Running process should fail");
1316    }
1317
1318    // ── Service routing tests (K2.1 T3: D19 + D1) ──────────────
1319
1320    fn setup_router_with_registry(
1321        agent_count: usize,
1322    ) -> (
1323        A2ARouter,
1324        Vec<Pid>,
1325        Vec<mpsc::Receiver<KernelMessage>>,
1326        Arc<crate::service::ServiceRegistry>,
1327    ) {
1328        let table = Arc::new(ProcessTable::new(64));
1329        let mut pids = Vec::new();
1330
1331        for i in 0..agent_count {
1332            let entry = ProcessEntry {
1333                pid: 0,
1334                agent_id: format!("agent-{i}"),
1335                state: ProcessState::Running,
1336                capabilities: AgentCapabilities::default(),
1337                resource_usage: ResourceUsage::default(),
1338                cancel_token: CancellationToken::new(),
1339                parent_pid: None,
1340            };
1341            let pid = table.insert(entry).unwrap();
1342            pids.push(pid);
1343        }
1344
1345        let checker = Arc::new(CapabilityChecker::new(table.clone()));
1346        let topic_router = Arc::new(TopicRouter::new(table.clone()));
1347        let registry = Arc::new(crate::service::ServiceRegistry::new());
1348        let router =
1349            A2ARouter::new(table, checker, topic_router).with_service_registry(registry.clone());
1350
1351        let mut receivers = Vec::new();
1352        for &pid in &pids {
1353            let rx = router.create_inbox(pid);
1354            receivers.push(rx);
1355        }
1356
1357        (router, pids, receivers, registry)
1358    }
1359
1360    #[tokio::test]
1361    async fn route_to_service_by_name() {
1362        let (router, pids, mut receivers, registry) = setup_router_with_registry(2);
1363
1364        // Register a service owned by pids[1]
1365        registry
1366            .register_entry(crate::service::ServiceEntry {
1367                name: "auth".into(),
1368                owner_pid: Some(pids[1]),
1369                endpoint: crate::service::ServiceEndpoint::AgentInbox(pids[1]),
1370                audit_level: crate::service::ServiceAuditLevel::Full,
1371                registered_at: chrono::Utc::now(),
1372            })
1373            .unwrap();
1374
1375        let msg = KernelMessage::text(pids[0], MessageTarget::Service("auth".into()), "validate");
1376        router.send(msg).await.unwrap();
1377
1378        let received = receivers[1].try_recv().unwrap();
1379        assert_eq!(received.from, pids[0]);
1380        assert!(matches!(
1381            received.payload,
1382            MessagePayload::Text(ref t) if t == "validate"
1383        ));
1384    }
1385
1386    #[tokio::test]
1387    async fn route_service_method() {
1388        let (router, pids, mut receivers, registry) = setup_router_with_registry(2);
1389
1390        registry
1391            .register_entry(crate::service::ServiceEntry {
1392                name: "auth".into(),
1393                owner_pid: Some(pids[1]),
1394                endpoint: crate::service::ServiceEndpoint::AgentInbox(pids[1]),
1395                audit_level: crate::service::ServiceAuditLevel::Full,
1396                registered_at: chrono::Utc::now(),
1397            })
1398            .unwrap();
1399
1400        let msg = KernelMessage::text(
1401            pids[0],
1402            MessageTarget::ServiceMethod {
1403                service: "auth".into(),
1404                method: "validate_token".into(),
1405            },
1406            "token-123",
1407        );
1408        router.send(msg).await.unwrap();
1409
1410        let received = receivers[1].try_recv().unwrap();
1411        assert_eq!(received.from, pids[0]);
1412        assert!(matches!(
1413            received.target,
1414            MessageTarget::ServiceMethod { ref service, ref method }
1415            if service == "auth" && method == "validate_token"
1416        ));
1417    }
1418
1419    #[tokio::test]
1420    async fn service_not_found_returns_error() {
1421        let (router, pids, _receivers, _registry) = setup_router_with_registry(2);
1422
1423        let msg = KernelMessage::text(
1424            pids[0],
1425            MessageTarget::Service("nonexistent".into()),
1426            "hello",
1427        );
1428        let result = router.send(msg).await;
1429        assert!(result.is_err());
1430        let err_msg = format!("{}", result.unwrap_err());
1431        assert!(
1432            err_msg.contains("service not found"),
1433            "expected 'service not found', got: {err_msg}"
1434        );
1435    }
1436
1437    #[tokio::test]
1438    async fn service_entry_registration() {
1439        let registry = crate::service::ServiceRegistry::new();
1440
1441        let entry = crate::service::ServiceEntry {
1442            name: "cache".into(),
1443            owner_pid: Some(42),
1444            endpoint: crate::service::ServiceEndpoint::AgentInbox(42),
1445            audit_level: crate::service::ServiceAuditLevel::Full,
1446            registered_at: chrono::Utc::now(),
1447        };
1448        registry.register_entry(entry).unwrap();
1449
1450        let retrieved = registry.get_entry("cache").unwrap();
1451        assert_eq!(retrieved.name, "cache");
1452        assert_eq!(retrieved.owner_pid, Some(42));
1453    }
1454
1455    #[tokio::test]
1456    async fn service_entry_with_audit_level() {
1457        let registry = crate::service::ServiceRegistry::new();
1458
1459        let entry = crate::service::ServiceEntry {
1460            name: "metrics".into(),
1461            owner_pid: Some(10),
1462            endpoint: crate::service::ServiceEndpoint::AgentInbox(10),
1463            audit_level: crate::service::ServiceAuditLevel::GateOnly,
1464            registered_at: chrono::Utc::now(),
1465        };
1466        registry.register_entry(entry).unwrap();
1467
1468        let retrieved = registry.get_entry("metrics").unwrap();
1469        assert_eq!(
1470            retrieved.audit_level,
1471            crate::service::ServiceAuditLevel::GateOnly
1472        );
1473    }
1474
1475    #[tokio::test]
1476    async fn resolve_target_finds_owner_pid() {
1477        let registry = crate::service::ServiceRegistry::new();
1478
1479        let entry = crate::service::ServiceEntry {
1480            name: "search".into(),
1481            owner_pid: Some(77),
1482            endpoint: crate::service::ServiceEndpoint::AgentInbox(77),
1483            audit_level: crate::service::ServiceAuditLevel::Full,
1484            registered_at: chrono::Utc::now(),
1485        };
1486        registry.register_entry(entry).unwrap();
1487
1488        assert_eq!(registry.resolve_target("search"), Some(77));
1489        assert_eq!(registry.resolve_target("nonexistent"), None);
1490    }
1491
1492    #[tokio::test]
1493    async fn service_without_registry_returns_error() {
1494        // Router without service registry should fail on Service target
1495        let (router_no_reg, pids, _receivers) = setup_router(2);
1496
1497        let msg = KernelMessage::text(pids[0], MessageTarget::Service("missing".into()), "hello");
1498        let result = router_no_reg.send(msg).await;
1499        assert!(result.is_err());
1500        let err_msg = format!("{}", result.unwrap_err());
1501        assert!(
1502            err_msg.contains("no service registry"),
1503            "expected 'no service registry', got: {err_msg}"
1504        );
1505    }
1506
1507    #[tokio::test]
1508    async fn external_service_no_pid_returns_error() {
1509        let (router, pids, _receivers, registry) = setup_router_with_registry(2);
1510
1511        // Register external service with no owner_pid
1512        registry
1513            .register_entry(crate::service::ServiceEntry {
1514                name: "redis".into(),
1515                owner_pid: None,
1516                endpoint: crate::service::ServiceEndpoint::External {
1517                    url: "redis://localhost:6379".into(),
1518                },
1519                audit_level: crate::service::ServiceAuditLevel::GateOnly,
1520                registered_at: chrono::Utc::now(),
1521            })
1522            .unwrap();
1523
1524        let msg = KernelMessage::text(pids[0], MessageTarget::Service("redis".into()), "ping");
1525        let result = router.send(msg).await;
1526        assert!(
1527            result.is_err(),
1528            "external service with no PID should fail routing"
1529        );
1530    }
1531
1532    // ── Request-response tests ──────────────────────────────────
1533
1534    #[tokio::test]
1535    async fn request_response_completes() {
1536        let (router, pids, mut receivers) = setup_router(2);
1537        let router = Arc::new(router);
1538        let router2 = Arc::clone(&router);
1539
1540        // Spawn a responder that reads from pids[1]'s inbox and replies.
1541        let from_pid = pids[0];
1542        let to_pid = pids[1];
1543        tokio::spawn(async move {
1544            let msg = receivers[1].recv().await.unwrap();
1545            // Build a correlated response back to the sender.
1546            let reply = KernelMessage::with_correlation(
1547                to_pid,
1548                MessageTarget::Process(from_pid),
1549                MessagePayload::Text("pong".into()),
1550                msg.id.clone(),
1551            );
1552            router2.try_complete_request(reply);
1553        });
1554
1555        let request = KernelMessage::text(from_pid, MessageTarget::Process(to_pid), "ping");
1556        let response = router
1557            .request(request, Duration::from_secs(5))
1558            .await
1559            .unwrap();
1560        assert!(matches!(
1561            response.payload,
1562            MessagePayload::Text(ref t) if t == "pong"
1563        ));
1564        assert_eq!(router.pending_request_count(), 0);
1565    }
1566
1567    #[tokio::test]
1568    async fn request_response_timeout() {
1569        let (router, pids, _receivers) = setup_router(2);
1570
1571        let request = KernelMessage::text(pids[0], MessageTarget::Process(pids[1]), "no-reply");
1572        let result = router.request(request, Duration::from_millis(50)).await;
1573        assert!(result.is_err());
1574        let err_msg = format!("{}", result.unwrap_err());
1575        assert!(
1576            err_msg.contains("timeout"),
1577            "expected timeout error, got: {err_msg}"
1578        );
1579        assert_eq!(router.pending_request_count(), 0);
1580    }
1581
1582    #[tokio::test]
1583    async fn try_complete_request_matching() {
1584        let (router, pids, _receivers) = setup_router(2);
1585
1586        // Manually register a pending request.
1587        let (tx, rx) = oneshot::channel();
1588        let request_id = "req-42".to_string();
1589        router.pending_requests.insert(
1590            request_id.clone(),
1591            PendingRequest {
1592                response_tx: tx,
1593                sent_at: Instant::now(),
1594            },
1595        );
1596
1597        // Build a response with matching correlation_id.
1598        let reply = KernelMessage::with_correlation(
1599            pids[1],
1600            MessageTarget::Process(pids[0]),
1601            MessagePayload::Text("reply".into()),
1602            request_id,
1603        );
1604        let completed = router.try_complete_request(reply);
1605        assert!(
1606            completed,
1607            "try_complete_request should return true for matching id"
1608        );
1609
1610        let response = rx.await.unwrap();
1611        assert!(matches!(
1612            response.payload,
1613            MessagePayload::Text(ref t) if t == "reply"
1614        ));
1615    }
1616
1617    #[tokio::test]
1618    async fn try_complete_request_no_match() {
1619        let (router, pids, _receivers) = setup_router(2);
1620
1621        // No pending requests registered.
1622        let reply = KernelMessage::with_correlation(
1623            pids[1],
1624            MessageTarget::Process(pids[0]),
1625            MessagePayload::Text("orphan".into()),
1626            "nonexistent-id".into(),
1627        );
1628        let completed = router.try_complete_request(reply);
1629        assert!(
1630            !completed,
1631            "try_complete_request should return false for non-matching id"
1632        );
1633
1634        // Also test message with no correlation_id.
1635        let plain = KernelMessage::text(pids[1], MessageTarget::Process(pids[0]), "plain");
1636        assert!(!router.try_complete_request(plain));
1637    }
1638
1639    #[tokio::test]
1640    async fn pending_request_count_tracks_correctly() {
1641        let (router, _pids, _receivers) = setup_router(2);
1642        assert_eq!(router.pending_request_count(), 0);
1643
1644        // Insert two pending requests.
1645        let (tx1, _rx1) = oneshot::channel();
1646        let (tx2, _rx2) = oneshot::channel();
1647        router.pending_requests.insert(
1648            "req-a".into(),
1649            PendingRequest {
1650                response_tx: tx1,
1651                sent_at: Instant::now(),
1652            },
1653        );
1654        assert_eq!(router.pending_request_count(), 1);
1655
1656        router.pending_requests.insert(
1657            "req-b".into(),
1658            PendingRequest {
1659                response_tx: tx2,
1660                sent_at: Instant::now(),
1661            },
1662        );
1663        assert_eq!(router.pending_request_count(), 2);
1664
1665        // Complete one via try_complete_request.
1666        let reply = KernelMessage::with_correlation(
1667            1,
1668            MessageTarget::Process(2),
1669            MessagePayload::Text("done".into()),
1670            "req-a".into(),
1671        );
1672        router.try_complete_request(reply);
1673        assert_eq!(router.pending_request_count(), 1);
1674
1675        // Remove the other manually.
1676        router.pending_requests.remove("req-b");
1677        assert_eq!(router.pending_request_count(), 0);
1678    }
1679
1680    // ── C4: Routing-time gate tests ────────────────────────────────
1681
1682    #[cfg(feature = "exochain")]
1683    #[tokio::test]
1684    async fn routing_gate_denies_message() {
1685        struct DenyGate;
1686        impl crate::gate::GateBackend for DenyGate {
1687            fn check(
1688                &self,
1689                _agent: &str,
1690                _action: &str,
1691                _ctx: &serde_json::Value,
1692            ) -> crate::gate::GateDecision {
1693                crate::gate::GateDecision::Deny {
1694                    reason: "blocked by policy".into(),
1695                    receipt: None,
1696                }
1697            }
1698        }
1699
1700        let (router, pids, _receivers) = setup_router(2);
1701        let router = router.with_gate(Arc::new(DenyGate));
1702
1703        let msg = KernelMessage::text(pids[0], MessageTarget::Process(pids[1]), "hello");
1704        let result = router.send(msg).await;
1705        assert!(result.is_err());
1706        let err_msg = format!("{}", result.unwrap_err());
1707        assert!(
1708            err_msg.contains("routing gate denied"),
1709            "expected 'routing gate denied', got: {err_msg}"
1710        );
1711    }
1712
1713    #[cfg(feature = "exochain")]
1714    #[tokio::test]
1715    async fn routing_gate_permits_message() {
1716        struct PermitGate;
1717        impl crate::gate::GateBackend for PermitGate {
1718            fn check(
1719                &self,
1720                _agent: &str,
1721                _action: &str,
1722                _ctx: &serde_json::Value,
1723            ) -> crate::gate::GateDecision {
1724                crate::gate::GateDecision::Permit { token: None }
1725            }
1726        }
1727
1728        let (router, pids, mut receivers) = setup_router(2);
1729        let router = router.with_gate(Arc::new(PermitGate));
1730
1731        let msg = KernelMessage::text(pids[0], MessageTarget::Process(pids[1]), "hello");
1732        router.send(msg).await.unwrap();
1733
1734        let received = receivers[1].try_recv().unwrap();
1735        assert_eq!(received.from, pids[0]);
1736        assert!(matches!(
1737            received.payload,
1738            MessagePayload::Text(ref t) if t == "hello"
1739        ));
1740    }
1741
1742    #[cfg(feature = "exochain")]
1743    #[tokio::test]
1744    async fn routing_gate_defer_still_delivers() {
1745        struct DeferGate;
1746        impl crate::gate::GateBackend for DeferGate {
1747            fn check(
1748                &self,
1749                _agent: &str,
1750                _action: &str,
1751                _ctx: &serde_json::Value,
1752            ) -> crate::gate::GateDecision {
1753                crate::gate::GateDecision::Defer {
1754                    reason: "pending review".into(),
1755                }
1756            }
1757        }
1758
1759        let (router, pids, mut receivers) = setup_router(2);
1760        let router = router.with_gate(Arc::new(DeferGate));
1761
1762        let msg = KernelMessage::text(pids[0], MessageTarget::Process(pids[1]), "deferred");
1763        router.send(msg).await.unwrap(); // should succeed (defer = deliver)
1764        assert!(receivers[1].try_recv().is_ok());
1765    }
1766
1767    // ── W5: Test hardening — additional coverage ─────────────────
1768
1769    #[tokio::test]
1770    async fn multiple_messages_queued_in_order() {
1771        let (router, pids, mut receivers) = setup_router(2);
1772
1773        for i in 0..5 {
1774            let msg =
1775                KernelMessage::text(pids[0], MessageTarget::Process(pids[1]), format!("msg-{i}"));
1776            router.send(msg).await.unwrap();
1777        }
1778
1779        // Messages should arrive in FIFO order
1780        for i in 0..5 {
1781            let received = receivers[1].try_recv().unwrap();
1782            let expected = format!("msg-{i}");
1783            assert!(
1784                matches!(received.payload, MessagePayload::Text(ref t) if t == &expected),
1785                "expected '{expected}' but got {:?}",
1786                received.payload,
1787            );
1788        }
1789    }
1790
1791    #[tokio::test]
1792    async fn unsubscribe_stops_topic_delivery() {
1793        let (router, pids, mut receivers) = setup_router(3);
1794
1795        router.topic_router().subscribe(pids[1], "events");
1796        router.topic_router().subscribe(pids[2], "events");
1797
1798        // Unsubscribe pids[1]
1799        router.topic_router().unsubscribe(pids[1], "events");
1800
1801        let msg = KernelMessage::text(
1802            pids[0],
1803            MessageTarget::Topic("events".into()),
1804            "after-unsub",
1805        );
1806        router.send(msg).await.unwrap();
1807
1808        // pids[1] should NOT receive (unsubscribed)
1809        assert!(
1810            receivers[1].try_recv().is_err(),
1811            "unsubscribed agent should not receive"
1812        );
1813
1814        // pids[2] should still receive
1815        let received = receivers[2].try_recv().unwrap();
1816        assert!(matches!(
1817            received.payload,
1818            MessagePayload::Text(ref t) if t == "after-unsub"
1819        ));
1820    }
1821
1822    #[tokio::test]
1823    async fn publish_to_empty_topic_succeeds() {
1824        let (router, pids, _receivers) = setup_router(2);
1825
1826        // Publish to a topic with no subscribers — should succeed with no error
1827        let msg = KernelMessage::text(
1828            pids[0],
1829            MessageTarget::Topic("empty-topic".into()),
1830            "nobody-listening",
1831        );
1832        let result = router.send(msg).await;
1833        assert!(result.is_ok(), "publish to empty topic should succeed");
1834    }
1835
1836    #[tokio::test]
1837    async fn broadcast_with_zero_other_agents() {
1838        let (router, pids, mut receivers) = setup_router(1);
1839
1840        let msg = KernelMessage::text(pids[0], MessageTarget::Broadcast, "alone");
1841        let result = router.send(msg).await;
1842        assert!(result.is_ok(), "broadcast with only sender should succeed");
1843
1844        // Sender should not receive their own broadcast
1845        assert!(receivers[0].try_recv().is_err());
1846    }
1847
1848    #[tokio::test]
1849    async fn ipc_scope_restricted_allows_listed_pids() {
1850        let table = Arc::new(ProcessTable::new(64));
1851
1852        use crate::capability::IpcScope;
1853        // Create target first so we know its PID for the restricted list
1854        let target_entry = ProcessEntry {
1855            pid: 0,
1856            agent_id: "target".to_owned(),
1857            state: ProcessState::Running,
1858            capabilities: AgentCapabilities::default(),
1859            resource_usage: ResourceUsage::default(),
1860            cancel_token: CancellationToken::new(),
1861            parent_pid: None,
1862        };
1863        let target_pid = table.insert(target_entry).unwrap();
1864
1865        // Create sender with restricted IPC scope that includes the target
1866        let sender_entry = ProcessEntry {
1867            pid: 0,
1868            agent_id: "restricted-sender".to_owned(),
1869            state: ProcessState::Running,
1870            capabilities: AgentCapabilities {
1871                ipc_scope: IpcScope::Restricted(vec![target_pid]),
1872                ..Default::default()
1873            },
1874            resource_usage: ResourceUsage::default(),
1875            cancel_token: CancellationToken::new(),
1876            parent_pid: None,
1877        };
1878        let sender_pid = table.insert(sender_entry).unwrap();
1879
1880        let checker = Arc::new(CapabilityChecker::new(table.clone()));
1881        let topic_router = Arc::new(TopicRouter::new(table.clone()));
1882        let router = A2ARouter::new(table, checker, topic_router);
1883        let _rx_sender = router.create_inbox(sender_pid);
1884        let mut rx_target = router.create_inbox(target_pid);
1885
1886        let msg = KernelMessage::text(sender_pid, MessageTarget::Process(target_pid), "allowed");
1887        let result = router.send(msg).await;
1888        assert!(result.is_ok(), "restricted sender should reach allowed PID");
1889
1890        let received = rx_target.try_recv().unwrap();
1891        assert!(matches!(
1892            received.payload,
1893            MessagePayload::Text(ref t) if t == "allowed"
1894        ));
1895    }
1896
1897    #[tokio::test]
1898    async fn ipc_scope_restricted_blocks_unlisted_pids() {
1899        let table = Arc::new(ProcessTable::new(64));
1900
1901        use crate::capability::IpcScope;
1902        let target_entry = ProcessEntry {
1903            pid: 0,
1904            agent_id: "target".to_owned(),
1905            state: ProcessState::Running,
1906            capabilities: AgentCapabilities::default(),
1907            resource_usage: ResourceUsage::default(),
1908            cancel_token: CancellationToken::new(),
1909            parent_pid: None,
1910        };
1911        let target_pid = table.insert(target_entry).unwrap();
1912
1913        // Restricted to PID 999 (not the target)
1914        let sender_entry = ProcessEntry {
1915            pid: 0,
1916            agent_id: "restricted-sender".to_owned(),
1917            state: ProcessState::Running,
1918            capabilities: AgentCapabilities {
1919                ipc_scope: IpcScope::Restricted(vec![999]),
1920                ..Default::default()
1921            },
1922            resource_usage: ResourceUsage::default(),
1923            cancel_token: CancellationToken::new(),
1924            parent_pid: None,
1925        };
1926        let sender_pid = table.insert(sender_entry).unwrap();
1927
1928        let checker = Arc::new(CapabilityChecker::new(table.clone()));
1929        let topic_router = Arc::new(TopicRouter::new(table.clone()));
1930        let router = A2ARouter::new(table, checker, topic_router);
1931        let _rx_sender = router.create_inbox(sender_pid);
1932        let _rx_target = router.create_inbox(target_pid);
1933
1934        let msg = KernelMessage::text(sender_pid, MessageTarget::Process(target_pid), "blocked");
1935        let result = router.send(msg).await;
1936        assert!(
1937            result.is_err(),
1938            "restricted sender should be blocked from unlisted PID"
1939        );
1940    }
1941
1942    #[test]
1943    fn create_inbox_returns_receiver() {
1944        let table = Arc::new(ProcessTable::new(64));
1945        let checker = Arc::new(CapabilityChecker::new(table.clone()));
1946        let topic_router = Arc::new(TopicRouter::new(table.clone()));
1947        let router = A2ARouter::new(table, checker, topic_router);
1948
1949        let rx = router.create_inbox(10);
1950        assert!(router.has_inbox(10));
1951        assert_eq!(router.inbox_count(), 1);
1952        drop(rx);
1953    }
1954
1955    #[test]
1956    fn create_inbox_replaces_existing() {
1957        let table = Arc::new(ProcessTable::new(64));
1958        let checker = Arc::new(CapabilityChecker::new(table.clone()));
1959        let topic_router = Arc::new(TopicRouter::new(table.clone()));
1960        let router = A2ARouter::new(table, checker, topic_router);
1961
1962        let _rx1 = router.create_inbox(10);
1963        assert_eq!(router.inbox_count(), 1);
1964
1965        // Replace — old receiver is invalidated
1966        let _rx2 = router.create_inbox(10);
1967        assert_eq!(router.inbox_count(), 1);
1968        assert!(router.has_inbox(10));
1969    }
1970
1971    #[test]
1972    fn remove_nonexistent_inbox_is_noop() {
1973        let table = Arc::new(ProcessTable::new(64));
1974        let checker = Arc::new(CapabilityChecker::new(table.clone()));
1975        let topic_router = Arc::new(TopicRouter::new(table.clone()));
1976        let router = A2ARouter::new(table, checker, topic_router);
1977
1978        // Should not panic
1979        router.remove_inbox(999);
1980        assert_eq!(router.inbox_count(), 0);
1981    }
1982
1983    #[test]
1984    fn inbox_count_tracks_multiple_inboxes() {
1985        let table = Arc::new(ProcessTable::new(64));
1986        let checker = Arc::new(CapabilityChecker::new(table.clone()));
1987        let topic_router = Arc::new(TopicRouter::new(table.clone()));
1988        let router = A2ARouter::new(table, checker, topic_router);
1989
1990        let _rx1 = router.create_inbox(1);
1991        let _rx2 = router.create_inbox(2);
1992        let _rx3 = router.create_inbox(3);
1993        assert_eq!(router.inbox_count(), 3);
1994
1995        router.remove_inbox(2);
1996        assert_eq!(router.inbox_count(), 2);
1997        assert!(!router.has_inbox(2));
1998        assert!(router.has_inbox(1));
1999        assert!(router.has_inbox(3));
2000    }
2001
2002    #[tokio::test]
2003    async fn send_to_kernel_target_succeeds() {
2004        let (router, pids, _receivers) = setup_router(1);
2005
2006        let msg = KernelMessage::text(pids[0], MessageTarget::Kernel, "kernel-msg");
2007        let result = router.send(msg).await;
2008        assert!(
2009            result.is_ok(),
2010            "kernel target routing should succeed (even if no-op)"
2011        );
2012    }
2013
2014    #[tokio::test]
2015    async fn send_to_remote_node_fails() {
2016        let (router, pids, _receivers) = setup_router(1);
2017
2018        let msg = KernelMessage::text(
2019            pids[0],
2020            MessageTarget::RemoteNode {
2021                node_id: "remote-1".into(),
2022                target: Box::new(MessageTarget::Process(42)),
2023            },
2024            "remote-msg",
2025        );
2026        let result = router.send(msg).await;
2027        assert!(
2028            result.is_err(),
2029            "remote node routing should fail (not yet implemented)"
2030        );
2031        let err_msg = format!("{}", result.unwrap_err());
2032        assert!(
2033            err_msg.contains("remote routing"),
2034            "expected remote routing error, got: {err_msg}"
2035        );
2036    }
2037
2038    #[tokio::test]
2039    async fn suspended_process_can_still_send() {
2040        let table = Arc::new(ProcessTable::new(64));
2041
2042        let sender_entry = ProcessEntry {
2043            pid: 0,
2044            agent_id: "suspended-sender".to_owned(),
2045            state: ProcessState::Suspended,
2046            capabilities: AgentCapabilities::default(),
2047            resource_usage: ResourceUsage::default(),
2048            cancel_token: CancellationToken::new(),
2049            parent_pid: None,
2050        };
2051        let sender_pid = table.insert(sender_entry).unwrap();
2052
2053        let target_entry = ProcessEntry {
2054            pid: 0,
2055            agent_id: "target".to_owned(),
2056            state: ProcessState::Running,
2057            capabilities: AgentCapabilities::default(),
2058            resource_usage: ResourceUsage::default(),
2059            cancel_token: CancellationToken::new(),
2060            parent_pid: None,
2061        };
2062        let target_pid = table.insert(target_entry).unwrap();
2063
2064        let checker = Arc::new(CapabilityChecker::new(table.clone()));
2065        let topic_router = Arc::new(TopicRouter::new(table.clone()));
2066        let router = A2ARouter::new(table, checker, topic_router);
2067        let _rx_sender = router.create_inbox(sender_pid);
2068        let mut rx_target = router.create_inbox(target_pid);
2069
2070        let msg = KernelMessage::text(
2071            sender_pid,
2072            MessageTarget::Process(target_pid),
2073            "from-suspended",
2074        );
2075        let result = router.send(msg).await;
2076        assert!(
2077            result.is_ok(),
2078            "suspended process should be allowed to send"
2079        );
2080
2081        let received = rx_target.try_recv().unwrap();
2082        assert!(matches!(
2083            received.payload,
2084            MessagePayload::Text(ref t) if t == "from-suspended"
2085        ));
2086    }
2087
2088    #[tokio::test]
2089    async fn topic_multiple_subscribers_receive_same_message() {
2090        let (router, pids, mut receivers) = setup_router(5);
2091
2092        // Subscribe pids[1..5] to the topic
2093        for &pid in &pids[1..] {
2094            router.topic_router().subscribe(pid, "news");
2095        }
2096
2097        let msg = KernelMessage::text(pids[0], MessageTarget::Topic("news".into()), "breaking");
2098        router.send(msg).await.unwrap();
2099
2100        // All subscribers should receive the same payload
2101        for (i, rx) in receivers[1..].iter_mut().enumerate() {
2102            let received = rx.try_recv().unwrap_or_else(|_| {
2103                panic!(
2104                    "subscriber {} (pid {}) should have received message",
2105                    i + 1,
2106                    pids[i + 1]
2107                )
2108            });
2109            assert!(matches!(
2110                received.payload,
2111                MessagePayload::Text(ref t) if t == "breaking"
2112            ));
2113            assert_eq!(received.from, pids[0]);
2114        }
2115    }
2116
2117    #[tokio::test]
2118    async fn broadcast_skips_scope_restricted_targets() {
2119        let table = Arc::new(ProcessTable::new(64));
2120
2121        use crate::capability::IpcScope;
2122        // Sender with restricted scope (empty list = nobody allowed)
2123        let sender_entry = ProcessEntry {
2124            pid: 0,
2125            agent_id: "restricted-broadcaster".to_owned(),
2126            state: ProcessState::Running,
2127            capabilities: AgentCapabilities {
2128                ipc_scope: IpcScope::Restricted(vec![]),
2129                ..Default::default()
2130            },
2131            resource_usage: ResourceUsage::default(),
2132            cancel_token: CancellationToken::new(),
2133            parent_pid: None,
2134        };
2135        let sender_pid = table.insert(sender_entry).unwrap();
2136
2137        let target_entry = ProcessEntry {
2138            pid: 0,
2139            agent_id: "target".to_owned(),
2140            state: ProcessState::Running,
2141            capabilities: AgentCapabilities::default(),
2142            resource_usage: ResourceUsage::default(),
2143            cancel_token: CancellationToken::new(),
2144            parent_pid: None,
2145        };
2146        let target_pid = table.insert(target_entry).unwrap();
2147
2148        let checker = Arc::new(CapabilityChecker::new(table.clone()));
2149        let topic_router = Arc::new(TopicRouter::new(table.clone()));
2150        let router = A2ARouter::new(table, checker, topic_router);
2151        let _rx_sender = router.create_inbox(sender_pid);
2152        let mut rx_target = router.create_inbox(target_pid);
2153
2154        let msg = KernelMessage::text(sender_pid, MessageTarget::Broadcast, "restricted-broadcast");
2155        let result = router.send(msg).await;
2156        assert!(
2157            result.is_ok(),
2158            "broadcast itself should succeed even if all targets are blocked"
2159        );
2160
2161        // Target should NOT receive (scope blocks it)
2162        assert!(
2163            rx_target.try_recv().is_err(),
2164            "restricted broadcast should not reach any target"
2165        );
2166    }
2167
2168    #[test]
2169    fn service_registry_accessor() {
2170        let table = Arc::new(ProcessTable::new(64));
2171        let checker = Arc::new(CapabilityChecker::new(table.clone()));
2172        let topic_router = Arc::new(TopicRouter::new(table.clone()));
2173
2174        let router = A2ARouter::new(table.clone(), checker.clone(), topic_router.clone());
2175        assert!(
2176            router.service_registry().is_none(),
2177            "no registry by default"
2178        );
2179
2180        let registry = Arc::new(crate::service::ServiceRegistry::new());
2181        let router = router.with_service_registry(registry);
2182        assert!(
2183            router.service_registry().is_some(),
2184            "registry should be present after with_service_registry"
2185        );
2186    }
2187
2188    #[test]
2189    fn topic_router_accessor() {
2190        let table = Arc::new(ProcessTable::new(64));
2191        let checker = Arc::new(CapabilityChecker::new(table.clone()));
2192        let topic_router = Arc::new(TopicRouter::new(table.clone()));
2193        let expected = Arc::clone(&topic_router);
2194
2195        let router = A2ARouter::new(table, checker, topic_router);
2196        assert!(Arc::ptr_eq(router.topic_router(), &expected));
2197    }
2198
2199    #[tokio::test]
2200    async fn json_payload_routes() {
2201        let (router, pids, mut receivers) = setup_router(2);
2202
2203        let msg = KernelMessage::new(
2204            pids[0],
2205            MessageTarget::Process(pids[1]),
2206            MessagePayload::Json(serde_json::json!({"key": "value"})),
2207        );
2208        router.send(msg).await.unwrap();
2209
2210        let received = receivers[1].try_recv().unwrap();
2211        assert!(matches!(
2212            received.payload,
2213            MessagePayload::Json(ref v) if v["key"] == "value"
2214        ));
2215    }
2216
2217    #[tokio::test]
2218    async fn signal_payload_routes() {
2219        use crate::ipc::KernelSignal;
2220        let (router, pids, mut receivers) = setup_router(2);
2221
2222        let msg = KernelMessage::signal(
2223            pids[0],
2224            MessageTarget::Process(pids[1]),
2225            KernelSignal::Shutdown,
2226        );
2227        router.send(msg).await.unwrap();
2228
2229        let received = receivers[1].try_recv().unwrap();
2230        assert!(matches!(
2231            received.payload,
2232            MessagePayload::Signal(KernelSignal::Shutdown)
2233        ));
2234    }
2235
2236    #[tokio::test]
2237    async fn request_to_nonexistent_target_cleans_up_pending() {
2238        let table = Arc::new(ProcessTable::new(64));
2239
2240        let sender_entry = ProcessEntry {
2241            pid: 0,
2242            agent_id: "sender".to_owned(),
2243            state: ProcessState::Running,
2244            capabilities: AgentCapabilities::default(),
2245            resource_usage: ResourceUsage::default(),
2246            cancel_token: CancellationToken::new(),
2247            parent_pid: None,
2248        };
2249        let sender_pid = table.insert(sender_entry).unwrap();
2250
2251        // Create target in process table but no inbox
2252        let target_entry = ProcessEntry {
2253            pid: 0,
2254            agent_id: "no-inbox-target".to_owned(),
2255            state: ProcessState::Running,
2256            capabilities: AgentCapabilities::default(),
2257            resource_usage: ResourceUsage::default(),
2258            cancel_token: CancellationToken::new(),
2259            parent_pid: None,
2260        };
2261        let target_pid = table.insert(target_entry).unwrap();
2262
2263        let checker = Arc::new(CapabilityChecker::new(table.clone()));
2264        let topic_router = Arc::new(TopicRouter::new(table.clone()));
2265        let router = A2ARouter::new(table, checker, topic_router);
2266        let _rx_sender = router.create_inbox(sender_pid);
2267        // Intentionally no inbox for target
2268
2269        let msg = KernelMessage::text(sender_pid, MessageTarget::Process(target_pid), "request");
2270        let result = router.request(msg, Duration::from_millis(100)).await;
2271        assert!(result.is_err(), "request to PID without inbox should fail");
2272        assert_eq!(
2273            router.pending_request_count(),
2274            0,
2275            "pending request should be cleaned up on send failure"
2276        );
2277    }
2278
2279    #[tokio::test]
2280    async fn service_method_not_found_returns_error() {
2281        let (router, pids, _receivers, _registry) = setup_router_with_registry(2);
2282
2283        let msg = KernelMessage::text(
2284            pids[0],
2285            MessageTarget::ServiceMethod {
2286                service: "nonexistent-svc".into(),
2287                method: "do_thing".into(),
2288            },
2289            "call",
2290        );
2291        let result = router.send(msg).await;
2292        assert!(result.is_err());
2293        let err_msg = format!("{}", result.unwrap_err());
2294        assert!(
2295            err_msg.contains("service not found"),
2296            "expected 'service not found' for missing ServiceMethod target, got: {err_msg}"
2297        );
2298    }
2299}