use crate::ipc::state::{Check, CheckStatus};
use crate::manifest::CheckHint;
pub fn build_check(hint: &CheckHint, stdout: &str) -> Check {
let payload = crate::manifest::fenced_payload(
stdout,
crate::manifest::CHECK_BLOCK_BEGIN,
crate::manifest::CHECK_BLOCK_END,
);
let value: serde_json::Value = match serde_json::from_str(payload) {
Ok(v) => v,
Err(e) => return unknown(hint, format!("check stdout was not JSON: {e}")),
};
let Some(obj) = value.as_object() else {
return unknown(hint, "check stdout was not a JSON object".to_string());
};
let status = match obj.get(&hint.status_field) {
Some(v) => match serde_json::from_value::<CheckStatus>(v.clone()) {
Ok(s) => s,
Err(_) => {
return unknown(
hint,
format!(
"`{}` = {v} is not one of ok/warn/fail/unknown",
hint.status_field
),
);
}
},
None => {
return unknown(hint, format!("stdout has no `{}` field", hint.status_field));
}
};
let detail = obj.get(&hint.detail_field).and_then(json_to_detail);
Check {
name: hint.name.clone(),
label: hint.label.clone(),
status,
detail,
troubleshoot: hint.troubleshoot.clone(),
health_hidden: !hint.health,
}
}
pub fn build_check_failed(hint: &CheckHint, exit_code: i32, stderr: &str) -> Check {
let snippet: String = stderr.trim().chars().take(200).collect();
let detail = if snippet.is_empty() {
format!("check script exited {exit_code}")
} else {
format!("check script exited {exit_code}: {snippet}")
};
unknown(hint, detail)
}
fn unknown(hint: &CheckHint, detail: String) -> Check {
Check {
name: hint.name.clone(),
label: hint.label.clone(),
status: CheckStatus::Unknown,
detail: Some(detail),
troubleshoot: hint.troubleshoot.clone(),
health_hidden: !hint.health,
}
}
pub fn json_to_detail(v: &serde_json::Value) -> Option<String> {
match v {
serde_json::Value::Null => None,
serde_json::Value::String(s) if s.is_empty() => None,
serde_json::Value::String(s) => Some(s.clone()),
serde_json::Value::Number(n) => Some(n.to_string()),
serde_json::Value::Bool(b) => Some(b.to_string()),
other => Some(other.to_string()),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn hint(name: &str) -> CheckHint {
CheckHint {
name: name.into(),
label: None,
status_field: "status".into(),
detail_field: "detail".into(),
troubleshoot: None,
fleet: true,
health: true,
alert: None,
}
}
#[test]
fn build_check_reads_status_and_detail() {
let c = build_check(
&hint("bitlocker"),
r#"{"status":"warn","detail":"D: unprotected"}"#,
);
assert_eq!(c.name, "bitlocker");
assert_eq!(c.status, CheckStatus::Warn);
assert_eq!(c.detail.as_deref(), Some("D: unprotected"));
}
#[test]
fn build_check_honours_custom_fields_and_troubleshoot() {
let h = CheckHint {
name: "patch".into(),
label: Some("Windows 更新プログラム".into()),
status_field: "compliance".into(),
detail_field: "summary".into(),
troubleshoot: Some("fix-patch".into()),
fleet: true,
health: true,
alert: None,
};
let c = build_check(
&h,
r#"{"compliance":"fail","summary":"12 missing","extra":[1,2]}"#,
);
assert_eq!(c.status, CheckStatus::Fail);
assert_eq!(c.label.as_deref(), Some("Windows 更新プログラム"));
assert_eq!(c.detail.as_deref(), Some("12 missing"));
assert_eq!(c.troubleshoot.as_deref(), Some("fix-patch"));
}
#[test]
fn build_check_detail_optional() {
let c = build_check(&hint("x"), r#"{"status":"ok"}"#);
assert_eq!(c.status, CheckStatus::Ok);
assert!(c.detail.is_none());
}
#[test]
fn build_check_degrades_on_bad_input() {
assert_eq!(
build_check(&hint("x"), "not json").status,
CheckStatus::Unknown
);
assert_eq!(
build_check(&hint("x"), "[1,2,3]").status,
CheckStatus::Unknown
);
assert_eq!(
build_check(&hint("x"), r#"{"detail":"hi"}"#).status,
CheckStatus::Unknown
);
let bad = build_check(&hint("x"), r#"{"status":"green"}"#);
assert_eq!(bad.status, CheckStatus::Unknown);
assert!(bad.detail.unwrap().contains("green"));
}
#[test]
fn build_check_reads_fenced_payload() {
let stdout = "updating...\n#KANADE-CHECK-BEGIN\n{\"status\":\"fail\",\"detail\":\"D: unprotected\"}\n#KANADE-CHECK-END\ndone.\n";
let c = build_check(&hint("bitlocker"), stdout);
assert_eq!(c.status, CheckStatus::Fail);
assert_eq!(c.detail.as_deref(), Some("D: unprotected"));
}
#[test]
fn build_check_sets_health_hidden_from_hint() {
let shown = build_check(&hint("bitlocker"), r#"{"status":"ok"}"#);
assert!(!shown.health_hidden);
let mut h = hint("myapp-up-to-date");
h.health = false;
assert!(build_check(&h, r#"{"status":"fail"}"#).health_hidden);
assert!(build_check_failed(&h, 1, "boom").health_hidden);
}
#[test]
fn build_check_failed_surfaces_exit_and_stderr() {
let c = build_check_failed(&hint("av"), 1, " Get-MpComputerStatus: access denied ");
assert_eq!(c.status, CheckStatus::Unknown);
let d = c.detail.unwrap();
assert!(d.contains("exited 1"));
assert!(d.contains("access denied"));
}
#[test]
fn json_to_detail_renders_arrays_as_compact_json() {
let v: serde_json::Value = serde_json::json!(["C:", "D:"]);
assert_eq!(json_to_detail(&v).as_deref(), Some(r#"["C:","D:"]"#));
assert!(json_to_detail(&serde_json::Value::Null).is_none());
}
}