Files
yvgude--lean-ctx/rust/tests/issue_249_index_observability.rs
wehub-resource-sync 26382a7ac6
CI / Clippy (push) Failing after 15m13s
CI / Test (ubuntu-latest) (push) Failing after 16m1s
CI / Test (macos-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Build (no embeddings / no ORT) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Cookbook (Node) (push) Has been cancelled
CI / Pi Extension (Node) (push) Has been cancelled
CI / Rust SDK (lean-ctx-client) (push) Has been cancelled
CI / Embed SDK (lean-ctx-sdk) (push) Has been cancelled
CI / Python SDK (leanctx) (push) Has been cancelled
CI / Hermes Plugin (Python) (push) Has been cancelled
CI / SDK Conformance Matrix (push) Has been cancelled
CI / Coverage (push) Has been cancelled
CI / cargo-deny (push) Has been cancelled
CI / Adversarial Safety (push) Has been cancelled
CI / Benchmarks (push) Has been cancelled
CI / Output-Quality Gate (eval A/B) (push) Has been cancelled
CI / Documentation (push) Has been cancelled
CI / CI Green (push) Has been cancelled
JetBrains Plugin / Actionlint (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (rust) (push) Has been cancelled
JetBrains Plugin / Validation (push) Has been cancelled
JetBrains Plugin / Build (push) Has been cancelled
JetBrains Plugin / Test (push) Has been cancelled
Security Check / Security Scan (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:35:30 +08:00

91 lines
3.9 KiB
Rust

//! 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") };
}