use serde::{Deserialize, Serialize};
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum LifecycleState {
Provisioning,
Active,
Suspended,
Rotating,
Degraded,
Quarantined,
Decommissioning,
Decommissioned,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LifecycleEvent {
pub from: LifecycleState,
pub to: LifecycleState,
pub reason: String,
pub initiated_by: String,
pub timestamp: u64,
}
pub struct LifecycleManager {
agent_id: String,
state: LifecycleState,
events: Vec<LifecycleEvent>,
}
impl LifecycleManager {
pub fn new(agent_id: &str) -> Self {
Self {
agent_id: agent_id.to_string(),
state: LifecycleState::Provisioning,
events: Vec::new(),
}
}
pub fn state(&self) -> LifecycleState {
self.state
}
pub fn agent_id(&self) -> &str {
&self.agent_id
}
pub fn events(&self) -> &[LifecycleEvent] {
&self.events
}
pub fn transition(
&mut self,
to: LifecycleState,
reason: &str,
initiated_by: &str,
) -> Result<&LifecycleEvent, String> {
if !self.can_transition(to) {
return Err(format!(
"invalid transition from {:?} to {:?}",
self.state, to
));
}
let event = LifecycleEvent {
from: self.state,
to,
reason: reason.to_string(),
initiated_by: initiated_by.to_string(),
timestamp: epoch_now(),
};
self.state = to;
self.events.push(event);
Ok(self.events.last().expect("just pushed"))
}
pub fn can_transition(&self, to: LifecycleState) -> bool {
allowed_transitions(self.state).contains(&to)
}
pub fn activate(&mut self, reason: &str) -> Result<&LifecycleEvent, String> {
self.transition(LifecycleState::Active, reason, "system")
}
pub fn suspend(&mut self, reason: &str) -> Result<&LifecycleEvent, String> {
self.transition(LifecycleState::Suspended, reason, "system")
}
pub fn quarantine(&mut self, reason: &str) -> Result<&LifecycleEvent, String> {
self.transition(LifecycleState::Quarantined, reason, "system")
}
pub fn decommission(&mut self, reason: &str) -> Result<&LifecycleEvent, String> {
self.transition(LifecycleState::Decommissioning, reason, "system")
}
}
fn allowed_transitions(from: LifecycleState) -> &'static [LifecycleState] {
use LifecycleState::*;
match from {
Provisioning => &[Active],
Active => &[Suspended, Rotating, Degraded, Decommissioning],
Suspended => &[Active, Decommissioning],
Rotating => &[Active],
Degraded => &[Active, Quarantined, Decommissioning],
Quarantined => &[Active, Decommissioning],
Decommissioning => &[Decommissioned],
Decommissioned => &[],
}
}
fn epoch_now() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_initial_state_is_provisioning() {
let mgr = LifecycleManager::new("agent-1");
assert_eq!(mgr.state(), LifecycleState::Provisioning);
assert_eq!(mgr.agent_id(), "agent-1");
assert!(mgr.events().is_empty());
}
#[test]
fn test_activate_from_provisioning() {
let mut mgr = LifecycleManager::new("agent-1");
let event = mgr.activate("initial activation").unwrap();
assert_eq!(event.from, LifecycleState::Provisioning);
assert_eq!(event.to, LifecycleState::Active);
assert_eq!(event.reason, "initial activation");
assert_eq!(mgr.state(), LifecycleState::Active);
}
#[test]
fn test_suspend_from_active() {
let mut mgr = LifecycleManager::new("agent-1");
mgr.activate("boot").unwrap();
let event = mgr.suspend("maintenance window").unwrap();
assert_eq!(event.from, LifecycleState::Active);
assert_eq!(event.to, LifecycleState::Suspended);
assert_eq!(mgr.state(), LifecycleState::Suspended);
}
#[test]
fn test_reactivate_from_suspended() {
let mut mgr = LifecycleManager::new("agent-1");
mgr.activate("boot").unwrap();
mgr.suspend("pause").unwrap();
let event = mgr.activate("resume").unwrap();
assert_eq!(event.from, LifecycleState::Suspended);
assert_eq!(event.to, LifecycleState::Active);
}
#[test]
fn test_quarantine_from_degraded() {
let mut mgr = LifecycleManager::new("agent-1");
mgr.activate("boot").unwrap();
mgr.transition(LifecycleState::Degraded, "high error rate", "monitor")
.unwrap();
let event = mgr.quarantine("policy violation detected").unwrap();
assert_eq!(event.from, LifecycleState::Degraded);
assert_eq!(event.to, LifecycleState::Quarantined);
}
#[test]
fn test_decommission_flow() {
let mut mgr = LifecycleManager::new("agent-1");
mgr.activate("boot").unwrap();
mgr.decommission("end of life").unwrap();
assert_eq!(mgr.state(), LifecycleState::Decommissioning);
mgr.transition(LifecycleState::Decommissioned, "cleanup done", "system")
.unwrap();
assert_eq!(mgr.state(), LifecycleState::Decommissioned);
}
#[test]
fn test_decommissioned_is_terminal() {
let mut mgr = LifecycleManager::new("agent-1");
mgr.activate("boot").unwrap();
mgr.decommission("bye").unwrap();
mgr.transition(LifecycleState::Decommissioned, "done", "system")
.unwrap();
let result = mgr.activate("try again");
assert!(result.is_err());
assert!(result
.unwrap_err()
.contains("invalid transition from Decommissioned"));
}
#[test]
fn test_invalid_transition_from_provisioning() {
let mut mgr = LifecycleManager::new("agent-1");
let result = mgr.suspend("not allowed");
assert!(result.is_err());
}
#[test]
fn test_invalid_transition_returns_descriptive_error() {
let mut mgr = LifecycleManager::new("agent-1");
let err = mgr.suspend("nope").unwrap_err();
assert!(err.contains("Provisioning"));
assert!(err.contains("Suspended"));
}
#[test]
fn test_can_transition_returns_true_for_valid() {
let mut mgr = LifecycleManager::new("agent-1");
assert!(mgr.can_transition(LifecycleState::Active));
assert!(!mgr.can_transition(LifecycleState::Suspended));
mgr.activate("boot").unwrap();
assert!(mgr.can_transition(LifecycleState::Suspended));
assert!(mgr.can_transition(LifecycleState::Rotating));
assert!(mgr.can_transition(LifecycleState::Degraded));
assert!(mgr.can_transition(LifecycleState::Decommissioning));
assert!(!mgr.can_transition(LifecycleState::Quarantined));
}
#[test]
fn test_rotating_returns_to_active() {
let mut mgr = LifecycleManager::new("agent-1");
mgr.activate("boot").unwrap();
mgr.transition(LifecycleState::Rotating, "key rotation", "security")
.unwrap();
assert_eq!(mgr.state(), LifecycleState::Rotating);
mgr.activate("rotation complete").unwrap();
assert_eq!(mgr.state(), LifecycleState::Active);
}
#[test]
fn test_event_history_records_all_transitions() {
let mut mgr = LifecycleManager::new("agent-1");
mgr.activate("boot").unwrap();
mgr.suspend("pause").unwrap();
mgr.activate("resume").unwrap();
let events = mgr.events();
assert_eq!(events.len(), 3);
assert_eq!(events[0].to, LifecycleState::Active);
assert_eq!(events[1].to, LifecycleState::Suspended);
assert_eq!(events[2].to, LifecycleState::Active);
}
#[test]
fn test_event_timestamps_are_monotonic() {
let mut mgr = LifecycleManager::new("agent-1");
mgr.activate("boot").unwrap();
mgr.suspend("pause").unwrap();
mgr.activate("resume").unwrap();
let events = mgr.events();
for window in events.windows(2) {
assert!(window[1].timestamp >= window[0].timestamp);
}
}
#[test]
fn test_lifecycle_state_serde_roundtrip() {
let state = LifecycleState::Quarantined;
let json = serde_json::to_string(&state).unwrap();
let deserialized: LifecycleState = serde_json::from_str(&json).unwrap();
assert_eq!(state, deserialized);
}
#[test]
fn test_lifecycle_event_serde_roundtrip() {
let event = LifecycleEvent {
from: LifecycleState::Active,
to: LifecycleState::Suspended,
reason: "maintenance".to_string(),
initiated_by: "admin".to_string(),
timestamp: 1700000000,
};
let json = serde_json::to_string(&event).unwrap();
let deserialized: LifecycleEvent = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.from, event.from);
assert_eq!(deserialized.to, event.to);
assert_eq!(deserialized.reason, event.reason);
}
#[test]
fn test_quarantined_can_reactivate() {
let mut mgr = LifecycleManager::new("agent-1");
mgr.activate("boot").unwrap();
mgr.transition(LifecycleState::Degraded, "issues", "monitor")
.unwrap();
mgr.quarantine("violation").unwrap();
mgr.activate("cleared").unwrap();
assert_eq!(mgr.state(), LifecycleState::Active);
}
#[test]
fn test_quarantined_can_decommission() {
let mut mgr = LifecycleManager::new("agent-1");
mgr.activate("boot").unwrap();
mgr.transition(LifecycleState::Degraded, "issues", "monitor")
.unwrap();
mgr.quarantine("violation").unwrap();
mgr.decommission("permanent removal").unwrap();
assert_eq!(mgr.state(), LifecycleState::Decommissioning);
}
}