diff --git a/v2/crates/wifi-densepose-bfld/tests/privacy_class_capability.rs b/v2/crates/wifi-densepose-bfld/tests/privacy_class_capability.rs new file mode 100644 index 00000000..482971d3 --- /dev/null +++ b/v2/crates/wifi-densepose-bfld/tests/privacy_class_capability.rs @@ -0,0 +1,142 @@ +//! `PrivacyClass::allows_network` and `allows_matter` const-helper truth +//! tables, plus a cross-consistency check against the `Sink` trait constants. +//! Iter 1 introduced these helpers; iter 3 introduced the `Sink::MIN_CLASS` +//! mechanism. The two APIs must agree. +//! +//! Why both APIs: `allows_network` / `allows_matter` are point-in-time +//! Boolean queries for ergonomics ("can I publish this frame?"); the `Sink` +//! marker-trait + `MIN_CLASS` const provides the structural enforcement at +//! compile-time. Drift between them is a silent correctness bug — this iter +//! pins the constraint that they always agree. + +use wifi_densepose_bfld::sink::{LocalKind, MatterKind, NetworkKind, Sink}; +use wifi_densepose_bfld::PrivacyClass; + +const ALL_CLASSES: [PrivacyClass; 4] = [ + PrivacyClass::Raw, + PrivacyClass::Derived, + PrivacyClass::Anonymous, + PrivacyClass::Restricted, +]; + +// --- direct truth tables ------------------------------------------------ + +#[test] +fn allows_network_truth_table() { + assert!(!PrivacyClass::Raw.allows_network()); + assert!(PrivacyClass::Derived.allows_network()); + assert!(PrivacyClass::Anonymous.allows_network()); + assert!(PrivacyClass::Restricted.allows_network()); +} + +#[test] +fn allows_matter_truth_table() { + assert!(!PrivacyClass::Raw.allows_matter()); + assert!(!PrivacyClass::Derived.allows_matter()); + assert!(PrivacyClass::Anonymous.allows_matter()); + assert!(PrivacyClass::Restricted.allows_matter()); +} + +// --- monotonicity property --------------------------------------------- + +#[test] +fn allows_matter_implies_allows_network() { + // Matter is a subset of Network — if a class is Matter-eligible, it + // must also be Network-eligible. The reverse is not true (Derived is + // Network-eligible but not Matter-eligible). + for c in ALL_CLASSES { + if c.allows_matter() { + assert!( + c.allows_network(), + "{c:?}: allows_matter without allows_network is a contract violation", + ); + } + } +} + +#[test] +fn allows_network_strictly_excludes_raw() { + // Class 0 (Raw) is the only class that fails allows_network. Any future + // refactor that lets Raw cross a NetworkSink violates ADR-118 invariant I1. + for c in ALL_CLASSES { + let expected = !matches!(c, PrivacyClass::Raw); + assert_eq!( + c.allows_network(), + expected, + "{c:?}: allows_network drift", + ); + } +} + +#[test] +fn allows_matter_strictly_requires_class_two_or_three() { + for c in ALL_CLASSES { + let expected = matches!(c, PrivacyClass::Anonymous | PrivacyClass::Restricted); + assert_eq!(c.allows_matter(), expected, "{c:?}: allows_matter drift"); + } +} + +// --- cross-consistency with Sink::MIN_CLASS ---------------------------- + +/// For a sink with `MIN_CLASS = K`, a class `C` should be accepted iff +/// `C.as_u8() >= K.as_u8()`. Iter 3 implemented exactly this in `check_class`. +/// The helpers above must agree. +fn check_consistency(class: PrivacyClass, helper_says_allowed: bool) { + let sink_min = S::MIN_CLASS.as_u8(); + let class_byte = class.as_u8(); + let sink_says_allowed = class_byte >= sink_min; + assert_eq!( + helper_says_allowed, + sink_says_allowed, + "{class:?} vs {} ({} >= {} should be {}, helper said {})", + S::KIND, + class_byte, + sink_min, + sink_says_allowed, + helper_says_allowed, + ); +} + +#[test] +fn local_sink_accepts_every_class_per_helper() { + for c in ALL_CLASSES { + // LocalSink has MIN_CLASS = Raw (byte 0) — accepts all. + check_consistency::(c, true); + } +} + +#[test] +fn network_sink_consistency_matches_allows_network() { + for c in ALL_CLASSES { + check_consistency::(c, c.allows_network()); + } +} + +#[test] +fn matter_sink_consistency_matches_allows_matter() { + for c in ALL_CLASSES { + check_consistency::(c, c.allows_matter()); + } +} + +// --- byte-value pinning ----------------------------------------------- + +#[test] +fn as_u8_returns_documented_byte_values() { + assert_eq!(PrivacyClass::Raw.as_u8(), 0); + assert_eq!(PrivacyClass::Derived.as_u8(), 1); + assert_eq!(PrivacyClass::Anonymous.as_u8(), 2); + assert_eq!(PrivacyClass::Restricted.as_u8(), 3); +} + +#[test] +fn class_byte_ordering_matches_information_density() { + // Higher numerical class = less information density. Sanity check. + let raw = PrivacyClass::Raw.as_u8(); + let derived = PrivacyClass::Derived.as_u8(); + let anonymous = PrivacyClass::Anonymous.as_u8(); + let restricted = PrivacyClass::Restricted.as_u8(); + assert!(raw < derived); + assert!(derived < anonymous); + assert!(anonymous < restricted); +}