use crate::brain::tools::context::ContextStore;
fn tmp_path(name: &str) -> std::path::PathBuf {
let mut p = std::env::temp_dir();
p.push(format!("opencrabs-ctx-{}-{name}", std::process::id()));
let _ = std::fs::create_dir_all(&p);
p.join("context_store.json")
}
fn store_with_bulk(session: &str, facts: usize) -> ContextStore {
let mut s = ContextStore::new(session.to_string());
s.facts = (0..facts)
.map(|i| format!("fact number {i} recorded"))
.collect();
s
}
#[tokio::test]
async fn concurrent_saves_never_leave_a_partial_object() {
let path = tmp_path("concurrent");
let _ = std::fs::remove_file(&path);
let mut handles = Vec::new();
for i in 0..32 {
let path = path.clone();
handles.push(tokio::spawn(async move {
let store = store_with_bulk("s", if i % 2 == 0 { 400 } else { 2 });
store.save(&path).await
}));
}
for h in handles {
let _ = h.await.expect("save task panicked");
}
ContextStore::load(&path, "s")
.await
.expect("store did not parse after concurrent saves");
}
#[tokio::test]
async fn every_intermediate_read_parses() {
let path = tmp_path("interleaved");
let _ = std::fs::remove_file(&path);
store_with_bulk("s", 10).save(&path).await.expect("seed");
let writer = {
let path = path.clone();
tokio::spawn(async move {
for i in 0..40 {
let store = store_with_bulk("s", if i % 2 == 0 { 300 } else { 1 });
let _ = store.save(&path).await;
}
})
};
let reader = {
let path = path.clone();
tokio::spawn(async move {
for _ in 0..40 {
if path.exists() {
ContextStore::load(&path, "s")
.await
.expect("a concurrent read saw a partial object");
}
tokio::task::yield_now().await;
}
})
};
writer.await.expect("writer panicked");
reader.await.expect("reader panicked");
}
#[tokio::test]
async fn no_temp_files_are_left_behind() {
let path = tmp_path("no-temps");
let _ = std::fs::remove_file(&path);
for i in 0..8 {
store_with_bulk("s", i * 20)
.save(&path)
.await
.expect("save");
}
let dir = path.parent().expect("parent");
let strays: Vec<_> = std::fs::read_dir(dir)
.expect("read dir")
.filter_map(|e| e.ok())
.map(|e| e.file_name().to_string_lossy().into_owned())
.filter(|n| n.ends_with(".tmp"))
.collect();
assert!(strays.is_empty(), "temp files left behind: {strays:?}");
}