clawdb 0.1.2

The cognitive database for AI agents — unified memory, semantic retrieval, branching, sync, and governance.
Documentation
use std::time::Duration;

use clawdb::{ClawDB, ClawDBConfig, ClawDBError, ClawDBResult, ClawDBSession};
use rstest::rstest;
use tempfile::TempDir;
use tokio::time::sleep;
use uuid::Uuid;

async fn test_engine() -> ClawDBResult<(ClawDB, TempDir)> {
    let temp = TempDir::new()?;
    let cfg = ClawDBConfig::default_for_dir(temp.path());
    let db = ClawDB::new(cfg).await?;
    Ok((db, temp))
}

async fn test_session(db: &ClawDB, role: &str) -> ClawDBResult<ClawDBSession> {
    db.session(
        Uuid::new_v4(),
        role,
        vec!["memory:read".to_string(), "memory:write".to_string()],
    )
    .await
}

#[tokio::test]
async fn engine_opens_all_components_healthy() -> ClawDBResult<()> {
    let (db, _tmp) = test_engine().await?;
    let report = db.health().await?;
    assert!(matches!(report.overall, clawdb::HealthStatus::Healthy));
    db.close().await
}

#[tokio::test]
async fn engine_graceful_shutdown() -> ClawDBResult<()> {
    let (db, _tmp) = test_engine().await?;
    let session = test_session(&db, "assistant").await?;
    let _ = db.remember(&session, "warmup").await?;
    db.shutdown().await
}

#[tokio::test]
async fn session_created_and_validated() -> ClawDBResult<()> {
    let (db, _tmp) = test_engine().await?;
    let sess = test_session(&db, "assistant").await?;
    let ctx = db.validate_session(&sess.guard_token).await?;
    assert_eq!(ctx.agent_id, sess.agent_id);
    db.close().await
}

#[tokio::test]
#[ignore = "requires controllable guard token TTL support"]
async fn session_expires_after_ttl() -> ClawDBResult<()> {
    let (db, _tmp) = test_engine().await?;
    let sess = test_session(&db, "assistant").await?;
    sleep(Duration::from_secs(2)).await;
    let err = db.validate_session(&sess.guard_token).await.unwrap_err();
    assert!(matches!(err, ClawDBError::SessionExpired(_)));
    db.close().await
}

#[tokio::test]
async fn session_revoked_denies_access() -> ClawDBResult<()> {
    let (db, _tmp) = test_engine().await?;
    let sess = test_session(&db, "assistant").await?;
    db.revoke_session(sess.id).await?;
    let err = db.validate_session(&sess.guard_token).await.unwrap_err();
    assert!(matches!(err, ClawDBError::SessionNotFound(_)) || matches!(err, ClawDBError::Guard(_)));
    db.close().await
}

#[tokio::test]
async fn remember_stores_in_core_and_vector() -> ClawDBResult<()> {
    let (db, _tmp) = test_engine().await?;
    let sess = test_session(&db, "assistant").await?;
    let r = db.remember(&sess, "dual write memory").await?;
    let recalled = db.recall(&sess, &[r.memory_id]).await?;
    assert_eq!(recalled.len(), 1);
    db.close().await
}

#[tokio::test]
#[ignore = "requires loaded deny policy in guard engine"]
async fn remember_guard_denied() -> ClawDBResult<()> {
    let (db, _tmp) = test_engine().await?;
    let sess = test_session(&db, "denied-role").await?;
    let err = db.remember(&sess, "blocked").await.unwrap_err();
    assert!(matches!(err, ClawDBError::Guard(_)));
    db.close().await
}

#[tokio::test]
async fn search_returns_semantic_results() -> ClawDBResult<()> {
    let (db, _tmp) = test_engine().await?;
    let sess = test_session(&db, "assistant").await?;
    for i in 0..5 {
        let _ = db.remember(&sess, &format!("semantic memory {i}")).await?;
    }
    let res = db.search_with_options(&sess, "semantic", 5, true, None).await?;
    assert!(!res.is_empty());
    db.close().await
}

#[tokio::test]
#[ignore = "requires redaction policy in guard engine"]
async fn search_guard_redacts_fields() -> ClawDBResult<()> {
    let (db, _tmp) = test_engine().await?;
    let sess = test_session(&db, "assistant").await?;
    let res = db.search(&sess, "anything").await?;
    if let Some(first) = res.first() {
        assert!(first.get("content").is_none() || first["content"] == "[REDACTED]");
    }
    db.close().await
}

#[tokio::test]
async fn recall_returns_all_requested() -> ClawDBResult<()> {
    let (db, _tmp) = test_engine().await?;
    let sess = test_session(&db, "assistant").await?;
    let mut ids = Vec::new();
    for i in 0..3 {
        let r = db.remember(&sess, &format!("recall-{i}")).await?;
        ids.push(r.memory_id);
    }
    let out = db.recall(&sess, &ids).await?;
    assert_eq!(out.len(), 3);
    db.close().await
}

#[tokio::test]
async fn transaction_commit_atomic() -> ClawDBResult<()> {
    let (db, _tmp) = test_engine().await?;
    let sess = test_session(&db, "assistant").await?;
    db.transaction(&sess, |_tx| async {
        Ok::<_, ClawDBError>(())
    })
    .await?;
    db.close().await
}

