nixfleet_state_machine/rollout/transitions/
pruned.rs

1//! `Pruned` source state. Absorbing — the in-memory state-machine
2//! instance has been freed; the row persists for audit (RFC-0008 §3 +
3//! d1bc6df1 architect fix-up). Any further event is structurally a
4//! defect.
5
6use chrono::{DateTime, Utc};
7
8use super::illegal;
9use crate::rollout::effect::RolloutEffect;
10use crate::rollout::error::RolloutTransitionError;
11use crate::rollout::event::RolloutEvent;
12use crate::rollout::state::{RolloutRecord, RolloutState};
13
14pub(super) fn step(
15    record: RolloutRecord,
16    event: RolloutEvent,
17    _now: DateTime<Utc>,
18) -> Result<(RolloutRecord, Vec<RolloutEffect>), RolloutTransitionError> {
19    Err(illegal(RolloutState::Pruned, &event, record.rollout_id))
20}
21
22#[cfg(test)]
23mod tests {
24    use super::*;
25    use chrono::TimeZone;
26
27    fn t0() -> DateTime<Utc> {
28        Utc.with_ymd_and_hms(2026, 5, 16, 1, 0, 0).unwrap()
29    }
30
31    fn pruned_record() -> RolloutRecord {
32        RolloutRecord {
33            rollout_id: "r1".into(),
34            channel: "stable".into(),
35            target_ref: "ref-1".into(),
36            state: RolloutState::Pruned,
37            current_wave: 0,
38            opened_event_log_seq: None,
39            last_transition_event_log_seq: None,
40            opened_at: t0(),
41            terminal_at: Some(t0()),
42            superseded_at: None,
43        }
44    }
45
46    #[test]
47    fn every_event_is_illegal_from_pruned() {
48        let event = RolloutEvent::SuccessorOpened {
49            superseded_rollout_id: "r1".into(),
50            successor_rollout_id: "r2".into(),
51            at: t0(),
52        };
53        let err = step(pruned_record(), event, t0()).unwrap_err();
54        assert!(matches!(
55            err,
56            RolloutTransitionError::IllegalForState {
57                from: RolloutState::Pruned,
58                ..
59            }
60        ));
61    }
62}