//! ADR-262 P1 acceptance gates. Each test below IS an acceptance criterion. //! //! - round-trip: snapshot → FieldEvent → serde → equal //! - is_fusable: emitted event passes the §11 fusability invariant //! - fusion ingest accept: `RuFieldFusion::ingest` accepts it + `infer` runs //! - privacy safety: `Derived` never maps to a low-privacy class (the §3.3 trap) //! - determinism: same snapshot + same signer seed → identical event use rufield_core::{FusionEngine, InferenceQuery, PrivacyClass}; use rufield_fusion::RuFieldFusion; use rufield_provenance::{is_fusable, verify_event, Signer}; use wifi_densepose_rufield::{ map_privacy, snapshot_to_field_event, RuViewPrivacyClass, SensingClass, SensingFeatures, SensingSnapshot, SignalField, }; const SEED: &[u8; 32] = b"adr-262-bridge-seed-32-bytes-ok!"; fn signer() -> Signer { Signer::from_seed(SEED) } /// A representative snapshot with a real signal field (so a position is derived). fn sample_snapshot() -> SensingSnapshot { SensingSnapshot { timestamp_ns: 1_791_986_400_123_456_789, features: SensingFeatures { mean_rssi: -52.5, variance: 0.73, motion_band_power: 2.4, breathing_band_power: 0.6, dominant_freq_hz: 0.27, change_points: 2, spectral_power: 4.1, }, classification: SensingClass { motion_level: "high".into(), presence: true, confidence: 0.88, }, signal_field: Some(SignalField { grid_size: [2, 1, 2], // peak at flat index 2 → cell [1,0,0] values: vec![0.1, 0.2, 0.9, 0.3], }), trust_class: RuViewPrivacyClass::Anonymous, demoted: false, identity_bound: false, node_id: "esp32_room_01".into(), } } #[test] fn gate_round_trip_serde_equal() { let ev = snapshot_to_field_event(&sample_snapshot(), &signer()); let json = serde_json::to_string(&ev).expect("serialize"); let back: rufield_core::FieldEvent = serde_json::from_str(&json).expect("deserialize"); assert_eq!(ev, back, "FieldEvent must round-trip through serde unchanged"); } #[test] fn gate_is_fusable_verified_receipt() { let ev = snapshot_to_field_event(&sample_snapshot(), &signer()); // Real (non-synthetic) event must carry a verifying ed25519 signature. assert!(!ev.provenance.synthetic, "live event must NOT be marked synthetic"); assert!(ev.provenance.signature_hex.is_some(), "must be signed"); assert!(verify_event(&ev).is_ok(), "signature must verify"); assert!(is_fusable(&ev), "verified receipt ⇒ fusable (§11 invariant)"); } #[test] fn gate_fusion_ingest_accepts_and_infers() { let ev = snapshot_to_field_event(&sample_snapshot(), &signer()); let mut engine = RuFieldFusion::new(); engine.ingest(ev).expect("fusion engine must accept the signed event"); // infer() must run without error (may or may not produce inferences). let inferences = engine .infer(&InferenceQuery::all()) .expect("infer() must run"); // The graph recorded the event/sensor provenance nodes. assert!( engine.graph().node_count() >= 2, "ingest should record sensor + event nodes" ); let _ = inferences; // count is not an accuracy claim } #[test] fn gate_privacy_safety_derived_never_maps_to_low_privacy() { // THE critical §3.3 gate. Derived carries identity ⇒ P4/P5, NEVER P1. let p4 = map_privacy(RuViewPrivacyClass::Derived, false); let p5 = map_privacy(RuViewPrivacyClass::Derived, true); assert_eq!(p4, PrivacyClass::P4); assert_eq!(p5, PrivacyClass::P5); assert!(p4 >= PrivacyClass::P4, "Derived must be in the identity tier"); assert_ne!(p4, PrivacyClass::P1, "Derived must NEVER be P1"); // And end-to-end: an emitted event from a Derived snapshot must be P4/P5. let mut snap = sample_snapshot(); snap.trust_class = RuViewPrivacyClass::Derived; let ev = snapshot_to_field_event(&snap, &signer()); assert!( ev.observation.privacy_class >= PrivacyClass::P4, "emitted Derived event must be P4 or P5, got {:?}", ev.observation.privacy_class ); assert_eq!(ev.observation.privacy_class, ev.tensor.privacy_class); } /// Full §3.3 table over every RuView class → expected RuField class. #[test] fn gate_privacy_table_over_every_ruview_class() { let cases = [ (RuViewPrivacyClass::Raw, false, PrivacyClass::P0), (RuViewPrivacyClass::Derived, false, PrivacyClass::P4), (RuViewPrivacyClass::Derived, true, PrivacyClass::P5), (RuViewPrivacyClass::Anonymous, false, PrivacyClass::P2), (RuViewPrivacyClass::Restricted, false, PrivacyClass::P2), ]; for (ruview, id_bound, expected) in cases { assert_eq!( map_privacy(ruview, id_bound), expected, "{ruview:?} (identity_bound={id_bound}) must map to {expected:?}" ); } } /// Fail-closed: a demoted Raw snapshot must NOT emit P0 (raw) — it floors to P2. #[test] fn gate_demotion_is_fail_closed() { let mut snap = sample_snapshot(); snap.trust_class = RuViewPrivacyClass::Raw; // would be P0 snap.demoted = true; // governed engine demotion let ev = snapshot_to_field_event(&snap, &signer()); assert!( ev.observation.privacy_class >= PrivacyClass::P2, "demoted cycle must floor to >= P2, got {:?}", ev.observation.privacy_class ); } #[test] fn gate_determinism_same_seed_identical_event() { let snap = sample_snapshot(); let a = snapshot_to_field_event(&snap, &Signer::from_seed(SEED)); let b = snapshot_to_field_event(&snap, &Signer::from_seed(SEED)); assert_eq!(a, b, "same snapshot + same signer seed ⇒ identical event"); // Including the signature (ed25519 is deterministic). assert_eq!(a.provenance.signature_hex, b.provenance.signature_hex); } #[test] fn no_fabricated_position_when_field_absent() { let mut snap = sample_snapshot(); snap.signal_field = None; let ev = snapshot_to_field_event(&snap, &signer()); assert!(ev.observation.range_m.is_none(), "no field ⇒ no fabricated range"); assert!(ev.observation.space_cell.is_none(), "no field ⇒ no fabricated cell"); assert!( ev.observation.motion_vector.is_none(), "no field ⇒ no fabricated motion vector" ); } #[test] fn derives_real_position_from_field_peak() { let ev = snapshot_to_field_event(&sample_snapshot(), &signer()); // peak at flat index 2, grid [2,1,2] (row-major) → cell [1,0,0] assert_eq!(ev.observation.space_cell, Some([1, 0, 0])); assert_eq!(ev.observation.range_m, Some(1.0)); }