use super::*;
use std::time::Duration;
#[test]
fn cache_stores_and_retrieves() {
let mut cache = SessionCache::new();
let result = cache.store("/test/file.rs", "fn main() {}");
assert!(!result.was_hit);
assert_eq!(result.line_count, 1);
assert!(cache.get("/test/file.rs").is_some());
}
#[test]
fn cache_hit_on_same_content() {
let mut cache = SessionCache::new();
cache.store("/test/file.rs", "content");
let result = cache.store("/test/file.rs", "content");
assert!(result.was_hit, "same content should be a cache hit");
}
#[test]
fn full_delivery_rereads_are_counted() {
let entry = CacheEntry::new(
"content",
"hash".to_string(),
1,
1,
"/test.rs".to_string(),
None,
);
assert_eq!(entry.bump_reread(), 1);
assert_eq!(entry.bump_reread(), 2);
}
#[test]
fn marking_full_delivery_resets_rereads() {
let mut entry = CacheEntry::new(
"content",
"hash".to_string(),
1,
1,
"/test.rs".to_string(),
None,
);
entry.bump_reread();
entry.mark_full_delivered(None);
assert_eq!(entry.bump_reread(), 1);
}
#[test]
fn full_degradation_threshold_uses_positive_env_value() {
let _env_lock = crate::core::data_dir::test_env_lock();
crate::test_env::set_var("LCTX_FULL_DEGRADATION_THRESHOLD", "3");
assert_eq!(full_degradation_threshold(), 3);
crate::test_env::remove_var("LCTX_FULL_DEGRADATION_THRESHOLD");
assert_eq!(
full_degradation_threshold(),
DEFAULT_FULL_DEGRADATION_THRESHOLD
);
}
#[test]
fn cache_miss_on_changed_content() {
let mut cache = SessionCache::new();
cache.store("/test/file.rs", "old content");
let result = cache.store("/test/file.rs", "new content");
assert!(!result.was_hit, "changed content should not be a cache hit");
}
#[test]
fn last_mode_is_available_only_for_current_content() {
let mut cache = SessionCache::new();
cache.store("/test/file.rs", "old content");
cache.get_mut("/test/file.rs").unwrap().last_mode = "map".to_string();
assert_eq!(cache.last_mode("/test/file.rs"), Some("map".to_string()));
cache.store("/test/file.rs", "new content");
assert_eq!(cache.last_mode("/test/file.rs"), None);
}
#[test]
fn file_refs_are_sequential() {
let mut cache = SessionCache::new();
assert_eq!(cache.get_file_ref("/a.rs"), "F1");
assert_eq!(cache.get_file_ref("/b.rs"), "F2");
assert_eq!(cache.get_file_ref("/a.rs"), "F1"); }
#[test]
fn cache_clear_resets_everything() {
let mut cache = SessionCache::new();
cache.store("/a.rs", "a");
cache.store("/b.rs", "b");
let count = cache.clear();
assert_eq!(count, 2);
assert!(cache.get("/a.rs").is_none());
assert_eq!(cache.get_file_ref("/c.rs"), "F1"); }
#[test]
fn cache_invalidate_removes_entry() {
let mut cache = SessionCache::new();
cache.store("/test.rs", "test");
assert!(cache.invalidate("/test.rs"));
assert!(!cache.invalidate("/nonexistent.rs"));
}
#[test]
fn cache_stats_track_correctly() {
let mut cache = SessionCache::new();
cache.store("/a.rs", "hello");
cache.store("/a.rs", "hello"); let stats = cache.get_stats();
assert_eq!(stats.total_reads(), 2);
assert_eq!(stats.cache_hits(), 1);
assert!(stats.hit_rate() > 0.0);
}
#[test]
fn current_full_content_serves_cached_when_fresh() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("handover.md");
std::fs::write(&file, "HANDOVER V1\n").unwrap();
let path = file.to_str().unwrap();
let mut cache = SessionCache::new();
cache.store(path, "HANDOVER V1\n");
let (content, tokens) = cache
.current_full_content(path)
.expect("cache entry")
.expect("admitted");
assert_eq!(content, "HANDOVER V1\n");
assert!(tokens > 0);
}
#[test]
fn current_full_content_rereads_when_file_changed() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("handover.md");
std::fs::write(&file, "HANDOVER V1\n").unwrap();
let path = file.to_str().unwrap();
let mut cache = SessionCache::new();
cache.store(path, "HANDOVER V1\n");
std::thread::sleep(std::time::Duration::from_millis(10));
std::fs::write(&file, "HANDOVER V2 CHANGED\n").unwrap();
let (content, _) = cache
.current_full_content(path)
.expect("cache entry")
.expect("admitted");
assert_eq!(
content, "HANDOVER V2 CHANGED\n",
"stale cached copy must be re-read from disk, not served as-is"
);
}
#[test]
fn current_full_content_none_without_entry() {
let cache = SessionCache::new();
assert!(cache.current_full_content("/no/such/file.rs").is_none());
}
#[test]
fn current_full_content_withholds_the_cached_copy_when_its_source_is_gone() {
let dir = tempfile::tempdir().unwrap();
let canon = dir.path().canonicalize().unwrap();
let file = canon.join("gone.md");
std::fs::write(&file, "ORIGINAL\n").unwrap();
let path = file.to_str().unwrap().to_string();
let mut cache = SessionCache::new();
cache.store(&path, "ORIGINAL\n");
std::fs::remove_file(&file).unwrap();
let refusal = cache
.current_full_content(&path)
.expect("cache entry")
.expect_err("a deleted source must not be served from the cache");
assert!(refusal.contains("store.source_unavailable"), "{refusal}");
assert!(!refusal.contains("ORIGINAL"), "the refusal is content-free");
}
#[test]
fn record_cache_hit_works_through_shared_ref() {
let mut cache = SessionCache::new();
cache.store("/x.rs", "hello world");
let shared: &SessionCache = &cache;
assert!(shared.record_cache_hit("/x.rs").is_some());
assert!(shared.record_cache_hit("/x.rs").is_some());
assert_eq!(cache.get("/x.rs").unwrap().read_count(), 3);
assert_eq!(cache.get_stats().cache_hits(), 2);
}
#[test]
fn concurrent_cache_hits_are_lossless() {
use std::sync::Arc;
let mut cache = SessionCache::new();
cache.store("/a.rs", "a");
cache.store("/b.rs", "b");
let cache = Arc::new(cache);
let threads = 8;
let iters = 25;
let start = Arc::new(std::sync::Barrier::new(threads));
let handles: Vec<_> = (0..threads)
.map(|_| {
let c = Arc::clone(&cache);
let start = Arc::clone(&start);
std::thread::spawn(move || {
start.wait();
let began = std::time::Instant::now();
for _ in 0..iters {
assert!(c.record_cache_hit("/a.rs").is_some());
assert!(c.record_cache_hit("/b.rs").is_some());
}
(began, std::time::Instant::now())
})
})
.collect();
let spans: Vec<_> = handles.into_iter().map(|h| h.join().unwrap()).collect();
let last_start = spans.iter().map(|(began, _)| *began).max().unwrap();
let first_end = spans.iter().map(|(_, ended)| *ended).min().unwrap();
assert!(
last_start < first_end,
"premise: all threads must record hits concurrently"
);
let total = (threads * iters) as u64;
assert_eq!(cache.get_stats().cache_hits(), total * 2);
assert_eq!(cache.get("/a.rs").unwrap().read_count(), 1 + total as u32);
assert_eq!(cache.get("/b.rs").unwrap().read_count(), 1 + total as u32);
}
#[test]
fn md5_is_deterministic() {
let h1 = compute_md5("test content");
let h2 = compute_md5("test content");
assert_eq!(h1, h2);
assert_ne!(h1, compute_md5("different"));
}
#[test]
fn rrf_eviction_prefers_recent() {
let key_a = "a.rs".to_string();
let key_b = "b.rs".to_string();
let recent = CacheEntry::new("a", "h1".to_string(), 1, 10, "/a.rs".to_string(), None);
let old = CacheEntry::new("b", "h2".to_string(), 1, 10, "/b.rs".to_string(), None);
let t_old = Instant::now();
std::thread::sleep(std::time::Duration::from_millis(10));
let t_recent = Instant::now();
old.set_last_access(t_old);
recent.set_last_access(t_recent);
let now = Instant::now();
let entries: Vec<(&String, &CacheEntry)> = vec![(&key_a, &recent), (&key_b, &old)];
let scores = eviction_scores_rrf(&entries, now);
let score_a = scores.iter().find(|(p, _)| p == "a.rs").unwrap().1;
let score_b = scores.iter().find(|(p, _)| p == "b.rs").unwrap().1;
assert!(
score_a > score_b,
"recently accessed entries should score higher via RRF"
);
}
#[test]
fn rrf_eviction_prefers_frequent() {
let now = Instant::now();
let key_a = "a.rs".to_string();
let key_b = "b.rs".to_string();
let frequent = {
let e = CacheEntry::new("a", "h1".to_string(), 1, 10, "/a.rs".to_string(), None);
e.set_read_count(20);
e
};
let rare = CacheEntry::new("b", "h2".to_string(), 1, 10, "/b.rs".to_string(), None);
let entries: Vec<(&String, &CacheEntry)> = vec![(&key_a, &frequent), (&key_b, &rare)];
let scores = eviction_scores_rrf(&entries, now);
let score_a = scores.iter().find(|(p, _)| p == "a.rs").unwrap().1;
let score_b = scores.iter().find(|(p, _)| p == "b.rs").unwrap().1;
assert!(
score_a > score_b,
"frequently accessed entries should score higher via RRF"
);
}
#[test]
fn cache_budget_resolver_precedence() {
assert_eq!(resolve_cache_max_tokens(Some("250000"), 999), 250_000);
assert_eq!(resolve_cache_max_tokens(Some(" 80000 "), 0), 80_000);
assert_eq!(resolve_cache_max_tokens(Some("0"), 123_456), 123_456);
assert_eq!(resolve_cache_max_tokens(Some(""), 123_456), 123_456);
assert_eq!(resolve_cache_max_tokens(Some("lots"), 123_456), 123_456);
assert_eq!(resolve_cache_max_tokens(None, 42_000), 42_000);
assert_eq!(resolve_cache_max_tokens(None, 0), DEFAULT_CACHE_MAX_TOKENS);
assert_eq!(
resolve_cache_max_tokens(Some("0"), 0),
DEFAULT_CACHE_MAX_TOKENS
);
}
#[test]
fn evict_if_needed_removes_lowest_score() {
let _env_lock = crate::core::data_dir::test_env_lock();
crate::test_env::set_var("LEAN_CTX_CACHE_MAX_TOKENS", "50");
let mut cache = SessionCache::new();
let big_content = "a]".repeat(30); cache.store("/old.rs", &big_content);
let new_content = "b ".repeat(30); cache.store("/new.rs", &new_content);
assert!(
cache.total_cached_tokens() <= 60,
"eviction should have kicked in"
);
crate::test_env::remove_var("LEAN_CTX_CACHE_MAX_TOKENS");
}
#[test]
fn stale_detection_flags_newer_file() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("stale.txt");
let p = path.to_string_lossy().to_string();
std::fs::write(&path, "one").unwrap();
let mut cache = SessionCache::new();
cache.store(&p, "one");
let entry = cache.get(&p).unwrap();
assert!(!is_cache_entry_stale(&p, entry.stored_mtime));
std::thread::sleep(Duration::from_secs(1));
std::fs::write(&path, "two").unwrap();
let entry = cache.get(&p).unwrap();
assert!(is_cache_entry_stale(&p, entry.stored_mtime));
}
#[test]
fn stale_detection_flags_backward_mtime() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("backward.txt");
let p = path.to_string_lossy().to_string();
std::fs::write(&path, "one").unwrap();
let mut cache = SessionCache::new();
cache.store(&p, "one");
let entry_mtime = cache.get(&p).unwrap().stored_mtime;
assert!(!is_cache_entry_stale(&p, entry_mtime));
std::fs::write(&path, "zero").unwrap();
let f = std::fs::OpenOptions::new().write(true).open(&path).unwrap();
f.set_modified(SystemTime::now() - Duration::from_hours(1))
.unwrap();
drop(f);
assert!(
is_cache_entry_stale(&p, entry_mtime),
"older mtime must read as stale"
);
}
#[test]
fn verified_staleness_catches_same_mtime_content_change() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("sneaky.txt");
let p = path.to_string_lossy().to_string();
std::fs::write(&path, "one").unwrap();
let original_mtime = std::fs::metadata(&path).unwrap().modified().unwrap();
let mut cache = SessionCache::new();
cache.store(&p, "one");
let (mtime, hash) = {
let e = cache.get(&p).unwrap();
(e.stored_mtime, e.hash.clone())
};
assert!(!is_cache_entry_stale_verified(&p, mtime, &hash));
std::fs::write(&path, "two").unwrap();
let f = std::fs::OpenOptions::new().write(true).open(&path).unwrap();
f.set_modified(original_mtime).unwrap();
drop(f);
assert!(
!is_cache_entry_stale(&p, mtime),
"test premise: the mtime check alone is fooled"
);
assert!(
is_cache_entry_stale_verified(&p, mtime, &hash),
"hash verification must catch the change"
);
}
#[test]
fn verified_staleness_flags_unreadable_file() {
let mut cache = SessionCache::new();
cache.store("/nonexistent/file.rs", "content");
let (mtime, hash) = {
let e = cache.get("/nonexistent/file.rs").unwrap();
(e.stored_mtime, e.hash.clone())
};
assert!(is_cache_entry_stale_verified(
"/nonexistent/file.rs",
mtime,
&hash
));
}
#[test]
fn compressed_outputs_cached_and_retrieved() {
let mut cache = SessionCache::new();
cache.store("/test.rs", "fn main() {}");
cache.set_compressed("/test.rs", "map", "compressed map output".to_string());
assert_eq!(
cache.get_compressed("/test.rs", "map"),
Some(&"compressed map output".to_string())
);
assert_eq!(cache.get_compressed("/test.rs", "signatures"), None);
}
#[test]
fn compressed_variant_tracks_delivered_conversation() {
let mut cache = SessionCache::new();
cache.store("/test.rs", "fn main() {}");
assert_eq!(
cache.compressed_delivered_conversation("/test.rs", "signatures"),
VariantDelivery::Absent
);
cache.set_compressed("/test.rs", "signatures", "sig view".to_string());
assert_ne!(
cache.compressed_delivered_conversation("/test.rs", "signatures"),
VariantDelivery::Absent,
"stored variant must report delivery state"
);
assert_eq!(
cache.compressed_delivered_conversation("/other.rs", "signatures"),
VariantDelivery::Absent
);
}
#[test]
fn compressed_outputs_evict_least_recently_used_variant() {
let mut cache = SessionCache::new();
cache.store("/test.rs", "fn main() {}");
for index in 0..6 {
cache.set_compressed(
"/test.rs",
&format!("variant-{index}"),
format!("output-{index}"),
);
}
assert!(cache.get_compressed("/test.rs", "variant-0").is_some());
cache.set_compressed("/test.rs", "variant-6", "output-6".to_string());
assert!(cache.get_compressed("/test.rs", "variant-0").is_some());
assert!(cache.get_compressed("/test.rs", "variant-1").is_none());
for index in 2..=6 {
assert!(
cache
.get_compressed("/test.rs", &format!("variant-{index}"))
.is_some()
);
}
assert!(
cache
.get("/test.rs")
.is_some_and(|entry| entry.was_compressed_variant_evicted("variant-1"))
);
}
#[test]
fn compressed_outputs_cleared_on_content_change() {
let mut cache = SessionCache::new();
cache.store("/test.rs", "old content");
cache.set_compressed("/test.rs", "map", "old map".to_string());
assert!(cache.get_compressed("/test.rs", "map").is_some());
cache.store("/test.rs", "new content");
assert_eq!(cache.get_compressed("/test.rs", "map"), None);
}
#[test]
fn compressed_outputs_survive_same_content_store() {
let mut cache = SessionCache::new();
cache.store("/test.rs", "content");
cache.set_compressed("/test.rs", "map", "cached map".to_string());
let result = cache.store("/test.rs", "content");
assert!(result.was_hit);
assert_eq!(
cache.get_compressed("/test.rs", "map"),
Some(&"cached map".to_string())
);
}
#[test]
fn trimming_render_variants_records_a_variant_eviction() {
let mut cache = SessionCache::new();
cache.store("/test.rs", "content");
cache.set_compressed("/test.rs", "map", "cached map".to_string());
assert_eq!(cache.trim_compressed_outputs(), 1);
assert_eq!(cache.get_compressed("/test.rs", "map"), None);
assert!(cache.was_compressed_variant_evicted("/test.rs", "map"));
}
#[test]
fn compressed_outputs_cleared_on_invalidate() {
let mut cache = SessionCache::new();
cache.store("/test.rs", "content");
cache.set_compressed("/test.rs", "signatures", "cached sigs".to_string());
cache.invalidate("/test.rs");
assert_eq!(cache.get_compressed("/test.rs", "signatures"), None);
}
#[test]
fn compressed_outputs_cleared_on_clear() {
let mut cache = SessionCache::new();
cache.store("/a.rs", "a");
cache.set_compressed("/a.rs", "map", "map_a".to_string());
cache.clear();
assert_eq!(cache.get_compressed("/a.rs", "map"), None);
}