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()),
65 None => CapabilityReport::disabled("mdns"),
66 });
67
68 caps.push(match &cores.certmesh {
70 Some(core) => CapabilityReport::present(core.status()),
71 None => CapabilityReport::disabled("certmesh"),
72 });
73
74 caps.push(match &cores.dns {
76 Some(rt) if rt.status().await.running => CapabilityReport::present(rt.core().status()),
77 Some(_) => CapabilityReport::stopped("dns"),
78 None => CapabilityReport::disabled("dns"),
79 });
80
81 caps.push(match &cores.health {
83 Some(rt) if rt.status().await.running => CapabilityReport::present(rt.core().status()),
84 Some(_) => CapabilityReport::stopped("health"),
85 None => CapabilityReport::disabled("health"),
86 });
87
88 caps.push(match &cores.proxy {
90 Some(rt) => {
91 let listeners = rt.status().await;
92 let summary = if listeners.is_empty() {
93 "no listeners".to_string()
94 } else {
95 format!("{} listeners", listeners.len())
96 };
97 CapabilityReport::present(CapabilityStatus {
98 name: "proxy".to_string(),
99 summary,
100 healthy: true,
101 })
102 }
103 None => CapabilityReport::disabled("proxy"),
104 });
105
106 caps.push(match &cores.udp {
108 Some(rt) => CapabilityReport::present(Capability::status(rt.as_ref())),
109 None => CapabilityReport::disabled("udp"),
110 });
111
112 caps.push(match &cores.runtime {
114 Some(rt) => CapabilityReport::present(rt.capability_status().await),
115 None => CapabilityReport::disabled("runtime"),
116 });
117
118 caps
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124
125 #[tokio::test]
126 async fn all_disabled_ladder_is_the_canonical_seven_rungs() {
127 let caps = assemble_capabilities(&Cores::default()).await;
131 let rungs: Vec<(&str, &str, bool, bool)> = caps
132 .iter()
133 .map(|c| {
134 (
135 c.status.name.as_str(),
136 c.status.summary.as_str(),
137 c.status.healthy,
138 c.enabled,
139 )
140 })
141 .collect();
142 assert_eq!(
143 rungs,
144 vec![
145 ("mdns", "disabled", false, false),
146 ("certmesh", "disabled", false, false),
147 ("dns", "disabled", false, false),
148 ("health", "disabled", false, false),
149 ("proxy", "disabled", false, false),
150 ("udp", "disabled", false, false),
151 ("runtime", "disabled", false, false),
152 ]
153 );
154 }
155
156 #[tokio::test]
157 async fn capability_status_projection_matches_v1_status_shape() {
158 let caps = assemble_capabilities(&Cores::default()).await;
160 let statuses: Vec<CapabilityStatus> = caps.into_iter().map(|c| c.status).collect();
161 let json = serde_json::to_value(&statuses).unwrap();
162 let first = &json[0];
163 assert_eq!(first["name"], "mdns");
164 assert_eq!(first["summary"], "disabled");
165 assert_eq!(first["healthy"], false);
166 assert!(first.get("enabled").is_none(), "/v1/status omits `enabled`");
167 }
168}