26382a7ac6
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
JetBrains Plugin / Actionlint (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
CodeQL / Analyze (rust) (push) Waiting to run
JetBrains Plugin / Validation (push) Waiting to run
JetBrains Plugin / Build (push) Waiting to run
JetBrains Plugin / Test (push) Blocked by required conditions
Security Check / Security Scan (push) Waiting to run
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
496 lines
18 KiB
Rust
496 lines
18 KiB
Rust
//! Comprehensive scenario tests for compaction-aware cache behavior.
|
|
//! Tests cover: delivery flag reset, stub-path guards, policy modes,
|
|
//! compaction sync, and edge cases.
|
|
|
|
use lean_ctx::core::cache::SessionCache;
|
|
use lean_ctx::core::protocol::CrpMode;
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
// 1. SessionCache — reset_delivery_flags and is_full_delivered
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
mod cache_delivery_flags {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn new_entry_is_not_delivered() {
|
|
let mut cache = SessionCache::default();
|
|
cache.store("/tmp/test.rs", "fn main() {}");
|
|
assert!(!cache.is_full_delivered("/tmp/test.rs"));
|
|
}
|
|
|
|
#[test]
|
|
fn mark_delivered_then_check() {
|
|
let mut cache = SessionCache::default();
|
|
cache.store("/tmp/test.rs", "fn main() {}");
|
|
cache.mark_full_delivered("/tmp/test.rs");
|
|
assert!(cache.is_full_delivered("/tmp/test.rs"));
|
|
}
|
|
|
|
#[test]
|
|
fn reset_flags_clears_all_entries() {
|
|
let mut cache = SessionCache::default();
|
|
cache.store("/tmp/a.rs", "a");
|
|
cache.store("/tmp/b.rs", "b");
|
|
cache.store("/tmp/c.rs", "c");
|
|
cache.mark_full_delivered("/tmp/a.rs");
|
|
cache.mark_full_delivered("/tmp/b.rs");
|
|
cache.mark_full_delivered("/tmp/c.rs");
|
|
|
|
let count = cache.reset_delivery_flags();
|
|
assert_eq!(count, 3);
|
|
assert!(!cache.is_full_delivered("/tmp/a.rs"));
|
|
assert!(!cache.is_full_delivered("/tmp/b.rs"));
|
|
assert!(!cache.is_full_delivered("/tmp/c.rs"));
|
|
}
|
|
|
|
#[test]
|
|
fn reset_flags_returns_zero_when_none_delivered() {
|
|
let mut cache = SessionCache::default();
|
|
cache.store("/tmp/a.rs", "a");
|
|
cache.store("/tmp/b.rs", "b");
|
|
assert_eq!(cache.reset_delivery_flags(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn reset_preserves_cache_content() {
|
|
let mut cache = SessionCache::default();
|
|
cache.store("/tmp/a.rs", "content here");
|
|
cache.mark_full_delivered("/tmp/a.rs");
|
|
|
|
cache.reset_delivery_flags();
|
|
|
|
// Entry still exists with content
|
|
let entry = cache.get("/tmp/a.rs").unwrap();
|
|
assert!(entry.original_tokens > 0);
|
|
assert!(entry.content().is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn reset_preserves_file_refs() {
|
|
let mut cache = SessionCache::default();
|
|
cache.store("/tmp/a.rs", "content");
|
|
let ref1 = cache.get_file_ref("/tmp/a.rs");
|
|
cache.mark_full_delivered("/tmp/a.rs");
|
|
|
|
cache.reset_delivery_flags();
|
|
|
|
let ref2 = cache.get_file_ref("/tmp/a.rs");
|
|
assert_eq!(ref1, ref2);
|
|
}
|
|
|
|
#[test]
|
|
fn partial_reset_counts_correctly() {
|
|
let mut cache = SessionCache::default();
|
|
cache.store("/tmp/a.rs", "a");
|
|
cache.store("/tmp/b.rs", "b");
|
|
cache.store("/tmp/c.rs", "c");
|
|
cache.mark_full_delivered("/tmp/a.rs");
|
|
cache.mark_full_delivered("/tmp/c.rs");
|
|
// b is NOT delivered
|
|
|
|
let count = cache.reset_delivery_flags();
|
|
assert_eq!(count, 2); // only a and c had flag set
|
|
}
|
|
|
|
#[test]
|
|
fn is_full_delivered_nonexistent_path() {
|
|
let cache = SessionCache::default();
|
|
assert!(!cache.is_full_delivered("/nonexistent/path.rs"));
|
|
}
|
|
|
|
#[test]
|
|
fn hash_change_resets_delivery() {
|
|
let mut cache = SessionCache::default();
|
|
cache.store("/tmp/a.rs", "version 1");
|
|
cache.mark_full_delivered("/tmp/a.rs");
|
|
assert!(cache.is_full_delivered("/tmp/a.rs"));
|
|
|
|
// Store different content → hash changes → flag resets
|
|
cache.store("/tmp/a.rs", "version 2");
|
|
assert!(!cache.is_full_delivered("/tmp/a.rs"));
|
|
}
|
|
|
|
#[test]
|
|
fn invalidate_removes_delivery_state() {
|
|
let mut cache = SessionCache::default();
|
|
cache.store("/tmp/a.rs", "content");
|
|
cache.mark_full_delivered("/tmp/a.rs");
|
|
|
|
cache.invalidate("/tmp/a.rs");
|
|
assert!(!cache.is_full_delivered("/tmp/a.rs"));
|
|
}
|
|
|
|
#[test]
|
|
fn double_reset_is_idempotent() {
|
|
let mut cache = SessionCache::default();
|
|
cache.store("/tmp/a.rs", "a");
|
|
cache.mark_full_delivered("/tmp/a.rs");
|
|
|
|
assert_eq!(cache.reset_delivery_flags(), 1);
|
|
assert_eq!(cache.reset_delivery_flags(), 0); // already reset
|
|
}
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
// 2. Compaction Sync — radar detection and flag reset
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
mod compaction_sync_scenarios {
|
|
use super::*;
|
|
use lean_ctx::server::compaction_sync::{LAST_COMPACTION_TS, sync_if_compacted};
|
|
use serial_test::serial;
|
|
use std::io::Write;
|
|
use std::sync::atomic::Ordering;
|
|
use tempfile::TempDir;
|
|
|
|
fn reset_compaction_ts() {
|
|
LAST_COMPACTION_TS.store(0, Ordering::Relaxed);
|
|
}
|
|
|
|
fn make_cache(paths: &[&str]) -> SessionCache {
|
|
let mut cache = SessionCache::default();
|
|
for p in paths {
|
|
cache.store(p, "fn test() { todo!() }");
|
|
cache.mark_full_delivered(p);
|
|
}
|
|
cache
|
|
}
|
|
|
|
fn write_radar(dir: &TempDir, events: &[(&str, u64)]) {
|
|
let path = dir.path().join("context_radar.jsonl");
|
|
let mut f = std::fs::File::create(&path).unwrap();
|
|
for (event_type, ts) in events {
|
|
writeln!(f, r#"{{"ts":{ts},"event_type":"{event_type}","tokens":0}}"#).unwrap();
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
#[serial]
|
|
fn no_radar_file_does_nothing() {
|
|
reset_compaction_ts();
|
|
let dir = TempDir::new().unwrap();
|
|
let mut cache = make_cache(&["/tmp/a.rs"]);
|
|
assert!(!sync_if_compacted(&mut cache, dir.path()));
|
|
assert!(cache.is_full_delivered("/tmp/a.rs"));
|
|
}
|
|
|
|
#[test]
|
|
#[serial]
|
|
fn radar_without_compaction_does_nothing() {
|
|
reset_compaction_ts();
|
|
let dir = TempDir::new().unwrap();
|
|
write_radar(
|
|
&dir,
|
|
&[
|
|
("mcp_call", 1000),
|
|
("file_read", 2000),
|
|
("native_tool", 3000),
|
|
],
|
|
);
|
|
let mut cache = make_cache(&["/tmp/a.rs"]);
|
|
assert!(!sync_if_compacted(&mut cache, dir.path()));
|
|
assert!(cache.is_full_delivered("/tmp/a.rs"));
|
|
}
|
|
|
|
#[test]
|
|
#[serial]
|
|
fn compaction_resets_all_flags() {
|
|
reset_compaction_ts();
|
|
let dir = TempDir::new().unwrap();
|
|
write_radar(&dir, &[("mcp_call", 1000), ("compaction", 2000)]);
|
|
let mut cache = make_cache(&["/tmp/a.rs", "/tmp/b.rs", "/tmp/c.rs"]);
|
|
|
|
assert!(sync_if_compacted(&mut cache, dir.path()));
|
|
assert!(!cache.is_full_delivered("/tmp/a.rs"));
|
|
assert!(!cache.is_full_delivered("/tmp/b.rs"));
|
|
assert!(!cache.is_full_delivered("/tmp/c.rs"));
|
|
}
|
|
|
|
#[test]
|
|
#[serial]
|
|
fn second_call_after_same_compaction_does_nothing() {
|
|
reset_compaction_ts();
|
|
let dir = TempDir::new().unwrap();
|
|
write_radar(&dir, &[("compaction", 5000)]);
|
|
let mut cache = make_cache(&["/tmp/a.rs"]);
|
|
|
|
assert!(sync_if_compacted(&mut cache, dir.path()));
|
|
cache.mark_full_delivered("/tmp/a.rs");
|
|
|
|
// Same compaction event → no reset
|
|
assert!(!sync_if_compacted(&mut cache, dir.path()));
|
|
assert!(cache.is_full_delivered("/tmp/a.rs"));
|
|
}
|
|
|
|
#[test]
|
|
#[serial]
|
|
fn newer_compaction_triggers_new_reset() {
|
|
reset_compaction_ts();
|
|
let dir = TempDir::new().unwrap();
|
|
write_radar(&dir, &[("compaction", 1000)]);
|
|
let mut cache = make_cache(&["/tmp/a.rs"]);
|
|
|
|
sync_if_compacted(&mut cache, dir.path());
|
|
cache.store("/tmp/a.rs", "fn test() { todo!() }");
|
|
cache.mark_full_delivered("/tmp/a.rs");
|
|
|
|
// Add newer compaction event
|
|
write_radar(&dir, &[("compaction", 1000), ("compaction", 3000)]);
|
|
assert!(sync_if_compacted(&mut cache, dir.path()));
|
|
assert!(!cache.is_full_delivered("/tmp/a.rs"));
|
|
}
|
|
|
|
#[test]
|
|
#[serial]
|
|
fn multiple_compactions_takes_latest() {
|
|
reset_compaction_ts();
|
|
let dir = TempDir::new().unwrap();
|
|
write_radar(
|
|
&dir,
|
|
&[
|
|
("mcp_call", 100),
|
|
("compaction", 200),
|
|
("mcp_call", 300),
|
|
("compaction", 400),
|
|
("mcp_call", 500),
|
|
],
|
|
);
|
|
let mut cache = make_cache(&["/tmp/a.rs"]);
|
|
assert!(sync_if_compacted(&mut cache, dir.path()));
|
|
|
|
// Check that ts=400 was stored (not 200)
|
|
let ts = LAST_COMPACTION_TS.load(Ordering::Relaxed);
|
|
assert_eq!(ts, 400);
|
|
}
|
|
|
|
#[test]
|
|
#[serial]
|
|
fn empty_cache_compaction_returns_true_but_resets_zero() {
|
|
reset_compaction_ts();
|
|
let dir = TempDir::new().unwrap();
|
|
write_radar(&dir, &[("compaction", 1000)]);
|
|
let mut cache = SessionCache::default();
|
|
|
|
// Returns true (compaction detected) but nothing to reset
|
|
assert!(sync_if_compacted(&mut cache, dir.path()));
|
|
}
|
|
|
|
#[test]
|
|
#[serial]
|
|
fn malformed_radar_lines_are_skipped() {
|
|
reset_compaction_ts();
|
|
let dir = TempDir::new().unwrap();
|
|
let path = dir.path().join("context_radar.jsonl");
|
|
let mut f = std::fs::File::create(&path).unwrap();
|
|
writeln!(f, "this is not json").unwrap();
|
|
writeln!(f, r#"{{"ts":2000,"event_type":"compaction","tokens":0}}"#).unwrap();
|
|
writeln!(f, "{{invalid").unwrap();
|
|
drop(f);
|
|
|
|
let mut cache = make_cache(&["/tmp/a.rs"]);
|
|
assert!(sync_if_compacted(&mut cache, dir.path()));
|
|
assert!(!cache.is_full_delivered("/tmp/a.rs"));
|
|
}
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
// 3. Cache Policy — effective_cache_policy behavior
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
mod cache_policy {
|
|
use lean_ctx::server::compaction_sync::effective_cache_policy;
|
|
|
|
#[test]
|
|
fn default_policy_is_aggressive() {
|
|
// Without env var override, default should be "aggressive"
|
|
// (OnceLock means this test depends on execution order,
|
|
// but the default Config returns None → "aggressive")
|
|
let policy = effective_cache_policy();
|
|
assert!(
|
|
matches!(policy, "aggressive" | "safe" | "off"),
|
|
"policy must be one of the three valid values, got: {policy}"
|
|
);
|
|
}
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
// 4. Integration: ctx_read stub behavior with delivery flags
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
mod ctx_read_stub_behavior {
|
|
use super::*;
|
|
use lean_ctx::tools::ctx_read::handle_with_task_resolved;
|
|
use std::io::Write;
|
|
use tempfile::NamedTempFile;
|
|
|
|
fn read_full(cache: &mut SessionCache, path: &str) -> String {
|
|
let output = handle_with_task_resolved(cache, path, "full", CrpMode::Off, None);
|
|
output.content
|
|
}
|
|
|
|
#[test]
|
|
fn first_read_delivers_content() {
|
|
let mut f = NamedTempFile::new().unwrap();
|
|
writeln!(f, "fn hello() {{ println!(\"world\"); }}").unwrap();
|
|
let path = f.path().to_str().unwrap();
|
|
|
|
let mut cache = SessionCache::default();
|
|
let content = read_full(&mut cache, path);
|
|
assert!(
|
|
content.contains("hello") || content.contains("fn"),
|
|
"first read should deliver file content, got: {content}"
|
|
);
|
|
assert!(!content.contains("[unchanged"));
|
|
}
|
|
|
|
#[test]
|
|
fn second_read_returns_stub() {
|
|
let mut f = NamedTempFile::new().unwrap();
|
|
writeln!(f, "fn hello() {{ println!(\"world\"); }}").unwrap();
|
|
let path = f.path().to_str().unwrap();
|
|
|
|
let mut cache = SessionCache::default();
|
|
let _ = read_full(&mut cache, path); // first → delivers content
|
|
let content = read_full(&mut cache, path); // second → stub
|
|
assert!(
|
|
content.contains("unchanged") || content.contains("cached"),
|
|
"second read should be a stub, got: {content}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn after_reset_flags_delivers_content_again() {
|
|
let mut f = NamedTempFile::new().unwrap();
|
|
writeln!(f, "fn hello() {{ println!(\"world\"); }}").unwrap();
|
|
let path = f.path().to_str().unwrap();
|
|
|
|
let mut cache = SessionCache::default();
|
|
let _ = read_full(&mut cache, path); // first read
|
|
let _ = read_full(&mut cache, path); // stub
|
|
|
|
// Simulate compaction: reset flags
|
|
cache.reset_delivery_flags();
|
|
|
|
let content = read_full(&mut cache, path); // should deliver again
|
|
assert!(
|
|
content.contains("hello") || content.contains("fn"),
|
|
"after reset, should deliver content again, got: {content}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn after_re_delivery_third_read_is_stub_again() {
|
|
let mut f = NamedTempFile::new().unwrap();
|
|
writeln!(f, "fn hello() {{ println!(\"world\"); }}").unwrap();
|
|
let path = f.path().to_str().unwrap();
|
|
|
|
let mut cache = SessionCache::default();
|
|
let _ = read_full(&mut cache, path); // first → content
|
|
let _ = read_full(&mut cache, path); // second → stub
|
|
|
|
cache.reset_delivery_flags();
|
|
let _ = read_full(&mut cache, path); // third → content (post-compaction)
|
|
let content = read_full(&mut cache, path); // fourth → stub again
|
|
assert!(
|
|
content.contains("unchanged") || content.contains("cached"),
|
|
"after re-delivery, next read should be stub again, got: {content}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn content_change_always_delivers_new_content() {
|
|
let f = NamedTempFile::new().unwrap();
|
|
let path = f.path().to_str().unwrap();
|
|
|
|
std::fs::write(path, "version 1\n").unwrap();
|
|
let mut cache = SessionCache::default();
|
|
let _ = read_full(&mut cache, path); // first → content
|
|
|
|
// Modify file
|
|
std::fs::write(path, "version 2\n").unwrap();
|
|
let content = read_full(&mut cache, path);
|
|
assert!(
|
|
content.contains("version 2"),
|
|
"after content change, should deliver new content, got: {content}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn fresh_read_always_delivers_content() {
|
|
let mut f = NamedTempFile::new().unwrap();
|
|
writeln!(f, "fn hello() {{}}").unwrap();
|
|
let path = f.path().to_str().unwrap();
|
|
|
|
let mut cache = SessionCache::default();
|
|
let _ = handle_with_task_resolved(&mut cache, path, "full", CrpMode::Off, None);
|
|
let _ = handle_with_task_resolved(&mut cache, path, "full", CrpMode::Off, None); // stub
|
|
|
|
// fresh=true → invalidate → re-read
|
|
cache.invalidate(path);
|
|
let output = handle_with_task_resolved(&mut cache, path, "full", CrpMode::Off, None);
|
|
assert!(
|
|
!output.content.contains("unchanged"),
|
|
"fresh read should deliver content, got: {}",
|
|
output.content
|
|
);
|
|
}
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
// 5. Edge cases and robustness
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
mod edge_cases {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn large_number_of_entries_reset_performance() {
|
|
let mut cache = SessionCache::default();
|
|
for i in 0..500 {
|
|
let path = format!("/tmp/file_{i}.rs");
|
|
cache.store(&path, &format!("content {i}"));
|
|
cache.mark_full_delivered(&path);
|
|
}
|
|
|
|
let start = std::time::Instant::now();
|
|
let count = cache.reset_delivery_flags();
|
|
let elapsed = start.elapsed();
|
|
|
|
assert_eq!(count, 500);
|
|
assert!(
|
|
elapsed.as_millis() < 10,
|
|
"reset_delivery_flags should be fast, took {elapsed:?}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn concurrent_store_and_reset_dont_panic() {
|
|
let mut cache = SessionCache::default();
|
|
for i in 0..100 {
|
|
let path = format!("/tmp/file_{i}.rs");
|
|
cache.store(&path, &format!("v{i}"));
|
|
if i % 2 == 0 {
|
|
cache.mark_full_delivered(&path);
|
|
}
|
|
}
|
|
let count = cache.reset_delivery_flags();
|
|
assert_eq!(count, 50);
|
|
|
|
// Verify none are delivered
|
|
for i in 0..100 {
|
|
let path = format!("/tmp/file_{i}.rs");
|
|
assert!(!cache.is_full_delivered(&path));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn reset_after_clear_is_zero() {
|
|
let mut cache = SessionCache::default();
|
|
cache.store("/tmp/a.rs", "content");
|
|
cache.mark_full_delivered("/tmp/a.rs");
|
|
cache.clear();
|
|
assert_eq!(cache.reset_delivery_flags(), 0);
|
|
}
|
|
}
|