196 lines
6.6 KiB
Rust
196 lines
6.6 KiB
Rust
//! m7p3 social scale correctness tests.
|
||
//!
|
||
//! Validates the CoEngagementIndex eviction invariant at 2× capacity and
|
||
//! the social graph memory bound at production scale.
|
||
|
||
#![allow(clippy::unwrap_used, clippy::cast_precision_loss)]
|
||
|
||
use tidaldb::entities::CoEngagementIndex;
|
||
use tidaldb::schema::EntityId;
|
||
|
||
// ── CoEngagementIndex eviction correctness ────────────────────────────────────
|
||
|
||
/// Insert 2× capacity edges; verify edge_count <= capacity invariant always holds.
|
||
///
|
||
/// This is the core memory-bound invariant: no matter how many edges are added,
|
||
/// the index never grows beyond `capacity`.
|
||
#[test]
|
||
fn eviction_correctness_at_2x_capacity() {
|
||
let capacity = 500;
|
||
let index = CoEngagementIndex::with_capacity(capacity);
|
||
|
||
// Insert 2×capacity items for a single user — each step may trigger eviction.
|
||
for i in 1u64..=(2 * capacity as u64) {
|
||
index.record_positive(1, EntityId::new(i));
|
||
assert!(
|
||
index.edge_count() <= capacity,
|
||
"edge_count={} exceeded capacity={capacity} at step i={i}",
|
||
index.edge_count()
|
||
);
|
||
}
|
||
|
||
assert!(
|
||
index.edge_count() <= capacity,
|
||
"final edge_count={} must be <= capacity={capacity}",
|
||
index.edge_count()
|
||
);
|
||
}
|
||
|
||
/// High-weight edges must survive when low-weight edges fill to 2× capacity.
|
||
///
|
||
/// Inserts one very high-weight edge (weight=100.0) then floods with low-weight
|
||
/// edges until we exceed 2× capacity. The high-weight edge must still be present.
|
||
#[test]
|
||
fn high_weight_edge_survival_under_eviction() {
|
||
let capacity = 100;
|
||
let index = CoEngagementIndex::with_capacity(capacity);
|
||
|
||
// Insert one very strong edge directly.
|
||
let anchor_item_a = 99_999u64;
|
||
let anchor_item_b = 88_888u64;
|
||
index.insert_edge(anchor_item_a, anchor_item_b, 100.0);
|
||
|
||
// Now flood with 2× capacity low-weight record_positive calls.
|
||
// Each record_positive adds up to USER_RECENT_CAPACITY new edges.
|
||
for i in 1u64..=(2 * capacity as u64) {
|
||
index.record_positive(1, EntityId::new(i));
|
||
}
|
||
|
||
// The capacity invariant must hold.
|
||
assert!(
|
||
index.edge_count() <= capacity,
|
||
"edge_count={} exceeds capacity={capacity}",
|
||
index.edge_count()
|
||
);
|
||
|
||
// The high-weight anchor edge must have survived.
|
||
let anchor_score = index.score(EntityId::new(anchor_item_a), EntityId::new(anchor_item_b));
|
||
assert!(
|
||
anchor_score > 50.0,
|
||
"anchor edge (weight=100) was evicted: score={anchor_score}"
|
||
);
|
||
}
|
||
|
||
/// Max observed edge_count must never exceed capacity across any sequence of insertions.
|
||
///
|
||
/// Uses multiple users and varied items to stress the eviction path at
|
||
/// 2× capacity.
|
||
#[test]
|
||
fn co_engagement_memory_bounded_at_2x_insertions() {
|
||
let capacity = 200;
|
||
let index = CoEngagementIndex::with_capacity(capacity);
|
||
|
||
// 10 users, each engaging with 40 items → many cross-user edges.
|
||
for user_id in 1u64..=10 {
|
||
for item_id in 1u64..=40 {
|
||
let item = (user_id - 1) * 40 + item_id;
|
||
index.record_positive(user_id, EntityId::new(item));
|
||
assert!(
|
||
index.edge_count() <= capacity,
|
||
"capacity invariant violated at user={user_id} item={item_id}: \
|
||
edge_count={} > capacity={capacity}",
|
||
index.edge_count()
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Eviction does not cause self-loops under adversarial access patterns.
|
||
///
|
||
/// Alternating engagement with two items should never create (a, a) or (b, b) edges.
|
||
#[test]
|
||
fn no_self_loops_after_eviction() {
|
||
let capacity = 5;
|
||
let index = CoEngagementIndex::with_capacity(capacity);
|
||
|
||
for i in 0..50u64 {
|
||
let item = if i % 2 == 0 { 1 } else { 2 };
|
||
index.record_positive(1, EntityId::new(item));
|
||
}
|
||
|
||
assert_eq!(
|
||
index.score(EntityId::new(1), EntityId::new(1)),
|
||
0.0,
|
||
"self-loop (1, 1) created"
|
||
);
|
||
assert_eq!(
|
||
index.score(EntityId::new(2), EntityId::new(2)),
|
||
0.0,
|
||
"self-loop (2, 2) created"
|
||
);
|
||
}
|
||
|
||
/// After eviction, the index correctly reports top candidates.
|
||
///
|
||
/// After eviction the remaining edges should be consistent and queryable.
|
||
#[test]
|
||
fn top_candidates_consistent_after_eviction() {
|
||
let capacity = 10;
|
||
let index = CoEngagementIndex::with_capacity(capacity);
|
||
|
||
// User engages with 30 items, which will trigger eviction.
|
||
for i in 1u64..=30 {
|
||
index.record_positive(1, EntityId::new(i));
|
||
}
|
||
|
||
assert!(
|
||
index.edge_count() <= capacity,
|
||
"edge_count={} > capacity={capacity}",
|
||
index.edge_count()
|
||
);
|
||
|
||
// top_candidates should return a consistent subset.
|
||
// For seed item 30 (last inserted), top candidates should be non-empty.
|
||
// We can't assert exact values after eviction, but the operation must succeed.
|
||
let candidates = index.top_candidates(EntityId::new(30), 5);
|
||
for (id, weight) in &candidates {
|
||
assert!(id.as_u64() > 0, "invalid candidate id=0");
|
||
assert!(*weight > 0.0, "zero-weight candidate");
|
||
}
|
||
}
|
||
|
||
// ── Property test: edge_count never exceeds capacity ──────────────────────────
|
||
|
||
mod proptests {
|
||
use proptest::prelude::*;
|
||
use tidaldb::entities::CoEngagementIndex;
|
||
use tidaldb::schema::EntityId;
|
||
|
||
proptest! {
|
||
/// The CoEngagementIndex memory bound invariant must hold for any sequence
|
||
/// of record_positive calls, regardless of capacity, user count, or item count.
|
||
#[test]
|
||
fn edge_count_never_exceeds_capacity(
|
||
capacity in 5usize..=200,
|
||
users in 1u64..=10,
|
||
items_per_user in 1u64..=30,
|
||
) {
|
||
let index = CoEngagementIndex::with_capacity(capacity);
|
||
|
||
for user_id in 1..=users {
|
||
for item_idx in 0..items_per_user {
|
||
let item_id = user_id * 1000 + item_idx;
|
||
index.record_positive(user_id, EntityId::new(item_id));
|
||
|
||
prop_assert!(
|
||
index.edge_count() <= capacity,
|
||
"edge_count={} exceeded capacity={} at user={} item_idx={}",
|
||
index.edge_count(),
|
||
capacity,
|
||
user_id,
|
||
item_idx,
|
||
);
|
||
}
|
||
}
|
||
|
||
// Final invariant
|
||
prop_assert!(
|
||
index.edge_count() <= capacity,
|
||
"final edge_count={} exceeds capacity={}",
|
||
index.edge_count(),
|
||
capacity,
|
||
);
|
||
}
|
||
}
|
||
}
|