use super::*;
use trusty_common::memory_core::palace::Drawer;
use uuid::Uuid;
#[tokio::test]
async fn backfill_state_palace_is_disabled_without_a_lane() {
let tmp = tempfile::tempdir().expect("tempdir");
let state = AppState::new(tmp.path().to_path_buf());
assert!(
state.bm25.is_none(),
"the lane must still be off by default — this PR does not flip it"
);
let handle = test_handle(tmp.path(), &["some content"]);
let report = backfill_state_palace(&state, &handle, "anything", false).await;
assert_eq!(report.status, BackfillStatus::Disabled);
assert_eq!(
report.missing_after, None,
"a run that never happened has verified nothing"
);
assert!(!report.fully_indexed());
}
fn test_handle(
dir: &std::path::Path,
contents: &[&str],
) -> trusty_common::memory_core::retrieval::PalaceHandle {
use trusty_common::memory_core::palace::PalaceId;
use trusty_common::memory_core::store::kg::KnowledgeGraph;
use trusty_common::memory_core::store::vector::UsearchStore;
let vs = UsearchStore::new(dir.join("idx.usearch"), 384).expect("vector store");
let kg = KnowledgeGraph::open(&dir.join("kg.db")).expect("kg");
let handle = trusty_common::memory_core::retrieval::PalaceHandle::new(
PalaceId::new("unit-test"),
String::new(),
vs,
kg,
);
{
let mut drawers = handle.drawers.write();
for c in contents {
drawers.push(Drawer::new(Uuid::new_v4(), *c));
}
}
handle
}
#[tokio::test]
async fn backfill_reports_index_unavailable_when_the_snapshot_cannot_be_opened() {
let tmp = tempfile::tempdir().expect("tempdir");
let lane = crate::bm25_lane::Bm25Lane::with_limits(tmp.path().to_path_buf(), 3, None);
std::fs::create_dir_all(tmp.path().join("ghost")).expect("palace dir");
std::fs::write(
tmp.path().join("ghost").join("bm25"),
b"a file where the index directory belongs",
)
.expect("block the index dir");
let docs = PalaceDocs::from_pairs(vec![("d1".to_string(), "alpha beta".to_string())]);
let started = std::time::Instant::now();
let report = backfill_palace(&lane, "ghost", docs, false).await;
assert_eq!(report.status, BackfillStatus::IndexUnavailable);
assert_eq!(report.indexed, 0);
assert_eq!(report.drawers_total, 1);
assert_eq!(
report.missing_after, None,
"a coverage question that could not be asked must never read as covered"
);
assert!(!report.fully_indexed());
assert!(
started.elapsed() < std::time::Duration::from_secs(5),
"an unopenable index must fail fast, not spend the palace budget"
);
lane.shutdown().await;
}
#[tokio::test]
async fn empty_palace_is_already_indexed() {
let tmp = tempfile::tempdir().expect("tempdir");
let lane = crate::bm25_lane::Bm25Lane::with_limits(tmp.path().to_path_buf(), 3, None);
let report = backfill_palace(&lane, "empty", PalaceDocs::default(), false).await;
assert_eq!(report.status, BackfillStatus::AlreadyIndexed);
assert!(report.fully_indexed());
assert_eq!(report.missing_after, Some(0));
assert_eq!(report.drawers_total, 0);
}
#[tokio::test]
async fn a_palace_of_only_blank_drawers_is_covered_and_counted() {
let tmp = tempfile::tempdir().expect("tempdir");
let lane = crate::bm25_lane::Bm25Lane::with_limits(tmp.path().to_path_buf(), 3, None);
let docs = PalaceDocs {
docs: Vec::new(),
skipped_empty: 3,
};
let report = backfill_palace(&lane, "blanks", docs, false).await;
assert!(report.fully_indexed());
assert_eq!(report.drawers_total, 3, "the palace holds three drawers");
assert_eq!(report.skipped_empty, 3);
}
#[test]
fn fully_indexed_requires_a_verified_empty_missing_set() {
let base = BackfillReport {
palace: "p".into(),
status: BackfillStatus::Completed,
drawers_total: 10,
skipped_empty: 0,
indexed: 10,
failed: 0,
missing_after: Some(0),
final_doc_count: Some(10),
elapsed_ms: 1,
};
assert!(base.fully_indexed());
let stale_bloat = BackfillReport {
missing_after: Some(1),
final_doc_count: Some(400),
..base.clone()
};
assert!(
!stale_bloat.fully_indexed(),
"400 documents cannot cover 10 drawers when one of the 10 is absent"
);
assert_eq!(stale_bloat.stale_doc_estimate(), Some(390));
let no_verification = BackfillReport {
missing_after: None,
..base.clone()
};
assert!(
!no_verification.fully_indexed(),
"an unverifiable run must not claim coverage"
);
for status in [
BackfillStatus::AlreadyIndexed,
BackfillStatus::Completed,
BackfillStatus::Partial,
BackfillStatus::IndexUnavailable,
BackfillStatus::Disabled,
] {
let unverified = BackfillReport {
status,
missing_after: None,
..base.clone()
};
assert!(
!unverified.fully_indexed(),
"{status:?} must not claim coverage without a verified missing set"
);
}
}
#[test]
fn short_circuit_reports_are_never_covered() {
for status in [
BackfillStatus::Disabled,
BackfillStatus::IndexUnavailable,
BackfillStatus::Partial,
] {
let r = BackfillReport::short_circuit("p", status, 7);
assert_eq!(r.missing_after, None);
assert!(!r.fully_indexed(), "{status:?}");
assert_eq!(r.drawers_total, 7);
}
}
#[test]
fn stale_doc_estimate_is_reported_not_acted_on() {
let mut r = BackfillReport::short_circuit("p", BackfillStatus::Completed, 10);
r.skipped_empty = 2;
r.missing_after = Some(0);
r.final_doc_count = Some(50);
assert_eq!(
r.stale_doc_estimate(),
Some(42),
"8 indexable drawers, 50 documents held"
);
assert!(r.fully_indexed(), "stale documents do not remove coverage");
r.final_doc_count = Some(3);
assert_eq!(r.stale_doc_estimate(), Some(0), "never negative");
r.final_doc_count = None;
assert_eq!(r.stale_doc_estimate(), None);
}
#[test]
fn docs_from_drawers_splits_blank_from_indexable() {
let room = Uuid::new_v4();
let drawers: Vec<Drawer> = ["real content", "", " ", "\n\t", "also real"]
.iter()
.map(|c| Drawer::new(room, *c))
.collect();
let split = docs_from_drawers(&drawers);
let texts: Vec<&str> = split.docs.iter().map(|(_, t)| t.as_str()).collect();
assert_eq!(texts, vec!["real content", "also real"]);
assert_eq!(split.skipped_empty, 3);
assert_eq!(split.drawers_total(), 5);
for (id, _) in &split.docs {
assert!(
drawers.iter().any(|d| d.id.to_string() == *id),
"doc ids must be the drawer ids the coverage probe will ask about"
);
}
}
#[test]
fn palace_docs_reads_the_drawer_table() {
let tmp = tempfile::tempdir().expect("tempdir");
let handle = test_handle(tmp.path(), &["alpha", " ", "beta"]);
let split = palace_docs(&handle);
assert_eq!(split.docs.len(), 2);
assert_eq!(split.skipped_empty, 1);
assert_eq!(split.drawers_total(), 3);
}
#[test]
fn startup_backfill_respects_the_opt_out() {
let _env = crate::commands::env_test_lock().blocking_lock();
assert_eq!(ENV_NO_BACKFILL, "TRUSTY_BM25_NO_BACKFILL");
let prev = std::env::var(ENV_NO_BACKFILL).ok();
for (value, expected) in [("1", true), ("0", false), ("true", false), ("", false)] {
unsafe { std::env::set_var(ENV_NO_BACKFILL, value) };
assert_eq!(startup_backfill_opted_out(), expected, "value {value:?}");
}
unsafe { std::env::remove_var(ENV_NO_BACKFILL) };
assert!(
!startup_backfill_opted_out(),
"an unset variable must leave the sweep enabled"
);
if let Some(v) = prev {
unsafe { std::env::set_var(ENV_NO_BACKFILL, v) };
}
}
#[tokio::test]
async fn coverage_probe_classifies_an_unreadable_index_as_unreachable() {
let tmp = tempfile::tempdir().expect("tempdir");
let lane = crate::bm25_lane::Bm25Lane::with_limits(tmp.path().to_path_buf(), 3, None);
let ids = vec!["a".to_string(), "b".to_string()];
lane.index("ok", "a", "alpha").await.expect("seed");
assert_eq!(
probe_coverage(&lane, "ok", &ids).await,
Coverage::Missing(1),
"a healthy index answers by identity: `a` is present, `b` is not"
);
std::fs::create_dir_all(tmp.path().join("broken")).expect("palace dir");
std::fs::write(tmp.path().join("broken").join("bm25"), b"not a directory")
.expect("block the index dir");
assert_eq!(
probe_coverage(&lane, "broken", &ids).await,
Coverage::Unreachable,
"an index that cannot be opened must never report a missing set"
);
lane.shutdown().await;
}
fn create_palaces_on_disk(state: &AppState, count: usize) -> Vec<String> {
use trusty_common::memory_core::palace::{Palace, PalaceId};
(0..count)
.map(|i| {
let id = format!("sweep-{i}");
let handle = state
.registry
.create_palace(
&state.data_root,
Palace {
id: PalaceId::new(&id),
name: id.clone(),
description: None,
created_at: chrono::Utc::now(),
data_dir: state.data_root.join(&id),
},
)
.expect("create palace on disk");
let drawer = Drawer::new(Uuid::new_v4(), "content worth indexing");
handle
.kg
.upsert_drawer_sync(&drawer)
.expect("persist drawer");
handle.drawers.write().push(drawer);
id
})
.collect()
}
#[tokio::test]
async fn startup_sweep_enumerates_every_palace_on_disk() {
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path().to_path_buf();
let ids = create_palaces_on_disk(&AppState::new(root.clone()), 3);
let cold = AppState::new(root);
assert!(
cold.registry.list().is_empty(),
"precondition: the registry must be cold, so an LRU-based enumeration finds nothing"
);
let out = run_startup_sweep(&cold).await;
assert_eq!(
out.enumerated,
Some(3),
"every palace on disk must be enumerated, not just the open ones"
);
assert_eq!(
out.swept, 3,
"each has a drawer, so each must be backfilled"
);
assert_eq!(
out.incomplete, 3,
"the lane is off, so none can be verified"
);
assert!(
!out.all_verified(),
"a sweep that verified nothing must never read as complete"
);
let mut queued = crate::bm25_repair::dirty_palaces(&cold);
queued.sort();
assert_eq!(
queued, ids,
"every unverified palace must be queued for repair"
);
}
fn find_file(root: &std::path::Path, name: &str) -> Option<std::path::PathBuf> {
for entry in std::fs::read_dir(root).ok()?.flatten() {
let path = entry.path();
if path.is_dir() {
if let Some(found) = find_file(&path, name) {
return Some(found);
}
} else if path.file_name().is_some_and(|f| f == name) {
return Some(path);
}
}
None
}
#[tokio::test]
async fn startup_sweep_marks_unopenable_palaces_instead_of_skipping_them() {
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path().to_path_buf();
create_palaces_on_disk(&AppState::new(root.clone()), 1);
let kg_path = find_file(&root, "kg.redb").expect("the palace must have a KG store on disk");
std::fs::remove_file(&kg_path).expect("remove kg.db");
std::fs::create_dir(&kg_path).expect("a directory cannot be opened as a redb file");
let cold = AppState::new(root);
let out = run_startup_sweep(&cold).await;
assert_eq!(
out.enumerated,
Some(1),
"precondition: the row is still decodable, so the palace IS enumerated"
);
assert_eq!(out.unopenable, 1, "an unopenable palace must be counted");
assert!(
!out.all_verified(),
"a palace that could not be examined must not read as verified"
);
assert_eq!(
crate::bm25_repair::dirty_palaces(&cold),
vec!["sweep-0".to_string()],
"and queued for repair rather than skipped"
);
}
#[tokio::test]
async fn startup_sweep_counts_an_undecodable_palace_instead_of_skipping_it() {
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path().to_path_buf();
create_palaces_on_disk(&AppState::new(root.clone()), 1);
std::fs::write(
root.join("sweep-0").join("palace.json"),
b"{ not valid json",
)
.expect("corrupt palace.json");
let cold = AppState::new(root);
let out = run_startup_sweep(&cold).await;
assert_eq!(
out.enumerated,
Some(1),
"a palace whose metadata will not decode must still be SEEN"
);
assert_eq!(out.unopenable, 1, "and recorded as unopenable");
assert!(
!out.all_verified(),
"a sweep that could not read a palace must never report clean"
);
assert_eq!(
crate::bm25_repair::dirty_palaces(&cold),
vec!["sweep-0".to_string()],
"and queued, so the state is visible rather than silent"
);
}
#[tokio::test]
async fn a_sweep_that_cannot_enumerate_verifies_nothing() {
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path().join("not-a-directory");
std::fs::write(&root, b"x").expect("write file");
let state = AppState::new(root);
let out = run_startup_sweep(&state).await;
assert_eq!(
out.enumerated, None,
"the enumeration failed — say so in the type"
);
assert_eq!(out.swept, 0);
assert!(
!out.all_verified(),
"a sweep that examined nothing has zero incomplete palaces only because \
it looked at none — that must never read as complete"
);
assert!(
crate::bm25_repair::dirty_palaces(&state).is_empty(),
"nothing was examined, so nothing can be queued — the log line is the alarm"
);
}