tidaldb/tidal/tests/m7p3_social_scale.rs
2026-02-23 22:41:16 -07:00

196 lines
6.6 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! 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,
);
}
}
}