use tempfile::TempDir;
use tokensave::tokensave::TokenSave;
async fn make_project() -> (TempDir, TokenSave) {
let tmp = TempDir::new().unwrap();
std::fs::write(tmp.path().join("a.rs"), "pub fn hello() {}").unwrap();
let cg = TokenSave::init(tmp.path()).await.unwrap();
(tmp, cg)
}
#[tokio::test]
async fn record_decision_persists_and_recalls() {
let (_tmp, cg) = make_project().await;
let id = cg
.record_decision(
"use JWT for auth",
Some("session tokens flagged by legal"),
&["src/auth.rs".to_string()],
&["security".to_string(), "decision".to_string()],
)
.await
.unwrap();
assert!(id > 0);
let hits = cg.session_recall(Some("JWT"), None, 10).await.unwrap();
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].text, "use JWT for auth");
assert_eq!(
hits[0].reason.as_deref(),
Some("session tokens flagged by legal")
);
assert_eq!(hits[0].files, vec!["src/auth.rs"]);
assert_eq!(hits[0].tags, vec!["security", "decision"]);
}
#[tokio::test]
async fn session_recall_orders_newest_first_when_no_query() {
let (_tmp, cg) = make_project().await;
cg.record_decision("first", None, &[], &[]).await.unwrap();
tokio::time::sleep(std::time::Duration::from_millis(1100)).await;
cg.record_decision("second", None, &[], &[]).await.unwrap();
let hits = cg.session_recall(None, None, 10).await.unwrap();
assert_eq!(hits.len(), 2);
assert_eq!(hits[0].text, "second");
assert_eq!(hits[1].text, "first");
}
#[tokio::test]
async fn record_code_area_upserts_touch_count() {
let (_tmp, cg) = make_project().await;
cg.record_code_area("src/auth.rs", Some("OAuth provider"))
.await
.unwrap();
cg.record_code_area("src/auth.rs", None).await.unwrap();
cg.record_code_area("src/auth.rs", None).await.unwrap();
let areas = cg.list_code_areas(10).await.unwrap();
assert_eq!(areas.len(), 1);
assert_eq!(areas[0].path, "src/auth.rs");
assert_eq!(areas[0].touch_count, 3);
assert_eq!(areas[0].description.as_deref(), Some("OAuth provider"));
}
async fn insert_decision_at(cg: &TokenSave, text: &str, created_at: i64) {
cg.db()
.conn()
.execute(
"INSERT INTO memory_decisions (text, reason, created_at, files, tags) \
VALUES (?1, NULL, ?2, '[]', '[]')",
libsql::params![text, created_at],
)
.await
.unwrap();
}
#[tokio::test]
async fn session_recall_ranks_by_recency_decay_without_dropping_old() {
let (_tmp, cg) = make_project().await;
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs() as i64;
let very_old = now - 180 * 24 * 60 * 60;
let recent = now - 60;
insert_decision_at(&cg, "ancient decision", very_old).await;
insert_decision_at(&cg, "fresh decision", recent).await;
let hits = cg.session_recall(None, None, 10).await.unwrap();
assert_eq!(hits.len(), 2);
let texts: Vec<&str> = hits.iter().map(|d| d.text.as_str()).collect();
assert!(texts.contains(&"ancient decision"));
assert!(texts.contains(&"fresh decision"));
assert_eq!(hits[0].text, "fresh decision");
assert_eq!(hits[1].text, "ancient decision");
}
#[tokio::test]
async fn session_delta_is_compact_and_budget_bounded() {
let (_tmp, cg) = make_project().await;
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs() as i64;
for i in 0..12 {
insert_decision_at(&cg, &format!("decision number {i}"), now - i64::from(i)).await;
}
for i in 0..8 {
cg.record_code_area(&format!("src/area_{i}.rs"), None)
.await
.unwrap();
}
let delta = cg.session_delta().await.unwrap();
assert!(
delta.recent_decisions.len() <= 5,
"decisions capped, got {}",
delta.recent_decisions.len()
);
assert!(
delta.recent_code_areas.len() <= 5,
"code areas capped, got {}",
delta.recent_code_areas.len()
);
assert!(!delta.recent_decisions.is_empty());
assert!(!delta.recent_code_areas.is_empty());
assert!(!delta.recent_decisions[0].summary.is_empty());
assert_eq!(delta.recent_decisions[0].summary, "decision number 0");
}
#[tokio::test]
async fn session_delta_truncates_long_text() {
let (_tmp, cg) = make_project().await;
let long = "x".repeat(500);
cg.record_decision(&long, None, &[], &[]).await.unwrap();
let delta = cg.session_delta().await.unwrap();
assert_eq!(delta.recent_decisions.len(), 1);
let summary = &delta.recent_decisions[0].summary;
assert!(summary.ends_with('…'));
assert!(
summary.chars().count() <= 121,
"summary too long: {} chars",
summary.chars().count()
);
}
#[tokio::test]
async fn session_recall_filters_by_since() {
let (_tmp, cg) = make_project().await;
cg.record_decision("old decision", None, &[], &[])
.await
.unwrap();
tokio::time::sleep(std::time::Duration::from_millis(1100)).await;
let cutoff = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs() as i64;
tokio::time::sleep(std::time::Duration::from_millis(1100)).await;
cg.record_decision("new decision", None, &[], &[])
.await
.unwrap();
let hits = cg.session_recall(None, Some(cutoff), 10).await.unwrap();
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].text, "new decision");
}