//! End-to-end pipeline tests for `BfldEmitter`. ADR-118 ยง2.1. #![cfg(feature = "std")] use wifi_densepose_bfld::coherence_gate::DEBOUNCE_NS; use wifi_densepose_bfld::{ BfldEmitter, GateAction, IdentityEmbedding, PrivacyClass, SensingInputs, EMBEDDING_DIM, }; fn inputs(ts_ns: u64, risk_factors: [f32; 4]) -> SensingInputs { let [sep, stab, consist, risk_conf] = risk_factors; SensingInputs { timestamp_ns: ts_ns, presence: true, motion: 0.5, person_count: 1, sensing_confidence: 0.9, sep, stab, consist, risk_conf, rf_signature_hash: Some([0xCD; 32]), } } fn dummy_embedding() -> IdentityEmbedding { IdentityEmbedding::from_raw([0.1; EMBEDDING_DIM]) } #[test] fn emitter_emits_event_under_low_risk() { let mut e = BfldEmitter::new("seed-01"); let out = e .emit(inputs(0, [0.2, 0.2, 0.2, 0.2]), Some(dummy_embedding())) .expect("low risk should produce an event"); assert_eq!(out.node_id, "seed-01"); assert!(out.presence); assert!(out.identity_risk_score.is_some()); assert_eq!(e.current_action(), GateAction::Accept); } #[test] fn emitter_drops_event_under_sustained_high_risk() { let mut e = BfldEmitter::new("seed-01"); // First call: score ~ 0.7 pending Reject. Event still emits this turn // because the gate hasn't promoted yet (current is still Accept). let first = e.emit(inputs(0, [1.0, 1.0, 1.0, 0.8]), Some(dummy_embedding())); assert!(first.is_some(), "first high-risk call still emits"); // After debounce: current becomes Reject -> event dropped. let after = e.emit( inputs(DEBOUNCE_NS, [1.0, 1.0, 1.0, 0.8]), Some(dummy_embedding()), ); assert!(after.is_none(), "post-debounce Reject drops the event"); assert_eq!(e.current_action(), GateAction::Reject); } #[test] fn emitter_drains_ring_on_recalibrate() { let mut e = BfldEmitter::new("seed-01"); // Pump 5 embeddings under a slow rising score so the ring fills. for i in 0..5 { let _ = e.emit( inputs(i * 1_000_000, [0.3, 0.3, 0.3, 0.3]), Some(dummy_embedding()), ); } assert_eq!(e.ring_len(), 5); // Now push a Recalibrate-grade score and run past debounce. e.emit(inputs(10_000_000, [1.0, 1.0, 1.0, 1.0]), Some(dummy_embedding())); let _ = e.emit( inputs(10_000_000 + DEBOUNCE_NS, [1.0, 1.0, 1.0, 1.0]), Some(dummy_embedding()), ); assert_eq!(e.current_action(), GateAction::Recalibrate); assert_eq!(e.ring_len(), 0, "Recalibrate must drain the embedding ring"); } #[test] fn restricted_class_strips_identity_fields_in_emitted_event() { let mut e = BfldEmitter::new("seed-01").with_privacy_class(PrivacyClass::Restricted); let out = e .emit(inputs(0, [0.2, 0.2, 0.2, 0.2]), Some(dummy_embedding())) .expect("Accept should emit"); assert!( out.identity_risk_score.is_none(), "class 3 must strip identity_risk_score", ); assert!( out.rf_signature_hash.is_none(), "class 3 must strip rf_signature_hash", ); } #[test] fn with_zone_sets_default_zone_id_on_event() { let mut e = BfldEmitter::new("seed-01").with_zone("kitchen"); let out = e .emit(inputs(0, [0.1, 0.1, 0.1, 0.1]), Some(dummy_embedding())) .unwrap(); assert_eq!(out.zone_id.as_deref(), Some("kitchen")); } #[test] fn embedding_is_pushed_to_ring_even_when_event_dropped() { let mut e = BfldEmitter::new("seed-01"); // Drive into Reject state. e.emit(inputs(0, [1.0, 1.0, 1.0, 0.8]), Some(dummy_embedding())); e.emit( inputs(DEBOUNCE_NS, [1.0, 1.0, 1.0, 0.8]), Some(dummy_embedding()), ); assert_eq!(e.current_action(), GateAction::Reject); // Even though the gate dropped events, the embeddings landed in the ring. assert_eq!(e.ring_len(), 2); } #[test] fn ring_unchanged_when_no_embedding_supplied() { let mut e = BfldEmitter::new("seed-01"); let _ = e.emit(inputs(0, [0.1, 0.1, 0.1, 0.1]), None); assert_eq!(e.ring_len(), 0); }