- 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
186 lines
5.4 KiB
Rust
186 lines
5.4 KiB
Rust
//! 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"
|
|
);
|
|
}
|