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
91 lines
3.9 KiB
Rust
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") };
|
|
}
|