use std::time::Duration;
use claw_core::prelude::*;
use claw_core::{ClawConfig, MemoryRecord, MemoryType, ToolOutput};
use tempfile::TempDir;
use uuid::Uuid;
async fn make_engine(dir: &TempDir) -> ClawEngine {
let config = ClawConfig::builder()
.db_path(dir.path().join("test.db"))
.cache_size_mb(1)
.build()
.expect("config");
ClawEngine::open(config).await.expect("engine open")
}
#[tokio::test]
async fn engine_opens_and_closes() {
let dir = TempDir::new().expect("tempdir");
let engine = make_engine(&dir).await;
engine.close().await;
}
#[tokio::test]
async fn insert_and_get_memory() {
let dir = TempDir::new().expect("tempdir");
let engine = make_engine(&dir).await;
let record = MemoryRecord::new(
"Paris is the capital of France",
MemoryType::Semantic,
vec!["geography".to_string()],
None,
);
let id = engine.insert_memory(&record).await.expect("insert");
let fetched = engine.get_memory(id).await.expect("get");
assert_eq!(fetched.id, id);
assert_eq!(fetched.content, "Paris is the capital of France");
assert_eq!(fetched.memory_type, MemoryType::Semantic);
assert_eq!(fetched.tags, vec!["geography".to_string()]);
assert!(fetched.ttl_seconds.is_none());
engine.close().await;
}
#[tokio::test]
async fn update_memory() {
let dir = TempDir::new().expect("tempdir");
let engine = make_engine(&dir).await;
let record = MemoryRecord::new("original", MemoryType::Working, vec![], None);
let id = engine.insert_memory(&record).await.expect("insert");
let before = engine.get_memory(id).await.expect("get before");
tokio::time::sleep(Duration::from_millis(10)).await;
engine
.update_memory(id, "updated content")
.await
.expect("update");
let after = engine.get_memory(id).await.expect("get after");
assert_eq!(after.content, "updated content");
assert!(after.updated_at >= before.updated_at);
engine.close().await;
}
#[tokio::test]
async fn delete_memory() {
let dir = TempDir::new().expect("tempdir");
let engine = make_engine(&dir).await;
let record = MemoryRecord::new("to delete", MemoryType::Episodic, vec![], None);
let id = engine.insert_memory(&record).await.expect("insert");
engine.delete_memory(id).await.expect("delete");
let result = engine.get_memory(id).await;
assert!(
matches!(result, Err(ClawError::NotFound { .. })),
"expected NotFound, got {result:?}"
);
engine.close().await;
}
#[tokio::test]
async fn list_memories_with_type_filter() {
let dir = TempDir::new().expect("tempdir");
let engine = make_engine(&dir).await;
let types = [
MemoryType::Semantic,
MemoryType::Semantic,
MemoryType::Episodic,
MemoryType::Working,
MemoryType::Procedural,
];
for (i, mt) in types.iter().enumerate() {
let r = MemoryRecord::new(format!("record {i}"), mt.clone(), vec![], None);
engine.insert_memory(&r).await.expect("insert");
}
let semantic = engine
.list_memories(Some(MemoryType::Semantic))
.await
.expect("list");
assert_eq!(semantic.len(), 2, "expected 2 semantic records");
let all = engine.list_memories(None).await.expect("list all");
assert_eq!(all.len(), 5);
engine.close().await;
}
#[tokio::test]
async fn search_by_tag() {
let dir = TempDir::new().expect("tempdir");
let engine = make_engine(&dir).await;
let r1 = MemoryRecord::new(
"alpha",
MemoryType::Semantic,
vec!["rust".to_string()],
None,
);
let r2 = MemoryRecord::new(
"beta",
MemoryType::Semantic,
vec!["python".to_string()],
None,
);
let r3 = MemoryRecord::new(
"gamma",
MemoryType::Semantic,
vec!["rust".to_string(), "python".to_string()],
None,
);
engine.insert_memory(&r1).await.expect("insert r1");
engine.insert_memory(&r2).await.expect("insert r2");
engine.insert_memory(&r3).await.expect("insert r3");
let rust_records = engine.search_by_tag("rust").await.expect("search");
assert_eq!(rust_records.len(), 2);
let python_records = engine.search_by_tag("python").await.expect("search");
assert_eq!(python_records.len(), 2);
let go_records = engine.search_by_tag("go").await.expect("search");
assert_eq!(go_records.len(), 0);
engine.close().await;
}
#[tokio::test]
async fn expire_ttl() {
let dir = TempDir::new().expect("tempdir");
let engine = make_engine(&dir).await;
let r = MemoryRecord::new("short-lived", MemoryType::Working, vec![], Some(1));
engine.insert_memory(&r).await.expect("insert");
let expired_early = engine.expire_ttl_memories().await.expect("expire");
assert_eq!(expired_early, 0, "should not have expired yet");
tokio::time::sleep(Duration::from_secs(2)).await;
let expired = engine.expire_ttl_memories().await.expect("expire");
assert_eq!(expired, 1);
engine.close().await;
}
#[tokio::test]
async fn session_lifecycle() {
let dir = TempDir::new().expect("tempdir");
let engine = make_engine(&dir).await;
let sid = engine.start_session().await.expect("start");
assert!(!sid.is_empty());
let session = engine.get_session(&sid).await.expect("get");
assert_eq!(session.id, sid);
assert!(session.ended_at.is_none());
engine.end_session(&sid).await.expect("end");
let ended = engine.get_session(&sid).await.expect("get ended");
assert!(ended.ended_at.is_some());
let sessions = engine.list_sessions(None).await.expect("list").items;
assert!(!sessions.is_empty());
assert!(sessions.iter().any(|s| s.id == sid));
engine.close().await;
}
#[tokio::test]
async fn tool_output_record_and_list() {
let dir = TempDir::new().expect("tempdir");
let engine = make_engine(&dir).await;
let session_id = "test-session-tool";
let output = ToolOutput {
id: Uuid::new_v4(),
session_id: session_id.to_string(),
tool_name: "calculator".to_string(),
output: serde_json::json!({"result": 42}),
success: true,
created_at: chrono::Utc::now(),
};
engine.record_tool_output(&output).await.expect("record");
let outputs = engine.list_tool_outputs(session_id).await.expect("list");
assert_eq!(outputs.len(), 1);
assert_eq!(outputs[0].tool_name, "calculator");
assert!(outputs[0].success);
engine.close().await;
}
#[tokio::test]
async fn transaction_commit() {
let dir = TempDir::new().expect("tempdir");
let engine = make_engine(&dir).await;
let r1 = MemoryRecord::new("tx record 1", MemoryType::Semantic, vec![], None);
let r2 = MemoryRecord::new("tx record 2", MemoryType::Episodic, vec![], None);
let id1 = r1.id;
let id2 = r2.id;
let mut tx = engine.transaction().await.expect("begin tx");
tx.insert_memory(&r1).await.expect("insert r1 in tx");
tx.insert_memory(&r2).await.expect("insert r2 in tx");
tx.commit().await.expect("commit");
let fetched1 = engine.get_memory(id1).await.expect("get r1");
let fetched2 = engine.get_memory(id2).await.expect("get r2");
assert_eq!(fetched1.content, "tx record 1");
assert_eq!(fetched2.content, "tx record 2");
engine.close().await;
}
#[tokio::test]
async fn transaction_rollback() {
let dir = TempDir::new().expect("tempdir");
let engine = make_engine(&dir).await;
let r1 = MemoryRecord::new("rollback record 1", MemoryType::Semantic, vec![], None);
let r2 = MemoryRecord::new("rollback record 2", MemoryType::Episodic, vec![], None);
let id1 = r1.id;
let id2 = r2.id;
let mut tx = engine.transaction().await.expect("begin tx");
tx.insert_memory(&r1).await.expect("insert r1");
tx.insert_memory(&r2).await.expect("insert r2");
tx.rollback().await.expect("rollback");
let res1 = engine.get_memory(id1).await;
let res2 = engine.get_memory(id2).await;
assert!(matches!(res1, Err(ClawError::NotFound { .. })));
assert!(matches!(res2, Err(ClawError::NotFound { .. })));
engine.close().await;
}
#[tokio::test]
async fn snapshot_create_and_restore() {
let dir = TempDir::new().expect("tempdir");
let snap_dir = dir.path().join("snapshots");
let db_path = dir.path().join("test.db");
let config = ClawConfig::builder()
.db_path(&db_path)
.cache_size_mb(1)
.snapshot_dir(&snap_dir)
.build()
.expect("config");
let mut engine = ClawEngine::open(config.clone()).await.expect("open");
let record = MemoryRecord::new("original content", MemoryType::Semantic, vec![], None);
let id = engine.insert_memory(&record).await.expect("insert");
let meta = engine.snapshot_create().await.expect("snapshot");
assert!(meta.path.exists());
engine
.update_memory(id, "mutated content")
.await
.expect("update");
let mutated = engine.get_memory(id).await.expect("get mutated");
assert_eq!(mutated.content, "mutated content");
engine.restore(&meta.path).await.expect("restore");
let restored = engine.get_memory(id).await.expect("get restored");
assert_eq!(restored.content, "original content");
engine.close().await;
}
#[tokio::test]
async fn cache_stats_after_ops() {
let dir = TempDir::new().expect("tempdir");
let engine = make_engine(&dir).await;
let record = MemoryRecord::new("cached record", MemoryType::Semantic, vec![], None);
let id = engine.insert_memory(&record).await.expect("insert");
let _ = engine.get_memory(id).await.expect("first read");
let _ = engine.get_memory(id).await.expect("second read");
let stats = engine.cache_stats().await;
assert!(
stats.hit_count > 0,
"expected at least one cache hit, got hit_count={}",
stats.hit_count
);
engine.close().await;
}
#[tokio::test]
async fn fts_search() {
let dir = TempDir::new().expect("tempdir");
let engine = make_engine(&dir).await;
let r1 = MemoryRecord::new("the quick brown fox", MemoryType::Semantic, vec![], None);
let r2 = MemoryRecord::new("lazy dog sleeps", MemoryType::Episodic, vec![], None);
let r3 = MemoryRecord::new("quick lazy cat", MemoryType::Working, vec![], None);
engine.insert_memory(&r1).await.expect("insert r1");
engine.insert_memory(&r2).await.expect("insert r2");
engine.insert_memory(&r3).await.expect("insert r3");
let results = engine.fts_search("quick").await.expect("fts quick");
assert_eq!(results.len(), 2, "expected 2 results for 'quick'");
let results = engine.fts_search("fox").await.expect("fts fox");
assert_eq!(results.len(), 1);
assert_eq!(results[0].content, "the quick brown fox");
let results = engine.fts_search("lazy").await.expect("fts lazy");
assert_eq!(results.len(), 2);
engine.close().await;
}