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 #[cfg(feature = "os-patterns")]
129 async fn liveness_check(&self) -> crate::health::ProbeResult {
130 crate::health::ProbeResult::Live
131 }
132
133 #[cfg(feature = "os-patterns")]
138 async fn readiness_check(&self) -> crate::health::ProbeResult {
139 crate::health::ProbeResult::Ready
140 }
141}
142
143pub struct ServiceRegistry {
154 services: DashMap<String, Arc<dyn SystemService>>,
155 entries: DashMap<String, ServiceEntry>,
156}
157
158impl ServiceRegistry {
159 pub fn new() -> Self {
161 Self {
162 services: DashMap::new(),
163 entries: DashMap::new(),
164 }
165 }
166
167 pub fn register(
172 &self,
173 service: Arc<dyn SystemService>,
174 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
175 let name = service.name().to_owned();
176 if self.services.contains_key(&name) {
177 return Err(format!("service already registered: {name}").into());
178 }
179 info!(service = %name, "registering service");
180 self.services.insert(name, service);
181 Ok(())
182 }
183
184 pub fn unregister(&self, name: &str) -> Option<Arc<dyn SystemService>> {
186 self.services.remove(name).map(|(_, s)| s)
187 }
188
189 pub fn get(&self, name: &str) -> Option<Arc<dyn SystemService>> {
191 self.services.get(name).map(|s| s.value().clone())
192 }
193
194 pub fn list(&self) -> Vec<(String, ServiceType)> {
196 self.services
197 .iter()
198 .map(|entry| (entry.key().clone(), entry.value().service_type()))
199 .collect()
200 }
201
202 pub async fn start_all(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
207 for entry in self.services.iter() {
208 let name = entry.key().clone();
209 info!(service = %name, "starting service");
210 if let Err(e) = entry.value().start().await {
211 warn!(service = %name, error = %e, "service failed to start");
212 }
213 }
214 Ok(())
215 }
216
217 pub async fn stop_all(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
221 for entry in self.services.iter() {
222 let name = entry.key().clone();
223 info!(service = %name, "stopping service");
224 if let Err(e) = entry.value().stop().await {
225 warn!(service = %name, error = %e, "service failed to stop");
226 }
227 }
228 Ok(())
229 }
230
231 pub fn snapshot(&self) -> Vec<(String, Arc<dyn SystemService>)> {
237 self.services
238 .iter()
239 .map(|entry| (entry.key().clone(), entry.value().clone()))
240 .collect()
241 }
242
243 pub async fn health_all(&self) -> Vec<(String, HealthStatus)> {
245 let mut results = Vec::new();
246 for entry in self.services.iter() {
247 let name = entry.key().clone();
248 let status = entry.value().health_check().await;
249 results.push((name, status));
250 }
251 results
252 }
253
254 pub fn register_entry(
260 &self,
261 entry: ServiceEntry,
262 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
263 let name = entry.name.clone();
264 if self.entries.contains_key(&name) {
265 return Err(format!("service entry already registered: {name}").into());
266 }
267 info!(service = %name, "registering service entry");
268 self.entries.insert(name, entry);
269 Ok(())
270 }
271
272 pub fn get_entry(&self, name: &str) -> Option<ServiceEntry> {
274 self.entries.get(name).map(|e| e.value().clone())
275 }
276
277 pub fn resolve_target(&self, name: &str) -> Option<Pid> {
282 self.entries.get(name).and_then(|e| e.value().owner_pid)
283 }
284
285 pub fn list_entries(&self) -> Vec<ServiceEntry> {
287 self.entries.iter().map(|e| e.value().clone()).collect()
288 }
289
290 pub fn unregister_entry(&self, name: &str) -> Option<ServiceEntry> {
292 self.entries.remove(name).map(|(_, e)| e)
293 }
294
295 #[cfg(feature = "exochain")]
301 pub fn register_with_tree(
302 &self,
303 service: Arc<dyn SystemService>,
304 tree_manager: &crate::tree_manager::TreeManager,
305 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
306 let name = service.name().to_owned();
307 self.register(service)?;
308
309 if let Err(e) = tree_manager.register_service(&name) {
311 tracing::debug!(service = %name, error = %e, "failed to register service in tree");
312 }
313
314 Ok(())
315 }
316
317 pub fn len(&self) -> usize {
319 self.services.len()
320 }
321
322 pub fn is_empty(&self) -> bool {
324 self.services.is_empty()
325 }
326
327 #[cfg(feature = "exochain")]
333 pub fn register_with_contract(
334 &self,
335 service: Arc<dyn SystemService>,
336 methods: Vec<String>,
337 chain: &crate::chain::ChainManager,
338 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
339 let name = service.name().to_owned();
340 let stype = service.service_type().to_string();
341
342 self.register(service)?;
344
345 let contract_content = serde_json::json!({
347 "service": &name,
348 "type": &stype,
349 "methods": &methods,
350 });
351 let content_hash = {
352 use sha2::{Digest, Sha256};
353 let bytes = serde_json::to_string(&contract_content)
354 .unwrap_or_default();
355 format!("{:x}", Sha256::digest(bytes.as_bytes()))
356 };
357
358 let contract = ServiceContract {
359 service_name: name,
360 version: "1.0.0".into(),
361 methods,
362 content_hash,
363 };
364
365 self.register_contract(&contract, chain)?;
366
367 Ok(())
368 }
369
370 #[cfg(feature = "exochain")]
376 pub fn register_contract(
377 &self,
378 contract: &ServiceContract,
379 chain: &crate::chain::ChainManager,
380 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
381 info!(
382 service = %contract.service_name,
383 version = %contract.version,
384 "registering service contract on chain"
385 );
386 chain.append(
387 &contract.service_name,
388 "service.contract.register",
389 Some(serde_json::json!({
390 "service": contract.service_name,
391 "version": contract.version,
392 "methods": contract.methods,
393 "hash": contract.content_hash,
394 })),
395 );
396 Ok(())
397 }
398}
399
400#[derive(Debug, Clone, Serialize, Deserialize)]
409pub struct ServiceContract {
410 pub service_name: String,
412 pub version: String,
414 pub methods: Vec<String>,
416 pub content_hash: String,
418}
419
420impl Default for ServiceRegistry {
421 fn default() -> Self {
422 Self::new()
423 }
424}
425
426#[async_trait]
434pub trait ServiceApi: Send + Sync {
435 async fn call(
437 &self,
438 service: &str,
439 method: &str,
440 params: serde_json::Value,
441 ) -> Result<serde_json::Value, Box<dyn std::error::Error + Send + Sync>>;
442
443 async fn list_services(&self) -> Vec<ServiceInfo>;
445
446 async fn health(
448 &self,
449 service: &str,
450 ) -> Result<HealthStatus, Box<dyn std::error::Error + Send + Sync>>;
451}
452
453#[derive(Debug, Clone, Serialize, Deserialize)]
455pub struct ServiceInfo {
456 pub name: String,
458 pub service_type: String,
460 pub healthy: bool,
462}
463
464pub struct ShellAdapter {
466 api: Arc<dyn ServiceApi>,
467}
468
469impl ShellAdapter {
470 pub fn new(api: Arc<dyn ServiceApi>) -> Self {
472 Self { api }
473 }
474
475 pub async fn execute(
480 &self,
481 command: &str,
482 ) -> Result<serde_json::Value, Box<dyn std::error::Error + Send + Sync>> {
483 let parts: Vec<&str> = command.splitn(2, ' ').collect();
484 let (service_method, args_str) = match parts.as_slice() {
485 [sm] => (*sm, ""),
486 [sm, args] => (*sm, *args),
487 _ => return Err("empty command".into()),
488 };
489
490 let (service, method) = service_method
491 .split_once('.')
492 .ok_or_else(|| format!("expected 'service.method', got '{service_method}'"))?;
493
494 let params = if args_str.is_empty() {
495 serde_json::Value::Null
496 } else if args_str.starts_with('{') || args_str.starts_with('[') {
497 serde_json::from_str(args_str)?
498 } else {
499 serde_json::json!({"args": args_str})
500 };
501
502 self.api.call(service, method, params).await
503 }
504}
505
506pub struct McpAdapter {
508 api: Arc<dyn ServiceApi>,
509}
510
511impl McpAdapter {
512 pub fn new(api: Arc<dyn ServiceApi>) -> Self {
514 Self { api }
515 }
516
517 pub async fn handle_tool_call(
522 &self,
523 tool_name: &str,
524 arguments: serde_json::Value,
525 ) -> Result<serde_json::Value, Box<dyn std::error::Error + Send + Sync>> {
526 let (service, method) = tool_name
527 .split_once('_')
528 .or_else(|| tool_name.split_once('.'))
529 .ok_or_else(|| format!("invalid tool name format: {tool_name}"))?;
530
531 self.api.call(service, method, arguments).await
532 }
533
534 pub async fn list_tools(&self) -> Vec<serde_json::Value> {
536 let services = self.api.list_services().await;
537 services
538 .into_iter()
539 .map(|s| {
540 serde_json::json!({
541 "name": s.name,
542 "description": format!("WeftOS {} service", s.service_type),
543 })
544 })
545 .collect()
546 }
547}
548
549impl clawft_types::Registry for ServiceRegistry {
552 type Value = Arc<dyn SystemService>;
553
554 fn get(&self, key: &str) -> Option<Self::Value> {
555 self.services.get(key).map(|s| s.value().clone())
556 }
557
558 fn list_keys(&self) -> Vec<String> {
559 self.services.iter().map(|e| e.key().clone()).collect()
560 }
561
562 fn contains(&self, key: &str) -> bool {
563 self.services.contains_key(key)
564 }
565
566 fn count(&self) -> usize {
567 self.services.len()
568 }
569}
570
571#[cfg(test)]
572mod tests {
573 use super::*;
574
575 struct MockService {
577 name: String,
578 service_type: ServiceType,
579 }
580
581 impl MockService {
582 fn new(name: &str, stype: ServiceType) -> Self {
583 Self {
584 name: name.to_owned(),
585 service_type: stype,
586 }
587 }
588 }
589
590 #[async_trait]
591 impl SystemService for MockService {
592 fn name(&self) -> &str {
593 &self.name
594 }
595
596 fn service_type(&self) -> ServiceType {
597 self.service_type.clone()
598 }
599
600 async fn start(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
601 Ok(())
602 }
603
604 async fn stop(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
605 Ok(())
606 }
607
608 async fn health_check(&self) -> HealthStatus {
609 HealthStatus::Healthy
610 }
611 }
612
613 #[test]
614 fn register_and_get() {
615 let registry = ServiceRegistry::new();
616 let svc = Arc::new(MockService::new("test-svc", ServiceType::Core));
617 registry.register(svc).unwrap();
618
619 let retrieved = registry.get("test-svc");
620 assert!(retrieved.is_some());
621 assert_eq!(retrieved.unwrap().name(), "test-svc");
622 }
623
624 #[test]
625 fn register_duplicate_fails() {
626 let registry = ServiceRegistry::new();
627 let svc1 = Arc::new(MockService::new("dup-svc", ServiceType::Core));
628 let svc2 = Arc::new(MockService::new("dup-svc", ServiceType::Plugin));
629
630 registry.register(svc1).unwrap();
631 let result = registry.register(svc2);
632 assert!(result.is_err());
633 }
634
635 #[test]
636 fn unregister() {
637 let registry = ServiceRegistry::new();
638 let svc = Arc::new(MockService::new("rm-svc", ServiceType::Core));
639 registry.register(svc).unwrap();
640
641 let removed = registry.unregister("rm-svc");
642 assert!(removed.is_some());
643 assert!(registry.get("rm-svc").is_none());
644 }
645
646 #[test]
647 fn list_services() {
648 let registry = ServiceRegistry::new();
649 registry
650 .register(Arc::new(MockService::new("svc-a", ServiceType::Core)))
651 .unwrap();
652 registry
653 .register(Arc::new(MockService::new("svc-b", ServiceType::Cron)))
654 .unwrap();
655
656 let list = registry.list();
657 assert_eq!(list.len(), 2);
658 }
659
660 #[tokio::test]
661 async fn start_and_stop_all() {
662 let registry = ServiceRegistry::new();
663 registry
664 .register(Arc::new(MockService::new("svc-1", ServiceType::Core)))
665 .unwrap();
666 registry
667 .register(Arc::new(MockService::new("svc-2", ServiceType::Plugin)))
668 .unwrap();
669
670 registry.start_all().await.unwrap();
671 registry.stop_all().await.unwrap();
672 }
673
674 #[tokio::test]
675 async fn health_all() {
676 let registry = ServiceRegistry::new();
677 registry
678 .register(Arc::new(MockService::new("svc-1", ServiceType::Core)))
679 .unwrap();
680 registry
681 .register(Arc::new(MockService::new("svc-2", ServiceType::Plugin)))
682 .unwrap();
683
684 let health = registry.health_all().await;
685 assert_eq!(health.len(), 2);
686 for (_, status) in &health {
687 assert_eq!(*status, HealthStatus::Healthy);
688 }
689 }
690
691 #[test]
692 fn len_and_is_empty() {
693 let registry = ServiceRegistry::new();
694 assert!(registry.is_empty());
695 assert_eq!(registry.len(), 0);
696
697 registry
698 .register(Arc::new(MockService::new("svc", ServiceType::Core)))
699 .unwrap();
700 assert!(!registry.is_empty());
701 assert_eq!(registry.len(), 1);
702 }
703
704 #[test]
705 fn service_type_display() {
706 assert_eq!(ServiceType::Core.to_string(), "core");
707 assert_eq!(ServiceType::Plugin.to_string(), "plugin");
708 assert_eq!(ServiceType::Cron.to_string(), "cron");
709 assert_eq!(ServiceType::Api.to_string(), "api");
710 assert_eq!(
711 ServiceType::Custom("webhook".into()).to_string(),
712 "custom(webhook)"
713 );
714 }
715
716 #[test]
719 fn register_and_get_entry() {
720 let registry = ServiceRegistry::new();
721 let entry = ServiceEntry {
722 name: "auth".into(),
723 owner_pid: Some(42),
724 endpoint: ServiceEndpoint::AgentInbox(42),
725 audit_level: ServiceAuditLevel::Full,
726 registered_at: Utc::now(),
727 };
728 registry.register_entry(entry).unwrap();
729
730 let retrieved = registry.get_entry("auth").unwrap();
731 assert_eq!(retrieved.name, "auth");
732 assert_eq!(retrieved.owner_pid, Some(42));
733 }
734
735 #[test]
736 fn register_entry_duplicate_fails() {
737 let registry = ServiceRegistry::new();
738 let entry1 = ServiceEntry {
739 name: "dup".into(),
740 owner_pid: Some(1),
741 endpoint: ServiceEndpoint::AgentInbox(1),
742 audit_level: ServiceAuditLevel::Full,
743 registered_at: Utc::now(),
744 };
745 let entry2 = ServiceEntry {
746 name: "dup".into(),
747 owner_pid: Some(2),
748 endpoint: ServiceEndpoint::AgentInbox(2),
749 audit_level: ServiceAuditLevel::Full,
750 registered_at: Utc::now(),
751 };
752 registry.register_entry(entry1).unwrap();
753 let result = registry.register_entry(entry2);
754 assert!(result.is_err());
755 }
756
757 #[test]
758 fn resolve_target_returns_owner_pid() {
759 let registry = ServiceRegistry::new();
760 let entry = ServiceEntry {
761 name: "cache".into(),
762 owner_pid: Some(99),
763 endpoint: ServiceEndpoint::AgentInbox(99),
764 audit_level: ServiceAuditLevel::Full,
765 registered_at: Utc::now(),
766 };
767 registry.register_entry(entry).unwrap();
768
769 assert_eq!(registry.resolve_target("cache"), Some(99));
770 assert_eq!(registry.resolve_target("missing"), None);
771 }
772
773 #[test]
774 fn resolve_target_external_returns_none() {
775 let registry = ServiceRegistry::new();
776 let entry = ServiceEntry {
777 name: "redis".into(),
778 owner_pid: None,
779 endpoint: ServiceEndpoint::External {
780 url: "redis://localhost:6379".into(),
781 },
782 audit_level: ServiceAuditLevel::GateOnly,
783 registered_at: Utc::now(),
784 };
785 registry.register_entry(entry).unwrap();
786
787 assert_eq!(registry.resolve_target("redis"), None);
788 }
789
790 #[test]
791 fn unregister_entry() {
792 let registry = ServiceRegistry::new();
793 let entry = ServiceEntry {
794 name: "temp".into(),
795 owner_pid: Some(5),
796 endpoint: ServiceEndpoint::AgentInbox(5),
797 audit_level: ServiceAuditLevel::Full,
798 registered_at: Utc::now(),
799 };
800 registry.register_entry(entry).unwrap();
801
802 let removed = registry.unregister_entry("temp");
803 assert!(removed.is_some());
804 assert!(registry.get_entry("temp").is_none());
805 }
806
807 #[test]
808 fn list_entries() {
809 let registry = ServiceRegistry::new();
810 registry
811 .register_entry(ServiceEntry {
812 name: "svc-a".into(),
813 owner_pid: Some(1),
814 endpoint: ServiceEndpoint::AgentInbox(1),
815 audit_level: ServiceAuditLevel::Full,
816 registered_at: Utc::now(),
817 })
818 .unwrap();
819 registry
820 .register_entry(ServiceEntry {
821 name: "svc-b".into(),
822 owner_pid: None,
823 endpoint: ServiceEndpoint::Container { id: "c1".into() },
824 audit_level: ServiceAuditLevel::GateOnly,
825 registered_at: Utc::now(),
826 })
827 .unwrap();
828
829 let list = registry.list_entries();
830 assert_eq!(list.len(), 2);
831 }
832
833 #[test]
834 fn service_entry_serde_roundtrip() {
835 let entry = ServiceEntry {
836 name: "auth".into(),
837 owner_pid: Some(42),
838 endpoint: ServiceEndpoint::AgentInbox(42),
839 audit_level: ServiceAuditLevel::Full,
840 registered_at: Utc::now(),
841 };
842 let json = serde_json::to_string(&entry).unwrap();
843 let restored: ServiceEntry = serde_json::from_str(&json).unwrap();
844 assert_eq!(restored.name, "auth");
845 assert_eq!(restored.owner_pid, Some(42));
846 assert_eq!(restored.audit_level, ServiceAuditLevel::Full);
847 }
848
849 #[test]
850 fn service_endpoint_variants_serde() {
851 let endpoints = vec![
852 ServiceEndpoint::AgentInbox(1),
853 ServiceEndpoint::External {
854 url: "https://api.example.com".into(),
855 },
856 ServiceEndpoint::Container {
857 id: "container-abc".into(),
858 },
859 ];
860 for ep in endpoints {
861 let json = serde_json::to_string(&ep).unwrap();
862 let _: ServiceEndpoint = serde_json::from_str(&json).unwrap();
863 }
864 }
865
866 struct MockServiceApi;
869
870 #[async_trait]
871 impl ServiceApi for MockServiceApi {
872 async fn call(
873 &self,
874 service: &str,
875 method: &str,
876 _params: serde_json::Value,
877 ) -> Result<serde_json::Value, Box<dyn std::error::Error + Send + Sync>> {
878 Ok(serde_json::json!({"service": service, "method": method}))
879 }
880 async fn list_services(&self) -> Vec<ServiceInfo> {
881 vec![ServiceInfo {
882 name: "test".into(),
883 service_type: "core".into(),
884 healthy: true,
885 }]
886 }
887 async fn health(
888 &self,
889 _service: &str,
890 ) -> Result<HealthStatus, Box<dyn std::error::Error + Send + Sync>> {
891 Ok(HealthStatus::Healthy)
892 }
893 }
894
895 #[tokio::test]
896 async fn shell_adapter_parses_command() {
897 let api = Arc::new(MockServiceApi);
898 let shell = ShellAdapter::new(api);
899 let result = shell.execute("kernel.status").await.unwrap();
900 assert_eq!(result["service"], "kernel");
901 assert_eq!(result["method"], "status");
902 }
903
904 #[tokio::test]
905 async fn shell_adapter_with_args() {
906 let api = Arc::new(MockServiceApi);
907 let shell = ShellAdapter::new(api);
908 let result = shell
909 .execute("agent.spawn {\"name\":\"test\"}")
910 .await
911 .unwrap();
912 assert_eq!(result["service"], "agent");
913 }
914
915 #[tokio::test]
916 async fn mcp_adapter_routes_tool_call() {
917 let api = Arc::new(MockServiceApi);
918 let mcp = McpAdapter::new(api);
919 let result = mcp
920 .handle_tool_call("kernel_status", serde_json::json!({}))
921 .await
922 .unwrap();
923 assert_eq!(result["service"], "kernel");
924 }
925
926 #[tokio::test]
927 async fn mcp_adapter_list_tools() {
928 let api = Arc::new(MockServiceApi);
929 let mcp = McpAdapter::new(api);
930 let tools = mcp.list_tools().await;
931 assert_eq!(tools.len(), 1);
932 }
933
934 #[test]
935 fn audit_level_default_is_full() {
936 assert_eq!(ServiceAuditLevel::default(), ServiceAuditLevel::Full);
937 }
938
939 #[test]
940 fn dual_registry_independent() {
941 let registry = ServiceRegistry::new();
943
944 registry
946 .register(Arc::new(MockService::new("both", ServiceType::Core)))
947 .unwrap();
948
949 registry
951 .register_entry(ServiceEntry {
952 name: "both".into(),
953 owner_pid: Some(1),
954 endpoint: ServiceEndpoint::AgentInbox(1),
955 audit_level: ServiceAuditLevel::Full,
956 registered_at: Utc::now(),
957 })
958 .unwrap();
959
960 assert!(registry.get("both").is_some());
961 assert!(registry.get_entry("both").is_some());
962 }
963
964 #[test]
967 fn service_contract_serde() {
968 let contract = ServiceContract {
969 service_name: "auth".into(),
970 version: "1.0.0".into(),
971 methods: vec!["login".into(), "logout".into()],
972 content_hash: "abc123".into(),
973 };
974 let json = serde_json::to_string(&contract).unwrap();
975 let restored: ServiceContract = serde_json::from_str(&json).unwrap();
976 assert_eq!(restored.service_name, "auth");
977 assert_eq!(restored.methods.len(), 2);
978 }
979
980 #[test]
981 #[cfg(feature = "exochain")]
982 fn contract_on_chain() {
983 let registry = ServiceRegistry::new();
984 let chain = crate::chain::ChainManager::new(0, 1000);
985 let contract = ServiceContract {
986 service_name: "auth".into(),
987 version: "1.0.0".into(),
988 methods: vec!["login".into(), "logout".into()],
989 content_hash: "abc123".into(),
990 };
991 registry.register_contract(&contract, &chain).unwrap();
992 assert_eq!(chain.len(), 2);
994 let tail = chain.tail(1);
995 assert_eq!(tail[0].kind, "service.contract.register");
996 }
997
998 #[test]
999 #[cfg(feature = "exochain")]
1000 fn contract_version_immutability() {
1001 let chain = crate::chain::ChainManager::new(0, 1000);
1002 let registry = ServiceRegistry::new();
1003 let v1 = ServiceContract {
1004 service_name: "auth".into(),
1005 version: "1.0.0".into(),
1006 methods: vec!["login".into()],
1007 content_hash: "v1hash".into(),
1008 };
1009 let v2 = ServiceContract {
1010 service_name: "auth".into(),
1011 version: "2.0.0".into(),
1012 methods: vec!["login".into(), "refresh".into()],
1013 content_hash: "v2hash".into(),
1014 };
1015 registry.register_contract(&v1, &chain).unwrap();
1016 registry.register_contract(&v2, &chain).unwrap();
1017 assert_eq!(chain.len(), 3); }
1020
1021 #[test]
1024 #[cfg(feature = "exochain")]
1025 fn register_with_contract_anchors_to_chain() {
1026 let chain = crate::chain::ChainManager::new(0, 1000);
1027 let registry = ServiceRegistry::new();
1028 let svc = Arc::new(MockService::new("api-v1", ServiceType::Api));
1029
1030 registry
1031 .register_with_contract(
1032 svc,
1033 vec!["get".into(), "set".into(), "delete".into()],
1034 &chain,
1035 )
1036 .unwrap();
1037
1038 assert!(registry.get("api-v1").is_some());
1040
1041 let events = chain.tail(1);
1043 assert_eq!(events[0].kind, "service.contract.register");
1044 }
1045
1046 #[test]
1047 #[cfg(feature = "exochain")]
1048 fn register_with_contract_duplicate_fails() {
1049 let chain = crate::chain::ChainManager::new(0, 1000);
1050 let registry = ServiceRegistry::new();
1051 let svc1 = Arc::new(MockService::new("dup-c3", ServiceType::Core));
1052 let svc2 = Arc::new(MockService::new("dup-c3", ServiceType::Core));
1053
1054 registry
1055 .register_with_contract(svc1, vec!["ping".into()], &chain)
1056 .unwrap();
1057 let result = registry.register_with_contract(svc2, vec!["ping".into()], &chain);
1059 assert!(result.is_err());
1060 }
1061
1062 #[test]
1063 #[cfg(feature = "exochain")]
1064 fn register_with_contract_hash_deterministic() {
1065 let chain = crate::chain::ChainManager::new(0, 1000);
1066 let registry = ServiceRegistry::new();
1067 let svc = Arc::new(MockService::new("hash-svc", ServiceType::Plugin));
1068
1069 registry
1070 .register_with_contract(
1071 svc,
1072 vec!["alpha".into(), "beta".into()],
1073 &chain,
1074 )
1075 .unwrap();
1076
1077 let events = chain.tail(1);
1078 let payload = events[0].payload.as_ref().unwrap();
1079 let hash = payload["hash"].as_str().unwrap();
1080 assert_eq!(hash.len(), 64);
1082 assert!(hash.chars().all(|c| c.is_ascii_hexdigit()));
1083 }
1084
1085 #[test]
1088 fn service_type_serde_roundtrip_core() {
1089 let st = ServiceType::Core;
1090 let json = serde_json::to_string(&st).unwrap();
1091 let restored: ServiceType = serde_json::from_str(&json).unwrap();
1092 assert_eq!(restored, ServiceType::Core);
1093 }
1094
1095 #[test]
1096 fn service_type_serde_roundtrip_plugin() {
1097 let st = ServiceType::Plugin;
1098 let json = serde_json::to_string(&st).unwrap();
1099 let restored: ServiceType = serde_json::from_str(&json).unwrap();
1100 assert_eq!(restored, ServiceType::Plugin);
1101 }
1102
1103 #[test]
1104 fn service_type_serde_roundtrip_custom() {
1105 let st = ServiceType::Custom("webhook".into());
1106 let json = serde_json::to_string(&st).unwrap();
1107 let restored: ServiceType = serde_json::from_str(&json).unwrap();
1108 assert_eq!(restored, ServiceType::Custom("webhook".into()));
1109 }
1110
1111 #[test]
1112 fn service_audit_level_serde_roundtrip() {
1113 for level in [ServiceAuditLevel::Full, ServiceAuditLevel::GateOnly] {
1114 let json = serde_json::to_string(&level).unwrap();
1115 let restored: ServiceAuditLevel = serde_json::from_str(&json).unwrap();
1116 assert_eq!(restored, level);
1117 }
1118 }
1119
1120 #[test]
1121 fn service_info_serde_roundtrip() {
1122 let info = ServiceInfo {
1123 name: "cache".into(),
1124 service_type: "core".into(),
1125 healthy: true,
1126 };
1127 let json = serde_json::to_string(&info).unwrap();
1128 let restored: ServiceInfo = serde_json::from_str(&json).unwrap();
1129 assert_eq!(restored.name, "cache");
1130 assert_eq!(restored.service_type, "core");
1131 assert!(restored.healthy);
1132 }
1133
1134 #[test]
1135 fn service_info_unhealthy_roundtrip() {
1136 let info = ServiceInfo {
1137 name: "broken".into(),
1138 service_type: "plugin".into(),
1139 healthy: false,
1140 };
1141 let json = serde_json::to_string(&info).unwrap();
1142 let restored: ServiceInfo = serde_json::from_str(&json).unwrap();
1143 assert!(!restored.healthy);
1144 }
1145
1146 #[test]
1147 fn snapshot_returns_all_services() {
1148 let registry = ServiceRegistry::new();
1149 registry
1150 .register(Arc::new(MockService::new("svc-a", ServiceType::Core)))
1151 .unwrap();
1152 registry
1153 .register(Arc::new(MockService::new("svc-b", ServiceType::Plugin)))
1154 .unwrap();
1155
1156 let snapshot = registry.snapshot();
1157 assert_eq!(snapshot.len(), 2);
1158 let names: Vec<&str> = snapshot.iter().map(|(n, _)| n.as_str()).collect();
1159 assert!(names.contains(&"svc-a"));
1160 assert!(names.contains(&"svc-b"));
1161 }
1162
1163 #[test]
1164 fn snapshot_is_independent_of_registry() {
1165 let registry = ServiceRegistry::new();
1166 registry
1167 .register(Arc::new(MockService::new("snap-svc", ServiceType::Core)))
1168 .unwrap();
1169
1170 let snapshot = registry.snapshot();
1171 registry.unregister("snap-svc");
1173 assert!(registry.get("snap-svc").is_none());
1174 assert_eq!(snapshot.len(), 1);
1175 }
1176
1177 #[test]
1178 fn get_nonexistent_service_returns_none() {
1179 let registry = ServiceRegistry::new();
1180 assert!(registry.get("ghost").is_none());
1181 }
1182
1183 #[test]
1184 fn get_nonexistent_entry_returns_none() {
1185 let registry = ServiceRegistry::new();
1186 assert!(registry.get_entry("ghost").is_none());
1187 }
1188
1189 #[tokio::test]
1190 async fn shell_adapter_missing_dot_returns_error() {
1191 let api = Arc::new(MockServiceApi);
1192 let shell = ShellAdapter::new(api);
1193 let result = shell.execute("nodot").await;
1194 assert!(result.is_err());
1195 }
1196
1197 #[tokio::test]
1198 async fn shell_adapter_json_array_args() {
1199 let api = Arc::new(MockServiceApi);
1200 let shell = ShellAdapter::new(api);
1201 let result = shell.execute("svc.method [1,2,3]").await.unwrap();
1202 assert_eq!(result["service"], "svc");
1203 assert_eq!(result["method"], "method");
1204 }
1205
1206 #[tokio::test]
1207 async fn mcp_adapter_dot_separator() {
1208 let api = Arc::new(MockServiceApi);
1209 let mcp = McpAdapter::new(api);
1210 let result = mcp
1211 .handle_tool_call("kernel.status", serde_json::json!({}))
1212 .await
1213 .unwrap();
1214 assert_eq!(result["service"], "kernel");
1215 assert_eq!(result["method"], "status");
1216 }
1217
1218 #[tokio::test]
1219 async fn mcp_adapter_invalid_tool_name_returns_error() {
1220 let api = Arc::new(MockServiceApi);
1221 let mcp = McpAdapter::new(api);
1222 let result = mcp
1223 .handle_tool_call("noseparator", serde_json::json!({}))
1224 .await;
1225 assert!(result.is_err());
1226 }
1227
1228 #[test]
1229 fn service_type_all_variants_serde() {
1230 let variants = vec![
1231 ServiceType::Core,
1232 ServiceType::Plugin,
1233 ServiceType::Cron,
1234 ServiceType::Api,
1235 ServiceType::Custom("special".into()),
1236 ];
1237 for v in variants {
1238 let json = serde_json::to_string(&v).unwrap();
1239 let _: ServiceType = serde_json::from_str(&json).unwrap();
1240 }
1241 }
1242
1243 #[test]
1244 fn service_entry_external_endpoint_roundtrip() {
1245 let entry = ServiceEntry {
1246 name: "ext".into(),
1247 owner_pid: None,
1248 endpoint: ServiceEndpoint::External {
1249 url: "https://api.example.com/v2".into(),
1250 },
1251 audit_level: ServiceAuditLevel::GateOnly,
1252 registered_at: Utc::now(),
1253 };
1254 let json = serde_json::to_string(&entry).unwrap();
1255 let restored: ServiceEntry = serde_json::from_str(&json).unwrap();
1256 assert_eq!(restored.name, "ext");
1257 assert!(restored.owner_pid.is_none());
1258 assert_eq!(restored.audit_level, ServiceAuditLevel::GateOnly);
1259 }
1260
1261 #[test]
1262 fn service_entry_container_endpoint_roundtrip() {
1263 let entry = ServiceEntry {
1264 name: "docker-svc".into(),
1265 owner_pid: None,
1266 endpoint: ServiceEndpoint::Container {
1267 id: "abc123".into(),
1268 },
1269 audit_level: ServiceAuditLevel::Full,
1270 registered_at: Utc::now(),
1271 };
1272 let json = serde_json::to_string(&entry).unwrap();
1273 let restored: ServiceEntry = serde_json::from_str(&json).unwrap();
1274 assert!(matches!(
1275 restored.endpoint,
1276 ServiceEndpoint::Container { ref id } if id == "abc123"
1277 ));
1278 }
1279}