use std::path::{Path, PathBuf};
use supercode_harness::reduce::ReductionLog;
use supercode_harness::session::Session;
use supercode_harness::sidecar::SidecarWriter;
use supercode_harness::{ChatMessage, SessionStore};
fn fixture(name: &str) -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures")
.join(name)
}
fn temp_dir(tag: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!(
"supercode-sidecarstore-{tag}-{}",
std::process::id()
));
std::fs::create_dir_all(&dir).unwrap();
dir
}
fn family_present(root: &Path, name: &str) -> Vec<String> {
let exts = [
"jsonl",
"meta.json",
"sidecar.jsonl",
"reduction.json",
"events.jsonl",
];
exts.iter()
.filter(|ext| root.join(format!("{name}.{ext}")).exists())
.map(|ext| ext.to_string())
.collect()
}
#[test]
fn sidecar_store_lifecycle() {
let dir = temp_dir("lifecycle");
let store = SessionStore::open(&dir).unwrap();
let name = "sess1";
store
.save(name, "A session", "{\"role\":\"user\"}\n")
.unwrap();
store
.save_reduction_log(name, &ReductionLog::default())
.unwrap();
std::fs::write(dir.join(format!("{name}.events.jsonl")), "{}\n").unwrap();
let session = Session::from_codex(fixture("codex_session.jsonl")).unwrap();
let imported_count = session.messages.len();
let sidecar_path = store.sidecar_path(name);
assert_eq!(sidecar_path, dir.join(format!("{name}.sidecar.jsonl")));
let mut writer = SidecarWriter::create(&sidecar_path, &session).unwrap();
let appended = vec![
ChatMessage::user("hello again"),
ChatMessage::assistant("sure, on it"),
ChatMessage::tool_result("call_9", "shell", "ok"),
];
for m in &appended {
writer.append(m).unwrap();
}
drop(writer);
assert_eq!(
family_present(&dir, name).len(),
5,
"all five family members should exist before archive/delete"
);
let raw = store
.load_sidecar(name)
.unwrap()
.expect("sidecar must exist");
let reloaded = Session::from_native_str(&raw).unwrap();
assert_eq!(
reloaded.messages.len(),
imported_count + appended.len(),
"reloaded messages must be imported + appended"
);
for (i, (orig, got)) in session.messages.iter().zip(&reloaded.messages).enumerate() {
assert_eq!(orig.role, got.role, "imported message {i} role");
assert_eq!(orig.content, got.content, "imported message {i} content");
assert_eq!(orig.metadata, got.metadata, "imported message {i} metadata");
}
for (i, (orig, got)) in appended
.iter()
.zip(&reloaded.messages[imported_count..])
.enumerate()
{
assert_eq!(orig.role, got.role, "appended message {i} role");
assert_eq!(orig.content, got.content, "appended message {i} content");
}
store.archive(name).unwrap();
assert_eq!(
family_present(&dir, name).len(),
0,
"nothing should remain in the active dir after archive"
);
let archived_dir = dir.join("archived");
assert_eq!(
family_present(&archived_dir, name).len(),
5,
"all five family members should have moved into archived/"
);
let raw_after_archive = store
.load_sidecar(name)
.unwrap()
.expect("sidecar still resolves (archived fallback) after archive");
assert_eq!(raw_after_archive, raw, "archived sidecar content unchanged");
assert!(
store.load_reduction_log(name).unwrap().is_some(),
"reduction log still resolves after archive"
);
store.delete(name).unwrap();
assert!(store.load_sidecar(name).unwrap().is_none());
assert!(store.load_reduction_log(name).unwrap().is_none());
assert_eq!(
family_present(&dir, name).len(),
0,
"no active family member should remain after delete"
);
assert_eq!(
family_present(&archived_dir, name).len(),
0,
"no archived family member should remain after delete"
);
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn torn_tail_tolerated() {
let dir = temp_dir("torn");
let store = SessionStore::open(&dir).unwrap();
let name = "torn1";
let session = Session::from_codex(fixture("codex_session.jsonl")).unwrap();
let sidecar_path = store.sidecar_path(name);
let mut writer = SidecarWriter::create(&sidecar_path, &session).unwrap();
writer
.append(&ChatMessage::user("first appended turn"))
.unwrap();
writer
.append(&ChatMessage::assistant("second appended turn"))
.unwrap();
writer
.append(&ChatMessage::assistant(
"third appended turn, about to be torn",
))
.unwrap();
drop(writer);
let mut bytes = std::fs::read(&sidecar_path).unwrap();
let cut = bytes.len().saturating_sub(20);
bytes.truncate(cut);
std::fs::write(&sidecar_path, &bytes).unwrap();
let raw = store.load_sidecar(name).unwrap().unwrap();
let reloaded = Session::from_native_str(&raw).expect("torn tail must not error");
assert!(reloaded
.messages
.iter()
.any(|m| m.content.as_deref() == Some("first appended turn")));
assert!(reloaded
.messages
.iter()
.any(|m| m.content.as_deref() == Some("second appended turn")));
assert!(!reloaded
.messages
.iter()
.any(|m| m.content.as_deref() == Some("third appended turn, about to be torn")));
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn invalid_name_never_produces_sidecar_or_reduction_files() {
let dir = temp_dir("badname");
let store = SessionStore::open(&dir).unwrap();
for bad in ["../escape", "..", "a/b", "/abs", "", " ", ".", "x\0y"] {
assert!(
store.save_sidecar(bad, "{}").is_err(),
"save_sidecar should reject `{bad}`"
);
assert!(
store.load_sidecar(bad).is_err(),
"load_sidecar should reject `{bad}`"
);
assert!(
store
.save_reduction_log(bad, &ReductionLog::default())
.is_err(),
"save_reduction_log should reject `{bad}`"
);
assert!(
store.load_reduction_log(bad).is_err(),
"load_reduction_log should reject `{bad}`"
);
}
assert!(!dir.parent().unwrap().join("escape.sidecar.jsonl").exists());
std::fs::remove_dir_all(&dir).ok();
}