use sha2::{Digest, Sha256};
use std::{
fs,
io::{self, Read, Write},
path::{Path, PathBuf},
sync::atomic::{AtomicU64, Ordering},
time::{Duration, SystemTime},
};
pub const DEFAULT_TTL: Duration = Duration::from_secs(60 * 60 * 24 * 90);
const ENTRY_VERSION: u32 = 1;
#[derive(Debug, Clone, Copy, Default)]
pub struct CacheStats {
pub hits: u64,
pub misses: u64,
pub stores: u64,
pub evictions: u64,
}
#[derive(Debug)]
pub struct LlmCache {
root: PathBuf,
ttl: Duration,
hits: AtomicU64,
misses: AtomicU64,
stores: AtomicU64,
evictions: AtomicU64,
}
impl LlmCache {
#[must_use]
pub const fn new(root: PathBuf) -> Self {
Self::with_ttl(root, DEFAULT_TTL)
}
#[must_use]
pub const fn with_ttl(root: PathBuf, ttl: Duration) -> Self {
Self {
root,
ttl,
hits: AtomicU64::new(0),
misses: AtomicU64::new(0),
stores: AtomicU64::new(0),
evictions: AtomicU64::new(0),
}
}
#[must_use]
pub fn default_cache_dir() -> PathBuf {
if let Ok(explicit) = std::env::var("SSG_LLM_CACHE_DIR") {
if !explicit.is_empty() {
return PathBuf::from(explicit);
}
}
if let Ok(xdg) = std::env::var("XDG_CACHE_HOME") {
if !xdg.is_empty() {
return PathBuf::from(xdg).join("ssg").join("llm");
}
}
#[cfg(target_os = "macos")]
{
if let Ok(home) = std::env::var("HOME") {
if !home.is_empty() {
return PathBuf::from(home)
.join("Library")
.join("Caches")
.join("ssg")
.join("llm");
}
}
}
#[cfg(target_os = "windows")]
{
if let Ok(local) = std::env::var("LOCALAPPDATA") {
if !local.is_empty() {
return PathBuf::from(local).join("ssg").join("llm");
}
}
}
if let Ok(home) = std::env::var("HOME") {
if !home.is_empty() {
return generic_unix_cache_dir(&home);
}
}
PathBuf::from(".ssg-llm-cache")
}
#[must_use]
pub fn compute_key(
endpoint: &str,
model: &str,
prompt: &str,
timeout_secs: u64,
) -> [u8; 32] {
let mut hasher = Sha256::new();
hasher.update(b"ssg-llm-cache-v1\x00");
hasher.update((endpoint.len() as u64).to_le_bytes());
hasher.update(endpoint.as_bytes());
hasher.update(b"\x00");
hasher.update((model.len() as u64).to_le_bytes());
hasher.update(model.as_bytes());
hasher.update(b"\x00");
hasher.update((prompt.len() as u64).to_le_bytes());
hasher.update(prompt.as_bytes());
hasher.update(b"\x00");
hasher.update(timeout_secs.to_le_bytes());
hasher.finalize().into()
}
pub fn get(&self, key: &[u8; 32]) -> Option<String> {
let path = self.entry_path(key);
let mut file = match fs::File::open(&path) {
Ok(f) => f,
Err(e) if e.kind() == io::ErrorKind::NotFound => {
let _ = self.misses.fetch_add(1, Ordering::Relaxed);
return None;
}
Err(_) => {
let _ = self.misses.fetch_add(1, Ordering::Relaxed);
return None;
}
};
if entry_is_expired(&file, self.ttl) {
let _ = fs::remove_file(&path);
let _ = self.evictions.fetch_add(1, Ordering::Relaxed);
let _ = self.misses.fetch_add(1, Ordering::Relaxed);
return None;
}
let mut buf = String::new();
if file.read_to_string(&mut buf).is_err() {
let _ = fs::remove_file(&path);
let _ = self.evictions.fetch_add(1, Ordering::Relaxed);
let _ = self.misses.fetch_add(1, Ordering::Relaxed);
return None;
}
if let Some(payload) = parse_entry(&buf, key) {
let _ = self.hits.fetch_add(1, Ordering::Relaxed);
Some(payload)
} else {
let _ = fs::remove_file(&path);
let _ = self.evictions.fetch_add(1, Ordering::Relaxed);
let _ = self.misses.fetch_add(1, Ordering::Relaxed);
None
}
}
pub fn set(&self, key: &[u8; 32], payload: &str) -> io::Result<()> {
let path = self.entry_path(key);
ensure_parent_dir(&path)?;
let key_hex = encode_hex(key);
let body = serde_json::json!({
"version": ENTRY_VERSION,
"key_hex": key_hex,
"payload_len": payload.len(),
"payload": payload,
})
.to_string();
let tmp = path.with_extension(format!(
"tmp.{}.{}",
std::process::id(),
next_tmp_seq(),
));
{
let mut f = fs::File::create(&tmp)?;
f.write_all(body.as_bytes())?;
f.sync_all()?;
}
fs::rename(&tmp, &path)?;
let _ = self.stores.fetch_add(1, Ordering::Relaxed);
Ok(())
}
pub fn evict(&self, key: &[u8; 32]) -> io::Result<()> {
let path = self.entry_path(key);
match fs::remove_file(&path) {
Ok(()) => {
let _ = self.evictions.fetch_add(1, Ordering::Relaxed);
Ok(())
}
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()),
Err(e) => Err(e),
}
}
#[must_use]
pub fn stats(&self) -> CacheStats {
CacheStats {
hits: self.hits.load(Ordering::Relaxed),
misses: self.misses.load(Ordering::Relaxed),
stores: self.stores.load(Ordering::Relaxed),
evictions: self.evictions.load(Ordering::Relaxed),
}
}
#[must_use]
pub fn root(&self) -> &Path {
&self.root
}
fn entry_path(&self, key: &[u8; 32]) -> PathBuf {
let hex = encode_hex(key);
let (shard, rest) = hex.split_at(2);
self.root.join(shard).join(format!("{rest}.json"))
}
}
fn entry_is_expired(file: &fs::File, ttl: Duration) -> bool {
let Ok(meta) = (|| {
fail_point!("llm_cache::get-metadata-err", |_| Err(()));
file.metadata().map_err(|_| ())
})() else {
return false;
};
let Ok(modified) = (|| {
fail_point!("llm_cache::get-modified-err", |_| Err(()));
meta.modified().map_err(|_| ())
})() else {
return false;
};
let Ok(age) = SystemTime::now().duration_since(modified) else {
return false;
};
age > ttl
}
fn ensure_parent_dir(path: &Path) -> io::Result<()> {
let parent = (|| {
fail_point!("llm_cache::set-no-parent", |_| None);
path.parent()
})();
if let Some(parent) = parent {
fs::create_dir_all(parent)?;
}
Ok(())
}
fn generic_unix_cache_dir(home: &str) -> PathBuf {
PathBuf::from(home).join(".cache").join("ssg").join("llm")
}
fn encode_hex(bytes: &[u8; 32]) -> String {
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut out = String::with_capacity(64);
for &b in bytes {
out.push(HEX[(b >> 4) as usize] as char);
out.push(HEX[(b & 0x0f) as usize] as char);
}
out
}
fn parse_entry(text: &str, key: &[u8; 32]) -> Option<String> {
let v: serde_json::Value = serde_json::from_str(text).ok()?;
let version = v.get("version")?.as_u64()?;
if u32::try_from(version).ok()? != ENTRY_VERSION {
return None;
}
let key_hex = v.get("key_hex")?.as_str()?;
if key_hex != encode_hex(key) {
return None;
}
let payload = v.get("payload")?.as_str()?;
let stored_len = usize::try_from(v.get("payload_len")?.as_u64()?).ok()?;
if stored_len != payload.len() {
return None;
}
Some(payload.to_string())
}
fn next_tmp_seq() -> u64 {
static SEQ: AtomicU64 = AtomicU64::new(0);
SEQ.fetch_add(1, Ordering::Relaxed)
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use std::thread;
use std::time::Duration;
fn cache_for_test() -> (tempfile::TempDir, LlmCache) {
let dir = tempfile::tempdir().unwrap();
let cache = LlmCache::new(dir.path().to_path_buf());
(dir, cache)
}
#[test]
fn encode_hex_zero_padded() {
let mut bytes = [0u8; 32];
bytes[0] = 0x0a;
bytes[31] = 0xff;
let hex = encode_hex(&bytes);
assert_eq!(hex.len(), 64);
assert!(hex.starts_with("0a"));
assert!(hex.ends_with("ff"));
}
#[test]
fn compute_key_is_deterministic() {
let k1 = LlmCache::compute_key("e", "m", "p", 1);
let k2 = LlmCache::compute_key("e", "m", "p", 1);
assert_eq!(k1, k2);
}
#[test]
fn compute_key_differs_on_endpoint() {
let a = LlmCache::compute_key("e1", "m", "p", 1);
let b = LlmCache::compute_key("e2", "m", "p", 1);
assert_ne!(a, b);
}
#[test]
fn compute_key_differs_on_model() {
let a = LlmCache::compute_key("e", "m1", "p", 1);
let b = LlmCache::compute_key("e", "m2", "p", 1);
assert_ne!(a, b);
}
#[test]
fn compute_key_differs_on_prompt() {
let a = LlmCache::compute_key("e", "m", "p1", 1);
let b = LlmCache::compute_key("e", "m", "p2", 1);
assert_ne!(a, b);
}
#[test]
fn compute_key_differs_on_timeout() {
let a = LlmCache::compute_key("e", "m", "p", 1);
let b = LlmCache::compute_key("e", "m", "p", 2);
assert_ne!(a, b);
}
#[test]
fn compute_key_resists_length_collision() {
let a = LlmCache::compute_key("ab", "cd", "p", 1);
let b = LlmCache::compute_key("abc", "d", "p", 1);
assert_ne!(a, b);
}
#[test]
#[serial_test::parallel]
fn round_trip_hit() {
let (_d, cache) = cache_for_test();
let key = LlmCache::compute_key("e", "m", "p", 1);
assert!(cache.get(&key).is_none());
cache.set(&key, "the answer").unwrap();
assert_eq!(cache.get(&key).as_deref(), Some("the answer"));
}
#[test]
#[serial_test::parallel]
fn miss_counter_advances_on_absent_key() {
let (_d, cache) = cache_for_test();
let key = LlmCache::compute_key("e", "m", "p", 1);
let _ = cache.get(&key);
let _ = cache.get(&key);
assert_eq!(cache.stats().misses, 2);
assert_eq!(cache.stats().hits, 0);
}
#[test]
#[serial_test::parallel]
fn hit_counter_advances_on_present_key() {
let (_d, cache) = cache_for_test();
let key = LlmCache::compute_key("e", "m", "p", 1);
cache.set(&key, "x").unwrap();
let _ = cache.get(&key);
let _ = cache.get(&key);
assert_eq!(cache.stats().hits, 2);
}
#[test]
#[serial_test::parallel]
fn evict_removes_entry() {
let (_d, cache) = cache_for_test();
let key = LlmCache::compute_key("e", "m", "p", 1);
cache.set(&key, "x").unwrap();
cache.evict(&key).unwrap();
assert!(cache.get(&key).is_none());
}
#[test]
fn evict_missing_is_ok() {
let (_d, cache) = cache_for_test();
let key = LlmCache::compute_key("e", "m", "p", 1);
cache.evict(&key).unwrap();
}
#[test]
#[serial_test::parallel]
fn ttl_zero_expires_immediately() {
let dir = tempfile::tempdir().unwrap();
let cache = LlmCache::with_ttl(
dir.path().to_path_buf(),
Duration::from_nanos(1),
);
let key = LlmCache::compute_key("e", "m", "p", 1);
cache.set(&key, "x").unwrap();
thread::sleep(Duration::from_millis(5));
assert!(cache.get(&key).is_none());
assert!(cache.stats().evictions >= 1);
}
#[test]
#[serial_test::parallel]
fn corrupt_json_evicts_and_misses() {
let (_d, cache) = cache_for_test();
let key = LlmCache::compute_key("e", "m", "p", 1);
cache.set(&key, "x").unwrap();
let p = cache.entry_path(&key);
fs::write(&p, "{ not json").unwrap();
assert!(cache.get(&key).is_none());
assert!(!p.exists(), "corrupt entry should have been evicted");
}
#[test]
#[serial_test::parallel]
fn length_mismatch_evicts() {
let (_d, cache) = cache_for_test();
let key = LlmCache::compute_key("e", "m", "p", 1);
cache.set(&key, "abcdef").unwrap();
let p = cache.entry_path(&key);
let body = serde_json::json!({
"version": ENTRY_VERSION,
"key_hex": encode_hex(&key),
"payload_len": 9999,
"payload": "abcdef",
});
fs::write(&p, body.to_string()).unwrap();
assert!(cache.get(&key).is_none());
}
#[test]
#[serial_test::parallel]
fn version_mismatch_evicts() {
let (_d, cache) = cache_for_test();
let key = LlmCache::compute_key("e", "m", "p", 1);
cache.set(&key, "x").unwrap();
let p = cache.entry_path(&key);
let body = serde_json::json!({
"version": 9999,
"key_hex": encode_hex(&key),
"payload_len": 1,
"payload": "x",
});
fs::write(&p, body.to_string()).unwrap();
assert!(cache.get(&key).is_none());
}
#[test]
#[serial_test::parallel]
fn key_mismatch_evicts() {
let (_d, cache) = cache_for_test();
let key = LlmCache::compute_key("e", "m", "p", 1);
let other = LlmCache::compute_key("x", "y", "z", 9);
cache.set(&key, "x").unwrap();
let p = cache.entry_path(&key);
let body = serde_json::json!({
"version": ENTRY_VERSION,
"key_hex": encode_hex(&other),
"payload_len": 1,
"payload": "x",
});
fs::write(&p, body.to_string()).unwrap();
assert!(cache.get(&key).is_none());
}
#[test]
#[serial_test::parallel]
fn sharding_uses_first_two_hex_chars() {
let (dir, cache) = cache_for_test();
let key = [0xab; 32];
cache.set(&key, "x").unwrap();
let shard = dir.path().join("ab");
assert!(shard.is_dir(), "expected shard dir {shard:?}");
}
#[test]
#[serial_test::parallel]
fn concurrent_distinct_keys_do_not_collide() {
let (_d, cache) = cache_for_test();
let cache = std::sync::Arc::new(cache);
let mut handles = Vec::new();
for i in 0..50 {
let c = std::sync::Arc::clone(&cache);
handles.push(thread::spawn(move || {
let key = LlmCache::compute_key("e", "m", &format!("p{i}"), 1);
c.set(&key, &format!("v{i}")).unwrap();
}));
}
for h in handles {
h.join().unwrap();
}
for i in 0..50 {
let key = LlmCache::compute_key("e", "m", &format!("p{i}"), 1);
assert_eq!(
cache.get(&key).as_deref(),
Some(format!("v{i}").as_str()),
"missing entry for key {i}"
);
}
}
#[test]
#[serial_test::parallel]
fn concurrent_same_key_last_writer_wins_no_corruption() {
let (_d, cache) = cache_for_test();
let cache = std::sync::Arc::new(cache);
let key = LlmCache::compute_key("e", "m", "p", 1);
let mut handles = Vec::new();
for i in 0..20 {
let c = std::sync::Arc::clone(&cache);
handles.push(thread::spawn(move || {
c.set(&key, &format!("v{i}")).unwrap();
}));
}
for h in handles {
h.join().unwrap();
}
let got = cache.get(&key).expect("entry should exist");
assert!(got.starts_with('v'));
}
fn with_env_vars<F: FnOnce()>(vars: &[(&str, Option<&str>)], f: F) {
use std::sync::Mutex;
static ENV_LOCK: Mutex<()> = Mutex::new(());
let _guard = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
let mut prev: Vec<(String, Option<String>)> = Vec::new();
for (key, value) in vars {
prev.push(((*key).to_string(), std::env::var(key).ok()));
match value {
Some(v) => std::env::set_var(key, v),
None => std::env::remove_var(key),
}
}
f();
for (key, value) in prev.into_iter().rev() {
match value {
Some(v) => std::env::set_var(&key, v),
None => std::env::remove_var(&key),
}
}
}
#[test]
fn default_cache_dir_respects_explicit_override() {
with_env_vars(
&[
("SSG_LLM_CACHE_DIR", Some("/outer-sentinel")),
("SSG_LLM_CACHE_DIR", Some("/tmp/ssg-test-cache")),
],
|| {
assert_eq!(
LlmCache::default_cache_dir(),
PathBuf::from("/tmp/ssg-test-cache")
);
},
);
}
#[test]
fn default_cache_dir_empty_override_falls_through_to_xdg() {
with_env_vars(
&[
("SSG_LLM_CACHE_DIR", None),
("SSG_LLM_CACHE_DIR", Some("")),
("XDG_CACHE_HOME", Some("/xdg-root")),
],
|| {
assert_eq!(
LlmCache::default_cache_dir(),
PathBuf::from("/xdg-root").join("ssg").join("llm")
);
},
);
}
#[cfg(target_os = "macos")]
#[test]
fn default_cache_dir_empty_xdg_uses_home_library_caches() {
with_env_vars(
&[
("SSG_LLM_CACHE_DIR", None),
("XDG_CACHE_HOME", Some("")),
("HOME", Some("/home-test")),
],
|| {
assert_eq!(
LlmCache::default_cache_dir(),
PathBuf::from("/home-test")
.join("Library")
.join("Caches")
.join("ssg")
.join("llm")
);
},
);
}
#[test]
fn default_cache_dir_without_home_uses_relative_fallback() {
with_env_vars(
&[
("SSG_LLM_CACHE_DIR", None),
("XDG_CACHE_HOME", None),
("LOCALAPPDATA", None),
("HOME", None),
],
|| {
assert_eq!(
LlmCache::default_cache_dir(),
PathBuf::from(".ssg-llm-cache")
);
},
);
}
#[test]
fn default_cache_dir_empty_home_uses_relative_fallback() {
with_env_vars(
&[
("SSG_LLM_CACHE_DIR", None),
("XDG_CACHE_HOME", None),
("LOCALAPPDATA", None),
("HOME", Some("")),
],
|| {
assert_eq!(
LlmCache::default_cache_dir(),
PathBuf::from(".ssg-llm-cache")
);
},
);
}
#[test]
fn root_returns_constructor_path() {
let dir = tempfile::tempdir().unwrap();
let cache = LlmCache::new(dir.path().to_path_buf());
assert_eq!(cache.root(), dir.path());
}
#[test]
fn stats_default_is_zero() {
let s = CacheStats::default();
assert_eq!(s.hits, 0);
assert_eq!(s.misses, 0);
assert_eq!(s.stores, 0);
assert_eq!(s.evictions, 0);
}
#[test]
#[serial_test::parallel]
fn stats_store_counter_increments() {
let (_d, cache) = cache_for_test();
let k1 = LlmCache::compute_key("e", "m", "p1", 1);
let k2 = LlmCache::compute_key("e", "m", "p2", 1);
cache.set(&k1, "x").unwrap();
cache.set(&k2, "y").unwrap();
assert_eq!(cache.stats().stores, 2);
}
#[test]
#[serial_test::parallel]
fn get_returns_none_when_entry_is_a_directory() {
let (_d, cache) = cache_for_test();
let key = LlmCache::compute_key("e", "m", "p", 1);
let path = cache.entry_path(&key);
fs::create_dir_all(&path).unwrap();
assert!(cache.get(&key).is_none());
}
#[test]
#[serial_test::parallel]
fn get_returns_none_on_missing_entry_increments_miss() {
let (_d, cache) = cache_for_test();
let key = LlmCache::compute_key("e", "m", "missing", 1);
let before = cache.stats().misses;
assert!(cache.get(&key).is_none());
assert!(cache.stats().misses > before);
}
#[test]
fn parse_entry_returns_none_for_missing_fields() {
let key = LlmCache::compute_key("e", "m", "p", 1);
let json = format!(
r#"{{"key_hex":"{}","payload":"x","payload_len":1}}"#,
encode_hex(&key)
);
assert!(parse_entry(&json, &key).is_none());
}
#[test]
fn parse_entry_returns_none_for_non_object_payload() {
let key = LlmCache::compute_key("e", "m", "p", 1);
assert!(parse_entry("[1,2,3]", &key).is_none());
assert!(parse_entry("null", &key).is_none());
}
#[test]
fn evict_propagates_unexpected_io_error() {
let (_d, cache) = cache_for_test();
let key = LlmCache::compute_key("e", "m", "evict-dir", 1);
let path = cache.entry_path(&key);
fs::create_dir_all(&path).unwrap();
let res = cache.evict(&key);
let _ = res;
}
#[cfg(unix)]
#[test]
#[serial_test::parallel]
fn get_open_permission_error_counts_as_miss() {
use std::os::unix::fs::PermissionsExt;
let (_d, cache) = cache_for_test();
let key = LlmCache::compute_key("e", "m", "denied", 1);
cache.set(&key, "x").unwrap();
let path = cache.entry_path(&key);
fs::set_permissions(&path, fs::Permissions::from_mode(0o000)).unwrap();
let misses_before = cache.stats().misses;
assert!(
cache.get(&key).is_none(),
"EACCES must be treated as a miss"
);
assert_eq!(cache.stats().misses, misses_before + 1);
fs::set_permissions(&path, fs::Permissions::from_mode(0o644)).unwrap();
}
#[test]
#[serial_test::parallel]
fn get_hits_when_mtime_is_in_the_future() {
let (_d, cache) = cache_for_test();
let key = LlmCache::compute_key("e", "m", "future", 1);
cache.set(&key, "from-tomorrow").unwrap();
let path = cache.entry_path(&key);
let f = fs::OpenOptions::new().write(true).open(&path).unwrap();
f.set_modified(SystemTime::now() + Duration::from_secs(3600))
.unwrap();
drop(f);
assert_eq!(cache.get(&key).as_deref(), Some("from-tomorrow"));
}
#[test]
#[serial_test::parallel]
fn set_fails_when_root_is_a_file() {
let dir = tempfile::tempdir().unwrap();
let root_file = dir.path().join("rootfile");
fs::write(&root_file, "not a dir").unwrap();
let cache = LlmCache::new(root_file);
let key = LlmCache::compute_key("e", "m", "p", 1);
assert!(cache.set(&key, "x").is_err());
assert_eq!(cache.stats().stores, 0);
}
#[test]
fn next_tmp_seq_is_monotonic() {
let a = next_tmp_seq();
let b = next_tmp_seq();
let c = next_tmp_seq();
assert!(b > a);
assert!(c > b);
}
#[test]
fn generic_unix_cache_dir_joins_expected_components() {
assert_eq!(
generic_unix_cache_dir("/home/alice"),
PathBuf::from("/home/alice")
.join(".cache")
.join("ssg")
.join("llm")
);
}
#[test]
fn llm_cache_debug_impl_includes_type_name() {
let (_d, cache) = cache_for_test();
let debug = format!("{cache:?}");
assert!(debug.contains("LlmCache"));
}
#[test]
fn cache_stats_debug_impl_includes_type_name() {
let stats = CacheStats::default();
let debug = format!("{stats:?}");
assert!(debug.contains("CacheStats"));
}
#[cfg(feature = "test-fault-injection")]
mod fault {
use super::*;
use serial_test::serial;
#[test]
#[serial]
fn entry_is_expired_treats_metadata_error_as_not_expired() {
let (_d, cache) = cache_for_test();
let key = LlmCache::compute_key("e", "m", "metadata-err", 1);
cache.set(&key, "x").unwrap();
fail::cfg("llm_cache::get-metadata-err", "return").unwrap();
let hit = cache.get(&key);
let _ = fail::cfg("llm_cache::get-metadata-err", "off");
assert_eq!(
hit.as_deref(),
Some("x"),
"metadata() failure must fall back to 'not expired', not a miss"
);
}
#[test]
#[serial]
fn entry_is_expired_treats_modified_error_as_not_expired() {
let (_d, cache) = cache_for_test();
let key = LlmCache::compute_key("e", "m", "modified-err", 1);
cache.set(&key, "y").unwrap();
fail::cfg("llm_cache::get-modified-err", "return").unwrap();
let hit = cache.get(&key);
let _ = fail::cfg("llm_cache::get-modified-err", "off");
assert_eq!(
hit.as_deref(),
Some("y"),
"modified() failure must fall back to 'not expired', not a miss"
);
}
#[test]
#[serial]
fn set_skips_mkdir_when_parent_forced_to_none() {
let (_d, cache) = cache_for_test();
let key = LlmCache::compute_key("e", "m", "no-parent", 1);
fail::cfg("llm_cache::set-no-parent", "return").unwrap();
let result = cache.set(&key, "z");
let _ = fail::cfg("llm_cache::set-no-parent", "off");
assert!(
result.is_err(),
"set() should fail when the parent directory was never created"
);
}
}
}