mod env;
mod loader;
mod store;
pub use env::{cold_reload_timeout, warmboot_max_indexes, LAST_QUERIED_WRITE_INTERVAL_SECS};
pub use loader::{get_or_load_index, LazyLoadError};
pub use store::{select_warmboot_entries, ColdIndexStore};
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
use std::time::Duration;
use crate::core::registry::{IndexId, IndexRegistry};
use crate::service::persistence::PersistedIndex;
fn mk_entry(id: &str, q: Option<u64>, i: Option<u64>) -> PersistedIndex {
PersistedIndex {
id: id.to_string(),
root_path: PathBuf::from(format!("/tmp/{id}")),
last_queried_unix: q,
last_indexed_unix: i,
..Default::default()
}
}
fn build_mock_handle(id: &str) -> crate::core::registry::IndexHandle {
use std::sync::Arc;
use tokio::sync::RwLock;
let index_id = IndexId::new(id.to_string());
let root_path = PathBuf::from(format!("/tmp/test-{id}"));
let indexer = Arc::new(RwLock::new(crate::core::indexer::CodeIndexer::new(
id, &root_path,
)));
crate::core::registry::IndexHandle::bare(index_id, indexer, root_path)
}
#[test]
#[serial_test::serial]
fn warmboot_max_indexes_unset_returns_none() {
unsafe { std::env::remove_var("TRUSTY_WARMBOOT_MAX_INDEXES") };
assert!(warmboot_max_indexes().is_none());
}
#[test]
#[serial_test::serial]
fn warmboot_max_indexes_zero_returns_some_zero() {
unsafe { std::env::set_var("TRUSTY_WARMBOOT_MAX_INDEXES", "0") };
assert_eq!(warmboot_max_indexes(), Some(0));
unsafe { std::env::remove_var("TRUSTY_WARMBOOT_MAX_INDEXES") };
}
#[test]
#[serial_test::serial]
fn warmboot_max_indexes_parses_env() {
unsafe { std::env::set_var("TRUSTY_WARMBOOT_MAX_INDEXES", "10") };
assert_eq!(warmboot_max_indexes(), Some(10));
unsafe { std::env::remove_var("TRUSTY_WARMBOOT_MAX_INDEXES") };
}
#[test]
#[serial_test::serial]
fn cold_reload_timeout_default_is_30s() {
unsafe { std::env::remove_var("TRUSTY_INDEX_COLD_RELOAD_TIMEOUT_SECS") };
assert_eq!(cold_reload_timeout(), Duration::from_secs(30));
}
#[test]
#[serial_test::serial]
fn cold_reload_timeout_parses_env() {
unsafe { std::env::set_var("TRUSTY_INDEX_COLD_RELOAD_TIMEOUT_SECS", "15") };
assert_eq!(cold_reload_timeout(), Duration::from_secs(15));
unsafe { std::env::remove_var("TRUSTY_INDEX_COLD_RELOAD_TIMEOUT_SECS") };
}
#[test]
fn select_all_eager_when_no_cap() {
let entries = vec![mk_entry("a", None, None), mk_entry("b", Some(100), None)];
let (eager, cold) = select_warmboot_entries(entries.clone(), None);
assert_eq!(eager.len(), 2);
assert!(cold.is_empty());
}
#[test]
fn select_all_cold_when_cap_zero() {
let entries = vec![mk_entry("a", None, None), mk_entry("b", Some(100), None)];
let (eager, cold) = select_warmboot_entries(entries, Some(0));
assert!(eager.is_empty());
assert_eq!(cold.len(), 2);
}
#[test]
fn select_all_eager_when_cap_exceeds_count() {
let entries = vec![mk_entry("a", Some(1), None), mk_entry("b", Some(2), None)];
let (eager, cold) = select_warmboot_entries(entries, Some(10));
assert_eq!(eager.len(), 2);
assert!(cold.is_empty());
}
#[test]
fn select_top_n_by_recency() {
let entries = vec![
mk_entry("a", None, None),
mk_entry("b", Some(200), None),
mk_entry("c", Some(300), None),
mk_entry("d", None, Some(150)),
];
let (eager, cold) = select_warmboot_entries(entries, Some(2));
assert_eq!(eager.len(), 2);
assert_eq!(cold.len(), 2);
let eager_ids: Vec<&str> = eager.iter().map(|e| e.id.as_str()).collect();
assert!(
eager_ids.contains(&"c"),
"c (sort_key=300) must be in eager: {eager_ids:?}"
);
assert!(
eager_ids.contains(&"b"),
"b (sort_key=200) must be in eager: {eager_ids:?}"
);
}
#[test]
fn select_tie_breaks_by_id_ascending() {
let entries = vec![
mk_entry("ccc", Some(100), None),
mk_entry("aaa", Some(100), None),
mk_entry("bbb", Some(100), None),
];
let (eager, cold) = select_warmboot_entries(entries, Some(2));
let eager_ids: Vec<&str> = eager.iter().map(|e| e.id.as_str()).collect();
assert!(eager_ids.contains(&"aaa"), "aaa expected in eager");
assert!(eager_ids.contains(&"bbb"), "bbb expected in eager");
let cold_ids: Vec<&str> = cold.iter().map(|e| e.id.as_str()).collect();
assert!(cold_ids.contains(&"ccc"), "ccc expected in cold");
}
#[test]
fn cold_store_register_and_contains() {
let store = ColdIndexStore::new();
assert!(store.is_empty());
let entries = vec![
mk_entry("idx1", None, None),
mk_entry("idx2", Some(1), None),
];
store.register_cold_entries(entries);
assert_eq!(store.len(), 2);
assert!(store.contains(&IndexId::new("idx1".to_string())));
assert!(store.contains(&IndexId::new("idx2".to_string())));
assert!(!store.contains(&IndexId::new("unknown".to_string())));
}
#[test]
fn cold_store_len() {
let store = ColdIndexStore::new();
store.register_cold_entries(vec![mk_entry("a", None, None)]);
assert_eq!(store.len(), 1);
store.mark_loaded(&IndexId::new("a".to_string()));
assert_eq!(store.len(), 0);
}
#[tokio::test]
async fn get_or_load_index_hot_path() {
let registry = IndexRegistry::default();
let cold = ColdIndexStore::new();
let id = IndexId::new("hot-idx".to_string());
registry.register(build_mock_handle("hot-idx"));
let result = get_or_load_index(&id, ®istry, &cold, Duration::from_secs(5), |_e| async {
false })
.await;
assert!(result.is_ok(), "hot-path should return Ok");
}
#[tokio::test]
async fn get_or_load_index_not_found() {
let registry = IndexRegistry::default();
let cold = ColdIndexStore::new();
let id = IndexId::new("no-such".to_string());
let result = get_or_load_index(&id, ®istry, &cold, Duration::from_secs(5), |_e| async {
false
})
.await;
assert!(
matches!(result, Err(LazyLoadError::NotFound)),
"unknown id must return NotFound"
);
}
#[tokio::test]
async fn get_or_load_index_loads_cold_index() {
let registry = IndexRegistry::default();
let cold = ColdIndexStore::new();
let id = IndexId::new("cold-idx".to_string());
cold.register_cold_entries(vec![mk_entry("cold-idx", None, None)]);
let registry_clone = registry.clone();
let result = get_or_load_index(
&id,
®istry,
&cold,
Duration::from_secs(5),
move |_e| async move {
registry_clone.register(build_mock_handle("cold-idx"));
true
},
)
.await;
assert!(result.is_ok(), "cold index should load successfully");
assert!(!cold.contains(&id), "cold store must be cleared after load");
}
#[tokio::test]
async fn get_or_load_index_gate_none_but_index_just_became_hot() {
let registry = IndexRegistry::default();
let cold = ColdIndexStore::new();
let id = IndexId::new("race-idx".to_string());
cold.register_cold_entries(vec![mk_entry("race-idx", None, None)]);
let registry_clone = registry.clone();
let cold_clone = cold.clone();
let id_clone = id.clone();
let result = get_or_load_index(
&id,
®istry,
&cold,
Duration::from_secs(5),
move |_e| async move {
registry_clone.register(build_mock_handle("race-idx"));
cold_clone.mark_loaded(&id_clone);
true
},
)
.await;
assert!(
result.is_ok(),
"index loaded by restore_fn must return Ok, not NotFound"
);
}
#[tokio::test]
async fn get_or_load_index_returns_loading_on_timeout() {
let registry = IndexRegistry::default();
let cold = ColdIndexStore::new();
let id = IndexId::new("slow-idx".to_string());
cold.register_cold_entries(vec![mk_entry("slow-idx", None, None)]);
let result = get_or_load_index(
&id,
®istry,
&cold,
Duration::from_millis(50), |_e| async {
tokio::time::sleep(Duration::from_secs(5)).await;
true
},
)
.await;
assert!(
matches!(result, Err(LazyLoadError::Loading { .. })),
"timeout must return Loading error"
);
}
#[test]
fn cold_store_mark_failed_evicts_from_entries() {
let store = ColdIndexStore::new();
store.register_cold_entries(vec![mk_entry("f1", None, None)]);
let id = IndexId::new("f1".to_string());
assert!(store.contains(&id), "must be in entries before mark_failed");
assert_eq!(store.len(), 1);
assert!(!store.is_failed(&id));
store.mark_failed(&id);
assert!(
!store.contains(&id),
"must NOT be in entries after mark_failed"
);
assert_eq!(store.len(), 0, "entries len must decrease to 0");
assert!(store.is_failed(&id), "must appear in failed_entries");
}
#[test]
fn cold_store_mark_failed_failed_len() {
let store = ColdIndexStore::new();
store.register_cold_entries(vec![mk_entry("fa", None, None), mk_entry("fb", None, None)]);
assert_eq!(store.failed_len(), 0);
assert_eq!(store.len(), 2);
store.mark_failed(&IndexId::new("fa".to_string()));
assert_eq!(store.failed_len(), 1);
assert_eq!(store.len(), 1);
store.mark_failed(&IndexId::new("fb".to_string()));
assert_eq!(store.failed_len(), 2);
assert_eq!(store.len(), 0);
}
#[tokio::test]
async fn get_or_load_index_restore_false_marks_failed() {
let registry = IndexRegistry::default();
let cold = ColdIndexStore::new();
let id = IndexId::new("fail-idx".to_string());
cold.register_cold_entries(vec![mk_entry("fail-idx", None, None)]);
let result = get_or_load_index(&id, ®istry, &cold, Duration::from_secs(5), |_e| async {
false
})
.await;
assert!(
matches!(result, Err(LazyLoadError::RestoreFailed)),
"restore_fn=false must return RestoreFailed"
);
assert!(
!cold.contains(&id),
"entry must be evicted from entries after restore failure"
);
assert!(
cold.is_failed(&id),
"entry must appear in failed_entries after restore failure"
);
assert_eq!(
cold.len(),
0,
"indexes_lazy must be 0 after restore failure"
);
assert_eq!(cold.failed_len(), 1, "indexes_failed must be 1");
}
#[tokio::test]
async fn get_or_load_index_gate_recheck_is_failed_short_circuits() {
let registry = IndexRegistry::default();
let cold = ColdIndexStore::new();
let id = IndexId::new("toctou-failed-idx".to_string());
cold.register_cold_entries(vec![mk_entry("toctou-failed-idx", None, None)]);
cold.mark_failed(&id);
let mut restore_called = 0u32;
let result = get_or_load_index(&id, ®istry, &cold, Duration::from_secs(5), |_e| {
restore_called += 1;
async { false }
})
.await;
assert!(
matches!(
result,
Err(LazyLoadError::NotFound) | Err(LazyLoadError::RestoreFailed)
),
"must short-circuit without calling restore_fn when already failed"
);
assert_eq!(
restore_called, 0,
"restore_fn must not be called when entry is already in failed_entries"
);
assert!(cold.is_failed(&id), "id must still be in failed_entries");
}
#[tokio::test]
async fn get_or_load_index_second_call_after_failure_does_not_retry() {
let registry = IndexRegistry::default();
let cold = ColdIndexStore::new();
let id = IndexId::new("fail-idx2".to_string());
cold.register_cold_entries(vec![mk_entry("fail-idx2", None, None)]);
let mut restore_call_count = 0u32;
let _ = get_or_load_index(&id, ®istry, &cold, Duration::from_secs(5), |_e| {
restore_call_count += 1;
async { false }
})
.await;
let result2 = get_or_load_index(&id, ®istry, &cold, Duration::from_secs(5), |_e| {
restore_call_count += 1;
async { false }
})
.await;
assert!(
matches!(result2, Err(LazyLoadError::NotFound)),
"second call after failure must return NotFound (not re-attempt restore)"
);
assert_eq!(
restore_call_count, 1,
"restore_fn must be called exactly once (not re-attempted after failure)"
);
}
}