From 0c0d5292a880b17c07697155a1e1d74197d2f078 Mon Sep 17 00:00:00 2001 From: Felix Martin Date: Mon, 25 May 2026 13:00:34 -0400 Subject: [PATCH] Restrict admin override exits --- src/state_machine.rs | 40 +++++++++++++++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/src/state_machine.rs b/src/state_machine.rs index ba86216..e51b536 100644 --- a/src/state_machine.rs +++ b/src/state_machine.rs @@ -109,12 +109,11 @@ pub fn transition_runtime( (Review, ContinuePlanning) => Ok(Planning), (Review, EndWorkPeriod) => Ok(Locked), (_, EnterAdminOverride) => Ok(AdminOverride), - (AdminOverride, ExitAdminOverride(AdminOverride)) => { - Err(TransitionError::InvalidAdminOverrideExitTarget { - target: AdminOverride, - }) + (AdminOverride, ExitAdminOverride(Locked)) => Ok(Locked), + (AdminOverride, ExitAdminOverride(Planning)) => Ok(Planning), + (AdminOverride, ExitAdminOverride(target)) => { + Err(TransitionError::InvalidAdminOverrideExitTarget { target }) } - (AdminOverride, ExitAdminOverride(next)) => Ok(next), _ => Err(TransitionError::IllegalRuntimeTransition { current, action }), } } @@ -237,6 +236,37 @@ mod tests { ); } + #[test] + fn admin_override_cannot_exit_to_active_or_transition() { + for target in [RuntimeState::Active, RuntimeState::Transition] { + let err = transition_runtime( + RuntimeState::AdminOverride, + RuntimeAction::ExitAdminOverride(target), + ) + .unwrap_err(); + + assert_eq!(err, TransitionError::InvalidAdminOverrideExitTarget { target }); + } + } + + #[test] + fn admin_override_can_exit_to_locked_or_planning() { + assert_eq!( + transition_runtime( + RuntimeState::AdminOverride, + RuntimeAction::ExitAdminOverride(RuntimeState::Locked), + ), + Ok(RuntimeState::Locked) + ); + assert_eq!( + transition_runtime( + RuntimeState::AdminOverride, + RuntimeAction::ExitAdminOverride(RuntimeState::Planning), + ), + Ok(RuntimeState::Planning) + ); + } + #[test] fn commitment_violation_recovery_is_explicit() { assert_eq!(