//! Regression test for issue #249 — "semantic index keeps warming up, never //! finishes, and there is no way to see what state it is in". //! //! Root cause: on a repo whose compressed BM25 index exceeded the (RAM-profile //! derived) disk cap, `BM25Index::save` silently returned `Ok(())` without //! writing, so `load` returned `None` on every call and the index rebuilt from //! scratch forever — invisibly. This test drives the *real* orchestrator build //! pipeline with a deliberately tiny cap and asserts that: //! 1. the "could not persist (too large)" condition is now RECORDED, and //! 2. it is OBSERVABLE via both `status_json` and `bm25_summary`, with an //! actionable remedy (so an operator/agent can fix it instead of guessing). use std::time::{Duration, Instant}; use lean_ctx::core::index_orchestrator; /// Poll the orchestrator until the BM25 component leaves the building/idle /// state (Ready or Failed) or we time out. fn wait_until_built(root: &str, timeout: Duration) -> index_orchestrator::Bm25Summary { let deadline = Instant::now() + timeout; loop { let summary = index_orchestrator::bm25_summary(root); if summary.state == "ready" || summary.state == "failed" { return summary; } if Instant::now() >= deadline { return summary; } std::thread::sleep(Duration::from_millis(100)); } } #[test] fn oversized_index_records_observable_not_persisted_note() { let data_dir = tempfile::tempdir().expect("data dir"); let repo = tempfile::tempdir().expect("repo dir"); // Isolate the index store and force the "too large" branch for any non-empty // index by setting the disk ceiling to 0 MB. // TODO: Audit that the environment access only happens in single-threaded code. unsafe { std::env::set_var("LEAN_CTX_DATA_DIR", data_dir.path()) }; // TODO: Audit that the environment access only happens in single-threaded code. unsafe { std::env::set_var("LEAN_CTX_BM25_MAX_CACHE_MB", "0") }; // A small but non-empty source tree so the build produces real chunks. for i in 0..5 { std::fs::write( repo.path().join(format!("mod_{i}.rs")), format!("pub fn handler_{i}() {{ println!(\"work {i}\"); }}\n"), ) .expect("write source file"); } let root = repo.path().to_string_lossy().to_string(); index_orchestrator::ensure_all_background(&root); let summary = wait_until_built(&root, Duration::from_secs(30)); // The build itself succeeds (index is usable in memory) ... assert_eq!( summary.state, "ready", "build should succeed in memory even when too large to persist; got {summary:?}" ); // ... but the "not persisted" condition must be RECORDED (no silent success). let note = summary.note.clone().unwrap_or_default(); assert!( note.contains("NOT persisted"), "too-large build must record a non-persistence note, got: {note:?}" ); assert!( note.contains("LEAN_CTX_BM25_MAX_CACHE_MB") && note.contains("reindex"), "note must carry an actionable remedy, got: {note:?}" ); // ... and it must be OBSERVABLE through the machine-readable status surface // that `ctx_index status` returns. let status = index_orchestrator::status_json(&root); let parsed: serde_json::Value = serde_json::from_str(&status).expect("status_json valid JSON"); let bm25_note = parsed["bm25_index"]["note"].as_str().unwrap_or(""); assert!( bm25_note.contains("NOT persisted"), "status_json must expose the non-persistence note, got: {status}" ); // TODO: Audit that the environment access only happens in single-threaded code. unsafe { std::env::remove_var("LEAN_CTX_BM25_MAX_CACHE_MB") }; // TODO: Audit that the environment access only happens in single-threaded code. unsafe { std::env::remove_var("LEAN_CTX_DATA_DIR") }; }