use adk_core::{
Agent, Content, InvocationContext as InvocationContextTrait, Part, ReadonlyContext,
RequestContext,
};
use adk_runner::InvocationContext;
use adk_session::{Events, Session, State};
use async_trait::async_trait;
use proptest::prelude::*;
use std::collections::HashMap;
use std::sync::Arc;
struct MockEvents;
impl Events for MockEvents {
fn all(&self) -> Vec<adk_core::Event> {
Vec::new()
}
fn len(&self) -> usize {
0
}
fn at(&self, _index: usize) -> Option<&adk_core::Event> {
None
}
}
struct MockStateView;
impl State for MockStateView {
fn get(&self, _key: &str) -> Option<serde_json::Value> {
None
}
fn set(&mut self, _key: String, _value: serde_json::Value) {}
fn all(&self) -> HashMap<String, serde_json::Value> {
HashMap::new()
}
}
struct MockSession;
impl Session for MockSession {
fn id(&self) -> &str {
"session-abc"
}
fn app_name(&self) -> &str {
"test-app"
}
fn user_id(&self) -> &str {
"user-123"
}
fn state(&self) -> &dyn State {
static VIEW: MockStateView = MockStateView;
&VIEW
}
fn events(&self) -> &dyn Events {
static EVENTS: MockEvents = MockEvents;
&EVENTS
}
fn last_update_time(&self) -> chrono::DateTime<chrono::Utc> {
chrono::Utc::now()
}
}
struct MockAgent {
name: String,
}
#[async_trait]
impl Agent for MockAgent {
fn name(&self) -> &str {
&self.name
}
fn description(&self) -> &str {
"mock"
}
fn sub_agents(&self) -> &[Arc<dyn Agent>] {
&[]
}
async fn run(
&self,
_ctx: Arc<dyn InvocationContextTrait>,
) -> adk_core::Result<adk_core::EventStream> {
Ok(Box::pin(futures::stream::empty()))
}
}
fn make_ctx(
app: &str,
user: &str,
session: &str,
invocation: &str,
agent: &str,
branch: &str,
) -> InvocationContext {
let agent = Arc::new(MockAgent { name: agent.to_string() });
let content =
Content { role: "user".to_string(), parts: vec![Part::Text { text: "hi".to_string() }] };
let ctx = InvocationContext::new(
invocation.to_string(),
agent,
user.to_string(),
app.to_string(),
session.to_string(),
content,
Arc::new(MockSession),
)
.expect("test identity values must be valid");
if branch.is_empty() { ctx } else { ctx.with_branch(branch.to_string()) }
}
#[test]
fn try_identity_returns_correct_session_triple() {
let ctx = make_ctx("my-app", "alice", "sess-1", "inv-1", "planner", "");
let identity = ctx.try_identity().expect("try_identity should succeed");
assert_eq!(identity.app_name.as_ref(), "my-app");
assert_eq!(identity.user_id.as_ref(), "alice");
assert_eq!(identity.session_id.as_ref(), "sess-1");
}
#[test]
fn try_identity_with_special_chars() {
let ctx = make_ctx(
"org:weather-app",
"tenant:alice@example.com",
"sess/2024/abc",
"inv-1",
"agent",
"",
);
let identity = ctx.try_identity().expect("special chars should be accepted");
assert_eq!(identity.app_name.as_ref(), "org:weather-app");
assert_eq!(identity.user_id.as_ref(), "tenant:alice@example.com");
assert_eq!(identity.session_id.as_ref(), "sess/2024/abc");
}
#[test]
fn try_execution_identity_returns_full_capsule() {
let ctx = make_ctx("my-app", "alice", "sess-1", "inv-42", "planner", "main.sub");
let exec = ctx.try_execution_identity().expect("should succeed");
assert_eq!(exec.adk.app_name.as_ref(), "my-app");
assert_eq!(exec.adk.user_id.as_ref(), "alice");
assert_eq!(exec.adk.session_id.as_ref(), "sess-1");
assert_eq!(exec.invocation_id.as_ref(), "inv-42");
assert_eq!(exec.branch, "main.sub");
assert_eq!(exec.agent_name, "planner");
}
#[test]
fn try_execution_identity_default_branch_is_empty() {
let ctx = make_ctx("app", "user", "sess", "inv", "agent", "");
let exec = ctx.try_execution_identity().unwrap();
assert_eq!(exec.branch, "");
}
#[test]
fn identity_and_execution_identity_share_session_triple() {
let ctx = make_ctx("app-x", "user-y", "sess-z", "inv-w", "agent-v", "branch-u");
let identity = ctx.try_identity().unwrap();
let exec = ctx.try_execution_identity().unwrap();
assert_eq!(identity, exec.adk);
}
#[test]
fn request_context_overrides_user_id() {
let ctx = make_ctx("app", "original-user", "sess", "inv", "agent", "").with_request_context(
RequestContext {
user_id: "auth-user-override".to_string(),
scopes: vec!["read".to_string()],
metadata: HashMap::new(),
},
);
assert_eq!(ctx.user_id(), "auth-user-override");
}
#[test]
fn try_identity_uses_auth_user_when_request_context_set() {
let ctx = make_ctx("app", "original-user", "sess", "inv", "agent", "").with_request_context(
RequestContext {
user_id: "auth-user".to_string(),
scopes: vec![],
metadata: HashMap::new(),
},
);
let identity = ctx.try_identity().unwrap();
assert_eq!(identity.user_id.as_ref(), "auth-user");
assert_eq!(identity.app_name.as_ref(), "app");
assert_eq!(identity.session_id.as_ref(), "sess");
}
#[test]
fn try_execution_identity_uses_auth_user_when_request_context_set() {
let ctx = make_ctx("app", "original-user", "sess", "inv", "agent", "main")
.with_request_context(RequestContext {
user_id: "auth-user".to_string(),
scopes: vec!["admin".to_string()],
metadata: HashMap::new(),
});
let exec = ctx.try_execution_identity().unwrap();
assert_eq!(exec.adk.user_id.as_ref(), "auth-user");
assert_eq!(exec.adk.app_name.as_ref(), "app");
assert_eq!(exec.adk.session_id.as_ref(), "sess");
assert_eq!(exec.invocation_id.as_ref(), "inv");
assert_eq!(exec.branch, "main");
assert_eq!(exec.agent_name, "agent");
}
#[test]
fn without_request_context_user_id_is_original() {
let ctx = make_ctx("app", "original-user", "sess", "inv", "agent", "");
assert_eq!(ctx.user_id(), "original-user");
let identity = ctx.try_identity().unwrap();
assert_eq!(identity.user_id.as_ref(), "original-user");
}
fn arb_valid_id() -> impl Strategy<Value = String> {
"[a-zA-Z0-9:@/_\\-\\.]{1,64}"
}
fn arb_branch() -> impl Strategy<Value = String> {
"[a-z\\.]{0,20}"
}
fn arb_agent_name() -> impl Strategy<Value = String> {
"[a-z_]{1,20}"
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_execution_identity_projection(
app in arb_valid_id(),
user in arb_valid_id(),
session in arb_valid_id(),
invocation in arb_valid_id(),
agent in arb_agent_name(),
branch in arb_branch(),
) {
let ctx = make_ctx(&app, &user, &session, &invocation, &agent, &branch);
let identity = ctx.try_identity().expect("try_identity must succeed for valid inputs");
let exec = ctx.try_execution_identity().expect("try_execution_identity must succeed");
prop_assert_eq!(identity.app_name.as_ref(), ctx.app_name());
prop_assert_eq!(identity.user_id.as_ref(), ctx.user_id());
prop_assert_eq!(identity.session_id.as_ref(), ctx.session_id());
prop_assert_eq!(&identity, &exec.adk);
prop_assert_eq!(exec.invocation_id.as_ref(), ctx.invocation_id());
prop_assert_eq!(exec.branch.as_str(), ctx.branch());
prop_assert_eq!(exec.agent_name.as_str(), ctx.agent_name());
}
#[test]
fn prop_auth_override_consistent_projection(
app in arb_valid_id(),
original_user in arb_valid_id(),
auth_user in arb_valid_id(),
session in arb_valid_id(),
invocation in arb_valid_id(),
agent in arb_agent_name(),
branch in arb_branch(),
) {
let ctx = make_ctx(&app, &original_user, &session, &invocation, &agent, &branch)
.with_request_context(RequestContext {
user_id: auth_user.clone(),
scopes: vec![],
metadata: HashMap::new(),
});
let identity = ctx.try_identity().expect("try_identity must succeed");
let exec = ctx.try_execution_identity().expect("try_execution_identity must succeed");
prop_assert_eq!(identity.user_id.as_ref(), auth_user.as_str());
prop_assert_eq!(exec.adk.user_id.as_ref(), auth_user.as_str());
prop_assert_eq!(&identity, &exec.adk);
prop_assert_eq!(identity.app_name.as_ref(), app.as_str());
prop_assert_eq!(identity.session_id.as_ref(), session.as_str());
}
}