use clawdb::{ClawDB, ClawDBConfig, ClawDBError, ClawDBResult, ClawDBSession};
use proptest::prelude::*;
use std::collections::HashSet;
use std::sync::Arc;
use tempfile::TempDir;
use tokio::sync::Mutex;
use uuid::Uuid;
fn arb_memory_content() -> impl Strategy<Value = String> {
r#"[a-zA-Z0-9_\-\.]{5,50}"#
}
async fn setup_test_db() -> 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]
fn prop_concurrent_remembers_no_corruption() {
proptest!(|(contents in prop::collection::vec(arb_memory_content(), 5..20)| {
futures::executor::block_on(async {
let (db, _tmp) = setup_test_db().await.unwrap();
let sess = test_session(&db, "assistant").await.unwrap();
let mut handles = vec![];
for content in contents {
let db_clone = db.clone();
let sess_clone = sess.clone();
let handle = tokio::spawn(async move {
db_clone.remember(&sess_clone, &content).await
});
handles.push(handle);
}
let results: Vec<_> = futures::future::join_all(handles)
.await
.into_iter()
.collect();
let successes = results.iter().filter(|r| r.is_ok()).count();
prop_assert!(successes > 0, "At least one remember should succeed");
let _ = db.close().await;
}).unwrap();
});
}
#[tokio::test]
fn prop_concurrent_branches_isolated() {
proptest!(|(branch_names in prop::collection::vec(
r#"[a-z][a-z0-9_]{0,20}"#,
3..10
)| {
futures::executor::block_on(async {
let (db, _tmp) = setup_test_db().await.unwrap();
let sess = test_session(&db, "assistant").await.unwrap();
let mut handles = vec![];
for name in &branch_names {
let db_clone = db.clone();
let sess_clone = sess.clone();
let name_clone = name.clone();
let handle = tokio::spawn(async move {
db_clone.branch(&sess_clone, &name_clone).await
});
handles.push(handle);
}
let results: Vec<_> = futures::future::join_all(handles)
.await
.into_iter()
.collect();
let branch_ids: HashSet<_> = results
.iter()
.filter_map(|r| r.as_ref().ok().map(|id| id.clone()))
.collect();
prop_assert_eq!(branch_ids.len(), branch_names.len(),
"Each branch operation should create a unique ID");
let _ = db.close().await;
}).unwrap();
});
}
#[tokio::test]
fn prop_transaction_isolation_level() {
proptest!(|(batch_size in 2usize..10)| {
futures::executor::block_on(async {
let (db, _tmp) = setup_test_db().await.unwrap();
let sess = test_session(&db, "assistant").await.unwrap();
let counter = Arc::new(Mutex::new(0u32));
let mut handles = vec![];
for i in 0..batch_size {
let db_clone = db.clone();
let sess_clone = sess.clone();
let counter_clone = counter.clone();
let handle = tokio::spawn(async move {
let result = db_clone
.transaction(&sess_clone, |_tx| async {
let mut guard = counter_clone.lock().await;
*guard += 1;
Ok::<_, ClawDBError>(())
})
.await;
(i, result)
});
handles.push(handle);
}
let results: Vec<_> = futures::future::join_all(handles)
.await
.into_iter()
.collect();
let attempts = results.len();
prop_assert_eq!(attempts, batch_size, "All transaction attempts should complete");
let final_count = *counter.lock().await;
prop_assert_eq!(final_count, batch_size as u32, "All transactions should have executed");
let _ = db.close().await;
}).unwrap();
});
}
#[tokio::test]
fn prop_full_workflow_consistency() {
proptest!(|(
memory_ops in 1usize..5,
branch_ops in 1usize..3
)| {
futures::executor::block_on(async {
let (db, _tmp) = setup_test_db().await.unwrap();
let sess = test_session(&db, "assistant").await.unwrap();
let mut memory_ids = vec![];
for i in 0..memory_ops {
if let Ok(result) = db.remember(&sess, &format!("memory-{}", i)).await {
memory_ids.push(result.memory_id);
}
}
prop_assert!(!memory_ids.is_empty(), "At least one memory should be stored");
let mut branch_ids = vec![];
for i in 0..branch_ops {
if let Ok(id) = db.branch(&sess, &format!("branch-{}", i)).await {
branch_ids.push(id);
}
}
prop_assert!(!branch_ids.is_empty(), "At least one branch should be created");
if branch_ids.len() >= 2 {
let merge_result = db.merge(&sess, branch_ids[0].clone(), branch_ids[1].clone()).await;
prop_assert!(merge_result.is_ok(), "Merge should succeed with valid branches");
}
if !memory_ids.is_empty() {
let recalled = db.recall(&sess, &memory_ids).await;
prop_assert!(recalled.is_ok(), "Recall should succeed");
let memories = recalled.unwrap();
prop_assert_eq!(memories.len(), memory_ids.len(),
"Should recall all stored memories");
}
let _ = db.close().await;
}).unwrap();
});
}
#[tokio::test]
fn prop_diff_reflects_concurrent_changes() {
proptest!(|(branch_name in "[a-z][a-z0-9_]{0,15}")| {
futures::executor::block_on(async {
let (db, _tmp) = setup_test_db().await.unwrap();
let sess = test_session(&db, "assistant").await.unwrap();
let _ = db.remember(&sess, "initial").await;
let branch_a = match db.branch(&sess, &format!("{}-a", branch_name)).await {
Ok(id) => id,
Err(_) => return Ok(()),
};
let branch_b = match db.branch(&sess, &format!("{}-b", branch_name)).await {
Ok(id) => id,
Err(_) => return Ok(()),
};
let diff_result = db.diff(&sess, branch_a.clone(), branch_b.clone()).await;
prop_assert!(diff_result.is_ok(), "Diff should complete successfully");
let _ = db.close().await;
}).unwrap();
});
}
#[tokio::test]
fn prop_concurrent_syncs_safe() {
proptest!(|(sync_count in 2usize..6)| {
futures::executor::block_on(async {
let (db, _tmp) = setup_test_db().await.unwrap();
let sess = test_session(&db, "assistant").await.unwrap();
for i in 0..2 {
let _ = db.remember(&sess, &format!("sync-test-{}", i)).await;
}
let mut handles = vec![];
for _ in 0..sync_count {
let db_clone = db.clone();
let sess_clone = sess.clone();
let handle = tokio::spawn(async move {
db_clone.sync(&sess_clone).await
});
handles.push(handle);
}
let results: Vec<_> = futures::future::join_all(handles)
.await
.into_iter()
.collect();
let successes = results.iter().filter(|r| r.is_ok()).count();
prop_assert!(successes > 0, "At least one sync should succeed");
let _ = db.close().await;
}).unwrap();
});
}
#[tokio::test]
fn prop_random_operation_sequence_safe() {
proptest!(|(
ops in prop::collection::vec(0u8..5, 10..30)
)| {
futures::executor::block_on(async {
let (db, _tmp) = setup_test_db().await.unwrap();
let sess = test_session(&db, "assistant").await.unwrap();
for (idx, op) in ops.iter().enumerate() {
let result = match op % 5 {
0 => db.remember(&sess, &format!("op-{}", idx)).await.map(|_| ()),
1 => db.search(&sess, "test").await.map(|_| ()),
2 => db.branch(&sess, &format!("branch-{}", idx)).await.map(|_| ()),
3 => db.sync(&sess).await.map(|_| ()),
_ => db.health().await.map(|_| ()),
};
let _ = result;
}
let _ = db.close().await;
}).unwrap();
});
}