use std::collections::HashMap;
use std::sync::Arc;
use adk_core::{Content, Part};
use adk_runner::MutableSession;
use adk_session::{Event, Events, Session, State};
use proptest::prelude::*;
use adk_core::Session as AdkCoreSession;
struct MockEvents(Vec<Event>);
impl Events for MockEvents {
fn all(&self) -> Vec<Event> {
self.0.clone()
}
fn len(&self) -> usize {
self.0.len()
}
fn at(&self, i: usize) -> Option<&Event> {
self.0.get(i)
}
}
struct MockState(HashMap<String, serde_json::Value>);
impl adk_session::ReadonlyState for MockState {
fn get(&self, key: &str) -> Option<serde_json::Value> {
self.0.get(key).cloned()
}
fn all(&self) -> HashMap<String, serde_json::Value> {
self.0.clone()
}
}
impl State for MockState {
fn get(&self, key: &str) -> Option<serde_json::Value> {
self.0.get(key).cloned()
}
fn set(&mut self, key: String, value: serde_json::Value) {
self.0.insert(key, value);
}
fn all(&self) -> HashMap<String, serde_json::Value> {
self.0.clone()
}
}
struct MockSession {
state: MockState,
events: MockEvents,
}
impl Session for MockSession {
fn id(&self) -> &str {
"s1"
}
fn app_name(&self) -> &str {
"app"
}
fn user_id(&self) -> &str {
"u1"
}
fn state(&self) -> &dyn State {
&self.state
}
fn events(&self) -> &dyn Events {
&self.events
}
fn last_update_time(&self) -> chrono::DateTime<chrono::Utc> {
chrono::Utc::now()
}
}
fn empty_session() -> Arc<dyn Session> {
Arc::new(MockSession { state: MockState(HashMap::new()), events: MockEvents(vec![]) })
}
fn make_event(author: &str, text: &str) -> Event {
let mut e = Event::new("inv-1");
e.author = author.to_string();
e.llm_response.content = Some(Content {
role: if author == "user" { "user" } else { "model" }.to_string(),
parts: vec![Part::Text { text: text.to_string() }],
});
e
}
fn arb_state_key() -> impl Strategy<Value = String> {
prop_oneof![
"[a-z_]{1,20}", "temp:[a-z_]{1,15}", ]
}
fn arb_state_value() -> impl Strategy<Value = serde_json::Value> {
prop_oneof![
any::<i64>().prop_map(serde_json::Value::from),
"[a-zA-Z0-9 ]{0,30}".prop_map(serde_json::Value::from),
any::<bool>().prop_map(serde_json::Value::from),
Just(serde_json::Value::Null),
]
}
fn arb_delta() -> impl Strategy<Value = HashMap<String, serde_json::Value>> {
proptest::collection::hash_map(arb_state_key(), arb_state_value(), 0..10)
}
fn arb_author() -> impl Strategy<Value = String> {
prop_oneof![
Just("user".to_string()),
Just("agent-alpha".to_string()),
Just("agent-beta".to_string()),
]
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_apply_state_delta_merges_correctly(
deltas in proptest::collection::vec(arb_delta(), 1..5),
) {
let session = empty_session();
let mutable = MutableSession::new(session);
let mut expected = HashMap::new();
for delta in &deltas {
mutable.apply_state_delta(delta);
for (k, v) in delta {
if !k.starts_with("temp:") {
expected.insert(k.clone(), v.clone());
}
}
}
let actual = AdkCoreSession::state(&mutable).all();
prop_assert_eq!(&actual, &expected);
}
#[test]
fn prop_append_event_preserves_order(
authors in proptest::collection::vec(arb_author(), 1..20),
) {
let session = empty_session();
let mutable = MutableSession::new(session);
for (i, author) in authors.iter().enumerate() {
mutable.append_event(make_event(author, &format!("msg-{i}")));
}
let snapshot = mutable.events_snapshot();
prop_assert_eq!(snapshot.len(), authors.len());
prop_assert_eq!(mutable.events_len(), authors.len());
for (i, (event, expected_author)) in snapshot.iter().zip(authors.iter()).enumerate() {
prop_assert_eq!(
&event.author, expected_author,
"event {} author mismatch", i
);
}
}
#[test]
fn prop_conversation_history_maps_roles_correctly(
authors in proptest::collection::vec(arb_author(), 1..15),
) {
let session = empty_session();
let mutable = MutableSession::new(session);
for (i, author) in authors.iter().enumerate() {
mutable.append_event(make_event(author, &format!("msg-{i}")));
}
let history = mutable.conversation_history_for_agent_impl(None);
prop_assert_eq!(history.len(), authors.len());
for (content, author) in history.iter().zip(authors.iter()) {
let expected_role = if author == "user" { "user" } else { "model" };
prop_assert_eq!(
&content.role, expected_role,
"role mismatch for author"
);
}
}
#[test]
fn prop_conversation_history_filters_by_agent(
authors in proptest::collection::vec(arb_author(), 1..15),
) {
let session = empty_session();
let mutable = MutableSession::new(session);
for (i, author) in authors.iter().enumerate() {
mutable.append_event(make_event(author, &format!("msg-{i}")));
}
let filtered = mutable.conversation_history_for_agent_impl(Some("agent-alpha"));
let expected_count = authors.iter()
.filter(|a| *a == "user" || *a == "agent-alpha")
.count();
prop_assert_eq!(filtered.len(), expected_count);
for content in &filtered {
let role = &content.role;
prop_assert!(
role == "user" || role == "model",
"unexpected role '{role}' in filtered history"
);
}
}
}
#[test]
fn apply_state_delta_empty_is_noop() {
let session = empty_session();
let mutable = MutableSession::new(session);
let mut delta = HashMap::new();
delta.insert("key1".to_string(), serde_json::json!("val1"));
mutable.apply_state_delta(&delta);
mutable.apply_state_delta(&HashMap::new());
let state = AdkCoreSession::state(&mutable).all();
assert_eq!(state.len(), 1);
assert_eq!(state.get("key1"), Some(&serde_json::json!("val1")));
}
#[test]
fn apply_state_delta_skips_temp_keys() {
let session = empty_session();
let mutable = MutableSession::new(session);
let mut delta = HashMap::new();
delta.insert("normal_key".to_string(), serde_json::json!("kept"));
delta.insert("temp:scratch".to_string(), serde_json::json!("skipped"));
mutable.apply_state_delta(&delta);
let state = AdkCoreSession::state(&mutable).all();
assert_eq!(state.len(), 1);
assert!(state.contains_key("normal_key"));
assert!(!state.contains_key("temp:scratch"));
}