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 fn present(status: CapabilityStatus) -> Self {
25 Self {
26 status,
27 enabled: true,
28 }
29 }
30
31 fn disabled(name: &str) -> Self {
32 Self {
33 status: CapabilityStatus {
34 name: name.to_string(),
35 summary: "disabled".to_string(),
36 healthy: false,
37 },
38 enabled: false,
39 }
40 }
41
42 fn stopped(name: &str) -> Self {
43 Self {
44 status: CapabilityStatus {
45 name: name.to_string(),
46 summary: "stopped".to_string(),
47 healthy: false,
48 },
49 enabled: true,
50 }
51 }
52}
53
54pub async fn assemble_capabilities(cores: &Cores) -> Vec<CapabilityReport> {
60 let mut caps = Vec::with_capacity(7);
61
62 caps.push(match &cores.mdns {
64 Some(core) => CapabilityReport::present(core.status().await),
65 None => CapabilityReport::disabled("mdns"),
66 });
67
68 caps.push(match &cores.certmesh {
70 Some(core) => CapabilityReport::present(core.status().await),
71 None => CapabilityReport::disabled("certmesh"),
72 });
73
74 caps.push(match &cores.dns {
76 Some(rt) if rt.status().await.running => {
77 CapabilityReport::present(rt.core().status().await)
78 }
79 Some(_) => CapabilityReport::stopped("dns"),
80 None => CapabilityReport::disabled("dns"),
81 });
82
83 caps.push(match &cores.health {
85 Some(rt) if rt.status().await.running => {
86 CapabilityReport::present(rt.core().status().await)
87 }
88 Some(_) => CapabilityReport::stopped("health"),
89 None => CapabilityReport::disabled("health"),
90 });
91
92 caps.push(match &cores.proxy {
94 Some(rt) => {
95 let listeners = rt.status().await;
96 let summary = if listeners.is_empty() {
97 "no listeners".to_string()
98 } else {
99 format!("{} listeners", listeners.len())
100 };
101 CapabilityReport::present(CapabilityStatus {
102 name: "proxy".to_string(),
103 summary,
104 healthy: true,
105 })
106 }
107 None => CapabilityReport::disabled("proxy"),
108 });
109
110 caps.push(match &cores.udp {
112 Some(rt) => CapabilityReport::present(Capability::status(rt.as_ref()).await),
113 None => CapabilityReport::disabled("udp"),
114 });
115
116 caps.push(match &cores.runtime {
118 Some(rt) => CapabilityReport::present(Capability::status(rt.as_ref()).await),
119 None => CapabilityReport::disabled("runtime"),
120 });
121
122 caps
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128
129 #[tokio::test]
130 async fn all_disabled_ladder_is_the_canonical_seven_rungs() {
131 let caps = assemble_capabilities(&Cores::default()).await;
135 let rungs: Vec<(&str, &str, bool, bool)> = caps
136 .iter()
137 .map(|c| {
138 (
139 c.status.name.as_str(),
140 c.status.summary.as_str(),
141 c.status.healthy,
142 c.enabled,
143 )
144 })
145 .collect();
146 assert_eq!(
147 rungs,
148 vec![
149 ("mdns", "disabled", false, false),
150 ("certmesh", "disabled", false, false),
151 ("dns", "disabled", false, false),
152 ("health", "disabled", false, false),
153 ("proxy", "disabled", false, false),
154 ("udp", "disabled", false, false),
155 ("runtime", "disabled", false, false),
156 ]
157 );
158 }
159
160 #[tokio::test]
161 async fn capability_status_projection_matches_v1_status_shape() {
162 let caps = assemble_capabilities(&Cores::default()).await;
164 let statuses: Vec<CapabilityStatus> = caps.into_iter().map(|c| c.status).collect();
165 let json = serde_json::to_value(&statuses).unwrap();
166 let first = &json[0];
167 assert_eq!(first["name"], "mdns");
168 assert_eq!(first["summary"], "disabled");
169 assert_eq!(first["healthy"], false);
170 assert!(first.get("enabled").is_none(), "/v1/status omits `enabled`");
171 }
172}