use sessionwiki::util::project_key;
use std::path::Path;
static DATA_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
fn case_insensitive(dir: &Path) -> bool {
let probe = dir.join("CaseProbe");
std::fs::create_dir_all(&probe).unwrap();
dir.join("caseprobe").exists()
}
fn scratch() -> tempfile::TempDir {
match std::env::var_os("SESSIONWIKI_CASE_DIR") {
Some(dir) => tempfile::tempdir_in(dir).unwrap(),
None => tempfile::tempdir().unwrap(),
}
}
#[test]
fn a_case_variant_of_an_existing_directory_takes_its_on_disk_case() {
let root = scratch();
let base = std::fs::canonicalize(root.path()).unwrap();
std::fs::create_dir_all(base.join("MyProject/APO")).unwrap();
if !case_insensitive(&base) {
eprintln!("skipped: {} is case-sensitive", base.display());
return;
}
let variant = format!("{}/myproject/apo", base.display());
assert_eq!(
project_key(&variant),
format!("{}/MyProject/APO", base.display())
);
}
#[test]
fn a_path_spelled_as_on_disk_or_missing_is_left_alone() {
let root = scratch();
let base = std::fs::canonicalize(root.path()).unwrap();
std::fs::create_dir_all(base.join("Exact")).unwrap();
let exact = format!("{}/Exact", base.display());
assert_eq!(project_key(&exact), exact);
let gone = format!("{}/Deleted/Project", base.display());
assert_eq!(project_key(&gone), gone);
assert_eq!(project_key("relative/dir"), "relative/dir");
assert_eq!(project_key(""), "");
}
#[test]
fn two_directories_that_differ_only_in_case_are_not_merged() {
let root = scratch();
let base = std::fs::canonicalize(root.path()).unwrap();
if case_insensitive(&base) {
eprintln!("skipped: {} cannot hold both spellings", base.display());
return;
}
std::fs::create_dir_all(base.join("Proj")).unwrap();
std::fs::create_dir_all(base.join("proj")).unwrap();
for name in ["Proj", "proj"] {
let p = format!("{}/{name}", base.display());
assert_eq!(project_key(&p), p);
}
std::fs::create_dir_all(base.join("Only")).unwrap();
let other = format!("{}/only", base.display());
assert_eq!(project_key(&other), other);
}
#[test]
fn opening_an_index_respells_variants_recorded_before_the_fix() {
let _data = DATA_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let root = scratch();
let base = std::fs::canonicalize(root.path()).unwrap();
std::fs::create_dir_all(base.join("MyProject/APO")).unwrap();
if !case_insensitive(&base) {
eprintln!("skipped: {} is case-sensitive", base.display());
return;
}
let data = tempfile::tempdir().unwrap();
std::env::set_var("SESSIONWIKI_DATA", data.path());
let conn = sessionwiki::index::open().unwrap();
let real = format!("{}/MyProject/APO", base.display());
let variant = format!("{}/myproject/apo", base.display());
for (id, project) in [("a", &real), ("b", &variant), ("c", &variant)] {
conn.execute(
"INSERT INTO files(path, mtime, size, session_id, tool, project, title, started, msg_count, kind)
VALUES (?1, 0, 0, ?2, 'codex', ?3, ?2, '2026-09-01T00:00:00+00:00', 1, 'main')",
rusqlite::params![format!("/store/{id}.jsonl"), id, project],
)
.unwrap();
}
conn.execute("DELETE FROM meta WHERE key = 'project_case_upto'", [])
.unwrap();
drop(conn);
let conn = sessionwiki::index::open().unwrap();
let projects = sessionwiki::index::projects(&conn).unwrap();
let names: Vec<(&str, i64)> = projects
.iter()
.map(|p| (p.project.as_str(), p.sessions))
.collect();
assert_eq!(names, [(real.as_str(), 3)], "{names:?}");
}
#[test]
fn a_variant_written_after_the_pass_is_respelled_on_the_next_open() {
let _data = DATA_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let root = scratch();
let base = std::fs::canonicalize(root.path()).unwrap();
std::fs::create_dir_all(base.join("MyProject/APO")).unwrap();
if !case_insensitive(&base) {
eprintln!("skipped: {} is case-sensitive", base.display());
return;
}
let data = tempfile::tempdir().unwrap();
std::env::set_var("SESSIONWIKI_DATA", data.path());
let conn = sessionwiki::index::open().unwrap();
conn.execute(
"INSERT INTO files(path, mtime, size, session_id, tool, project, title, started, msg_count, kind)
VALUES ('/store/earlier.jsonl', 0, 0, 'earlier', 'codex', '/other', 'e', '2026-09-01T00:00:00+00:00', 1, 'main')",
[],
)
.unwrap();
drop(conn);
drop(sessionwiki::index::open().unwrap());
let real = format!("{}/MyProject/APO", base.display());
let variant = format!("{}/myproject/apo", base.display());
let conn = sessionwiki::index::open().unwrap();
conn.execute(
"INSERT INTO files(path, mtime, size, session_id, tool, project, title, started, msg_count, kind)
VALUES ('/store/late.jsonl', 0, 0, 'late', 'codex', ?1, 'late', '2026-09-01T00:00:00+00:00', 1, 'main')",
rusqlite::params![variant],
)
.unwrap();
drop(conn);
let conn = sessionwiki::index::open().unwrap();
let project: String = conn
.query_row(
"SELECT project FROM files WHERE session_id = 'late'",
[],
|r| r.get(0),
)
.unwrap();
assert_eq!(
project, real,
"a late write from an older binary was never respelled"
);
}