Skip to main content

clawft_kernel/
service.rs

1//! System service registry and lifecycle management.
2//!
3//! The [`ServiceRegistry`] manages named services that implement the
4//! [`SystemService`] trait, providing start/stop lifecycle and health
5//! check aggregation.
6
7use std::sync::Arc;
8
9use async_trait::async_trait;
10use dashmap::DashMap;
11use serde::{Deserialize, Serialize};
12use tracing::{info, warn};
13
14use chrono::{DateTime, Utc};
15
16use crate::health::HealthStatus;
17use crate::process::Pid;
18
19/// Type of system service.
20#[non_exhaustive]
21#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
22pub enum ServiceType {
23    /// Core kernel service (message bus, process table, etc.).
24    Core,
25    /// Plugin-provided service.
26    Plugin,
27    /// Cron/scheduler service.
28    Cron,
29    /// API/HTTP service.
30    Api,
31    /// Custom service with a user-defined label.
32    Custom(String),
33}
34
35impl std::fmt::Display for ServiceType {
36    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37        match self {
38            ServiceType::Core => write!(f, "core"),
39            ServiceType::Plugin => write!(f, "plugin"),
40            ServiceType::Cron => write!(f, "cron"),
41            ServiceType::Api => write!(f, "api"),
42            ServiceType::Custom(s) => write!(f, "custom({s})"),
43        }
44    }
45}
46
47// ── Service identity model (D1, D9, D19) ───────────────────────
48
49/// How to reach a service at runtime.
50///
51/// A service can be backed by an in-kernel agent (most common),
52/// an external system (Redis, HTTP endpoint), or a container.
53#[non_exhaustive]
54#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
55pub enum ServiceEndpoint {
56    /// Backed by an in-kernel agent (route to its inbox).
57    AgentInbox(Pid),
58    /// Backed by an external system (K3/K4 ServiceApi adapter required).
59    External {
60        /// URL or connection string for the external system.
61        url: String,
62    },
63    /// Backed by a managed container (K4).
64    Container {
65        /// Container identifier.
66        id: String,
67    },
68}
69
70/// Audit level for service call witnessing (D9).
71///
72/// Controls how much of a service's activity is recorded in the
73/// ExoChain. The default is `Full` -- every call is witnessed.
74#[non_exhaustive]
75#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
76pub enum ServiceAuditLevel {
77    /// Witness every call (default, per D9).
78    #[default]
79    Full,
80    /// Only log governance gate decisions (opt-out for high-frequency services).
81    GateOnly,
82}
83
84/// First-class service identity in the registry (D1).
85///
86/// A `ServiceEntry` is metadata about a service -- who owns it, how
87/// to reach it, and how deeply to audit it. It lives alongside (not
88/// replacing) the `SystemService` trait implementations.
89#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct ServiceEntry {
91    /// Service name (unique within the registry).
92    pub name: String,
93    /// PID of the agent that owns this service (`None` for external services).
94    pub owner_pid: Option<Pid>,
95    /// How to reach the service at runtime.
96    pub endpoint: ServiceEndpoint,
97    /// Audit depth for ExoChain witnessing.
98    pub audit_level: ServiceAuditLevel,
99    /// When this entry was registered.
100    pub registered_at: DateTime<Utc>,
101}
102
103/// A system service managed by the kernel.
104///
105/// Services are started during boot and stopped during shutdown.
106/// Each service provides a health check for monitoring.
107#[async_trait]
108pub trait SystemService: Send + Sync {
109    /// Human-readable service name.
110    fn name(&self) -> &str;
111
112    /// Service type category.
113    fn service_type(&self) -> ServiceType;
114
115    /// Start the service.
116    async fn start(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
117
118    /// Stop the service.
119    async fn stop(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
120
121    /// Perform a health check.
122    async fn health_check(&self) -> HealthStatus;
123
124    /// Optional operator-facing detail (metrics, endpoints, …).
125    ///
126    /// Surfaced next to health in `kernel.services` / `weft status`
127    /// without changing the Healthy/Degraded/Unhealthy classification.
128    /// Default: `None` (no extra detail). WEFT-333.
129    fn health_detail(&self) -> Option<String> {
130        None
131    }
132
133    /// Liveness probe (K2b-G2, os-patterns).
134    ///
135    /// Returns whether the service process is alive. Default implementation
136    /// returns `Live` for backward compatibility.
137    #[cfg(feature = "os-patterns")]
138    async fn liveness_check(&self) -> crate::health::ProbeResult {
139        crate::health::ProbeResult::Live
140    }
141
142    /// Readiness probe (K2b-G2, os-patterns).
143    ///
144    /// Returns whether the service is ready to accept traffic. Default
145    /// implementation returns `Ready` for backward compatibility.
146    #[cfg(feature = "os-patterns")]
147    async fn readiness_check(&self) -> crate::health::ProbeResult {
148        crate::health::ProbeResult::Ready
149    }
150}
151
152/// Registry of system services with lifecycle management.
153///
154/// Uses [`DashMap`] for concurrent access from multiple kernel
155/// subsystems. Maintains two maps:
156///
157/// - `services`: `SystemService` trait object implementations (existing)
158/// - `entries`: `ServiceEntry` metadata for service identity (D1, K2.1)
159///
160/// A service can have metadata before it has a running implementation
161/// (useful for external services), and vice versa.
162pub struct ServiceRegistry {
163    services: DashMap<String, Arc<dyn SystemService>>,
164    entries: DashMap<String, ServiceEntry>,
165}
166
167impl ServiceRegistry {
168    /// Create a new, empty service registry.
169    pub fn new() -> Self {
170        Self {
171            services: DashMap::new(),
172            entries: DashMap::new(),
173        }
174    }
175
176    /// Register a service.
177    ///
178    /// Returns an error if a service with the same name is already
179    /// registered.
180    pub fn register(
181        &self,
182        service: Arc<dyn SystemService>,
183    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
184        let name = service.name().to_owned();
185        if self.services.contains_key(&name) {
186            return Err(format!("service already registered: {name}").into());
187        }
188        info!(service = %name, "registering service");
189        self.services.insert(name, service);
190        Ok(())
191    }
192
193    /// Unregister a service by name.
194    pub fn unregister(&self, name: &str) -> Option<Arc<dyn SystemService>> {
195        self.services.remove(name).map(|(_, s)| s)
196    }
197
198    /// Get a service by name.
199    pub fn get(&self, name: &str) -> Option<Arc<dyn SystemService>> {
200        self.services.get(name).map(|s| s.value().clone())
201    }
202
203    /// List all registered services with their types.
204    pub fn list(&self) -> Vec<(String, ServiceType)> {
205        self.services
206            .iter()
207            .map(|entry| (entry.key().clone(), entry.value().service_type()))
208            .collect()
209    }
210
211    /// Start all registered services.
212    ///
213    /// Individual service failures are logged as warnings but do not
214    /// prevent other services from starting.
215    pub async fn start_all(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
216        for entry in self.services.iter() {
217            let name = entry.key().clone();
218            info!(service = %name, "starting service");
219            if let Err(e) = entry.value().start().await {
220                warn!(service = %name, error = %e, "service failed to start");
221            }
222        }
223        Ok(())
224    }
225
226    /// Stop all registered services.
227    ///
228    /// Individual service failures are logged as warnings.
229    pub async fn stop_all(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
230        for entry in self.services.iter() {
231            let name = entry.key().clone();
232            info!(service = %name, "stopping service");
233            if let Err(e) = entry.value().stop().await {
234                warn!(service = %name, error = %e, "service failed to stop");
235            }
236        }
237        Ok(())
238    }
239
240    /// Return a snapshot of all services as a `Vec`.
241    ///
242    /// This copies all `(name, Arc<dyn SystemService>)` pairs out of
243    /// the `DashMap`, so the returned collection owns no DashMap refs
244    /// and is safe to hold across await points and send across threads.
245    pub fn snapshot(&self) -> Vec<(String, Arc<dyn SystemService>)> {
246        self.services
247            .iter()
248            .map(|entry| (entry.key().clone(), entry.value().clone()))
249            .collect()
250    }
251
252    /// Run health checks on all registered services.
253    pub async fn health_all(&self) -> Vec<(String, HealthStatus)> {
254        let mut results = Vec::new();
255        for entry in self.services.iter() {
256            let name = entry.key().clone();
257            let status = entry.value().health_check().await;
258            results.push((name, status));
259        }
260        results
261    }
262
263    // ── ServiceEntry metadata (D1, K2.1) ──────────────────────────
264
265    /// Register a service entry (metadata, not a running implementation).
266    ///
267    /// Returns an error if an entry with the same name already exists.
268    pub fn register_entry(
269        &self,
270        entry: ServiceEntry,
271    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
272        let name = entry.name.clone();
273        if self.entries.contains_key(&name) {
274            return Err(format!("service entry already registered: {name}").into());
275        }
276        info!(service = %name, "registering service entry");
277        self.entries.insert(name, entry);
278        Ok(())
279    }
280
281    /// Get a service entry by name.
282    pub fn get_entry(&self, name: &str) -> Option<ServiceEntry> {
283        self.entries.get(name).map(|e| e.value().clone())
284    }
285
286    /// Resolve a service name to its owning agent PID.
287    ///
288    /// Returns `None` if the service is not registered or has no
289    /// `owner_pid` (e.g. external services).
290    pub fn resolve_target(&self, name: &str) -> Option<Pid> {
291        self.entries.get(name).and_then(|e| e.value().owner_pid)
292    }
293
294    /// List all registered service entries.
295    pub fn list_entries(&self) -> Vec<ServiceEntry> {
296        self.entries.iter().map(|e| e.value().clone()).collect()
297    }
298
299    /// Remove a service entry by name.
300    pub fn unregister_entry(&self, name: &str) -> Option<ServiceEntry> {
301        self.entries.remove(name).map(|(_, e)| e)
302    }
303
304    /// Register a service and create a resource tree node + chain event.
305    ///
306    /// When the exochain feature is enabled and a tree manager is provided,
307    /// creates a node at `/kernel/services/{name}` in the resource tree
308    /// and appends a corresponding chain event via `TreeManager`.
309    #[cfg(feature = "exochain")]
310    pub fn register_with_tree(
311        &self,
312        service: Arc<dyn SystemService>,
313        tree_manager: &crate::tree_manager::TreeManager,
314    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
315        let name = service.name().to_owned();
316        self.register(service)?;
317
318        // Create tree node + chain event through the unified TreeManager path
319        if let Err(e) = tree_manager.register_service(&name) {
320            tracing::debug!(service = %name, error = %e, "failed to register service in tree");
321        }
322
323        Ok(())
324    }
325
326    /// Get the number of registered services.
327    pub fn len(&self) -> usize {
328        self.services.len()
329    }
330
331    /// Check whether the registry is empty.
332    pub fn is_empty(&self) -> bool {
333        self.services.is_empty()
334    }
335
336    /// Register a service and automatically create a chain-anchored contract (C3).
337    ///
338    /// This is the recommended registration path for K4+ services that want
339    /// immutable API contracts stored on the ExoChain. It combines service
340    /// registration, contract creation, and chain logging in one call.
341    #[cfg(feature = "exochain")]
342    pub fn register_with_contract(
343        &self,
344        service: Arc<dyn SystemService>,
345        methods: Vec<String>,
346        chain: &crate::chain::ChainManager,
347    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
348        let name = service.name().to_owned();
349        let stype = service.service_type().to_string();
350
351        // Register the service first
352        self.register(service)?;
353
354        // Build canonical contract content and hash it
355        let contract_content = serde_json::json!({
356            "service": &name,
357            "type": &stype,
358            "methods": &methods,
359        });
360        let content_hash = {
361            use sha2::{Digest, Sha256};
362            let bytes = serde_json::to_string(&contract_content).unwrap_or_default();
363            format!("{:x}", Sha256::digest(bytes.as_bytes()))
364        };
365
366        let contract = ServiceContract {
367            service_name: name,
368            version: "1.0.0".into(),
369            methods,
370            content_hash,
371        };
372
373        self.register_contract(&contract, chain)?;
374
375        Ok(())
376    }
377
378    /// Register a service contract and log it to the chain (K2 C3, K4 G2).
379    ///
380    /// A service contract is a versioned interface declaration that is
381    /// anchored in the ExoChain for immutability. Once a contract version
382    /// is registered, it cannot be changed — only superseded by a new version.
383    #[cfg(feature = "exochain")]
384    pub fn register_contract(
385        &self,
386        contract: &ServiceContract,
387        chain: &crate::chain::ChainManager,
388    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
389        info!(
390            service = %contract.service_name,
391            version = %contract.version,
392            "registering service contract on chain"
393        );
394        chain.append(
395            &contract.service_name,
396            "service.contract.register",
397            Some(serde_json::json!({
398                "service": contract.service_name,
399                "version": contract.version,
400                "methods": contract.methods,
401                "hash": contract.content_hash,
402            })),
403        );
404        Ok(())
405    }
406}
407
408// ---------------------------------------------------------------------------
409// K4 G2: Service contracts (K2 C3)
410// ---------------------------------------------------------------------------
411
412/// A versioned service contract anchored in the ExoChain.
413///
414/// Contracts declare the interface a service exposes. Once registered
415/// on-chain, a contract version is immutable.
416#[derive(Debug, Clone, Serialize, Deserialize)]
417pub struct ServiceContract {
418    /// Name of the service this contract belongs to.
419    pub service_name: String,
420    /// Semantic version string (e.g. "1.0.0").
421    pub version: String,
422    /// Method names exposed by this contract version.
423    pub methods: Vec<String>,
424    /// SHAKE-256 hash of the canonical contract content.
425    pub content_hash: String,
426}
427
428impl Default for ServiceRegistry {
429    fn default() -> Self {
430        Self::new()
431    }
432}
433
434// ── ServiceApi trait (K3 C2) ─────────────────────────────────────
435
436/// Internal API surface for protocol adapters (Shell, MCP, HTTP).
437///
438/// Protocol adapters bind to this trait to invoke kernel services
439/// through a uniform interface. The kernel provides a concrete
440/// implementation backed by the ServiceRegistry + A2ARouter.
441#[async_trait]
442pub trait ServiceApi: Send + Sync {
443    /// Call a method on a named service.
444    async fn call(
445        &self,
446        service: &str,
447        method: &str,
448        params: serde_json::Value,
449    ) -> Result<serde_json::Value, Box<dyn std::error::Error + Send + Sync>>;
450
451    /// List available services.
452    async fn list_services(&self) -> Vec<ServiceInfo>;
453
454    /// Get service health.
455    async fn health(
456        &self,
457        service: &str,
458    ) -> Result<HealthStatus, Box<dyn std::error::Error + Send + Sync>>;
459}
460
461/// Service info returned by [`ServiceApi::list_services`].
462#[derive(Debug, Clone, Serialize, Deserialize)]
463pub struct ServiceInfo {
464    /// Service name.
465    pub name: String,
466    /// Service type label (e.g. "core", "plugin").
467    pub service_type: String,
468    /// Whether the service is currently healthy.
469    pub healthy: bool,
470}
471
472/// Shell protocol adapter -- dispatches shell commands through [`ServiceApi`].
473pub struct ShellAdapter {
474    api: Arc<dyn ServiceApi>,
475}
476
477impl ShellAdapter {
478    /// Create a new shell adapter bound to the given service API.
479    pub fn new(api: Arc<dyn ServiceApi>) -> Self {
480        Self { api }
481    }
482
483    /// Execute a shell-style command string through the service API.
484    ///
485    /// Parses `"service.method arg1 arg2"` format into a
486    /// [`ServiceApi::call`].
487    pub async fn execute(
488        &self,
489        command: &str,
490    ) -> Result<serde_json::Value, Box<dyn std::error::Error + Send + Sync>> {
491        let parts: Vec<&str> = command.splitn(2, ' ').collect();
492        let (service_method, args_str) = match parts.as_slice() {
493            [sm] => (*sm, ""),
494            [sm, args] => (*sm, *args),
495            _ => return Err("empty command".into()),
496        };
497
498        let (service, method) = service_method
499            .split_once('.')
500            .ok_or_else(|| format!("expected 'service.method', got '{service_method}'"))?;
501
502        let params = if args_str.is_empty() {
503            serde_json::Value::Null
504        } else if args_str.starts_with('{') || args_str.starts_with('[') {
505            serde_json::from_str(args_str)?
506        } else {
507            serde_json::json!({"args": args_str})
508        };
509
510        self.api.call(service, method, params).await
511    }
512}
513
514/// MCP protocol adapter -- dispatches MCP tool calls through [`ServiceApi`].
515pub struct McpAdapter {
516    api: Arc<dyn ServiceApi>,
517}
518
519impl McpAdapter {
520    /// Create a new MCP adapter bound to the given service API.
521    pub fn new(api: Arc<dyn ServiceApi>) -> Self {
522        Self { api }
523    }
524
525    /// Handle an MCP `tool_call` by routing through the service API.
526    ///
527    /// MCP tool names map to `service.method` via either underscore or
528    /// dot separator (e.g. `"kernel_status"` -> `("kernel", "status")`).
529    pub async fn handle_tool_call(
530        &self,
531        tool_name: &str,
532        arguments: serde_json::Value,
533    ) -> Result<serde_json::Value, Box<dyn std::error::Error + Send + Sync>> {
534        let (service, method) = tool_name
535            .split_once('_')
536            .or_else(|| tool_name.split_once('.'))
537            .ok_or_else(|| format!("invalid tool name format: {tool_name}"))?;
538
539        self.api.call(service, method, arguments).await
540    }
541
542    /// List available tools (mapped from services).
543    pub async fn list_tools(&self) -> Vec<serde_json::Value> {
544        let services = self.api.list_services().await;
545        services
546            .into_iter()
547            .map(|s| {
548                serde_json::json!({
549                    "name": s.name,
550                    "description": format!("WeftOS {} service", s.service_type),
551                })
552            })
553            .collect()
554    }
555}
556
557// ── Concrete ServiceApi backed by ServiceRegistry ──────────────────
558
559/// Concrete [`ServiceApi`] implementation backed by a [`ServiceRegistry`].
560///
561/// Routes `call()` requests to the registered [`SystemService`] by name,
562/// maps `list_services()` to the registry snapshot, and delegates
563/// `health()` to each service's health check.
564///
565/// This is the production implementation that protocol adapters
566/// (Shell, MCP, HTTP) hold via `Arc<dyn ServiceApi>`.
567pub struct KernelServiceApi {
568    registry: Arc<ServiceRegistry>,
569}
570
571impl KernelServiceApi {
572    /// Create a new kernel service API backed by the given registry.
573    pub fn new(registry: Arc<ServiceRegistry>) -> Self {
574        Self { registry }
575    }
576
577    /// Get a reference to the underlying registry.
578    pub fn registry(&self) -> &ServiceRegistry {
579        &self.registry
580    }
581}
582
583#[async_trait]
584impl ServiceApi for KernelServiceApi {
585    async fn call(
586        &self,
587        service: &str,
588        method: &str,
589        params: serde_json::Value,
590    ) -> Result<serde_json::Value, Box<dyn std::error::Error + Send + Sync>> {
591        // Verify the service exists in the registry.
592        let _svc = self
593            .registry
594            .get(service)
595            .ok_or_else(|| format!("service not found: {service}"))?;
596
597        // Build a JSON envelope describing the call. In a full integration
598        // this would dispatch to the service's message inbox or method table.
599        // For now, return an acknowledgement so the protocol wire path is
600        // exercisable end-to-end.
601        Ok(serde_json::json!({
602            "service": service,
603            "method": method,
604            "params": params,
605            "status": "dispatched",
606        }))
607    }
608
609    async fn list_services(&self) -> Vec<ServiceInfo> {
610        // Use snapshot() to avoid holding DashMap refs across the await.
611        let snapshot = self.registry.snapshot();
612        let mut infos = Vec::with_capacity(snapshot.len());
613        for (name, svc) in &snapshot {
614            let health = svc.health_check().await;
615            infos.push(ServiceInfo {
616                name: name.clone(),
617                service_type: svc.service_type().to_string(),
618                healthy: health == HealthStatus::Healthy,
619            });
620        }
621        infos
622    }
623
624    async fn health(
625        &self,
626        service: &str,
627    ) -> Result<HealthStatus, Box<dyn std::error::Error + Send + Sync>> {
628        let svc = self
629            .registry
630            .get(service)
631            .ok_or_else(|| format!("service not found: {service}"))?;
632        Ok(svc.health_check().await)
633    }
634}
635
636// ── Registry trait implementation ────────────────────────────────────
637
638impl clawft_types::Registry for ServiceRegistry {
639    type Value = Arc<dyn SystemService>;
640
641    fn get(&self, key: &str) -> Option<Self::Value> {
642        self.services.get(key).map(|s| s.value().clone())
643    }
644
645    fn list_keys(&self) -> Vec<String> {
646        self.services.iter().map(|e| e.key().clone()).collect()
647    }
648
649    fn contains(&self, key: &str) -> bool {
650        self.services.contains_key(key)
651    }
652
653    fn count(&self) -> usize {
654        self.services.len()
655    }
656}
657
658#[cfg(test)]
659mod tests {
660    use super::*;
661
662    /// A mock service for testing.
663    struct MockService {
664        name: String,
665        service_type: ServiceType,
666    }
667
668    impl MockService {
669        fn new(name: &str, stype: ServiceType) -> Self {
670            Self {
671                name: name.to_owned(),
672                service_type: stype,
673            }
674        }
675    }
676
677    #[async_trait]
678    impl SystemService for MockService {
679        fn name(&self) -> &str {
680            &self.name
681        }
682
683        fn service_type(&self) -> ServiceType {
684            self.service_type.clone()
685        }
686
687        async fn start(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
688            Ok(())
689        }
690
691        async fn stop(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
692            Ok(())
693        }
694
695        async fn health_check(&self) -> HealthStatus {
696            HealthStatus::Healthy
697        }
698    }
699
700    #[test]
701    fn register_and_get() {
702        let registry = ServiceRegistry::new();
703        let svc = Arc::new(MockService::new("test-svc", ServiceType::Core));
704        registry.register(svc).unwrap();
705
706        let retrieved = registry.get("test-svc");
707        assert!(retrieved.is_some());
708        assert_eq!(retrieved.unwrap().name(), "test-svc");
709    }
710
711    #[test]
712    fn register_duplicate_fails() {
713        let registry = ServiceRegistry::new();
714        let svc1 = Arc::new(MockService::new("dup-svc", ServiceType::Core));
715        let svc2 = Arc::new(MockService::new("dup-svc", ServiceType::Plugin));
716
717        registry.register(svc1).unwrap();
718        let result = registry.register(svc2);
719        assert!(result.is_err());
720    }
721
722    #[test]
723    fn unregister() {
724        let registry = ServiceRegistry::new();
725        let svc = Arc::new(MockService::new("rm-svc", ServiceType::Core));
726        registry.register(svc).unwrap();
727
728        let removed = registry.unregister("rm-svc");
729        assert!(removed.is_some());
730        assert!(registry.get("rm-svc").is_none());
731    }
732
733    #[test]
734    fn list_services() {
735        let registry = ServiceRegistry::new();
736        registry
737            .register(Arc::new(MockService::new("svc-a", ServiceType::Core)))
738            .unwrap();
739        registry
740            .register(Arc::new(MockService::new("svc-b", ServiceType::Cron)))
741            .unwrap();
742
743        let list = registry.list();
744        assert_eq!(list.len(), 2);
745    }
746
747    #[tokio::test]
748    async fn start_and_stop_all() {
749        let registry = ServiceRegistry::new();
750        registry
751            .register(Arc::new(MockService::new("svc-1", ServiceType::Core)))
752            .unwrap();
753        registry
754            .register(Arc::new(MockService::new("svc-2", ServiceType::Plugin)))
755            .unwrap();
756
757        registry.start_all().await.unwrap();
758        registry.stop_all().await.unwrap();
759    }
760
761    #[tokio::test]
762    async fn health_all() {
763        let registry = ServiceRegistry::new();
764        registry
765            .register(Arc::new(MockService::new("svc-1", ServiceType::Core)))
766            .unwrap();
767        registry
768            .register(Arc::new(MockService::new("svc-2", ServiceType::Plugin)))
769            .unwrap();
770
771        let health = registry.health_all().await;
772        assert_eq!(health.len(), 2);
773        for (_, status) in &health {
774            assert_eq!(*status, HealthStatus::Healthy);
775        }
776    }
777
778    #[test]
779    fn len_and_is_empty() {
780        let registry = ServiceRegistry::new();
781        assert!(registry.is_empty());
782        assert_eq!(registry.len(), 0);
783
784        registry
785            .register(Arc::new(MockService::new("svc", ServiceType::Core)))
786            .unwrap();
787        assert!(!registry.is_empty());
788        assert_eq!(registry.len(), 1);
789    }
790
791    #[test]
792    fn service_type_display() {
793        assert_eq!(ServiceType::Core.to_string(), "core");
794        assert_eq!(ServiceType::Plugin.to_string(), "plugin");
795        assert_eq!(ServiceType::Cron.to_string(), "cron");
796        assert_eq!(ServiceType::Api.to_string(), "api");
797        assert_eq!(
798            ServiceType::Custom("webhook".into()).to_string(),
799            "custom(webhook)"
800        );
801    }
802
803    // ── ServiceEntry tests (K2.1 T3: D1) ───────────────────────
804
805    #[test]
806    fn register_and_get_entry() {
807        let registry = ServiceRegistry::new();
808        let entry = ServiceEntry {
809            name: "auth".into(),
810            owner_pid: Some(42),
811            endpoint: ServiceEndpoint::AgentInbox(42),
812            audit_level: ServiceAuditLevel::Full,
813            registered_at: Utc::now(),
814        };
815        registry.register_entry(entry).unwrap();
816
817        let retrieved = registry.get_entry("auth").unwrap();
818        assert_eq!(retrieved.name, "auth");
819        assert_eq!(retrieved.owner_pid, Some(42));
820    }
821
822    #[test]
823    fn register_entry_duplicate_fails() {
824        let registry = ServiceRegistry::new();
825        let entry1 = ServiceEntry {
826            name: "dup".into(),
827            owner_pid: Some(1),
828            endpoint: ServiceEndpoint::AgentInbox(1),
829            audit_level: ServiceAuditLevel::Full,
830            registered_at: Utc::now(),
831        };
832        let entry2 = ServiceEntry {
833            name: "dup".into(),
834            owner_pid: Some(2),
835            endpoint: ServiceEndpoint::AgentInbox(2),
836            audit_level: ServiceAuditLevel::Full,
837            registered_at: Utc::now(),
838        };
839        registry.register_entry(entry1).unwrap();
840        let result = registry.register_entry(entry2);
841        assert!(result.is_err());
842    }
843
844    #[test]
845    fn resolve_target_returns_owner_pid() {
846        let registry = ServiceRegistry::new();
847        let entry = ServiceEntry {
848            name: "cache".into(),
849            owner_pid: Some(99),
850            endpoint: ServiceEndpoint::AgentInbox(99),
851            audit_level: ServiceAuditLevel::Full,
852            registered_at: Utc::now(),
853        };
854        registry.register_entry(entry).unwrap();
855
856        assert_eq!(registry.resolve_target("cache"), Some(99));
857        assert_eq!(registry.resolve_target("missing"), None);
858    }
859
860    #[test]
861    fn resolve_target_external_returns_none() {
862        let registry = ServiceRegistry::new();
863        let entry = ServiceEntry {
864            name: "redis".into(),
865            owner_pid: None,
866            endpoint: ServiceEndpoint::External {
867                url: "redis://localhost:6379".into(),
868            },
869            audit_level: ServiceAuditLevel::GateOnly,
870            registered_at: Utc::now(),
871        };
872        registry.register_entry(entry).unwrap();
873
874        assert_eq!(registry.resolve_target("redis"), None);
875    }
876
877    #[test]
878    fn unregister_entry() {
879        let registry = ServiceRegistry::new();
880        let entry = ServiceEntry {
881            name: "temp".into(),
882            owner_pid: Some(5),
883            endpoint: ServiceEndpoint::AgentInbox(5),
884            audit_level: ServiceAuditLevel::Full,
885            registered_at: Utc::now(),
886        };
887        registry.register_entry(entry).unwrap();
888
889        let removed = registry.unregister_entry("temp");
890        assert!(removed.is_some());
891        assert!(registry.get_entry("temp").is_none());
892    }
893
894    #[test]
895    fn list_entries() {
896        let registry = ServiceRegistry::new();
897        registry
898            .register_entry(ServiceEntry {
899                name: "svc-a".into(),
900                owner_pid: Some(1),
901                endpoint: ServiceEndpoint::AgentInbox(1),
902                audit_level: ServiceAuditLevel::Full,
903                registered_at: Utc::now(),
904            })
905            .unwrap();
906        registry
907            .register_entry(ServiceEntry {
908                name: "svc-b".into(),
909                owner_pid: None,
910                endpoint: ServiceEndpoint::Container { id: "c1".into() },
911                audit_level: ServiceAuditLevel::GateOnly,
912                registered_at: Utc::now(),
913            })
914            .unwrap();
915
916        let list = registry.list_entries();
917        assert_eq!(list.len(), 2);
918    }
919
920    #[test]
921    fn service_entry_serde_roundtrip() {
922        let entry = ServiceEntry {
923            name: "auth".into(),
924            owner_pid: Some(42),
925            endpoint: ServiceEndpoint::AgentInbox(42),
926            audit_level: ServiceAuditLevel::Full,
927            registered_at: Utc::now(),
928        };
929        let json = serde_json::to_string(&entry).unwrap();
930        let restored: ServiceEntry = serde_json::from_str(&json).unwrap();
931        assert_eq!(restored.name, "auth");
932        assert_eq!(restored.owner_pid, Some(42));
933        assert_eq!(restored.audit_level, ServiceAuditLevel::Full);
934    }
935
936    #[test]
937    fn service_endpoint_variants_serde() {
938        let endpoints = vec![
939            ServiceEndpoint::AgentInbox(1),
940            ServiceEndpoint::External {
941                url: "https://api.example.com".into(),
942            },
943            ServiceEndpoint::Container {
944                id: "container-abc".into(),
945            },
946        ];
947        for ep in endpoints {
948            let json = serde_json::to_string(&ep).unwrap();
949            let _: ServiceEndpoint = serde_json::from_str(&json).unwrap();
950        }
951    }
952
953    // ── ServiceApi tests (K3 C2) ──────────────────────
954
955    struct MockServiceApi;
956
957    #[async_trait]
958    impl ServiceApi for MockServiceApi {
959        async fn call(
960            &self,
961            service: &str,
962            method: &str,
963            _params: serde_json::Value,
964        ) -> Result<serde_json::Value, Box<dyn std::error::Error + Send + Sync>> {
965            Ok(serde_json::json!({"service": service, "method": method}))
966        }
967        async fn list_services(&self) -> Vec<ServiceInfo> {
968            vec![ServiceInfo {
969                name: "test".into(),
970                service_type: "core".into(),
971                healthy: true,
972            }]
973        }
974        async fn health(
975            &self,
976            _service: &str,
977        ) -> Result<HealthStatus, Box<dyn std::error::Error + Send + Sync>> {
978            Ok(HealthStatus::Healthy)
979        }
980    }
981
982    #[tokio::test]
983    async fn shell_adapter_parses_command() {
984        let api = Arc::new(MockServiceApi);
985        let shell = ShellAdapter::new(api);
986        let result = shell.execute("kernel.status").await.unwrap();
987        assert_eq!(result["service"], "kernel");
988        assert_eq!(result["method"], "status");
989    }
990
991    #[tokio::test]
992    async fn shell_adapter_with_args() {
993        let api = Arc::new(MockServiceApi);
994        let shell = ShellAdapter::new(api);
995        let result = shell
996            .execute("agent.spawn {\"name\":\"test\"}")
997            .await
998            .unwrap();
999        assert_eq!(result["service"], "agent");
1000    }
1001
1002    #[tokio::test]
1003    async fn mcp_adapter_routes_tool_call() {
1004        let api = Arc::new(MockServiceApi);
1005        let mcp = McpAdapter::new(api);
1006        let result = mcp
1007            .handle_tool_call("kernel_status", serde_json::json!({}))
1008            .await
1009            .unwrap();
1010        assert_eq!(result["service"], "kernel");
1011    }
1012
1013    #[tokio::test]
1014    async fn mcp_adapter_list_tools() {
1015        let api = Arc::new(MockServiceApi);
1016        let mcp = McpAdapter::new(api);
1017        let tools = mcp.list_tools().await;
1018        assert_eq!(tools.len(), 1);
1019    }
1020
1021    #[test]
1022    fn audit_level_default_is_full() {
1023        assert_eq!(ServiceAuditLevel::default(), ServiceAuditLevel::Full);
1024    }
1025
1026    #[test]
1027    fn dual_registry_independent() {
1028        // SystemService and ServiceEntry registrations are independent
1029        let registry = ServiceRegistry::new();
1030
1031        // Register a SystemService
1032        registry
1033            .register(Arc::new(MockService::new("both", ServiceType::Core)))
1034            .unwrap();
1035
1036        // Register a ServiceEntry with the same name (independent map)
1037        registry
1038            .register_entry(ServiceEntry {
1039                name: "both".into(),
1040                owner_pid: Some(1),
1041                endpoint: ServiceEndpoint::AgentInbox(1),
1042                audit_level: ServiceAuditLevel::Full,
1043                registered_at: Utc::now(),
1044            })
1045            .unwrap();
1046
1047        assert!(registry.get("both").is_some());
1048        assert!(registry.get_entry("both").is_some());
1049    }
1050
1051    // --- K4 G2: Service contract tests ---
1052
1053    #[test]
1054    fn service_contract_serde() {
1055        let contract = ServiceContract {
1056            service_name: "auth".into(),
1057            version: "1.0.0".into(),
1058            methods: vec!["login".into(), "logout".into()],
1059            content_hash: "abc123".into(),
1060        };
1061        let json = serde_json::to_string(&contract).unwrap();
1062        let restored: ServiceContract = serde_json::from_str(&json).unwrap();
1063        assert_eq!(restored.service_name, "auth");
1064        assert_eq!(restored.methods.len(), 2);
1065    }
1066
1067    #[test]
1068    #[cfg(feature = "exochain")]
1069    fn contract_on_chain() {
1070        let registry = ServiceRegistry::new();
1071        let chain = crate::chain::ChainManager::new(0, 1000);
1072        let contract = ServiceContract {
1073            service_name: "auth".into(),
1074            version: "1.0.0".into(),
1075            methods: vec!["login".into(), "logout".into()],
1076            content_hash: "abc123".into(),
1077        };
1078        registry.register_contract(&contract, &chain).unwrap();
1079        // Chain should have genesis + 1 contract event
1080        assert_eq!(chain.len(), 2);
1081        let tail = chain.tail(1);
1082        assert_eq!(tail[0].kind, "service.contract.register");
1083    }
1084
1085    #[test]
1086    #[cfg(feature = "exochain")]
1087    fn contract_version_immutability() {
1088        let chain = crate::chain::ChainManager::new(0, 1000);
1089        let registry = ServiceRegistry::new();
1090        let v1 = ServiceContract {
1091            service_name: "auth".into(),
1092            version: "1.0.0".into(),
1093            methods: vec!["login".into()],
1094            content_hash: "v1hash".into(),
1095        };
1096        let v2 = ServiceContract {
1097            service_name: "auth".into(),
1098            version: "2.0.0".into(),
1099            methods: vec!["login".into(), "refresh".into()],
1100            content_hash: "v2hash".into(),
1101        };
1102        registry.register_contract(&v1, &chain).unwrap();
1103        registry.register_contract(&v2, &chain).unwrap();
1104        // Both versions recorded — chain is append-only so v1 cannot be mutated
1105        assert_eq!(chain.len(), 3); // genesis + v1 + v2
1106    }
1107
1108    // --- C3: register_with_contract tests ---
1109
1110    #[test]
1111    #[cfg(feature = "exochain")]
1112    fn register_with_contract_anchors_to_chain() {
1113        let chain = crate::chain::ChainManager::new(0, 1000);
1114        let registry = ServiceRegistry::new();
1115        let svc = Arc::new(MockService::new("api-v1", ServiceType::Api));
1116
1117        registry
1118            .register_with_contract(
1119                svc,
1120                vec!["get".into(), "set".into(), "delete".into()],
1121                &chain,
1122            )
1123            .unwrap();
1124
1125        // Service should be registered
1126        assert!(registry.get("api-v1").is_some());
1127
1128        // Contract should be on chain
1129        let events = chain.tail(1);
1130        assert_eq!(events[0].kind, "service.contract.register");
1131    }
1132
1133    #[test]
1134    #[cfg(feature = "exochain")]
1135    fn register_with_contract_duplicate_fails() {
1136        let chain = crate::chain::ChainManager::new(0, 1000);
1137        let registry = ServiceRegistry::new();
1138        let svc1 = Arc::new(MockService::new("dup-c3", ServiceType::Core));
1139        let svc2 = Arc::new(MockService::new("dup-c3", ServiceType::Core));
1140
1141        registry
1142            .register_with_contract(svc1, vec!["ping".into()], &chain)
1143            .unwrap();
1144        // Second registration with same name should fail
1145        let result = registry.register_with_contract(svc2, vec!["ping".into()], &chain);
1146        assert!(result.is_err());
1147    }
1148
1149    #[test]
1150    #[cfg(feature = "exochain")]
1151    fn register_with_contract_hash_deterministic() {
1152        let chain = crate::chain::ChainManager::new(0, 1000);
1153        let registry = ServiceRegistry::new();
1154        let svc = Arc::new(MockService::new("hash-svc", ServiceType::Plugin));
1155
1156        registry
1157            .register_with_contract(svc, vec!["alpha".into(), "beta".into()], &chain)
1158            .unwrap();
1159
1160        let events = chain.tail(1);
1161        let payload = events[0].payload.as_ref().unwrap();
1162        let hash = payload["hash"].as_str().unwrap();
1163        // Hash should be a 64-char hex string (SHA-256)
1164        assert_eq!(hash.len(), 64);
1165        assert!(hash.chars().all(|c| c.is_ascii_hexdigit()));
1166    }
1167
1168    // ── Sprint 09a: serde roundtrip tests ────────────────────────
1169
1170    #[test]
1171    fn service_type_serde_roundtrip_core() {
1172        let st = ServiceType::Core;
1173        let json = serde_json::to_string(&st).unwrap();
1174        let restored: ServiceType = serde_json::from_str(&json).unwrap();
1175        assert_eq!(restored, ServiceType::Core);
1176    }
1177
1178    #[test]
1179    fn service_type_serde_roundtrip_plugin() {
1180        let st = ServiceType::Plugin;
1181        let json = serde_json::to_string(&st).unwrap();
1182        let restored: ServiceType = serde_json::from_str(&json).unwrap();
1183        assert_eq!(restored, ServiceType::Plugin);
1184    }
1185
1186    #[test]
1187    fn service_type_serde_roundtrip_custom() {
1188        let st = ServiceType::Custom("webhook".into());
1189        let json = serde_json::to_string(&st).unwrap();
1190        let restored: ServiceType = serde_json::from_str(&json).unwrap();
1191        assert_eq!(restored, ServiceType::Custom("webhook".into()));
1192    }
1193
1194    #[test]
1195    fn service_audit_level_serde_roundtrip() {
1196        for level in [ServiceAuditLevel::Full, ServiceAuditLevel::GateOnly] {
1197            let json = serde_json::to_string(&level).unwrap();
1198            let restored: ServiceAuditLevel = serde_json::from_str(&json).unwrap();
1199            assert_eq!(restored, level);
1200        }
1201    }
1202
1203    #[test]
1204    fn service_info_serde_roundtrip() {
1205        let info = ServiceInfo {
1206            name: "cache".into(),
1207            service_type: "core".into(),
1208            healthy: true,
1209        };
1210        let json = serde_json::to_string(&info).unwrap();
1211        let restored: ServiceInfo = serde_json::from_str(&json).unwrap();
1212        assert_eq!(restored.name, "cache");
1213        assert_eq!(restored.service_type, "core");
1214        assert!(restored.healthy);
1215    }
1216
1217    #[test]
1218    fn service_info_unhealthy_roundtrip() {
1219        let info = ServiceInfo {
1220            name: "broken".into(),
1221            service_type: "plugin".into(),
1222            healthy: false,
1223        };
1224        let json = serde_json::to_string(&info).unwrap();
1225        let restored: ServiceInfo = serde_json::from_str(&json).unwrap();
1226        assert!(!restored.healthy);
1227    }
1228
1229    #[test]
1230    fn snapshot_returns_all_services() {
1231        let registry = ServiceRegistry::new();
1232        registry
1233            .register(Arc::new(MockService::new("svc-a", ServiceType::Core)))
1234            .unwrap();
1235        registry
1236            .register(Arc::new(MockService::new("svc-b", ServiceType::Plugin)))
1237            .unwrap();
1238
1239        let snapshot = registry.snapshot();
1240        assert_eq!(snapshot.len(), 2);
1241        let names: Vec<&str> = snapshot.iter().map(|(n, _)| n.as_str()).collect();
1242        assert!(names.contains(&"svc-a"));
1243        assert!(names.contains(&"svc-b"));
1244    }
1245
1246    #[test]
1247    fn snapshot_is_independent_of_registry() {
1248        let registry = ServiceRegistry::new();
1249        registry
1250            .register(Arc::new(MockService::new("snap-svc", ServiceType::Core)))
1251            .unwrap();
1252
1253        let snapshot = registry.snapshot();
1254        // Remove from registry -- snapshot should still have it
1255        registry.unregister("snap-svc");
1256        assert!(registry.get("snap-svc").is_none());
1257        assert_eq!(snapshot.len(), 1);
1258    }
1259
1260    #[test]
1261    fn get_nonexistent_service_returns_none() {
1262        let registry = ServiceRegistry::new();
1263        assert!(registry.get("ghost").is_none());
1264    }
1265
1266    #[test]
1267    fn get_nonexistent_entry_returns_none() {
1268        let registry = ServiceRegistry::new();
1269        assert!(registry.get_entry("ghost").is_none());
1270    }
1271
1272    #[tokio::test]
1273    async fn shell_adapter_missing_dot_returns_error() {
1274        let api = Arc::new(MockServiceApi);
1275        let shell = ShellAdapter::new(api);
1276        let result = shell.execute("nodot").await;
1277        assert!(result.is_err());
1278    }
1279
1280    #[tokio::test]
1281    async fn shell_adapter_json_array_args() {
1282        let api = Arc::new(MockServiceApi);
1283        let shell = ShellAdapter::new(api);
1284        let result = shell.execute("svc.method [1,2,3]").await.unwrap();
1285        assert_eq!(result["service"], "svc");
1286        assert_eq!(result["method"], "method");
1287    }
1288
1289    #[tokio::test]
1290    async fn mcp_adapter_dot_separator() {
1291        let api = Arc::new(MockServiceApi);
1292        let mcp = McpAdapter::new(api);
1293        let result = mcp
1294            .handle_tool_call("kernel.status", serde_json::json!({}))
1295            .await
1296            .unwrap();
1297        assert_eq!(result["service"], "kernel");
1298        assert_eq!(result["method"], "status");
1299    }
1300
1301    #[tokio::test]
1302    async fn mcp_adapter_invalid_tool_name_returns_error() {
1303        let api = Arc::new(MockServiceApi);
1304        let mcp = McpAdapter::new(api);
1305        let result = mcp
1306            .handle_tool_call("noseparator", serde_json::json!({}))
1307            .await;
1308        assert!(result.is_err());
1309    }
1310
1311    #[test]
1312    fn service_type_all_variants_serde() {
1313        let variants = vec![
1314            ServiceType::Core,
1315            ServiceType::Plugin,
1316            ServiceType::Cron,
1317            ServiceType::Api,
1318            ServiceType::Custom("special".into()),
1319        ];
1320        for v in variants {
1321            let json = serde_json::to_string(&v).unwrap();
1322            let _: ServiceType = serde_json::from_str(&json).unwrap();
1323        }
1324    }
1325
1326    #[test]
1327    fn service_entry_external_endpoint_roundtrip() {
1328        let entry = ServiceEntry {
1329            name: "ext".into(),
1330            owner_pid: None,
1331            endpoint: ServiceEndpoint::External {
1332                url: "https://api.example.com/v2".into(),
1333            },
1334            audit_level: ServiceAuditLevel::GateOnly,
1335            registered_at: Utc::now(),
1336        };
1337        let json = serde_json::to_string(&entry).unwrap();
1338        let restored: ServiceEntry = serde_json::from_str(&json).unwrap();
1339        assert_eq!(restored.name, "ext");
1340        assert!(restored.owner_pid.is_none());
1341        assert_eq!(restored.audit_level, ServiceAuditLevel::GateOnly);
1342    }
1343
1344    #[test]
1345    fn service_entry_container_endpoint_roundtrip() {
1346        let entry = ServiceEntry {
1347            name: "docker-svc".into(),
1348            owner_pid: None,
1349            endpoint: ServiceEndpoint::Container {
1350                id: "abc123".into(),
1351            },
1352            audit_level: ServiceAuditLevel::Full,
1353            registered_at: Utc::now(),
1354        };
1355        let json = serde_json::to_string(&entry).unwrap();
1356        let restored: ServiceEntry = serde_json::from_str(&json).unwrap();
1357        assert!(matches!(
1358            restored.endpoint,
1359            ServiceEndpoint::Container { ref id } if id == "abc123"
1360        ));
1361    }
1362
1363    // ── KernelServiceApi tests (ADR-035) ────────────────────────
1364
1365    #[tokio::test]
1366    async fn kernel_api_call_dispatches_to_registered_service() {
1367        let registry = Arc::new(ServiceRegistry::new());
1368        registry
1369            .register(Arc::new(MockService::new("kernel", ServiceType::Core)))
1370            .unwrap();
1371
1372        let api = KernelServiceApi::new(registry);
1373        let result = api
1374            .call("kernel", "status", serde_json::json!({}))
1375            .await
1376            .unwrap();
1377        assert_eq!(result["service"], "kernel");
1378        assert_eq!(result["method"], "status");
1379        assert_eq!(result["status"], "dispatched");
1380    }
1381
1382    #[tokio::test]
1383    async fn kernel_api_call_unknown_service_errors() {
1384        let registry = Arc::new(ServiceRegistry::new());
1385        let api = KernelServiceApi::new(registry);
1386        let result = api
1387            .call("nonexistent", "method", serde_json::Value::Null)
1388            .await;
1389        assert!(result.is_err());
1390        let msg = result.unwrap_err().to_string();
1391        assert!(msg.contains("service not found"));
1392    }
1393
1394    #[tokio::test]
1395    async fn kernel_api_list_services_returns_registered() {
1396        let registry = Arc::new(ServiceRegistry::new());
1397        registry
1398            .register(Arc::new(MockService::new("auth", ServiceType::Core)))
1399            .unwrap();
1400        registry
1401            .register(Arc::new(MockService::new("cron", ServiceType::Cron)))
1402            .unwrap();
1403
1404        let api = KernelServiceApi::new(registry);
1405        let list = api.list_services().await;
1406        assert_eq!(list.len(), 2);
1407
1408        let names: Vec<&str> = list.iter().map(|s| s.name.as_str()).collect();
1409        assert!(names.contains(&"auth"));
1410        assert!(names.contains(&"cron"));
1411
1412        // MockService always returns Healthy
1413        for info in &list {
1414            assert!(info.healthy);
1415        }
1416    }
1417
1418    #[tokio::test]
1419    async fn kernel_api_list_services_empty_registry() {
1420        let registry = Arc::new(ServiceRegistry::new());
1421        let api = KernelServiceApi::new(registry);
1422        let list = api.list_services().await;
1423        assert!(list.is_empty());
1424    }
1425
1426    #[tokio::test]
1427    async fn kernel_api_health_returns_status() {
1428        let registry = Arc::new(ServiceRegistry::new());
1429        registry
1430            .register(Arc::new(MockService::new("cache", ServiceType::Core)))
1431            .unwrap();
1432
1433        let api = KernelServiceApi::new(registry);
1434        let status = api.health("cache").await.unwrap();
1435        assert_eq!(status, HealthStatus::Healthy);
1436    }
1437
1438    #[tokio::test]
1439    async fn kernel_api_health_unknown_service_errors() {
1440        let registry = Arc::new(ServiceRegistry::new());
1441        let api = KernelServiceApi::new(registry);
1442        let result = api.health("ghost").await;
1443        assert!(result.is_err());
1444    }
1445
1446    #[tokio::test]
1447    async fn kernel_api_with_shell_adapter_end_to_end() {
1448        let registry = Arc::new(ServiceRegistry::new());
1449        registry
1450            .register(Arc::new(MockService::new("agent", ServiceType::Core)))
1451            .unwrap();
1452
1453        let api: Arc<dyn ServiceApi> = Arc::new(KernelServiceApi::new(registry));
1454        let shell = ShellAdapter::new(api);
1455        let result = shell
1456            .execute("agent.spawn {\"name\":\"test\"}")
1457            .await
1458            .unwrap();
1459        assert_eq!(result["service"], "agent");
1460        assert_eq!(result["method"], "spawn");
1461        assert_eq!(result["status"], "dispatched");
1462    }
1463
1464    #[tokio::test]
1465    async fn kernel_api_with_mcp_adapter_end_to_end() {
1466        let registry = Arc::new(ServiceRegistry::new());
1467        registry
1468            .register(Arc::new(MockService::new("kernel", ServiceType::Core)))
1469            .unwrap();
1470
1471        let api: Arc<dyn ServiceApi> = Arc::new(KernelServiceApi::new(registry));
1472        let mcp = McpAdapter::new(api);
1473        let result = mcp
1474            .handle_tool_call("kernel_status", serde_json::json!({}))
1475            .await
1476            .unwrap();
1477        assert_eq!(result["service"], "kernel");
1478        assert_eq!(result["method"], "status");
1479    }
1480
1481    #[test]
1482    fn kernel_api_registry_accessor() {
1483        let registry = Arc::new(ServiceRegistry::new());
1484        registry
1485            .register(Arc::new(MockService::new("svc", ServiceType::Core)))
1486            .unwrap();
1487
1488        let api = KernelServiceApi::new(registry);
1489        assert!(api.registry().get("svc").is_some());
1490    }
1491}