//! Acceptance tests for ADR-121 §2.5 — `CoherenceGate` hysteresis + debounce. use wifi_densepose_bfld::coherence_gate::{DEBOUNCE_NS, HYSTERESIS}; use wifi_densepose_bfld::{CoherenceGate, GateAction}; #[test] fn fresh_gate_starts_in_accept_with_no_pending() { let g = CoherenceGate::new(); assert_eq!(g.current(), GateAction::Accept); assert_eq!(g.pending(), None); } #[test] fn low_score_stays_in_accept_with_no_pending() { let mut g = CoherenceGate::new(); let out = g.evaluate(0.3, 0); assert_eq!(out, GateAction::Accept); assert_eq!(g.pending(), None); } #[test] fn score_just_past_boundary_but_within_hysteresis_does_not_pend() { // current = Accept, upper edge = 0.5, hysteresis = 0.05 → need >= 0.55 to start pending. let mut g = CoherenceGate::new(); let out = g.evaluate(0.52, 0); assert_eq!(out, GateAction::Accept); assert_eq!(g.pending(), None, "0.52 must not start a pending transition"); } #[test] fn score_clearly_past_hysteresis_starts_pending() { let mut g = CoherenceGate::new(); let out = g.evaluate(0.6, 0); assert_eq!(out, GateAction::Accept, "still Accept until debounce elapses"); assert_eq!(g.pending(), Some(GateAction::PredictOnly)); } #[test] fn pending_action_promotes_after_full_debounce() { let mut g = CoherenceGate::new(); g.evaluate(0.6, 0); assert_eq!(g.current(), GateAction::Accept); let out = g.evaluate(0.6, DEBOUNCE_NS); assert_eq!(out, GateAction::PredictOnly); assert_eq!(g.pending(), None); } #[test] fn pending_action_does_not_promote_before_debounce() { let mut g = CoherenceGate::new(); g.evaluate(0.6, 0); let out = g.evaluate(0.6, DEBOUNCE_NS - 1); assert_eq!(out, GateAction::Accept); assert_eq!(g.pending(), Some(GateAction::PredictOnly)); } #[test] fn returning_to_current_band_cancels_pending() { let mut g = CoherenceGate::new(); g.evaluate(0.6, 0); // pending PredictOnly let out = g.evaluate(0.4, 1_000_000_000); // 1s later, back in Accept band assert_eq!(out, GateAction::Accept); assert_eq!(g.pending(), None, "returning to current band cancels pending"); } #[test] fn changing_pending_target_resets_the_debounce_clock() { let mut g = CoherenceGate::new(); g.evaluate(0.6, 0); // pending PredictOnly at t=0 g.evaluate(0.95, 1_000_000_000); // pending Recalibrate at t=1s (clock reset) // At t=1s + DEBOUNCE_NS - 1, still not promoted (Recalibrate pending since 1s) let out = g.evaluate(0.95, 1_000_000_000 + DEBOUNCE_NS - 1); assert_eq!(out, GateAction::Accept); // At t=1s + DEBOUNCE_NS, promoted to Recalibrate let out = g.evaluate(0.95, 1_000_000_000 + DEBOUNCE_NS); assert_eq!(out, GateAction::Recalibrate); } #[test] fn downward_transitions_also_require_hysteresis() { let mut g = CoherenceGate::new(); // Force gate into PredictOnly state. g.evaluate(0.6, 0); g.evaluate(0.6, DEBOUNCE_NS); assert_eq!(g.current(), GateAction::PredictOnly); // 0.48 is below 0.5 but only by 0.02 — within hysteresis envelope. let out = g.evaluate(0.48, 2 * DEBOUNCE_NS); assert_eq!(out, GateAction::PredictOnly); assert_eq!(g.pending(), None, "0.48 is within downward hysteresis"); // 0.44 is below 0.5 - 0.05 = 0.45 → starts pending Accept. g.evaluate(0.44, 3 * DEBOUNCE_NS); assert_eq!(g.pending(), Some(GateAction::Accept)); } #[test] fn spike_to_one_then_back_to_zero_never_promotes_to_recalibrate() { let mut g = CoherenceGate::new(); g.evaluate(1.0, 0); // pending Recalibrate at t=0 // 1 second later score is back to 0 — cancel pending. let out = g.evaluate(0.0, 1_000_000_000); assert_eq!(out, GateAction::Accept); assert_eq!(g.pending(), None); // Even waiting longer, the gate stays in Accept. let out = g.evaluate(0.0, 100 * DEBOUNCE_NS); assert_eq!(out, GateAction::Accept); } #[test] fn boundary_value_with_hysteresis_does_not_promote() { // Edge: current=Accept, score = upper_edge + HYSTERESIS - epsilon (just below). let mut g = CoherenceGate::new(); let out = g.evaluate(0.5 + HYSTERESIS - 0.0001, 0); assert_eq!(out, GateAction::Accept); assert_eq!(g.pending(), None); } #[test] fn boundary_value_at_hysteresis_exact_does_pend() { let mut g = CoherenceGate::new(); let out = g.evaluate(0.5 + HYSTERESIS, 0); assert_eq!(out, GateAction::Accept); assert_eq!(g.pending(), Some(GateAction::PredictOnly)); } #[test] fn nan_score_stays_in_current_action_with_no_pending() { let mut g = CoherenceGate::new(); let out = g.evaluate(f32::NAN, 0); // NaN maps to Accept via from_score; gate stays in Accept and clears pending. assert_eq!(out, GateAction::Accept); assert_eq!(g.pending(), None); }