1use koi_common::capability::{Capability, CapabilityStatus};
10
11use crate::cores::Cores;
12
13pub struct CapabilityReport {
19 pub status: CapabilityStatus,
20 pub enabled: bool,
21}
22
23impl CapabilityReport {
24 pub fn into_card(self) -> serde_json::Value {
29 serde_json::json!({
30 "name": self.status.name,
31 "enabled": self.enabled,
32 "healthy": self.status.healthy,
33 "summary": self.status.summary,
34 })
35 }
36
37 fn present(status: CapabilityStatus) -> Self {
38 Self {
39 status,
40 enabled: true,
41 }
42 }
43
44 fn disabled(name: &str) -> Self {
45 Self {
46 status: CapabilityStatus {
47 name: name.to_string(),
48 summary: "disabled".to_string(),
49 healthy: false,
50 },
51 enabled: false,
52 }
53 }
54
55 fn stopped(name: &str) -> Self {
56 Self {
57 status: CapabilityStatus {
58 name: name.to_string(),
59 summary: "stopped".to_string(),
60 healthy: false,
61 },
62 enabled: true,
63 }
64 }
65}
66
67pub async fn assemble_capabilities(cores: &Cores) -> Vec<CapabilityReport> {
73 let mut caps = Vec::with_capacity(7);
74
75 caps.push(match &cores.mdns {
77 Some(core) => CapabilityReport::present(core.status().await),
78 None => CapabilityReport::disabled("mdns"),
79 });
80
81 caps.push(match &cores.certmesh {
83 Some(core) => CapabilityReport::present(core.status().await),
84 None => CapabilityReport::disabled("certmesh"),
85 });
86
87 caps.push(match &cores.dns {
89 Some(rt) if rt.status().await.running => {
90 CapabilityReport::present(rt.core().status().await)
91 }
92 Some(_) => CapabilityReport::stopped("dns"),
93 None => CapabilityReport::disabled("dns"),
94 });
95
96 caps.push(match &cores.health {
98 Some(rt) if rt.status().await.running => {
99 CapabilityReport::present(rt.core().status().await)
100 }
101 Some(_) => CapabilityReport::stopped("health"),
102 None => CapabilityReport::disabled("health"),
103 });
104
105 caps.push(match &cores.proxy {
107 Some(rt) => {
108 let listeners = rt.status().await;
109 let summary = if listeners.is_empty() {
110 "no listeners".to_string()
111 } else {
112 format!("{} listeners", listeners.len())
113 };
114 CapabilityReport::present(CapabilityStatus {
115 name: "proxy".to_string(),
116 summary,
117 healthy: true,
118 })
119 }
120 None => CapabilityReport::disabled("proxy"),
121 });
122
123 caps.push(match &cores.udp {
125 Some(rt) => CapabilityReport::present(Capability::status(rt.as_ref()).await),
126 None => CapabilityReport::disabled("udp"),
127 });
128
129 caps.push(match &cores.runtime {
131 Some(rt) => CapabilityReport::present(Capability::status(rt.as_ref()).await),
132 None => CapabilityReport::disabled("runtime"),
133 });
134
135 caps
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141
142 #[tokio::test]
143 async fn all_disabled_ladder_is_the_canonical_seven_rungs() {
144 let caps = assemble_capabilities(&Cores::default()).await;
148 let rungs: Vec<(&str, &str, bool, bool)> = caps
149 .iter()
150 .map(|c| {
151 (
152 c.status.name.as_str(),
153 c.status.summary.as_str(),
154 c.status.healthy,
155 c.enabled,
156 )
157 })
158 .collect();
159 assert_eq!(
160 rungs,
161 vec![
162 ("mdns", "disabled", false, false),
163 ("certmesh", "disabled", false, false),
164 ("dns", "disabled", false, false),
165 ("health", "disabled", false, false),
166 ("proxy", "disabled", false, false),
167 ("udp", "disabled", false, false),
168 ("runtime", "disabled", false, false),
169 ]
170 );
171 }
172
173 #[tokio::test]
174 async fn capability_status_projection_matches_v1_status_shape() {
175 let caps = assemble_capabilities(&Cores::default()).await;
177 let statuses: Vec<CapabilityStatus> = caps.into_iter().map(|c| c.status).collect();
178 let json = serde_json::to_value(&statuses).unwrap();
179 let first = &json[0];
180 assert_eq!(first["name"], "mdns");
181 assert_eq!(first["summary"], "disabled");
182 assert_eq!(first["healthy"], false);
183 assert!(first.get("enabled").is_none(), "/v1/status omits `enabled`");
184 }
185}