//! M9p1 integration test — Signal Scope & Share Contract. //! //! Validates the public `signal_scoped` API end-to-end through a real WAL and //! the load-bearing M9 invariant: community/session/agent-scoped writes are //! durably logged but never perturb the *local* profile. //! //! ROADMAP §Milestone 9 Phase 1 acceptance: //! - Default behavior is local-only; sharing is explicit opt-in. //! - The local profile remains intact across scoped writes. //! - Share eligibility is decided by `SharePolicy` (per-intent). #![allow(clippy::unwrap_used)] use std::{collections::BTreeSet, time::Duration}; use tidaldb::{ CommunityId, ShareIntent, ShareMode, SharePolicy, SignalScope, TidalDb, schema::{DecaySpec, EntityId, EntityKind, SchemaBuilder, Timestamp, Window}, }; fn schema() -> tidaldb::schema::Schema { let mut b = SchemaBuilder::new(); let _ = b .signal( "view", EntityKind::Item, DecaySpec::Exponential { half_life: Duration::from_secs(7 * 24 * 3600), }, ) .windows(&[Window::AllTime]) .velocity(false) .add(); let _ = b .signal( "low_quality", EntityKind::Item, DecaySpec::Exponential { half_life: Duration::from_secs(24 * 3600), }, ) .windows(&[Window::AllTime]) .velocity(false) .add(); b.build().unwrap() } #[test] fn default_signal_is_local() { let db = TidalDb::builder() .ephemeral() .with_schema(schema()) .open() .unwrap(); let id = EntityId::new(1); db.signal("view", id, 1.0, Timestamp::now()).unwrap(); // A plain `signal` is local — it shows up in the local profile. assert!( db.read_decay_score(id, "view", 0) .unwrap() .is_some_and(|s| s > 0.0) ); } #[test] fn community_scope_does_not_touch_local_profile() { let db = TidalDb::builder() .ephemeral() .with_schema(schema()) .open() .unwrap(); let id = EntityId::new(2); // A community-scoped write is accepted and durably logged... db.signal_scoped( "view", id, 1.0, Timestamp::now(), SignalScope::Community(CommunityId(1)), 1, ) .unwrap(); // ...but the local profile is untouched (local-profile-intact invariant). assert!( db.read_decay_score(id, "view", 0).unwrap().is_none(), "community write must not create a local aggregate" ); assert_eq!( db.read_windowed_count(id, "view", Window::AllTime).unwrap(), 0 ); } #[test] fn local_and_community_writes_are_independent() { let db = TidalDb::builder() .ephemeral() .with_schema(schema()) .open() .unwrap(); let id = EntityId::new(3); let now = Timestamp::now(); // Interleave a local and a community write on the same entity. db.signal_scoped("view", id, 1.0, now, SignalScope::Local, 0) .unwrap(); db.signal_scoped( "view", id, 1.0, now, SignalScope::Community(CommunityId(9)), 1, ) .unwrap(); // Only the local write affects the local windowed count. assert_eq!( db.read_windowed_count(id, "view", Window::AllTime).unwrap(), 1, "exactly one (the local) write must count toward the local profile" ); } #[test] fn share_policy_decides_per_intent_eligibility() { // The SharePolicy is the per-intent gate that a community-sync layer (M9p2) // consults before writing a community-scoped signal. Verify its contract // directly: `low_quality` is allowed to ship, `view` is not. let policy = SharePolicy { version: 1, allowed_intents: BTreeSet::from([ShareIntent::LowQuality]), mode: ShareMode::CommunityShare, }; let scope = SignalScope::Community(CommunityId(1)); assert!(policy.is_signal_share_eligible(scope, "low_quality")); assert!(!policy.is_signal_share_eligible(scope, "view")); // Local scope is never eligible, even for an allowed intent. assert!(!policy.is_signal_share_eligible(SignalScope::Local, "low_quality")); } #[test] fn scoped_writes_survive_reopen_without_polluting_local() { // Durability: a community write is in the WAL and replays on reopen, but // still must not appear in the recovered local profile. let home = tidaldb::TempTidalHome::new().unwrap(); let id = EntityId::new(42); let now = Timestamp::now(); { let db = TidalDb::builder() .with_data_dir(home.path()) .with_schema(schema()) .open() .unwrap(); db.signal_scoped("view", id, 1.0, now, SignalScope::Local, 0) .unwrap(); db.signal_scoped( "view", id, 1.0, now, SignalScope::Community(CommunityId(1)), 1, ) .unwrap(); db.shutdown().unwrap(); } // Reopen and replay the WAL. let db = TidalDb::builder() .with_data_dir(home.path()) .with_schema(schema()) .open() .unwrap(); // Exactly the one local write is reflected in the recovered local profile. assert_eq!( db.read_windowed_count(id, "view", Window::AllTime).unwrap(), 1, "after replay, only the local write should affect the local profile" ); }