173 lines
6.6 KiB
Rust
173 lines
6.6 KiB
Rust
//! 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));
|
|
}
|