tidaldb/tidal/tests/m9p3_purge.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

232 lines
7.1 KiB
Rust

//! M9p3 integration test — Retroactive Purge & Deterministic Rematerialization.
//!
//! Validates the full purge flow: multiple users contribute to a community,
//! one user purges their prior contributions, and the community aggregate
//! rematerializes to exactly the remaining contributors — deterministically,
//! idempotently, with the local profile untouched and the purge surviving a
//! close/reopen.
//!
//! ROADMAP §Milestone 9 Phase 3.
#![allow(clippy::unwrap_used)]
use std::time::Duration;
use tidaldb::{
CommunityId, ShareIntent, SharePolicy, TidalDb, UserId,
schema::{DecaySpec, EntityId, EntityKind, SchemaBuilder, Timestamp, Window},
};
fn schema() -> tidaldb::schema::Schema {
let mut b = SchemaBuilder::new();
let _ = b
.signal(
"like",
EntityKind::Item,
DecaySpec::Exponential {
half_life: Duration::from_secs(7 * 24 * 3600),
},
)
.windows(&[Window::AllTime])
.velocity(false)
.add();
b.build().unwrap()
}
fn policy() -> SharePolicy {
SharePolicy::community_share(1, [ShareIntent::Like])
}
/// Join `users` and have each contribute one `like` on `item` to `community`.
fn seed(db: &TidalDb, community: CommunityId, item: EntityId, users: &[u64]) {
let now = Timestamp::now();
for &u in users {
db.join_community(UserId(u), community, policy()).unwrap();
assert!(
db.signal_for_community("like", item, 1.0, now, UserId(u), community)
.unwrap()
);
}
}
#[test]
fn purge_removes_only_target_user_and_rematerializes() {
let db = TidalDb::builder()
.ephemeral()
.with_schema(schema())
.open()
.unwrap();
let (community, item) = (CommunityId(1), EntityId::new(100));
seed(&db, community, item, &[1, 2, 3]);
// Aggregate reflects all 3 contributors.
assert_eq!(
db.read_community_windowed_count(community, item, "like", Window::AllTime)
.unwrap(),
3
);
// User 2 purges their prior contributions (all epochs).
let receipt = db
.purge_prior_contributions(UserId(2), community, 0..=u32::MAX)
.unwrap();
assert_eq!(receipt.contributions_removed, 1);
assert!(receipt.watermark_ns > 0);
// Rematerialized aggregate = remaining 2 contributors.
assert_eq!(
db.read_community_windowed_count(community, item, "like", Window::AllTime)
.unwrap(),
2
);
// Purge watermark is exposed.
assert_eq!(
db.community_purge_watermark(community),
Some(receipt.watermark_ns)
);
}
#[test]
fn purge_is_idempotent() {
let db = TidalDb::builder()
.ephemeral()
.with_schema(schema())
.open()
.unwrap();
let (community, item) = (CommunityId(1), EntityId::new(1));
seed(&db, community, item, &[1, 2, 3]);
db.purge_prior_contributions(UserId(2), community, 0..=u32::MAX)
.unwrap();
let after_first = db
.read_community_windowed_count(community, item, "like", Window::AllTime)
.unwrap();
// Re-purge the same range: removes nothing further, aggregate unchanged.
let receipt2 = db
.purge_prior_contributions(UserId(2), community, 0..=u32::MAX)
.unwrap();
assert_eq!(receipt2.contributions_removed, 0);
assert_eq!(
db.read_community_windowed_count(community, item, "like", Window::AllTime)
.unwrap(),
after_first,
"repeated purge must yield an identical aggregate"
);
}
#[test]
fn purge_does_not_touch_local_profile() {
let db = TidalDb::builder()
.ephemeral()
.with_schema(schema())
.open()
.unwrap();
let (community, item) = (CommunityId(1), EntityId::new(7));
db.join_community(UserId(1), community, policy()).unwrap();
let now = Timestamp::now();
// User 1 contributes to the community AND has local likes on the same item.
db.signal_for_community("like", item, 1.0, now, UserId(1), community)
.unwrap();
db.signal("like", item, 1.0, now).unwrap();
db.signal("like", item, 1.0, now).unwrap();
let local_before = db
.read_windowed_count(item, "like", Window::AllTime)
.unwrap();
assert_eq!(local_before, 2);
// Purge the user's community contributions.
db.purge_prior_contributions(UserId(1), community, 0..=u32::MAX)
.unwrap();
// Community aggregate emptied, but the LOCAL profile is untouched.
assert_eq!(
db.read_community_windowed_count(community, item, "like", Window::AllTime)
.unwrap(),
0
);
assert_eq!(
db.read_windowed_count(item, "like", Window::AllTime)
.unwrap(),
2,
"purge must never touch the local profile"
);
}
#[test]
fn purge_respects_epoch_range_via_rejoin() {
let db = TidalDb::builder()
.ephemeral()
.with_schema(schema())
.open()
.unwrap();
let (community, item) = (CommunityId(2), EntityId::new(3));
let user = UserId(5);
let now = Timestamp::now();
// Epoch 0 contribution.
db.join_community(user, community, policy()).unwrap();
db.signal_for_community("like", item, 1.0, now, user, community)
.unwrap();
// Leave + rejoin -> epoch 1 contribution.
db.leave_community(user, community, tidaldb::LeaveMode::StopForward)
.unwrap();
db.rejoin_community(user, community, policy()).unwrap();
db.signal_for_community("like", item, 1.0, now, user, community)
.unwrap();
assert_eq!(
db.read_community_windowed_count(community, item, "like", Window::AllTime)
.unwrap(),
2
);
// Purge only epoch 0; the epoch-1 (post-rejoin) contribution survives.
let receipt = db
.purge_prior_contributions(user, community, 0..=0)
.unwrap();
assert_eq!(receipt.contributions_removed, 1);
assert_eq!(
db.read_community_windowed_count(community, item, "like", Window::AllTime)
.unwrap(),
1
);
}
#[test]
fn purge_survives_reopen() {
let home = tidaldb::TempTidalHome::new().unwrap();
let (community, item) = (CommunityId(1), EntityId::new(100));
{
let db = TidalDb::builder()
.with_data_dir(home.path())
.with_schema(schema())
.open()
.unwrap();
seed(&db, community, item, &[1, 2, 3]);
db.purge_prior_contributions(UserId(2), community, 0..=u32::MAX)
.unwrap();
assert_eq!(
db.read_community_windowed_count(community, item, "like", Window::AllTime)
.unwrap(),
2
);
db.shutdown().unwrap();
}
// Reopen: the community aggregate is restored from the Tag::Community
// checkpoint with the purge already applied.
let db = TidalDb::builder()
.with_data_dir(home.path())
.with_schema(schema())
.open()
.unwrap();
assert_eq!(
db.read_community_windowed_count(community, item, "like", Window::AllTime)
.unwrap(),
2,
"purged community aggregate must survive reopen"
);
}