507 lines
15 KiB
Rust
507 lines
15 KiB
Rust
//! Comprehensive tests for PERMIT/DEFER/DENY decision logic
|
|
//!
|
|
//! Tests cover:
|
|
//! - Three-filter decision pipeline
|
|
//! - Threshold configurations
|
|
//! - Edge cases and boundary conditions
|
|
//! - Security scenarios (policy violations, replay detection)
|
|
|
|
use cognitum_gate_tilezero::decision::{EvidenceDecision, GateDecision, GateThresholds};
|
|
|
|
#[cfg(test)]
|
|
mod gate_decision {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_decision_display() {
|
|
assert_eq!(GateDecision::Permit.to_string(), "permit");
|
|
assert_eq!(GateDecision::Defer.to_string(), "defer");
|
|
assert_eq!(GateDecision::Deny.to_string(), "deny");
|
|
}
|
|
|
|
#[test]
|
|
fn test_decision_equality() {
|
|
assert_eq!(GateDecision::Permit, GateDecision::Permit);
|
|
assert_eq!(GateDecision::Defer, GateDecision::Defer);
|
|
assert_eq!(GateDecision::Deny, GateDecision::Deny);
|
|
|
|
assert_ne!(GateDecision::Permit, GateDecision::Defer);
|
|
assert_ne!(GateDecision::Permit, GateDecision::Deny);
|
|
assert_ne!(GateDecision::Defer, GateDecision::Deny);
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod evidence_decision {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_evidence_values() {
|
|
let accept = EvidenceDecision::Accept;
|
|
let cont = EvidenceDecision::Continue;
|
|
let reject = EvidenceDecision::Reject;
|
|
|
|
assert_eq!(accept, EvidenceDecision::Accept);
|
|
assert_eq!(cont, EvidenceDecision::Continue);
|
|
assert_eq!(reject, EvidenceDecision::Reject);
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod threshold_configuration {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_default_thresholds() {
|
|
let thresholds = GateThresholds::default();
|
|
|
|
assert_eq!(thresholds.tau_deny, 0.01);
|
|
assert_eq!(thresholds.tau_permit, 100.0);
|
|
assert_eq!(thresholds.min_cut, 5.0);
|
|
assert_eq!(thresholds.max_shift, 0.5);
|
|
assert_eq!(thresholds.permit_ttl_ns, 60_000_000_000);
|
|
}
|
|
|
|
#[test]
|
|
fn test_custom_thresholds() {
|
|
let thresholds = GateThresholds {
|
|
tau_deny: 0.05,
|
|
tau_permit: 50.0,
|
|
min_cut: 10.0,
|
|
max_shift: 0.3,
|
|
permit_ttl_ns: 30_000_000_000,
|
|
theta_uncertainty: 15.0,
|
|
theta_confidence: 3.0,
|
|
};
|
|
|
|
assert_eq!(thresholds.tau_deny, 0.05);
|
|
assert_eq!(thresholds.tau_permit, 50.0);
|
|
assert_eq!(thresholds.min_cut, 10.0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_threshold_ordering() {
|
|
let thresholds = GateThresholds::default();
|
|
|
|
// tau_deny < 1 < tau_permit (typical e-process thresholds)
|
|
assert!(thresholds.tau_deny < 1.0);
|
|
assert!(thresholds.tau_permit > 1.0);
|
|
assert!(thresholds.tau_deny < thresholds.tau_permit);
|
|
}
|
|
|
|
#[test]
|
|
fn test_conformal_thresholds() {
|
|
let thresholds = GateThresholds::default();
|
|
|
|
// theta_confidence < theta_uncertainty (smaller set = more confident)
|
|
assert!(thresholds.theta_confidence < thresholds.theta_uncertainty);
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod three_filter_logic {
|
|
use super::*;
|
|
|
|
/// Test the structural filter (min-cut check)
|
|
#[test]
|
|
fn test_structural_filter_deny() {
|
|
// If min-cut is below threshold, should DENY
|
|
let thresholds = GateThresholds::default();
|
|
|
|
// Low min-cut (below threshold of 5.0)
|
|
let min_cut = 3.0;
|
|
let shift_pressure = 0.1; // OK
|
|
let e_aggregate = 150.0; // OK
|
|
|
|
let decision = apply_three_filters(min_cut, shift_pressure, e_aggregate, &thresholds);
|
|
assert_eq!(decision, GateDecision::Deny);
|
|
}
|
|
|
|
/// Test the shift filter (coherence check)
|
|
#[test]
|
|
fn test_shift_filter_defer() {
|
|
let thresholds = GateThresholds::default();
|
|
|
|
// OK min-cut, high shift pressure
|
|
let min_cut = 10.0; // OK
|
|
let shift_pressure = 0.8; // Above threshold of 0.5
|
|
let e_aggregate = 150.0; // OK
|
|
|
|
let decision = apply_three_filters(min_cut, shift_pressure, e_aggregate, &thresholds);
|
|
assert_eq!(decision, GateDecision::Defer);
|
|
}
|
|
|
|
/// Test the evidence filter (e-value check)
|
|
#[test]
|
|
fn test_evidence_filter_deny() {
|
|
let thresholds = GateThresholds::default();
|
|
|
|
// OK min-cut, OK shift, low e-value (evidence against coherence)
|
|
let min_cut = 10.0;
|
|
let shift_pressure = 0.1;
|
|
let e_aggregate = 0.005; // Below tau_deny of 0.01
|
|
|
|
let decision = apply_three_filters(min_cut, shift_pressure, e_aggregate, &thresholds);
|
|
assert_eq!(decision, GateDecision::Deny);
|
|
}
|
|
|
|
#[test]
|
|
fn test_evidence_filter_defer() {
|
|
let thresholds = GateThresholds::default();
|
|
|
|
// OK min-cut, OK shift, moderate e-value (insufficient evidence)
|
|
let min_cut = 10.0;
|
|
let shift_pressure = 0.1;
|
|
let e_aggregate = 50.0; // Between tau_deny (0.01) and tau_permit (100)
|
|
|
|
let decision = apply_three_filters(min_cut, shift_pressure, e_aggregate, &thresholds);
|
|
assert_eq!(decision, GateDecision::Defer);
|
|
}
|
|
|
|
#[test]
|
|
fn test_all_filters_pass_permit() {
|
|
let thresholds = GateThresholds::default();
|
|
|
|
// Everything OK
|
|
let min_cut = 10.0;
|
|
let shift_pressure = 0.1;
|
|
let e_aggregate = 150.0; // Above tau_permit of 100
|
|
|
|
let decision = apply_three_filters(min_cut, shift_pressure, e_aggregate, &thresholds);
|
|
assert_eq!(decision, GateDecision::Permit);
|
|
}
|
|
|
|
// Helper function to simulate the three-filter logic
|
|
fn apply_three_filters(
|
|
min_cut: f64,
|
|
shift_pressure: f64,
|
|
e_aggregate: f64,
|
|
thresholds: &GateThresholds,
|
|
) -> GateDecision {
|
|
// 1. Structural filter
|
|
if min_cut < thresholds.min_cut {
|
|
return GateDecision::Deny;
|
|
}
|
|
|
|
// 2. Shift filter
|
|
if shift_pressure >= thresholds.max_shift {
|
|
return GateDecision::Defer;
|
|
}
|
|
|
|
// 3. Evidence filter
|
|
if e_aggregate < thresholds.tau_deny {
|
|
return GateDecision::Deny;
|
|
}
|
|
if e_aggregate < thresholds.tau_permit {
|
|
return GateDecision::Defer;
|
|
}
|
|
|
|
GateDecision::Permit
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod boundary_conditions {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_min_cut_at_threshold() {
|
|
let thresholds = GateThresholds::default();
|
|
|
|
// Exactly at threshold
|
|
let decision = decide_structural(5.0, &thresholds);
|
|
assert_eq!(decision, GateDecision::Permit); // >= threshold is OK
|
|
}
|
|
|
|
#[test]
|
|
fn test_min_cut_just_below() {
|
|
let thresholds = GateThresholds::default();
|
|
|
|
let decision = decide_structural(4.999, &thresholds);
|
|
assert_eq!(decision, GateDecision::Deny);
|
|
}
|
|
|
|
#[test]
|
|
fn test_e_value_at_deny_threshold() {
|
|
let thresholds = GateThresholds::default();
|
|
|
|
let decision = decide_evidence(0.01, &thresholds);
|
|
assert_eq!(decision, EvidenceDecision::Continue); // Exactly at threshold continues
|
|
}
|
|
|
|
#[test]
|
|
fn test_e_value_at_permit_threshold() {
|
|
let thresholds = GateThresholds::default();
|
|
|
|
let decision = decide_evidence(100.0, &thresholds);
|
|
assert_eq!(decision, EvidenceDecision::Accept);
|
|
}
|
|
|
|
#[test]
|
|
fn test_zero_values() {
|
|
let thresholds = GateThresholds::default();
|
|
|
|
assert_eq!(decide_structural(0.0, &thresholds), GateDecision::Deny);
|
|
assert_eq!(decide_evidence(0.0, &thresholds), EvidenceDecision::Reject);
|
|
}
|
|
|
|
// Helper functions
|
|
fn decide_structural(min_cut: f64, thresholds: &GateThresholds) -> GateDecision {
|
|
if min_cut >= thresholds.min_cut {
|
|
GateDecision::Permit
|
|
} else {
|
|
GateDecision::Deny
|
|
}
|
|
}
|
|
|
|
fn decide_evidence(e_aggregate: f64, thresholds: &GateThresholds) -> EvidenceDecision {
|
|
if e_aggregate < thresholds.tau_deny {
|
|
EvidenceDecision::Reject
|
|
} else if e_aggregate >= thresholds.tau_permit {
|
|
EvidenceDecision::Accept
|
|
} else {
|
|
EvidenceDecision::Continue
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod filter_priority {
|
|
use super::*;
|
|
|
|
/// Structural filter has highest priority (checked first)
|
|
#[test]
|
|
fn test_structural_overrides_evidence() {
|
|
let thresholds = GateThresholds::default();
|
|
|
|
// Low min-cut but high e-value
|
|
let min_cut = 1.0; // Fail structural
|
|
let e_aggregate = 1000.0; // Would pass evidence
|
|
|
|
// Structural failure should result in DENY
|
|
let decision = if min_cut < thresholds.min_cut {
|
|
GateDecision::Deny
|
|
} else if e_aggregate >= thresholds.tau_permit {
|
|
GateDecision::Permit
|
|
} else {
|
|
GateDecision::Defer
|
|
};
|
|
|
|
assert_eq!(decision, GateDecision::Deny);
|
|
}
|
|
|
|
/// Shift filter checked after structural
|
|
#[test]
|
|
fn test_shift_overrides_evidence() {
|
|
let thresholds = GateThresholds::default();
|
|
|
|
// Good min-cut, high shift, high e-value
|
|
let min_cut = 10.0; // Pass structural
|
|
let shift_pressure = 0.9; // Fail shift
|
|
let e_aggregate = 1000.0; // Would pass evidence
|
|
|
|
let decision = if min_cut < thresholds.min_cut {
|
|
GateDecision::Deny
|
|
} else if shift_pressure >= thresholds.max_shift {
|
|
GateDecision::Defer
|
|
} else if e_aggregate >= thresholds.tau_permit {
|
|
GateDecision::Permit
|
|
} else {
|
|
GateDecision::Defer
|
|
};
|
|
|
|
assert_eq!(decision, GateDecision::Defer);
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod ttl_scenarios {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_permit_ttl() {
|
|
let thresholds = GateThresholds::default();
|
|
assert_eq!(thresholds.permit_ttl_ns, 60_000_000_000); // 60 seconds
|
|
}
|
|
|
|
#[test]
|
|
fn test_custom_short_ttl() {
|
|
let thresholds = GateThresholds {
|
|
permit_ttl_ns: 1_000_000_000, // 1 second
|
|
..Default::default()
|
|
};
|
|
|
|
assert_eq!(thresholds.permit_ttl_ns, 1_000_000_000);
|
|
}
|
|
|
|
#[test]
|
|
fn test_custom_long_ttl() {
|
|
let thresholds = GateThresholds {
|
|
permit_ttl_ns: 3600_000_000_000, // 1 hour
|
|
..Default::default()
|
|
};
|
|
|
|
assert_eq!(thresholds.permit_ttl_ns, 3600_000_000_000);
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod extreme_values {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_very_high_e_value() {
|
|
let thresholds = GateThresholds::default();
|
|
|
|
let decision = decide_evidence_full(1e10, &thresholds);
|
|
assert_eq!(decision, EvidenceDecision::Accept);
|
|
}
|
|
|
|
#[test]
|
|
fn test_very_low_e_value() {
|
|
let thresholds = GateThresholds::default();
|
|
|
|
let decision = decide_evidence_full(1e-10, &thresholds);
|
|
assert_eq!(decision, EvidenceDecision::Reject);
|
|
}
|
|
|
|
#[test]
|
|
fn test_very_high_min_cut() {
|
|
let thresholds = GateThresholds::default();
|
|
|
|
let decision = decide_structural_full(1000.0, &thresholds);
|
|
assert_eq!(decision, GateDecision::Permit);
|
|
}
|
|
|
|
// Helper
|
|
fn decide_evidence_full(e_aggregate: f64, thresholds: &GateThresholds) -> EvidenceDecision {
|
|
if e_aggregate < thresholds.tau_deny {
|
|
EvidenceDecision::Reject
|
|
} else if e_aggregate >= thresholds.tau_permit {
|
|
EvidenceDecision::Accept
|
|
} else {
|
|
EvidenceDecision::Continue
|
|
}
|
|
}
|
|
|
|
fn decide_structural_full(min_cut: f64, thresholds: &GateThresholds) -> GateDecision {
|
|
if min_cut >= thresholds.min_cut {
|
|
GateDecision::Permit
|
|
} else {
|
|
GateDecision::Deny
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod serialization {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_decision_serialization() {
|
|
let decisions = [
|
|
GateDecision::Permit,
|
|
GateDecision::Defer,
|
|
GateDecision::Deny,
|
|
];
|
|
|
|
for decision in &decisions {
|
|
let json = serde_json::to_string(decision).unwrap();
|
|
let restored: GateDecision = serde_json::from_str(&json).unwrap();
|
|
assert_eq!(*decision, restored);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_decision_json_values() {
|
|
assert_eq!(
|
|
serde_json::to_string(&GateDecision::Permit).unwrap(),
|
|
"\"permit\""
|
|
);
|
|
assert_eq!(
|
|
serde_json::to_string(&GateDecision::Defer).unwrap(),
|
|
"\"defer\""
|
|
);
|
|
assert_eq!(
|
|
serde_json::to_string(&GateDecision::Deny).unwrap(),
|
|
"\"deny\""
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_thresholds_serialization() {
|
|
let thresholds = GateThresholds::default();
|
|
let json = serde_json::to_string(&thresholds).unwrap();
|
|
let restored: GateThresholds = serde_json::from_str(&json).unwrap();
|
|
|
|
assert_eq!(thresholds.tau_deny, restored.tau_deny);
|
|
assert_eq!(thresholds.tau_permit, restored.tau_permit);
|
|
assert_eq!(thresholds.min_cut, restored.min_cut);
|
|
}
|
|
}
|
|
|
|
// Property-based tests
|
|
#[cfg(test)]
|
|
mod property_tests {
|
|
use super::*;
|
|
use proptest::prelude::*;
|
|
|
|
proptest! {
|
|
#[test]
|
|
fn prop_permit_requires_all_pass(
|
|
min_cut in 0.0f64..100.0,
|
|
shift in 0.0f64..1.0,
|
|
e_val in 0.001f64..1000.0
|
|
) {
|
|
let thresholds = GateThresholds::default();
|
|
|
|
let structural_ok = min_cut >= thresholds.min_cut;
|
|
let shift_ok = shift < thresholds.max_shift;
|
|
let evidence_ok = e_val >= thresholds.tau_permit;
|
|
|
|
let decision = apply_filters(min_cut, shift, e_val, &thresholds);
|
|
|
|
if decision == GateDecision::Permit {
|
|
assert!(structural_ok && shift_ok && evidence_ok);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn prop_structural_fail_is_deny(min_cut in 0.0f64..4.9) {
|
|
let thresholds = GateThresholds::default();
|
|
// Any structural failure (min_cut < 5.0) should result in Deny
|
|
let decision = apply_filters(min_cut, 0.0, 1000.0, &thresholds);
|
|
assert_eq!(decision, GateDecision::Deny);
|
|
}
|
|
|
|
#[test]
|
|
fn prop_evidence_deny_threshold(e_val in 0.0f64..0.009) {
|
|
let thresholds = GateThresholds::default();
|
|
// E-value below tau_deny should result in Deny (if structural passes)
|
|
let decision = apply_filters(100.0, 0.0, e_val, &thresholds);
|
|
assert_eq!(decision, GateDecision::Deny);
|
|
}
|
|
}
|
|
|
|
fn apply_filters(
|
|
min_cut: f64,
|
|
shift_pressure: f64,
|
|
e_aggregate: f64,
|
|
thresholds: &GateThresholds,
|
|
) -> GateDecision {
|
|
if min_cut < thresholds.min_cut {
|
|
return GateDecision::Deny;
|
|
}
|
|
if shift_pressure >= thresholds.max_shift {
|
|
return GateDecision::Defer;
|
|
}
|
|
if e_aggregate < thresholds.tau_deny {
|
|
return GateDecision::Deny;
|
|
}
|
|
if e_aggregate < thresholds.tau_permit {
|
|
return GateDecision::Defer;
|
|
}
|
|
GateDecision::Permit
|
|
}
|
|
}
|