Skip to main content

meerkat_runtime/
interrupt_public_result.rs

1use crate::meerkat_machine::dsl;
2use crate::runtime_state::RuntimeState;
3use crate::traits::RuntimeDriverError;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum UserInterruptObservation {
7    Accepted,
8    StagedNoop,
9    NotReady(RuntimeState),
10    Destroyed,
11    NotInterruptible,
12}
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum UserInterruptPublicResult {
16    Interrupted,
17    /// #348: a staged (not-yet-promoted) session interrupt is a typed no-op
18    /// terminal, distinct from a real `Interrupted` cancellation.
19    StagedNoop,
20    NotFound,
21    SessionBusy,
22    Conflict,
23}
24
25impl UserInterruptObservation {
26    fn into_dsl(self) -> dsl::UserInterruptObservationKind {
27        match self {
28            Self::Accepted => dsl::UserInterruptObservationKind::Accepted,
29            Self::StagedNoop => dsl::UserInterruptObservationKind::StagedNoop,
30            Self::NotReady(RuntimeState::Idle) => dsl::UserInterruptObservationKind::IdleNoop,
31            Self::NotReady(RuntimeState::Attached) => {
32                dsl::UserInterruptObservationKind::AttachedNoop
33            }
34            Self::NotReady(RuntimeState::Destroyed) | Self::Destroyed => {
35                dsl::UserInterruptObservationKind::Destroyed
36            }
37            Self::NotReady(_) | Self::NotInterruptible => {
38                dsl::UserInterruptObservationKind::NotInterruptible
39            }
40        }
41    }
42}
43
44pub fn resolve_user_interrupt_public_result(
45    observation: UserInterruptObservation,
46    target_present: bool,
47    staged_promotion_busy: bool,
48) -> Result<UserInterruptPublicResult, RuntimeDriverError> {
49    let mut authority = dsl::MeerkatMachineAuthority::new();
50    let transition = dsl::MeerkatMachineMutator::apply(
51        &mut authority,
52        dsl::MeerkatMachineInput::ResolveUserInterruptPublicResult {
53            observation: observation.into_dsl(),
54            target_present,
55            staged_promotion_busy,
56        },
57    )
58    .map_err(|err| {
59        RuntimeDriverError::Internal(crate::meerkat_machine::dsl_authority::map_error(
60            err,
61            "ResolveUserInterruptPublicResult",
62        ))
63    })?;
64
65    let mut resolved = None;
66    for effect in transition.into_effects() {
67        let dsl::MeerkatMachineEffect::UserInterruptPublicResultResolved { result } = effect else {
68            return Err(RuntimeDriverError::Internal(format!(
69                "unexpected user interrupt public-result effect: {effect:?}"
70            )));
71        };
72        if resolved.replace(result).is_some() {
73            return Err(RuntimeDriverError::Internal(
74                "generated user interrupt authority emitted multiple public results".to_string(),
75            ));
76        }
77    }
78
79    match resolved.ok_or_else(|| {
80        RuntimeDriverError::Internal(
81            "generated user interrupt authority emitted no public result".to_string(),
82        )
83    })? {
84        dsl::UserInterruptPublicResultKind::Interrupted => {
85            Ok(UserInterruptPublicResult::Interrupted)
86        }
87        dsl::UserInterruptPublicResultKind::StagedNoop => Ok(UserInterruptPublicResult::StagedNoop),
88        dsl::UserInterruptPublicResultKind::NotFound => Ok(UserInterruptPublicResult::NotFound),
89        dsl::UserInterruptPublicResultKind::SessionBusy => {
90            Ok(UserInterruptPublicResult::SessionBusy)
91        }
92        dsl::UserInterruptPublicResultKind::Conflict => Ok(UserInterruptPublicResult::Conflict),
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    #[test]
101    fn generated_interrupt_result_classifies_noop_success() {
102        let result = resolve_user_interrupt_public_result(
103            UserInterruptObservation::NotReady(RuntimeState::Idle),
104            true,
105            false,
106        )
107        .expect("idle interrupt result should classify");
108        assert_eq!(result, UserInterruptPublicResult::Interrupted);
109    }
110
111    #[test]
112    fn generated_interrupt_result_classifies_staged_noop_success() {
113        // #348: a staged-session interrupt now resolves to the typed
114        // `StagedNoop` terminal, not `Interrupted` (no live run was cancelled).
115        let result =
116            resolve_user_interrupt_public_result(UserInterruptObservation::StagedNoop, true, false)
117                .expect("staged noop interrupt result should classify");
118        assert_eq!(result, UserInterruptPublicResult::StagedNoop);
119    }
120
121    #[test]
122    fn generated_interrupt_result_classifies_destroyed_missing_as_not_found() {
123        let result =
124            resolve_user_interrupt_public_result(UserInterruptObservation::Destroyed, false, false)
125                .expect("destroyed missing interrupt result should classify");
126        assert_eq!(result, UserInterruptPublicResult::NotFound);
127    }
128
129    #[test]
130    fn generated_interrupt_result_classifies_promoting_rejection_as_session_busy() {
131        let result = resolve_user_interrupt_public_result(
132            UserInterruptObservation::NotInterruptible,
133            true,
134            true,
135        )
136        .expect("promoting interrupt result should classify");
137        assert_eq!(result, UserInterruptPublicResult::SessionBusy);
138    }
139}