- 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
185 lines
5.5 KiB
Rust
185 lines
5.5 KiB
Rust
//! M10p2 integration test — Agent Capability & Scope Controls.
|
|
//!
|
|
//! Validates the ROADMAP M10 UAT agent-rights flow: a trusted agent with a
|
|
//! community-write capability can contribute, an experimental agent without it
|
|
//! is rejected, revocation takes effect immediately, and capabilities +
|
|
//! revocation history survive close/reopen.
|
|
//!
|
|
//! ROADMAP §Milestone 10 Phase 2.
|
|
|
|
#![allow(clippy::unwrap_used)]
|
|
|
|
use std::time::Duration;
|
|
|
|
use tidaldb::{
|
|
CommunityId, 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])
|
|
}
|
|
|
|
#[test]
|
|
fn trusted_agent_writes_experimental_is_rejected() {
|
|
let db = TidalDb::builder()
|
|
.ephemeral()
|
|
.with_schema(schema())
|
|
.open()
|
|
.unwrap();
|
|
let (community, item, user) = (CommunityId(1), EntityId::new(5), UserId(1));
|
|
db.join_community(user, community, share()).unwrap();
|
|
|
|
// A_trusted gets community write; A_experimental gets only community read.
|
|
db.grant_capability(
|
|
"A_trusted",
|
|
vec![ScopePermission::read_write(ScopeClass::Community)],
|
|
None,
|
|
)
|
|
.unwrap();
|
|
db.grant_capability(
|
|
"A_experimental",
|
|
vec![ScopePermission::read_only(ScopeClass::Community)],
|
|
None,
|
|
)
|
|
.unwrap();
|
|
|
|
let now = Timestamp::now();
|
|
// Trusted agent: allowed -> contribution recorded.
|
|
assert!(
|
|
db.agent_signal_for_community("A_trusted", "like", item, 1.0, now, user, community)
|
|
.unwrap()
|
|
);
|
|
// Experimental agent: denied by capability policy -> error, nothing written.
|
|
assert!(
|
|
db.agent_signal_for_community("A_experimental", "like", item, 1.0, now, user, community)
|
|
.is_err()
|
|
);
|
|
// Unknown agent: also denied.
|
|
assert!(
|
|
db.agent_signal_for_community("A_unknown", "like", item, 1.0, now, user, community)
|
|
.is_err()
|
|
);
|
|
|
|
// Exactly one contribution (from A_trusted) landed.
|
|
assert_eq!(
|
|
db.read_community_windowed_count(community, item, "like", Window::AllTime)
|
|
.unwrap(),
|
|
1
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn revocation_takes_effect_immediately() {
|
|
let db = TidalDb::builder()
|
|
.ephemeral()
|
|
.with_schema(schema())
|
|
.open()
|
|
.unwrap();
|
|
let (community, item, user) = (CommunityId(1), EntityId::new(5), UserId(1));
|
|
db.join_community(user, community, share()).unwrap();
|
|
let token = db
|
|
.grant_capability(
|
|
"A",
|
|
vec![ScopePermission::read_write(ScopeClass::Community)],
|
|
None,
|
|
)
|
|
.unwrap();
|
|
|
|
let now = Timestamp::now();
|
|
assert!(db.check_capability("A", ScopeClass::Community, true));
|
|
assert!(
|
|
db.agent_signal_for_community("A", "like", item, 1.0, now, user, community)
|
|
.unwrap()
|
|
);
|
|
|
|
// Revoke -> the very next check/write is denied (atomic, no sweeper).
|
|
assert!(db.revoke_capability(&token).unwrap());
|
|
assert!(!db.check_capability("A", ScopeClass::Community, true));
|
|
assert!(
|
|
db.agent_signal_for_community("A", "like", item, 1.0, now, user, community)
|
|
.is_err()
|
|
);
|
|
|
|
// Revoking an unknown token returns false.
|
|
assert!(!db.revoke_capability("does-not-exist").unwrap());
|
|
}
|
|
|
|
#[test]
|
|
fn ttl_expiry_denies() {
|
|
let db = TidalDb::builder()
|
|
.ephemeral()
|
|
.with_schema(schema())
|
|
.open()
|
|
.unwrap();
|
|
// Grant with a zero TTL -> already expired.
|
|
db.grant_capability(
|
|
"A",
|
|
vec![ScopePermission::read_write(ScopeClass::Community)],
|
|
Some(Duration::from_nanos(0)),
|
|
)
|
|
.unwrap();
|
|
assert!(!db.check_capability("A", ScopeClass::Community, true));
|
|
}
|
|
|
|
#[test]
|
|
fn capabilities_and_revocations_survive_reopen() {
|
|
let home = tidaldb::TempTidalHome::new().unwrap();
|
|
let token_id;
|
|
{
|
|
let db = TidalDb::builder()
|
|
.with_data_dir(home.path())
|
|
.with_schema(schema())
|
|
.open()
|
|
.unwrap();
|
|
db.grant_capability(
|
|
"keep",
|
|
vec![ScopePermission::read_write(ScopeClass::Community)],
|
|
None,
|
|
)
|
|
.unwrap();
|
|
token_id = db
|
|
.grant_capability(
|
|
"revoked_agent",
|
|
vec![ScopePermission::read_write(ScopeClass::Community)],
|
|
None,
|
|
)
|
|
.unwrap();
|
|
db.revoke_capability(&token_id).unwrap();
|
|
db.shutdown().unwrap();
|
|
}
|
|
|
|
let db = TidalDb::builder()
|
|
.with_data_dir(home.path())
|
|
.with_schema(schema())
|
|
.open()
|
|
.unwrap();
|
|
// The live capability is restored and usable.
|
|
assert!(db.check_capability("keep", ScopeClass::Community, true));
|
|
// The revoked one stays revoked across restart.
|
|
assert!(!db.check_capability("revoked_agent", ScopeClass::Community, true));
|
|
// Revocation history is inspectable.
|
|
let history = db.revocation_history();
|
|
assert!(
|
|
history
|
|
.iter()
|
|
.any(|(tid, agent, _)| tid == &token_id && agent == "revoked_agent")
|
|
);
|
|
}
|