1use crate::ipc::state::{Check, CheckStatus};
11use crate::manifest::CheckHint;
12
13pub fn build_check(hint: &CheckHint, stdout: &str) -> Check {
19 let payload = crate::manifest::fenced_payload(
24 stdout,
25 crate::manifest::CHECK_BLOCK_BEGIN,
26 crate::manifest::CHECK_BLOCK_END,
27 );
28 let value: serde_json::Value = match serde_json::from_str(payload) {
29 Ok(v) => v,
30 Err(e) => return unknown(hint, format!("check stdout was not JSON: {e}")),
31 };
32 let Some(obj) = value.as_object() else {
33 return unknown(hint, "check stdout was not a JSON object".to_string());
34 };
35 let status = match obj.get(&hint.status_field) {
36 Some(v) => match serde_json::from_value::<CheckStatus>(v.clone()) {
37 Ok(s) => s,
38 Err(_) => {
39 return unknown(
40 hint,
41 format!(
42 "`{}` = {v} is not one of ok/warn/fail/unknown",
43 hint.status_field
44 ),
45 );
46 }
47 },
48 None => {
49 return unknown(hint, format!("stdout has no `{}` field", hint.status_field));
50 }
51 };
52 let detail = obj.get(&hint.detail_field).and_then(json_to_detail);
53 Check {
54 name: hint.name.clone(),
55 label: hint.label.clone(),
56 status,
57 detail,
58 troubleshoot: hint.troubleshoot.clone(),
59 health_hidden: !hint.health,
62 }
63}
64
65pub fn build_check_failed(hint: &CheckHint, exit_code: i32, stderr: &str) -> Check {
71 let snippet: String = stderr.trim().chars().take(200).collect();
72 let detail = if snippet.is_empty() {
73 format!("check script exited {exit_code}")
74 } else {
75 format!("check script exited {exit_code}: {snippet}")
76 };
77 unknown(hint, detail)
78}
79
80fn unknown(hint: &CheckHint, detail: String) -> Check {
81 Check {
82 name: hint.name.clone(),
83 label: hint.label.clone(),
84 status: CheckStatus::Unknown,
85 detail: Some(detail),
86 troubleshoot: hint.troubleshoot.clone(),
87 health_hidden: !hint.health,
90 }
91}
92
93pub fn json_to_detail(v: &serde_json::Value) -> Option<String> {
99 match v {
100 serde_json::Value::Null => None,
101 serde_json::Value::String(s) if s.is_empty() => None,
102 serde_json::Value::String(s) => Some(s.clone()),
103 serde_json::Value::Number(n) => Some(n.to_string()),
104 serde_json::Value::Bool(b) => Some(b.to_string()),
105 other => Some(other.to_string()),
107 }
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113
114 fn hint(name: &str) -> CheckHint {
115 CheckHint {
116 name: name.into(),
117 label: None,
118 status_field: "status".into(),
119 detail_field: "detail".into(),
120 troubleshoot: None,
121 fleet: true,
122 health: true,
123 alert: None,
124 }
125 }
126
127 #[test]
128 fn build_check_reads_status_and_detail() {
129 let c = build_check(
130 &hint("bitlocker"),
131 r#"{"status":"warn","detail":"D: unprotected"}"#,
132 );
133 assert_eq!(c.name, "bitlocker");
134 assert_eq!(c.status, CheckStatus::Warn);
135 assert_eq!(c.detail.as_deref(), Some("D: unprotected"));
136 }
137
138 #[test]
139 fn build_check_honours_custom_fields_and_troubleshoot() {
140 let h = CheckHint {
141 name: "patch".into(),
142 label: Some("Windows 更新プログラム".into()),
143 status_field: "compliance".into(),
144 detail_field: "summary".into(),
145 troubleshoot: Some("fix-patch".into()),
146 fleet: true,
147 health: true,
148 alert: None,
149 };
150 let c = build_check(
151 &h,
152 r#"{"compliance":"fail","summary":"12 missing","extra":[1,2]}"#,
153 );
154 assert_eq!(c.status, CheckStatus::Fail);
155 assert_eq!(c.label.as_deref(), Some("Windows 更新プログラム"));
156 assert_eq!(c.detail.as_deref(), Some("12 missing"));
157 assert_eq!(c.troubleshoot.as_deref(), Some("fix-patch"));
158 }
159
160 #[test]
161 fn build_check_detail_optional() {
162 let c = build_check(&hint("x"), r#"{"status":"ok"}"#);
163 assert_eq!(c.status, CheckStatus::Ok);
164 assert!(c.detail.is_none());
165 }
166
167 #[test]
168 fn build_check_degrades_on_bad_input() {
169 assert_eq!(
171 build_check(&hint("x"), "not json").status,
172 CheckStatus::Unknown
173 );
174 assert_eq!(
176 build_check(&hint("x"), "[1,2,3]").status,
177 CheckStatus::Unknown
178 );
179 assert_eq!(
181 build_check(&hint("x"), r#"{"detail":"hi"}"#).status,
182 CheckStatus::Unknown
183 );
184 let bad = build_check(&hint("x"), r#"{"status":"green"}"#);
186 assert_eq!(bad.status, CheckStatus::Unknown);
187 assert!(bad.detail.unwrap().contains("green"));
188 }
189
190 #[test]
191 fn build_check_reads_fenced_payload() {
192 let stdout = "updating...\n#KANADE-CHECK-BEGIN\n{\"status\":\"fail\",\"detail\":\"D: unprotected\"}\n#KANADE-CHECK-END\ndone.\n";
195 let c = build_check(&hint("bitlocker"), stdout);
196 assert_eq!(c.status, CheckStatus::Fail);
197 assert_eq!(c.detail.as_deref(), Some("D: unprotected"));
198 }
199
200 #[test]
201 fn build_check_sets_health_hidden_from_hint() {
202 let shown = build_check(&hint("bitlocker"), r#"{"status":"ok"}"#);
204 assert!(!shown.health_hidden);
205
206 let mut h = hint("myapp-up-to-date");
209 h.health = false;
210 assert!(build_check(&h, r#"{"status":"fail"}"#).health_hidden);
211 assert!(build_check_failed(&h, 1, "boom").health_hidden);
212 }
213
214 #[test]
215 fn build_check_failed_surfaces_exit_and_stderr() {
216 let c = build_check_failed(&hint("av"), 1, " Get-MpComputerStatus: access denied ");
217 assert_eq!(c.status, CheckStatus::Unknown);
218 let d = c.detail.unwrap();
219 assert!(d.contains("exited 1"));
220 assert!(d.contains("access denied"));
221 }
222
223 #[test]
224 fn json_to_detail_renders_arrays_as_compact_json() {
225 let v: serde_json::Value = serde_json::json!(["C:", "D:"]);
226 assert_eq!(json_to_detail(&v).as_deref(), Some(r#"["C:","D:"]"#));
227 assert!(json_to_detail(&serde_json::Value::Null).is_none());
228 }
229}