tidaldb/tidal/tests/m8p4_session.rs
jx12n 3bcfb3c576 feat: Bazel build, crate docs/ai-lookup, docker images, and engine hardening
- Add BUILD.bazel across tidal, tidal-net, tidal-server, tidalctl for bzlmod build
- Add tidal/ crate docs (README, CHANGELOG, CONTRIBUTING, AGENTS, CLAUDE, API, ARCHITECTURE) and ai-lookup reference
- Add docker standalone/cluster/deploy images, compose, and prometheus config
- Harden WAL (batch format, writer, dedup, diagnostics), text syncer/collectors, and vector registry
- Expand tidalctl CLI and tests; restructure WAL/visibility integration test suites
- Refine tidal-net transport/client/server and tidal-server cluster/scatter-gather
2026-06-07 18:29:38 -06:00

183 lines
6.2 KiB
Rust

//! m8p4 integration tests: session continuity and agent memory across regions.
//!
//! Tests use components directly (no `TidalDb` instances needed) for speed
//! and determinism. No async, no sleep, no background threads.
#![allow(clippy::unwrap_used)]
use std::{sync::Arc, time::Instant};
use tidaldb::{
replication::{
IdempotencyKey, IdempotencyStore, InProcessSessionTransportFactory,
SessionReplicationBridge, ShardId,
},
session::SessionSeqNoTracker,
wal::format::session::{SessionSeqNo, SessionWalEvent},
};
// -- helpers ------------------------------------------------------------------
fn signal_event(session_id: u64, entity_id: u64, seqno: u64) -> SessionWalEvent {
SessionWalEvent::Signal {
session_id,
entity_id,
weight: 1.0,
ts_ns: 1_000_000_000,
signal_name: "view".to_string(),
annotation: None,
session_seqno: Some(SessionSeqNo(seqno)),
idempotency_key: Some(u128::from(seqno) * 1_000_000 + u128::from(session_id)),
}
}
fn make_two_bridges() -> (SessionReplicationBridge, SessionReplicationBridge) {
let shards = [ShardId(0), ShardId(1)];
let mut transports = InProcessSessionTransportFactory::new(&shards).build();
let t0 = transports.remove(&ShardId(0)).unwrap();
let t1 = transports.remove(&ShardId(1)).unwrap();
let b0 = SessionReplicationBridge::new(
t0,
Arc::new(IdempotencyStore::new(1000)),
Arc::new(SessionSeqNoTracker::new()),
);
let b1 = SessionReplicationBridge::new(
t1,
Arc::new(IdempotencyStore::new(1000)),
Arc::new(SessionSeqNoTracker::new()),
);
(b0, b1)
}
// -- Test 1: SeqNo HWM rejects duplicates -------------------------------------
#[test]
fn test_session_seqno_hwm_rejects_duplicates() {
let tracker = SessionSeqNoTracker::new();
let session = 1u64;
// Sequence 1..5 -- all accepted.
for i in 1..=5u64 {
assert!(
tracker.should_apply(session, SessionSeqNo(i)),
"seqno {i} should be accepted"
);
}
// Re-send seqno 3 -- rejected.
assert!(
!tracker.should_apply(session, SessionSeqNo(3)),
"seqno 3 is a duplicate, should be rejected"
);
// Seqno 6 -- accepted (monotone resume).
assert!(tracker.should_apply(session, SessionSeqNo(6)));
// HWM should be 6.
assert_eq!(tracker.hwm(session), SessionSeqNo(6));
}
// -- Test 2: 5 session signals replicated region A -> B -----------------------
#[test]
fn test_session_cross_region_visibility() {
let (b0, b1) = make_two_bridges();
let session_id = 10u64;
let events: Vec<SessionWalEvent> = (1..=5)
.map(|i| signal_event(session_id, 100 + i, i))
.collect();
// Ship from region A (shard 0) to region B (shard 1).
let start = Instant::now();
let shipped = b0.ship(ShardId(1), session_id, &events).unwrap();
assert_eq!(shipped, 5, "all 5 events should be shipped");
// Receive and apply on region B.
let mut received = Vec::new();
let applied = b1.recv_and_apply(|e| received.push(e.clone())).unwrap();
let elapsed = start.elapsed();
assert_eq!(applied, 5, "all 5 events should be applied on follower");
assert_eq!(received.len(), 5);
// AC: session visible in region B within 2 seconds (in-process transport).
assert!(
elapsed.as_secs() < 2,
"cross-region visibility must be < 2s, was {elapsed:?}"
);
}
// -- Test 3: Idempotent replication (no double-counting) ----------------------
#[test]
fn test_session_replication_idempotent() {
let (b0, b1) = make_two_bridges();
let session_id = 20u64;
let events = vec![signal_event(session_id, 200, 1)];
// First ship -- gets through.
b0.ship(ShardId(1), session_id, &events).unwrap();
let applied_first = b1.recv_and_apply(|_| {}).unwrap();
assert_eq!(applied_first, 1);
// Second ship of the same events -- ship HWM blocks re-shipping.
let reshipped = b0.ship(ShardId(1), session_id, &events).unwrap();
assert_eq!(reshipped, 0, "ship HWM should prevent re-shipping");
}
// -- Test 4: Sequential events only ship the new ones -------------------------
#[test]
fn test_incremental_shipping() {
let (b0, b1) = make_two_bridges();
let session_id = 30u64;
// Ship batch 1 (seqno 1-3).
let batch1: Vec<_> = (1..=3)
.map(|i| signal_event(session_id, 300 + i, i))
.collect();
let shipped1 = b0.ship(ShardId(1), session_id, &batch1).unwrap();
assert_eq!(shipped1, 3);
b1.recv_and_apply(|_| {}).unwrap();
// Ship batch 2 (seqno 1-5, but 1-3 already sent -> only 4-5 shipped).
let batch2: Vec<_> = (1..=5)
.map(|i| signal_event(session_id, 300 + i, i))
.collect();
let shipped2 = b0.ship(ShardId(1), session_id, &batch2).unwrap();
assert_eq!(shipped2, 2, "only new events (seqno 4-5) should be shipped");
let mut count = 0;
let applied = b1.recv_and_apply(|_| count += 1).unwrap();
assert_eq!(applied, 2);
assert_eq!(count, 2);
}
// NOTE: the former Tests 5-7 exercised HardNegIndex::apply_replication_hide/unhide,
// the per-index HLC-ordered hide/unhide API removed during the M0-M10 remediation
// (it was dead code -- never wired to any production path). HLC-monotonic
// hard-negative convergence across shards is resolved in a single place: the
// LWWRegister<HardNegAction> in replication/reconcile.rs, which drives HardNegIndex
// through add()/remove() (Hide -> add, Unhide -> remove). The three convergence
// scenarios the removed tests covered (lower-HLC unhide loses, higher-HLC unhide
// wins, multi-shard hide union) are re-expressed against that live reconcile path
// in src/replication/reconcile_tests.rs.
// -- Test 8: Idempotency store catches within-window duplicates ---------------
#[test]
fn test_idempotency_store_deduplication() {
let store = IdempotencyStore::new(100);
let key = IdempotencyKey::derive(1, SessionSeqNo(1), b"op");
assert!(store.check_and_record(key), "first time: accepted");
assert!(!store.check_and_record(key), "second time: duplicate");
assert!(!store.check_and_record(key), "third time: still duplicate");
// Different key is accepted.
let key2 = IdempotencyKey::derive(1, SessionSeqNo(2), b"op");
assert!(store.check_and_record(key2));
}