#![cfg(feature = "sqlite")]
use klieo_core::ids::RunId;
use klieo_runlog::projector::project;
use klieo_runlog::store::{RunLogQuery, RunLogStore};
use klieo_runlog::store_sqlite::SqliteRunLogStore;
use klieo_runlog::types::{Cost, RunStatus};
use tempfile::NamedTempFile;
fn tmp_db() -> NamedTempFile {
NamedTempFile::new().unwrap()
}
#[tokio::test]
async fn put_then_get_round_trip() {
let f = tmp_db();
let store = SqliteRunLogStore::new(f.path()).await.unwrap();
let log = project(RunId::new(), "writer", &[]);
let rid = log.run_id;
store.put(&log).await.unwrap();
let back = store.get(rid).await.unwrap().unwrap();
assert_eq!(back.agent, "writer");
}
#[tokio::test]
async fn list_orders_recent_first_and_respects_limit() {
let f = tmp_db();
let store = SqliteRunLogStore::new(f.path()).await.unwrap();
for _ in 0..5 {
store
.put(&project(RunId::new(), "writer", &[]))
.await
.unwrap();
}
let rows = store.list(&RunLogQuery::recent(3)).await.unwrap();
assert_eq!(rows.len(), 3);
for w in rows.windows(2) {
assert!(w[0].started_at >= w[1].started_at);
}
}
#[tokio::test]
async fn list_status_filter_pushed_into_query_composes_with_limit() {
let f = tmp_db();
let store = SqliteRunLogStore::new(f.path()).await.unwrap();
for _ in 0..2 {
let mut log = project(RunId::new(), "w", &[]);
log.status = RunStatus::Completed;
store.put(&log).await.unwrap();
}
for _ in 0..3 {
store.put(&project(RunId::new(), "w", &[])).await.unwrap();
}
let done = store
.list(&RunLogQuery {
status: Some(RunStatus::Completed),
limit: Some(2),
..Default::default()
})
.await
.unwrap();
assert_eq!(done.len(), 2);
assert!(done.iter().all(|r| r.status == RunStatus::Completed));
}
#[tokio::test]
async fn list_offset_paginates() {
let f = tmp_db();
let store = SqliteRunLogStore::new(f.path()).await.unwrap();
for _ in 0..5 {
store.put(&project(RunId::new(), "w", &[])).await.unwrap();
}
let page2 = store
.list(&RunLogQuery {
limit: Some(2),
offset: 2,
..Default::default()
})
.await
.unwrap();
assert_eq!(page2.len(), 2, "offset+limit returns the second page");
}
#[tokio::test]
async fn list_filtered_by_agent() {
let f = tmp_db();
let store = SqliteRunLogStore::new(f.path()).await.unwrap();
store
.put(&project(RunId::new(), "writer", &[]))
.await
.unwrap();
store
.put(&project(RunId::new(), "editor", &[]))
.await
.unwrap();
let rows = store
.list(&RunLogQuery {
agent: Some("writer".into()),
limit: Some(10),
..Default::default()
})
.await
.unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].agent, "writer");
}
#[tokio::test]
async fn default_query_is_unbounded_not_zero() {
let f = tmp_db();
let store = SqliteRunLogStore::new(f.path()).await.unwrap();
for _ in 0..5 {
store.put(&project(RunId::new(), "w", &[])).await.unwrap();
}
let rows = store.list(&RunLogQuery::default()).await.unwrap();
assert_eq!(
rows.len(),
5,
"a no-limit query returns every row, not zero"
);
}
#[tokio::test]
async fn delete_removes_log() {
let f = tmp_db();
let store = SqliteRunLogStore::new(f.path()).await.unwrap();
let log = project(RunId::new(), "writer", &[]);
let rid = log.run_id;
store.put(&log).await.unwrap();
store.delete(rid).await.unwrap();
assert!(store.get(rid).await.unwrap().is_none());
}
#[tokio::test]
async fn cost_estimate_round_trips_prompt_and_completion_split() {
let f = tmp_db();
let store = SqliteRunLogStore::new(f.path()).await.unwrap();
let mut log = project(RunId::new(), "writer", &[]);
log.cost_estimate = Some(Cost::new(0.005, 0.015, 0.0));
let rid = log.run_id;
store.put(&log).await.unwrap();
let back = store.get(rid).await.unwrap().expect("row exists");
let cost = back.cost_estimate.expect("cost preserved");
assert!((cost.prompt_usd - 0.005).abs() < 1e-12);
assert!((cost.completion_usd - 0.015).abs() < 1e-12);
assert!((cost.total_usd - 0.020).abs() < 1e-12);
}
#[tokio::test]
async fn cost_estimate_with_cache_keeps_total_authoritative_on_round_trip() {
let f = tmp_db();
let store = SqliteRunLogStore::new(f.path()).await.unwrap();
let mut log = project(RunId::new(), "writer", &[]);
log.cost_estimate = Some(Cost::new(0.005, 0.015, 0.001));
let rid = log.run_id;
store.put(&log).await.unwrap();
let cost = store
.get(rid)
.await
.unwrap()
.expect("row exists")
.cost_estimate
.expect("cost preserved");
assert!(
(cost.cache_usd).abs() < 1e-12,
"cache_usd reconstructs as 0"
);
assert!(
(cost.total_usd - 0.021).abs() < 1e-12,
"stored total (incl. cache) stays authoritative"
);
}