use crate::cli::color;
use crate::cli::output::{OutputConfig, OutputFormat};
const MARK_WIDTH: usize = 5;
const TITLE_WIDTH: usize = 11;
const CONTINUATION: &str = " ";
fn pad_to(text: &str, visible: usize, width: usize) -> String {
format!("{text}{}", " ".repeat(width.saturating_sub(visible)))
}
fn agent_cell(check: &Check, width: usize, color_enabled: bool) -> String {
if width == 0 {
return String::new();
}
match check.agent {
Some(agent) => pad_to(
&color::dim(agent, color_enabled),
agent.chars().count(),
width,
),
None => " ".repeat(width),
}
}
pub const EXIT_DEGRADED: i32 = 7;
mod verdict {
use std::sync::atomic::{AtomicI32, Ordering};
static PENDING: AtomicI32 = AtomicI32::new(0);
fn severity(code: i32) -> u8 {
match code {
0 => 0,
super::EXIT_DEGRADED => 1,
_ => 2,
}
}
pub fn record(code: i32) {
let mut current = PENDING.load(Ordering::SeqCst);
while severity(code) > severity(current) {
match PENDING.compare_exchange(current, code, Ordering::SeqCst, Ordering::SeqCst) {
Ok(_) => return,
Err(observed) => current = observed,
}
}
}
pub fn pending() -> i32 {
PENDING.load(Ordering::SeqCst)
}
#[cfg(test)]
pub fn reset() {
PENDING.store(0, Ordering::SeqCst);
}
}
#[cfg(test)]
pub use verdict::reset as reset_exit_code;
pub use verdict::{pending as pending_exit_code, record as record_exit_code};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Group {
Runtime,
Coverage,
Platform,
}
impl Group {
pub const ALL: [Group; 3] = [Group::Runtime, Group::Coverage, Group::Platform];
pub fn title(self) -> &'static str {
match self {
Group::Runtime => "Runtime",
Group::Coverage => "Coverage",
Group::Platform => "Platform",
}
}
pub fn key(self) -> &'static str {
match self {
Group::Runtime => "runtime",
Group::Coverage => "coverage",
Group::Platform => "platform",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Section {
Environment,
Daemon,
Hooks,
ModelRelay,
Persistence,
Connection,
Cloud,
Policy,
Inventory,
Telemetry,
Update,
Integrity,
}
impl Section {
pub const ALL: [Section; 12] = [
Section::Environment,
Section::Daemon,
Section::Persistence,
Section::Update,
Section::Hooks,
Section::ModelRelay,
Section::Policy,
Section::Inventory,
Section::Integrity,
Section::Connection,
Section::Cloud,
Section::Telemetry,
];
pub fn group(self) -> Group {
match self {
Section::Environment | Section::Daemon | Section::Persistence | Section::Update => {
Group::Runtime
}
Section::Hooks
| Section::ModelRelay
| Section::Policy
| Section::Inventory
| Section::Integrity => Group::Coverage,
Section::Connection | Section::Cloud | Section::Telemetry => Group::Platform,
}
}
pub fn of_group(group: Group) -> impl Iterator<Item = Section> {
Section::ALL.into_iter().filter(move |s| s.group() == group)
}
pub fn title(self) -> &'static str {
match self {
Section::Environment => "Environment",
Section::Daemon => "Daemon",
Section::Hooks => "Hooks",
Section::ModelRelay => "Model Relay",
Section::Persistence => "Persistence",
Section::Connection => "Connection",
Section::Cloud => "Cloud",
Section::Policy => "Policy",
Section::Inventory => "Inventory",
Section::Telemetry => "Telemetry",
Section::Update => "Update",
Section::Integrity => "Integrity",
}
}
pub fn key(self) -> &'static str {
match self {
Section::Environment => "environment",
Section::Daemon => "daemon",
Section::Hooks => "hooks",
Section::ModelRelay => "model_relay",
Section::Persistence => "persistence",
Section::Connection => "connection",
Section::Cloud => "cloud",
Section::Policy => "policy",
Section::Inventory => "inventory",
Section::Telemetry => "telemetry",
Section::Update => "update",
Section::Integrity => "integrity",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum State {
Ok,
Off,
Degraded,
Pending,
Failed,
Unknown(Section),
NotApplicable,
}
impl State {
fn severity(self) -> u8 {
match self {
State::NotApplicable => 0,
State::Ok => 1,
State::Pending => 2,
State::Off => 3,
State::Degraded => 4,
State::Unknown(_) => 5,
State::Failed => 6,
}
}
pub fn key(self) -> &'static str {
match self {
State::Ok => "ok",
State::Off => "off",
State::Degraded => "degraded",
State::Pending => "pending",
State::Failed => "failed",
State::Unknown(_) => "unknown",
State::NotApplicable => "not_applicable",
}
}
pub fn is_failure(self) -> bool {
self == State::Failed
}
pub fn is_warning(self) -> bool {
matches!(
self,
State::Off | State::Degraded | State::Pending | State::Unknown(_)
)
}
pub fn requires_remedy(self) -> bool {
self.is_failure() || self.is_warning()
}
pub fn mark(self, color_enabled: bool) -> String {
if color_enabled {
let glyph = match self {
State::Ok => color::checkmark(true).to_string(),
State::Failed => color::cross(true).to_string(),
State::NotApplicable => color::dim("\u{00b7}", true),
_ => color::warning_mark(true).to_string(),
};
format!("{glyph} ")
} else {
let label = match self {
State::Ok => "OK ",
State::Failed => "ERR ",
State::NotApplicable => "-- ",
_ => "WARN ",
};
label.to_string()
}
}
}
#[derive(Debug, Clone)]
pub struct Check {
pub section: Section,
pub state: State,
pub headline: String,
pub detail: Vec<String>,
pub code: Option<&'static str>,
pub remedy: Option<String>,
pub source: Option<String>,
pub agent: Option<&'static str>,
generated_headline: bool,
}
impl Check {
fn new(section: Section, state: State, headline: impl Into<String>) -> Self {
Self {
section,
state,
headline: headline.into(),
detail: Vec::new(),
code: None,
remedy: None,
source: None,
agent: None,
generated_headline: false,
}
}
pub fn ok(section: Section, headline: impl Into<String>) -> Self {
Self::new(section, State::Ok, headline)
}
pub fn off(section: Section, headline: impl Into<String>) -> Self {
Self::new(section, State::Off, headline)
}
pub fn degraded(section: Section, headline: impl Into<String>) -> Self {
Self::new(section, State::Degraded, headline)
}
pub fn pending(section: Section, headline: impl Into<String>) -> Self {
Self::new(section, State::Pending, headline)
}
pub fn failed(section: Section, headline: impl Into<String>) -> Self {
Self::new(section, State::Failed, headline)
}
pub fn unknown(section: Section, blocker: Section) -> Self {
Self {
section,
state: State::Unknown(blocker),
headline: format!("Cannot check — waiting on {}", blocker.title()),
detail: Vec::new(),
code: Some(crate::error::ERR_SUBSYSTEM_DEGRADED),
remedy: Some(format!(
"Fix {} first, then run `openlatch doctor` again.",
blocker.title()
)),
source: None,
agent: None,
generated_headline: true,
}
}
pub fn not_applicable(section: Section, headline: impl Into<String>) -> Self {
Self::new(section, State::NotApplicable, headline)
}
#[must_use]
pub fn code(mut self, code: &'static str) -> Self {
self.code = Some(code);
self
}
#[must_use]
pub fn remedy(mut self, remedy: impl Into<String>) -> Self {
self.remedy = Some(remedy.into());
self
}
#[must_use]
pub fn source(mut self, source: impl Into<String>) -> Self {
self.source = Some(source.into());
self
}
#[must_use]
pub fn headline(mut self, headline: impl Into<String>) -> Self {
self.headline = headline.into();
self.generated_headline = false;
self
}
#[must_use]
pub fn detail(mut self, line: impl Into<String>) -> Self {
self.detail.push(line.into());
self
}
#[must_use]
pub fn detail_opt(mut self, line: Option<impl Into<String>>) -> Self {
if let Some(line) = line {
self.detail.push(line.into());
}
self
}
#[must_use]
pub fn agent(mut self, agent: &'static str) -> Self {
self.agent = Some(agent);
self
}
pub fn validate(&self) -> Option<String> {
if !self.state.requires_remedy() {
return None;
}
match (self.code, &self.remedy) {
(Some(_), Some(_)) => None,
(None, Some(_)) => Some(format!(
"{}: '{}' is {} but carries no OL-XXXX code",
self.section.title(),
self.headline,
self.state.key()
)),
(Some(_), None) => Some(format!(
"{}: '{}' is {} but carries no remedy",
self.section.title(),
self.headline,
self.state.key()
)),
(None, None) => Some(format!(
"{}: '{}' is {} but carries neither code nor remedy",
self.section.title(),
self.headline,
self.state.key()
)),
}
}
pub fn to_json(&self) -> serde_json::Value {
let mut value = serde_json::json!({
"section": self.section.key(),
"state": self.state.key(),
"blocked_by": match self.state {
State::Unknown(b) => Some(b.key()),
_ => None,
},
"pass": !self.state.is_failure(),
"headline": self.headline,
"detail": self.detail,
"code": self.code,
"remedy": self.remedy,
"source": self.source,
});
if let (Some(agent), Some(object)) = (self.agent, value.as_object_mut()) {
object.insert("agent".into(), serde_json::json!(agent));
}
value
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Overall {
Healthy,
Degraded,
Broken,
}
impl Overall {
pub fn key(self) -> &'static str {
match self {
Overall::Healthy => "healthy",
Overall::Degraded => "degraded",
Overall::Broken => "broken",
}
}
fn label(self) -> &'static str {
match self {
Overall::Healthy => "HEALTHY",
Overall::Degraded => "DEGRADED",
Overall::Broken => "BROKEN",
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct Counts {
pub ok: usize,
pub warned: usize,
pub failed: usize,
}
#[derive(Debug, Clone, Default)]
pub struct Report {
checks: Vec<Check>,
}
impl Report {
pub fn new() -> Self {
Self::default()
}
pub fn push(&mut self, check: Check) {
debug_assert!(
check.validate().is_none(),
"contract violation: {}",
check.validate().unwrap_or_default()
);
self.checks.push(check);
}
#[must_use]
pub fn with(mut self, check: Check) -> Self {
self.push(check);
self
}
pub fn replace_section(&mut self, section: Section, check: Check) {
self.checks.retain(|c| c.section != section);
self.push(check);
}
pub fn is_empty(&self) -> bool {
self.checks.is_empty()
}
pub fn checks(&self) -> &[Check] {
&self.checks
}
pub fn section_checks(&self, section: Section) -> impl Iterator<Item = &Check> {
self.checks.iter().filter(move |c| c.section == section)
}
pub fn section_state(&self, section: Section) -> State {
self.section_checks(section)
.map(|c| c.state)
.max_by_key(|s| s.severity())
.unwrap_or(State::NotApplicable)
}
pub fn agents(&self) -> Vec<&'static str> {
let mut seen: Vec<&'static str> = Vec::new();
for agent in self.checks.iter().filter_map(|c| c.agent) {
if !seen.contains(&agent) {
seen.push(agent);
}
}
seen
}
pub fn agent_section_state(&self, agent: &str, section: Section) -> Option<State> {
self.section_checks(section)
.filter(|c| c.agent.is_some_and(|a| a == agent))
.map(|c| c.state)
.max_by_key(|s| s.severity())
}
fn agent_sections(&self) -> Vec<Section> {
Section::ALL
.into_iter()
.filter(|s| self.section_checks(*s).any(|c| c.agent.is_some()))
.collect()
}
fn agent_column_width(&self) -> usize {
let agents = self.agents();
if agents.len() < 2 {
return 0;
}
agents.iter().map(|a| a.chars().count()).max().unwrap_or(0) + 2
}
pub fn counts(&self) -> Counts {
let mut counts = Counts::default();
for check in &self.checks {
if check.state.is_failure() {
counts.failed += 1;
} else if check.state.is_warning() {
counts.warned += 1;
} else if check.state == State::Ok {
counts.ok += 1;
}
}
counts
}
pub fn overall(&self) -> Overall {
let counts = self.counts();
if counts.failed > 0 {
Overall::Broken
} else if counts.warned > 0 {
Overall::Degraded
} else {
Overall::Healthy
}
}
pub fn exit_code(&self) -> i32 {
match self.overall() {
Overall::Healthy => 0,
Overall::Degraded => EXIT_DEGRADED,
Overall::Broken => 1,
}
}
pub fn record_verdict(&self) {
record_exit_code(self.exit_code());
}
pub fn validate(&self) -> Vec<String> {
let mut problems: Vec<String> = self.checks.iter().filter_map(Check::validate).collect();
for section in Section::ALL {
if self.section_checks(section).next().is_none() {
problems.push(format!(
"{} has no check — every section is always reported (P6)",
section.title()
));
}
}
problems
}
pub fn issues(&self) -> Vec<String> {
self.checks
.iter()
.filter(|c| c.state.requires_remedy())
.map(|c| {
let mut line = format!("{} — {}", c.section.title(), c.headline);
if let Some(remedy) = &c.remedy {
line.push(' ');
line.push_str(remedy);
}
line
})
.collect()
}
pub fn render(&self, output: &OutputConfig) {
debug_assert!(
self.validate().is_empty(),
"contract violations: {:?}",
self.validate()
);
if output.format == OutputFormat::Json || output.quiet {
return;
}
let counts = self.counts();
let overall = self.overall();
let headline = format!(
"Overall: {} — {} failed, {} warning{}, {} ok",
overall.label(),
counts.failed,
counts.warned,
if counts.warned == 1 { "" } else { "s" },
counts.ok,
);
eprintln!(
"{}",
match overall {
Overall::Healthy => color::green(&headline, output.color),
Overall::Degraded => headline.clone(),
Overall::Broken => color::red(&headline, output.color),
}
);
eprintln!();
for group in Group::ALL {
eprintln!("{}", color::bold(group.title(), output.color));
for section in Section::of_group(group) {
self.render_section_line(section, output);
}
eprintln!();
}
let matrix = self.agent_matrix_lines(0, 2, output);
if !matrix.is_empty() {
for line in &matrix {
eprintln!("{line}");
}
eprintln!();
}
}
fn render_section_line(&self, section: Section, output: &OutputConfig) {
for line in self.section_lines(section, output) {
eprintln!("{line}");
}
}
fn section_lines(&self, section: Section, output: &OutputConfig) -> Vec<String> {
let state = self.section_state(section);
let title_cell = format!(" {:<TITLE_WIDTH$} ", section.title());
let blank_cell = " ".repeat(title_cell.len());
let interesting: Vec<&Check> = if output.verbose {
self.section_checks(section).collect()
} else {
self.section_checks(section)
.filter(|c| c.state.requires_remedy())
.collect()
};
if interesting.is_empty() {
return vec![format!(
"{title_cell}{}{}",
state.mark(output.color),
self.section_summary(section)
)];
}
let agent_width = if interesting.iter().any(|c| c.agent.is_some()) {
self.agent_column_width()
} else {
0
};
let indent = " ".repeat(title_cell.len() + MARK_WIDTH + agent_width);
let mut lines = Vec::new();
for (index, check) in interesting.iter().enumerate() {
let cell = if index == 0 { &title_cell } else { &blank_cell };
lines.push(format!(
"{cell}{}{}{}{}",
check.state.mark(output.color),
agent_cell(check, agent_width, output.color),
check.headline,
match check.code {
Some(code) if check.state.requires_remedy() =>
format!(" {}", color::dim(&format!("[{code}]"), output.color)),
_ => String::new(),
}
));
lines.extend(self.continuation_lines(check, &indent, output));
}
lines
}
fn agent_matrix_lines(
&self,
heading_indent: usize,
row_indent: usize,
output: &OutputConfig,
) -> Vec<String> {
let agents = self.agents();
let sections = self.agent_sections();
if agents.len() < 2 || sections.is_empty() {
return Vec::new();
}
let widths: Vec<usize> = agents
.iter()
.map(|a| a.chars().count().max(MARK_WIDTH) + 2)
.collect();
let mut lines = vec![format!(
"{}{}",
" ".repeat(heading_indent),
color::bold("Agents", output.color)
)];
let row = " ".repeat(row_indent);
let mut header = format!("{row}{}", " ".repeat(TITLE_WIDTH + 2));
for (agent, width) in agents.iter().zip(&widths) {
header.push_str(&pad_to(
&color::dim(agent, output.color),
agent.chars().count(),
*width,
));
}
lines.push(header.trim_end().to_string());
for section in sections {
let mut line = format!("{row}{:<TITLE_WIDTH$} ", section.title());
for (agent, width) in agents.iter().zip(&widths) {
line.push_str(&match self.agent_section_state(agent, section) {
Some(state) => pad_to(&state.mark(output.color), MARK_WIDTH, *width),
None => pad_to(&color::dim("\u{2014}", output.color), 1, *width),
});
}
lines.push(line.trim_end().to_string());
}
lines
}
fn section_summary(&self, section: Section) -> String {
let checks: Vec<&Check> = self.section_checks(section).collect();
match checks.as_slice() {
[] => "no data".to_string(),
[only] => only.headline.clone(),
many => {
let passed = many.iter().filter(|c| c.state == State::Ok).count();
let skipped = many.len() - passed;
if skipped == 0 {
format!("{passed} checks passed")
} else {
format!("{passed} passed, {skipped} not applicable")
}
}
}
}
fn continuation_lines(
&self,
check: &Check,
indent: &str,
output: &OutputConfig,
) -> Vec<String> {
let mut lines = Vec::new();
if let (State::Unknown(blocker), false) = (check.state, check.generated_headline) {
lines.push(format!(
"{indent}{}",
color::dim(&format!("waiting on : {}", blocker.title()), output.color)
));
}
for line in &check.detail {
lines.push(format!("{indent}{}", color::dim(line, output.color)));
}
if let Some(source) = &check.source {
lines.push(format!(
"{indent}{}",
color::dim(&format!("found in : {source}"), output.color)
));
}
if let Some(remedy) = &check.remedy {
lines.push(format!(
"{indent}{}",
color::dim(&format!("to fix : {remedy}"), output.color)
));
}
lines
}
pub fn render_section(&self, section: Section, output: &OutputConfig) {
if output.format == OutputFormat::Json || output.quiet {
return;
}
eprintln!("{}", color::bold(section.title(), output.color));
let checks: Vec<&Check> = self.section_checks(section).collect();
let agent_width = if checks.iter().any(|c| c.agent.is_some()) {
self.agent_column_width()
} else {
0
};
let indent = format!("{CONTINUATION}{}", " ".repeat(agent_width));
for check in checks {
eprintln!(
" {}{}{}{}",
check.state.mark(output.color),
agent_cell(check, agent_width, output.color),
check.headline,
match check.code {
Some(code) if check.state.requires_remedy() =>
format!(" {}", color::dim(&format!("[{code}]"), output.color)),
_ => String::new(),
}
);
for line in self.continuation_lines(check, &indent, output) {
eprintln!("{line}");
}
}
}
pub fn render_compact(&self, output: &OutputConfig) {
if output.format == OutputFormat::Json || output.quiet {
return;
}
for group in Group::ALL {
eprintln!(" {}", color::bold(group.title(), output.color));
for section in Section::of_group(group) {
let state = self.section_state(section);
let headline = if state.requires_remedy() {
self.section_checks(section)
.filter(|c| c.state == state)
.map(|c| c.headline.clone())
.next()
.unwrap_or_else(|| "no data".to_string())
} else {
self.section_summary(section)
};
eprintln!(
" {:<TITLE_WIDTH$} {}{}",
section.title(),
state.mark(output.color),
headline
);
}
}
let matrix = self.agent_matrix_lines(2, 4, output);
if !matrix.is_empty() {
eprintln!();
for line in &matrix {
eprintln!("{line}");
}
}
if self.overall() != Overall::Healthy {
eprintln!();
eprintln!(" Run `openlatch doctor` for causes and remedies.");
}
}
pub fn to_json(&self) -> serde_json::Value {
let counts = self.counts();
let mut agents = serde_json::Map::new();
for agent in self.agents() {
let sections: Vec<serde_json::Value> = Section::ALL
.iter()
.filter_map(|section| {
self.agent_section_state(agent, *section).map(|state| {
serde_json::json!({
"section": section.key(),
"state": state.key(),
})
})
})
.collect();
agents.insert(
agent.to_string(),
serde_json::json!({ "sections": sections }),
);
}
let sections: Vec<serde_json::Value> = Section::ALL
.iter()
.map(|section| {
serde_json::json!({
"section": section.key(),
"group": section.group().key(),
"state": self.section_state(*section).key(),
"summary": self.section_summary(*section),
"checks": self
.section_checks(*section)
.map(Check::to_json)
.collect::<Vec<_>>(),
})
})
.collect();
serde_json::json!({
"overall": self.overall().key(),
"exit_code": self.exit_code(),
"groups": Group::ALL
.iter()
.map(|g| serde_json::json!({
"group": g.key(),
"sections": Section::of_group(*g)
.map(|s| s.key())
.collect::<Vec<_>>(),
}))
.collect::<Vec<_>>(),
"summary": {
"ok": counts.ok,
"warned": counts.warned,
"failed": counts.failed,
},
"sections": sections,
"issues": self.issues(),
"agents": serde_json::Value::Object(agents),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn plain() -> OutputConfig {
OutputConfig {
format: OutputFormat::Human,
verbose: false,
debug: false,
quiet: true, color: false,
}
}
fn all_green() -> Report {
let mut report = Report::new();
for section in Section::ALL {
report.push(Check::ok(section, "fine"));
}
report
}
#[test]
fn a_healthy_report_exits_zero() {
let report = all_green();
assert_eq!(report.overall(), Overall::Healthy);
assert_eq!(report.exit_code(), 0);
}
#[test]
fn a_disabled_feature_warns_and_exits_two() {
let mut report = all_green();
report.push(
Check::off(Section::ModelRelay, "Disabled in config")
.code(crate::error::ERR_MODEL_RELAY_NOT_RUNNING)
.remedy("set [model_relay] enabled = true, then `openlatch restart`"),
);
assert_eq!(report.section_state(Section::ModelRelay), State::Off);
assert_eq!(report.overall(), Overall::Degraded);
assert_eq!(report.exit_code(), EXIT_DEGRADED);
}
#[test]
fn a_failure_outranks_a_warning() {
let mut report = all_green();
report.push(
Check::off(Section::Persistence, "Disabled")
.code(crate::error::ERR_NO_SUPERVISOR)
.remedy("`openlatch system supervision enable`"),
);
report.push(
Check::failed(Section::ModelRelay, "Cannot bind 7600")
.code(crate::error::ERR_MODEL_RELAY_PORT_IN_USE)
.remedy("free the port"),
);
assert_eq!(report.overall(), Overall::Broken);
assert_eq!(report.exit_code(), 1);
}
#[test]
fn a_section_takes_the_worst_state_of_its_checks() {
let mut report = all_green();
report.push(Check::ok(Section::Daemon, "port bound"));
report.push(
Check::degraded(Section::Daemon, "one subsystem restarting")
.code(crate::error::ERR_SUBSYSTEM_DEGRADED)
.remedy("check the daemon log"),
);
assert_eq!(report.section_state(Section::Daemon), State::Degraded);
}
#[test]
fn every_non_ok_check_carries_a_code_and_a_remedy() {
let offenders = [
Check::off(Section::Cloud, "x"),
Check::degraded(Section::Cloud, "x"),
Check::pending(Section::Cloud, "x"),
Check::failed(Section::Cloud, "x"),
];
for check in offenders {
assert!(
check.validate().is_some(),
"{} without code/remedy must be rejected",
check.state.key()
);
}
assert!(Check::unknown(Section::Cloud, Section::Daemon)
.validate()
.is_none());
assert!(Check::ok(Section::Cloud, "x").validate().is_none());
assert!(Check::not_applicable(Section::Cloud, "x")
.validate()
.is_none());
}
#[test]
fn a_dead_daemon_produces_exactly_one_failure() {
let mut report = Report::new();
report.push(Check::ok(Section::Environment, "fine"));
report.push(
Check::failed(Section::Daemon, "not running")
.code(crate::error::ERR_DAEMON_START_FAILED)
.remedy("`openlatch start`"),
);
for section in [
Section::Hooks,
Section::ModelRelay,
Section::Connection,
Section::Cloud,
Section::Policy,
Section::Inventory,
Section::Integrity,
] {
report.push(Check::unknown(section, Section::Daemon));
}
report.push(Check::ok(Section::Persistence, "fine"));
report.push(Check::not_applicable(Section::Telemetry, "opt-out"));
report.push(Check::ok(Section::Update, "current"));
assert_eq!(report.counts().failed, 1);
assert_eq!(report.overall(), Overall::Broken);
assert!(report.validate().is_empty(), "{:?}", report.validate());
}
#[test]
fn validate_rejects_a_missing_section() {
let mut report = Report::new();
report.push(Check::ok(Section::Daemon, "fine"));
let problems = report.validate();
assert!(
problems.iter().any(|p| p.contains("Model Relay")),
"a missing section must be reported: {problems:?}"
);
}
#[test]
fn every_section_belongs_to_exactly_one_non_empty_group() {
let grouped: Vec<Section> = Group::ALL
.iter()
.flat_map(|g| Section::of_group(*g))
.collect();
assert_eq!(
grouped.len(),
Section::ALL.len(),
"every section belongs to exactly one group"
);
for group in Group::ALL {
assert!(
Section::of_group(group).next().is_some(),
"{} has no sections",
group.title()
);
}
}
#[test]
fn section_order_follows_group_order() {
let grouped: Vec<Section> = Group::ALL
.iter()
.flat_map(|g| Section::of_group(*g))
.collect();
assert_eq!(grouped, Section::ALL.to_vec());
}
#[test]
fn a_green_section_collapses_and_a_problem_does_not() {
let mut report = Report::new();
for section in Section::ALL {
report.push(Check::ok(section, "fine"));
}
report.push(Check::ok(Section::Hooks, "also fine"));
assert_eq!(
report.section_summary(Section::Hooks),
"2 checks passed",
"several green checks collapse to a count"
);
assert_eq!(
report.section_summary(Section::Daemon),
"fine",
"a lone check speaks for itself"
);
report.push(Check::not_applicable(Section::Update, "not compiled in"));
assert_eq!(
report.section_summary(Section::Update),
"1 passed, 1 not applicable"
);
}
#[test]
fn human_and_json_carry_the_same_sections() {
let report = all_green();
let json = report.to_json();
let sections = json["sections"].as_array().expect("sections array");
assert_eq!(sections.len(), Section::ALL.len());
for (rendered, expected) in sections.iter().zip(Section::ALL) {
assert_eq!(rendered["section"], expected.key());
}
}
#[test]
fn json_keeps_the_legacy_pass_field_meaning_not_a_failure() {
let warn = Check::off(Section::ModelRelay, "off")
.code(crate::error::ERR_MODEL_RELAY_NOT_RUNNING)
.remedy("x");
assert_eq!(warn.to_json()["pass"], serde_json::Value::Bool(true));
let fail = Check::failed(Section::ModelRelay, "broken")
.code(crate::error::ERR_MODEL_RELAY_PORT_IN_USE)
.remedy("x");
assert_eq!(fail.to_json()["pass"], serde_json::Value::Bool(false));
}
#[test]
fn check_agent_is_absent_not_null_when_unset() {
let host_wide = Check::ok(Section::ModelRelay, "listening");
let json = host_wide.to_json();
assert!(
json.get("agent").is_none(),
"an unstamped check must carry no `agent` key, not a null one: {json}"
);
let stamped = Check::ok(Section::Hooks, "installed").agent("claude-code");
assert_eq!(stamped.to_json()["agent"], serde_json::json!("claude-code"));
}
#[test]
fn a_blocker_is_named_exactly_once() {
let generated = Check::unknown(Section::Policy, Section::Cloud);
assert!(
generated.headline.contains("Cloud"),
"the generated headline must name the blocker for the compact view"
);
assert!(
generated.generated_headline,
"an untouched `unknown` keeps its generated headline"
);
let overridden = Check::unknown(Section::Policy, Section::Cloud)
.headline("Enforcement state unknown — the platform is unreachable");
assert!(
!overridden.generated_headline,
"overriding the headline hands the blocker to the structural line"
);
assert!(
!overridden.headline.contains("Cloud"),
"an overridden headline is free not to repeat the blocker"
);
}
#[test]
fn marks_are_five_columns_wide_in_no_color_mode() {
for state in [
State::Ok,
State::Off,
State::Degraded,
State::Pending,
State::Failed,
State::Unknown(Section::Daemon),
State::NotApplicable,
] {
assert_eq!(state.mark(false).len(), 5, "{:?} misaligns", state);
}
}
#[test]
fn the_recorded_verdict_keeps_the_worst_code() {
reset_exit_code();
record_exit_code(EXIT_DEGRADED);
record_exit_code(1);
assert_eq!(pending_exit_code(), 1, "failure outranks degraded");
reset_exit_code();
record_exit_code(1);
record_exit_code(EXIT_DEGRADED);
assert_eq!(pending_exit_code(), 1, "degraded cannot clear a failure");
reset_exit_code();
record_exit_code(EXIT_DEGRADED);
record_exit_code(0);
assert_eq!(
pending_exit_code(),
EXIT_DEGRADED,
"success cannot clear a warning"
);
reset_exit_code();
assert_eq!(pending_exit_code(), 0);
}
#[test]
fn rendering_is_silent_in_quiet_mode() {
let report = all_green();
report.render(&plain());
report.render_compact(&plain());
let two = two_agent_report();
two.render(&plain());
two.render_compact(&plain());
}
fn unfolded() -> OutputConfig {
OutputConfig {
verbose: true,
..plain()
}
}
fn two_agent_report() -> Report {
let mut report = all_green();
report.push(Check::ok(Section::Environment, "Agent: Claude Code").agent("claude-code"));
report.push(Check::ok(Section::Environment, "Agent: Codex CLI").agent("codex-cli"));
report.push(Check::ok(Section::Hooks, "Enforced").agent("claude-code"));
report.push(
Check::failed(
Section::Hooks,
"Monitored — installed and capturing, enforcing nothing",
)
.code(crate::error::ERR_HOOK_CONFLICT)
.remedy("Trust the hook group.")
.agent("codex-cli"),
);
report.push(
Check::ok(Section::ModelRelay, "Agent wired to the listener").agent("claude-code"),
);
report
}
#[test]
fn agents_are_named_once_each_in_detection_order() {
let report = two_agent_report();
assert_eq!(report.agents(), vec!["claude-code", "codex-cli"]);
assert_eq!(
all_green().agents(),
Vec::<&str>::new(),
"a report with nothing tagged names no agents"
);
}
#[test]
fn a_cell_is_the_worst_of_that_agents_checks_in_that_section() {
let mut report = two_agent_report();
report.push(
Check::degraded(Section::Hooks, "Staged binary is a release behind")
.code(crate::error::ERR_HOOK_CONFLICT)
.remedy("`openlatch doctor --fix`")
.agent("claude-code"),
);
assert_eq!(
report.agent_section_state("claude-code", Section::Hooks),
Some(State::Degraded)
);
assert_eq!(
report.agent_section_state("codex-cli", Section::Hooks),
Some(State::Failed)
);
assert_eq!(report.section_state(Section::Hooks), State::Failed);
}
#[test]
fn an_agent_that_files_nothing_in_a_section_has_no_state() {
let report = two_agent_report();
assert_eq!(
report.agent_section_state("codex-cli", Section::ModelRelay),
None
);
let matrix = report.agent_matrix_lines(0, 2, &plain()).join("\n");
let relay = matrix
.lines()
.find(|l| l.contains(Section::ModelRelay.title()))
.expect("the matrix must carry a Model Relay row");
assert!(
relay.contains('\u{2014}'),
"an agent with no check there renders an em dash: {relay:?}"
);
assert!(
!relay.contains(State::NotApplicable.mark(false).trim()),
"the em dash must not be a NotApplicable mark in disguise: {relay:?}"
);
}
#[test]
fn a_host_wide_check_never_reaches_the_matrix() {
let mut report = all_green();
report.push(Check::ok(Section::Environment, "Agent: Claude Code").agent("claude-code"));
report.push(Check::ok(Section::Environment, "Agent: Codex CLI").agent("codex-cli"));
report.push(Check::ok(
Section::ModelRelay,
"Listening on 127.0.0.1:7600",
));
let matrix = report.agent_matrix_lines(0, 2, &plain()).join("\n");
assert!(
!matrix.contains(Section::ModelRelay.title()),
"an untagged section has no matrix row: {matrix}"
);
assert!(
matrix.contains(Section::Environment.title()),
"the tagged section does: {matrix}"
);
assert!(
report
.section_lines(Section::ModelRelay, &unfolded())
.iter()
.any(|l| l.contains("Listening on")),
"the host-wide check still renders in its own section"
);
}
#[test]
fn a_single_agent_host_renders_exactly_as_an_untagged_one() {
let mut tagged = all_green();
tagged.push(Check::ok(Section::Environment, "Agent: Claude Code").agent("claude-code"));
tagged.push(Check::ok(Section::Hooks, "Enforced").agent("claude-code"));
let mut untagged = all_green();
untagged.push(Check::ok(Section::Environment, "Agent: Claude Code"));
untagged.push(Check::ok(Section::Hooks, "Enforced"));
for section in Section::ALL {
assert_eq!(
tagged.section_lines(section, &unfolded()),
untagged.section_lines(section, &unfolded()),
"{} renders differently once a single agent is tagged",
section.title()
);
}
assert_eq!(tagged.agent_column_width(), 0);
assert!(tagged.agent_matrix_lines(0, 2, &unfolded()).is_empty());
}
#[test]
fn both_agents_are_named_on_the_rows_that_belong_to_them() {
let report = two_agent_report();
let hooks = report.section_lines(Section::Hooks, &unfolded());
let enforced = hooks
.iter()
.find(|l| l.contains("Enforced"))
.expect("the passing row");
let monitored = hooks
.iter()
.find(|l| l.contains("Monitored"))
.expect("the failing row");
assert!(enforced.contains("claude-code"), "{enforced:?}");
assert!(monitored.contains("codex-cli"), "{monitored:?}");
let host_wide = hooks
.iter()
.find(|l| l.contains("fine"))
.expect("the untagged row");
assert!(!host_wide.contains("claude-code") && !host_wide.contains("codex-cli"));
let column = |line: &str, needle: &str| line.find(needle).expect("headline");
assert_eq!(
column(enforced, "Enforced"),
column(monitored, "Monitored"),
"headlines must share a column"
);
assert_eq!(column(enforced, "Enforced"), column(host_wide, "fine"));
}
#[test]
fn the_matrix_has_one_column_per_agent_and_one_row_per_tagged_section() {
let report = two_agent_report();
let lines = report.agent_matrix_lines(0, 2, &plain());
let header = &lines[1];
assert!(header.contains("claude-code") && header.contains("codex-cli"));
let rows: Vec<&String> = lines[2..].iter().collect();
let titles: Vec<&str> = rows
.iter()
.map(|l| l.trim_start().split(" ").next().unwrap_or_default())
.collect();
assert_eq!(
titles,
vec![
Section::Environment.title(),
Section::Hooks.title(),
Section::ModelRelay.title()
],
"only the sections carrying agent-tagged checks get a row, in section order"
);
}
#[test]
fn three_agents_still_fit_eighty_columns() {
let mut report = all_green();
for agent in ["claude-code", "codex-cli", "cline"] {
report.push(Check::ok(Section::Environment, format!("Agent: {agent}")).agent(agent));
report.push(Check::ok(Section::Hooks, "Enforced").agent(agent));
}
for line in report.agent_matrix_lines(2, 4, &plain()) {
assert!(
line.chars().count() <= 80,
"the matrix must not overflow 80 columns: {} wide — {line:?}",
line.chars().count()
);
}
}
#[test]
fn json_gains_a_per_agent_rollup_and_moves_nothing() {
let report = two_agent_report();
let before = all_green().to_json();
let json = report.to_json();
for key in [
"overall",
"exit_code",
"groups",
"summary",
"sections",
"issues",
] {
assert!(json.get(key).is_some(), "{key} must survive");
}
assert_eq!(before["agents"], serde_json::json!({}));
let agents = json["agents"].as_object().expect("an agents object");
assert_eq!(agents.len(), 2);
assert!(agents.contains_key("claude-code") && agents.contains_key("codex-cli"));
let codex: Vec<(&str, &str)> = agents["codex-cli"]["sections"]
.as_array()
.expect("sections")
.iter()
.map(|s| {
(
s["section"].as_str().unwrap_or_default(),
s["state"].as_str().unwrap_or_default(),
)
})
.collect();
assert_eq!(
codex,
vec![("environment", "ok"), ("hooks", "failed")],
"a section the agent files nothing in is absent, not null"
);
}
#[test]
fn the_rollup_is_an_object_a_consumer_can_index() {
let json = two_agent_report().to_json();
assert!(
json["agents"].is_object(),
"the rollup is keyed by agent, not listed: {}",
json["agents"]
);
assert!(json["agents"]["claude-code"]["sections"].is_array());
}
}