Files
ruvnet--RuView/v2/crates/wifi-densepose-bfld/tests/embedding_ring.rs
T
ruv 60eaaa5af1 feat(adr-118/p2.2): EmbeddingRing 64-entry FIFO buffer — 53/53 GREEN
Iter 8. Lands the lifecycle half of ADR-120 §2.5: a bounded, in-place,
no_std-compatible ring of IdentityEmbeddings. Insertion is O(1); when
full, push evicts the oldest entry, whose Drop runs and zeroizes the
f32 storage. drain() clears the ring on the coherence-gate Recalibrate
action (ADR-121 §2.4).

Added:
- src/embedding_ring.rs (no_std-compatible; no heap):
  * EmbeddingRing struct with [Option<IdentityEmbedding>; RING_CAPACITY=64]
    backing array, head cursor, count
  * EmbeddingRing::new() / Default impl
  * push(emb) -> Option<IdentityEmbedding>  (evicted oldest when full)
  * len / is_empty / capacity / is_full / iter
  * iter() returns occupied slots in insertion order (oldest first)
  * drain() -> usize  (empties the ring, returns count drained)
- pub use EmbeddingRing, RING_CAPACITY from lib.rs

Uses `[const { None }; RING_CAPACITY]` (stable since 1.79) to initialize
the slot array for a non-Copy element type.

tests/embedding_ring.rs (9 named tests, all green):
  new_ring_is_empty
  default_constructor_matches_new
  push_below_capacity_returns_none
  iter_yields_in_insertion_order
  push_at_capacity_evicts_oldest_and_returns_it
    (verifies eviction reports the FIRST pushed value, not the last)
  push_beyond_capacity_keeps_last_n_entries
    (after 74 pushes into a 64-slot ring, the surviving 64 are positions 10..74)
  drain_empties_the_ring_and_returns_count
  drain_on_empty_ring_returns_zero
  ring_can_be_refilled_after_drain
    (post-drain push lands cleanly at index 0; iter yields exactly that entry)

ACs progressed:
- I2 ↑ — ring eviction and explicit drain both drop IdentityEmbeddings,
  which the iter-7 Drop impl zeroizes. The "in-RAM-only" lifecycle is now
  end-to-end: bounded buffer in, FIFO out, drain on Recalibrate.

Test config:
- cargo test --no-default-features → 31 passed (22 + 9)
- cargo test                       → 53 passed (44 + 9)

Out of scope (next iter target):
- PrivacyGate::demote(frame, target_class) — ADR-120 §2.4 monotonic class
  transition with field zeroization, refusing demote-to-Raw (compile-fail).
- SoulMatchOracle stub trait + no-op default impl (ADR-121 §2.6) so the
  Recalibrate exemption hook is wireable from `--features soul-signature`.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-05-24 14:37:03 -04:00

105 lines
2.9 KiB
Rust

//! Acceptance tests for ADR-120 §2.5 `EmbeddingRing` lifecycle.
use wifi_densepose_bfld::{EmbeddingRing, IdentityEmbedding, EMBEDDING_DIM, RING_CAPACITY};
fn embedding_with_first(v: f32) -> IdentityEmbedding {
let mut arr = [0.0f32; EMBEDDING_DIM];
arr[0] = v;
IdentityEmbedding::from_raw(arr)
}
#[test]
fn new_ring_is_empty() {
let r = EmbeddingRing::new();
assert_eq!(r.len(), 0);
assert!(r.is_empty());
assert!(!r.is_full());
assert_eq!(r.capacity(), RING_CAPACITY);
assert_eq!(r.iter().count(), 0);
}
#[test]
fn default_constructor_matches_new() {
let r = EmbeddingRing::default();
assert_eq!(r.len(), 0);
}
#[test]
fn push_below_capacity_returns_none() {
let mut r = EmbeddingRing::new();
for i in 0..5 {
let evicted = r.push(embedding_with_first(i as f32));
assert!(evicted.is_none(), "no eviction expected at i={i}");
}
assert_eq!(r.len(), 5);
}
#[test]
fn iter_yields_in_insertion_order() {
let mut r = EmbeddingRing::new();
for i in 0..5 {
r.push(embedding_with_first(i as f32));
}
let firsts: Vec<f32> = r.iter().map(|e| e.as_slice()[0]).collect();
assert_eq!(firsts, vec![0.0, 1.0, 2.0, 3.0, 4.0]);
}
#[test]
fn push_at_capacity_evicts_oldest_and_returns_it() {
let mut r = EmbeddingRing::new();
for i in 0..RING_CAPACITY {
r.push(embedding_with_first(i as f32));
}
assert!(r.is_full());
let evicted = r
.push(embedding_with_first(999.0))
.expect("must evict when full");
// The evicted slot held the very first push (first = 0.0).
assert_eq!(evicted.as_slice()[0], 0.0);
assert_eq!(r.len(), RING_CAPACITY);
}
#[test]
fn push_beyond_capacity_keeps_last_n_entries() {
let mut r = EmbeddingRing::new();
// Push capacity + 10 entries; the first 10 must have been evicted.
for i in 0..(RING_CAPACITY + 10) {
r.push(embedding_with_first(i as f32));
}
let firsts: Vec<f32> = r.iter().map(|e| e.as_slice()[0]).collect();
let expected: Vec<f32> = (10..(RING_CAPACITY + 10) as i32)
.map(|i| i as f32)
.collect();
assert_eq!(firsts, expected);
}
#[test]
fn drain_empties_the_ring_and_returns_count() {
let mut r = EmbeddingRing::new();
for i in 0..7 {
r.push(embedding_with_first(i as f32));
}
let drained = r.drain();
assert_eq!(drained, 7);
assert!(r.is_empty());
assert_eq!(r.iter().count(), 0);
}
#[test]
fn drain_on_empty_ring_returns_zero() {
let mut r = EmbeddingRing::new();
assert_eq!(r.drain(), 0);
assert!(r.is_empty());
}
#[test]
fn ring_can_be_refilled_after_drain() {
let mut r = EmbeddingRing::new();
r.push(embedding_with_first(1.0));
r.push(embedding_with_first(2.0));
r.drain();
r.push(embedding_with_first(42.0));
assert_eq!(r.len(), 1);
assert_eq!(r.iter().next().unwrap().as_slice()[0], 42.0);
}