#![cfg(all(feature = "runlog-sqlite", feature = "test-utils"))]
use std::sync::Arc;
use async_trait::async_trait;
use klieo::runlog::{RunLogStore, RunStatus, SqliteRunLogStore};
use klieo::test_utils::{FakeLlmClient, FakeToolInvoker};
use klieo::{Agent, AgentContext, Episode, RunId, ToolDef};
struct RecordingAgent {
episodes: Vec<Episode>,
}
#[async_trait]
impl Agent for RecordingAgent {
type Input = ();
type Output = RunId;
type Error = klieo::Error;
fn name(&self) -> &str {
"recorder"
}
fn system_prompt(&self) -> &str {
""
}
fn tools(&self) -> &[ToolDef] {
&[]
}
async fn run(&self, ctx: AgentContext, _input: ()) -> Result<RunId, Self::Error> {
let run_id = ctx.run_id;
for episode in &self.episodes {
ctx.episodic
.record(run_id, episode.clone())
.await
.expect("episode record must succeed in the in-memory store");
}
Ok(run_id)
}
}
async fn app_with_runlog(store_path: &std::path::Path) -> klieo::App {
klieo::App::builder()
.llm(Arc::new(FakeLlmClient::new("fake")))
.memory(
klieo::memory_sqlite::MemorySqlite::new(
":memory:",
Arc::new(klieo::memory_sqlite::DummyEmbedder),
)
.await
.unwrap(),
)
.bus(klieo::bus_memory::MemoryBus::new())
.tools_invoker(Arc::new(FakeToolInvoker::new()))
.sqlite_runlog(store_path.to_path_buf())
.build()
.await
.unwrap()
}
#[tokio::test]
async fn run_with_episodes_auto_persists_a_runlog() {
let dir = tempfile::tempdir().unwrap();
let store_path = dir.path().join("runlog.db");
let app = app_with_runlog(&store_path).await;
let agent = RecordingAgent {
episodes: vec![
Episode::Started {
agent: "recorder".into(),
},
Episode::Completed,
],
};
let run_id = app.run(&agent, ()).await.unwrap();
let store = SqliteRunLogStore::new(&store_path).await.unwrap();
let persisted = store
.get(run_id)
.await
.unwrap()
.expect("a run that recorded episodes must auto-persist a RunLog");
assert_eq!(persisted.run_id, run_id);
assert_eq!(persisted.agent, "recorder");
assert_eq!(
persisted.status,
RunStatus::Completed,
"the projected log must reflect the recorded Started→Completed timeline"
);
}
#[tokio::test]
async fn run_without_episodes_persists_nothing() {
let dir = tempfile::tempdir().unwrap();
let store_path = dir.path().join("runlog.db");
let app = app_with_runlog(&store_path).await;
let agent = RecordingAgent { episodes: vec![] };
let run_id = app.run(&agent, ()).await.unwrap();
let store = SqliteRunLogStore::new(&store_path).await.unwrap();
let persisted = store.get(run_id).await.unwrap();
assert!(
persisted.is_none(),
"a run that recorded zero episodes must write no RunLog (skip-empty)"
);
}