#![cfg(feature = "sqlite")]
use adk_graph::checkpoint::{Checkpointer, SqliteCheckpointer};
use adk_graph::state::Checkpoint;
use serde_json::json;
fn checkpoint(thread: &str, id: &str, step: usize) -> Checkpoint {
serde_json::from_value(json!({
"thread_id": thread,
"checkpoint_id": id,
"state": { "value": step },
"step": step,
"pending_nodes": ["next"],
"metadata": {},
"created_at": "2026-08-20T00:00:00Z"
}))
.expect("test checkpoint must build")
}
#[tokio::test]
async fn from_pool_adopts_the_callers_pool() {
let pool =
sqlx::SqlitePool::connect("sqlite::memory:").await.expect("caller opens its own pool");
let cp = SqliteCheckpointer::from_pool(pool.clone())
.await
.expect("from_pool must accept an open pool");
cp.save(&checkpoint("t-1", "cp-1", 1)).await.expect("save through the adopted pool");
let loaded = cp.load("t-1").await.expect("load must succeed");
assert_eq!(
loaded.map(|c| (c.checkpoint_id, c.step)),
Some(("cp-1".to_string(), 1)),
"a checkpoint saved through the adopted pool must load back"
);
}
#[tokio::test]
async fn the_caller_can_query_what_the_checkpointer_wrote() {
let pool =
sqlx::SqlitePool::connect("sqlite::memory:").await.expect("caller opens its own pool");
let cp = SqliteCheckpointer::from_pool(pool.clone())
.await
.expect("from_pool must accept an open pool");
cp.save(&checkpoint("t-shared", "cp-shared", 4)).await.expect("save");
let (id, step): (String, i64) =
sqlx::query_as("SELECT id, step FROM graph_checkpoints WHERE thread_id = ?")
.bind("t-shared")
.fetch_one(&pool)
.await
.expect("the caller's pool must see the checkpointer's row");
assert_eq!(
(id.as_str(), step),
("cp-shared", 4),
"the checkpointer must write through the pool it was given"
);
}
#[tokio::test]
async fn from_pool_creates_the_full_schema() {
let pool = sqlx::SqlitePool::connect("sqlite::memory:").await.expect("pool");
SqliteCheckpointer::from_pool(pool.clone()).await.expect("from_pool");
let columns: Vec<String> =
sqlx::query_scalar("SELECT name FROM pragma_table_info('graph_checkpoints') ORDER BY name")
.fetch_all(&pool)
.await
.expect("the table must exist in the caller's database");
for required in [
"attempts",
"child_ledger",
"cleared_interrupt",
"created_at",
"id",
"metadata",
"pending_nodes",
"state",
"step",
"thread_id",
] {
assert!(
columns.iter().any(|c| c == required),
"column `{required}` missing from a from_pool database; found {columns:?}"
);
}
let indexes: Vec<String> =
sqlx::query_scalar("SELECT name FROM sqlite_master WHERE type = 'index'")
.fetch_all(&pool)
.await
.expect("index query");
assert!(
indexes.iter().any(|i| i == "idx_graph_checkpoints_thread"),
"the thread index must be created on a caller-supplied pool; found {indexes:?}"
);
}
#[tokio::test]
async fn adopting_the_same_pool_twice_succeeds() {
let pool = sqlx::SqlitePool::connect("sqlite::memory:").await.expect("pool");
let first = SqliteCheckpointer::from_pool(pool.clone()).await;
assert!(first.is_ok(), "first adoption: {:?}", first.err());
let second = SqliteCheckpointer::from_pool(pool.clone()).await;
assert!(
second.is_ok(),
"re-running the schema on an initialized database must not fail: {:?}",
second.err()
);
first.unwrap().save(&checkpoint("t-twice", "cp-twice", 9)).await.expect("save via the first");
let loaded = second.unwrap().load("t-twice").await.expect("load via the second");
assert_eq!(
loaded.map(|c| c.checkpoint_id),
Some("cp-twice".to_string()),
"both checkpointers must address the same database"
);
}