use kaptan_safety::*; use kaptan_time::Timestamp; fn time(value: u64) -> Timestamp { Timestamp::from_microseconds(value) } fn healthy() -> SafetyInputs { SafetyInputs { estimator_healthy: true, imu_fresh: true, phase_authorized: true, output_healthy: true, } } fn supervisor() -> SafetySupervisor { SafetySupervisor::new(SafetyConfig::new(100).unwrap()) } fn evaluate(s: &mut SafetySupervisor, now: u64, inputs: SafetyInputs) -> CommitOutcome<(), ()> { s.evaluate_and_commit(time(now), inputs, || Ok(())) } #[test] fn dwell_and_owner_local_commit() { assert_eq!( SafetyConfig::new(0), Err(SafetyConfigError::ZeroPrearmDwell) ); let mut a = supervisor(); let mut b = supervisor(); assert_eq!( a.request_arm(time(10), healthy()).state(), SafetyState::PreArm ); for now in [10, 109] { assert!(matches!( a.evaluate_and_commit(time(now), healthy(), || -> Result<(), ()> { panic!("prearm cannot commit") }), CommitOutcome::Inhibited(_) )); } let mut calls = 0; assert_eq!( a.evaluate_and_commit(time(110), healthy(), || { calls += 1; Ok::<_, ()>(42) }), CommitOutcome::Committed(42) ); assert_eq!(calls, 1); // Healthy armed evaluations keep committing without restarting the dwell. assert_eq!( evaluate(&mut a, 111, healthy()), CommitOutcome::Committed(()) ); assert!(matches!( b.evaluate_and_commit(time(110), healthy(), || -> Result<(), ()> { panic!("another owner's armed state cannot commit here") }), CommitOutcome::Inhibited(_) )); } #[test] fn failed_arm_and_prearm_are_consumed() { let mut s = supervisor(); let mut stale = healthy(); stale.imu_fresh = false; assert_eq!( s.request_arm(time(1), stale).failure(), Some(SafetyFailure::ImuStale) ); evaluate(&mut s, 200, healthy()); assert_eq!(s.state(), SafetyState::Disarmed); s.request_arm(time(201), healthy()); evaluate(&mut s, 202, stale); evaluate(&mut s, 400, healthy()); assert_eq!(s.state(), SafetyState::Disarmed); } #[test] fn failed_arm_health_priority_and_recovery_require_a_new_transaction() { for (inputs, expected) in [ ( SafetyInputs { estimator_healthy: false, imu_fresh: false, phase_authorized: false, output_healthy: false, }, SafetyFailure::ActuatorFault, ), ( SafetyInputs { estimator_healthy: false, imu_fresh: false, phase_authorized: false, ..healthy() }, SafetyFailure::EstimatorUnhealthy, ), ( SafetyInputs { imu_fresh: false, phase_authorized: false, ..healthy() }, SafetyFailure::ImuStale, ), ( SafetyInputs { phase_authorized: false, ..healthy() }, SafetyFailure::PhaseUnauthorized, ), ] { let mut s = supervisor(); let expected_state = if expected == SafetyFailure::ActuatorFault { SafetyState::Failsafe } else { SafetyState::Disarmed }; let decision = s.request_arm(time(0), inputs); assert_eq!(decision.failure(), Some(expected)); assert_eq!(decision.state(), expected_state); assert!(matches!( evaluate(&mut s, 100, healthy()), CommitOutcome::Inhibited(_) )); assert_eq!(s.state(), expected_state); if expected == SafetyFailure::ActuatorFault { assert_eq!( s.request_arm(time(101), healthy()).state(), SafetyState::Failsafe ); s.request_disarm(); } assert_eq!( s.request_arm(time(102), healthy()).state(), SafetyState::PreArm ); assert_eq!( evaluate(&mut s, 202, healthy()), CommitOutcome::Committed(()) ); } } #[test] fn regressing_arm_request_consumes_prearm_and_requires_explicit_reset() { let mut s = supervisor(); assert_eq!( s.request_arm(time(100), healthy()).state(), SafetyState::PreArm ); let decision = s.request_arm(time(99), healthy()); assert_eq!(decision.state(), SafetyState::Failsafe); assert_eq!(decision.failure(), Some(SafetyFailure::ClockRegression)); assert!(matches!( evaluate(&mut s, 200, healthy()), CommitOutcome::Inhibited(_) )); s.request_disarm(); s.reset_time_epoch(|| Ok::<_, ()>(())).unwrap(); assert_eq!( s.request_arm(time(0), healthy()).state(), SafetyState::PreArm ); } #[test] fn current_health_revokes_before_callback_and_recovery_cannot_rearm() { let mut s = supervisor(); s.request_arm(time(0), healthy()); assert_eq!( evaluate(&mut s, 100, healthy()), CommitOutcome::Committed(()) ); let diagnostic = s.decision(); let mut stale = healthy(); stale.phase_authorized = false; assert!(matches!( s.evaluate_and_commit(time(101), stale, || -> Result<(), ()> { panic!("stale evidence must inhibit before callback") }), CommitOutcome::Inhibited(_) )); assert_eq!(diagnostic.state(), SafetyState::Armed); // retained diagnostic only assert_eq!( s.decision().failure(), Some(SafetyFailure::PhaseUnauthorized) ); assert!(matches!( evaluate(&mut s, 102, healthy()), CommitOutcome::Inhibited(_) )); assert_eq!( s.request_arm(time(103), healthy()).state(), SafetyState::Failsafe ); s.request_disarm(); assert_eq!( s.request_arm(time(104), healthy()).state(), SafetyState::PreArm ); assert_eq!( evaluate(&mut s, 204, healthy()), CommitOutcome::Committed(()) ); } #[test] fn callback_error_and_actuator_health_require_disarm() { let mut s = supervisor(); s.request_arm(time(0), healthy()); assert_eq!( s.evaluate_and_commit(time(100), healthy(), || Err::<(), _>("commit")), CommitOutcome::Failed("commit") ); assert_eq!(s.state(), SafetyState::Failsafe); evaluate(&mut s, 101, healthy()); assert_eq!( s.request_arm(time(102), healthy()).state(), SafetyState::Failsafe ); s.request_disarm(); s.request_arm(time(103), healthy()); let mut broken = healthy(); broken.output_healthy = false; evaluate(&mut s, 104, broken); assert_eq!(s.decision().failure(), Some(SafetyFailure::ActuatorFault)); evaluate(&mut s, 300, healthy()); assert_eq!(s.state(), SafetyState::Failsafe); } #[test] fn clock_fault_survives_disarm_and_failed_reset() { let mut s = supervisor(); evaluate(&mut s, 100, healthy()); evaluate(&mut s, 99, healthy()); assert_eq!(s.decision().failure(), Some(SafetyFailure::ClockRegression)); assert_eq!( s.reset_time_epoch(|| Ok::<_, ()>(())), Err(ResetError::NotDisarmed) ); s.request_disarm(); assert_eq!( s.request_arm(time(101), healthy()).state(), SafetyState::Failsafe ); s.request_disarm(); assert_eq!( s.reset_time_epoch(|| Err("inhibit")), Err(ResetError::Inhibit("inhibit")) ); s.request_disarm(); assert_eq!( s.request_arm(time(102), healthy()).state(), SafetyState::Failsafe ); s.request_disarm(); s.reset_time_epoch(|| Ok::<_, ()>(())).unwrap(); assert_eq!( s.request_arm(time(0), healthy()).state(), SafetyState::PreArm ); }