use lellm_graph::{
Checkpoint, CheckpointStoreError, InMemoryBlobStore, SerdeCheckpointCodec, State, StateExt,
TraceId, TypedCheckpointStore,
};
const TEST_GRAPH_HASH: u64 = 0xABCD_EF01_2345_6789;
#[tokio::test]
async fn test_checkpoint_restore_roundtrip() {
let store = InMemoryBlobStore::new();
let codec = SerdeCheckpointCodec::<State>::new();
let typed = TypedCheckpointStore::new(&store, codec);
let trace_id = TraceId::new();
let mut state = State::new();
state.insert("user_id".to_string(), serde_json::json!("u123"));
state.insert("step".to_string(), serde_json::json!(42));
let cp = Checkpoint::new("process_order", &state, TEST_GRAPH_HASH);
let cp_id = cp.checkpoint_id.clone();
typed
.save_with_trace(&trace_id, &cp, TEST_GRAPH_HASH)
.await
.expect("save should succeed");
let restored = typed
.load(&cp_id, TEST_GRAPH_HASH)
.await
.expect("load should succeed")
.expect("checkpoint should exist");
assert_eq!(restored.checkpoint_id, cp_id);
assert_eq!(restored.current_node.0, "process_order");
assert_eq!(restored.state.get_str("user_id"), Some("u123"));
assert_eq!(restored.state.get_i64("step"), Some(42));
assert_eq!(restored.current_node.to_string(), "process_order");
}
#[tokio::test]
async fn test_load_latest_checkpoint() {
let store = InMemoryBlobStore::new();
let codec = SerdeCheckpointCodec::<State>::new();
let typed = TypedCheckpointStore::new(&store, codec);
let trace_id = TraceId::new();
let latest = typed
.load_latest(&trace_id, TEST_GRAPH_HASH)
.await
.expect("load_latest should succeed");
assert!(latest.is_none());
let state1 = State::new();
let cp1 = Checkpoint::new("node_a", &state1, TEST_GRAPH_HASH);
typed
.save_with_trace(&trace_id, &cp1, TEST_GRAPH_HASH)
.await
.expect("save cp1");
let state2 = State::new();
let cp2 = Checkpoint::new("node_b", &state2, TEST_GRAPH_HASH);
typed
.save_with_trace(&trace_id, &cp2, TEST_GRAPH_HASH)
.await
.expect("save cp2");
let latest = typed
.load_latest(&trace_id, TEST_GRAPH_HASH)
.await
.expect("load_latest should succeed")
.expect("should have latest checkpoint");
assert_eq!(latest.checkpoint_id, cp2.checkpoint_id);
}
#[tokio::test]
async fn test_trace_isolation() {
let store = InMemoryBlobStore::new();
let codec = SerdeCheckpointCodec::<State>::new();
let typed = TypedCheckpointStore::new(&store, codec);
let trace_a = TraceId::new();
let trace_b = TraceId::new();
let state_a = State::new();
let cp_a = Checkpoint::new("node_a", &state_a, TEST_GRAPH_HASH);
typed
.save_with_trace(&trace_a, &cp_a, TEST_GRAPH_HASH)
.await
.expect("save cp_a");
let state_b = State::new();
let cp_b = Checkpoint::new("node_b", &state_b, TEST_GRAPH_HASH);
typed
.save_with_trace(&trace_b, &cp_b, TEST_GRAPH_HASH)
.await
.expect("save cp_b");
let latest_a = typed
.load_latest(&trace_a, TEST_GRAPH_HASH)
.await
.expect("load_latest trace_a");
assert!(latest_a.is_some());
assert_eq!(latest_a.unwrap().checkpoint_id, cp_a.checkpoint_id);
let latest_b = typed
.load_latest(&trace_b, TEST_GRAPH_HASH)
.await
.expect("load_latest trace_b");
assert!(latest_b.is_some());
assert_eq!(latest_b.unwrap().checkpoint_id, cp_b.checkpoint_id);
}
#[tokio::test]
async fn test_graph_hash_mismatch_on_load() {
let store = InMemoryBlobStore::new();
let codec = SerdeCheckpointCodec::<State>::new();
let typed = TypedCheckpointStore::new(&store, codec);
let trace_id = TraceId::new();
let state_init = State::new();
let cp = Checkpoint::new("node_a", &state_init, TEST_GRAPH_HASH);
let cp_id = cp.checkpoint_id.clone();
typed
.save_with_trace(&trace_id, &cp, TEST_GRAPH_HASH)
.await
.expect("save should succeed");
let wrong_hash = TEST_GRAPH_HASH ^ 0xFF;
let result = typed.load(&cp_id, wrong_hash).await;
match result {
Err(CheckpointStoreError::GraphMismatch { expected, actual }) => {
assert_eq!(expected, wrong_hash);
assert_eq!(actual, TEST_GRAPH_HASH);
}
other => panic!("expected GraphMismatch, got: {other:?}"),
}
}