nixfleet_state_machine/rollout/transitions/
superseded.rs1use chrono::{DateTime, Utc};
5
6use super::{illegal, transition_effect};
7use crate::rollout::effect::RolloutEffect;
8use crate::rollout::error::RolloutTransitionError;
9use crate::rollout::event::RolloutEvent;
10use crate::rollout::state::{RolloutRecord, RolloutState};
11
12pub(super) fn step(
13 mut record: RolloutRecord,
14 event: RolloutEvent,
15 _now: DateTime<Utc>,
16) -> Result<(RolloutRecord, Vec<RolloutEffect>), RolloutTransitionError> {
17 match event {
18 RolloutEvent::RetentionExpired { at, .. } => {
19 record.state = RolloutState::Pruned;
20 let effects = vec![transition_effect(
21 &record,
22 RolloutState::Superseded,
23 RolloutState::Pruned,
24 at,
25 )];
26 Ok((record, effects))
27 }
28 RolloutEvent::HostStateChanged { .. }
31 | RolloutEvent::HostJoined { .. }
32 | RolloutEvent::WaveAdvanced { .. }
33 | RolloutEvent::RolloutTerminal { .. }
34 | RolloutEvent::SuccessorOpened { .. } => Ok((record, Vec::new())),
37 RolloutEvent::RolloutOpened { .. } | RolloutEvent::OperatorClearance { .. } => Err(
38 illegal(RolloutState::Superseded, &event, record.rollout_id.clone()),
39 ),
40 }
41}
42
43#[cfg(test)]
44mod tests {
45 use super::*;
46 use chrono::TimeZone;
47
48 fn t0() -> DateTime<Utc> {
49 Utc.with_ymd_and_hms(2026, 5, 16, 1, 0, 0).unwrap()
50 }
51
52 fn superseded_record() -> RolloutRecord {
53 RolloutRecord {
54 rollout_id: "r1".into(),
55 channel: "stable".into(),
56 target_ref: "ref-1".into(),
57 state: RolloutState::Superseded,
58 current_wave: 0,
59 opened_event_log_seq: None,
60 last_transition_event_log_seq: None,
61 opened_at: t0(),
62 terminal_at: None,
63 superseded_at: Some(t0()),
64 }
65 }
66
67 #[test]
68 fn retention_expired_transitions_to_pruned() {
69 let event = RolloutEvent::RetentionExpired {
70 rollout_id: "r1".into(),
71 at: t0(),
72 };
73 let (record, _) = step(superseded_record(), event, t0()).unwrap();
74 assert_eq!(record.state, RolloutState::Pruned);
75 }
76
77 #[test]
78 fn re_successor_opened_is_idempotent() {
79 let event = RolloutEvent::SuccessorOpened {
80 superseded_rollout_id: "r1".into(),
81 successor_rollout_id: "r3".into(),
82 at: t0(),
83 };
84 let (record, effects) = step(superseded_record(), event, t0()).unwrap();
85 assert_eq!(record.state, RolloutState::Superseded);
86 assert!(effects.is_empty());
87 }
88}