wifi-densepose/v2/crates/wifi-densepose-sensing-server/tests/rufield_surface_test.rs

179 lines
6.9 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! 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<FieldEvent> {
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));
}
}