//! ADR-262 **P3** acceptance gate — the live RuField surface. //! //! In-process integration test (mirrors the `/ws/sensing` / #1050 oneshot //! style with `tower::ServiceExt::oneshot`): drives synthetic sensing cycles //! through the real `FieldSurface` + the real `/api/field` router, and asserts: //! //! 1. an injected `Anonymous` (occupancy) cycle surfaces a **well-formed signed //! `FieldEvent`** — `Modality::WifiCsi`, privacy class consistent with the //! trust (P2, never P1), `is_fusable` (ed25519 receipt verifies), real //! timestamp; //! 2. an empty / no-presence cycle produces **no phantom event** (explicit //! empty payload); //! 3. the **privacy-safety pin** — an injected `Derived` (identity) trust state //! never surfaces as a low-privacy event on `/api/field` (held edge-local). //! //! These gates are plumbing + privacy-safety, NOT accuracy (ADR-262 §0 / §6). use std::sync::Arc; use axum::body::Body; use axum::http::{Request, StatusCode}; use tokio::sync::RwLock; use tower::ServiceExt; // `oneshot` use wifi_densepose_rufield::{is_fusable, verify_event, FieldEvent, Modality, PrivacyClass}; use wifi_densepose_sensing_server::rufield_surface::{ self, FieldState, FieldSurface, RuViewPrivacyClass, SensingClass, SensingFeatures, SignalField, }; /// A fixed dev seed for deterministic, signed events under test. const TEST_SEED: &[u8; 32] = b"adr262-p3-integration-test-seed!"; fn features() -> SensingFeatures { SensingFeatures { mean_rssi: -55.0, variance: 0.4, motion_band_power: 2.0, breathing_band_power: 0.3, dominant_freq_hz: 0.25, change_points: 1, spectral_power: 3.0, } } fn class(presence: bool) -> SensingClass { SensingClass { motion_level: if presence { "low".into() } else { "none".into() }, presence, confidence: if presence { 0.82 } else { 0.05 }, } } /// A small 2×1×2 signal field with a clear peak, so the bridge derives a real /// (non-fabricated) position from the strongest cell. fn signal_field() -> SignalField { SignalField { grid_size: [2, 1, 2], values: vec![0.1, 0.2, 0.9, 0.3], // peak at index 2 } } /// Build a `FieldState` + the real `/api/field` + `/ws/field` router over it. fn surface_router() -> (FieldState, axum::Router) { let state: FieldState = Arc::new(RwLock::new(FieldSurface::from_seed(TEST_SEED, true))); let app = rufield_surface::router(state.clone()); (state, app) } /// Drive one cycle into the surface (the in-process equivalent of the live /// sensing loop calling `emit()` per cycle). async fn inject(state: &FieldState, trust: RuViewPrivacyClass, presence: bool, identity_bound: bool) { let snap = rufield_surface::build_snapshot( 1_791_986_400_000_000_000, "esp32_node_7".into(), features(), class(presence), Some(signal_field()), trust, false, // demoted identity_bound, ); state.write().await.emit(&snap); } /// `GET /api/field` and parse the `events` array. async fn get_field_events(app: &axum::Router) -> Vec { let resp = app .clone() .oneshot( Request::builder() .uri("/api/field") .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(resp.status(), StatusCode::OK, "/api/field must return 200"); let bytes = axum::body::to_bytes(resp.into_body(), usize::MAX).await.unwrap(); let v: serde_json::Value = serde_json::from_slice(&bytes).unwrap(); assert_eq!(v["spec"], "rufield"); serde_json::from_value(v["events"].clone()).expect("events array deserializes to FieldEvents") } #[tokio::test] async fn gate_anonymous_cycle_surfaces_wellformed_signed_event() { let (state, app) = surface_router(); inject(&state, RuViewPrivacyClass::Anonymous, true, false).await; let events = get_field_events(&app).await; assert_eq!(events.len(), 1, "one occupancy cycle ⇒ exactly one surfaced event"); let ev = &events[0]; // Well-formed: WiFi-CSI modality, real timestamp. assert_eq!(ev.tensor.modality, Modality::WifiCsi); assert_eq!(ev.timestamp_ns, 1_791_986_400_000_000_000); assert!(ev.timestamp_ns > 0, "real (non-zero) timestamp"); // Privacy consistent with the injected trust: Anonymous → P2, NEVER P1. assert_eq!(ev.observation.privacy_class, PrivacyClass::P2); assert_ne!(ev.observation.privacy_class, PrivacyClass::P1); // Signed + fusable: the ed25519 receipt verifies (real, non-synthetic). assert!(!ev.provenance.synthetic, "live event is non-synthetic"); assert!(verify_event(ev).is_ok(), "ed25519 signature must verify"); assert!(is_fusable(ev), "verified receipt ⇒ fusable"); // Real position derived from the signal-field peak (not fabricated). assert!(ev.observation.range_m.is_some(), "field peak ⇒ a real range readout"); } #[tokio::test] async fn gate_empty_cycle_produces_no_phantom_event() { let (state, app) = surface_router(); // A no-presence cycle: nothing to describe. inject(&state, RuViewPrivacyClass::Anonymous, false, false).await; let events = get_field_events(&app).await; assert!( events.is_empty(), "no-presence cycle must surface no phantom event (explicit empty payload)" ); } #[tokio::test] async fn gate_derived_trust_never_surfaces_low_privacy() { // The privacy-safety pin (ADR-262 §3.3 / §6): a Derived (identity) trust // state maps to P4/P5 and is held edge-local — it must NEVER appear on the // network surface, and certainly never as a low-privacy (P1/P2) event. for identity_bound in [false, true] { let (state, app) = surface_router(); inject(&state, RuViewPrivacyClass::Derived, true, identity_bound).await; let events = get_field_events(&app).await; assert!( events.is_empty(), "Derived cycle (identity_bound={identity_bound}) must not surface on /api/field" ); } } #[tokio::test] async fn gate_mixed_stream_surfaces_only_egress_safe_events() { // Determinism / privacy-safety over a stream: Anonymous cycles surface, // interleaved Derived cycles are dropped — the surface only ever carries // egress-safe (P1/P2) events. let (state, app) = surface_router(); inject(&state, RuViewPrivacyClass::Anonymous, true, false).await; // P2 → surfaced inject(&state, RuViewPrivacyClass::Derived, true, false).await; // P4 → dropped inject(&state, RuViewPrivacyClass::Anonymous, true, false).await; // P2 → surfaced inject(&state, RuViewPrivacyClass::Derived, true, true).await; // P5 → dropped let events = get_field_events(&app).await; assert_eq!(events.len(), 2, "only the two Anonymous cycles surface"); for ev in &events { assert_eq!(ev.observation.privacy_class, PrivacyClass::P2); assert!(is_fusable(ev)); } }