use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorkerObservation {
pub worker_id: String,
pub configured: bool,
pub in_daemon_pool: bool,
pub reachable: bool,
pub admin_disabled: bool,
pub temporarily_bypassed: bool,
pub facts_known: bool,
pub command_admissible: bool,
}
impl WorkerObservation {
#[must_use]
pub fn ready(worker_id: impl Into<String>) -> Self {
Self {
worker_id: worker_id.into(),
configured: true,
in_daemon_pool: true,
reachable: true,
admin_disabled: false,
temporarily_bypassed: false,
facts_known: true,
command_admissible: true,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum WorkerDiffState {
Ready,
MissingFromFleet,
RecoveredNotRejoined,
AdminDisabled,
TemporarilyBypassed,
Unreachable,
FactsUnknown,
CommandIneligible,
Unconfigured,
}
impl WorkerDiffState {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
WorkerDiffState::Ready => "ready",
WorkerDiffState::MissingFromFleet => "missing_from_fleet",
WorkerDiffState::RecoveredNotRejoined => "recovered_not_rejoined",
WorkerDiffState::AdminDisabled => "admin_disabled",
WorkerDiffState::TemporarilyBypassed => "temporarily_bypassed",
WorkerDiffState::Unreachable => "unreachable",
WorkerDiffState::FactsUnknown => "facts_unknown",
WorkerDiffState::CommandIneligible => "command_ineligible",
WorkerDiffState::Unconfigured => "unconfigured",
}
}
#[must_use]
pub fn is_usable(self) -> bool {
matches!(self, WorkerDiffState::Ready)
}
}
#[must_use]
pub fn derive_worker_diff(obs: &WorkerObservation) -> WorkerDiffState {
if obs.admin_disabled {
return WorkerDiffState::AdminDisabled;
}
if obs.temporarily_bypassed {
return WorkerDiffState::TemporarilyBypassed;
}
if obs.configured && !obs.in_daemon_pool {
return if obs.reachable {
WorkerDiffState::RecoveredNotRejoined
} else {
WorkerDiffState::MissingFromFleet
};
}
if obs.in_daemon_pool && !obs.reachable {
return WorkerDiffState::Unreachable;
}
if !obs.configured && obs.in_daemon_pool {
return WorkerDiffState::Unconfigured;
}
if !obs.facts_known {
return WorkerDiffState::FactsUnknown;
}
if !obs.command_admissible {
return WorkerDiffState::CommandIneligible;
}
WorkerDiffState::Ready
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WorkerDiffRow {
pub worker_id: String,
pub state: WorkerDiffState,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FleetDiff {
pub workers: Vec<WorkerDiffRow>,
pub state_counts: BTreeMap<String, usize>,
pub ready_workers: usize,
pub explanation: String,
}
impl FleetDiff {
#[must_use]
pub fn capacity_collapsed(&self) -> bool {
self.ready_workers == 0 && !self.workers.is_empty()
}
#[must_use]
pub fn count(&self, state: WorkerDiffState) -> usize {
self.state_counts.get(state.as_str()).copied().unwrap_or(0)
}
}
#[must_use]
pub fn build_fleet_diff(observations: &[WorkerObservation]) -> FleetDiff {
let mut workers = Vec::with_capacity(observations.len());
let mut state_counts: BTreeMap<String, usize> = BTreeMap::new();
let mut ready_workers = 0;
for obs in observations {
let state = derive_worker_diff(obs);
if state.is_usable() {
ready_workers += 1;
}
*state_counts.entry(state.as_str().to_string()).or_insert(0) += 1;
workers.push(WorkerDiffRow {
worker_id: obs.worker_id.clone(),
state,
});
}
let explanation = if observations.is_empty() {
"no workers configured".to_string()
} else if ready_workers > 0 {
format!("{ready_workers}/{} worker(s) ready", observations.len())
} else {
let mut parts: Vec<String> = state_counts
.iter()
.map(|(state, n)| format!("{state}={n}"))
.collect();
parts.sort();
format!(
"0/{} ready — capacity collapse: {}",
observations.len(),
parts.join(", ")
)
};
FleetDiff {
workers,
state_counts,
ready_workers,
explanation,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn missing_configured_worker() {
let mut obs = WorkerObservation::ready("css");
obs.in_daemon_pool = false;
obs.reachable = false;
assert_eq!(derive_worker_diff(&obs), WorkerDiffState::MissingFromFleet);
}
#[test]
fn recovered_but_not_rejoined_worker() {
let mut obs = WorkerObservation::ready("css");
obs.in_daemon_pool = false;
obs.reachable = true;
assert_eq!(
derive_worker_diff(&obs),
WorkerDiffState::RecoveredNotRejoined
);
}
#[test]
fn admin_disabled_worker() {
let mut obs = WorkerObservation::ready("css");
obs.admin_disabled = true;
obs.reachable = false;
obs.in_daemon_pool = false;
assert_eq!(derive_worker_diff(&obs), WorkerDiffState::AdminDisabled);
}
#[test]
fn healthy_worker_rejected_only_for_a_specific_command_capability() {
let mut obs = WorkerObservation::ready("css");
obs.command_admissible = false;
assert_eq!(derive_worker_diff(&obs), WorkerDiffState::CommandIneligible);
}
#[test]
fn temporary_bypass_and_unreachable_and_unconfigured() {
let mut bypass = WorkerObservation::ready("a");
bypass.temporarily_bypassed = true;
assert_eq!(
derive_worker_diff(&bypass),
WorkerDiffState::TemporarilyBypassed
);
let mut unreachable = WorkerObservation::ready("b");
unreachable.reachable = false; assert_eq!(
derive_worker_diff(&unreachable),
WorkerDiffState::Unreachable
);
let mut orphan = WorkerObservation::ready("c");
orphan.configured = false; assert_eq!(derive_worker_diff(&orphan), WorkerDiffState::Unconfigured);
}
#[test]
fn ready_when_all_green() {
assert_eq!(
derive_worker_diff(&WorkerObservation::ready("css")),
WorkerDiffState::Ready
);
}
#[test]
fn fleet_diff_explains_capacity_collapse() {
let mut admin = WorkerObservation::ready("a");
admin.admin_disabled = true;
let mut unreachable = WorkerObservation::ready("b");
unreachable.reachable = false;
let mut ineligible = WorkerObservation::ready("c");
ineligible.command_admissible = false;
let diff = build_fleet_diff(&[admin, unreachable, ineligible]);
assert_eq!(diff.ready_workers, 0);
assert!(diff.capacity_collapsed());
assert_eq!(diff.count(WorkerDiffState::AdminDisabled), 1);
assert_eq!(diff.count(WorkerDiffState::Unreachable), 1);
assert_eq!(diff.count(WorkerDiffState::CommandIneligible), 1);
assert!(diff.explanation.contains("capacity collapse"));
assert!(diff.explanation.contains("admin_disabled=1"));
assert!(diff.explanation.contains("command_ineligible=1"));
}
#[test]
fn fleet_diff_reports_ready_count() {
let diff = build_fleet_diff(&[WorkerObservation::ready("a"), {
let mut b = WorkerObservation::ready("b");
b.command_admissible = false;
b
}]);
assert_eq!(diff.ready_workers, 1);
assert!(!diff.capacity_collapsed());
assert!(diff.explanation.contains("1/2 worker(s) ready"));
}
#[test]
fn empty_fleet_is_explained() {
let diff = build_fleet_diff(&[]);
assert_eq!(diff.ready_workers, 0);
assert!(!diff.capacity_collapsed());
assert!(diff.explanation.contains("no workers configured"));
}
#[test]
fn fleet_diff_serializes_with_stable_tokens() {
let diff = build_fleet_diff(&[{
let mut a = WorkerObservation::ready("a");
a.admin_disabled = true;
a
}]);
let value = serde_json::to_value(&diff).unwrap();
assert_eq!(value["workers"][0]["state"], "admin_disabled");
assert_eq!(value["state_counts"]["admin_disabled"], 1);
let back: FleetDiff = serde_json::from_value(value).unwrap();
assert_eq!(back, diff);
}
}