use adk_graph::edge::{END, START};
use adk_graph::graph::StateGraph;
use adk_graph::node::{ExecutionConfig, NodeOutput};
use adk_graph::state::{Reducer, State, StateSchema};
use serde_json::json;
use std::time::Duration;
#[tokio::test]
async fn appends_apply_in_node_order_not_completion_order() {
let schema = StateSchema::builder().channel_with_reducer("log", Reducer::Append).build();
let graph = StateGraph::new(schema)
.add_node_fn("alpha", |_ctx| async move {
tokio::time::sleep(Duration::from_millis(60)).await;
Ok(NodeOutput::new().with_update("log", json!("alpha")))
})
.add_node_fn("zulu", |_ctx| async move {
Ok(NodeOutput::new().with_update("log", json!("zulu")))
})
.add_edge(START, "alpha")
.add_edge(START, "zulu")
.add_edge("alpha", END)
.add_edge("zulu", END)
.compile()
.unwrap();
let state = graph.invoke(State::new(), ExecutionConfig::new("order-1")).await.unwrap();
assert_eq!(
state.get("log"),
Some(&json!(["alpha", "zulu"])),
"updates must be applied in node order, not in the order the futures resolved"
);
}
#[tokio::test]
async fn the_result_is_the_same_when_completion_order_reverses() {
async fn run(alpha_delay_ms: u64, zulu_delay_ms: u64) -> State {
let schema = StateSchema::builder().channel_with_reducer("log", Reducer::Append).build();
let graph = StateGraph::new(schema)
.add_node_fn("alpha", move |_ctx| async move {
tokio::time::sleep(Duration::from_millis(alpha_delay_ms)).await;
Ok(NodeOutput::new().with_update("log", json!("alpha")))
})
.add_node_fn("zulu", move |_ctx| async move {
tokio::time::sleep(Duration::from_millis(zulu_delay_ms)).await;
Ok(NodeOutput::new().with_update("log", json!("zulu")))
})
.add_edge(START, "alpha")
.add_edge(START, "zulu")
.add_edge("alpha", END)
.add_edge("zulu", END)
.compile()
.unwrap();
graph.invoke(State::new(), ExecutionConfig::new("order-2")).await.unwrap()
}
let alpha_slow = run(60, 0).await;
let zulu_slow = run(0, 60).await;
assert_eq!(
alpha_slow.get("log"),
zulu_slow.get("log"),
"reversing which node is slower must not change the state"
);
assert_eq!(alpha_slow.get("log"), Some(&json!(["alpha", "zulu"])));
}
#[tokio::test]
async fn the_streamed_path_agrees_with_invoke() {
use adk_graph::stream::StreamMode;
use futures::StreamExt;
let schema = StateSchema::builder().channel_with_reducer("log", Reducer::Append).build();
let build = || {
StateGraph::new(schema.clone())
.add_node_fn("alpha", |_ctx| async move {
tokio::time::sleep(Duration::from_millis(60)).await;
Ok(NodeOutput::new().with_update("log", json!("alpha")))
})
.add_node_fn("zulu", |_ctx| async move {
Ok(NodeOutput::new().with_update("log", json!("zulu")))
})
.add_edge(START, "alpha")
.add_edge(START, "zulu")
.add_edge("alpha", END)
.add_edge("zulu", END)
.compile()
.unwrap()
.with_checkpointer(adk_graph::checkpoint::MemoryCheckpointer::new())
};
let invoked = build().invoke(State::new(), ExecutionConfig::new("stream-a")).await.unwrap();
let graph = build();
let stream = graph.stream(State::new(), ExecutionConfig::new("stream-b"), StreamMode::Values);
let mut stream = Box::pin(stream);
while stream.next().await.is_some() {}
let streamed = graph.get_state("stream-b").await.unwrap().unwrap_or_default();
assert_eq!(
invoked.get("log"),
streamed.get("log"),
"invoke and stream must order updates the same way"
);
}
#[test]
fn prop_state_does_not_depend_on_completion_order() {
use proptest::prelude::*;
proptest!(ProptestConfig::with_cases(32), |(delays in prop::collection::vec(0u64..40, 3..=4))| {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_time()
.build()
.expect("runtime");
let state = runtime.block_on(async {
let schema =
StateSchema::builder().channel_with_reducer("log", Reducer::Append).build();
let mut graph = StateGraph::new(schema);
let names = ["n0", "n1", "n2", "n3"];
for (index, delay) in delays.iter().enumerate() {
let name = names[index];
let delay = *delay;
graph = graph.add_node_fn(name, move |_ctx| async move {
tokio::time::sleep(Duration::from_millis(delay)).await;
Ok(NodeOutput::new().with_update("log", json!(name)))
});
}
for name in names.iter().take(delays.len()) {
graph = graph.add_edge(START, name).add_edge(name, END);
}
let compiled = graph.compile().expect("compile");
compiled.invoke(State::new(), ExecutionConfig::new("prop")).await.expect("run")
});
let expected: Vec<_> =
(0..delays.len()).map(|i| json!(["n0", "n1", "n2", "n3"][i])).collect();
prop_assert_eq!(state.get("log"), Some(&json!(expected)));
});
}