use std::time::SystemTime;
use serde_json::json;
use crate::llm::cache::Cache;
fn set_mtime(path: &std::path::Path, mtime: SystemTime) {
let file = std::fs::OpenOptions::new()
.write(true)
.open(path)
.expect("open for mtime set");
file.set_modified(mtime).expect("set_modified");
}
#[test]
fn evict_if_needed_removes_until_under_limit_and_reports_freed() {
let temp = tempfile::tempdir().expect("tempdir");
let cache = Cache::new(temp.path().to_path_buf(), 30, 60);
let k1 = cache.key(
"sys",
"alpha",
"http://endpoint/v1",
"model",
"openai",
Some(0.2),
);
let k2 = cache.key(
"sys",
"beta",
"http://endpoint/v1",
"model",
"openai",
Some(0.2),
);
cache
.put(
&k1,
&json!({"payload": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}),
)
.expect("put k1");
cache
.put(
&k2,
&json!({"payload": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"}),
)
.expect("put k2");
let freed = cache.evict_if_needed().expect("eviction");
let remaining = crate::test_support::two_level_tree_size(temp.path());
assert!(
remaining <= 60,
"tree must be under max_bytes after eviction, was {remaining}"
);
assert!(
freed > 0,
"freed must be positive when eviction ran, was {freed}"
);
}
#[test]
fn eviction_removes_oldest_entry_first() {
let temp = tempfile::tempdir().expect("tempdir");
let cache = Cache::new(temp.path().to_path_buf(), 30, 60);
let k_old = cache.key(
"sys",
"older",
"http://endpoint/v1",
"model",
"openai",
Some(0.2),
);
let k_new = cache.key(
"sys",
"newer",
"http://endpoint/v1",
"model",
"openai",
Some(0.2),
);
cache
.put(
&k_old,
&json!({"payload": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}),
)
.expect("put old");
cache
.put(
&k_new,
&json!({"payload": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"}),
)
.expect("put new");
let old_path = cache.entry_path(&k_old);
let new_path = cache.entry_path(&k_new);
set_mtime(
&old_path,
SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(86_400),
);
let freed = cache.evict_if_needed().expect("eviction");
assert!(freed > 0, "eviction must run, freed {freed}");
assert!(
!old_path.exists(),
"the older entry must be evicted: {old_path:?}"
);
assert!(
new_path.exists(),
"the newer entry must survive eviction: {new_path:?}"
);
}
#[test]
fn evict_if_needed_is_a_no_op_when_already_under_limit() {
let temp = tempfile::tempdir().expect("tempdir");
let cache = Cache::new(temp.path().to_path_buf(), 30, 10 * 1024 * 1024);
let k = cache.key(
"sys",
"content",
"http://endpoint/v1",
"model",
"openai",
Some(0.2),
);
cache.put(&k, &json!({"x": 1})).expect("put");
let freed = cache.evict_if_needed().expect("eviction");
assert_eq!(freed, 0, "no eviction needed, freed must be 0");
assert!(
cache.entry_path(&k).exists(),
"the entry must survive a no-op eviction"
);
}
#[test]
fn eviction_ignores_directories_that_are_not_shards() {
let temp = tempfile::tempdir().expect("tempdir");
let root = temp.path().to_path_buf();
let cache = Cache::new(root.clone(), 30, 1);
let key = cache.key(
"sys",
"content",
"http://e/v1",
"model",
"openai",
Some(0.2),
);
cache.put(&key, &serde_json::json!({"a": 1})).expect("put");
let stray_file = root.join("notes.txt");
std::fs::write(&stray_file, b"do not delete me").expect("stray file");
let foreign_dir = root.join("zz");
std::fs::create_dir_all(&foreign_dir).expect("foreign dir");
let foreign_file = foreign_dir.join("important.bin");
std::fs::write(&foreign_file, vec![0u8; 4096]).expect("foreign file");
cache.evict_if_needed().expect("eviction runs");
assert!(
foreign_file.exists(),
"a directory that is not a shard must not be walked, let alone emptied"
);
assert!(
stray_file.exists(),
"a stray file in the root is not an entry"
);
}
#[test]
fn eviction_ignores_foreign_files_inside_a_valid_shard() {
let temp = tempfile::tempdir().expect("tempdir");
let cache = Cache::new(temp.path().to_path_buf(), 30, 1);
let key = cache.key("sys", "content", "http://e/v1", "model", "openai", None);
cache.put(&key, &json!({"a": 1})).expect("cache entry");
let entry_path = cache.entry_path(&key);
let shard = entry_path.parent().expect("shard");
let foreign = shard.join("notes.json");
std::fs::write(&foreign, vec![0_u8; 4096]).expect("foreign file");
cache.evict_if_needed().expect("eviction");
assert!(
foreign.exists(),
"eviction removed a file it did not create"
);
}
#[test]
fn eviction_requires_the_digest_to_be_lower_hex_and_match_its_shard() {
let temp = tempfile::tempdir().expect("tempdir");
let cache = Cache::new(temp.path().to_path_buf(), 30, 0);
let wrong_shard = temp.path().join("ff");
std::fs::create_dir_all(&wrong_shard).expect("wrong shard");
let wrong_shard_entry = wrong_shard.join(format!("{}.json", "a".repeat(64)));
std::fs::write(&wrong_shard_entry, b"x").expect("foreign file");
let invalid_hex_shard = temp.path().join("ab");
std::fs::create_dir_all(&invalid_hex_shard).expect("invalid hex shard");
let invalid_hex_entry = invalid_hex_shard.join(format!("ab{}g.json", "a".repeat(61)));
std::fs::write(&invalid_hex_entry, b"x").expect("foreign file");
cache.evict_if_needed().expect("eviction");
assert!(
wrong_shard_entry.exists(),
"digest belongs to another shard"
);
assert!(invalid_hex_entry.exists(), "digest is not lower-case hex");
}