- 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
265 lines
8.2 KiB
Rust
265 lines
8.2 KiB
Rust
//! M10p3 integration test — Provenance, Explainability, Remove-by-Scope.
|
|
//!
|
|
//! Validates that every community contribution carries provenance (user, writer
|
|
//! agent, epoch), that an aggregate can be explained as the contributions that
|
|
//! compose it, and that contributions can be removed precisely by user or by
|
|
//! agent — non-globally, deterministically, with the local profile intact and
|
|
//! the removal surviving reopen. Exercises the M10 UAT: revoke `A_trusted`, then
|
|
//! remove `A_trusted`'s prior contributions.
|
|
//!
|
|
//! ROADMAP §Milestone 10 Phase 3.
|
|
|
|
#![allow(clippy::unwrap_used)]
|
|
|
|
use std::time::Duration;
|
|
|
|
use tidaldb::{
|
|
CommunityId, RemoveScope, ScopeClass, ScopePermission, 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 share() -> SharePolicy {
|
|
SharePolicy::community_share(1, [ShareIntent::Like])
|
|
}
|
|
|
|
fn grant_writer(db: &TidalDb, agent: &str) {
|
|
db.grant_capability(
|
|
agent,
|
|
vec![ScopePermission::read_write(ScopeClass::Community)],
|
|
None,
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn contributions_carry_provenance_and_explain() {
|
|
let db = TidalDb::builder()
|
|
.ephemeral()
|
|
.with_schema(schema())
|
|
.open()
|
|
.unwrap();
|
|
let (c, item) = (CommunityId(1), EntityId::new(5));
|
|
db.join_community(UserId(1), c, share()).unwrap();
|
|
grant_writer(&db, "A_trusted");
|
|
let now = Timestamp::now();
|
|
|
|
// A direct user write + an agent-attributed write on the same item.
|
|
db.signal_for_community("like", item, 1.0, now, UserId(1), c)
|
|
.unwrap();
|
|
db.agent_signal_for_community("A_trusted", "like", item, 1.0, now, UserId(1), c)
|
|
.unwrap();
|
|
|
|
let provenance = db.explain_community_contributions(c, item, "like").unwrap();
|
|
assert_eq!(
|
|
provenance.len(),
|
|
2,
|
|
"two distinct contributors (direct + agent)"
|
|
);
|
|
// Direct write has empty writer; agent write is attributed to A_trusted.
|
|
assert!(
|
|
provenance
|
|
.iter()
|
|
.any(|p| p.user == UserId(1) && p.writer_agent.is_empty())
|
|
);
|
|
assert!(
|
|
provenance
|
|
.iter()
|
|
.any(|p| p.user == UserId(1) && p.writer_agent == "A_trusted")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn remove_by_agent_is_precise() {
|
|
let db = TidalDb::builder()
|
|
.ephemeral()
|
|
.with_schema(schema())
|
|
.open()
|
|
.unwrap();
|
|
let (c, item, user) = (CommunityId(1), EntityId::new(5), UserId(1));
|
|
db.join_community(user, c, share()).unwrap();
|
|
grant_writer(&db, "A_trusted");
|
|
grant_writer(&db, "A_other");
|
|
let now = Timestamp::now();
|
|
|
|
db.agent_signal_for_community("A_trusted", "like", item, 1.0, now, user, c)
|
|
.unwrap();
|
|
db.agent_signal_for_community("A_other", "like", item, 1.0, now, user, c)
|
|
.unwrap();
|
|
db.signal_for_community("like", item, 1.0, now, user, c)
|
|
.unwrap(); // direct
|
|
assert_eq!(
|
|
db.read_community_windowed_count(c, item, "like", Window::AllTime)
|
|
.unwrap(),
|
|
3
|
|
);
|
|
|
|
// Remove ONLY A_trusted's contributions.
|
|
let receipt = db
|
|
.remove_from_personalization(RemoveScope::CommunityAgent {
|
|
writer_agent: "A_trusted".to_string(),
|
|
community: c,
|
|
})
|
|
.unwrap();
|
|
assert_eq!(receipt.contributions_removed, 1);
|
|
|
|
// A_other's + the direct contribution remain (precise, non-global).
|
|
assert_eq!(
|
|
db.read_community_windowed_count(c, item, "like", Window::AllTime)
|
|
.unwrap(),
|
|
2
|
|
);
|
|
let remaining = db.explain_community_contributions(c, item, "like").unwrap();
|
|
assert!(!remaining.iter().any(|p| p.writer_agent == "A_trusted"));
|
|
assert!(remaining.iter().any(|p| p.writer_agent == "A_other"));
|
|
}
|
|
|
|
#[test]
|
|
fn m10_uat_revoke_and_remove_agent() {
|
|
// The ROADMAP M10 UAT step: U revokes A_trusted community scope and removes
|
|
// A_trusted's prior contributions from C; the local profile is untouched.
|
|
let db = TidalDb::builder()
|
|
.ephemeral()
|
|
.with_schema(schema())
|
|
.open()
|
|
.unwrap();
|
|
let (c, item, user) = (CommunityId(1), EntityId::new(5), UserId(1));
|
|
db.join_community(user, c, share()).unwrap();
|
|
let token = db
|
|
.grant_capability(
|
|
"A_trusted",
|
|
vec![ScopePermission::read_write(ScopeClass::Community)],
|
|
None,
|
|
)
|
|
.unwrap();
|
|
let now = Timestamp::now();
|
|
|
|
db.agent_signal_for_community("A_trusted", "like", item, 1.0, now, user, c)
|
|
.unwrap();
|
|
db.signal("like", item, 1.0, now).unwrap(); // U's own local like
|
|
assert_eq!(
|
|
db.read_community_windowed_count(c, item, "like", Window::AllTime)
|
|
.unwrap(),
|
|
1
|
|
);
|
|
let local_before = db
|
|
.read_windowed_count(item, "like", Window::AllTime)
|
|
.unwrap();
|
|
|
|
// 1. Revoke A_trusted's community scope -> future writes denied immediately.
|
|
db.revoke_capability(&token).unwrap();
|
|
assert!(
|
|
db.agent_signal_for_community("A_trusted", "like", item, 1.0, now, user, c)
|
|
.is_err()
|
|
);
|
|
|
|
// 2. Remove A_trusted's prior contributions from C.
|
|
db.remove_from_personalization(RemoveScope::CommunityAgent {
|
|
writer_agent: "A_trusted".to_string(),
|
|
community: c,
|
|
})
|
|
.unwrap();
|
|
assert_eq!(
|
|
db.read_community_windowed_count(c, item, "like", Window::AllTime)
|
|
.unwrap(),
|
|
0
|
|
);
|
|
|
|
// 3. U's local profile is completely intact.
|
|
assert_eq!(
|
|
db.read_windowed_count(item, "like", Window::AllTime)
|
|
.unwrap(),
|
|
local_before
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn remove_by_user_scope_delegates_to_purge() {
|
|
let db = TidalDb::builder()
|
|
.ephemeral()
|
|
.with_schema(schema())
|
|
.open()
|
|
.unwrap();
|
|
let (c, item) = (CommunityId(1), EntityId::new(5));
|
|
let now = Timestamp::now();
|
|
for u in 1..=3u64 {
|
|
db.join_community(UserId(u), c, share()).unwrap();
|
|
db.signal_for_community("like", item, 1.0, now, UserId(u), c)
|
|
.unwrap();
|
|
}
|
|
let receipt = db
|
|
.remove_from_personalization(RemoveScope::CommunityUser {
|
|
user: UserId(2),
|
|
community: c,
|
|
epoch_range: 0..=u32::MAX,
|
|
})
|
|
.unwrap();
|
|
assert_eq!(receipt.contributions_removed, 1);
|
|
assert_eq!(
|
|
db.read_community_windowed_count(c, item, "like", Window::AllTime)
|
|
.unwrap(),
|
|
2
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn remove_survives_reopen() {
|
|
let home = tidaldb::TempTidalHome::new().unwrap();
|
|
let (c, item, user) = (CommunityId(1), EntityId::new(5), UserId(1));
|
|
{
|
|
let db = TidalDb::builder()
|
|
.with_data_dir(home.path())
|
|
.with_schema(schema())
|
|
.open()
|
|
.unwrap();
|
|
db.join_community(user, c, share()).unwrap();
|
|
grant_writer(&db, "A_trusted");
|
|
let now = Timestamp::now();
|
|
db.agent_signal_for_community("A_trusted", "like", item, 1.0, now, user, c)
|
|
.unwrap();
|
|
db.signal_for_community("like", item, 1.0, now, user, c)
|
|
.unwrap();
|
|
db.remove_from_personalization(RemoveScope::CommunityAgent {
|
|
writer_agent: "A_trusted".to_string(),
|
|
community: c,
|
|
})
|
|
.unwrap();
|
|
assert_eq!(
|
|
db.read_community_windowed_count(c, item, "like", Window::AllTime)
|
|
.unwrap(),
|
|
1
|
|
);
|
|
db.shutdown().unwrap();
|
|
}
|
|
// Reopen: the remove-by-agent outcome is preserved (the agent's contribution
|
|
// stays removed, the direct one stays).
|
|
let db = TidalDb::builder()
|
|
.with_data_dir(home.path())
|
|
.with_schema(schema())
|
|
.open()
|
|
.unwrap();
|
|
assert_eq!(
|
|
db.read_community_windowed_count(c, item, "like", Window::AllTime)
|
|
.unwrap(),
|
|
1
|
|
);
|
|
let prov = db.explain_community_contributions(c, item, "like").unwrap();
|
|
assert!(!prov.iter().any(|p| p.writer_agent == "A_trusted"));
|
|
}
|