claw-core 0.1.2

Embedded local database engine for ClawDB — an agent-native cognitive database
Documentation
//! Integration tests for claw-core.

use std::time::Duration;

use claw_core::prelude::*;
use claw_core::{ClawConfig, MemoryRecord, MemoryType, ToolOutput};
use tempfile::TempDir;
use uuid::Uuid;

/// Create a test engine in a temporary directory.
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")
}

// ── 1. engine_opens_and_closes ───────────────────────────────────────────────

#[tokio::test]
async fn engine_opens_and_closes() {
    let dir = TempDir::new().expect("tempdir");
    let engine = make_engine(&dir).await;
    engine.close().await;
}

// ── 2. insert_and_get_memory ─────────────────────────────────────────────────

#[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;
}

// ── 3. update_memory ─────────────────────────────────────────────────────────

#[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;
}

// ── 4. delete_memory ─────────────────────────────────────────────────────────

#[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;
}

// ── 5. list_memories_with_type_filter ────────────────────────────────────────

#[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;
}

// ── 6. search_by_tag ─────────────────────────────────────────────────────────

#[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;
}

// ── 7. expire_ttl ────────────────────────────────────────────────────────────

#[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;
}

// ── 8. session_lifecycle ─────────────────────────────────────────────────────

#[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;
}

// ── 9. tool_output_record_and_list ───────────────────────────────────────────

#[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;
}

// ── 10. transaction_commit ───────────────────────────────────────────────────

#[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;
}

// ── 11. transaction_rollback ─────────────────────────────────────────────────

#[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;
}

// ── 12. snapshot_create_and_restore ─────────────────────────────────────────

#[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;
}

// ── 13. cache_stats_after_ops ────────────────────────────────────────────────

#[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");

    // insert_memory populates cache; first get_memory should be a hit.
    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;
}

// ── 14. fts_search ───────────────────────────────────────────────────────────

#[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;
}