use super::*;
fn nid(b: u8) -> NodeId {
NodeId::from_bytes([b; 32])
}
fn h(b: u8) -> Hash {
Hash::from_bytes([b; 32])
}
fn provider(node: NodeId) -> Provider {
Provider {
node,
coverage: Coverage::full(1),
}
}
fn small_cfg() -> RecordStoreConfig {
RecordStoreConfig {
max_records_per_publisher: 3,
max_records_global: 6,
max_providers_per_hash: 4,
ttl_us: 1_000_000, }
}
#[test]
fn insert_new_returns_inserted_and_increments_counts() {
let mut s = RecordStore::new(small_cfg());
assert_eq!(s.len(), 0);
let out = s.insert_at(nid(1), h(1), Coverage::full(1), 0);
assert_eq!(out, InsertOutcome::Inserted);
assert!(out.accepted());
assert_eq!(s.len(), 1);
assert_eq!(s.publisher_record_count(&nid(1)), 1);
assert_eq!(s.providers_at(&h(1), 0), vec![provider(nid(1))]);
}
#[test]
fn re_insert_same_holder_hash_refreshes_not_duplicates() {
let mut s = RecordStore::new(small_cfg());
s.insert_at(nid(1), h(1), Coverage::full(1), 100);
let out = s.insert_at(nid(1), h(1), Coverage::full(1), 200);
assert_eq!(out, InsertOutcome::Refreshed);
assert!(out.accepted());
assert_eq!(s.len(), 1);
assert_eq!(s.publisher_record_count(&nid(1)), 1);
let entries = s.by_hash.get(&h(1)).unwrap();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].receive_us, 200);
assert_eq!(entries[0].expiry_us, 200 + 1_000_000);
}
#[test]
fn per_publisher_cap_hard_rejects() {
let mut s = RecordStore::new(small_cfg());
for i in 1..=3u8 {
assert_eq!(
s.insert_at(nid(7), h(i), Coverage::full(1), i.into()),
InsertOutcome::Inserted
);
}
let out = s.insert_at(nid(7), h(4), Coverage::full(1), 10);
assert_eq!(out, InsertOutcome::RejectedQuotaExceeded);
assert!(!out.accepted());
assert_eq!(s.publisher_record_count(&nid(7)), 3);
assert_eq!(s.len(), 3);
}
#[test]
fn per_publisher_cap_does_not_block_other_publishers() {
let mut s = RecordStore::new(small_cfg());
for i in 1..=3u8 {
s.insert_at(nid(7), h(i), Coverage::full(1), i.into());
}
assert_eq!(
s.insert_at(nid(8), h(4), Coverage::full(1), 100),
InsertOutcome::Inserted
);
assert_eq!(s.publisher_record_count(&nid(8)), 1);
}
#[test]
fn global_cap_evicts_oldest_when_publisher_below_quota() {
let mut s = RecordStore::new(small_cfg());
for (p, h_byte, ts) in [
(1u8, 1u8, 10u64),
(1, 2, 20),
(1, 3, 30),
(2, 4, 40),
(2, 5, 50),
(2, 6, 60),
] {
s.insert_at(nid(p), h(h_byte), Coverage::full(1), ts);
}
assert_eq!(s.len(), 6);
let out = s.insert_at(nid(3), h(7), Coverage::full(1), 70);
assert_eq!(out, InsertOutcome::Inserted);
assert_eq!(s.len(), 6);
assert!(s.providers_at(&h(1), 0).is_empty(), "oldest hash evicted");
assert_eq!(s.providers_at(&h(7), 0), vec![provider(nid(3))]);
assert_eq!(s.publisher_record_count(&nid(1)), 2);
}
#[test]
fn ttl_expiry_drops_record_on_providers_at() {
let mut s = RecordStore::new(small_cfg());
s.insert_at(nid(1), h(1), Coverage::full(1), 1_000);
assert_eq!(s.providers_at(&h(1), 1_000).len(), 1);
let after = 1_000 + 2_000_000;
assert!(s.providers_at(&h(1), after).is_empty());
assert_eq!(s.len(), 0);
assert_eq!(s.publisher_record_count(&nid(1)), 0);
}
#[test]
fn gc_sweep_removes_every_expired_record() {
let mut cfg = small_cfg();
cfg.ttl_us = 100;
let mut s = RecordStore::new(cfg);
for i in 1..=4u8 {
s.insert_at(nid(i), h(i), Coverage::full(1), 0);
}
assert_eq!(s.len(), 4);
let removed = s.gc(10_000);
assert_eq!(removed, 4);
assert_eq!(s.len(), 0);
}
#[test]
fn per_hash_cap_evicts_oldest_holder_for_that_hash() {
let mut s = RecordStore::new(small_cfg());
for i in 1..=4u8 {
assert_eq!(
s.insert_at(nid(i), h(1), Coverage::full(1), i.into()),
InsertOutcome::Inserted
);
}
assert_eq!(s.providers_at(&h(1), 0).len(), 4);
assert_eq!(
s.insert_at(nid(5), h(1), Coverage::full(1), 100),
InsertOutcome::Inserted
);
let providers: std::collections::HashSet<NodeId> = s
.providers_at(&h(1), 0)
.into_iter()
.map(|p| p.node)
.collect();
assert!(!providers.contains(&nid(1)));
assert!(providers.contains(&nid(5)));
assert_eq!(providers.len(), 4);
}
#[test]
fn default_config_matches_adr_022() {
let d = RecordStoreConfig::default();
assert_eq!(d.max_records_per_publisher, 100_000);
assert_eq!(d.max_records_global, 1_000_000);
assert_eq!(d.max_providers_per_hash, MAX_PROVIDERS_PER_HASH);
assert_eq!(d.max_providers_per_hash, 50);
assert_eq!(d.ttl_us, 3_600_000_000);
}
#[test]
fn publisher_quota_beats_global_lru() {
let cfg = RecordStoreConfig {
max_records_per_publisher: 3,
max_records_global: 3,
max_providers_per_hash: 100,
ttl_us: 1_000_000,
};
let mut s = RecordStore::new(cfg);
for i in 1..=3u8 {
assert_eq!(
s.insert_at(nid(1), h(i), Coverage::full(1), i.into()),
InsertOutcome::Inserted
);
}
let len_before = s.len();
let count_before = s.publisher_record_count(&nid(1));
let out = s.insert_at(nid(1), h(99), Coverage::full(1), 100);
assert_eq!(out, InsertOutcome::RejectedQuotaExceeded);
assert_eq!(s.len(), len_before, "rejected insert must not evict");
assert_eq!(s.publisher_record_count(&nid(1)), count_before);
for i in 1..=3u8 {
assert!(!s.providers_at(&h(i), 0).is_empty(), "h({i}) intact");
}
}
#[test]
fn outcome_accepted_helper() {
assert!(InsertOutcome::Inserted.accepted());
assert!(InsertOutcome::Refreshed.accepted());
assert!(!InsertOutcome::RejectedQuotaExceeded.accepted());
}
#[test]
fn refresh_does_not_count_against_quota() {
let mut s = RecordStore::new(small_cfg());
for i in 1..=3u8 {
s.insert_at(nid(1), h(i), Coverage::full(1), i.into());
}
assert_eq!(s.publisher_record_count(&nid(1)), 3);
let out = s.insert_at(nid(1), h(1), Coverage::full(1), 100);
assert_eq!(out, InsertOutcome::Refreshed);
assert_eq!(s.publisher_record_count(&nid(1)), 3);
assert_eq!(
s.insert_at(nid(1), h(99), Coverage::full(1), 200),
InsertOutcome::RejectedQuotaExceeded
);
}
#[test]
fn ttl_is_anchored_to_wall_clock_regardless_of_insert_volume() {
let mut cfg = small_cfg();
cfg.max_records_per_publisher = usize::MAX;
cfg.max_records_global = usize::MAX;
cfg.max_providers_per_hash = usize::MAX;
cfg.ttl_us = 1_000;
let mut s = RecordStore::new(cfg);
for i in 0..10_000u32 {
let mut holder = [0u8; 32];
holder[..4].copy_from_slice(&i.to_le_bytes());
s.insert_at(NodeId::from_bytes(holder), h(0xEE), Coverage::full(1), 500);
}
s.insert_at(nid(0xCC), h(0xFF), Coverage::full(1), 500);
let entries = s.by_hash.get(&h(0xFF)).unwrap();
assert_eq!(entries.len(), 1);
assert_eq!(
entries[0].expiry_us, 1_500,
"TTL drifted to {} — must be exactly receive_us + ttl_us",
entries[0].expiry_us,
);
assert_eq!(s.providers_at(&h(0xFF), 1_499).len(), 1);
assert!(s.providers_at(&h(0xFF), 1_501).is_empty());
}
#[test]
fn insert_at_per_hash_cap_does_not_also_trigger_global_eviction() {
let cfg = RecordStoreConfig {
max_records_per_publisher: 4,
max_records_global: 4,
max_providers_per_hash: 2,
ttl_us: 1_000_000,
};
let mut s = RecordStore::new(cfg);
s.insert_at(nid(1), h(0xAA), Coverage::full(1), 10);
s.insert_at(nid(2), h(0xAA), Coverage::full(1), 20);
s.insert_at(nid(3), h(0xBB), Coverage::full(1), 30);
s.insert_at(nid(4), h(0xBB), Coverage::full(1), 40);
assert_eq!(s.len(), 4);
let out = s.insert_at(nid(5), h(0xAA), Coverage::full(1), 50);
assert_eq!(out, InsertOutcome::Inserted);
assert_eq!(s.len(), 4, "global count must remain at the cap");
let a_holders: std::collections::HashSet<NodeId> = s
.providers_at(&h(0xAA), 0)
.into_iter()
.map(|p| p.node)
.collect();
assert!(!a_holders.contains(&nid(1)));
assert!(a_holders.contains(&nid(2)));
assert!(a_holders.contains(&nid(5)));
let b_holders: std::collections::HashSet<NodeId> = s
.providers_at(&h(0xBB), 0)
.into_iter()
.map(|p| p.node)
.collect();
assert!(
b_holders.contains(&nid(3)),
"hash B's nid(3) must NOT be evicted by the per-hash-cap insert into hash A"
);
assert!(b_holders.contains(&nid(4)));
}
fn assert_tri_index_consistent(s: &RecordStore) {
let by_hash_total: usize = s.by_hash.values().map(Vec::len).sum();
assert_eq!(
by_hash_total,
s.global_lru.len(),
"by_hash total vs global_lru"
);
let mut counted: std::collections::HashMap<NodeId, usize> = std::collections::HashMap::new();
for (hash, entries) in &s.by_hash {
assert!(!entries.is_empty(), "empty provider bucket must be pruned");
for e in entries {
assert!(
s.global_lru
.contains(&(e.receive_us, e.sequence, *hash, e.holder)),
"by_hash entry missing from global_lru"
);
*counted.entry(e.holder).or_insert(0) += 1;
}
}
assert_eq!(counted, s.by_publisher_count, "by_publisher_count drift");
}
#[test]
fn tri_index_stays_consistent_across_mixed_operations() {
let cfg = RecordStoreConfig {
max_records_per_publisher: 5,
max_records_global: 8,
max_providers_per_hash: 3,
ttl_us: 1_000,
};
let mut s = RecordStore::new(cfg);
for (p, hb, ts) in [
(1u8, 1u8, 100u64),
(2, 1, 110),
(3, 1, 120), (1, 2, 130),
(2, 2, 140),
(3, 2, 150),
(1, 3, 160),
(2, 3, 170), ] {
assert_eq!(
s.insert_at(nid(p), h(hb), Coverage::full(1), ts),
InsertOutcome::Inserted
);
}
assert_eq!(s.len(), 8);
assert_tri_index_consistent(&s);
assert_eq!(
s.insert_at(nid(4), h(1), Coverage::full(1), 180),
InsertOutcome::Inserted
);
assert_eq!(s.len(), 8, "per-hash eviction must not change global count");
assert!(
!s.providers_at(&h(1), 0).iter().any(|p| p.node == nid(1)),
"oldest h(1) holder evicted by per-hash cap"
);
assert_tri_index_consistent(&s);
assert_eq!(
s.insert_at(nid(4), h(3), Coverage::full(1), 190),
InsertOutcome::Inserted
);
assert_eq!(s.len(), 8, "global cap holds the store at 8");
assert_tri_index_consistent(&s);
assert_eq!(
s.insert_at(nid(3), h(1), Coverage::full(1), 200),
InsertOutcome::Refreshed
);
assert_eq!(s.len(), 8);
assert_tri_index_consistent(&s);
let removed = s.gc(1_175);
assert_eq!(removed, 5, "GC must drop the expired cohort, not no-op");
assert_eq!(s.len(), 3);
assert_tri_index_consistent(&s);
for (p, ts) in [(7u8, 6_000u64), (8, 6_001), (9, 6_002)] {
assert_eq!(
s.insert_at(nid(p), h(1), Coverage::full(1), ts),
InsertOutcome::Inserted
);
}
assert_tri_index_consistent(&s);
}
#[test]
fn gc_drops_orphan_global_lru_key_without_spinning() {
let mut cfg = small_cfg();
cfg.ttl_us = 100;
let mut s = RecordStore::new(cfg);
s.insert_at(nid(1), h(1), Coverage::full(1), 0);
s.global_lru.insert((5, 999, h(0xEE), nid(2)));
let removed = s.gc(10_000);
assert_eq!(removed, 2);
assert!(s.is_empty());
assert_tri_index_consistent(&s);
}
#[test]
fn providers_at_scrubs_only_expired_holders_in_mixed_bucket() {
let mut cfg = small_cfg();
cfg.ttl_us = 1_000;
cfg.max_providers_per_hash = 10; let mut s = RecordStore::new(cfg);
s.insert_at(nid(1), h(1), Coverage::full(1), 0); s.insert_at(nid(2), h(1), Coverage::full(1), 5_000); s.insert_at(nid(3), h(1), Coverage::full(1), 5_500); let live: std::collections::HashSet<NodeId> = s
.providers_at(&h(1), 2_000)
.into_iter()
.map(|p| p.node)
.collect();
assert_eq!(live.len(), 2);
assert!(!live.contains(&nid(1)), "expired holder scrubbed");
assert!(live.contains(&nid(2)));
assert!(live.contains(&nid(3)));
assert_eq!(
s.publisher_record_count(&nid(1)),
0,
"count drops for expired"
);
assert_eq!(s.publisher_record_count(&nid(2)), 1, "live count intact");
assert_eq!(s.len(), 2);
assert_tri_index_consistent(&s);
}