use crate::ui::{self, BAD, BOLD, DIM, GOOD, WARN, pad, paint};
use pitboard_core::doctor::{Check, Diagnosis, Level};
use serde_json::{Value, json};
fn is_codex(check: &Check) -> bool {
check.code.starts_with("codex_")
}
pub fn human(checks: &[Check]) -> String {
let width = |codex: bool| {
checks
.iter()
.filter(|c| is_codex(c) == codex)
.map(|c| ui::columns(&c.name))
.max()
.unwrap_or(0)
};
let (claude_width, codex_width) = (width(false), width(true));
let mut out = String::new();
let mut in_codex = false;
for c in checks {
if !in_codex && is_codex(c) {
in_codex = true;
out.push_str(&format!("\n{}\n", paint(BOLD, "Codex")));
}
let width = if is_codex(c) {
codex_width
} else {
claude_width
};
let mark = match c.level {
Level::Ok => paint(GOOD, "✓"),
Level::Warn => paint(WARN, "!"),
Level::Fail => paint(BAD, "✗"),
};
out.push_str(&format!("{mark} {} {}\n", pad(&c.name, width), c.detail));
if !c.advice.is_empty() {
out.push_str(&format!(
" {} {}\n",
pad("", width),
paint(DIM, &c.advice)
));
}
}
let count = |level| checks.iter().filter(|c| c.level == level).count();
let summary = match (count(Level::Fail), count(Level::Warn)) {
(0, 0) => paint(GOOD, "Everything pitboard relies on holds."),
(0, w) => paint(WARN, format!("{w} to look at; nothing is broken.")),
(f, _) => paint(
BAD,
format!("{f} broken: do not switch accounts until fixed."),
),
};
out.push_str(&format!("\n{summary}\n"));
out
}
pub fn json(diagnosis: &Diagnosis) -> Value {
let report = json!({
"environment": diagnosis.environment,
"checks": diagnosis.checks.iter().map(|c| json!({
"code": c.code,
"name": c.name,
"level": match c.level { Level::Ok => "ok", Level::Warn => "warn", Level::Fail => "fail" },
"detail": c.detail,
"advice": c.advice,
})).collect::<Vec<_>>(),
});
diagnosis.redaction.over_json(&report)
}
#[cfg(test)]
mod tests {
use super::*;
fn check(level: Level) -> Check {
Check {
code: "credential",
name: "credential".into(),
level,
detail: "detail".into(),
advice: if level == Level::Ok {
String::new()
} else {
"do this".into()
},
}
}
#[test]
fn the_summary_says_whether_anything_is_broken() {
let plain = |checks: &[Check]| anstream::adapter::strip_str(&human(checks)).to_string();
assert!(plain(&[check(Level::Ok)]).ends_with("Everything pitboard relies on holds.\n"));
assert!(plain(&[check(Level::Ok), check(Level::Warn)]).contains("1 to look at"));
assert!(plain(&[check(Level::Fail)]).contains("1 broken"));
}
#[test]
fn codex_checks_get_a_heading_of_their_own() {
let plain = |checks: &[Check]| anstream::adapter::strip_str(&human(checks)).to_string();
let claude = [check(Level::Ok), check(Level::Warn)];
assert!(!plain(&claude).contains("Codex"));
let before = plain(&[check(Level::Ok)]);
let mut codex = check(Level::Ok);
codex.code = "codex_backend";
codex.name = "Codex login store".into();
let mut running = check(Level::Ok);
running.code = "codex_running";
running.name = "running Codex".into();
let text = plain(&[check(Level::Ok), codex, running]);
let lines: Vec<&str> = text.lines().collect();
let heading = lines.iter().position(|l| *l == "Codex").expect(&text);
assert_eq!(
lines[heading - 1],
"",
"set apart from Claude Code's: {text}"
);
assert!(lines[heading + 1].contains("Codex login store"), "{text}");
assert_eq!(text.matches("\nCodex\n").count(), 1, "{text}");
assert!(
before.starts_with(&text[..text.find("\nCodex").unwrap()]),
"what comes before it is what a machine without Codex shows: {text}"
);
}
#[test]
fn a_codex_account_is_listed_under_codex() {
let plain = |checks: &[Check]| anstream::adapter::strip_str(&human(checks)).to_string();
let mut backend = check(Level::Ok);
backend.code = "codex_backend";
backend.name = "Codex login store".into();
let mut account = check(Level::Ok);
account.code = "codex_parked_login";
account.name = "account codex/a-rather-long-label".into();
let before = plain(&[check(Level::Ok)]);
let text = plain(&[check(Level::Ok), backend, account]);
let lines: Vec<&str> = text.lines().collect();
let heading = lines.iter().position(|l| *l == "Codex").expect(&text);
let at = lines
.iter()
.position(|l| l.contains("codex/a-rather-long-label"))
.unwrap();
assert!(at > heading, "{text}");
assert_eq!(lines[0], before.lines().next().unwrap(), "{text}");
}
}