#[tokio::test]
async fn transaction_rollback_atomic() -> ClawDBResult<()> {
    let (db, _tmp) = test_engine().await?;
    let sess = test_session(&db, "assistant").await?;
    let r = db
        .transaction(&sess, |_tx| async {
            Err::<(), _>(ClawDBError::TransactionFailed {
                tx_id: Uuid::new_v4(),
                reason: "forced".to_string(),
            })
        })
        .await;
    assert!(r.is_err());
    db.close().await
}

#[tokio::test]
#[ignore = "requires deterministic conflict fixture"]
async fn transaction_conflict_aborted() -> ClawDBResult<()> {
    let (db, _tmp) = test_engine().await?;
    let _ = db;
    Ok(())
}

#[tokio::test]
#[ignore = "requires branch-isolated write API"]
async fn branch_fork_isolates_writes() -> ClawDBResult<()> {
    let (db, _tmp) = test_engine().await?;
    let _ = db;
    Ok(())
}

#[tokio::test]
async fn branch_merge_applies_changes() -> ClawDBResult<()> {
    let (db, _tmp) = test_engine().await?;
    let sess = test_session(&db, "assistant").await?;
    let a = db.branch(&sess, "a").await?;
    let b = db.branch(&sess, "b").await?;
    let _ = db.merge(&sess, a, b).await?;
    db.close().await
}

#[tokio::test]
async fn branch_diff_shows_changes() -> ClawDBResult<()> {
    let (db, _tmp) = test_engine().await?;
    let sess = test_session(&db, "assistant").await?;
    let a = db.branch(&sess, "a2").await?;
    let b = db.branch(&sess, "b2").await?;
    let _diff = db.diff(&sess, a, b).await?;
    db.close().await
}

#[tokio::test]
async fn sync_push_sends_pending() -> ClawDBResult<()> {
    let (db, _tmp) = test_engine().await?;
    let sess = test_session(&db, "assistant").await?;
    let _ = db.sync(&sess).await?;
    db.close().await
}

#[tokio::test]
#[ignore = "requires mocked remote sync hub"]
async fn sync_pull_applies_remote() -> ClawDBResult<()> {
    let (db, _tmp) = test_engine().await?;
    let _ = db;
    Ok(())
}

#[tokio::test]
async fn event_bus_emits_on_remember() -> ClawDBResult<()> {
    let (db, _tmp) = test_engine().await?;
    let sess = test_session(&db, "assistant").await?;
    let mut sub = db.subscribe();
    let _ = db.remember(&sess, "event me").await?;
    let ev = tokio::time::timeout(Duration::from_secs(2), sub.recv()).await;
    assert!(ev.is_ok());
    db.close().await
}

#[tokio::test]
async fn event_bus_emits_on_sync() -> ClawDBResult<()> {
    let (db, _tmp) = test_engine().await?;
    let sess = test_session(&db, "assistant").await?;
    let mut sub = db.subscribe();
    let _ = db.sync(&sess).await?;
    let ev = tokio::time::timeout(Duration::from_secs(2), sub.recv()).await;
    assert!(ev.is_ok());
    db.close().await
}

#[tokio::test]
#[ignore = "requires dynamic plugin fixture"]
async fn plugin_loaded_and_hooks_called() -> ClawDBResult<()> {
    let (db, _tmp) = test_engine().await?;
    let _ = db;
    Ok(())
}

#[tokio::test]
#[ignore = "requires dynamic plugin fixture"]
async fn plugin_capability_denied() -> ClawDBResult<()> {
    let (db, _tmp) = test_engine().await?;
    let _ = db;
    Ok(())
}

#[tokio::test]
async fn router_routes_semantic_to_vector() -> ClawDBResult<()> {
    let (db, _tmp) = test_engine().await?;
    let sess = test_session(&db, "assistant").await?;
    let _ = db.search_with_options(&sess, "router", 5, true, None).await?;
    db.close().await
}

#[tokio::test]
async fn router_routes_keyword_to_core() -> ClawDBResult<()> {
    let (db, _tmp) = test_engine().await?;
    let sess = test_session(&db, "assistant").await?;
    let _ = db.search_with_options(&sess, "router", 5, false, None).await?;
    db.close().await
}

#[tokio::test]
#[ignore = "planner internals are not currently exposed"]
async fn planner_parallelises_remember() -> ClawDBResult<()> {
    let (db, _tmp) = test_engine().await?;
    let _ = db;
    Ok(())
}

#[tokio::test]
async fn full_workflow_remember_branch_merge_sync() -> ClawDBResult<()> {
    let (db, _tmp) = test_engine().await?;
    let sess = test_session(&db, "assistant").await?;
    let _ = db.remember(&sess, "workflow-1").await?;
    let a = db.branch(&sess, "workflow-a").await?;
    let b = db.branch(&sess, "workflow-b").await?;
    let _ = db.merge(&sess, a, b).await?;
    let _ = db.sync(&sess).await?;
    db.close().await
}

#[rstest]
#[case("assistant")]
#[case("writer")]
#[tokio::test]
async fn session_fixture_supports_roles(#[case] role: &str) -> ClawDBResult<()> {
    let (db, _tmp) = test_engine().await?;
    let sess = test_session(&db, role).await?;
    assert_eq!(sess.role, role);
    db.close().await
}