1use serde::{Deserialize, Serialize};
8use std::time::{SystemTime, UNIX_EPOCH};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
12pub enum LifecycleState {
13 Provisioning,
15 Active,
17 Suspended,
19 Rotating,
21 Degraded,
23 Quarantined,
25 Decommissioning,
27 Decommissioned,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct LifecycleEvent {
34 pub from: LifecycleState,
36 pub to: LifecycleState,
38 pub reason: String,
40 pub initiated_by: String,
42 pub timestamp: u64,
44}
45
46pub struct LifecycleManager {
48 agent_id: String,
49 state: LifecycleState,
50 events: Vec<LifecycleEvent>,
51}
52
53impl LifecycleManager {
54 pub fn new(agent_id: &str) -> Self {
58 Self {
59 agent_id: agent_id.to_string(),
60 state: LifecycleState::Provisioning,
61 events: Vec::new(),
62 }
63 }
64
65 pub fn state(&self) -> LifecycleState {
67 self.state
68 }
69
70 pub fn agent_id(&self) -> &str {
72 &self.agent_id
73 }
74
75 pub fn events(&self) -> &[LifecycleEvent] {
77 &self.events
78 }
79
80 pub fn transition(
85 &mut self,
86 to: LifecycleState,
87 reason: &str,
88 initiated_by: &str,
89 ) -> Result<&LifecycleEvent, String> {
90 if !self.can_transition(to) {
91 return Err(format!(
92 "invalid transition from {:?} to {:?}",
93 self.state, to
94 ));
95 }
96
97 let event = LifecycleEvent {
98 from: self.state,
99 to,
100 reason: reason.to_string(),
101 initiated_by: initiated_by.to_string(),
102 timestamp: epoch_now(),
103 };
104 self.state = to;
105 self.events.push(event);
106 Ok(self.events.last().expect("just pushed"))
107 }
108
109 pub fn can_transition(&self, to: LifecycleState) -> bool {
111 allowed_transitions(self.state).contains(&to)
112 }
113
114 pub fn activate(&mut self, reason: &str) -> Result<&LifecycleEvent, String> {
116 self.transition(LifecycleState::Active, reason, "system")
117 }
118
119 pub fn suspend(&mut self, reason: &str) -> Result<&LifecycleEvent, String> {
121 self.transition(LifecycleState::Suspended, reason, "system")
122 }
123
124 pub fn quarantine(&mut self, reason: &str) -> Result<&LifecycleEvent, String> {
126 self.transition(LifecycleState::Quarantined, reason, "system")
127 }
128
129 pub fn decommission(&mut self, reason: &str) -> Result<&LifecycleEvent, String> {
131 self.transition(LifecycleState::Decommissioning, reason, "system")
132 }
133}
134
135fn allowed_transitions(from: LifecycleState) -> &'static [LifecycleState] {
137 use LifecycleState::*;
138 match from {
139 Provisioning => &[Active],
140 Active => &[Suspended, Rotating, Degraded, Decommissioning],
141 Suspended => &[Active, Decommissioning],
142 Rotating => &[Active],
143 Degraded => &[Active, Quarantined, Decommissioning],
144 Quarantined => &[Active, Decommissioning],
145 Decommissioning => &[Decommissioned],
146 Decommissioned => &[],
147 }
148}
149
150fn epoch_now() -> u64 {
151 SystemTime::now()
152 .duration_since(UNIX_EPOCH)
153 .unwrap_or_default()
154 .as_secs()
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160
161 #[test]
162 fn test_initial_state_is_provisioning() {
163 let mgr = LifecycleManager::new("agent-1");
164 assert_eq!(mgr.state(), LifecycleState::Provisioning);
165 assert_eq!(mgr.agent_id(), "agent-1");
166 assert!(mgr.events().is_empty());
167 }
168
169 #[test]
170 fn test_activate_from_provisioning() {
171 let mut mgr = LifecycleManager::new("agent-1");
172 let event = mgr.activate("initial activation").unwrap();
173 assert_eq!(event.from, LifecycleState::Provisioning);
174 assert_eq!(event.to, LifecycleState::Active);
175 assert_eq!(event.reason, "initial activation");
176 assert_eq!(mgr.state(), LifecycleState::Active);
177 }
178
179 #[test]
180 fn test_suspend_from_active() {
181 let mut mgr = LifecycleManager::new("agent-1");
182 mgr.activate("boot").unwrap();
183 let event = mgr.suspend("maintenance window").unwrap();
184 assert_eq!(event.from, LifecycleState::Active);
185 assert_eq!(event.to, LifecycleState::Suspended);
186 assert_eq!(mgr.state(), LifecycleState::Suspended);
187 }
188
189 #[test]
190 fn test_reactivate_from_suspended() {
191 let mut mgr = LifecycleManager::new("agent-1");
192 mgr.activate("boot").unwrap();
193 mgr.suspend("pause").unwrap();
194 let event = mgr.activate("resume").unwrap();
195 assert_eq!(event.from, LifecycleState::Suspended);
196 assert_eq!(event.to, LifecycleState::Active);
197 }
198
199 #[test]
200 fn test_quarantine_from_degraded() {
201 let mut mgr = LifecycleManager::new("agent-1");
202 mgr.activate("boot").unwrap();
203 mgr.transition(LifecycleState::Degraded, "high error rate", "monitor")
204 .unwrap();
205 let event = mgr.quarantine("policy violation detected").unwrap();
206 assert_eq!(event.from, LifecycleState::Degraded);
207 assert_eq!(event.to, LifecycleState::Quarantined);
208 }
209
210 #[test]
211 fn test_decommission_flow() {
212 let mut mgr = LifecycleManager::new("agent-1");
213 mgr.activate("boot").unwrap();
214 mgr.decommission("end of life").unwrap();
215 assert_eq!(mgr.state(), LifecycleState::Decommissioning);
216
217 mgr.transition(LifecycleState::Decommissioned, "cleanup done", "system")
218 .unwrap();
219 assert_eq!(mgr.state(), LifecycleState::Decommissioned);
220 }
221
222 #[test]
223 fn test_decommissioned_is_terminal() {
224 let mut mgr = LifecycleManager::new("agent-1");
225 mgr.activate("boot").unwrap();
226 mgr.decommission("bye").unwrap();
227 mgr.transition(LifecycleState::Decommissioned, "done", "system")
228 .unwrap();
229
230 let result = mgr.activate("try again");
231 assert!(result.is_err());
232 assert!(result
233 .unwrap_err()
234 .contains("invalid transition from Decommissioned"));
235 }
236
237 #[test]
238 fn test_invalid_transition_from_provisioning() {
239 let mut mgr = LifecycleManager::new("agent-1");
240 let result = mgr.suspend("not allowed");
241 assert!(result.is_err());
242 }
243
244 #[test]
245 fn test_invalid_transition_returns_descriptive_error() {
246 let mut mgr = LifecycleManager::new("agent-1");
247 let err = mgr.suspend("nope").unwrap_err();
248 assert!(err.contains("Provisioning"));
249 assert!(err.contains("Suspended"));
250 }
251
252 #[test]
253 fn test_can_transition_returns_true_for_valid() {
254 let mut mgr = LifecycleManager::new("agent-1");
255 assert!(mgr.can_transition(LifecycleState::Active));
256 assert!(!mgr.can_transition(LifecycleState::Suspended));
257
258 mgr.activate("boot").unwrap();
259 assert!(mgr.can_transition(LifecycleState::Suspended));
260 assert!(mgr.can_transition(LifecycleState::Rotating));
261 assert!(mgr.can_transition(LifecycleState::Degraded));
262 assert!(mgr.can_transition(LifecycleState::Decommissioning));
263 assert!(!mgr.can_transition(LifecycleState::Quarantined));
264 }
265
266 #[test]
267 fn test_rotating_returns_to_active() {
268 let mut mgr = LifecycleManager::new("agent-1");
269 mgr.activate("boot").unwrap();
270 mgr.transition(LifecycleState::Rotating, "key rotation", "security")
271 .unwrap();
272 assert_eq!(mgr.state(), LifecycleState::Rotating);
273
274 mgr.activate("rotation complete").unwrap();
275 assert_eq!(mgr.state(), LifecycleState::Active);
276 }
277
278 #[test]
279 fn test_event_history_records_all_transitions() {
280 let mut mgr = LifecycleManager::new("agent-1");
281 mgr.activate("boot").unwrap();
282 mgr.suspend("pause").unwrap();
283 mgr.activate("resume").unwrap();
284
285 let events = mgr.events();
286 assert_eq!(events.len(), 3);
287 assert_eq!(events[0].to, LifecycleState::Active);
288 assert_eq!(events[1].to, LifecycleState::Suspended);
289 assert_eq!(events[2].to, LifecycleState::Active);
290 }
291
292 #[test]
293 fn test_event_timestamps_are_monotonic() {
294 let mut mgr = LifecycleManager::new("agent-1");
295 mgr.activate("boot").unwrap();
296 mgr.suspend("pause").unwrap();
297 mgr.activate("resume").unwrap();
298
299 let events = mgr.events();
300 for window in events.windows(2) {
301 assert!(window[1].timestamp >= window[0].timestamp);
302 }
303 }
304
305 #[test]
306 fn test_lifecycle_state_serde_roundtrip() {
307 let state = LifecycleState::Quarantined;
308 let json = serde_json::to_string(&state).unwrap();
309 let deserialized: LifecycleState = serde_json::from_str(&json).unwrap();
310 assert_eq!(state, deserialized);
311 }
312
313 #[test]
314 fn test_lifecycle_event_serde_roundtrip() {
315 let event = LifecycleEvent {
316 from: LifecycleState::Active,
317 to: LifecycleState::Suspended,
318 reason: "maintenance".to_string(),
319 initiated_by: "admin".to_string(),
320 timestamp: 1700000000,
321 };
322 let json = serde_json::to_string(&event).unwrap();
323 let deserialized: LifecycleEvent = serde_json::from_str(&json).unwrap();
324 assert_eq!(deserialized.from, event.from);
325 assert_eq!(deserialized.to, event.to);
326 assert_eq!(deserialized.reason, event.reason);
327 }
328
329 #[test]
330 fn test_quarantined_can_reactivate() {
331 let mut mgr = LifecycleManager::new("agent-1");
332 mgr.activate("boot").unwrap();
333 mgr.transition(LifecycleState::Degraded, "issues", "monitor")
334 .unwrap();
335 mgr.quarantine("violation").unwrap();
336 mgr.activate("cleared").unwrap();
337 assert_eq!(mgr.state(), LifecycleState::Active);
338 }
339
340 #[test]
341 fn test_quarantined_can_decommission() {
342 let mut mgr = LifecycleManager::new("agent-1");
343 mgr.activate("boot").unwrap();
344 mgr.transition(LifecycleState::Degraded, "issues", "monitor")
345 .unwrap();
346 mgr.quarantine("violation").unwrap();
347 mgr.decommission("permanent removal").unwrap();
348 assert_eq!(mgr.state(), LifecycleState::Decommissioning);
349 }
350}