use super::*;
fn tempdir() -> tempfile::TempDir {
tempfile::tempdir().expect("tempdir")
}
fn quarantined_snapshots(dir: &std::path::Path) -> Vec<std::path::PathBuf> {
let prefix = format!("{SNAPSHOT_FILENAME}{CORRUPT_SUFFIX}");
let mut found: Vec<_> = std::fs::read_dir(dir)
.expect("read tempdir")
.map(|e| e.expect("dir entry").path())
.filter(|p| {
p.file_name()
.and_then(|n| n.to_str())
.is_some_and(|n| n.starts_with(&prefix))
})
.collect();
found.sort();
found
}
#[test]
fn snapshot_written_by_the_daemon_is_read_in_place() {
let dir = tempdir();
let snap = dir.path().join(SNAPSHOT_FILENAME);
std::fs::write(
&snap,
br#"[{"doc_id":"drawer-a","text":"the quick brown fox"},
{"doc_id":"drawer-b","text":"lazy dog sleeping"}]"#,
)
.unwrap();
let idx = PalaceBm25Index::load_or_create(dir.path()).unwrap();
assert_eq!(idx.doc_count(), 2);
assert!(!idx.is_dirty(), "a freshly loaded snapshot is not dirty");
let hits = idx.search("fox", 10);
assert_eq!(hits.len(), 1, "got: {hits:?}");
assert_eq!(hits[0].doc_id, "drawer-a");
assert!(
idx.missing_docs(&["drawer-a".into(), "drawer-b".into()])
.is_empty(),
"both daemon-written documents must be present by id"
);
}
#[test]
fn flush_round_trips() {
let dir = tempdir();
let mut idx = PalaceBm25Index::load_or_create(dir.path()).unwrap();
idx.index_doc("x", "one two three");
idx.flush().unwrap();
let raw = std::fs::read_to_string(idx.snapshot_path()).unwrap();
assert!(raw.contains("\"doc_id\":\"x\""), "got: {raw}");
assert!(raw.contains("\"text\":\"one two three\""), "got: {raw}");
let reopened = PalaceBm25Index::load_or_create(dir.path()).unwrap();
assert_eq!(reopened.doc_count(), 1);
assert!(reopened.search("two", 5).iter().any(|h| h.doc_id == "x"));
}
#[test]
fn search_returns_hits() {
let dir = tempdir();
let mut idx = PalaceBm25Index::load_or_create(dir.path()).unwrap();
idx.index_doc("doc1", "authentication login password");
idx.index_doc("doc2", "rendering ui components");
let hits = idx.search("authentication", 5);
assert_eq!(hits.len(), 1, "got: {hits:?}");
assert_eq!(hits[0].doc_id, "doc1");
assert!(hits[0].score > 0.0);
}
#[test]
fn search_clamps_a_zero_top_k() {
let dir = tempdir();
let mut idx = PalaceBm25Index::load_or_create(dir.path()).unwrap();
idx.index_doc("doc1", "alpha");
assert_eq!(idx.search("alpha", 0).len(), 1);
}
#[test]
fn index_doc_marks_dirty() {
let dir = tempdir();
let mut idx = PalaceBm25Index::load_or_create(dir.path()).unwrap();
assert!(!idx.is_dirty());
idx.index_doc("d", "hello");
assert!(idx.is_dirty());
idx.flush().unwrap();
assert!(!idx.is_dirty());
}
#[test]
fn delete_doc_removes_and_marks_dirty() {
let dir = tempdir();
let mut idx = PalaceBm25Index::load_or_create(dir.path()).unwrap();
idx.index_doc("d", "alpha beta");
idx.flush().unwrap();
assert!(idx.delete_doc("d"));
assert!(idx.is_dirty());
assert_eq!(idx.doc_count(), 0);
assert!(idx.search("alpha", 5).is_empty());
idx.flush().unwrap();
assert!(!idx.delete_doc("never-existed"));
assert!(!idx.is_dirty(), "an unknown-id delete must not re-dirty");
}
#[test]
fn stats_track_docs_and_bytes() {
let dir = tempdir();
let mut idx = PalaceBm25Index::load_or_create(dir.path()).unwrap();
assert_eq!(idx.doc_count(), 0);
assert_eq!(idx.total_text_bytes(), 0);
idx.index_doc("a", "hello");
idx.index_doc("b", "world!");
assert_eq!(idx.doc_count(), 2);
assert_eq!(idx.total_text_bytes(), 11);
assert!(idx.delete_doc("a"));
assert_eq!(idx.doc_count(), 1);
assert_eq!(idx.total_text_bytes(), 6);
}
#[test]
fn missing_docs_answers_by_identity_not_count() {
let dir = tempdir();
let mut idx = PalaceBm25Index::load_or_create(dir.path()).unwrap();
idx.index_doc("a", "alpha");
idx.index_doc("stale", "a drawer that was deleted from the palace");
let asked = vec!["a".to_string(), "b".to_string()];
assert_eq!(
idx.doc_count(),
asked.len(),
"precondition: the count comparison is satisfied and still wrong"
);
assert_eq!(idx.missing_docs(&asked), vec!["b".to_string()]);
idx.index_doc("b", "beta");
assert!(idx.missing_docs(&asked).is_empty());
assert!(
idx.missing_docs(&[]).is_empty(),
"an empty request is trivially covered"
);
}
#[test]
fn load_recovers_from_a_corrupt_snapshot() {
let dir = tempdir();
std::fs::write(dir.path().join(SNAPSHOT_FILENAME), b"not valid json").unwrap();
let idx = PalaceBm25Index::load_or_create(dir.path()).unwrap();
assert_eq!(idx.doc_count(), 0);
}
#[test]
fn a_corrupt_snapshot_is_quarantined_before_the_next_flush_can_overwrite_it() {
const TRUNCATED: &[u8] = br#"[{"doc_id":"drawer-a","text":"the quick brown fox"},
{"doc_id":"drawer-b","text":"lazy dog sleep"#;
let dir = tempdir();
std::fs::write(dir.path().join(SNAPSHOT_FILENAME), TRUNCATED).unwrap();
let mut idx = PalaceBm25Index::load_or_create(dir.path()).unwrap();
assert_eq!(idx.doc_count(), 0, "the corrupt corpus is not readable");
idx.index_doc("drawer-c", "a write that arrives after the failed load");
idx.flush().expect("flush");
let quarantined = quarantined_snapshots(dir.path());
assert_eq!(
quarantined.len(),
1,
"the corrupt snapshot must be moved aside, got: {quarantined:?}"
);
assert_eq!(
std::fs::read(&quarantined[0]).unwrap(),
TRUNCATED,
"the quarantined file must hold the original bytes verbatim"
);
let live = std::fs::read_to_string(idx.snapshot_path()).unwrap();
assert!(live.contains("drawer-c"), "got: {live}");
assert!(
!live.contains("drawer-a"),
"the rebuilt snapshot is genuinely empty-plus-one — that is why the \
quarantine copy is the only thing standing between this flush and the \
corpus; got: {live}"
);
}
#[test]
#[cfg(unix)]
fn an_unquarantinable_corrupt_snapshot_fails_the_load() {
use std::os::unix::fs::PermissionsExt;
if unsafe { libc::geteuid() } == 0 {
return;
}
let dir = tempdir();
let snap = dir.path().join(SNAPSHOT_FILENAME);
std::fs::write(&snap, b"not valid json").unwrap();
std::fs::set_permissions(dir.path(), std::fs::Permissions::from_mode(0o500)).unwrap();
let result = PalaceBm25Index::load_or_create(dir.path());
std::fs::set_permissions(dir.path(), std::fs::Permissions::from_mode(0o755)).unwrap();
let err = result
.err()
.expect("a corrupt snapshot that cannot be moved aside must fail the load");
assert!(
format!("{err:#}").contains("quarantine corrupt BM25 snapshot"),
"got: {err:#}"
);
assert_eq!(
std::fs::read(&snap).unwrap(),
b"not valid json",
"the corrupt snapshot must survive a failed quarantine"
);
assert!(
quarantined_snapshots(dir.path()).is_empty(),
"no quarantine file can exist when the rename failed"
);
}
#[test]
#[cfg(unix)]
fn load_propagates_an_unreadable_snapshot() {
use std::os::unix::fs::PermissionsExt;
let dir = tempdir();
let snap = dir.path().join(SNAPSHOT_FILENAME);
std::fs::write(&snap, br#"[{"doc_id":"a","text":"alpha"}]"#).unwrap();
std::fs::set_permissions(&snap, std::fs::Permissions::from_mode(0o000)).unwrap();
let result = PalaceBm25Index::load_or_create(dir.path());
std::fs::set_permissions(&snap, std::fs::Permissions::from_mode(0o644)).unwrap();
if unsafe { libc::geteuid() } == 0 {
return;
}
assert!(
result.is_err(),
"an unreadable snapshot must propagate, not read as an empty corpus"
);
}
#[test]
#[cfg(unix)]
fn a_failed_flush_leaves_the_index_dirty() {
use std::os::unix::fs::PermissionsExt;
if unsafe { libc::geteuid() } == 0 {
return;
}
let dir = tempdir();
let mut idx = PalaceBm25Index::load_or_create(dir.path()).unwrap();
idx.index_doc("a", "alpha");
std::fs::set_permissions(dir.path(), std::fs::Permissions::from_mode(0o500)).unwrap();
let result = idx.flush();
std::fs::set_permissions(dir.path(), std::fs::Permissions::from_mode(0o755)).unwrap();
assert!(result.is_err(), "a read-only directory must fail the flush");
assert!(
idx.is_dirty(),
"a failed flush must leave the index dirty so the next tick retries"
);
idx.flush().expect("the retry must succeed");
assert!(!idx.is_dirty());
}