use super::*;
use crate::core::sm::context::model::{Round, ToolTrace};
use chrono::{TimeZone, Utc};
use tempfile::tempdir;
fn ts() -> chrono::DateTime<Utc> {
Utc.with_ymd_and_hms(2026, 3, 4, 5, 6, 7)
.single()
.expect("valid ts")
}
fn sample() -> SmConversation {
let mut c = SmConversation::new();
c.compressed_context = "earlier: goal g-1 spawned s-2".to_string();
c.recent_rounds.push_back(Round::new(
"hi",
"ho",
ts(),
vec![ToolTrace::new("session_new", "s-2")],
));
c.recent_rounds
.push_back(Round::new("again", "sure", ts(), Vec::new()));
c.total_rounds = 12;
c.token_estimate = 999;
c
}
#[test]
fn store_path_is_under_sm_subdir() {
let dir = tempdir().expect("tempdir");
let store = ConversationStore::new(dir.path());
assert!(store.dir().ends_with("sm"));
let p = store.path_for("abc");
assert!(p.ends_with("conversation-abc.json"));
assert!(p.starts_with(store.dir()));
}
#[test]
fn save_then_load_round_trips() {
let dir = tempdir().expect("tempdir");
let store = ConversationStore::new(dir.path());
let conv = sample();
store.save("c1", &conv).expect("save");
let loaded = store.load("c1").expect("load");
assert_eq!(loaded, conv);
}
#[test]
fn save_creates_sm_subdir() {
let dir = tempdir().expect("tempdir");
let store = ConversationStore::new(dir.path());
assert!(!store.dir().exists(), "no I/O on construction");
store.save("c1", &sample()).expect("save");
assert!(
store.path_for("c1").exists(),
"state file exists after save"
);
}
#[test]
fn save_is_atomic_overwrite() {
let dir = tempdir().expect("tempdir");
let store = ConversationStore::new(dir.path());
let mut first = SmConversation::new();
first.total_rounds = 1;
store.save("c1", &first).expect("first save");
let mut second = SmConversation::new();
second.total_rounds = 2;
second.compressed_context = "v2".to_string();
store.save("c1", &second).expect("second save");
let loaded = store.load("c1").expect("load");
assert_eq!(loaded.total_rounds, 2);
assert_eq!(loaded.compressed_context, "v2");
let leftovers: Vec<_> = std::fs::read_dir(store.dir())
.expect("read sm dir")
.filter_map(|e| e.ok())
.map(|e| e.file_name().to_string_lossy().into_owned())
.filter(|n| n.contains(".tmp."))
.collect();
assert!(
leftovers.is_empty(),
"no leftover temp files: {leftovers:?}"
);
}
#[test]
fn load_missing_is_fresh() {
let dir = tempdir().expect("tempdir");
let store = ConversationStore::new(dir.path());
let loaded = store.load("never-saved").expect("load missing");
assert_eq!(loaded, SmConversation::new());
}
#[test]
fn load_rejects_malformed_file() {
let dir = tempdir().expect("tempdir");
let store = ConversationStore::new(dir.path());
std::fs::create_dir_all(store.dir()).expect("mkdir");
std::fs::write(store.path_for("bad"), b"{ not json").expect("write garbage");
let err = store.load("bad").expect_err("malformed must error");
assert!(matches!(err, ConversationStoreError::Serde { .. }));
}
#[test]
fn conv_id_is_sanitised() {
let dir = tempdir().expect("tempdir");
let store = ConversationStore::new(dir.path());
let nasty = "../../etc/passwd";
store
.save(nasty, &SmConversation::new())
.expect("save sanitised");
let p = store.path_for(nasty);
assert!(p.starts_with(store.dir()), "path stays under sm dir: {p:?}");
let name = p
.file_name()
.expect("filename")
.to_string_lossy()
.into_owned();
assert!(!name.contains('/'), "no separators in filename: {name}");
assert!(!name.contains(".."), "no traversal in filename: {name}");
let loaded = store.load(nasty).expect("load sanitised");
assert_eq!(loaded, SmConversation::new());
}
#[test]
fn concurrent_saves_use_distinct_temp_paths() {
let dir = tempdir().expect("tempdir");
let store = ConversationStore::new(dir.path());
for n in 0..200u64 {
let mut c = SmConversation::new();
c.total_rounds = n;
store.save("hot", &c).expect("save");
}
let loaded = store.load("hot").expect("load");
assert_eq!(loaded.total_rounds, 199);
let leftovers: Vec<_> = std::fs::read_dir(store.dir())
.expect("read sm dir")
.filter_map(|e| e.ok())
.map(|e| e.file_name().to_string_lossy().into_owned())
.filter(|n| n.contains(".tmp."))
.collect();
assert!(
leftovers.is_empty(),
"no leftover temp files: {leftovers:?}"
);
}
#[test]
fn concurrent_saves_same_conv_id_do_not_corrupt() {
let dir = tempdir().expect("tempdir");
let store = ConversationStore::new(dir.path());
std::fs::create_dir_all(store.dir()).expect("mkdir");
let a = store.clone();
let b = store.clone();
let ha = std::thread::spawn(move || {
for _ in 0..200 {
let mut c = SmConversation::new();
c.compressed_context = "AAAA".to_string();
c.total_rounds = 1;
a.save("dup", &c).expect("save A");
}
});
let hb = std::thread::spawn(move || {
for _ in 0..200 {
let mut c = SmConversation::new();
c.compressed_context = "BBBB".to_string();
c.total_rounds = 2;
b.save("dup", &c).expect("save B");
}
});
ha.join().expect("thread A");
hb.join().expect("thread B");
let loaded = store.load("dup").expect("load must not be corrupt");
assert!(
loaded == {
let mut c = SmConversation::new();
c.compressed_context = "AAAA".to_string();
c.total_rounds = 1;
c
} || loaded == {
let mut c = SmConversation::new();
c.compressed_context = "BBBB".to_string();
c.total_rounds = 2;
c
},
"final state is one writer's intact value, got {loaded:?}"
);
let leftovers: Vec<_> = std::fs::read_dir(store.dir())
.expect("read sm dir")
.filter_map(|e| e.ok())
.map(|e| e.file_name().to_string_lossy().into_owned())
.filter(|n| n.contains(".tmp."))
.collect();
assert!(
leftovers.is_empty(),
"no leftover temp files: {leftovers:?}"
);
}