1use 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#[non_exhaustive]
21#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
22pub enum ServiceType {
23 Core,
25 Plugin,
27 Cron,
29 Api,
31 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#[non_exhaustive]
54#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
55pub enum ServiceEndpoint {
56 AgentInbox(Pid),
58 External {
60 url: String,
62 },
63 Container {
65 id: String,
67 },
68}
69
70#[non_exhaustive]
75#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
76pub enum ServiceAuditLevel {
77 #[default]
79 Full,
80 GateOnly,
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct ServiceEntry {
91 pub name: String,
93 pub owner_pid: Option<Pid>,
95 pub endpoint: ServiceEndpoint,
97 pub audit_level: ServiceAuditLevel,
99 pub registered_at: DateTime<Utc>,
101}
102
103#[async_trait]
108pub trait SystemService: Send + Sync {
109 fn name(&self) -> &str;
111
112 fn service_type(&self) -> ServiceType;
114
115 async fn start(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
117
118 async fn stop(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
120
121 async fn health_check(&self) -> HealthStatus;
123
124 fn health_detail(&self) -> Option<String> {
130 None
131 }
132
133 #[cfg(feature = "os-patterns")]
138 async fn liveness_check(&self) -> crate::health::ProbeResult {
139 crate::health::ProbeResult::Live
140 }
141
142 #[cfg(feature = "os-patterns")]
147 async fn readiness_check(&self) -> crate::health::ProbeResult {
148 crate::health::ProbeResult::Ready
149 }
150}
151
152pub struct ServiceRegistry {
163 services: DashMap<String, Arc<dyn SystemService>>,
164 entries: DashMap<String, ServiceEntry>,
165}
166
167impl ServiceRegistry {
168 pub fn new() -> Self {
170 Self {
171 services: DashMap::new(),
172 entries: DashMap::new(),
173 }
174 }
175
176 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 pub fn unregister(&self, name: &str) -> Option<Arc<dyn SystemService>> {
195 self.services.remove(name).map(|(_, s)| s)
196 }
197
198 pub fn get(&self, name: &str) -> Option<Arc<dyn SystemService>> {
200 self.services.get(name).map(|s| s.value().clone())
201 }
202
203 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 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 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 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 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 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 pub fn get_entry(&self, name: &str) -> Option<ServiceEntry> {
283 self.entries.get(name).map(|e| e.value().clone())
284 }
285
286 pub fn resolve_target(&self, name: &str) -> Option<Pid> {
291 self.entries.get(name).and_then(|e| e.value().owner_pid)
292 }
293
294 pub fn list_entries(&self) -> Vec<ServiceEntry> {
296 self.entries.iter().map(|e| e.value().clone()).collect()
297 }
298
299 pub fn unregister_entry(&self, name: &str) -> Option<ServiceEntry> {
301 self.entries.remove(name).map(|(_, e)| e)
302 }
303
304 #[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 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 pub fn len(&self) -> usize {
328 self.services.len()
329 }
330
331 pub fn is_empty(&self) -> bool {
333 self.services.is_empty()
334 }
335
336 #[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 self.register(service)?;
353
354 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 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
417pub struct ServiceContract {
418 pub service_name: String,
420 pub version: String,
422 pub methods: Vec<String>,
424 pub content_hash: String,
426}
427
428impl Default for ServiceRegistry {
429 fn default() -> Self {
430 Self::new()
431 }
432}
433
434#[async_trait]
442pub trait ServiceApi: Send + Sync {
443 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 async fn list_services(&self) -> Vec<ServiceInfo>;
453
454 async fn health(
456 &self,
457 service: &str,
458 ) -> Result<HealthStatus, Box<dyn std::error::Error + Send + Sync>>;
459}
460
461#[derive(Debug, Clone, Serialize, Deserialize)]
463pub struct ServiceInfo {
464 pub name: String,
466 pub service_type: String,
468 pub healthy: bool,
470}
471
472pub struct ShellAdapter {
474 api: Arc<dyn ServiceApi>,
475}
476
477impl ShellAdapter {
478 pub fn new(api: Arc<dyn ServiceApi>) -> Self {
480 Self { api }
481 }
482
483 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
514pub struct McpAdapter {
516 api: Arc<dyn ServiceApi>,
517}
518
519impl McpAdapter {
520 pub fn new(api: Arc<dyn ServiceApi>) -> Self {
522 Self { api }
523 }
524
525 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 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
557pub struct KernelServiceApi {
568 registry: Arc<ServiceRegistry>,
569}
570
571impl KernelServiceApi {
572 pub fn new(registry: Arc<ServiceRegistry>) -> Self {
574 Self { registry }
575 }
576
577 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 let _svc = self
593 .registry
594 .get(service)
595 .ok_or_else(|| format!("service not found: {service}"))?;
596
597 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 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
636impl 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 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 #[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 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 let registry = ServiceRegistry::new();
1030
1031 registry
1033 .register(Arc::new(MockService::new("both", ServiceType::Core)))
1034 .unwrap();
1035
1036 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 #[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 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 assert_eq!(chain.len(), 3); }
1107
1108 #[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 assert!(registry.get("api-v1").is_some());
1127
1128 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 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 assert_eq!(hash.len(), 64);
1165 assert!(hash.chars().all(|c| c.is_ascii_hexdigit()));
1166 }
1167
1168 #[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 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 #[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 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}