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
278 lines
6.8 KiB
Rust
278 lines
6.8 KiB
Rust
use lean_ctx::core::cache::SessionCache;
|
|
use lean_ctx::core::config::AutonomyConfig;
|
|
use lean_ctx::tools::CrpMode;
|
|
use lean_ctx::tools::autonomy::{
|
|
AutonomyState, enrich_after_read, maybe_auto_dedup, session_lifecycle_pre_hook,
|
|
shell_efficiency_hint,
|
|
};
|
|
use std::sync::OnceLock;
|
|
use std::sync::atomic::Ordering;
|
|
|
|
fn init_test_data_dir() {
|
|
static DIR: OnceLock<tempfile::TempDir> = OnceLock::new();
|
|
let dir = DIR.get_or_init(|| tempfile::tempdir().expect("tempdir"));
|
|
// TODO: Audit that the environment access only happens in single-threaded code.
|
|
unsafe { std::env::set_var("LEAN_CTX_DATA_DIR", dir.path()) };
|
|
}
|
|
|
|
fn make_state() -> AutonomyState {
|
|
init_test_data_dir();
|
|
AutonomyState::new()
|
|
}
|
|
|
|
fn make_disabled_state() -> AutonomyState {
|
|
init_test_data_dir();
|
|
let mut state = AutonomyState::new();
|
|
state.config.enabled = false;
|
|
state
|
|
}
|
|
|
|
#[test]
|
|
fn session_lifecycle_fires_once() {
|
|
let state = make_state();
|
|
let mut cache = SessionCache::new();
|
|
|
|
let _first = session_lifecycle_pre_hook(
|
|
&state,
|
|
"ctx_read",
|
|
&mut cache,
|
|
Some("fix auth bug"),
|
|
Some("/tmp/test-project"),
|
|
CrpMode::Tdd,
|
|
);
|
|
|
|
let second = session_lifecycle_pre_hook(
|
|
&state,
|
|
"ctx_read",
|
|
&mut cache,
|
|
Some("fix auth bug"),
|
|
Some("/tmp/test-project"),
|
|
CrpMode::Tdd,
|
|
);
|
|
|
|
assert!(
|
|
state.session_initialized.load(Ordering::SeqCst),
|
|
"flag must be set after first call"
|
|
);
|
|
assert!(second.is_none(), "second call must return None");
|
|
}
|
|
|
|
#[test]
|
|
fn session_lifecycle_skips_without_project_root() {
|
|
let state = make_state();
|
|
let mut cache = SessionCache::new();
|
|
|
|
let result = session_lifecycle_pre_hook(
|
|
&state,
|
|
"ctx_read",
|
|
&mut cache,
|
|
Some("fix auth bug"),
|
|
None,
|
|
CrpMode::Tdd,
|
|
);
|
|
|
|
assert!(result.is_none(), "must skip when project_root is None");
|
|
assert!(
|
|
!state.session_initialized.load(Ordering::SeqCst),
|
|
"flag must not be set without project_root"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn session_lifecycle_skips_overview_tool() {
|
|
let state = make_state();
|
|
let mut cache = SessionCache::new();
|
|
|
|
let result = session_lifecycle_pre_hook(
|
|
&state,
|
|
"ctx_overview",
|
|
&mut cache,
|
|
Some("task"),
|
|
None,
|
|
CrpMode::Tdd,
|
|
);
|
|
|
|
assert!(result.is_none(), "must skip when tool is ctx_overview");
|
|
assert!(
|
|
!state.session_initialized.load(Ordering::SeqCst),
|
|
"flag must NOT be set when skipped"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn session_lifecycle_skips_preload_tool() {
|
|
let state = make_state();
|
|
let mut cache = SessionCache::new();
|
|
|
|
let result = session_lifecycle_pre_hook(
|
|
&state,
|
|
"ctx_preload",
|
|
&mut cache,
|
|
Some("task"),
|
|
None,
|
|
CrpMode::Tdd,
|
|
);
|
|
|
|
assert!(result.is_none(), "must skip when tool is ctx_preload");
|
|
assert!(
|
|
!state.session_initialized.load(Ordering::SeqCst),
|
|
"flag must NOT be set when skipped"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn session_lifecycle_disabled_returns_none() {
|
|
let state = make_disabled_state();
|
|
let mut cache = SessionCache::new();
|
|
|
|
let result = session_lifecycle_pre_hook(
|
|
&state,
|
|
"ctx_read",
|
|
&mut cache,
|
|
Some("task"),
|
|
None,
|
|
CrpMode::Tdd,
|
|
);
|
|
|
|
assert!(result.is_none(), "disabled state must return None");
|
|
assert!(
|
|
!state.session_initialized.load(Ordering::SeqCst),
|
|
"flag must NOT be set when disabled"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn auto_dedup_fires_at_threshold() {
|
|
let state = make_state();
|
|
let mut cache = SessionCache::new();
|
|
|
|
for i in 0..8 {
|
|
let path = format!("test_file_{i}.rs");
|
|
let content = format!("fn func_{i}() {{ println!(\"hello {i}\"); }}");
|
|
cache.store(&path, &content);
|
|
}
|
|
|
|
maybe_auto_dedup(&state, &mut cache, "ctx_read");
|
|
assert!(
|
|
state.dedup_applied.load(Ordering::SeqCst),
|
|
"dedup must be applied at threshold"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn auto_dedup_skips_below_threshold() {
|
|
let state = make_state();
|
|
let mut cache = SessionCache::new();
|
|
|
|
for i in 0..3 {
|
|
let path = format!("test_file_{i}.rs");
|
|
cache.store(&path, &format!("content {i}"));
|
|
}
|
|
|
|
maybe_auto_dedup(&state, &mut cache, "ctx_read");
|
|
assert!(
|
|
!state.dedup_applied.load(Ordering::SeqCst),
|
|
"dedup must NOT be applied below threshold"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn auto_dedup_disabled() {
|
|
let state = make_disabled_state();
|
|
let mut cache = SessionCache::new();
|
|
|
|
for i in 0..10 {
|
|
cache.store(&format!("f{i}.rs"), &format!("c{i}"));
|
|
}
|
|
|
|
maybe_auto_dedup(&state, &mut cache, "ctx_read");
|
|
assert!(!state.dedup_applied.load(Ordering::SeqCst));
|
|
}
|
|
|
|
#[test]
|
|
fn shell_hint_grep_low_savings() {
|
|
let state = make_state();
|
|
let hint = shell_efficiency_hint(&state, "grep -rn pattern src/", 200, 190);
|
|
assert!(hint.is_some());
|
|
assert!(hint.unwrap().contains("ctx_search"));
|
|
}
|
|
|
|
#[test]
|
|
fn shell_hint_cat_low_savings() {
|
|
let state = make_state();
|
|
let hint = shell_efficiency_hint(&state, "cat src/main.rs", 500, 490);
|
|
assert!(hint.is_some());
|
|
assert!(hint.unwrap().contains("ctx_read"));
|
|
}
|
|
|
|
#[test]
|
|
fn shell_hint_none_for_good_savings() {
|
|
let state = make_state();
|
|
let hint = shell_efficiency_hint(&state, "grep -rn foo .", 1000, 200);
|
|
assert!(hint.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn shell_hint_none_for_non_search_command() {
|
|
let state = make_state();
|
|
let hint = shell_efficiency_hint(&state, "cargo build --release", 100, 95);
|
|
assert!(hint.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn shell_hint_disabled() {
|
|
let state = make_disabled_state();
|
|
let hint = shell_efficiency_hint(&state, "grep foo bar", 100, 95);
|
|
assert!(hint.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn config_defaults_all_enabled() {
|
|
let cfg = AutonomyConfig::default();
|
|
assert!(cfg.enabled);
|
|
assert!(cfg.auto_preload);
|
|
assert!(cfg.auto_dedup);
|
|
assert!(cfg.auto_related);
|
|
assert!(cfg.silent_preload);
|
|
assert_eq!(cfg.dedup_threshold, 8);
|
|
}
|
|
|
|
#[test]
|
|
fn enrich_after_read_disabled() {
|
|
let state = make_disabled_state();
|
|
let mut cache = SessionCache::new();
|
|
cache.store("test.rs", "fn main() {}");
|
|
|
|
let result = enrich_after_read(
|
|
&state,
|
|
&mut cache,
|
|
"test.rs",
|
|
None,
|
|
None,
|
|
CrpMode::Tdd,
|
|
false,
|
|
);
|
|
assert!(result.related_hint.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn enrich_after_read_no_index() {
|
|
let state = make_state();
|
|
let mut cache = SessionCache::new();
|
|
cache.store("test.rs", "fn main() {}");
|
|
|
|
let result = enrich_after_read(
|
|
&state,
|
|
&mut cache,
|
|
"test.rs",
|
|
Some("/nonexistent/path"),
|
|
None,
|
|
CrpMode::Tdd,
|
|
false,
|
|
);
|
|
assert!(
|
|
result.related_hint.is_none(),
|
|
"must return None when no project index exists"
|
|
);
|
|
}
|