#![allow(clippy::arithmetic_side_effects)]
use crate::control_types::DesignAssuranceLevel;
use crate::llmosafe_kernel::{CognitiveStability, KernelError};
#[cfg(feature = "std")]
use crate::llmosafe_detection::DetectionResult;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum SafetyDecision {
Proceed,
Warn(&'static str),
Escalate {
entropy: u16,
reason: EscalationReason,
cooldown_ms: u32,
},
Halt(KernelError, u32),
Exit(KernelError),
}
impl SafetyDecision {
pub fn can_proceed(&self) -> bool {
matches!(self, Self::Proceed | Self::Warn(_))
}
pub fn must_halt(&self) -> bool {
matches!(self, Self::Halt(..))
}
pub fn severity(&self) -> u8 {
match self {
Self::Proceed => 0,
Self::Warn(_) => 1,
Self::Escalate { .. } => 2,
Self::Halt(..) => 3,
Self::Exit(_) => 4,
}
}
pub fn is_blocking(&self) -> bool {
matches!(self, Self::Escalate { .. } | Self::Halt(..) | Self::Exit(_))
}
pub fn should_exit(&self) -> bool {
matches!(self, Self::Exit(_))
}
pub fn recommended_cooldown_ms(&self) -> u32 {
match self {
Self::Proceed | Self::Warn(_) | Self::Exit(_) => 0,
Self::Escalate { cooldown_ms, .. } => *cooldown_ms,
Self::Halt(_, cooldown_ms) => *cooldown_ms,
}
}
pub fn status_label(&self) -> &'static str {
match self {
Self::Proceed => "safe",
Self::Warn(_) => "warning",
Self::Escalate { .. } => "escalate",
Self::Halt(..) => "halt",
Self::Exit(_) => "exit",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum EscalationReason {
EntropyApproachingLimit,
SurpriseElevated,
BiasDetected,
ResourcePressure,
AnomalyDetected,
Custom(&'static str),
StuckAgent,
GoalDriftDetected,
ConfidenceDecaying,
AdversarialDetected,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum PressureLevel {
Nominal,
Elevated,
Critical,
Emergency,
}
impl PressureLevel {
pub fn from_percentage(pct: u8) -> Self {
match pct {
0..=25 => Self::Nominal,
26..=50 => Self::Elevated,
51..=75 => Self::Critical,
76..=100 => Self::Emergency,
_ => Self::Emergency, }
}
pub fn requires_action(&self) -> bool {
matches!(self, Self::Critical | Self::Emergency)
}
}
impl From<u8> for PressureLevel {
fn from(pct: u8) -> Self {
Self::from_percentage(pct)
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct EscalationPolicy {
pub warn_entropy: u16,
pub escalate_entropy: u16,
pub halt_entropy: u16,
pub warn_surprise: u16,
pub escalate_surprise: u16,
pub bias_escalates: bool,
pub escalate_pressure: PressureLevel,
pub dal: DesignAssuranceLevel,
}
impl Default for EscalationPolicy {
fn default() -> Self {
Self {
warn_entropy: 30000,
escalate_entropy: 40000,
halt_entropy: 50000,
warn_surprise: 42600,
escalate_surprise: 55700,
bias_escalates: true,
escalate_pressure: PressureLevel::Critical,
dal: DesignAssuranceLevel::A,
}
}
}
impl EscalationPolicy {
pub fn new() -> Self {
Self::default()
}
pub const fn with_warn_entropy(mut self, threshold: u16) -> Self {
self.warn_entropy = threshold;
self
}
pub const fn with_escalate_entropy(mut self, threshold: u16) -> Self {
self.escalate_entropy = threshold;
self
}
pub const fn with_halt_entropy(mut self, threshold: u16) -> Self {
self.halt_entropy = threshold;
self
}
pub const fn with_bias_escalates(mut self, value: bool) -> Self {
self.bias_escalates = value;
self
}
pub const fn with_dal(mut self, dal: DesignAssuranceLevel) -> Self {
self.dal = dal;
self
}
pub fn decide(&self, entropy: u16, surprise: u16, has_bias: bool) -> SafetyDecision {
self.canonical_decision(entropy, surprise, has_bias)
}
pub fn decide_with_pressure(
&self,
entropy: u16,
surprise: u16,
has_bias: bool,
pressure: PressureLevel,
) -> SafetyDecision {
if entropy >= self.halt_entropy {
return self.apply_dal_to_decision(SafetyDecision::Halt(
KernelError::CognitiveInstability,
30000,
));
}
if pressure >= self.escalate_pressure {
return self.apply_dal_to_decision(SafetyDecision::Escalate {
entropy,
reason: EscalationReason::ResourcePressure,
cooldown_ms: 5000,
});
}
self.canonical_decision(entropy, surprise, has_bias)
}
pub fn decide_from_stability(&self, stability: CognitiveStability) -> SafetyDecision {
match stability {
CognitiveStability::Stable => SafetyDecision::Proceed,
CognitiveStability::Pressure => SafetyDecision::Warn("cognitive pressure detected"),
CognitiveStability::Unstable => {
SafetyDecision::Halt(KernelError::CognitiveInstability, 30000)
}
}
}
fn canonical_decision(&self, entropy: u16, surprise: u16, has_bias: bool) -> SafetyDecision {
if entropy >= self.halt_entropy {
return self.apply_dal_to_decision(SafetyDecision::Halt(
KernelError::CognitiveInstability,
30000,
));
}
if has_bias && self.bias_escalates {
return self.apply_dal_to_decision(SafetyDecision::Escalate {
entropy,
reason: EscalationReason::BiasDetected,
cooldown_ms: 5000,
});
}
if entropy >= self.escalate_entropy {
return self.apply_dal_to_decision(SafetyDecision::Escalate {
entropy,
reason: EscalationReason::EntropyApproachingLimit,
cooldown_ms: 5000,
});
}
if surprise >= self.escalate_surprise {
return self.apply_dal_to_decision(SafetyDecision::Escalate {
entropy,
reason: EscalationReason::SurpriseElevated,
cooldown_ms: 5000,
});
}
if entropy >= self.warn_entropy {
return self.apply_dal_to_decision(SafetyDecision::Warn("entropy elevated"));
}
if surprise >= self.warn_surprise {
return self.apply_dal_to_decision(SafetyDecision::Warn("surprise elevated"));
}
self.apply_dal_to_decision(SafetyDecision::Proceed)
}
fn apply_dal_to_decision(&self, decision: SafetyDecision) -> SafetyDecision {
match self.dal {
DesignAssuranceLevel::A => decision,
DesignAssuranceLevel::B => match decision {
SafetyDecision::Halt(_, cooldown_ms) => SafetyDecision::Escalate {
entropy: 0,
reason: EscalationReason::Custom("DAL B: Halt downgraded"),
cooldown_ms,
},
SafetyDecision::Proceed
| SafetyDecision::Warn(_)
| SafetyDecision::Escalate { .. }
| SafetyDecision::Exit(_) => decision,
},
DesignAssuranceLevel::C => match decision {
SafetyDecision::Halt(..) | SafetyDecision::Escalate { .. } => {
SafetyDecision::Warn("DAL C: Escalation downgraded")
}
SafetyDecision::Proceed | SafetyDecision::Warn(_) | SafetyDecision::Exit(_) => {
decision
}
},
DesignAssuranceLevel::D => match decision {
SafetyDecision::Proceed | SafetyDecision::Warn(_) => decision,
SafetyDecision::Escalate { .. }
| SafetyDecision::Halt(..)
| SafetyDecision::Exit(_) => SafetyDecision::Warn("DAL D: Capped at Warn"),
},
DesignAssuranceLevel::E => SafetyDecision::Proceed,
}
}
#[cfg(feature = "std")]
pub fn decide_from_detection(
&self,
detection: &DetectionResult,
entropy: u16,
surprise: u16,
) -> SafetyDecision {
if !detection.adversarial_patterns.is_empty() {
return self
.apply_dal_to_decision(SafetyDecision::Halt(KernelError::BiasHaloDetected, 30000));
}
if detection.risk_score > 0.85 {
return self.apply_dal_to_decision(SafetyDecision::Halt(
KernelError::CognitiveInstability,
30000,
));
}
if detection.is_stuck {
return self.apply_dal_to_decision(SafetyDecision::Escalate {
entropy,
reason: EscalationReason::StuckAgent,
cooldown_ms: 5000,
});
}
if detection.is_drifting {
return self.apply_dal_to_decision(SafetyDecision::Escalate {
entropy,
reason: EscalationReason::GoalDriftDetected,
cooldown_ms: 5000,
});
}
if detection.is_decaying {
return self.apply_dal_to_decision(SafetyDecision::Warn("Confidence decay detected"));
}
if detection.is_low_confidence {
return self.apply_dal_to_decision(SafetyDecision::Warn("Low model confidence"));
}
self.canonical_decision(entropy, surprise, false)
}
}
#[cfg(feature = "std")]
pub struct SafetyContext {
max_entropy: u16,
max_surprise: u16,
any_bias: bool,
decision_count: usize,
policy: EscalationPolicy,
}
#[cfg(feature = "std")]
impl SafetyContext {
pub fn new(policy: EscalationPolicy) -> Self {
Self {
max_entropy: 0,
max_surprise: 0,
any_bias: false,
decision_count: 0,
policy,
}
}
pub fn default_context() -> Self {
Self::new(EscalationPolicy::default())
}
pub fn observe(&mut self, entropy: u16, surprise: u16, has_bias: bool) {
self.max_entropy = self.max_entropy.max(entropy);
self.max_surprise = self.max_surprise.max(surprise);
self.any_bias = self.any_bias || has_bias;
self.decision_count += 1;
}
pub fn finalize(&self) -> SafetyDecision {
self.policy
.decide(self.max_entropy, self.max_surprise, self.any_bias)
}
pub fn reset(&mut self) {
self.max_entropy = 0;
self.max_surprise = 0;
self.any_bias = false;
self.decision_count = 0;
}
pub fn observation_count(&self) -> usize {
self.decision_count
}
}
#[cfg(all(test, feature = "std"))]
mod tests {
use super::*;
#[test]
fn test_safety_decision_severity() {
assert_eq!(SafetyDecision::Proceed.severity(), 0);
assert_eq!(SafetyDecision::Warn("test").severity(), 1);
assert_eq!(
SafetyDecision::Escalate {
entropy: 500,
reason: EscalationReason::BiasDetected,
cooldown_ms: 5000,
}
.severity(),
2
);
assert_eq!(
SafetyDecision::Halt(KernelError::CognitiveInstability, 30000).severity(),
3
);
assert_eq!(
SafetyDecision::Exit(KernelError::CognitiveInstability).severity(),
4
);
}
#[test]
fn test_pressure_level_from_percentage() {
assert_eq!(PressureLevel::from_percentage(10), PressureLevel::Nominal);
assert_eq!(PressureLevel::from_percentage(30), PressureLevel::Elevated);
assert_eq!(PressureLevel::from_percentage(60), PressureLevel::Critical);
assert_eq!(PressureLevel::from_percentage(90), PressureLevel::Emergency);
assert_eq!(
PressureLevel::from_percentage(255),
PressureLevel::Emergency
);
}
#[test]
fn test_escalation_policy_default_decide() {
let policy = EscalationPolicy::default();
let decision = policy.decide(400, 100, false);
assert!(matches!(decision, SafetyDecision::Proceed));
let decision = policy.decide(31000, 100, false);
assert!(matches!(decision, SafetyDecision::Warn(_)));
let decision = policy.decide(41000, 100, false);
assert!(matches!(decision, SafetyDecision::Escalate { .. }));
let decision = policy.decide(51000, 100, false);
assert!(matches!(decision, SafetyDecision::Halt(..)));
let decision = policy.decide(400, 100, true);
assert!(matches!(decision, SafetyDecision::Escalate { .. }));
}
#[test]
fn test_escalation_policy_builder() {
let policy = EscalationPolicy::new()
.with_warn_entropy(500)
.with_escalate_entropy(700)
.with_halt_entropy(900)
.with_bias_escalates(false);
let decision = policy.decide(400, 100, true);
assert!(matches!(decision, SafetyDecision::Proceed));
let decision = policy.decide(550, 100, false);
assert!(matches!(decision, SafetyDecision::Warn(_)));
}
#[test]
fn test_safety_context_accumulation() {
let mut ctx = SafetyContext::default_context();
ctx.observe(300, 100, false);
ctx.observe(500, 200, false);
ctx.observe(400, 250, false);
assert_eq!(ctx.observation_count(), 3);
let decision = ctx.finalize();
assert!(matches!(decision, SafetyDecision::Proceed));
}
#[test]
fn test_safety_context_with_bias() {
let mut ctx = SafetyContext::default_context();
ctx.observe(300, 100, false);
ctx.observe(400, 100, true); ctx.observe(350, 100, false);
let decision = ctx.finalize();
assert!(matches!(decision, SafetyDecision::Escalate { .. }));
}
#[test]
fn test_pressure_level_ordering() {
assert!(PressureLevel::Nominal < PressureLevel::Elevated);
assert!(PressureLevel::Elevated < PressureLevel::Critical);
assert!(PressureLevel::Critical < PressureLevel::Emergency);
}
#[test]
fn test_decide_from_stability() {
let policy = EscalationPolicy::default();
let decision = policy.decide_from_stability(CognitiveStability::Stable);
assert!(matches!(decision, SafetyDecision::Proceed));
let decision = policy.decide_from_stability(CognitiveStability::Pressure);
assert!(matches!(decision, SafetyDecision::Warn(_)));
let decision = policy.decide_from_stability(CognitiveStability::Unstable);
assert!(matches!(decision, SafetyDecision::Halt(..)));
}
#[test]
fn test_safety_context_reset() {
let mut ctx = SafetyContext::default_context();
ctx.observe(800, 500, true);
assert_eq!(ctx.observation_count(), 1);
ctx.reset();
assert_eq!(ctx.observation_count(), 0);
assert_eq!(ctx.max_entropy, 0);
assert!(!ctx.any_bias);
}
#[test]
fn test_escalation_policy_with_pressure() {
let policy = EscalationPolicy::default().with_dal(DesignAssuranceLevel::A);
let decision = policy.decide_with_pressure(400, 100, false, PressureLevel::Nominal);
assert!(matches!(decision, SafetyDecision::Proceed));
let decision = policy.decide_with_pressure(400, 100, false, PressureLevel::Critical);
assert!(matches!(decision, SafetyDecision::Escalate { .. }));
}
#[test]
fn test_escalation_policy_cooldown_values() {
let policy = EscalationPolicy::default();
let decision = policy.decide(41000, 100, false);
assert!(matches!(
decision,
SafetyDecision::Escalate {
cooldown_ms: 5000,
..
}
));
let decision = policy.decide(400, 100, true);
assert!(matches!(
decision,
SafetyDecision::Escalate {
cooldown_ms: 5000,
..
}
));
let decision = policy.decide(400, 55800, false);
assert!(matches!(
decision,
SafetyDecision::Escalate {
cooldown_ms: 5000,
..
}
));
let decision = policy.decide(51000, 100, false);
assert!(matches!(decision, SafetyDecision::Halt(_, 30000)));
}
#[test]
fn test_escalation_policy_cooldown_non_zero() {
let policy = EscalationPolicy::default();
if let SafetyDecision::Escalate { cooldown_ms, .. } = policy.decide(41000, 100, false) {
assert_ne!(cooldown_ms, 0, "Escalate cooldown should be non-zero");
assert_eq!(cooldown_ms, 5000, "Escalate cooldown should be 5000ms");
} else {
panic!("Expected Escalate decision");
}
if let SafetyDecision::Halt(_, cooldown_ms) = policy.decide(51000, 100, false) {
assert_ne!(cooldown_ms, 0, "Halt cooldown should be non-zero");
assert_eq!(cooldown_ms, 30000, "Halt cooldown should be 30000ms");
} else {
panic!("Expected Halt decision");
}
}
#[test]
fn test_safety_decision_should_exit() {
assert!(!SafetyDecision::Proceed.should_exit());
assert!(!SafetyDecision::Warn("test").should_exit());
assert!(!SafetyDecision::Escalate {
entropy: 500,
reason: EscalationReason::BiasDetected,
cooldown_ms: 5000,
}
.should_exit());
assert!(!SafetyDecision::Halt(KernelError::CognitiveInstability, 30000).should_exit());
assert!(SafetyDecision::Exit(KernelError::ResourceExhaustion).should_exit());
}
#[test]
fn test_pressure_level_requires_action() {
assert!(!PressureLevel::Nominal.requires_action());
assert!(!PressureLevel::Elevated.requires_action());
assert!(PressureLevel::Critical.requires_action());
assert!(PressureLevel::Emergency.requires_action());
}
#[test]
fn test_must_halt_behavior() {
assert!(!SafetyDecision::Proceed.must_halt());
assert!(!SafetyDecision::Warn("test").must_halt());
assert!(!SafetyDecision::Escalate {
entropy: 500,
reason: EscalationReason::BiasDetected,
cooldown_ms: 5000,
}
.must_halt());
assert!(SafetyDecision::Halt(KernelError::CognitiveInstability, 30000).must_halt());
assert!(!SafetyDecision::Exit(KernelError::ResourceExhaustion).must_halt());
}
#[test]
fn test_default_context_boundaries() {
let mut ctx = SafetyContext::default_context();
assert_eq!(ctx.observation_count(), 0);
assert_eq!(ctx.max_entropy, 0);
assert_eq!(ctx.max_surprise, 0);
assert!(!ctx.any_bias);
let decision = ctx.finalize();
assert!(matches!(decision, SafetyDecision::Proceed));
ctx.observe(30000, 100, false);
let decision = ctx.finalize();
assert!(matches!(decision, SafetyDecision::Warn(_)));
assert_eq!(ctx.observation_count(), 1);
ctx.observe(30000, 42600, false);
let decision = ctx.finalize();
assert!(matches!(decision, SafetyDecision::Warn(_)));
}
#[test]
fn test_decide_from_stability_bypasses_dal() {
let policy = EscalationPolicy::default().with_dal(DesignAssuranceLevel::E);
let decision = policy.decide_from_stability(CognitiveStability::Unstable);
assert!(
matches!(decision, SafetyDecision::Halt(..)),
"decide_from_stability should intentionally bypass DAL logic"
);
}
#[test]
fn test_apply_dal_to_decision_boundaries() {
let halt_decision = SafetyDecision::Halt(KernelError::CognitiveInstability, 30000);
let escalate_decision = SafetyDecision::Escalate {
entropy: 40000,
reason: EscalationReason::ResourcePressure,
cooldown_ms: 5000,
};
let warn_decision = SafetyDecision::Warn("Test");
let policy_a = EscalationPolicy::default().with_dal(DesignAssuranceLevel::A);
assert!(matches!(
policy_a.apply_dal_to_decision(halt_decision),
SafetyDecision::Halt(..)
));
assert!(matches!(
policy_a.apply_dal_to_decision(escalate_decision),
SafetyDecision::Escalate { .. }
));
let policy_b = EscalationPolicy::default().with_dal(DesignAssuranceLevel::B);
assert!(matches!(
policy_b.apply_dal_to_decision(halt_decision),
SafetyDecision::Escalate { .. }
));
assert!(matches!(
policy_b.apply_dal_to_decision(escalate_decision),
SafetyDecision::Escalate { .. }
));
let policy_c = EscalationPolicy::default().with_dal(DesignAssuranceLevel::C);
assert!(matches!(
policy_c.apply_dal_to_decision(halt_decision),
SafetyDecision::Warn(_)
));
assert!(matches!(
policy_c.apply_dal_to_decision(escalate_decision),
SafetyDecision::Warn(_)
));
assert!(matches!(
policy_c.apply_dal_to_decision(warn_decision),
SafetyDecision::Warn(_)
));
let policy_d = EscalationPolicy::default().with_dal(DesignAssuranceLevel::D);
assert!(matches!(
policy_d.apply_dal_to_decision(halt_decision),
SafetyDecision::Warn(_)
));
assert!(matches!(
policy_d.apply_dal_to_decision(SafetyDecision::Exit(KernelError::ResourceExhaustion)),
SafetyDecision::Warn(_)
));
let policy_e = EscalationPolicy::default().with_dal(DesignAssuranceLevel::E);
assert!(matches!(
policy_e.apply_dal_to_decision(halt_decision),
SafetyDecision::Proceed
));
assert!(matches!(
policy_e.apply_dal_to_decision(escalate_decision),
SafetyDecision::Proceed
));
assert!(matches!(
policy_e.apply_dal_to_decision(warn_decision),
SafetyDecision::Proceed
));
}
#[test]
fn test_decide_boundaries() {
let policy = EscalationPolicy::default();
assert!(matches!(
policy.decide(29999, 100, false),
SafetyDecision::Proceed
));
assert!(matches!(
policy.decide(30000, 100, false),
SafetyDecision::Warn(_)
));
assert!(matches!(
policy.decide(30001, 100, false),
SafetyDecision::Warn(_)
));
assert!(matches!(
policy.decide(39999, 100, false),
SafetyDecision::Warn(_)
));
assert!(matches!(
policy.decide(40000, 100, false),
SafetyDecision::Escalate { .. }
));
assert!(matches!(
policy.decide(40001, 100, false),
SafetyDecision::Escalate { .. }
));
assert!(matches!(
policy.decide(49999, 100, false),
SafetyDecision::Escalate { .. }
));
assert!(matches!(
policy.decide(50000, 100, false),
SafetyDecision::Halt(..)
));
assert!(matches!(
policy.decide(50001, 100, false),
SafetyDecision::Halt(..)
));
assert!(matches!(
policy.decide(100, 42599, false),
SafetyDecision::Proceed
));
assert!(matches!(
policy.decide(100, 42600, false),
SafetyDecision::Warn(_)
));
assert!(matches!(
policy.decide(100, 42601, false),
SafetyDecision::Warn(_)
));
assert!(matches!(
policy.decide(100, 55699, false),
SafetyDecision::Warn(_)
));
assert!(matches!(
policy.decide(100, 55700, false),
SafetyDecision::Escalate { .. }
));
assert!(matches!(
policy.decide(100, 55701, false),
SafetyDecision::Escalate { .. }
));
}
#[test]
fn test_decision_path_isolation() {
let policy = EscalationPolicy::default();
assert!(matches!(
policy.decide(100, 100, true),
SafetyDecision::Escalate {
reason: EscalationReason::BiasDetected,
..
}
));
assert!(matches!(
policy.decide_with_pressure(100, 100, false, PressureLevel::Critical),
SafetyDecision::Escalate {
reason: EscalationReason::ResourcePressure,
..
}
));
assert!(matches!(
policy.decide(41000, 100, false),
SafetyDecision::Escalate {
reason: EscalationReason::EntropyApproachingLimit,
..
}
));
}
#[test]
fn test_decide_from_detection_adversarial_halt() {
let policy = EscalationPolicy::default().with_dal(DesignAssuranceLevel::A);
let detection = DetectionResult {
is_stuck: false,
is_drifting: false,
is_low_confidence: false,
is_decaying: false,
adversarial_patterns: vec!["ignore previous"],
risk_score: 0.1,
};
let decision = policy.decide_from_detection(&detection, 100, 100);
assert!(
matches!(decision, SafetyDecision::Halt(..)),
"adversarial patterns must cause Halt"
);
}
#[test]
fn test_decide_from_detection_high_risk_halt() {
let policy = EscalationPolicy::default().with_dal(DesignAssuranceLevel::A);
let detection = DetectionResult {
is_stuck: false,
is_drifting: false,
is_low_confidence: false,
is_decaying: false,
adversarial_patterns: vec![],
risk_score: 0.9, };
let decision = policy.decide_from_detection(&detection, 100, 100);
assert!(
matches!(decision, SafetyDecision::Halt(..)),
"high risk (>0.85) must cause Halt"
);
}
#[test]
fn test_decide_from_detection_stuck_escalate() {
let policy = EscalationPolicy::default().with_dal(DesignAssuranceLevel::A);
let detection = DetectionResult {
is_stuck: true,
is_drifting: false,
is_low_confidence: false,
is_decaying: false,
adversarial_patterns: vec![],
risk_score: 0.1,
};
let decision = policy.decide_from_detection(&detection, 40000, 100);
assert!(
matches!(
decision,
SafetyDecision::Escalate {
reason: EscalationReason::StuckAgent,
..
}
),
"stuck agent must cause Escalate"
);
}
#[test]
fn test_decide_from_detection_drifting_escalate() {
let policy = EscalationPolicy::default().with_dal(DesignAssuranceLevel::A);
let detection = DetectionResult {
is_stuck: false,
is_drifting: true,
is_low_confidence: false,
is_decaying: false,
adversarial_patterns: vec![],
risk_score: 0.1,
};
let decision = policy.decide_from_detection(&detection, 100, 100);
assert!(
matches!(
decision,
SafetyDecision::Escalate {
reason: EscalationReason::GoalDriftDetected,
..
}
),
"goal drift must cause Escalate"
);
}
#[test]
fn test_decide_from_detection_decaying_warn() {
let policy = EscalationPolicy::default().with_dal(DesignAssuranceLevel::A);
let detection = DetectionResult {
is_stuck: false,
is_drifting: false,
is_low_confidence: false,
is_decaying: true,
adversarial_patterns: vec![],
risk_score: 0.1,
};
let decision = policy.decide_from_detection(&detection, 100, 100);
assert!(
matches!(decision, SafetyDecision::Warn(_)),
"decaying confidence must cause Warn"
);
}
#[test]
fn test_decide_from_detection_low_confidence_warn() {
let policy = EscalationPolicy::default().with_dal(DesignAssuranceLevel::A);
let detection = DetectionResult {
is_stuck: false,
is_drifting: false,
is_low_confidence: true,
is_decaying: false,
adversarial_patterns: vec![],
risk_score: 0.1,
};
let decision = policy.decide_from_detection(&detection, 100, 100);
assert!(
matches!(decision, SafetyDecision::Warn(_)),
"low confidence must cause Warn"
);
}
#[test]
fn test_decide_from_detection_all_false_falls_through() {
let policy = EscalationPolicy::default().with_dal(DesignAssuranceLevel::A);
let detection = DetectionResult {
is_stuck: false,
is_drifting: false,
is_low_confidence: false,
is_decaying: false,
adversarial_patterns: vec![],
risk_score: 0.1,
};
let decision = policy.decide_from_detection(&detection, 100, 100);
assert!(
matches!(decision, SafetyDecision::Proceed),
"all false with low signals must Proceed"
);
let decision = policy.decide_from_detection(&detection, 41000, 100);
assert!(
matches!(decision, SafetyDecision::Escalate { .. }),
"all false with high entropy must Escalate via decide()"
);
}
#[test]
fn test_decide_with_pressure_elevated_no_halt() {
let policy = EscalationPolicy::default().with_dal(DesignAssuranceLevel::A);
let decision = policy.decide_with_pressure(100, 100, false, PressureLevel::Elevated);
assert!(
matches!(decision, SafetyDecision::Proceed),
"Elevated pressure below escalate_pressure must not halt"
);
}
#[test]
fn test_decide_with_pressure_emergency_halt() {
let policy = EscalationPolicy::default().with_dal(DesignAssuranceLevel::A);
let decision = policy.decide_with_pressure(100, 100, false, PressureLevel::Emergency);
assert!(
matches!(decision, SafetyDecision::Escalate { .. }),
"Emergency pressure must escalate (via pressure escalation)"
);
}
#[test]
fn test_decide_with_pressure_custom_escalate_pressure() {
let policy = EscalationPolicy {
escalate_pressure: PressureLevel::Elevated,
..EscalationPolicy::default()
}
.with_dal(DesignAssuranceLevel::A);
let decision = policy.decide_with_pressure(100, 100, false, PressureLevel::Elevated);
assert!(
matches!(decision, SafetyDecision::Escalate { .. }),
"custom escalate_pressure=Elevated must escalate at Elevated pressure"
);
let decision = policy.decide_with_pressure(100, 100, false, PressureLevel::Nominal);
assert!(
matches!(decision, SafetyDecision::Proceed),
"custom escalate_pressure=Elevated must not escalate at Nominal"
);
}
#[test]
fn test_exit_is_blocking() {
let exit = SafetyDecision::Exit(KernelError::ResourceExhaustion);
assert!(exit.is_blocking());
}
#[test]
fn test_exit_status_label() {
let exit = SafetyDecision::Exit(KernelError::ResourceExhaustion);
assert_eq!(exit.status_label(), "exit");
}
#[test]
fn test_exit_recommended_cooldown_ms() {
let exit = SafetyDecision::Exit(KernelError::ResourceExhaustion);
assert_eq!(exit.recommended_cooldown_ms(), 0);
}
}