#[derive(Debug, thiserror::Error)]
pub enum PruneError {
#[error("session manager unavailable (daemon unreachable or SM disabled); nothing to prune")]
SmUnavailable,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PruneAction {
Stop,
Decommission,
Skip(&'static str),
}
impl PruneAction {
pub fn label(&self) -> &'static str {
match self {
PruneAction::Stop => "stop",
PruneAction::Decommission => "decommission",
PruneAction::Skip(_) => "skip",
}
}
pub fn is_actionable(&self) -> bool {
!matches!(self, PruneAction::Skip(_))
}
}
pub fn normalize_verdict(verdict: &str) -> String {
verdict
.chars()
.filter(|c| !matches!(c, '-' | '_') && !c.is_whitespace())
.flat_map(|c| c.to_lowercase())
.collect()
}
pub fn decide(verdict: Option<&str>) -> PruneAction {
let Some(raw) = verdict else {
return PruneAction::Skip("no verdict yet");
};
match normalize_verdict(raw).as_str() {
"idle" => PruneAction::Stop,
"done" => PruneAction::Decommission,
"working" => PruneAction::Skip("working"),
"blockedonpermission" => PruneAction::Skip("blocked on permission"),
"errored" => PruneAction::Skip("errored"),
"unknown" | "" => PruneAction::Skip("unknown / no classifier"),
_ => PruneAction::Skip("unrecognized verdict"),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn decide_idle_stops() {
assert_eq!(decide(Some("idle")), PruneAction::Stop);
}
#[test]
fn decide_done_decommissions() {
assert_eq!(decide(Some("done")), PruneAction::Decommission);
}
#[test]
fn decide_working_skips() {
let action = decide(Some("working"));
assert!(matches!(action, PruneAction::Skip(_)));
assert!(!action.is_actionable());
}
#[test]
fn decide_blocked_skips() {
for spelling in [
"blockedonpermission",
"blocked-on-permission",
"blocked_on_permission",
"Blocked On Permission",
] {
assert!(
matches!(decide(Some(spelling)), PruneAction::Skip(_)),
"spelling {spelling:?} should skip"
);
}
}
#[test]
fn decide_errored_skips() {
assert!(matches!(decide(Some("errored")), PruneAction::Skip(_)));
}
#[test]
fn decide_unknown_skips() {
assert!(matches!(decide(Some("unknown")), PruneAction::Skip(_)));
}
#[test]
fn decide_no_verdict_skips() {
assert_eq!(decide(None), PruneAction::Skip("no verdict yet"));
}
#[test]
fn decide_unrecognized_skips() {
assert!(matches!(
decide(Some("compacting")),
PruneAction::Skip("unrecognized verdict")
));
}
#[test]
fn action_label_is_stable() {
assert_eq!(PruneAction::Stop.label(), "stop");
assert_eq!(PruneAction::Decommission.label(), "decommission");
assert_eq!(PruneAction::Skip("x").label(), "skip");
}
#[test]
fn is_actionable_distinguishes_skip() {
assert!(PruneAction::Stop.is_actionable());
assert!(PruneAction::Decommission.is_actionable());
assert!(!PruneAction::Skip("x").is_actionable());
}
#[test]
fn normalize_verdict_strips_separators() {
assert_eq!(
normalize_verdict("blocked-on_permission"),
"blockedonpermission"
);
assert_eq!(
normalize_verdict("blocked on permission"),
"blockedonpermission"
);
}
#[test]
fn normalize_verdict_lowercases() {
assert_eq!(normalize_verdict("IDLE"), "idle");
assert_eq!(normalize_verdict("Done"), "done");
}
#[test]
fn prune_error_is_sm_unavailable() {
let msg = PruneError::SmUnavailable.to_string();
assert!(msg.contains("session manager unavailable"), "{msg}");
assert!(msg.contains("nothing to prune"), "{msg}");
}
}