#![allow(clippy::pedantic, clippy::nursery, missing_docs)]
use std::sync::Arc;
use buffa::Message as _;
use polyc_crypto::approval::ApprovalSigner;
use polyc_crypto::session::{RevokedTokens, SessionScope, SessionSubject, mint_session};
use polyc_crypto::signing_role::{
HandoffRole, RoleTrustSet, SessionSigner, TurnReadRole, TurnReadSigner,
};
use polyc_eventlog::Event;
use polyc_eventlog_host::{EventLogHost, RewriteDecision};
use polyc_persona::PersonaHost;
use polyc_proto::kinds;
use polyc_proto::proto::polychrome::events::v1::RoutineFiredEvent;
use polyc_proto::proto::polychrome::persona::v1::ExternalIdentity;
use tokio_util::sync::CancellationToken;
use crate::routine_catalog::{RoutineCatalog, RoutineCatalogError, RoutineStatusRecord};
use super::*;
const NOW: u64 = 1_700_000_000_000;
const TEST_TTL_MS: u64 = 8 * 60 * 60 * 1000;
fn test_handoff_trust() -> RoleTrustSet<HandoffRole> {
crate::engine::fixture_handoff_trust()
}
fn test_signer() -> ApprovalSigner {
ApprovalSigner::from_seed(1)
}
fn routine_record(name: &str, creator_persona: &str) -> RoutineStatusRecord {
RoutineStatusRecord {
name: name.to_owned(),
uid: format!("{name}-uid"),
fire_conversation_id: format!("{name}-fire-conv"),
ready: true,
phase: Some("Ready".to_owned()),
message: None,
last_fire_time_ms: None,
next_fire_time_ms: None,
conditions_json: "[]".to_owned(),
creator_persona: creator_persona.to_owned(),
provenance_conversation_id: "conv-1".to_owned(),
schedule_json: r#"{"kind":"cron","expression":"0 9 * * *","timezone":null}"#.to_owned(),
next_fires_json: "[]".to_owned(),
suspended: false,
paused_by: None,
paused_at_ms: None,
pause_reason: None,
prompt: "post the morning standup".to_owned(),
scope: "private".to_owned(),
orphaned: false,
display_name: String::new(),
description: String::new(),
schedule_timezone: "UTC".to_owned(),
}
}
fn test_dashboard_cell(eventlog: &Arc<EventLogHost>) -> crate::dashboard::DashboardCell {
crate::dashboard::DashboardProjection::new(
Vec::new(),
eventlog.clone(),
polyc_payments::amount::DEFAULT_DECIMALS,
)
}
struct Fixture {
authority: QueryAuthority,
eventlog: Arc<EventLogHost>,
eventlog_shutdown: CancellationToken,
eventlog_dir: std::path::PathBuf,
persona: Arc<PersonaHost>,
persona_shutdown: CancellationToken,
persona_dir: std::path::PathBuf,
signer: ApprovalSigner,
revoked: Arc<RevokedTokens>,
}
impl Fixture {
async fn build(test_name: &str) -> Self {
let signer = test_signer();
let eventlog_dir = std::env::temp_dir().join(format!(
"polyc-query-authority-{test_name}-eventlog-{}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&eventlog_dir);
let eventlog_shutdown = CancellationToken::new();
let eventlog = Arc::new(
EventLogHost::spawn(
eventlog_dir.clone(),
eventlog_shutdown.clone(),
signer.relabel_for_test(),
)
.expect("spawn eventlog host"),
);
let persona_dir = std::env::temp_dir().join(format!(
"polyc-query-authority-{test_name}-persona-{}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&persona_dir);
let persona_shutdown = CancellationToken::new();
let persona = Arc::new(
PersonaHost::spawn(persona_dir.clone(), persona_shutdown.clone())
.expect("spawn persona host"),
);
let revoked = Arc::new(RevokedTokens::new());
let authority = QueryAuthority::new(
eventlog.clone(),
Arc::new(arc_swap::ArcSwapOption::new(Some(persona.clone()))),
test_dashboard_cell(&eventlog),
revoked.clone(),
signer.public_key_bytes(),
vec![signer.public_key_bytes()],
QueryLimits::default(),
None,
CacheConfig::disabled(),
test_handoff_trust(),
);
Self {
authority,
eventlog,
eventlog_shutdown,
eventlog_dir,
persona,
persona_shutdown,
persona_dir,
signer,
revoked,
}
}
fn authority_with_empty_persona_cell(&self) -> QueryAuthority {
QueryAuthority::new(
self.eventlog.clone(),
Arc::new(arc_swap::ArcSwapOption::empty()),
test_dashboard_cell(&self.eventlog),
self.revoked.clone(),
self.signer.public_key_bytes(),
vec![self.signer.public_key_bytes()],
QueryLimits::default(),
None,
CacheConfig::disabled(),
test_handoff_trust(),
)
}
fn authority_with(
&self,
catalog: Option<Arc<dyn RoutineCatalog>>,
limits: Option<QueryLimits>,
cache_config: CacheConfig,
) -> QueryAuthority {
QueryAuthority::new(
self.eventlog.clone(),
Arc::new(arc_swap::ArcSwapOption::new(Some(self.persona.clone()))),
test_dashboard_cell(&self.eventlog),
self.revoked.clone(),
self.signer.public_key_bytes(),
vec![self.signer.public_key_bytes()],
limits.unwrap_or_default(),
catalog,
cache_config,
test_handoff_trust(),
)
}
fn authority_with_routine_catalog(&self, catalog: Arc<dyn RoutineCatalog>) -> QueryAuthority {
self.authority_with(Some(catalog), None, CacheConfig::disabled())
}
fn authority_with_limits(&self, limits: QueryLimits) -> QueryAuthority {
self.authority_with(None, Some(limits), CacheConfig::disabled())
}
fn authority_with_cache_config(&self, cache_config: CacheConfig) -> QueryAuthority {
self.authority_with(None, None, cache_config)
}
fn authority_with_limits_and_cache_config(
&self,
limits: QueryLimits,
cache_config: CacheConfig,
) -> QueryAuthority {
self.authority_with(None, Some(limits), cache_config)
}
fn authority_with_routine_catalog_and_cache_config(
&self,
catalog: Arc<dyn RoutineCatalog>,
cache_config: CacheConfig,
) -> QueryAuthority {
self.authority_with(Some(catalog), None, cache_config)
}
async fn make_admin(&self, label: &str) -> String {
let identity = ExternalIdentity {
provider: "test".to_owned(),
scope: "s".to_owned(),
external_id: label.to_owned(),
display_name: label.to_owned(),
..Default::default()
};
self.persona
.set_admin(identity, true, NOW)
.await
.expect("set_admin")
}
async fn make_non_admin(&self, label: &str) -> String {
let identity = ExternalIdentity {
provider: "test".to_owned(),
scope: "s".to_owned(),
external_id: label.to_owned(),
display_name: label.to_owned(),
..Default::default()
};
self.persona
.attribute(
identity,
format!("conv-{label}"),
"initiator".to_owned(),
NOW,
)
.await
.expect("attribute")
.persona_id
}
async fn teardown(self) {
self.eventlog_shutdown.cancel();
drop(self.eventlog);
let _ = std::fs::remove_dir_all(&self.eventlog_dir);
self.persona_shutdown.cancel();
drop(self.persona);
let _ = std::fs::remove_dir_all(&self.persona_dir);
}
}
#[test]
fn sealed_set_is_crate_only() {
let _: crate::session::QueryScope = crate::session::QueryScope::Fleet;
let _ = crate::engine::PartitionEvents {
partition: "conv-x".to_string(),
events: Vec::new(),
};
let _ = crate::engine::ReferenceData::empty();
let _ = crate::provider::EventsTableProvider::new("conv-x");
let _: &str = crate::views::COMMITTED_TURNS_VIEW_SQL;
let _ = crate::statement_gate::check_statement_allowed("SELECT 1", false);
let _ = crate::decode::events_batch("conv-x", &[]);
}
#[tokio::test]
async fn admin_session_valid_admin_scopes_fleet() {
let fx = Fixture::build("admin-valid").await;
let persona_id = fx.make_admin("alice").await;
let token = mint_session(
&fx.signer.relabel_for_test(),
&SessionSubject::Persona {
persona_id: persona_id.clone(),
},
&[SessionScope::ExplorerRead],
NOW,
TEST_TTL_MS,
);
let principal = fx
.authority
.verify_admin_session(&token, NOW)
.await
.expect("a valid admin session must verify");
match &principal {
Principal::Admin(admin) => assert_eq!(admin.persona_id(), persona_id),
other => panic!("expected Principal::Admin, got {other:?}"),
}
let scoped = fx
.authority
.scope_for(&principal)
.await
.expect("Admin always scopes");
assert!(matches!(scoped.scope, QueryScope::Fleet));
assert_eq!(scoped.caller_identity(), Some(persona_id.as_str()));
fx.teardown().await;
}
#[tokio::test]
async fn admin_session_no_token_is_invalid_session() {
let fx = Fixture::build("admin-no-token").await;
let err = fx
.authority
.verify_admin_session("not-a-real-token", NOW)
.await
.unwrap_err();
assert!(matches!(err, PrincipalError::InvalidSession));
fx.teardown().await;
}
#[tokio::test]
async fn admin_session_expired_is_invalid_session() {
let fx = Fixture::build("admin-expired").await;
let persona_id = fx.make_admin("bob").await;
let token = mint_session(
&fx.signer.relabel_for_test(),
&SessionSubject::Persona {
persona_id: persona_id.clone(),
},
&[SessionScope::ExplorerRead],
NOW,
1_000,
);
let err = fx
.authority
.verify_admin_session(&token, NOW + 1_000)
.await
.unwrap_err();
assert!(matches!(err, PrincipalError::InvalidSession));
fx.teardown().await;
}
#[tokio::test]
async fn admin_session_revoked_is_invalid_session() {
let fx = Fixture::build("admin-revoked").await;
let persona_id = fx.make_admin("carol").await;
let token = mint_session(
&fx.signer.relabel_for_test(),
&SessionSubject::Persona {
persona_id: persona_id.clone(),
},
&[SessionScope::ExplorerRead],
NOW,
TEST_TTL_MS,
);
fx.revoked.revoke(&token);
let err = fx
.authority
.verify_admin_session(&token, NOW)
.await
.unwrap_err();
assert!(matches!(err, PrincipalError::InvalidSession));
fx.teardown().await;
}
#[tokio::test]
async fn admin_session_valid_non_admin_mints_a_persona_principal() {
let fx = Fixture::build("admin-non-admin").await;
let persona_id = fx.make_non_admin("dave").await;
let token = mint_session(
&fx.signer.relabel_for_test(),
&SessionSubject::Persona {
persona_id: persona_id.clone(),
},
&[SessionScope::ExplorerRead],
NOW,
TEST_TTL_MS,
);
let principal = fx
.authority
.verify_admin_session(&token, NOW)
.await
.expect("a valid non-admin session must verify as a persona principal");
match &principal {
Principal::Persona(persona) => assert_eq!(persona.persona_id(), persona_id),
other => panic!("expected Principal::Persona, got {other:?}"),
}
fx.teardown().await;
}
#[tokio::test]
async fn admin_session_unknown_persona_is_not_authorized_for_fleet() {
let fx = Fixture::build("admin-unknown-persona").await;
let token = mint_session(
&fx.signer.relabel_for_test(),
&SessionSubject::Persona {
persona_id: "persona-never-existed".to_owned(),
},
&[SessionScope::ExplorerRead],
NOW,
TEST_TTL_MS,
);
let err = fx
.authority
.verify_admin_session(&token, NOW)
.await
.unwrap_err();
assert!(matches!(err, PrincipalError::NotAuthorizedForFleet));
fx.teardown().await;
}
#[tokio::test]
async fn admin_session_store_down_is_unavailable_not_401_or_403() {
let fx = Fixture::build("admin-store-down").await;
let persona_id = fx.make_admin("erin").await;
let token = mint_session(
&fx.signer.relabel_for_test(),
&SessionSubject::Persona {
persona_id: persona_id.clone(),
},
&[SessionScope::ExplorerRead],
NOW,
TEST_TTL_MS,
);
let authority_no_store = fx.authority_with_empty_persona_cell();
let err = authority_no_store
.verify_admin_session(&token, NOW)
.await
.unwrap_err();
assert!(
matches!(err, PrincipalError::StoreUnavailable),
"an unreadable persona store must surface as a distinct, transient error — never fold \
into a 401/403 shape: {err:?}"
);
fx.teardown().await;
}
#[tokio::test]
async fn persona_execute_sees_only_its_own_participated_conversations() {
let fx = Fixture::build("persona-isolation").await;
let turn_a = uuid::Uuid::now_v7();
fx.eventlog
.append_batch(
"conv-a".to_owned(),
vec![
Event::new(kinds::tagged(kinds::TURN_START, &turn_a), Vec::new()),
Event::new(kinds::tagged(kinds::TURN_COMPLETE, &turn_a), Vec::new()),
],
)
.await
.expect("append conv-a");
let turn_b = uuid::Uuid::now_v7();
fx.eventlog
.append_batch(
"conv-b".to_owned(),
vec![
Event::new(kinds::tagged(kinds::TURN_START, &turn_b), Vec::new()),
Event::new(kinds::tagged(kinds::TURN_COMPLETE, &turn_b), Vec::new()),
],
)
.await
.expect("append conv-b");
let persona_a = fx
.persona
.attribute(
ExternalIdentity {
provider: "test".to_owned(),
scope: "s".to_owned(),
external_id: "persona-a".to_owned(),
display_name: "persona-a".to_owned(),
..Default::default()
},
"a".to_owned(),
"initiator".to_owned(),
NOW,
)
.await
.expect("attribute persona A")
.persona_id;
let _persona_b = fx
.persona
.attribute(
ExternalIdentity {
provider: "test".to_owned(),
scope: "s".to_owned(),
external_id: "persona-b".to_owned(),
display_name: "persona-b".to_owned(),
..Default::default()
},
"b".to_owned(),
"initiator".to_owned(),
NOW,
)
.await
.expect("attribute persona B")
.persona_id;
let token = mint_session(
&fx.signer.relabel_for_test(),
&SessionSubject::Persona {
persona_id: persona_a,
},
&[SessionScope::ExplorerRead],
NOW,
TEST_TTL_MS,
);
let principal = fx
.authority
.verify_admin_session(&token, NOW)
.await
.expect("a valid non-admin session must verify");
assert!(
matches!(principal, Principal::Persona(_)),
"persona A's own session must mint a persona principal, not admin"
);
let scoped = fx
.authority
.scope_for(&principal)
.await
.expect("a persona principal with a live persona store always scopes");
match &scoped.scope {
QueryScope::Conversations(ids) => assert_eq!(ids, &vec!["a".to_owned()]),
other => panic!("expected Conversations([\"a\"]), got {other:?}"),
}
let result = scoped
.execute("SELECT DISTINCT partition FROM events")
.await
.expect("persona-scoped query over its own participated conversation");
assert_eq!(
result.rows,
vec![vec![serde_json::json!("conv-a")]],
"persona A must see exactly its own conversation's partition, never persona B's: {:?}",
result.rows
);
let err = scoped.execute("EXPLAIN SELECT 1").await.unwrap_err();
assert!(matches!(err, ScopedQueryError::Rejected(_)));
fx.teardown().await;
}
#[tokio::test]
async fn persona_with_zero_participations_gets_an_empty_but_valid_scope() {
let fx = Fixture::build("persona-zero-participations").await;
let principal = Principal::Persona(PersonaPrincipal {
persona_id: "persona-never-participated".to_owned(),
});
let scoped = fx
.authority
.scope_for(&principal)
.await
.expect("zero participations is a valid, empty scope, not an error");
match &scoped.scope {
QueryScope::Conversations(ids) => {
assert!(
ids.is_empty(),
"expected zero participated conversations: {ids:?}"
);
}
other => panic!("expected an empty Conversations scope, got {other:?}"),
}
let result = scoped
.execute("SELECT COUNT(*) AS c FROM events")
.await
.expect("a zero-participation persona still runs queries successfully");
assert_eq!(result.rows, vec![vec![serde_json::json!(0)]]);
fx.teardown().await;
}
#[tokio::test]
async fn persona_scope_store_down_is_unavailable() {
let fx = Fixture::build("persona-store-down").await;
let authority_no_store = fx.authority_with_empty_persona_cell();
let principal = Principal::Persona(PersonaPrincipal {
persona_id: "persona-x".to_owned(),
});
let err = authority_no_store.scope_for(&principal).await.unwrap_err();
assert!(
matches!(err, PrincipalError::StoreUnavailable),
"a persona's participation resolution must surface store-unavailable as a distinct, \
transient error — never fold into a hard refusal: {err:?}"
);
fx.teardown().await;
}
#[test]
fn conversation_grant_round_trips() {
let signer = test_signer();
let token = mint_conversation_grant(
&signer.relabel_for_test(),
"conv-1",
GrantSubject::Turn("turn-1".to_owned()),
NOW + TEST_TTL_MS,
);
let authority_signer = signer.public_key_bytes();
let dir = std::env::temp_dir().join(format!(
"polyc-query-authority-grant-round-trip-{}",
std::process::id()
));
let eventlog = EventLogHost::spawn(dir, CancellationToken::new(), signer.relabel_for_test())
.expect("spawn eventlog host");
let eventlog = Arc::new(eventlog);
let authority = QueryAuthority::new(
eventlog.clone(),
Arc::new(arc_swap::ArcSwapOption::empty()),
test_dashboard_cell(&eventlog),
Arc::new(RevokedTokens::new()),
authority_signer.clone(),
vec![authority_signer],
QueryLimits::default(),
None,
CacheConfig::disabled(),
test_handoff_trust(),
);
let principal = authority
.verify_conversation_grant(&token, NOW)
.expect("a freshly minted grant must verify");
match principal {
Principal::ConversationGrant(grant) => {
assert_eq!(grant.conversation_id(), "conv-1");
assert_eq!(grant.turn_id(), Some("turn-1"));
assert_eq!(grant.subject(), &GrantSubject::Turn("turn-1".to_owned()));
}
other => panic!("expected Principal::ConversationGrant, got {other:?}"),
}
}
#[test]
fn conversation_grant_minted_before_rotation_requires_historical_trust() {
let fixture_signer = test_signer();
let retired = TurnReadSigner::from_seed(91);
let current = TurnReadSigner::from_seed(92);
let token = mint_conversation_grant(
&retired,
"conv-before-rotation",
GrantSubject::Turn("turn-before-rotation".to_owned()),
NOW + TEST_TTL_MS,
);
let dir = std::env::temp_dir().join(format!(
"polyc-query-authority-grant-history-{}",
std::process::id()
));
let eventlog = Arc::new(
EventLogHost::spawn(
dir,
CancellationToken::new(),
fixture_signer.relabel_for_test(),
)
.expect("spawn eventlog host"),
);
let authority = QueryAuthority::new(
eventlog.clone(),
Arc::new(arc_swap::ArcSwapOption::empty()),
test_dashboard_cell(&eventlog),
Arc::new(RevokedTokens::new()),
current.public_key_bytes(),
vec![fixture_signer.public_key_bytes()],
QueryLimits::default(),
None,
CacheConfig::disabled(),
test_handoff_trust(),
);
assert!(matches!(
authority.verify_conversation_grant(&token, NOW),
Err(PrincipalError::InvalidGrant)
));
let historical =
RoleTrustSet::<TurnReadRole>::checked(vec![current.identity(), retired.identity()])
.expect("valid rotation history");
let authority = authority.with_turn_read_trust_for_test(historical);
assert!(authority.verify_conversation_grant(&token, NOW).is_ok());
}
#[test]
fn conversation_grant_web_session_subject_round_trips() {
let signer = test_signer();
let token = mint_conversation_grant(
&signer.relabel_for_test(),
"conv-1",
GrantSubject::WebSession("persona-web-1".to_owned()),
NOW + TEST_TTL_MS,
);
let dir = std::env::temp_dir().join(format!(
"polyc-query-authority-grant-web-session-{}",
std::process::id()
));
let eventlog = Arc::new(
EventLogHost::spawn(dir, CancellationToken::new(), signer.relabel_for_test())
.expect("spawn eventlog host"),
);
let authority = QueryAuthority::new(
eventlog.clone(),
Arc::new(arc_swap::ArcSwapOption::empty()),
test_dashboard_cell(&eventlog),
Arc::new(RevokedTokens::new()),
signer.public_key_bytes(),
vec![signer.public_key_bytes()],
QueryLimits::default(),
None,
CacheConfig::disabled(),
test_handoff_trust(),
);
let principal = authority
.verify_conversation_grant(&token, NOW)
.expect("a freshly minted web-session grant must verify");
match principal {
Principal::ConversationGrant(grant) => {
assert_eq!(grant.conversation_id(), "conv-1");
assert_eq!(
grant.turn_id(),
None,
"a web-session grant must never report a fabricated turn id"
);
assert_eq!(
grant.subject(),
&GrantSubject::WebSession("persona-web-1".to_owned())
);
}
other => panic!("expected Principal::ConversationGrant, got {other:?}"),
}
}
#[test]
fn conversation_grant_expired_is_distinguishable_from_invalid() {
let signer = test_signer();
let token = mint_conversation_grant(
&signer.relabel_for_test(),
"conv-1",
GrantSubject::Turn("turn-1".to_owned()),
NOW - 1,
);
let dir = std::env::temp_dir().join(format!(
"polyc-query-authority-grant-expired-{}",
std::process::id()
));
let eventlog = EventLogHost::spawn(dir, CancellationToken::new(), signer.relabel_for_test())
.expect("spawn eventlog host");
let eventlog = Arc::new(eventlog);
let authority = QueryAuthority::new(
eventlog.clone(),
Arc::new(arc_swap::ArcSwapOption::empty()),
test_dashboard_cell(&eventlog),
Arc::new(RevokedTokens::new()),
signer.public_key_bytes(),
vec![signer.public_key_bytes()],
QueryLimits::default(),
None,
CacheConfig::disabled(),
test_handoff_trust(),
);
let err = authority
.verify_conversation_grant(&token, NOW)
.unwrap_err();
assert!(
matches!(err, PrincipalError::GrantExpired),
"an expired-but-otherwise-genuine grant must report GrantExpired, not InvalidGrant: \
{err:?}"
);
}
#[test]
fn conversation_grant_wrong_signer_is_invalid_grant() {
let minting_signer = ApprovalSigner::from_seed(1);
let verifying_signer = ApprovalSigner::from_seed(2);
let token = mint_conversation_grant(
&minting_signer.relabel_for_test(),
"conv-1",
GrantSubject::Turn("turn-1".to_owned()),
NOW + TEST_TTL_MS,
);
let dir = std::env::temp_dir().join(format!(
"polyc-query-authority-grant-wrong-signer-{}",
std::process::id()
));
let eventlog = EventLogHost::spawn(
dir,
CancellationToken::new(),
verifying_signer.relabel_for_test(),
)
.expect("spawn eventlog host");
let eventlog = Arc::new(eventlog);
let authority = QueryAuthority::new(
eventlog.clone(),
Arc::new(arc_swap::ArcSwapOption::empty()),
test_dashboard_cell(&eventlog),
Arc::new(RevokedTokens::new()),
verifying_signer.public_key_bytes(),
vec![verifying_signer.public_key_bytes()],
QueryLimits::default(),
None,
CacheConfig::disabled(),
test_handoff_trust(),
);
let err = authority
.verify_conversation_grant(&token, NOW)
.unwrap_err();
assert!(matches!(err, PrincipalError::InvalidGrant));
}
#[test]
fn conversation_grant_wrong_kind_tag_is_invalid_grant() {
let signer = test_signer();
let turn_read_signer: TurnReadSigner = signer.relabel_for_test();
let identity = turn_read_signer.identity();
let claims = GrantClaims {
kind: "some_other_signed_payload.v1".to_owned(),
issuer: identity.issuer().to_owned(),
key_id: identity.key_id().to_owned(),
conversation_id: "conv-x".to_owned(),
subject: GrantSubject::Turn("turn-y".to_owned()),
expires_at_ms: NOW + TEST_TTL_MS,
};
let canonical = serde_json::to_vec(&claims).expect("GrantClaims always serializes");
let signature = turn_read_signer.sign_turn_read_capability(&canonical);
let token = format!(
"{}.{}",
URL_SAFE_NO_PAD.encode(canonical),
URL_SAFE_NO_PAD.encode(signature)
);
let dir = std::env::temp_dir().join(format!(
"polyc-query-authority-grant-wrong-kind-{}",
std::process::id()
));
let eventlog = EventLogHost::spawn(dir, CancellationToken::new(), signer.relabel_for_test())
.expect("spawn eventlog host");
let eventlog = Arc::new(eventlog);
let authority = QueryAuthority::new(
eventlog.clone(),
Arc::new(arc_swap::ArcSwapOption::empty()),
test_dashboard_cell(&eventlog),
Arc::new(RevokedTokens::new()),
signer.public_key_bytes(),
vec![signer.public_key_bytes()],
QueryLimits::default(),
None,
CacheConfig::disabled(),
test_handoff_trust(),
);
let err = authority
.verify_conversation_grant(&token, NOW)
.unwrap_err();
assert!(
matches!(err, PrincipalError::InvalidGrant),
"a genuinely-signed payload with the wrong kind tag must be refused, not silently \
accepted"
);
}
#[test]
fn conversation_grant_malformed_shapes_are_invalid_grant() {
let signer = test_signer();
let dir = std::env::temp_dir().join(format!(
"polyc-query-authority-grant-malformed-{}",
std::process::id()
));
let eventlog = EventLogHost::spawn(dir, CancellationToken::new(), signer.relabel_for_test())
.expect("spawn eventlog host");
let eventlog = Arc::new(eventlog);
let authority = QueryAuthority::new(
eventlog.clone(),
Arc::new(arc_swap::ArcSwapOption::empty()),
test_dashboard_cell(&eventlog),
Arc::new(RevokedTokens::new()),
signer.public_key_bytes(),
vec![signer.public_key_bytes()],
QueryLimits::default(),
None,
CacheConfig::disabled(),
test_handoff_trust(),
);
assert!(
matches!(
authority.verify_conversation_grant("not-a-grant-token", NOW),
Err(PrincipalError::InvalidGrant)
),
"a token with no `.` separator must be rejected"
);
assert!(
matches!(
authority.verify_conversation_grant(".c2ln", NOW),
Err(PrincipalError::InvalidGrant)
),
"an empty claims segment must be rejected"
);
assert!(
matches!(
authority.verify_conversation_grant("Y2xhaW1z.", NOW),
Err(PrincipalError::InvalidGrant)
),
"an empty signature segment must be rejected"
);
assert!(
matches!(
authority.verify_conversation_grant("Y2xhaW1z.sig.with.dots", NOW),
Err(PrincipalError::InvalidGrant)
),
"a signature segment containing a stray `.` must be rejected"
);
}
#[tokio::test]
async fn fleet_execute_runs_over_every_conversation_partition() {
let fx = Fixture::build("fleet-execute").await;
let turn = uuid::Uuid::now_v7();
fx.eventlog
.append_batch(
"conv-fleet-1".to_owned(),
vec![
Event::new(kinds::tagged(kinds::TURN_START, &turn), Vec::new()),
Event::new(kinds::tagged(kinds::TURN_COMPLETE, &turn), Vec::new()),
],
)
.await
.expect("append a committed turn");
let persona_id = fx.make_admin("frank").await;
let token = mint_session(
&fx.signer.relabel_for_test(),
&SessionSubject::Persona {
persona_id: persona_id.clone(),
},
&[SessionScope::ExplorerRead],
NOW,
TEST_TTL_MS,
);
let principal = fx
.authority
.verify_admin_session(&token, NOW)
.await
.expect("valid admin session");
let scoped = fx.authority.scope_for(&principal).await.expect("scope_for");
let result = scoped
.execute("SELECT COUNT(*) AS c FROM events")
.await
.expect("fleet query over the committed view");
assert_eq!(result.columns, vec!["c"]);
assert_eq!(result.rows, vec![vec![serde_json::json!(2)]]);
fx.teardown().await;
}
fn block_every_blob_file(dir: &std::path::Path) -> usize {
let entries: Vec<std::path::PathBuf> = std::fs::read_dir(dir)
.expect("read the broken partition's data directory")
.map(|entry| entry.expect("read a directory entry").path())
.collect();
let mut blocked = 0_usize;
for path in entries {
let file_type = std::fs::symlink_metadata(&path)
.expect("stat a partition data entry")
.file_type();
if file_type.is_dir() {
blocked += block_every_blob_file(&path);
} else {
std::fs::remove_file(&path).expect("remove a blob file");
std::fs::create_dir(&path).expect("shadow the blob file with a directory");
blocked += 1;
}
}
blocked
}
#[tokio::test]
async fn fleet_execute_reports_skipped_unreadable_partitions() {
let signer = test_signer();
let eventlog_dir = std::env::temp_dir().join(format!(
"polyc-query-authority-fleet-skip-eventlog-{}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&eventlog_dir);
let persona_dir = std::env::temp_dir().join(format!(
"polyc-query-authority-fleet-skip-persona-{}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&persona_dir);
{
let shutdown = CancellationToken::new();
let host = EventLogHost::spawn(
eventlog_dir.clone(),
shutdown.clone(),
signer.relabel_for_test(),
)
.expect("spawn eventlog host");
let turn_ok = uuid::Uuid::now_v7();
host.append_batch(
"conv-fleet-skip-ok".to_owned(),
vec![
Event::new(kinds::tagged(kinds::TURN_START, &turn_ok), Vec::new()),
Event::new(kinds::tagged(kinds::TURN_COMPLETE, &turn_ok), Vec::new()),
],
)
.await
.expect("append conv-fleet-skip-ok");
let turn_broken = uuid::Uuid::now_v7();
host.append_batch(
"conv-fleet-skip-broken".to_owned(),
vec![
Event::new(kinds::tagged(kinds::TURN_START, &turn_broken), Vec::new()),
Event::new(
kinds::tagged(kinds::TURN_COMPLETE, &turn_broken),
Vec::new(),
),
],
)
.await
.expect("append conv-fleet-skip-broken");
shutdown.cancel();
drop(host);
}
let broken_dir = eventlog_dir.join("conv-fleet-skip-broken_data");
let blocked = block_every_blob_file(&broken_dir);
assert!(
blocked > 0,
"the broken partition must have written blob files to block: {broken_dir:?}"
);
let shutdown2 = CancellationToken::new();
let host2 = Arc::new(
EventLogHost::spawn(
eventlog_dir.clone(),
shutdown2.clone(),
signer.relabel_for_test(),
)
.expect("reopen eventlog host"),
);
let persona_shutdown = CancellationToken::new();
let persona = Arc::new(
PersonaHost::spawn(persona_dir.clone(), persona_shutdown.clone())
.expect("spawn persona host"),
);
let revoked = Arc::new(RevokedTokens::new());
let authority = QueryAuthority::new(
host2.clone(),
Arc::new(arc_swap::ArcSwapOption::new(Some(persona.clone()))),
test_dashboard_cell(&host2),
revoked,
signer.public_key_bytes(),
vec![signer.public_key_bytes()],
QueryLimits::default(),
None,
CacheConfig::disabled(),
test_handoff_trust(),
);
let identity = ExternalIdentity {
provider: "test".to_owned(),
scope: "s".to_owned(),
external_id: "fleet-skip-admin".to_owned(),
display_name: "fleet-skip-admin".to_owned(),
..Default::default()
};
let persona_id = persona
.set_admin(identity, true, NOW)
.await
.expect("set_admin");
let token = mint_session(
&signer.relabel_for_test(),
&SessionSubject::Persona { persona_id },
&[SessionScope::ExplorerRead],
NOW,
TEST_TTL_MS,
);
let principal = authority
.verify_admin_session(&token, NOW)
.await
.expect("valid admin session");
let scoped = authority.scope_for(&principal).await.expect("scope_for");
let result = scoped
.execute("SELECT COUNT(*) AS c FROM events")
.await
.expect("a fleet query must tolerate one unreadable partition, not fail outright");
assert_eq!(
result.rows,
vec![vec![serde_json::json!(2)]],
"only the readable partition's two committed rows are visible"
);
assert_eq!(
result.skipped_partitions, 1,
"the unreadable partition must be reported to the caller, not just \
tracing::warn!-logged server-side"
);
shutdown2.cancel();
drop(host2);
let _ = std::fs::remove_dir_all(&eventlog_dir);
persona_shutdown.cancel();
drop(persona);
let _ = std::fs::remove_dir_all(&persona_dir);
}
#[tokio::test]
async fn conversation_grant_execute_sees_only_its_own_partition() {
let fx = Fixture::build("grant-execute").await;
let turn_a = uuid::Uuid::now_v7();
fx.eventlog
.append_batch(
"conv-a".to_owned(),
vec![
Event::new(kinds::tagged(kinds::TURN_START, &turn_a), Vec::new()),
Event::new(kinds::tagged(kinds::TURN_COMPLETE, &turn_a), Vec::new()),
],
)
.await
.expect("append conv-a");
let turn_b = uuid::Uuid::now_v7();
fx.eventlog
.append_batch(
"conv-b".to_owned(),
vec![
Event::new(kinds::tagged(kinds::TURN_START, &turn_b), Vec::new()),
Event::new(kinds::tagged(kinds::TURN_COMPLETE, &turn_b), Vec::new()),
],
)
.await
.expect("append conv-b");
let token = mint_conversation_grant(
&fx.signer.relabel_for_test(),
"a",
GrantSubject::Turn("turn-1".to_owned()),
NOW + TEST_TTL_MS,
);
let principal = fx
.authority
.verify_conversation_grant(&token, NOW)
.expect("valid grant");
let scoped = fx.authority.scope_for(&principal).await.expect("scope_for");
let result = scoped
.execute("SELECT COUNT(*) AS c FROM events")
.await
.expect("conversation-scoped query");
assert_eq!(result.rows, vec![vec![serde_json::json!(2)]]);
let err = scoped.execute("EXPLAIN SELECT 1").await.unwrap_err();
assert!(matches!(err, ScopedQueryError::Rejected(_)));
fx.teardown().await;
}
#[tokio::test]
async fn conversation_grant_unknown_table_names_this_scopes_own_catalog() {
let fx = Fixture::build("grant-unknown-table").await;
let turn = uuid::Uuid::now_v7();
fx.eventlog
.append_batch(
"conv-a".to_owned(),
vec![
Event::new(kinds::tagged(kinds::TURN_START, &turn), Vec::new()),
Event::new(kinds::tagged(kinds::TURN_COMPLETE, &turn), Vec::new()),
],
)
.await
.expect("append conv-a");
let token = mint_conversation_grant(
&fx.signer.relabel_for_test(),
"a",
GrantSubject::Turn("turn-1".to_owned()),
NOW + TEST_TTL_MS,
);
let principal = fx
.authority
.verify_conversation_grant(&token, NOW)
.expect("valid grant");
let scoped = fx.authority.scope_for(&principal).await.expect("scope_for");
for table in ["callers", "turns", "personas", "events_raw"] {
let err = scoped
.execute(&format!("SELECT * FROM {table}"))
.await
.unwrap_err();
assert!(
matches!(err, ScopedQueryError::UnknownTable(_)),
"`{table}` must resolve to UnknownTable, not a generic internal failure: {err:?}"
);
let message = err.to_string();
assert_eq!(
message, *CONVERSATION_UNKNOWN_TABLE_MESSAGE,
"every unresolvable name must return the one fixed message: {message:?}"
);
assert!(
!message.contains(table),
"the message must never echo the name the caller guessed: {message:?}"
);
}
for table in CONVERSATION_CATALOG {
assert!(
CONVERSATION_UNKNOWN_TABLE_MESSAGE.contains(table),
"`{table}` is in this scope's catalog but the message does not name it"
);
scoped
.execute(&format!("SELECT COUNT(*) AS c FROM {table}"))
.await
.unwrap_or_else(|err| panic!("advertised table `{table}` must resolve: {err:?}"));
}
let fleet_persona = fx.make_admin("catalog-probe").await;
let fleet_token = mint_session(
&fx.signer.relabel_for_test(),
&SessionSubject::Persona {
persona_id: fleet_persona,
},
&[SessionScope::ExplorerRead],
NOW,
TEST_TTL_MS,
);
let fleet_principal = fx
.authority
.verify_admin_session(&fleet_token, NOW)
.await
.expect("valid admin session");
let fleet = fx
.authority
.scope_for(&fleet_principal)
.await
.expect("scope_for");
let listed = fleet
.execute("SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'")
.await
.expect("a Fleet session enumerates its own catalog");
assert!(
!listed.rows.is_empty(),
"the Fleet catalog enumeration returned nothing, so this direction would pass vacuously"
);
let mut resolvable = Vec::new();
for row in &listed.rows {
let name = row[0]
.as_str()
.unwrap_or_else(|| panic!("information_schema.tables.table_name must be text: {row:?}"))
.to_owned();
match scoped
.execute(&format!("SELECT * FROM {name} LIMIT 0"))
.await
{
Ok(_) => resolvable.push(name),
Err(ScopedQueryError::UnknownTable(_)) => {}
Err(err) => {
panic!("probing `{name}` against the grant scope failed unexpectedly: {err:?}")
}
}
}
resolvable.sort();
let mut advertised: Vec<String> = CONVERSATION_CATALOG
.iter()
.map(|table| (*table).to_owned())
.collect();
advertised.sort();
assert_eq!(
resolvable, advertised,
"CONVERSATION_CATALOG must name exactly what a grant scope can resolve — add the new \
table to that constant (which is what the UnknownTable message and both SQL-hatch tool \
descriptions are built from) rather than leaving callers to guess at it"
);
fx.teardown().await;
}
#[tokio::test]
async fn fleet_unknown_table_points_at_the_catalog_this_scope_can_enumerate() {
let fx = Fixture::build("fleet-unknown-table").await;
let turn = uuid::Uuid::now_v7();
fx.eventlog
.append_batch(
"conv-fleet-unknown".to_owned(),
vec![
Event::new(kinds::tagged(kinds::TURN_START, &turn), Vec::new()),
Event::new(kinds::tagged(kinds::TURN_COMPLETE, &turn), Vec::new()),
],
)
.await
.expect("append a committed turn");
let persona_id = fx.make_admin("grace").await;
let token = mint_session(
&fx.signer.relabel_for_test(),
&SessionSubject::Persona { persona_id },
&[SessionScope::ExplorerRead],
NOW,
TEST_TTL_MS,
);
let principal = fx
.authority
.verify_admin_session(&token, NOW)
.await
.expect("valid admin session");
let scoped = fx.authority.scope_for(&principal).await.expect("scope_for");
let err = scoped.execute("SELECT * FROM callers").await.unwrap_err();
assert!(
matches!(err, ScopedQueryError::UnknownTable(_)),
"a fleet-wide caller must get UnknownTable too: {err:?}"
);
assert_eq!(err.to_string(), FLEET_UNKNOWN_TABLE_MESSAGE);
scoped
.execute("SELECT table_name FROM information_schema.tables")
.await
.expect("the query the fleet message recommends must actually run");
fx.teardown().await;
}
async fn registered_columns(scoped: &ScopedQuery, table: &str) -> Vec<String> {
let result = scoped
.execute(&format!("SELECT * FROM {table} LIMIT 0"))
.await
.unwrap_or_else(|err| panic!("`{table}` must resolve for this scope: {err:?}"));
assert!(
!result.columns.is_empty(),
"`{table}` returned no columns, so every assertion built from it would pass vacuously"
);
result.columns
}
fn offered_columns(message: &str) -> Vec<String> {
let (_, offered) = message
.split_once("you can select ")
.unwrap_or_else(|| panic!("this message offers no columns at all: {message}"));
offered
.replace(", and ", ", ")
.replace(" and ", ", ")
.split(", ")
.map(ToOwned::to_owned)
.collect()
}
fn expected_unknown_column_message(guessed: &str, table: &str, columns: &[String]) -> String {
let borrowed: Vec<&str> = columns.iter().map(String::as_str).collect();
format!(
"there is no `{guessed}` column on `{table}`; you can select {}",
catalog_sentence(&borrowed)
)
}
#[tokio::test]
async fn conversation_grant_unknown_column_names_the_columns_that_table_really_has() {
let fx = Fixture::build("grant-unknown-column").await;
let turn = uuid::Uuid::now_v7();
fx.eventlog
.append_batch(
"conv-a".to_owned(),
vec![
Event::new(kinds::tagged(kinds::TURN_START, &turn), Vec::new()),
Event::new(kinds::tagged(kinds::TURN_COMPLETE, &turn), Vec::new()),
],
)
.await
.expect("append conv-a");
let token = mint_conversation_grant(
&fx.signer.relabel_for_test(),
"a",
GrantSubject::Turn("turn-1".to_owned()),
NOW + TEST_TTL_MS,
);
let principal = fx
.authority
.verify_conversation_grant(&token, NOW)
.expect("valid grant");
let scoped = fx.authority.scope_for(&principal).await.expect("scope_for");
let events_columns = registered_columns(&scoped, "events").await;
for (sql, guessed) in [
("SELECT message FROM events", "message"),
("SELECT event_type FROM events", "event_type"),
("SELECT type FROM events LIMIT 5", "type"),
("SELECT events.message FROM events", "message"),
(
"SELECT partition FROM events WHERE message = 'x'",
"message",
),
] {
let err = scoped.execute(sql).await.unwrap_err();
assert!(
matches!(err, ScopedQueryError::UnknownColumn(_)),
"`{sql}` must resolve to UnknownColumn, not a generic internal failure: {err:?}"
);
assert_eq!(
err.to_string(),
expected_unknown_column_message(guessed, "events", &events_columns),
"`{sql}` must name the column asked for and exactly the columns `events` registers"
);
}
let tool_calls_columns = registered_columns(&scoped, "tool_calls").await;
let err = scoped
.execute("SELECT tool FROM tool_calls")
.await
.unwrap_err();
assert_eq!(
err.to_string(),
expected_unknown_column_message("tool", "tool_calls", &tool_calls_columns)
);
let attribution_columns = registered_columns(&scoped, "attribution").await;
assert!(
!attribution_columns
.iter()
.any(|column| column.starts_with("identity_")),
"a redacted scope must never be offered a Fleet-only column: {attribution_columns:?}"
);
let err = scoped
.execute("SELECT identity_kind FROM attribution")
.await
.unwrap_err();
assert_eq!(
err.to_string(),
expected_unknown_column_message("identity_kind", "attribution", &attribution_columns),
"the offer is the redacted view's own schema, and the only `identity_` in the message is \
the name the caller themselves wrote"
);
let err = scoped.execute("SELECT nope").await.unwrap_err();
assert!(matches!(err, ScopedQueryError::UnknownColumn(_)));
assert_eq!(
err.to_string(),
"there is no `nope` column here; add a `FROM` clause naming the table to read it from"
);
let messages_columns = registered_columns(&scoped, "messages").await;
let joined: Vec<String> = events_columns
.iter()
.map(|column| format!("events.{column}"))
.chain(
messages_columns
.iter()
.map(|column| format!("messages.{column}")),
)
.collect();
let err = scoped
.execute("SELECT sender FROM events JOIN messages ON messages.turn_id = events.turn_id")
.await
.unwrap_err();
assert!(matches!(err, ScopedQueryError::UnknownColumn(_)));
assert_eq!(
err.to_string(),
format!(
"there is no `sender` column available where you used it; you can select {}",
column_list_sentence(&joined)
)
);
for sql in [
"SELECT turn_id FROM events ORDER BY nope",
"SELECT turn_id, kind FROM events GROUP BY turn_id, kind HAVING nope > 1",
] {
let err = scoped.execute(sql).await.unwrap_err();
let rendered = err.to_string();
assert!(
matches!(err, ScopedQueryError::UnknownColumn(_)),
"`{sql}` must resolve to UnknownColumn: {err:?}"
);
let mut offered = offered_columns(&rendered);
offered.sort();
let mut expected = events_columns.clone();
expected.sort();
assert_eq!(
offered, expected,
"`{sql}` must offer every `events` column exactly once: {rendered}"
);
}
let err = scoped
.execute("SELECT events.turn_id FROM events e")
.await
.unwrap_err();
assert!(matches!(err, ScopedQueryError::UnknownColumn(_)));
let aliased: Vec<String> = events_columns
.iter()
.map(|column| format!("e.{column}"))
.collect();
assert_eq!(
err.to_string(),
format!(
"there is no `events.turn_id` column available where you used it; that part of the \
query reads from `e`, so you can select {}",
column_list_sentence(&aliased)
)
);
let aliased_messages: Vec<String> = messages_columns
.iter()
.map(|column| format!("m.{column}"))
.collect();
let err = scoped
.execute(
"SELECT e.turn_id FROM events e WHERE EXISTS (SELECT 1 FROM messages m WHERE m.role = \
e.nope)",
)
.await
.unwrap_err();
assert!(matches!(err, ScopedQueryError::UnknownColumn(_)));
let rendered = err.to_string();
assert_eq!(
rendered,
format!(
"there is no `e.nope` column available where you used it; that part of the query \
reads from `m`, so you can select {}",
column_list_sentence(&aliased_messages)
)
);
assert!(
!rendered.contains("this query selects from"),
"the offer is the resolution site's schema, not the statement's: {rendered}"
);
fx.teardown().await;
}
#[tokio::test]
async fn fleet_attribution_still_carries_the_columns_a_grant_scope_is_not_offered() {
let fx = Fixture::build("fleet-attribution-columns").await;
let turn = uuid::Uuid::now_v7();
fx.eventlog
.append_batch(
"conv-fleet-columns".to_owned(),
vec![
Event::new(kinds::tagged(kinds::TURN_START, &turn), Vec::new()),
Event::new(kinds::tagged(kinds::TURN_COMPLETE, &turn), Vec::new()),
],
)
.await
.expect("append a committed turn");
let persona_id = fx.make_admin("ida").await;
let token = mint_session(
&fx.signer.relabel_for_test(),
&SessionSubject::Persona { persona_id },
&[SessionScope::ExplorerRead],
NOW,
TEST_TTL_MS,
);
let principal = fx
.authority
.verify_admin_session(&token, NOW)
.await
.expect("valid admin session");
let fleet = fx.authority.scope_for(&principal).await.expect("scope_for");
let columns = registered_columns(&fleet, "attribution").await;
assert!(
columns.iter().any(|column| column.starts_with("identity_")),
"Fleet `attribution` must still carry the redacted columns: {columns:?}"
);
fx.teardown().await;
}
#[tokio::test]
async fn a_table_this_scope_cannot_query_decides_the_refusal_and_leaks_no_columns() {
let fx = Fixture::build("grant-table-before-column").await;
let turn = uuid::Uuid::now_v7();
fx.eventlog
.append_batch(
"conv-a".to_owned(),
vec![
Event::new(kinds::tagged(kinds::TURN_START, &turn), Vec::new()),
Event::new(kinds::tagged(kinds::TURN_COMPLETE, &turn), Vec::new()),
],
)
.await
.expect("append conv-a");
let token = mint_conversation_grant(
&fx.signer.relabel_for_test(),
"a",
GrantSubject::Turn("turn-1".to_owned()),
NOW + TEST_TTL_MS,
);
let principal = fx
.authority
.verify_conversation_grant(&token, NOW)
.expect("valid grant");
let scoped = fx.authority.scope_for(&principal).await.expect("scope_for");
for sql in [
"SELECT display_name FROM personas",
"SELECT persona_id FROM personas WHERE anything = '1'",
"SELECT payload FROM events_raw",
"SELECT * FROM events_raw",
"SELECT anything FROM callers",
"SELECT e.turn_id FROM events e JOIN personas p ON p.persona_id = e.turn_id",
"SELECT bogus FROM events UNION ALL SELECT persona_id FROM personas",
"SELECT bogus, (SELECT max(persona_id) FROM personas) FROM events",
] {
let err = scoped.execute(sql).await.unwrap_err();
assert!(
matches!(err, ScopedQueryError::UnknownTable(_)),
"`{sql}` must refuse the TABLE, never resolve columns against it: {err:?}"
);
assert_eq!(err.to_string(), *CONVERSATION_UNKNOWN_TABLE_MESSAGE);
}
let fleet_persona = fx.make_admin("column-leak-probe").await;
let fleet_token = mint_session(
&fx.signer.relabel_for_test(),
&SessionSubject::Persona {
persona_id: fleet_persona,
},
&[SessionScope::ExplorerRead],
NOW,
TEST_TTL_MS,
);
let fleet_principal = fx
.authority
.verify_admin_session(&fleet_token, NOW)
.await
.expect("valid admin session");
let fleet = fx
.authority
.scope_for(&fleet_principal)
.await
.expect("scope_for");
let personas_columns = registered_columns(&fleet, "personas").await;
let mut in_scope: std::collections::HashSet<String> = std::collections::HashSet::new();
for table in CONVERSATION_CATALOG {
in_scope.extend(registered_columns(&scoped, table).await);
}
let fleet_only: Vec<&String> = personas_columns
.iter()
.filter(|column| !in_scope.contains(*column))
.collect();
assert!(
!fleet_only.is_empty(),
"every `personas` column is also in scope, so this assertion would pass vacuously"
);
for sql in [
"SELECT nope FROM events",
"SELECT turn_id FROM events ORDER BY nope",
"SELECT events.nope FROM events e",
"SELECT nope FROM events JOIN messages ON messages.turn_id = events.turn_id",
] {
let err = scoped.execute(sql).await.unwrap_err();
let ScopedQueryError::UnknownColumn(message) = err else {
panic!("`{sql}` must resolve to UnknownColumn: {err:?}");
};
for column in &fleet_only {
assert!(
!message.contains(column.as_str()),
"`{sql}`'s refusal must not name the out-of-scope `personas`.`{column}`: {message}"
);
}
}
fx.teardown().await;
}
#[tokio::test]
async fn a_genuine_engine_failure_still_collapses_into_internal() {
let fx = Fixture::build("grant-internal-still-internal").await;
let turn = uuid::Uuid::now_v7();
fx.eventlog
.append_batch(
"conv-a".to_owned(),
vec![
Event::new(kinds::tagged(kinds::TURN_START, &turn), Vec::new()),
Event::new(kinds::tagged(kinds::TURN_COMPLETE, &turn), Vec::new()),
],
)
.await
.expect("append conv-a");
let token = mint_conversation_grant(
&fx.signer.relabel_for_test(),
"a",
GrantSubject::Turn("turn-1".to_owned()),
NOW + TEST_TTL_MS,
);
let principal = fx
.authority
.verify_conversation_grant(&token, NOW)
.expect("valid grant");
let scoped = fx.authority.scope_for(&principal).await.expect("scope_for");
for sql in [
"SELECT no_such_function(turn_id) FROM events",
"SELECT turn_id + 1 FROM events",
] {
let err = scoped.execute(sql).await.unwrap_err();
assert!(
matches!(err, ScopedQueryError::Internal),
"`{sql}` is not a name the caller can correct and must stay Internal: {err:?}"
);
}
fx.teardown().await;
}
#[tokio::test]
async fn column_list_bound_covers_the_widest_registered_table() {
let fx = Fixture::build("column-bound").await;
let turn = uuid::Uuid::now_v7();
fx.eventlog
.append_batch(
"conv-column-bound".to_owned(),
vec![
Event::new(kinds::tagged(kinds::TURN_START, &turn), Vec::new()),
Event::new(kinds::tagged(kinds::TURN_COMPLETE, &turn), Vec::new()),
],
)
.await
.expect("append a committed turn");
let persona_id = fx.make_admin("hana").await;
let token = mint_session(
&fx.signer.relabel_for_test(),
&SessionSubject::Persona { persona_id },
&[SessionScope::ExplorerRead],
NOW,
TEST_TTL_MS,
);
let principal = fx
.authority
.verify_admin_session(&token, NOW)
.await
.expect("valid admin session");
let fleet = fx.authority.scope_for(&principal).await.expect("scope_for");
let listed = fleet
.execute("SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'")
.await
.expect("a Fleet session enumerates its own catalog");
assert!(
!listed.rows.is_empty(),
"the Fleet catalog enumeration returned nothing, so this bound would pass vacuously"
);
let mut widest = (String::new(), 0usize);
for row in &listed.rows {
let name = row[0]
.as_str()
.unwrap_or_else(|| panic!("information_schema.tables.table_name must be text: {row:?}"))
.to_owned();
let count = registered_columns(&fleet, &name).await.len();
if count > widest.1 {
widest = (name, count);
}
}
assert!(
widest.1 <= MAX_ADVERTISED_COLUMNS,
"`{}` registers {} columns, past the {MAX_ADVERTISED_COLUMNS} an UnknownColumn message \
spells out — raise the bound or accept that a miss against this table is truncated",
widest.0,
widest.1
);
let widest_columns = registered_columns(&fleet, &widest.0).await;
let err = fleet
.execute(&format!("SELECT * FROM {} ORDER BY nope", widest.0))
.await
.unwrap_err();
let ScopedQueryError::UnknownColumn(message) = err else {
panic!(
"an `ORDER BY` miss on `{}` must be UnknownColumn: {err:?}",
widest.0
);
};
let mut offered = offered_columns(&message);
offered.sort();
let mut expected = widest_columns;
expected.sort();
assert_eq!(
offered, expected,
"an `ORDER BY` miss against the widest table must offer its whole column set, each once \
— a duplicated list is both wrong and past the {MAX_ADVERTISED_COLUMNS} bound: {message}"
);
assert!(
!message.contains(" more"),
"a miss against a SINGLE table must never truncate: {message}"
);
fx.teardown().await;
}
#[tokio::test]
async fn fleet_query_over_source_budget_is_rejected_before_decode() {
let fx = Fixture::build("fleet-source-budget").await;
let persona_id = fx.make_admin("frank").await;
let turn = uuid::Uuid::now_v7();
fx.eventlog
.append_batch(
"conv-budget".to_owned(),
vec![
Event::new(kinds::tagged(kinds::TURN_START, &turn), Vec::new()),
Event::new(kinds::tagged(kinds::TURN_COMPLETE, &turn), Vec::new()),
Event::new("k2".to_owned(), Vec::new()),
Event::new("k3".to_owned(), Vec::new()),
Event::new("k4".to_owned(), Vec::new()),
Event::new("k5".to_owned(), Vec::new()),
],
)
.await
.expect("append conv-budget");
let limited_authority = fx.authority_with_limits(QueryLimits {
max_source_events: 4,
..QueryLimits::default()
});
let token = mint_session(
&fx.signer.relabel_for_test(),
&SessionSubject::Persona { persona_id },
&[SessionScope::ExplorerRead],
NOW,
TEST_TTL_MS,
);
let principal = limited_authority
.verify_admin_session(&token, NOW)
.await
.expect("valid admin session");
let scoped = limited_authority
.scope_for(&principal)
.await
.expect("admin always scopes");
let decode_calls_before =
crate::engine::DECODE_CALL_COUNT.load(std::sync::atomic::Ordering::SeqCst);
let err = scoped
.execute("SELECT COUNT(*) AS c FROM events_raw")
.await
.unwrap_err();
let decode_calls_after =
crate::engine::DECODE_CALL_COUNT.load(std::sync::atomic::Ordering::SeqCst);
assert!(
matches!(err, ScopedQueryError::SourceBudgetExceeded(_)),
"6 replayed events over a 4-event budget must be rejected before decode: {err:?}"
);
assert_eq!(
decode_calls_after, decode_calls_before,
"decode_partition_tables (the genuine decode fan-out entry point) must never be reached \
when the source budget rejects a query — no raw or typed batch may be constructed"
);
let message = err.to_string();
assert!(
!message.contains('6') && !message.contains('4'),
"the rejection message must not leak the actual or budgeted event count: {message:?}"
);
assert!(
message.contains("query_max_source_events"),
"a Fleet rejection must name the config an administrator can raise: {message:?}"
);
fx.teardown().await;
}
#[tokio::test]
async fn conversation_grant_query_over_source_budget_is_rejected_before_decode() {
let fx = Fixture::build("grant-source-budget").await;
let turn = uuid::Uuid::now_v7();
fx.eventlog
.append_batch(
"conv-a".to_owned(),
vec![
Event::new(kinds::tagged(kinds::TURN_START, &turn), Vec::new()),
Event::new(kinds::tagged(kinds::TURN_COMPLETE, &turn), Vec::new()),
Event::new("k2".to_owned(), Vec::new()),
],
)
.await
.expect("append conv-a");
let limited_authority = fx.authority_with_limits(QueryLimits {
max_source_events: 1,
..QueryLimits::default()
});
let token = mint_conversation_grant(
&fx.signer.relabel_for_test(),
"a",
GrantSubject::Turn("turn-1".to_owned()),
NOW + TEST_TTL_MS,
);
let principal = limited_authority
.verify_conversation_grant(&token, NOW)
.expect("valid grant");
let scoped = limited_authority
.scope_for(&principal)
.await
.expect("grant always scopes");
let decode_calls_before =
crate::engine::DECODE_CALL_COUNT.load(std::sync::atomic::Ordering::SeqCst);
let err = scoped
.execute("SELECT COUNT(*) AS c FROM events")
.await
.unwrap_err();
let decode_calls_after =
crate::engine::DECODE_CALL_COUNT.load(std::sync::atomic::Ordering::SeqCst);
assert!(
matches!(err, ScopedQueryError::SourceBudgetExceeded(_)),
"3 replayed events over a 1-event budget must be rejected before decode: {err:?}"
);
assert_eq!(
decode_calls_after, decode_calls_before,
"decode_partition_tables must never be reached when a conversation-grant query trips the \
source budget"
);
let message = err.to_string();
assert_eq!(
message, CONVERSATION_BUDGET_EXCEEDED_MESSAGE,
"a Conversations-scope rejection must use the caller's-own-history message, not the \
Fleet one, and must carry no count"
);
assert!(
!message.contains("query_max_source_events"),
"a Conversations-scope caller cannot raise a fleet-wide admin setting, so the message \
must not point at one: {message:?}"
);
fx.teardown().await;
}
#[tokio::test]
async fn replay_scoped_partitions_aborts_before_reading_every_fleet_partition() {
let fx = Fixture::build("fleet-bytes-early-abort").await;
let persona_id = fx.make_admin("early-abort").await;
fx.eventlog
.append_batch("conv-a".to_owned(), vec![Event::new("k0", vec![0u8; 10])])
.await
.expect("append conv-a");
fx.eventlog
.append_batch(
"conv-b".to_owned(),
(0..20)
.map(|i| Event::new(format!("k{i}"), vec![0u8; 1_000]))
.collect(),
)
.await
.expect("append conv-b");
fx.eventlog
.append_batch("conv-c".to_owned(), vec![Event::new("k0", vec![0u8; 10])])
.await
.expect("append conv-c");
let limited_authority = fx.authority_with_limits(QueryLimits {
max_source_bytes: 5_000,
..QueryLimits::default()
});
let token = mint_session(
&fx.signer.relabel_for_test(),
&SessionSubject::Persona { persona_id },
&[SessionScope::ExplorerRead],
NOW,
TEST_TTL_MS,
);
let principal = limited_authority
.verify_admin_session(&token, NOW)
.await
.expect("valid admin session");
let scoped = limited_authority
.scope_for(&principal)
.await
.expect("admin always scopes");
let err = scoped
.replay_scoped_partitions()
.await
.expect_err("20,000 bytes in conv-b alone must exceed the 5,000 byte budget");
match err {
ReplayError::BytesBudgetExceeded {
partitions_replayed,
bytes_read,
} => {
assert_eq!(
partitions_replayed, 2,
"must abort right after conv-b (the partition that tripped the budget) — \
reading conv-c too (3) would mean the whole Fleet scope was materialized \
before the budget was ever checked"
);
assert!(
partitions_replayed < 3,
"3 partitions exist in this deployment; the replay must stop strictly before \
the last one once the budget trips"
);
assert!(
bytes_read >= 5_000,
"bytes_read is the accumulated size at the abort — at or just past the 5,000 \
byte budget, recorded to the replayed-bytes histogram so over-budget queries \
are visible: {bytes_read}"
);
}
ReplayError::Internal => panic!("expected a byte-budget rejection, got Internal"),
}
fx.teardown().await;
}
#[tokio::test]
async fn fleet_query_over_bytes_budget_is_rejected_before_decode() {
let fx = Fixture::build("fleet-bytes-budget").await;
let persona_id = fx.make_admin("gina").await;
fx.eventlog
.append_batch(
"conv-bytes".to_owned(),
(0..5)
.map(|i| Event::new(format!("k{i}"), vec![0u8; 1_000]))
.collect(),
)
.await
.expect("append conv-bytes");
let limited_authority = fx.authority_with_limits(QueryLimits {
max_source_bytes: 500,
..QueryLimits::default()
});
let token = mint_session(
&fx.signer.relabel_for_test(),
&SessionSubject::Persona { persona_id },
&[SessionScope::ExplorerRead],
NOW,
TEST_TTL_MS,
);
let principal = limited_authority
.verify_admin_session(&token, NOW)
.await
.expect("valid admin session");
let scoped = limited_authority
.scope_for(&principal)
.await
.expect("admin always scopes");
let build_calls_before =
crate::engine::BUILD_CALL_COUNT.load(std::sync::atomic::Ordering::SeqCst);
let err = scoped
.execute("SELECT COUNT(*) AS c FROM events_raw")
.await
.unwrap_err();
let build_calls_after =
crate::engine::BUILD_CALL_COUNT.load(std::sync::atomic::Ordering::SeqCst);
assert!(
matches!(err, ScopedQueryError::SourceBudgetExceeded(_)),
"5,000 replayed bytes over a 500-byte budget must be rejected before decode: {err:?}"
);
assert_eq!(
build_calls_after, build_calls_before,
"QueryEngine::build must never be reached when the byte budget rejects a query"
);
let message = err.to_string();
assert_eq!(
message, FLEET_BYTES_BUDGET_EXCEEDED_MESSAGE,
"a Fleet byte-budget rejection must use the byte-budget message, not the event-count one"
);
assert!(
message.contains("query_max_source_bytes"),
"a Fleet rejection must name the config an administrator can raise: {message:?}"
);
assert!(
!message.contains("500") && !message.contains("5000"),
"the rejection message must not leak the actual or budgeted byte count: {message:?}"
);
fx.teardown().await;
}
#[tokio::test]
async fn conversation_grant_query_over_bytes_budget_is_rejected_before_decode() {
let fx = Fixture::build("grant-bytes-budget").await;
fx.eventlog
.append_batch(
"conv-a".to_owned(),
(0..5)
.map(|i| Event::new(format!("k{i}"), vec![0u8; 1_000]))
.collect(),
)
.await
.expect("append conv-a");
let limited_authority = fx.authority_with_limits(QueryLimits {
max_source_bytes: 500,
..QueryLimits::default()
});
let token = mint_conversation_grant(
&fx.signer.relabel_for_test(),
"a",
GrantSubject::Turn("turn-1".to_owned()),
NOW + TEST_TTL_MS,
);
let principal = limited_authority
.verify_conversation_grant(&token, NOW)
.expect("valid grant");
let scoped = limited_authority
.scope_for(&principal)
.await
.expect("grant always scopes");
let build_calls_before =
crate::engine::BUILD_CALL_COUNT.load(std::sync::atomic::Ordering::SeqCst);
let err = scoped
.execute("SELECT COUNT(*) AS c FROM events")
.await
.unwrap_err();
let build_calls_after =
crate::engine::BUILD_CALL_COUNT.load(std::sync::atomic::Ordering::SeqCst);
assert!(
matches!(err, ScopedQueryError::SourceBudgetExceeded(_)),
"5,000 replayed bytes over a 500-byte budget must be rejected before decode: {err:?}"
);
assert_eq!(
build_calls_after, build_calls_before,
"QueryEngine::build must never be reached when a conversation-grant query trips the \
byte budget"
);
let message = err.to_string();
assert_eq!(
message, CONVERSATION_BYTES_BUDGET_EXCEEDED_MESSAGE,
"a Conversations-scope byte-budget rejection must use the caller's-own-history message"
);
assert!(
!message.contains("query_max_source_bytes"),
"a Conversations-scope caller cannot raise a fleet-wide admin setting, so the message \
must not point at one: {message:?}"
);
fx.teardown().await;
}
#[tokio::test]
async fn conversation_grant_query_under_bytes_budget_succeeds_normally() {
let fx = Fixture::build("grant-bytes-under-budget").await;
let turn = uuid::Uuid::now_v7();
fx.eventlog
.append_batch(
"conv-a".to_owned(),
vec![
Event::new(kinds::tagged(kinds::TURN_START, &turn), Vec::new()),
Event::new(kinds::tagged(kinds::TURN_COMPLETE, &turn), Vec::new()),
],
)
.await
.expect("append conv-a");
let authority_with_generous_bytes = fx.authority_with_limits(QueryLimits {
max_source_bytes: 1_000_000,
..QueryLimits::default()
});
let token = mint_conversation_grant(
&fx.signer.relabel_for_test(),
"a",
GrantSubject::Turn("turn-1".to_owned()),
NOW + TEST_TTL_MS,
);
let principal = authority_with_generous_bytes
.verify_conversation_grant(&token, NOW)
.expect("valid grant");
let scoped = authority_with_generous_bytes
.scope_for(&principal)
.await
.expect("grant always scopes");
let output = scoped
.execute("SELECT COUNT(*) AS c FROM events")
.await
.expect("a scope genuinely under the byte budget must run normally");
assert!(
!output.rows.is_empty(),
"a normal query under budget must still return its real result: {output:?}"
);
fx.teardown().await;
}
#[tokio::test]
async fn execute_records_query_size_and_budget_rejection_metrics() {
use prometheus::{Encoder as _, TextEncoder};
fn scrape() -> String {
let mut buf = Vec::new();
TextEncoder::new()
.encode(&prometheus::default_registry().gather(), &mut buf)
.expect("encode");
String::from_utf8(buf).expect("utf8")
}
fn counter_value(text: &str, metric: &str, scope: &str) -> f64 {
let needle = format!("{metric}{{scope=\"{scope}\"}} ");
text.lines()
.find(|line| line.starts_with(&needle))
.and_then(|line| line.rsplit(' ').next())
.and_then(|v| v.parse::<f64>().ok())
.unwrap_or(0.0)
}
let fx = Fixture::build("metrics-observed").await;
let persona_id = fx.make_admin("mira").await;
let turn = uuid::Uuid::now_v7();
fx.eventlog
.append_batch(
"conv-metrics".to_owned(),
vec![
Event::new(kinds::tagged(kinds::TURN_START, &turn), Vec::new()),
Event::new(kinds::tagged(kinds::TURN_COMPLETE, &turn), Vec::new()),
],
)
.await
.expect("append conv-metrics");
let token = mint_session(
&fx.signer.relabel_for_test(),
&SessionSubject::Persona { persona_id },
&[SessionScope::ExplorerRead],
NOW,
TEST_TTL_MS,
);
let principal = fx
.authority
.verify_admin_session(&token, NOW)
.await
.expect("valid admin session");
let scoped = fx
.authority
.scope_for(&principal)
.await
.expect("admin always scopes");
let before_rejections = counter_value(
&scrape(),
"polychrome_query_source_budget_exceeded_total",
"fleet",
);
let sum_before = source_events_sum(&scrape());
scoped
.execute("SELECT COUNT(*) AS c FROM events")
.await
.expect("under-budget admin query");
let sum_after = source_events_sum(&scrape());
assert!(
sum_after > sum_before,
"a successful query must still advance polychrome_query_source_events's sum: \
before={sum_before} after={sum_after}"
);
let limited_authority = fx.authority_with_limits(QueryLimits {
max_source_events: 1,
..QueryLimits::default()
});
let limited_scoped = limited_authority
.scope_for(&principal)
.await
.expect("admin always scopes");
let err = limited_scoped
.execute("SELECT COUNT(*) AS c FROM events_raw")
.await
.unwrap_err();
assert!(matches!(err, ScopedQueryError::SourceBudgetExceeded(_)));
let after_rejections = counter_value(
&scrape(),
"polychrome_query_source_budget_exceeded_total",
"fleet",
);
assert_eq!(
after_rejections - before_rejections,
1.0,
"polychrome_query_source_budget_exceeded_total{{scope=\"fleet\"}} must increment by \
exactly 1 per rejection"
);
fx.teardown().await;
}
#[cfg(test)]
fn source_events_sum(text: &str) -> f64 {
text.lines()
.filter(|line| line.starts_with("polychrome_query_source_events_sum"))
.filter_map(|line| line.rsplit(' ').next())
.filter_map(|v| v.parse::<f64>().ok())
.sum()
}
#[tokio::test]
async fn build_base_session_state_configures_a_bounded_spill_directory() {
let spill_root = std::env::temp_dir().join("polyc-query-authority-tests-spill");
let quota_bytes = 2 * 1024 * 1024 * 1024;
let state = build_base_session_state(64 * 1024 * 1024, &spill_root, quota_bytes);
let disk_manager = state.runtime_env().disk_manager.clone();
assert!(
disk_manager.tmp_files_enabled(),
"spilling must stay enabled, just bounded — never silently disabled"
);
assert_eq!(
disk_manager.max_temp_directory_size(),
quota_bytes,
"the configured quota must be the caller-supplied value, not DataFusion's own 100GiB \
default"
);
let temp_dir_paths = disk_manager.temp_dir_paths();
assert_eq!(
temp_dir_paths.len(),
1,
"exactly one configured spill root is configured: {temp_dir_paths:?}"
);
assert!(
temp_dir_paths[0].starts_with(&spill_root),
"the spill dir must live under the caller-supplied root {spill_root:?}, got {:?}",
temp_dir_paths[0]
);
}
#[tokio::test]
async fn fleet_replay_admits_the_routine_scheduler_partition_but_no_other_non_conversation_partition()
{
let fx = Fixture::build("scheduler-partition-admission").await;
let turn = uuid::Uuid::now_v7();
fx.eventlog
.append_batch(
"conv-a".to_owned(),
vec![Event::new(
kinds::tagged(kinds::TURN_START, &turn),
Vec::new(),
)],
)
.await
.expect("append conv-a");
let fired = RoutineFiredEvent {
routine: "daily-standup".to_owned(),
occurrence: "daily-standup-1".to_owned(),
scheduled_at_ms: 1,
fired_at_ms: 2,
..Default::default()
};
fx.eventlog
.append_batch(
"routine-scheduler".to_owned(),
vec![Event::trusted(kinds::ROUTINE_FIRED, fired.encode_to_vec())],
)
.await
.expect("append routine-scheduler");
fx.eventlog
.append_batch(
"some-other-partition".to_owned(),
vec![Event::new(kinds::USAGE.to_owned(), Vec::new())],
)
.await
.expect("append some-other-partition");
let persona_id = fx.make_admin("grace").await;
let token = mint_session(
&fx.signer.relabel_for_test(),
&SessionSubject::Persona {
persona_id: persona_id.clone(),
},
&[SessionScope::ExplorerRead],
NOW,
TEST_TTL_MS,
);
let principal = fx
.authority
.verify_admin_session(&token, NOW)
.await
.expect("valid admin session");
let scoped = fx.authority.scope_for(&principal).await.expect("scope_for");
let result = scoped
.execute("SELECT DISTINCT partition FROM events_raw ORDER BY partition")
.await
.expect("fleet query over events_raw");
assert_eq!(
result.rows,
vec![
vec![serde_json::json!("conv-a")],
vec![serde_json::json!("routine-scheduler")],
],
"exactly conv-a and routine-scheduler must be admitted — some-other-partition must \
never appear: {:?}",
result.rows
);
fx.teardown().await;
}
struct FakeRoutineCatalog(Vec<RoutineStatusRecord>);
#[async_trait::async_trait]
impl RoutineCatalog for FakeRoutineCatalog {
async fn list_routines(&self) -> Result<Vec<RoutineStatusRecord>, RoutineCatalogError> {
Ok(self.0.clone())
}
}
#[tokio::test]
async fn fleet_execute_resolves_routines_through_the_supplied_routine_catalog() {
let fx = Fixture::build("routine-catalog-wiring").await;
let catalog = Arc::new(FakeRoutineCatalog(vec![RoutineStatusRecord {
name: "daily-standup".to_owned(),
uid: "uid-daily-standup".to_owned(),
fire_conversation_id: "fire-conv-daily-standup".to_owned(),
ready: true,
phase: Some("Ready".to_owned()),
message: None,
last_fire_time_ms: None,
next_fire_time_ms: Some(1_784_883_600_000),
conditions_json: "[]".to_owned(),
creator_persona: "persona-1".to_owned(),
provenance_conversation_id: "conv-1".to_owned(),
schedule_json: r#"{"kind":"cron","expression":"0 9 * * *","timezone":null}"#.to_owned(),
next_fires_json: r"[1784883600000]".to_owned(),
suspended: false,
paused_by: None,
paused_at_ms: None,
pause_reason: None,
prompt: "post the morning standup".to_owned(),
scope: "private".to_owned(),
orphaned: false,
display_name: String::new(),
description: String::new(),
schedule_timezone: "UTC".to_owned(),
}]));
let authority = fx.authority_with_routine_catalog(catalog);
let persona_id = fx.make_admin("henry").await;
let token = mint_session(
&fx.signer.relabel_for_test(),
&SessionSubject::Persona {
persona_id: persona_id.clone(),
},
&[SessionScope::ExplorerRead],
NOW,
TEST_TTL_MS,
);
let principal = authority
.verify_admin_session(&token, NOW)
.await
.expect("valid admin session");
let scoped = authority.scope_for(&principal).await.expect("scope_for");
let result = scoped
.execute("SELECT name, ready, next_fire_time_ms FROM routines")
.await
.expect("fleet query over routines");
assert_eq!(
result.rows,
vec![vec![
serde_json::json!("daily-standup"),
serde_json::json!(true),
serde_json::json!(1_784_883_600_000_i64),
]]
);
fx.teardown().await;
}
#[tokio::test]
async fn persona_scoped_routines_and_fires_see_only_the_callers_own_routine() {
let fx = Fixture::build("owner-scoped-routines").await;
let persona_a = fx.make_non_admin("alice-owner").await;
let persona_b = fx.make_non_admin("bob-owner").await;
let catalog = Arc::new(FakeRoutineCatalog(vec![
routine_record("routine-a", &persona_a),
routine_record("routine-b", &persona_b),
]));
let authority = fx.authority_with_routine_catalog(catalog);
for (routine, occurrence) in [("routine-a", "routine-a-1"), ("routine-b", "routine-b-1")] {
let fired = RoutineFiredEvent {
routine: routine.to_owned(),
occurrence: occurrence.to_owned(),
scheduled_at_ms: 1,
fired_at_ms: 2,
routine_uid: format!("{routine}-uid"),
..Default::default()
};
fx.eventlog
.append_batch(
"routine-scheduler".to_owned(),
vec![Event::trusted(kinds::ROUTINE_FIRED, fired.encode_to_vec())],
)
.await
.expect("append routine-scheduler fire");
}
async fn scope_for_persona(
authority: &QueryAuthority,
signer: &SessionSigner,
persona_id: &str,
) -> ScopedQuery {
let token = mint_session(
signer,
&SessionSubject::Persona {
persona_id: persona_id.to_owned(),
},
&[SessionScope::ExplorerRead],
NOW,
TEST_TTL_MS,
);
let principal = authority
.verify_admin_session(&token, NOW)
.await
.expect("valid session");
assert!(
matches!(principal, Principal::Persona(_)),
"a non-admin persona must mint Principal::Persona, got {principal:?}"
);
authority.scope_for(&principal).await.expect("scope_for")
}
let scoped_a = scope_for_persona(&authority, &fx.signer.relabel_for_test(), &persona_a).await;
let routines_a = scoped_a
.execute("SELECT name FROM routines ORDER BY name")
.await
.expect("persona a's own routines query");
assert_eq!(
routines_a.rows,
vec![vec![serde_json::json!("routine-a")]],
"persona a must see exactly their own routine, never bob's"
);
assert_eq!(
routines_a.columns,
vec!["name"],
"columns must come from the planned schema, not merely be non-empty because rows exist"
);
let fires_a = scoped_a
.execute("SELECT routine FROM fires ORDER BY routine")
.await
.expect("persona a's own fires query");
assert_eq!(
fires_a.rows,
vec![vec![serde_json::json!("routine-a")]],
"persona a must see exactly their own routine's fire, never bob's"
);
assert_eq!(fires_a.columns, vec!["routine"]);
let scoped_b = scope_for_persona(&authority, &fx.signer.relabel_for_test(), &persona_b).await;
let routines_b = scoped_b
.execute("SELECT name FROM routines ORDER BY name")
.await
.expect("persona b's own routines query");
assert_eq!(
routines_b.rows,
vec![vec![serde_json::json!("routine-b")]],
"persona b must see exactly their own routine, never alice's"
);
assert_eq!(routines_b.columns, vec!["name"]);
let fires_b = scoped_b
.execute("SELECT routine FROM fires ORDER BY routine")
.await
.expect("persona b's own fires query");
assert_eq!(
fires_b.rows,
vec![vec![serde_json::json!("routine-b")]],
"persona b must see exactly their own routine's fire, never alice's"
);
assert_eq!(fires_b.columns, vec!["routine"]);
let admin_persona = fx.make_admin("carol-admin").await;
let admin_token = mint_session(
&fx.signer.relabel_for_test(),
&SessionSubject::Persona {
persona_id: admin_persona,
},
&[SessionScope::ExplorerRead],
NOW,
TEST_TTL_MS,
);
let admin_principal = authority
.verify_admin_session(&admin_token, NOW)
.await
.expect("valid admin session");
let scoped_fleet = authority
.scope_for(&admin_principal)
.await
.expect("scope_for");
let routines_fleet = scoped_fleet
.execute("SELECT name FROM routines ORDER BY name")
.await
.expect("fleet routines query");
assert_eq!(
routines_fleet.rows,
vec![
vec![serde_json::json!("routine-a")],
vec![serde_json::json!("routine-b")],
],
"fleet must see every persona's routine, unfiltered"
);
let fires_fleet = scoped_fleet
.execute("SELECT routine FROM fires ORDER BY routine")
.await
.expect("fleet fires query");
assert_eq!(
fires_fleet.rows,
vec![
vec![serde_json::json!("routine-a")],
vec![serde_json::json!("routine-b")],
],
"fleet must see every routine's fire, unfiltered"
);
fx.teardown().await;
}
#[tokio::test]
async fn routine_overview_last_fire_breaks_a_fired_at_ms_tie_by_journal_position() {
let fx = Fixture::build("routine-overview-fire-tie").await;
let owner = fx.make_non_admin("dana-owner").await;
let catalog = Arc::new(FakeRoutineCatalog(vec![routine_record(
"routine-tie",
&owner,
)]));
let authority = fx.authority_with_routine_catalog(catalog);
for (occurrence, outcome) in [
(
"routine-tie-1",
polyc_proto::proto::polychrome::events::v1::RoutineFireOutcome::Ok,
),
(
"routine-tie-2",
polyc_proto::proto::polychrome::events::v1::RoutineFireOutcome::StoppedUngranted,
),
] {
let fired = RoutineFiredEvent {
routine: "routine-tie".to_owned(),
occurrence: occurrence.to_owned(),
scheduled_at_ms: 1,
fired_at_ms: 2,
routine_uid: "routine-tie-uid".to_owned(),
..Default::default()
};
let outcome_ev = polyc_proto::proto::polychrome::events::v1::RoutineFireOutcomeEvent {
routine: fired.routine.clone(),
occurrence: fired.occurrence.clone(),
outcome: outcome.into(),
fired_at_ms: fired.fired_at_ms,
..Default::default()
};
fx.eventlog
.append_batch(
"routine-scheduler".to_owned(),
vec![
Event::trusted(kinds::ROUTINE_FIRED, fired.encode_to_vec()),
Event::trusted(kinds::ROUTINE_FIRE_OUTCOME, outcome_ev.encode_to_vec()),
],
)
.await
.expect("append scheduler fire+outcome");
}
let token = mint_session(
&fx.signer.relabel_for_test(),
&SessionSubject::Persona {
persona_id: owner.clone(),
},
&[SessionScope::ExplorerRead],
NOW,
TEST_TTL_MS,
);
let principal = authority
.verify_admin_session(&token, NOW)
.await
.expect("valid session");
let scoped = authority.scope_for(&principal).await.expect("scope_for");
let overview = scoped
.execute("SELECT last_fire_outcome FROM routine_overview WHERE name = 'routine-tie'")
.await
.expect("overview query");
assert_eq!(
overview.rows,
vec![vec![serde_json::json!("stopped_ungranted")]],
"the LATER-APPENDED fire (higher journal position) must win an equal-fired_at_ms tie"
);
fx.teardown().await;
}
#[tokio::test]
async fn persona_scoped_routines_and_fires_are_empty_not_an_error_for_an_owner_with_none() {
let fx = Fixture::build("owner-scoped-routines-empty").await;
let persona_a = fx.make_non_admin("dana-no-routines").await;
let persona_b = fx.make_non_admin("erin-owner").await;
let catalog = Arc::new(FakeRoutineCatalog(vec![routine_record(
"erins-routine",
&persona_b,
)]));
let authority = fx.authority_with_routine_catalog(catalog);
let token = mint_session(
&fx.signer.relabel_for_test(),
&SessionSubject::Persona {
persona_id: persona_a.clone(),
},
&[SessionScope::ExplorerRead],
NOW,
TEST_TTL_MS,
);
let principal = authority
.verify_admin_session(&token, NOW)
.await
.expect("valid session");
let scoped = authority.scope_for(&principal).await.expect("scope_for");
let routines = scoped
.execute("SELECT name FROM routines")
.await
.expect("a routine-owner-less persona's routines query must still succeed");
assert!(
routines.rows.is_empty(),
"an owner with no routines must see zero rows, not erin's: {:?}",
routines.rows
);
assert_eq!(
routines.columns,
vec!["name"],
"an empty result must still carry its real column list (issue #1916), not an empty one"
);
let fires = scoped
.execute("SELECT routine FROM fires")
.await
.expect("a routine-owner-less persona's fires query must still succeed");
assert!(
fires.rows.is_empty(),
"an owner with no routines must see zero fires, not erin's: {:?}",
fires.rows
);
assert_eq!(
fires.columns,
vec!["routine"],
"an empty result must still carry its real column list (issue #1916), not an empty one"
);
let err = scoped.execute("SELECT * FROM turns").await.unwrap_err();
assert!(
matches!(err, ScopedQueryError::UnknownTable(_)),
"a persona session must get UnknownTable for a name it invented: {err:?}"
);
assert_eq!(err.to_string(), *OWNER_UNKNOWN_TABLE_MESSAGE);
for table in CONVERSATION_CATALOG.iter().chain(OWNER_ONLY_CATALOG.iter()) {
assert!(
OWNER_UNKNOWN_TABLE_MESSAGE.contains(table),
"`{table}` is registered for a persona session but the message does not name it"
);
scoped
.execute(&format!("SELECT COUNT(*) AS c FROM {table}"))
.await
.unwrap_or_else(|err| panic!("advertised table `{table}` must resolve: {err:?}"));
}
fx.teardown().await;
}
#[tokio::test]
async fn persona_scoped_fires_inner_join_eliminates_every_row_but_still_reports_columns() {
let fx = Fixture::build("owner-scoped-fires-join-elimination").await;
let persona_a = fx.make_non_admin("frank-owns-unfired-routine").await;
let persona_b = fx.make_non_admin("grace-owns-fired-routine").await;
let catalog = Arc::new(FakeRoutineCatalog(vec![
routine_record("routine-a", &persona_a),
routine_record("routine-b", &persona_b),
]));
let authority = fx.authority_with_routine_catalog(catalog);
let fired = RoutineFiredEvent {
routine: "routine-b".to_owned(),
occurrence: "routine-b-1".to_owned(),
scheduled_at_ms: 1,
fired_at_ms: 2,
routine_uid: "routine-b-uid".to_owned(),
..Default::default()
};
fx.eventlog
.append_batch(
"routine-scheduler".to_owned(),
vec![Event::trusted(kinds::ROUTINE_FIRED, fired.encode_to_vec())],
)
.await
.expect("append routine-scheduler fire");
let token = mint_session(
&fx.signer.relabel_for_test(),
&SessionSubject::Persona {
persona_id: persona_a.clone(),
},
&[SessionScope::ExplorerRead],
NOW,
TEST_TTL_MS,
);
let principal = authority
.verify_admin_session(&token, NOW)
.await
.expect("valid session");
let scoped = authority.scope_for(&principal).await.expect("scope_for");
let routines = scoped
.execute("SELECT name FROM routines")
.await
.expect("persona a's own routines query");
assert_eq!(routines.rows, vec![vec![serde_json::json!("routine-a")]]);
let fires = scoped
.execute("SELECT routine FROM fires")
.await
.expect("persona a's fires query, joined against a routine they own");
assert!(
fires.rows.is_empty(),
"persona a's own routine never fired, so the inner join must eliminate every row: {:?}",
fires.rows
);
assert_eq!(
fires.columns,
vec!["routine"],
"the inner join eliminating every row must not erase the planned column list (issue #1916)"
);
fx.teardown().await;
}
fn scrape_metrics() -> String {
use prometheus::Encoder as _;
let mut buf = Vec::new();
prometheus::TextEncoder::new()
.encode(&prometheus::default_registry().gather(), &mut buf)
.expect("encode");
String::from_utf8(buf).expect("utf8")
}
fn counter_value(text: &str, metric: &str) -> f64 {
let prefix = format!("{metric} ");
text.lines()
.find(|line| line.starts_with(&prefix))
.and_then(|line| line.rsplit(' ').next())
.and_then(|v| v.parse::<f64>().ok())
.unwrap_or(0.0)
}
async fn admin_scoped(fx: &Fixture, authority: &QueryAuthority, persona_id: String) -> ScopedQuery {
let token = mint_session(
&fx.signer.relabel_for_test(),
&SessionSubject::Persona { persona_id },
&[SessionScope::ExplorerRead],
NOW,
TEST_TTL_MS,
);
let principal = authority
.verify_admin_session(&token, NOW)
.await
.expect("valid admin session");
authority
.scope_for(&principal)
.await
.expect("admin always scopes")
}
#[tokio::test]
async fn second_identical_query_is_served_from_the_cache() {
let fx = Fixture::build("cache-hit").await;
let persona_id = fx.make_admin("cache-hit-admin").await;
fx.eventlog
.append_batch("conv-hit".to_owned(), vec![Event::new("k0", Vec::new())])
.await
.expect("append");
let cached_authority = fx.authority_with_cache_config(CacheConfig::new(Some(1)));
let scoped = admin_scoped(&fx, &cached_authority, persona_id).await;
let hits_before = counter_value(&scrape_metrics(), "polychrome_query_cache_hit_total");
scoped
.execute("SELECT COUNT(*) AS c FROM events_raw")
.await
.expect("first query (miss)");
scoped
.execute("SELECT COUNT(*) AS c FROM events_raw")
.await
.expect("second, identical query (hit)");
let hits_after = counter_value(&scrape_metrics(), "polychrome_query_cache_hit_total");
assert_eq!(
hits_after - hits_before,
1.0,
"the second identical query over an unchanged partition must be a single cache hit"
);
fx.teardown().await;
}
#[tokio::test]
async fn append_then_query_replays_only_the_tail() {
let fx = Fixture::build("cache-tail").await;
let persona_id = fx.make_admin("cache-tail-admin").await;
fx.eventlog
.append_batch("conv-tail".to_owned(), vec![Event::new("k0", Vec::new())])
.await
.expect("append first event");
let cached_authority = fx.authority_with_cache_config(CacheConfig::new(Some(1)));
let scoped = admin_scoped(&fx, &cached_authority, persona_id).await;
scoped
.execute("SELECT COUNT(*) AS c FROM events_raw")
.await
.expect("first query (miss, populates the cache)");
fx.eventlog
.append_batch("conv-tail".to_owned(), vec![Event::new("k1", Vec::new())])
.await
.expect("append second event");
let tails_before = counter_value(&scrape_metrics(), "polychrome_query_cache_tail_total");
let result = scoped
.execute("SELECT COUNT(*) AS c FROM events_raw WHERE kind_base IN ('k0', 'k1')")
.await
.expect("second query (tail)");
let tails_after = counter_value(&scrape_metrics(), "polychrome_query_cache_tail_total");
assert_eq!(
tails_after - tails_before,
1.0,
"an append onto an already-cached partition must resolve as exactly one tail, not a full \
rebuild"
);
assert_eq!(
result.rows,
vec![vec![serde_json::json!(2)]],
"the merged (cached base + replayed tail) result must count both events"
);
fx.teardown().await;
}
#[tokio::test]
async fn cache_tail_race_does_not_duplicate_rows_across_an_interleaved_append() {
let fx = Fixture::build("cache-race").await;
let persona_id = fx.make_admin("race-admin").await;
fx.eventlog
.append_batch(
"conv-race".to_owned(),
(0..3)
.map(|i| Event::new(format!("k{i}"), Vec::new()))
.collect(),
)
.await
.expect("append initial 3 events");
let cached_authority = fx.authority_with_cache_config(CacheConfig::new(Some(1)));
let scoped = admin_scoped(&fx, &cached_authority, persona_id).await;
scoped
.execute("SELECT COUNT(*) AS c FROM events_raw")
.await
.expect("first query (miss, populates the cache)");
fx.eventlog
.append_batch(
"conv-race".to_owned(),
(3..5)
.map(|i| Event::new(format!("k{i}"), Vec::new()))
.collect(),
)
.await
.expect("append 2 more events");
*scoped.race_inject_after_count_read.lock().expect("poison") = Some((
"conv-race".to_owned(),
(5..7)
.map(|i| Event::new(format!("k{i}"), Vec::new()))
.collect(),
));
scoped
.execute("SELECT COUNT(*) AS c FROM events_raw")
.await
.expect("second query (tail, racing an interleaved append)");
let result = scoped
.execute(
"SELECT COUNT(*) AS c FROM events_raw WHERE kind_base IN \
('k0','k1','k2','k3','k4','k5','k6')",
)
.await
.expect("third query");
assert_eq!(
result.rows,
vec![vec![serde_json::json!(7)]],
"exactly 7 real events were ever appended to conv-race across this test — a cached-tail \
race must never duplicate any of them: {result:?}"
);
fx.teardown().await;
}
#[tokio::test]
async fn cache_never_serves_a_payload_erased_by_an_in_place_rewrite() {
let fx = Fixture::build("cache-erasure").await;
let persona_id = fx.make_admin("erasure-admin").await;
fx.eventlog
.append_batch(
"conv-erasure".to_owned(),
vec![Event::new(
kinds::APPROVAL_DEFERRED,
br#"{"marker":"before"}"#.to_vec(),
)],
)
.await
.expect("append");
let cached_authority = fx.authority_with_cache_config(CacheConfig::new(Some(1)));
let scoped = admin_scoped(&fx, &cached_authority, persona_id).await;
let before = scoped
.execute("SELECT payload_json FROM events_raw WHERE kind_base = 'approval_deferred'")
.await
.expect("query before rewrite");
assert_eq!(
before.rows,
vec![vec![serde_json::json!(r#"{"marker":"before"}"#)]],
"sanity: the pre-rewrite payload must be visible first"
);
fx.eventlog
.rewrite_partition(
"conv-erasure".to_owned(),
"test-authority-rewrite".to_owned(),
Box::new(|event| {
if event.kind == kinds::APPROVAL_DEFERRED {
RewriteDecision::Replace(br#"{"marker":"after"}"#.to_vec())
} else {
RewriteDecision::Keep
}
}),
)
.await
.expect("rewrite_partition");
cached_authority.invalidate_partition("conv-erasure");
let after = scoped
.execute("SELECT payload_json FROM events_raw WHERE kind_base = 'approval_deferred'")
.await
.expect("query after rewrite");
assert_eq!(
after.rows,
vec![vec![serde_json::json!(r#"{"marker":"after"}"#)]],
"the erased (pre-rewrite) payload must NEVER be served again — the reported mutation \
must force a full rebuild even though the event count is unchanged"
);
fx.teardown().await;
}
#[tokio::test]
async fn destroying_a_partition_evicts_its_cache_entry() {
let fx = Fixture::build("cache-destroy").await;
let persona_id = fx.make_admin("destroy-admin").await;
fx.eventlog
.append_batch(
"conv-destroy".to_owned(),
vec![Event::new("k0", Vec::new())],
)
.await
.expect("append");
let cached_authority = fx.authority_with_cache_config(CacheConfig::new(Some(1)));
let scoped = admin_scoped(&fx, &cached_authority, persona_id).await;
let before = scoped
.execute(
"SELECT COUNT(*) AS c FROM events_raw WHERE partition = 'conv-destroy' AND kind_base = 'k0'",
)
.await
.expect("query before destroy");
assert_eq!(before.rows, vec![vec![serde_json::json!(1)]]);
fx.eventlog
.destroy_partition("conv-destroy".to_owned())
.await
.expect("destroy_partition");
let after = scoped
.execute("SELECT COUNT(*) AS c FROM events_raw WHERE partition = 'conv-destroy'")
.await
.expect("query after destroy");
assert_eq!(
after.rows,
vec![vec![serde_json::json!(0)]],
"a destroyed partition must read back empty, not the evicted cache's stale rows"
);
fx.teardown().await;
}
#[tokio::test]
async fn cache_disabled_replays_and_rebuilds_every_query() {
let fx = Fixture::build("cache-disabled").await;
let persona_id = fx.make_admin("disabled-admin").await;
fx.eventlog
.append_batch(
"conv-disabled".to_owned(),
vec![Event::new("k0", Vec::new())],
)
.await
.expect("append");
let scoped = admin_scoped(&fx, &fx.authority, persona_id).await;
let build_before = crate::engine::BUILD_CALL_COUNT.load(std::sync::atomic::Ordering::SeqCst);
scoped
.execute("SELECT COUNT(*) AS c FROM events_raw")
.await
.expect("first query");
scoped
.execute("SELECT COUNT(*) AS c FROM events_raw")
.await
.expect("second, identical query");
let build_after = crate::engine::BUILD_CALL_COUNT.load(std::sync::atomic::Ordering::SeqCst);
assert_eq!(
build_after - build_before,
2,
"with the kill switch off, every query — even a repeat of the exact same SQL over an \
unchanged partition — must reach engine assembly fresh"
);
fx.teardown().await;
}
#[tokio::test]
async fn cached_volume_budget_trips_before_engine_assembly() {
let fx = Fixture::build("cache-volume-budget").await;
let persona_id = fx.make_admin("volume-admin").await;
fx.eventlog
.append_batch(
"conv-volume".to_owned(),
(0..6)
.map(|i| Event::new(format!("k{i}"), Vec::new()))
.collect(),
)
.await
.expect("append");
let cache_config = CacheConfig {
enabled: true,
max_bytes: 64 * 1024 * 1024,
max_cached_source_events: 4,
};
let cached_authority = fx.authority_with_cache_config(cache_config);
let scoped = admin_scoped(&fx, &cached_authority, persona_id).await;
let build_before = crate::engine::BUILD_CALL_COUNT.load(std::sync::atomic::Ordering::SeqCst);
let err = scoped
.execute("SELECT COUNT(*) AS c FROM events_raw")
.await
.unwrap_err();
let build_after = crate::engine::BUILD_CALL_COUNT.load(std::sync::atomic::Ordering::SeqCst);
assert!(
matches!(err, ScopedQueryError::SourceBudgetExceeded(_)),
"6 effective cached-scan events over a 4-event cached-volume budget must be rejected: \
{err:?}"
);
assert_eq!(
build_after, build_before,
"QueryEngine::build_from_tables must never be reached when the cached-scan volume \
budget rejects a query"
);
let message = err.to_string();
assert!(
!message.contains('6') && !message.contains('4'),
"the cached-volume rejection message must not leak the actual or budgeted event count: \
{message:?}"
);
assert!(
message.contains("cached-scan volume"),
"the message must name what budget tripped: {message:?}"
);
fx.teardown().await;
}
#[tokio::test]
async fn cache_tail_over_bytes_budget_is_rejected_before_engine_assembly() {
let fx = Fixture::build("cache-tail-bytes-budget").await;
let persona_id = fx.make_admin("tail-bytes-admin").await;
fx.eventlog
.append_batch(
"conv-tail-bytes".to_owned(),
vec![Event::new("k0", vec![0u8; 10])],
)
.await
.expect("append first (small) event");
let limited_cached_authority = fx.authority_with_limits_and_cache_config(
QueryLimits {
max_source_bytes: 2_000,
..QueryLimits::default()
},
CacheConfig::new(Some(1)),
);
let scoped = admin_scoped(&fx, &limited_cached_authority, persona_id).await;
scoped
.execute("SELECT COUNT(*) AS c FROM events_raw")
.await
.expect("first query (miss, populates the cache; 10 bytes is comfortably under budget)");
fx.eventlog
.append_batch(
"conv-tail-bytes".to_owned(),
vec![Event::new("k1", vec![0u8; 5_000])],
)
.await
.expect("append the oversized tail event");
let tails_before = counter_value(&scrape_metrics(), "polychrome_query_cache_tail_total");
let build_before = crate::engine::BUILD_CALL_COUNT.load(std::sync::atomic::Ordering::SeqCst);
let err = scoped
.execute("SELECT COUNT(*) AS c FROM events_raw")
.await
.unwrap_err();
let build_after = crate::engine::BUILD_CALL_COUNT.load(std::sync::atomic::Ordering::SeqCst);
let tails_after = counter_value(&scrape_metrics(), "polychrome_query_cache_tail_total");
assert_eq!(
tails_after - tails_before,
1.0,
"this query must actually take the Tail arm (not a fresh Miss) — otherwise this test \
would only be re-proving the miss arm's own pre-existing budget, not the tail arm's"
);
assert!(
matches!(err, ScopedQueryError::SourceBudgetExceeded(_)),
"a 5,000-byte tail over a 2,000-byte budget must be rejected: {err:?}"
);
assert_eq!(
build_after, build_before,
"QueryEngine::build_from_tables must never be reached when the cache-tail arm's own \
byte budget rejects a query — proving the tail was never fully materialized before \
the rejection"
);
fx.teardown().await;
}
#[derive(Debug, PartialEq, Eq)]
struct RenderedScoping {
conversations: Option<Vec<String>>,
allow_explain: bool,
caller_identity: Option<String>,
conversation_id: Option<String>,
turn_id: Option<String>,
web_session_id: Option<String>,
}
fn scoping_of(scoped: &ScopedQuery) -> RenderedScoping {
RenderedScoping {
conversations: match &scoped.scope {
QueryScope::Fleet => None,
QueryScope::Conversations(ids) => Some(ids.clone()),
},
allow_explain: scoped.allow_explain,
caller_identity: scoped.caller_identity.clone(),
conversation_id: scoped.conversation_id.clone(),
turn_id: scoped.turn_id.clone(),
web_session_id: scoped.web_session_id.clone(),
}
}
#[tokio::test]
async fn scope_for_turn_matches_the_conversation_grant_path() {
let fx = Fixture::build("scope-for-turn-parity").await;
let token = mint_conversation_grant(
&fx.signer.relabel_for_test(),
"conv-parity",
GrantSubject::Turn("turn-parity".to_owned()),
NOW + TEST_TTL_MS,
);
let principal = fx
.authority
.verify_conversation_grant(&token, NOW)
.expect("a freshly minted grant must verify");
let via_grant = fx
.authority
.scope_for(&principal)
.await
.expect("a conversation grant always scopes");
let via_turn = fx.authority.scope_for_turn("conv-parity", "turn-parity");
assert_eq!(
scoping_of(&via_turn),
scoping_of(&via_grant),
"the trusted-side turn path and the verified-grant path must produce one session shape"
);
assert!(
!via_turn.allow_explain,
"a conversation-scoped session never gets EXPLAIN, however it was obtained"
);
assert_eq!(via_turn.turn_id(), Some("turn-parity"));
assert_eq!(via_turn.conversation_id(), Some("conv-parity"));
assert_eq!(
via_turn.caller_identity(),
None,
"matching the grant path's audit shape exactly is deliberate — see scope_for_turn's doc"
);
assert_eq!(via_turn.web_session_id(), None);
match &via_turn.scope {
QueryScope::Conversations(ids) => {
assert_eq!(
ids,
&vec!["conv-parity".to_owned()],
"one conversation, its own"
);
}
QueryScope::Fleet => panic!("a turn-scoped session must never be Fleet"),
}
fx.teardown().await;
}
#[test]
fn is_admitted_partition_admits_exactly_conv_prefixed_and_the_scheduler_partition() {
assert!(is_admitted_partition("conv-a"));
assert!(
is_admitted_partition(ROUTINE_SCHEDULER_PARTITION),
"the scheduler partition must be admitted"
);
assert!(
!is_admitted_partition("some-other-partition"),
"no other non-conversation partition is ever admitted"
);
}
#[tokio::test]
async fn cache_enabled_fleet_and_owner_scoped_fires_see_the_scheduler_partition() {
let fx = Fixture::build("cache-enabled-fires-admission").await;
let owner_persona = fx.make_non_admin("dana-owner").await;
let catalog = Arc::new(FakeRoutineCatalog(vec![routine_record(
"cache-path-standup",
&owner_persona,
)]));
let authority =
fx.authority_with_routine_catalog_and_cache_config(catalog, CacheConfig::new(Some(1)));
let fired = RoutineFiredEvent {
routine: "cache-path-standup".to_owned(),
occurrence: "cache-path-standup-1".to_owned(),
scheduled_at_ms: 1,
fired_at_ms: 2,
routine_uid: "cache-path-standup-uid".to_owned(),
..Default::default()
};
fx.eventlog
.append_batch(
"routine-scheduler".to_owned(),
vec![Event::trusted(kinds::ROUTINE_FIRED, fired.encode_to_vec())],
)
.await
.expect("append routine-scheduler fire");
let admin_persona = fx.make_admin("erin-admin").await;
let admin_token = mint_session(
&fx.signer.relabel_for_test(),
&SessionSubject::Persona {
persona_id: admin_persona.clone(),
},
&[SessionScope::ExplorerRead],
NOW,
TEST_TTL_MS,
);
let admin_principal = authority
.verify_admin_session(&admin_token, NOW)
.await
.expect("valid admin session");
let scoped_fleet = authority
.scope_for(&admin_principal)
.await
.expect("scope_for fleet");
for attempt in 0..2 {
let fleet_result = scoped_fleet
.execute("SELECT routine FROM fires ORDER BY routine")
.await
.unwrap_or_else(|err| panic!("fleet fires query attempt {attempt} failed: {err}"));
assert_eq!(
fleet_result.rows,
vec![vec![serde_json::json!("cache-path-standup")]],
"fleet-scoped fires must be non-empty with the cache enabled, attempt {attempt}: {:?}",
fleet_result.rows
);
assert!(
!fleet_result.columns.is_empty(),
"fleet-scoped fires must report non-empty columns, attempt {attempt}"
);
}
let owner_token = mint_session(
&fx.signer.relabel_for_test(),
&SessionSubject::Persona {
persona_id: owner_persona.clone(),
},
&[SessionScope::ExplorerRead],
NOW,
TEST_TTL_MS,
);
let owner_principal = authority
.verify_admin_session(&owner_token, NOW)
.await
.expect("valid owner session");
assert!(
matches!(owner_principal, Principal::Persona(_)),
"a non-admin persona must mint Principal::Persona, got {owner_principal:?}"
);
let scoped_owner = authority
.scope_for(&owner_principal)
.await
.expect("scope_for owner");
for attempt in 0..2 {
let owner_result = scoped_owner
.execute("SELECT routine FROM fires ORDER BY routine")
.await
.unwrap_or_else(|err| {
panic!("owner-scoped fires query attempt {attempt} failed: {err}")
});
assert_eq!(
owner_result.rows,
vec![vec![serde_json::json!("cache-path-standup")]],
"owner-scoped fires must be non-empty with the cache enabled, attempt {attempt}: {:?}",
owner_result.rows
);
assert!(
!owner_result.columns.is_empty(),
"owner-scoped fires must report non-empty columns, attempt {attempt}"
);
}
fx.teardown().await;
}
async fn scope_for_persona(
authority: &QueryAuthority,
signer: &SessionSigner,
persona_id: &str,
) -> ScopedQuery {
let token = mint_session(
signer,
&SessionSubject::Persona {
persona_id: persona_id.to_owned(),
},
&[SessionScope::ExplorerRead],
NOW,
TEST_TTL_MS,
);
let principal = authority
.verify_admin_session(&token, NOW)
.await
.expect("valid session");
assert!(
matches!(principal, Principal::Persona(_)),
"a non-admin persona must mint Principal::Persona, got {principal:?}"
);
authority.scope_for(&principal).await.expect("scope_for")
}
fn grant_event(
tool_name: &str,
approved: bool,
conversation_id: &str,
grant_scope: &str,
signer: &ApprovalSigner,
) -> Event {
let turn = uuid::Uuid::now_v7();
let (payload, ..) = polyc_crypto::approval::routine_grant_payload(
&format!("call-{tool_name}-{approved}"),
tool_name,
"{}",
"",
approved,
"owner-persona",
"",
"default",
"",
&[],
conversation_id,
&uuid::Uuid::new_v4().to_string(),
&turn.to_string(),
"hash-fixture",
grant_scope,
signer,
);
Event::new(kinds::tagged(kinds::APPROVAL_RESPONSE, &turn), payload)
}
#[tokio::test]
async fn persona_scoped_routine_grants_see_only_the_callers_own_routine() {
let fx = Fixture::build("owner-scoped-routine-grants").await;
let persona_a = fx.make_non_admin("gina-owner").await;
let persona_b = fx.make_non_admin("hana-owner").await;
let catalog = Arc::new(FakeRoutineCatalog(vec![
routine_record("routine-a", &persona_a),
routine_record("routine-b", &persona_b),
]));
let authority = fx.authority_with_routine_catalog(catalog);
for (routine, persona, tool) in [
("routine-a", &persona_a, "tool-a"),
("routine-b", &persona_b, "tool-b"),
] {
let fire_conv = format!("{routine}-fire-conv");
let identity = ExternalIdentity {
provider: "test".to_owned(),
scope: "s".to_owned(),
external_id: match routine {
"routine-a" => "gina-owner".to_owned(),
_ => "hana-owner".to_owned(),
},
display_name: persona.clone(),
..Default::default()
};
fx.persona
.attribute(identity, fire_conv.clone(), "initiator".to_owned(), NOW)
.await
.expect("attribute owner to fire conversation");
fx.eventlog
.append_batch(
format!("conv-{fire_conv}"),
vec![grant_event(tool, true, &fire_conv, "tool", &fx.signer)],
)
.await
.expect("append grant");
}
let scoped_a = scope_for_persona(&authority, &fx.signer.relabel_for_test(), &persona_a).await;
let grants_a = scoped_a
.execute("SELECT routine, tool_name, grant_scope, approved FROM routine_grants")
.await
.expect("persona a's routine_grants query");
assert_eq!(
grants_a.rows,
vec![vec![
serde_json::json!("routine-a"),
serde_json::json!("tool-a"),
serde_json::json!("tool"),
serde_json::json!(true),
]],
"persona a must see exactly their own routine's grant"
);
let active_a = scoped_a
.execute("SELECT routine, tool_name FROM routine_active_grants")
.await
.expect("persona a's routine_active_grants query");
assert_eq!(
active_a.rows,
vec![vec![
serde_json::json!("routine-a"),
serde_json::json!("tool-a")
]]
);
let overview_a = scoped_a
.execute(
"SELECT name, mode, active_tool_grants, setup_completed FROM routine_overview \
ORDER BY name",
)
.await
.expect("persona a's routine_overview query");
assert_eq!(
overview_a.rows,
vec![vec![
serde_json::json!("routine-a"),
serde_json::json!("individual"),
serde_json::json!(1),
serde_json::json!(false),
]],
"the aggregate answers from the tables alone, owner-scoped"
);
let overview_a_new_columns = scoped_a
.execute(
"SELECT name, prompt, suspended, paused_by, paused_at_ms, pause_reason, \
last_fire_outcome, stopped_tool FROM routine_overview",
)
.await
.expect("persona a's routine_overview POLY-160-columns query");
assert_eq!(
overview_a_new_columns.rows.len(),
1,
"persona a must see exactly one row on the POLY-160 columns too — never owner b's \
routine"
);
let row = &overview_a_new_columns.rows[0];
assert_eq!(row[0], serde_json::json!("routine-a"));
assert_eq!(row[1], serde_json::json!("post the morning standup"));
assert_eq!(row[2], serde_json::json!(false), "suspended");
assert_eq!(row[3], serde_json::Value::Null, "paused_by while active");
assert_eq!(row[4], serde_json::Value::Null, "paused_at_ms while active");
assert_eq!(row[5], serde_json::Value::Null, "pause_reason while active");
assert_eq!(row[6], serde_json::Value::Null, "no fire ever recorded");
assert_eq!(
row[7],
serde_json::Value::Null,
"no unattended denial ever recorded"
);
let admin_persona = fx.make_admin("ivy-admin").await;
let admin_token = mint_session(
&fx.signer.relabel_for_test(),
&SessionSubject::Persona {
persona_id: admin_persona,
},
&[SessionScope::ExplorerRead],
NOW,
TEST_TTL_MS,
);
let admin_principal = authority
.verify_admin_session(&admin_token, NOW)
.await
.expect("valid admin session");
let scoped_fleet = authority
.scope_for(&admin_principal)
.await
.expect("scope_for");
let grants_fleet = scoped_fleet
.execute("SELECT routine, tool_name FROM routine_grants ORDER BY routine")
.await
.expect("fleet routine_grants query");
assert_eq!(
grants_fleet.rows,
vec![
vec![serde_json::json!("routine-a"), serde_json::json!("tool-a")],
vec![serde_json::json!("routine-b"), serde_json::json!("tool-b")],
],
"fleet must see every routine's grants, unfiltered"
);
fx.teardown().await;
}
#[tokio::test]
async fn routine_overview_composes_grants_mode_denials_pending_and_setup() {
let fx = Fixture::build("routine-overview-composition").await;
let owner = fx.make_non_admin("kira-owner").await;
let catalog = Arc::new(FakeRoutineCatalog(vec![routine_record(
"routine-x",
&owner,
)]));
let authority = fx.authority_with_routine_catalog(catalog);
let fire_conv = "routine-x-fire-conv";
let partition = format!("conv-{fire_conv}");
fx.eventlog
.append_batch(
partition.clone(),
vec![
grant_event("revoked-tool", true, fire_conv, "tool", &fx.signer),
grant_event("kept-tool", true, fire_conv, "tool", &fx.signer),
grant_event("revoked-tool", false, fire_conv, "tool", &fx.signer),
grant_event("", true, fire_conv, "blanket_below_high", &fx.signer),
],
)
.await
.expect("append grant history");
let denial_turn = uuid::Uuid::now_v7();
let request = polyc_crypto::approval::request_payload(
"call-denied",
"denied-tool",
"{}",
"default",
"",
&[],
"",
"",
"",
&[],
false,
);
let (denied, ..) = polyc_crypto::approval::response_payload(
"call-denied",
"denied-tool",
"{}",
"",
false,
false,
&[],
&owner,
"",
"default",
"not allowed on unattended runs",
"",
fire_conv,
"nonce-denied",
&denial_turn.to_string(),
&fx.signer,
);
let pending_turn = uuid::Uuid::now_v7();
let pending_request = polyc_crypto::approval::request_payload(
"call-pending",
"pending-tool",
"{}",
"default",
"",
&[],
"",
"",
"",
&[],
false,
);
fx.eventlog
.append_batch(
partition.clone(),
vec![
Event::new(
kinds::tagged(kinds::APPROVAL_REQUEST, &denial_turn),
request,
),
Event::new(
kinds::tagged(kinds::APPROVAL_RESPONSE, &denial_turn),
denied,
),
Event::new(
kinds::tagged(kinds::APPROVAL_REQUEST, &pending_turn),
pending_request,
),
],
)
.await
.expect("append denial and pending request");
let fired = RoutineFiredEvent {
routine: "routine-x".to_owned(),
occurrence: "routine-x-1".to_owned(),
scheduled_at_ms: 10,
fired_at_ms: 20,
routine_uid: "routine-x-uid".to_owned(),
..Default::default()
};
let outcome_ev = polyc_proto::proto::polychrome::events::v1::RoutineFireOutcomeEvent {
routine: fired.routine.clone(),
occurrence: fired.occurrence.clone(),
outcome: polyc_proto::proto::polychrome::events::v1::RoutineFireOutcome::StoppedUngranted
.into(),
fired_at_ms: fired.fired_at_ms,
..Default::default()
};
let setup = serde_json::json!({ "routine_uid": "routine-x-uid" }).to_string();
fx.eventlog
.append_batch(
"routine-scheduler".to_owned(),
vec![
Event::trusted(kinds::ROUTINE_FIRED, fired.encode_to_vec()),
Event::trusted(kinds::ROUTINE_FIRE_OUTCOME, outcome_ev.encode_to_vec()),
Event::trusted(kinds::ROUTINE_SETUP_COMPLETED, setup.into_bytes()),
],
)
.await
.expect("append scheduler markers");
let admin_persona = fx.make_admin("liam-admin").await;
let admin_token = mint_session(
&fx.signer.relabel_for_test(),
&SessionSubject::Persona {
persona_id: admin_persona,
},
&[SessionScope::ExplorerRead],
NOW,
TEST_TTL_MS,
);
let admin_principal = authority
.verify_admin_session(&admin_token, NOW)
.await
.expect("valid admin session");
let scoped = authority
.scope_for(&admin_principal)
.await
.expect("scope_for");
let active = scoped
.execute("SELECT tool_name FROM routine_active_grants WHERE grant_scope = 'tool'")
.await
.expect("active grants query");
assert_eq!(
active.rows,
vec![vec![serde_json::json!("kept-tool")]],
"a latest disapproving record removes its key; the standing grant survives"
);
let overview = scoped
.execute(
"SELECT name, mode, setup_completed, fire_count, active_tool_grants, \
denial_count, pending_setup_approvals FROM routine_overview",
)
.await
.expect("overview query");
assert_eq!(
overview.rows,
vec![vec![
serde_json::json!("routine-x"),
serde_json::json!("auto"),
serde_json::json!(true),
serde_json::json!(1),
serde_json::json!(1),
serde_json::json!(1),
serde_json::json!(1),
]],
"the per-routine aggregate composes fires, grants, mode, denials, pending setup \
approvals, and setup state from the typed tables alone"
);
let extended = scoped
.execute(
"SELECT prompt, schedule_json, next_fires_json, suspended, paused_by, \
paused_at_ms, pause_reason, last_fire_at_ms, last_fire_outcome, stopped_tool \
FROM routine_overview",
)
.await
.expect("extended overview query");
let record = routine_record("routine-x", &owner);
assert_eq!(
extended.rows,
vec![vec![
serde_json::json!(record.prompt),
serde_json::json!(record.schedule_json),
serde_json::json!(record.next_fires_json),
serde_json::json!(record.suspended),
serde_json::Value::Null,
serde_json::Value::Null,
serde_json::Value::Null,
serde_json::json!(20),
serde_json::json!("stopped_ungranted"),
serde_json::json!("denied-tool"),
]],
"the routine's own spec fields pass through from `routines`, the latest fire's own \
outcome is picked by window rather than aggregated, and stopped_tool names the \
routine's latest unattended-fire denial"
);
fx.teardown().await;
}
#[tokio::test]
async fn cache_enabled_routine_setup_and_grants_see_their_partitions() {
let fx = Fixture::build("cache-enabled-routine-setup").await;
let owner = fx.make_non_admin("mona-owner").await;
let catalog = Arc::new(FakeRoutineCatalog(vec![routine_record(
"cache-routine",
&owner,
)]));
let authority =
fx.authority_with_routine_catalog_and_cache_config(catalog, CacheConfig::new(Some(1)));
let fire_conv = "cache-routine-fire-conv";
let owner_identity = ExternalIdentity {
provider: "test".to_owned(),
scope: "s".to_owned(),
external_id: "mona-owner".to_owned(),
display_name: "mona-owner".to_owned(),
..Default::default()
};
fx.persona
.attribute(
owner_identity,
fire_conv.to_owned(),
"initiator".to_owned(),
NOW,
)
.await
.expect("attribute owner to fire conversation");
fx.eventlog
.append_batch(
format!("conv-{fire_conv}"),
vec![grant_event(
"cache-tool",
true,
fire_conv,
"tool",
&fx.signer,
)],
)
.await
.expect("append grant");
let setup = serde_json::json!({ "routine_uid": "cache-routine-uid" }).to_string();
fx.eventlog
.append_batch(
"routine-scheduler".to_owned(),
vec![Event::trusted(
kinds::ROUTINE_SETUP_COMPLETED,
setup.into_bytes(),
)],
)
.await
.expect("append setup marker");
let admin_persona = fx.make_admin("nova-admin").await;
let admin_token = mint_session(
&fx.signer.relabel_for_test(),
&SessionSubject::Persona {
persona_id: admin_persona,
},
&[SessionScope::ExplorerRead],
NOW,
TEST_TTL_MS,
);
let admin_principal = authority
.verify_admin_session(&admin_token, NOW)
.await
.expect("valid admin session");
let scoped_fleet = authority
.scope_for(&admin_principal)
.await
.expect("scope_for fleet");
for attempt in 0..2 {
let setup_rows = scoped_fleet
.execute("SELECT routine_uid FROM routine_setup")
.await
.unwrap_or_else(|err| panic!("fleet routine_setup attempt {attempt} failed: {err}"));
assert_eq!(
setup_rows.rows,
vec![vec![serde_json::json!("cache-routine-uid")]],
"fleet routine_setup must be non-empty with the cache enabled, attempt {attempt}"
);
let grant_rows = scoped_fleet
.execute("SELECT tool_name FROM routine_grants")
.await
.unwrap_or_else(|err| panic!("fleet routine_grants attempt {attempt} failed: {err}"));
assert_eq!(
grant_rows.rows,
vec![vec![serde_json::json!("cache-tool")]],
"fleet routine_grants must be non-empty with the cache enabled, attempt {attempt}"
);
}
let scoped_owner = scope_for_persona(&authority, &fx.signer.relabel_for_test(), &owner).await;
for attempt in 0..2 {
let setup_rows = scoped_owner
.execute("SELECT routine_uid FROM routine_setup")
.await
.unwrap_or_else(|err| panic!("owner routine_setup attempt {attempt} failed: {err}"));
assert_eq!(
setup_rows.rows,
vec![vec![serde_json::json!("cache-routine-uid")]],
"owner routine_setup must be non-empty with the cache enabled, attempt {attempt}"
);
let overview = scoped_owner
.execute("SELECT name, setup_completed, active_tool_grants FROM routine_overview")
.await
.unwrap_or_else(|err| panic!("owner routine_overview attempt {attempt} failed: {err}"));
assert_eq!(
overview.rows,
vec![vec![
serde_json::json!("cache-routine"),
serde_json::json!(true),
serde_json::json!(1),
]],
"the owner-scoped aggregate must compose on the cache-enabled path, attempt {attempt}"
);
}
fx.teardown().await;
}
#[tokio::test]
async fn admin_session_removed_persona_is_not_authorized_for_fleet() {
let fx = Fixture::build("admin-removed-persona").await;
let admin_id = fx.make_admin("remover").await;
let target_id = fx.make_non_admin("removed-later").await;
let target_identity = ExternalIdentity {
provider: "test".to_owned(),
scope: "s".to_owned(),
external_id: "removed-later".to_owned(),
display_name: "removed-later".to_owned(),
..Default::default()
};
let token = mint_session(
&fx.signer.relabel_for_test(),
&SessionSubject::Persona {
persona_id: target_id.clone(),
},
&[SessionScope::ExplorerRead],
NOW,
TEST_TTL_MS,
);
fx.authority
.verify_admin_session(&token, NOW)
.await
.expect("precondition: the session verifies while the persona is live");
fx.persona
.remove_access(admin_id, target_identity, NOW + 1)
.await
.expect("remove_access");
let err = fx
.authority
.verify_admin_session(&token, NOW + 2)
.await
.unwrap_err();
assert!(
matches!(err, PrincipalError::NotAuthorizedForFleet),
"a de-admitted persona's still-valid session must mint no principal at all, got {err:?}"
);
fx.teardown().await;
}
async fn attribute_persona_to(fx: &Fixture, label: &str, conversation_ids: &[&str]) -> String {
let identity = ExternalIdentity {
provider: "test".to_owned(),
scope: "s".to_owned(),
external_id: label.to_owned(),
display_name: label.to_owned(),
..Default::default()
};
let mut persona_id = None;
for conversation_id in conversation_ids {
let resolved = fx
.persona
.attribute(
identity.clone(),
(*conversation_id).to_owned(),
"initiator".to_owned(),
NOW,
)
.await
.expect("attribute")
.persona_id;
persona_id = Some(resolved);
}
persona_id.expect("at least one conversation id")
}
fn expected_scope_hash(conversation_ids: &[&str]) -> String {
let mut ids: Vec<&str> = conversation_ids.to_vec();
ids.sort_by_key(|id| (id.len(), *id));
ids.dedup();
let mut buf = Vec::new();
buf.extend_from_slice(b"polychrome.search.scope.v1");
buf.push(0);
for id in ids {
let bytes = id.as_bytes();
buf.extend_from_slice(&u32::try_from(bytes.len()).unwrap().to_be_bytes());
buf.extend_from_slice(bytes);
}
blake3::hash(&buf).to_hex().to_string()
}
#[tokio::test]
async fn search_scope_removed_persona_refuses() {
let fx = Fixture::build("search-scope-removed").await;
let admin_id = fx.make_admin("remover").await;
let target_id = attribute_persona_to(&fx, "removed-later", &["conv-a"]).await;
let target_identity = ExternalIdentity {
provider: "test".to_owned(),
scope: "s".to_owned(),
external_id: "removed-later".to_owned(),
display_name: "removed-later".to_owned(),
..Default::default()
};
fx.persona
.remove_access(admin_id, target_identity, NOW + 1)
.await
.expect("remove_access");
let err = fx
.authority
.resolve_search_scope(&target_id, "conv-caller", "turn-1")
.await
.unwrap_err();
assert!(
matches!(err, SearchScopeError::PersonaNotActive),
"a de-admitted persona must refuse search-scope resolution: {err:?}"
);
fx.teardown().await;
}
#[tokio::test]
async fn search_scope_unknown_persona_refuses() {
let fx = Fixture::build("search-scope-unknown").await;
let err = fx
.authority
.resolve_search_scope("persona-never-existed", "conv-caller", "turn-1")
.await
.unwrap_err();
assert!(matches!(err, SearchScopeError::PersonaNotActive));
fx.teardown().await;
}
#[tokio::test]
async fn search_scope_store_down_is_unavailable() {
let fx = Fixture::build("search-scope-store-down").await;
let authority_no_store = fx.authority_with_empty_persona_cell();
let err = authority_no_store
.resolve_search_scope("persona-x", "conv-caller", "turn-1")
.await
.unwrap_err();
assert!(
matches!(err, SearchScopeError::StoreUnavailable),
"an unreadable persona store must surface as a distinct, transient error — never fold \
into a hard refusal: {err:?}"
);
fx.teardown().await;
}
#[tokio::test]
async fn search_scope_excludes_the_calling_conversation() {
let fx = Fixture::build("search-scope-excludes-caller").await;
let persona_id = attribute_persona_to(&fx, "caller-exclusion", &["conv-a", "conv-b"]).await;
let scope = fx
.authority
.resolve_search_scope(&persona_id, "conv-a", "turn-1")
.await
.expect("an active persona with a bounded participation set resolves");
assert_eq!(
scope.conversation_ids(),
&["conv-b".to_owned()],
"the calling conversation must never appear in its own search scope"
);
assert_eq!(scope.count(), 1);
assert_eq!(scope.hash(), expected_scope_hash(&["conv-b"]));
fx.teardown().await;
}
#[tokio::test]
async fn search_scope_excludes_a_tombstoned_conversation() {
let fx = Fixture::build("search-scope-excludes-tombstoned").await;
let persona_id =
attribute_persona_to(&fx, "tombstone-exclusion", &["conv-a", "conv-b", "conv-c"]).await;
fx.persona
.set_search_visibility(
persona_id.clone(),
"conv-b".to_owned(),
true,
persona_id.clone(),
NOW + 1,
)
.await
.expect("set_search_visibility");
let scope = fx
.authority
.resolve_search_scope(&persona_id, "conv-a", "turn-1")
.await
.expect("an active persona with a bounded participation set resolves");
assert_eq!(
scope.conversation_ids(),
&["conv-c".to_owned()],
"conv-a is excluded as the caller, conv-b as tombstoned — only conv-c remains: {:?}",
scope.conversation_ids()
);
fx.teardown().await;
}
#[tokio::test]
async fn search_scope_over_cap_refuses_with_count() {
let fx = Fixture::build("search-scope-over-cap").await;
let persona_id = attribute_persona_to(&fx, "over-cap", &["conv-a", "conv-b", "conv-c"]).await;
fx.authority.set_test_search_scope_cap(2);
let err = fx
.authority
.resolve_search_scope(&persona_id, "conv-a", "turn-1")
.await
.unwrap_err();
assert!(
matches!(err, SearchScopeError::OverCap { count: 3 }),
"expected OverCap{{count: 3}}, got {err:?}"
);
fx.teardown().await;
}
#[tokio::test]
async fn search_scope_hash_is_independent_of_enumeration_order() {
let fx = Fixture::build("search-scope-order-independent").await;
let ascending =
attribute_persona_to(&fx, "order-ascending", &["conv-a", "conv-b", "conv-c"]).await;
let descending =
attribute_persona_to(&fx, "order-descending", &["conv-c", "conv-b", "conv-a"]).await;
let via_ascending = fx
.authority
.resolve_search_scope(&ascending, "conv-x", "turn-1")
.await
.expect("ascending order resolves");
let via_descending = fx
.authority
.resolve_search_scope(&descending, "conv-x", "turn-1")
.await
.expect("descending order resolves");
assert_eq!(
via_ascending.hash(),
via_descending.hash(),
"the same conversation set, accrued in a different order, must hash identically"
);
assert_eq!(
via_ascending.conversation_ids(),
via_descending.conversation_ids()
);
fx.teardown().await;
}
#[tokio::test]
async fn search_scope_hash_changes_when_the_set_changes() {
let fx = Fixture::build("search-scope-hash-changes").await;
let smaller = attribute_persona_to(&fx, "hash-smaller", &["conv-a", "conv-b"]).await;
let larger = attribute_persona_to(&fx, "hash-larger", &["conv-a", "conv-b", "conv-c"]).await;
let via_smaller = fx
.authority
.resolve_search_scope(&smaller, "conv-x", "turn-1")
.await
.expect("smaller set resolves");
let via_larger = fx
.authority
.resolve_search_scope(&larger, "conv-x", "turn-1")
.await
.expect("larger set resolves");
assert_ne!(
via_smaller.hash(),
via_larger.hash(),
"a different conversation set must hash differently"
);
fx.teardown().await;
}
#[test]
fn core_requester_encoding_has_no_delimiter_collision() {
let first = core_requester_id("turn", &["a:b", "c"]);
let second = core_requester_id("turn", &["a", "b:c"]);
let another_kind = core_requester_id("web-session", &["a:b", "c"]);
assert_ne!(first, second);
assert_ne!(first, another_kind);
assert!(first.as_str().starts_with("turn-"));
}
mod credential_parity {
use super::*;
use crate::credential::{CredentialWitness, PresentedCredential, UnixClock};
struct FixedClock(u64);
impl UnixClock for FixedClock {
fn now_unix_ms(&self) -> u64 {
self.0
}
}
#[derive(Clone, Copy)]
enum Kind {
Bearer,
Grant,
}
fn present(kind: Kind, token: &str) -> PresentedCredential {
match kind {
Kind::Bearer => PresentedCredential::Bearer(token.to_owned()),
Kind::Grant => PresentedCredential::ConversationGrant(token.to_owned()),
}
}
#[derive(Debug, PartialEq, Eq)]
enum Decision {
Granted(String),
Refused(&'static str),
}
const fn refusal(error: &PrincipalError) -> &'static str {
match error {
PrincipalError::InvalidSession => "InvalidSession",
PrincipalError::StoreUnavailable => "StoreUnavailable",
PrincipalError::NotAuthorizedForFleet => "NotAuthorizedForFleet",
PrincipalError::InvalidGrant => "InvalidGrant",
PrincipalError::GrantExpired => "GrantExpired",
}
}
fn render(
scope: &QueryScope,
allow_explain: bool,
caller_identity: Option<&str>,
conversation_id: Option<&str>,
turn_id: Option<&str>,
web_session_id: Option<&str>,
) -> Decision {
let scope = match scope {
QueryScope::Fleet => "fleet".to_owned(),
QueryScope::Conversations(conversations) => {
let mut ids = conversations.clone();
ids.sort_unstable();
format!("conversations[{}]", ids.join(","))
}
};
Decision::Granted(format!(
"{scope} explain={allow_explain} caller={caller_identity:?} \
conversation={conversation_id:?} turn={turn_id:?} \
web_session={web_session_id:?}",
))
}
fn granted(scoping: &Scoping) -> Decision {
render(
&scoping.scope,
scoping.allow_explain,
scoping.caller_identity.as_deref(),
scoping.conversation_id.as_deref(),
scoping.turn_id.as_deref(),
scoping.web_session_id.as_deref(),
)
}
fn granted_session(session: &ScopedQuery) -> Decision {
render(
&session.scope,
session.allow_explain,
session.caller_identity.as_deref(),
session.conversation_id.as_deref(),
session.turn_id.as_deref(),
session.web_session_id.as_deref(),
)
}
async fn embedded(authority: &QueryAuthority, kind: Kind, token: &str, now: u64) -> Decision {
let verified = match kind {
Kind::Bearer => authority.verify_admin_session(token, now).await,
Kind::Grant => authority.verify_conversation_grant(token, now),
};
match verified {
Err(error) => Decision::Refused(refusal(&error)),
Ok(principal) => match authority.scope_for(&principal).await {
Err(error) => Decision::Refused(refusal(&error)),
Ok(session) => granted_session(&session),
},
}
}
async fn service(authority: &QueryAuthority, kind: Kind, token: &str, now: u64) -> Decision {
let admitted = CredentialWitness::admit(
present(kind, token),
authority.credential_authority(),
Arc::new(FixedClock(now)),
)
.await;
match admitted {
Err(error) => Decision::Refused(refusal(&error)),
Ok((_witness, scoping)) => granted(&scoping),
}
}
async fn parity(authority: &QueryAuthority, kind: Kind, token: &str, now: u64) -> Decision {
let embedded = embedded(authority, kind, token, now).await;
let service = service(authority, kind, token, now).await;
assert_eq!(
embedded, service,
"the embedded and service entry points must decide identically"
);
embedded
}
fn admin_token(fx: &Fixture, persona_id: &str, expires_in_ms: u64) -> String {
mint_session(
&fx.signer.relabel_for_test(),
&SessionSubject::Persona {
persona_id: persona_id.to_owned(),
},
&[SessionScope::ExplorerRead],
NOW,
expires_in_ms,
)
}
#[tokio::test]
async fn a_valid_admin_session_grants_the_same_fleet_scope_on_both_entry_points() {
let fx = Fixture::build("parity-valid").await;
let persona_id = fx.make_admin("alice").await;
let token = admin_token(&fx, &persona_id, TEST_TTL_MS);
let decision = parity(&fx.authority, Kind::Bearer, &token, NOW).await;
assert_eq!(
decision,
Decision::Granted(format!(
"fleet explain=true caller=Some({persona_id:?}) conversation=None turn=None \
web_session=None"
))
);
fx.teardown().await;
}
#[tokio::test]
async fn an_expired_session_refuses_identically_on_both_entry_points() {
let fx = Fixture::build("parity-expired").await;
let persona_id = fx.make_admin("alice").await;
let token = admin_token(&fx, &persona_id, TEST_TTL_MS);
let decision = parity(&fx.authority, Kind::Bearer, &token, NOW + TEST_TTL_MS + 1).await;
assert_eq!(decision, Decision::Refused("InvalidSession"));
fx.teardown().await;
}
#[tokio::test]
async fn a_revoked_session_refuses_identically_on_both_entry_points() {
let fx = Fixture::build("parity-revoked").await;
let persona_id = fx.make_admin("alice").await;
let token = admin_token(&fx, &persona_id, TEST_TTL_MS);
assert!(matches!(
parity(&fx.authority, Kind::Bearer, &token, NOW).await,
Decision::Granted(_)
));
fx.revoked.revoke(&token);
let decision = parity(&fx.authority, Kind::Bearer, &token, NOW).await;
assert_eq!(decision, Decision::Refused("InvalidSession"));
fx.teardown().await;
}
#[tokio::test]
async fn a_malformed_session_refuses_identically_on_both_entry_points() {
let fx = Fixture::build("parity-malformed").await;
for token in ["", "not-a-token", "aaaa.bbbb.cccc"] {
let decision = parity(&fx.authority, Kind::Bearer, token, NOW).await;
assert_eq!(
decision,
Decision::Refused("InvalidSession"),
"token {token:?}"
);
}
fx.teardown().await;
}
#[tokio::test]
async fn a_non_admin_session_scopes_to_its_own_participations_on_both_entry_points() {
let fx = Fixture::build("parity-non-admin").await;
let persona_id = fx.make_non_admin("bob").await;
let token = admin_token(&fx, &persona_id, TEST_TTL_MS);
let decision = parity(&fx.authority, Kind::Bearer, &token, NOW).await;
assert_eq!(
decision,
Decision::Granted(format!(
"conversations[conv-bob] explain=false caller=Some({persona_id:?}) \
conversation=None turn=None web_session=None"
))
);
fx.teardown().await;
}
#[tokio::test]
async fn a_participation_change_moves_both_entry_points_together() {
let fx = Fixture::build("parity-participation").await;
let persona_id = fx.make_non_admin("carol").await;
let token = admin_token(&fx, &persona_id, TEST_TTL_MS);
let before = parity(&fx.authority, Kind::Bearer, &token, NOW).await;
assert_eq!(
before,
Decision::Granted(format!(
"conversations[conv-carol] explain=false caller=Some({persona_id:?}) \
conversation=None turn=None web_session=None"
))
);
fx.persona
.attribute_persona(
persona_id.clone(),
"conv-carol-2".to_owned(),
"participant".to_owned(),
NOW,
)
.await
.expect("attribute the persona to a second conversation");
let after = parity(&fx.authority, Kind::Bearer, &token, NOW).await;
assert_eq!(
after,
Decision::Granted(format!(
"conversations[conv-carol,conv-carol-2] explain=false caller=Some({persona_id:?}) \
conversation=None turn=None web_session=None"
)),
"both entry points must observe the widened participation set"
);
fx.teardown().await;
}
#[tokio::test]
async fn a_merged_persona_resolves_identically_on_both_entry_points() {
let fx = Fixture::build("parity-merged").await;
let absorbed_identity = ExternalIdentity {
provider: "test".to_owned(),
scope: "s".to_owned(),
external_id: "dana-old".to_owned(),
display_name: "dana-old".to_owned(),
..Default::default()
};
let survivor_identity = ExternalIdentity {
provider: "test".to_owned(),
scope: "s".to_owned(),
external_id: "dana-new".to_owned(),
display_name: "dana-new".to_owned(),
..Default::default()
};
let first = fx
.persona
.attribute(
absorbed_identity.clone(),
"conv-dana-old".to_owned(),
"initiator".to_owned(),
NOW,
)
.await
.expect("provision the first persona")
.persona_id;
let second = fx
.persona
.attribute(
survivor_identity.clone(),
"conv-dana-new".to_owned(),
"initiator".to_owned(),
NOW,
)
.await
.expect("provision the second persona")
.persona_id;
assert_ne!(first, second, "the fixture needs two distinct personas");
fx.persona
.start_link(absorbed_identity, "link-code".to_owned(), TEST_TTL_MS, NOW)
.await
.expect("start the link");
fx.persona
.complete_link("link-code".to_owned(), survivor_identity, NOW)
.await
.expect("complete the link");
let first_is_alive = fx
.persona
.active_persona(first.clone())
.await
.expect("read the first persona")
.is_some_and(|active| active.persona_id == first);
let (absorbed, survivor) = if first_is_alive {
(second, first)
} else {
(first, second)
};
let token = admin_token(&fx, &absorbed, TEST_TTL_MS);
let decision = parity(&fx.authority, Kind::Bearer, &token, NOW).await;
assert_eq!(
decision,
Decision::Granted(format!(
"conversations[conv-dana-new,conv-dana-old] explain=false \
caller=Some({survivor:?}) conversation=None turn=None web_session=None"
)),
"both entry points must follow the merge alias to the same survivor and scope"
);
assert_ne!(
absorbed, survivor,
"the attributed identity must be the survivor, never the tombstone"
);
fx.teardown().await;
}
#[tokio::test]
async fn a_store_outage_refuses_identically_on_both_entry_points() {
let fx = Fixture::build("parity-store-outage").await;
let persona_id = fx.make_admin("erin").await;
let token = admin_token(&fx, &persona_id, TEST_TTL_MS);
let authority = fx.authority_with_empty_persona_cell();
let decision = parity(&authority, Kind::Bearer, &token, NOW).await;
assert_eq!(decision, Decision::Refused("StoreUnavailable"));
fx.teardown().await;
}
#[tokio::test]
async fn a_conversation_grant_decides_identically_on_both_entry_points() {
let fx = Fixture::build("parity-grant").await;
let token = mint_conversation_grant(
&fx.signer.relabel_for_test(),
"conv-grant",
GrantSubject::Turn("turn-7".to_owned()),
NOW + TEST_TTL_MS,
);
let decision = parity(&fx.authority, Kind::Grant, &token, NOW).await;
assert_eq!(
decision,
Decision::Granted(
"conversations[conv-grant] explain=false caller=None \
conversation=Some(\"conv-grant\") turn=Some(\"turn-7\") web_session=None"
.to_owned()
)
);
assert_eq!(
parity(&fx.authority, Kind::Grant, &token, NOW + TEST_TTL_MS + 1).await,
Decision::Refused("GrantExpired")
);
assert_eq!(
parity(&fx.authority, Kind::Grant, "not-a-grant", NOW).await,
Decision::Refused("InvalidGrant")
);
fx.teardown().await;
}
}