# Task 05: Cross-Region Session Integration Tests ## Delivers Integration test suite in `tidal/tests/m8p4_session.rs` verifying: agent roaming between regions, session visibility within 2 seconds, idempotent writes, and hard-negative monotonicity across regions. ## Complexity: M ## Dependencies - Tasks 01–04 complete ## Technical Design ```rust // tidal/tests/m8p4_session.rs use tidaldb::replication::{ InProcessTransportFactory, SessionReplicationBridge, IdempotencyStore, }; use tidaldb::session::{SessionId, SessionManager, SessionSeqNoTracker}; use tidaldb::entities::HardNegAction; use tidaldb::replication::crdt::HlcTimestamp; /// Helper: create a pair of TidalDb instances linked by InProcessTransport. async fn setup_two_region_cluster() -> (TidalDb, TidalDb, Arc) { let factory = Arc::new(InProcessTransportFactory::new()); let transport_a = factory.connect(RegionId(0)); let transport_b = factory.connect(RegionId(1)); let db_a = TidalDb::builder() .ephemeral() .with_schema(schema()) .with_cluster(NodeConfig { region_id: RegionId(0), shard_id: ShardId(0), role: NodeRole::Leader, }) .with_transport(transport_a) .open() .unwrap(); let db_b = TidalDb::builder() .ephemeral() .with_schema(schema()) .with_cluster(NodeConfig { region_id: RegionId(1), shard_id: ShardId(0), role: NodeRole::Follower, }) .with_transport(transport_b) .open() .unwrap(); (db_a, db_b, factory) } /// Agent roaming: session started in us-east, readable in eu-west. #[tokio::test] async fn test_session_cross_region_visibility() { let (db_a, db_b, _factory) = setup_two_region_cluster().await; let user = EntityId::new(1); let session_id = db_a.start_session(user, Default::default()).unwrap(); // Write 5 preference signals in region A. for i in 0..5u64 { let item = EntityId::new(100 + i); db_a.signal_in_session(session_id, "view", item, 1.0, Timestamp::now()).unwrap(); } // Allow replication to propagate (< 2 seconds using InProcessTransport). tokio::time::sleep(Duration::from_millis(200)).await; // Read session signals from region B. let session_b = db_b.get_session(session_id).unwrap(); assert!(session_b.is_some(), "session should be visible in region B"); let signals = db_b.session_signals(session_id).unwrap(); assert_eq!(signals.len(), 5, "all 5 preference signals should be visible in region B"); } /// Idempotent replication: duplicate session events produce no double-counting. #[tokio::test] async fn test_session_replication_idempotent() { let (db_a, db_b, factory) = setup_two_region_cluster().await; let user = EntityId::new(2); let session_id = db_a.start_session(user, Default::default()).unwrap(); let item = EntityId::new(200); db_a.signal_in_session(session_id, "like", item, 1.0, Timestamp::now()).unwrap(); // Let it replicate once. tokio::time::sleep(Duration::from_millis(100)).await; // Force re-send (simulated duplicate). factory.replay_last_session_batch(RegionId(1)).await; tokio::time::sleep(Duration::from_millis(50)).await; // Signal count on B should still be 1, not 2. let count = db_b.read_windowed_count(item, "like", Window::OneHour).unwrap(); // (Session signals are user-scoped; verify via session data, not global ledger) let session_data = db_b.session_signals(session_id).unwrap(); assert_eq!(session_data.len(), 1, "no double-counting from duplicate replication"); } /// Hard negative monotonicity: hide in region A, unhide (lower HLC) in region B. /// After replication: item is suppressed in BOTH regions. #[tokio::test] async fn test_hardneg_monotonicity_hide_wins() { let (db_a, db_b, _factory) = setup_two_region_cluster().await; let user = EntityId::new(3); let item = EntityId::new(300); // Region A hides item at t=100 (higher HLC). let ts_hide = HlcTimestamp { wall_ns: 100, logical: 0, node_id: 0 }; db_a.hide_item_with_ts(user, item, ts_hide).unwrap(); // Region B has an earlier unhide at t=50 (already in state before partition). let ts_unhide = HlcTimestamp { wall_ns: 50, logical: 0, node_id: 1 }; db_b.unhide_item_with_ts(user, item, ts_unhide).unwrap(); // Allow replication to propagate. tokio::time::sleep(Duration::from_millis(200)).await; // After replication: both regions should suppress the item. let results_b = db_b.retrieve(&Retrieve::builder() .for_user(user) .candidates(vec![item]) .build() .unwrap() ).unwrap(); assert!( results_b.items.is_empty(), "hidden item must not appear in region B results after replication" ); } /// Hard negative: explicit unhide with HIGHER HLC does clear the hide. #[tokio::test] async fn test_hardneg_explicit_unhide_with_higher_hlc() { let (db_a, db_b, _factory) = setup_two_region_cluster().await; let user = EntityId::new(4); let item = EntityId::new(400); // Both regions: hide at t=50. let ts_hide = HlcTimestamp { wall_ns: 50, logical: 0, node_id: 0 }; db_a.hide_item_with_ts(user, item, ts_hide).unwrap(); tokio::time::sleep(Duration::from_millis(100)).await; // User explicitly un-hides at t=200 (higher than hide). let ts_unhide = HlcTimestamp { wall_ns: 200, logical: 0, node_id: 1 }; db_b.unhide_item_with_ts(user, item, ts_unhide).unwrap(); tokio::time::sleep(Duration::from_millis(200)).await; // After full replication + LWW resolution: item should appear (unhide wins). let results_a = db_a.retrieve(&Retrieve::builder() .for_user(user) .candidates(vec![item]) .build() .unwrap() ).unwrap(); assert_eq!(results_a.items.len(), 1, "unhide with higher HLC should make item visible again"); } /// Seqno HWM: writes with seqno <= HWM are idempotent no-ops on receiver. #[tokio::test] async fn test_session_seqno_hwm_rejects_duplicates() { let tracker = SessionSeqNoTracker::new(); let session = SessionId::new(); // Sequence 1..5 -- all accepted. for i in 1..=5u64 { assert!(tracker.should_apply(session, SessionSeqNo(i))); } // Re-send seqno 3 -- rejected. assert!(!tracker.should_apply(session, SessionSeqNo(3))); // Seqno 6 -- accepted. assert!(tracker.should_apply(session, SessionSeqNo(6))); // HWM should be 6 now. assert_eq!(tracker.hwm(session), SessionSeqNo(6)); } ``` ## Acceptance Criteria - [ ] `test_session_cross_region_visibility`: 5 session signals written in region A visible in region B within 200ms (in-process transport) - [ ] `test_session_replication_idempotent`: duplicate session batch replay produces no double-counting - [ ] `test_hardneg_monotonicity_hide_wins`: hide at higher HLC suppresses item in both regions after cross-region replication - [ ] `test_hardneg_explicit_unhide_with_higher_hlc`: unhide at strictly higher HLC restores visibility after replication - [ ] `test_session_seqno_hwm_rejects_duplicates`: HWM tracker unit test with 5 monotonic accepts + 1 duplicate reject + 1 resume accept - [ ] All 5 tests pass in `cargo test --test m8p4_session` - [ ] `cargo clippy -D warnings` and `cargo fmt` pass