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