use proptest::prelude::*;
use arbiter_session::store::{CreateSessionRequest, SessionStore};
use arbiter_session::{DataSensitivity, SessionError, SessionStatus, TaskSession};
use chrono::Utc;
use uuid::Uuid;
fn make_request(
intent: &str,
tools: Vec<String>,
budget: u64,
time_limit: chrono::Duration,
) -> CreateSessionRequest {
CreateSessionRequest {
agent_id: Uuid::new_v4(),
delegation_chain_snapshot: vec![],
declared_intent: intent.to_string(),
authorized_tools: tools,
authorized_credentials: vec![],
time_limit,
call_budget: budget,
rate_limit_per_minute: None,
rate_limit_window_secs: 60,
data_sensitivity_ceiling: DataSensitivity::Internal,
}
}
fn tool_name_strategy() -> impl Strategy<Value = String> {
"[a-z][a-z0-9_]{0,31}"
}
fn intent_strategy() -> impl Strategy<Value = String> {
"[a-z ]{1,64}"
}
proptest! {
#[test]
fn create_then_get_is_active(
intent in intent_strategy(),
budget in 1u64..100,
) {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
rt.block_on(async {
let store = SessionStore::new();
let req = make_request(&intent, vec![], budget, chrono::Duration::hours(1));
let session = store.create(req).await;
prop_assert_eq!(session.status, SessionStatus::Active);
prop_assert_eq!(session.calls_made, 0);
prop_assert!(session.is_active());
let retrieved = store.get(session.session_id).await.unwrap();
prop_assert_eq!(retrieved.status, SessionStatus::Active);
prop_assert_eq!(retrieved.session_id, session.session_id);
Ok(())
})?;
}
#[test]
fn budget_enforcement(budget in 1u64..20) {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
rt.block_on(async {
let store = SessionStore::new();
let req = make_request("test intent", vec![], budget, chrono::Duration::hours(1));
let session = store.create(req).await;
for i in 0..budget {
let result = store.use_session(session.session_id, "any_tool", None).await;
prop_assert!(
result.is_ok(),
"call {} of {} should succeed, got: {:?}", i + 1, budget, result
);
}
let result = store.use_session(session.session_id, "any_tool", None).await;
prop_assert!(
matches!(result, Err(SessionError::BudgetExceeded { .. })),
"call {} should be BudgetExceeded, got: {:?}", budget + 1, result
);
Ok(())
})?;
}
#[test]
fn tool_whitelist_enforcement(
allowed_tool in tool_name_strategy(),
disallowed_tool in tool_name_strategy(),
) {
prop_assume!(allowed_tool != disallowed_tool);
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
rt.block_on(async {
let store = SessionStore::new();
let req = make_request(
"test",
vec![allowed_tool.clone()],
100,
chrono::Duration::hours(1),
);
let session = store.create(req).await;
let result = store.use_session(session.session_id, &allowed_tool, None).await;
prop_assert!(result.is_ok(), "allowed tool should succeed: {:?}", result);
let result = store.use_session(session.session_id, &disallowed_tool, None).await;
prop_assert!(
matches!(result, Err(SessionError::ToolNotAuthorized { .. })),
"disallowed tool '{}' should be rejected, got: {:?}", disallowed_tool, result
);
Ok(())
})?;
}
#[test]
fn model_tool_authorization_property(
tool_a in tool_name_strategy(),
tool_b in tool_name_strategy(),
) {
let session = TaskSession {
session_id: Uuid::new_v4(),
agent_id: Uuid::new_v4(),
delegation_chain_snapshot: vec![],
declared_intent: "test".into(),
authorized_tools: vec![tool_a.clone()],
authorized_credentials: vec![],
time_limit: chrono::Duration::hours(1),
call_budget: 100,
calls_made: 0,
rate_limit_per_minute: None,
rate_window_start: Utc::now(),
rate_window_calls: 0,
rate_limit_window_secs: 60,
data_sensitivity_ceiling: DataSensitivity::Internal,
created_at: Utc::now(),
status: SessionStatus::Active,
};
prop_assert!(session.is_tool_authorized(&tool_a));
if tool_a == tool_b {
prop_assert!(session.is_tool_authorized(&tool_b));
} else {
prop_assert!(!session.is_tool_authorized(&tool_b));
}
}
#[test]
fn empty_whitelist_allows_any_tool(tool in tool_name_strategy()) {
let session = TaskSession {
session_id: Uuid::new_v4(),
agent_id: Uuid::new_v4(),
delegation_chain_snapshot: vec![],
declared_intent: "test".into(),
authorized_tools: vec![],
authorized_credentials: vec![],
time_limit: chrono::Duration::hours(1),
call_budget: 100,
calls_made: 0,
rate_limit_per_minute: None,
rate_window_start: Utc::now(),
rate_window_calls: 0,
rate_limit_window_secs: 60,
data_sensitivity_ceiling: DataSensitivity::Internal,
created_at: Utc::now(),
status: SessionStatus::Active,
};
prop_assert!(
session.is_tool_authorized(&tool),
"empty authorized_tools should allow any tool, but '{}' was rejected", tool
);
}
}
#[test]
fn short_duration_session_is_expired() {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
rt.block_on(async {
let store = SessionStore::new();
let req = make_request(
"short lived session",
vec![],
100,
chrono::Duration::seconds(1),
);
let session = store.create(req).await;
tokio::time::sleep(std::time::Duration::from_millis(1100)).await;
let result = store
.use_session(session.session_id, "any_tool", None)
.await;
assert!(
matches!(result, Err(SessionError::Expired(_))),
"expired session should return error, got: {:?}",
result
);
});
}