Skip to main content

vtcode_core/cache/
mod.rs

1//! Unified caching system for VT Code
2//!
3//! This module provides a consolidated caching framework that replaces
4//! the multiple duplicate cache implementations throughout the codebase.
5//!
6//! Uses interior mutability with `RwLock` to allow `&self` methods,
7//! following the pattern from matklad's "Caches in Rust" article.
8
9use anyhow::Result;
10use rustc_hash::FxHashMap;
11use serde::{Deserialize, Serialize};
12use std::sync::{Arc, RwLock};
13use std::time::{Duration, SystemTime};
14
15/// Default TTL for cache entries (2 minutes for memory-constrained environments)
16pub const DEFAULT_CACHE_TTL: Duration = Duration::from_secs(120);
17
18/// Default maximum cache capacity (reduced from 10,000 to 1,000 for memory efficiency)
19pub const DEFAULT_MAX_CACHE_CAPACITY: usize = 1_000;
20
21/// Maximum number of items to return in context-limited operations
22pub const MAX_CONTEXT_ITEMS: usize = 5;
23
24/// Unified cache key trait for all cache types
25pub trait CacheKey: Send + Sync + std::hash::Hash + Eq + Clone + 'static {
26    fn to_cache_key(&self) -> String;
27}
28
29/// Unified cache value trait
30pub trait CacheValue: Send + Sync + Clone + 'static {}
31
32impl<T> CacheValue for T where T: Send + Sync + Clone + 'static {}
33
34/// Cache statistics with consistent structure across all cache types
35#[derive(Debug, Clone, Serialize, Deserialize, Default)]
36pub struct CacheStats {
37    pub hits: u64,
38    pub misses: u64,
39    pub evictions: u64,
40    pub current_size: usize,
41    pub max_size: usize,
42    pub total_memory_bytes: u64,
43}
44
45/// Cache entry with metadata
46#[derive(Debug, Clone)]
47pub struct CacheEntry<V> {
48    pub value: Arc<V>,
49    pub created_at: SystemTime,
50    pub last_accessed: SystemTime,
51    pub access_count: u64,
52    pub size_bytes: u64,
53}
54
55impl<V> CacheEntry<V> {
56    pub fn new(value: V, size_bytes: u64) -> Self {
57        Self::from_arc(Arc::new(value), size_bytes)
58    }
59
60    pub fn from_arc(value: Arc<V>, size_bytes: u64) -> Self {
61        let now = SystemTime::now();
62        Self {
63            value,
64            created_at: now,
65            last_accessed: now,
66            access_count: 1,
67            size_bytes,
68        }
69    }
70
71    pub fn mark_accessed(&mut self) {
72        self.last_accessed = SystemTime::now();
73        self.access_count += 1;
74    }
75
76    pub fn is_expired(&self, ttl: Duration) -> bool {
77        SystemTime::now()
78            .duration_since(self.created_at)
79            .map(|age| age > ttl)
80            .unwrap_or(true)
81    }
82}
83
84/// Unified cache backend with configurable eviction policies
85///
86/// Uses interior mutability via `RwLock` to allow `&self` methods,
87/// enabling easier use in concurrent contexts without borrow checker conflicts.
88pub struct UnifiedCache<K, V> {
89    inner: RwLock<UnifiedCacheInner<K, V>>,
90}
91
92/// Internal state for `UnifiedCache`, protected by `RwLock`
93struct UnifiedCacheInner<K, V> {
94    entries: FxHashMap<K, CacheEntry<V>>,
95    max_size: usize,
96    ttl: Duration,
97    stats: CacheStats,
98    eviction_policy: EvictionPolicy,
99}
100
101#[derive(Debug, Clone, Copy)]
102pub enum EvictionPolicy {
103    /// Least Recently Used
104    Lru,
105    /// Least Frequently Used
106    Lfu,
107    /// First In, First Out
108    Fifo,
109    /// Time-based expiration only
110    TtlOnly,
111}
112
113impl<K, V> UnifiedCache<K, V>
114where
115    K: CacheKey,
116    V: CacheValue,
117{
118    pub fn new(max_size: usize, ttl: Duration, eviction_policy: EvictionPolicy) -> Self {
119        Self {
120            inner: RwLock::new(UnifiedCacheInner {
121                entries: FxHashMap::with_capacity_and_hasher(max_size, Default::default()),
122                max_size,
123                ttl,
124                stats: CacheStats { max_size, ..Default::default() },
125                eviction_policy,
126            }),
127        }
128    }
129
130    /// Get value from cache with zero-copy access by default.
131    ///
132    /// Uses a read-first fast path and only attempts a non-blocking write lock
133    /// for best-effort metadata/stat updates.
134    pub fn get(&self, key: &K) -> Option<Arc<V>> {
135        enum LookupState<T> {
136            Hit(Arc<T>),
137            Expired,
138            Miss,
139        }
140
141        let state = {
142            let inner = self.inner.read().unwrap_or_else(|e| e.into_inner());
143            let ttl = inner.ttl;
144
145            match inner.entries.get(key) {
146                Some(entry) if !entry.is_expired(ttl) => LookupState::Hit(Arc::clone(&entry.value)),
147                Some(_) => LookupState::Expired,
148                None => LookupState::Miss,
149            }
150        };
151
152        match state {
153            LookupState::Hit(value) => {
154                if let Ok(mut inner) = self.inner.try_write() {
155                    if let Some(entry) = inner.entries.get_mut(key) {
156                        entry.mark_accessed();
157                    }
158                    inner.stats.hits += 1;
159                }
160                Some(value)
161            }
162            LookupState::Expired => {
163                if let Ok(mut inner) = self.inner.try_write() {
164                    let ttl = inner.ttl;
165                    let should_remove = inner.entries.get(key).map(|entry| entry.is_expired(ttl)).unwrap_or(false);
166                    if should_remove {
167                        Self::remove_inner(&mut inner, key);
168                    }
169                    inner.stats.misses += 1;
170                }
171                None
172            }
173            LookupState::Miss => {
174                if let Ok(mut inner) = self.inner.try_write() {
175                    inner.stats.misses += 1;
176                }
177                None
178            }
179        }
180    }
181
182    /// Get owned value (explicitly clones when needed)
183    pub fn get_owned(&self, key: &K) -> Option<V> {
184        self.get(key).map(|arc| (*arc).clone())
185    }
186
187    /// Insert value into cache with automatic eviction
188    pub fn insert(&self, key: K, value: V, size_bytes: u64) {
189        self.insert_entry(key, CacheEntry::new(value, size_bytes));
190    }
191
192    /// Insert an already shared value without cloning its payload.
193    pub fn insert_arc(&self, key: K, value: Arc<V>, size_bytes: u64) {
194        self.insert_entry(key, CacheEntry::from_arc(value, size_bytes));
195    }
196
197    fn insert_entry(&self, key: K, entry: CacheEntry<V>) {
198        let Ok(mut inner) = self.inner.write() else {
199            return;
200        };
201
202        // Remove expired entries first
203        Self::remove_expired_entries_inner(&mut inner);
204
205        // A zero-capacity cache must remain bounded and reject all inserts.
206        if inner.max_size == 0 {
207            return;
208        }
209
210        let size_bytes = entry.size_bytes;
211
212        // Replacements do not consume another capacity slot, so they must not
213        // evict an unrelated entry when the cache is full.
214        if let Some(current) = inner.entries.get_mut(&key) {
215            let previous = std::mem::replace(current, entry);
216            inner.stats.total_memory_bytes = inner.stats.total_memory_bytes.saturating_sub(previous.size_bytes);
217            inner.stats.current_size = inner.entries.len();
218            inner.stats.total_memory_bytes = inner.stats.total_memory_bytes.saturating_add(size_bytes);
219            return;
220        }
221
222        // Batch evict if over capacity: remove 10% of entries at once
223        // to avoid repeated O(n) scans on consecutive inserts
224        if inner.entries.len() >= inner.max_size {
225            let to_remove = (inner.max_size / 10).max(1);
226            Self::evict_batch_inner(&mut inner, to_remove);
227        }
228
229        inner.entries.insert(key, entry);
230        inner.stats.current_size = inner.entries.len();
231        inner.stats.total_memory_bytes = inner.stats.total_memory_bytes.saturating_add(size_bytes);
232    }
233
234    /// Remove expired entries based on TTL
235    fn remove_expired_entries_inner(inner: &mut UnifiedCacheInner<K, V>) {
236        let expired_keys: Vec<K> = inner
237            .entries
238            .iter()
239            .filter_map(|(k, v)| if v.is_expired(inner.ttl) { Some(k.clone()) } else { None })
240            .collect();
241
242        for key in expired_keys {
243            Self::remove_inner(inner, &key);
244        }
245    }
246
247    /// Evict one entry based on the eviction policy
248    fn evict_one_inner(inner: &mut UnifiedCacheInner<K, V>) {
249        if inner.entries.is_empty() {
250            return;
251        }
252
253        let key_to_remove = match inner.eviction_policy {
254            EvictionPolicy::Lru => Self::find_lru_entry_inner(inner),
255            EvictionPolicy::Lfu => Self::find_lfu_entry_inner(inner),
256            EvictionPolicy::Fifo => Self::find_fifo_entry_inner(inner),
257            EvictionPolicy::TtlOnly => Self::find_oldest_entry_inner(inner),
258        };
259
260        if let Some(key) = key_to_remove {
261            Self::remove_inner(inner, &key);
262            inner.stats.evictions += 1;
263        }
264    }
265
266    /// Batch-evict `count` entries based on the eviction policy.
267    /// Performs a single O(n log n) sort instead of `count` × O(n) linear scans.
268    fn evict_batch_inner(inner: &mut UnifiedCacheInner<K, V>, count: usize) {
269        if inner.entries.is_empty() {
270            return;
271        }
272
273        let keys_to_remove: Vec<K> = match inner.eviction_policy {
274            EvictionPolicy::Lru => {
275                let mut entries: Vec<_> = inner.entries.iter().map(|(k, e)| (k.clone(), e.last_accessed)).collect();
276                entries.sort_by_key(|(_, ts)| *ts);
277                entries.into_iter().take(count).map(|(k, _)| k).collect()
278            }
279            EvictionPolicy::Lfu => {
280                let mut entries: Vec<_> = inner.entries.iter().map(|(k, e)| (k.clone(), e.access_count)).collect();
281                entries.sort_by_key(|(_, c)| *c);
282                entries.into_iter().take(count).map(|(k, _)| k).collect()
283            }
284            EvictionPolicy::Fifo | EvictionPolicy::TtlOnly => {
285                let mut entries: Vec<_> = inner.entries.iter().map(|(k, e)| (k.clone(), e.created_at)).collect();
286                entries.sort_by_key(|(_, ts)| *ts);
287                entries.into_iter().take(count).map(|(k, _)| k).collect()
288            }
289        };
290
291        for key in &keys_to_remove {
292            Self::remove_inner(inner, key);
293        }
294        inner.stats.evictions += keys_to_remove.len() as u64;
295    }
296
297    fn find_lru_entry_inner(inner: &UnifiedCacheInner<K, V>) -> Option<K> {
298        inner
299            .entries
300            .iter()
301            .min_by_key(|(_, entry)| entry.last_accessed)
302            .map(|(k, _)| k.clone())
303    }
304
305    fn find_lfu_entry_inner(inner: &UnifiedCacheInner<K, V>) -> Option<K> {
306        inner
307            .entries
308            .iter()
309            .min_by_key(|(_, entry)| entry.access_count)
310            .map(|(k, _)| k.clone())
311    }
312
313    fn find_fifo_entry_inner(inner: &UnifiedCacheInner<K, V>) -> Option<K> {
314        inner
315            .entries
316            .iter()
317            .min_by_key(|(_, entry)| entry.created_at)
318            .map(|(k, _)| k.clone())
319    }
320
321    fn find_oldest_entry_inner(inner: &UnifiedCacheInner<K, V>) -> Option<K> {
322        Self::find_fifo_entry_inner(inner)
323    }
324
325    fn remove_inner(inner: &mut UnifiedCacheInner<K, V>, key: &K) {
326        if let Some(entry) = inner.entries.remove(key) {
327            inner.stats.total_memory_bytes = inner.stats.total_memory_bytes.saturating_sub(entry.size_bytes);
328            inner.stats.current_size = inner.entries.len();
329        }
330    }
331
332    /// Get cache statistics (returns owned clone)
333    pub fn stats(&self) -> CacheStats {
334        self.inner.read().map(|inner| inner.stats.clone()).unwrap_or_default()
335    }
336
337    /// Clear all entries
338    pub fn clear(&self) {
339        if let Ok(mut inner) = self.inner.write() {
340            inner.entries.clear();
341            inner.stats.current_size = 0;
342            inner.stats.total_memory_bytes = 0;
343        }
344    }
345
346    /// Remove a single cache entry by exact key.
347    pub fn remove(&self, key: &K) {
348        let Ok(mut inner) = self.inner.write() else {
349            return;
350        };
351        Self::remove_inner(&mut inner, key);
352    }
353
354    /// Get current size
355    pub fn len(&self) -> usize {
356        self.inner.read().map(|inner| inner.entries.len()).unwrap_or(0)
357    }
358
359    pub fn is_empty(&self) -> bool {
360        self.len() == 0
361    }
362
363    /// Invalidate cache entries matching a key prefix (selective eviction)
364    /// This replaces the old "clear entire cache" behavior with granular eviction
365    ///
366    /// # Example
367    /// ```ignore
368    /// cache.invalidate_prefix("grep_file:/workspace/src/");
369    /// // Only removes entries for that specific file, not entire cache
370    /// ```
371    pub fn invalidate_prefix(&self, prefix: &str) {
372        self.remove_where(|key| key.to_cache_key().starts_with(prefix));
373    }
374
375    /// Invalidate entries for a specific target path (e.g., file path)
376    /// This is a convenience wrapper for file-based invalidation
377    ///
378    /// # Example
379    /// ```ignore
380    /// cache.invalidate_path("/workspace/src/main.rs");
381    /// // Removes all cache entries related to this file
382    /// ```
383    pub fn invalidate_path(&self, path: &str) {
384        self.invalidate_prefix(&format!("{path}:"));
385    }
386
387    /// Invalidate cache entries matching a key suffix (selective eviction)
388    ///
389    /// # Example
390    /// ```ignore
391    /// cache.invalidate_suffix(":/workspace/src/main.rs");
392    /// // Only removes entries for that specific file
393    /// ```
394    pub fn invalidate_suffix(&self, suffix: &str) {
395        self.remove_where(|key| key.to_cache_key().ends_with(suffix));
396    }
397
398    /// Invalidate cache entries containing a substring (selective eviction)
399    ///
400    /// # Example
401    /// ```ignore
402    /// cache.invalidate_containing("/workspace/src/main.rs");
403    /// // Removes entries where the cache key contains this path
404    /// ```
405    pub fn invalidate_containing(&self, substring: &str) {
406        self.remove_where(|key| key.to_cache_key().contains(substring));
407    }
408
409    /// Remove all entries that satisfy a predicate.
410    ///
411    /// Returns the number of removed entries.
412    pub fn remove_where<F>(&self, mut predicate: F) -> usize
413    where
414        F: FnMut(&K) -> bool,
415    {
416        let Ok(mut inner) = self.inner.write() else {
417            return 0;
418        };
419
420        let keys_to_remove: Vec<K> = inner.entries.keys().filter(|key| predicate(key)).cloned().collect();
421
422        let removed_count = keys_to_remove.len();
423        for key in keys_to_remove {
424            Self::remove_inner(&mut inner, &key);
425        }
426        removed_count
427    }
428
429    /// Get total memory used by cache in bytes
430    pub fn total_memory_bytes(&self) -> u64 {
431        self.inner.read().map(|inner| inner.stats.total_memory_bytes).unwrap_or(0)
432    }
433
434    /// Estimate entry cost in bytes (for memory-aware decisions)
435    /// This is a heuristic based on entry metadata
436    pub fn estimate_entry_cost(entry: &CacheEntry<V>) -> u64 {
437        // Base: entry metadata + overhead
438        let base_overhead: u64 = 100; // Approximate Arc, SystemTime, etc.
439        let value_size = entry.size_bytes;
440        base_overhead + value_size
441    }
442
443    /// Reduce TTL for all entries in cache (for pressure-based tuning)
444    /// Returns the new TTL that was set
445    pub fn reduce_ttl(&self, factor: f64) -> Duration {
446        let Ok(mut inner) = self.inner.write() else {
447            return Duration::ZERO;
448        };
449        let new_ttl = Duration::from_secs_f64(inner.ttl.as_secs_f64() * factor);
450        inner.ttl = new_ttl;
451        new_ttl
452    }
453
454    /// Evict entries under memory pressure (aggressive cleanup)
455    ///
456    /// When memory pressure increases:
457    /// 1. Remove all expired entries first
458    /// 2. Evict least useful entries until target percentage reached
459    /// 3. Use access count and age for ranking
460    pub fn evict_under_pressure(&self, target_reduction_percent: u32) -> usize {
461        let Ok(mut inner) = self.inner.write() else {
462            return 0;
463        };
464
465        // Clamp percentage to 0-100
466        let target_percent = std::cmp::min(100, target_reduction_percent);
467
468        // Remove expired entries first (most efficient cleanup)
469        let expired_before = inner.entries.len();
470        Self::remove_expired_entries_inner(&mut inner);
471        let expired_removed = expired_before - inner.entries.len();
472
473        // Calculate target size
474        let current_size = inner.entries.len();
475        let target_size = (current_size * (100 - target_percent) as usize) / 100;
476
477        // Evict until we reach target
478        let mut evicted_count = expired_removed;
479        while inner.entries.len() > target_size && !inner.entries.is_empty() {
480            Self::evict_one_inner(&mut inner);
481            evicted_count += 1;
482        }
483
484        evicted_count
485    }
486
487    /// Clear a percentage of least-used entries (for aggressive cleanup under critical pressure)
488    /// Returns number of entries removed
489    pub fn clear_least_used(&self, percent_to_clear: u32) -> usize {
490        let Ok(mut inner) = self.inner.write() else {
491            return 0;
492        };
493
494        let percent = std::cmp::min(100, percent_to_clear);
495        let entries_to_remove = (inner.entries.len() * percent as usize) / 100;
496
497        let mut removed = 0usize;
498        for _ in 0..entries_to_remove {
499            if inner.entries.is_empty() {
500                break;
501            }
502            Self::evict_one_inner(&mut inner);
503            removed += 1;
504        }
505
506        removed
507    }
508
509    /// Get entries sorted by "usefulness" (access count and recency)
510    /// Higher score = more useful (keep these)
511    pub fn entries_by_usefulness(&self) -> Vec<(K, CacheEntry<V>)> {
512        let Ok(inner) = self.inner.read() else {
513            return Vec::new();
514        };
515
516        let now = SystemTime::now();
517        let mut entries: Vec<(K, CacheEntry<V>, u64)> = inner
518            .entries
519            .iter()
520            .map(|(k, entry)| {
521                // Score = access_count * recency_factor
522                let age_secs = now.duration_since(entry.last_accessed).unwrap_or_default().as_secs();
523
524                // Recency factor: recent entries get higher score
525                let recency_factor = std::cmp::max(1_u64, 3600 / (age_secs + 1));
526                let usefulness_score = entry.access_count * recency_factor;
527
528                (k.clone(), entry.clone(), usefulness_score)
529            })
530            .collect();
531
532        // Sort by usefulness descending (highest first)
533        entries.sort_by_key(|(_, _, score)| std::cmp::Reverse(*score));
534        entries.into_iter().map(|(k, e, _)| (k, e)).collect()
535    }
536}
537
538/// Helper function to estimate JSON value size without allocation
539pub fn estimate_json_size(value: &serde_json::Value) -> u64 {
540    match value {
541        serde_json::Value::Null => 4,
542        serde_json::Value::Bool(_) => 5,
543        serde_json::Value::Number(n) => n.to_string().len() as u64,
544        serde_json::Value::String(s) => s.len() as u64,
545        serde_json::Value::Array(arr) => arr
546            .iter()
547            .map(estimate_json_size)
548            .fold(0u64, |acc, size| acc.saturating_add(size)),
549        serde_json::Value::Object(obj) => obj
550            .iter()
551            .map(|(k, v)| (k.len() as u64).saturating_add(estimate_json_size(v)).saturating_add(3)) // +3 for quotes and colon
552            .fold(0u64, |acc, size| acc.saturating_add(size)),
553    }
554}
555
556/// Helper function to create cache key from serializable data
557pub fn create_cache_key<T: Serialize>(data: &T) -> Result<String> {
558    let json_bytes = serde_json::to_vec(data)?;
559
560    // Use a simple hash function instead of blake3 to avoid dependency
561    let mut hash = 0u64;
562    for (i, byte) in json_bytes.iter().enumerate() {
563        hash = hash.wrapping_mul(31).wrapping_add(*byte as u64);
564        hash = hash.rotate_left((i % 64) as u32);
565    }
566
567    Ok(format!("{hash:016x}"))
568}
569
570/// Context-aware cache that limits results to MAX_CONTEXT_ITEMS
571pub struct ContextAwareCache<K, V> {
572    inner: UnifiedCache<K, V>,
573}
574
575impl<K, V> ContextAwareCache<K, V>
576where
577    K: CacheKey,
578    V: CacheValue,
579{
580    pub fn new(max_size: usize, ttl: Duration, eviction_policy: EvictionPolicy) -> Self {
581        Self {
582            inner: UnifiedCache::new(max_size, ttl, eviction_policy),
583        }
584    }
585
586    /// Get results with automatic context limitation
587    pub fn get_context_limited<F>(&self, keys: &[K], mut process_fn: F) -> Vec<V>
588    where
589        F: FnMut(&K) -> Option<V>,
590    {
591        let mut results = Vec::with_capacity(MAX_CONTEXT_ITEMS.min(keys.len()));
592        let mut overflow_count = 0;
593
594        for key in keys {
595            if results.len() >= MAX_CONTEXT_ITEMS {
596                overflow_count += 1;
597                continue;
598            }
599
600            if let Some(value) = self.inner.get(key) {
601                results.push((*value).clone());
602            } else if let Some(value) = process_fn(key) {
603                // Cache the result for future use
604                let size = size_of_val(&value) as u64;
605                self.inner.insert(key.clone(), value.clone(), size);
606                results.push(value);
607            }
608        }
609
610        // Add overflow indication if needed
611        if overflow_count > 0 {
612            // This would need to be handled by the caller to add overflow indication
613            // For now, we just limit the results
614        }
615
616        results
617    }
618
619    pub fn stats(&self) -> CacheStats {
620        self.inner.stats()
621    }
622}
623
624#[cfg(test)]
625mod tests {
626    use super::*;
627
628    #[derive(Debug, Clone, PartialEq, Eq, Hash)]
629    struct TestKey(String);
630
631    impl CacheKey for TestKey {
632        fn to_cache_key(&self) -> String {
633            self.0.clone()
634        }
635    }
636
637    #[test]
638    fn test_cache_basic_operations() {
639        let cache = UnifiedCache::new(10, DEFAULT_CACHE_TTL, EvictionPolicy::Lru);
640        let key = TestKey("test".into());
641        let value: String = "test_value".into();
642
643        // Insert and retrieve
644        cache.insert(key.clone(), value.clone(), 100);
645        assert_eq!(*cache.get(&key).unwrap(), value);
646
647        // Check stats
648        let stats = cache.stats();
649        assert_eq!(stats.hits, 1);
650        assert_eq!(stats.misses, 0);
651        assert_eq!(stats.current_size, 1);
652    }
653
654    #[test]
655    fn insert_arc_preserves_value_arc_and_statistics() {
656        let cache = UnifiedCache::new(10, DEFAULT_CACHE_TTL, EvictionPolicy::Lru);
657        let key = TestKey("arc".into());
658        let value = Arc::new("shared value".to_string());
659
660        cache.insert_arc(key.clone(), Arc::clone(&value), value.len() as u64);
661
662        let cached = cache.get(&key).expect("inserted value should be cached");
663        assert!(Arc::ptr_eq(&cached, &value));
664        assert_eq!(cache.stats().total_memory_bytes, value.len() as u64);
665    }
666
667    #[test]
668    fn replacing_entry_updates_memory_statistics() {
669        let cache = UnifiedCache::new(10, DEFAULT_CACHE_TTL, EvictionPolicy::Lru);
670        let key = TestKey("replacement".into());
671
672        cache.insert(key.clone(), "small".to_string(), 5);
673        cache.insert(key, "larger replacement".to_string(), 18);
674
675        let stats = cache.stats();
676        assert_eq!(stats.current_size, 1);
677        assert_eq!(stats.total_memory_bytes, 18);
678    }
679
680    #[test]
681    fn replacing_entry_at_capacity_does_not_evict() {
682        let cache = UnifiedCache::new(1, DEFAULT_CACHE_TTL, EvictionPolicy::Lru);
683        let key = TestKey("replacement".into());
684
685        cache.insert(key.clone(), "old".to_string(), 3);
686        cache.insert(key.clone(), "new value".to_string(), 9);
687
688        assert_eq!(cache.get_owned(&key).as_deref(), Some("new value"));
689        let stats = cache.stats();
690        assert_eq!(stats.current_size, 1);
691        assert_eq!(stats.evictions, 0);
692        assert_eq!(stats.total_memory_bytes, 9);
693    }
694
695    #[test]
696    fn zero_capacity_cache_rejects_entries() {
697        let cache = UnifiedCache::new(0, DEFAULT_CACHE_TTL, EvictionPolicy::Lru);
698
699        cache.insert(TestKey("rejected".into()), "value".to_string(), 5);
700
701        assert_eq!(cache.len(), 0);
702        assert_eq!(cache.total_memory_bytes(), 0);
703        assert_eq!(cache.stats().evictions, 0);
704    }
705
706    #[test]
707    fn test_cache_expiration() {
708        let cache = UnifiedCache::new(10, Duration::from_millis(100), EvictionPolicy::Lru);
709        let key = TestKey("test".into());
710        let value: String = "test_value".into();
711
712        cache.insert(key.clone(), value, 100);
713        assert!(cache.get(&key).is_some());
714
715        // Wait for expiration
716        std::thread::sleep(Duration::from_millis(150));
717        assert!(cache.get(&key).is_none());
718    }
719
720    #[test]
721    fn test_context_limited_cache() {
722        let cache = ContextAwareCache::new(100, DEFAULT_CACHE_TTL, EvictionPolicy::Lru);
723        let keys: Vec<TestKey> = (0..10).map(|i| TestKey(i.to_string())).collect();
724
725        let results = cache.get_context_limited(&keys, |key| Some(format!("value_{}", key.0)));
726
727        // Should be limited to MAX_CONTEXT_ITEMS (5)
728        assert_eq!(results.len(), MAX_CONTEXT_ITEMS);
729        assert_eq!(results[0], "value_0");
730        assert_eq!(results[4], "value_4");
731    }
732
733    #[test]
734    fn test_pressure_aware_total_memory() {
735        let cache = UnifiedCache::new(10, DEFAULT_CACHE_TTL, EvictionPolicy::Lru);
736
737        // Insert three entries with known sizes
738        cache.insert(TestKey("k1".into()), "v1".to_string(), 100);
739        cache.insert(TestKey("k2".into()), "v2".to_string(), 200);
740        cache.insert(TestKey("k3".into()), "v3".to_string(), 300);
741
742        // Total should be 600 bytes
743        assert_eq!(cache.total_memory_bytes(), 600);
744    }
745
746    #[test]
747    fn test_pressure_aware_reduce_ttl() {
748        let cache: UnifiedCache<TestKey, String> = UnifiedCache::new(10, Duration::from_secs(300), EvictionPolicy::Lru);
749
750        // Reduce by 40% (Warning pressure)
751        let new_ttl = cache.reduce_ttl(0.4);
752        assert_eq!(new_ttl.as_secs(), 120); // 300 * 0.4 = 120s
753
754        // Reduce by 10% (Critical pressure)
755        let new_ttl = cache.reduce_ttl(0.1);
756        assert_eq!(new_ttl.as_secs(), 12); // 120 * 0.1 = 12s
757    }
758
759    #[test]
760    fn test_pressure_aware_evict_under_pressure() {
761        let cache: UnifiedCache<TestKey, String> =
762            UnifiedCache::new(20, Duration::from_secs(3600), EvictionPolicy::Lru);
763
764        // Insert 10 entries
765        for i in 0..10 {
766            cache.insert(TestKey(format!("key_{i}")), format!("value_{i}"), 100);
767        }
768
769        assert_eq!(cache.len(), 10);
770
771        // Evict to 50% (remove 5 entries)
772        let removed = cache.evict_under_pressure(50);
773        assert_eq!(removed, 5);
774        assert_eq!(cache.len(), 5);
775    }
776
777    #[test]
778    fn test_pressure_aware_clear_least_used() {
779        let cache: UnifiedCache<TestKey, String> =
780            UnifiedCache::new(20, Duration::from_secs(3600), EvictionPolicy::Lru);
781
782        // Insert 10 entries
783        for i in 0..10 {
784            cache.insert(TestKey(format!("key_{i}")), format!("value_{i}"), 100);
785        }
786
787        // Access some entries to mark them as used
788        let _ = cache.get(&TestKey("key_0".into()));
789        let _ = cache.get(&TestKey("key_1".into()));
790
791        assert_eq!(cache.len(), 10);
792
793        // Clear 30% least used
794        let removed = cache.clear_least_used(30);
795        assert!(removed <= 4, "Should remove ~3 entries (30% of 10)");
796        assert!(cache.len() >= 6, "Should have ~7 entries left");
797    }
798
799    #[test]
800    fn test_pressure_aware_entries_by_usefulness() {
801        let cache: UnifiedCache<TestKey, String> =
802            UnifiedCache::new(20, Duration::from_secs(3600), EvictionPolicy::Lru);
803
804        // Insert and access entries with different patterns
805        cache.insert(TestKey("hot".into()), "value".to_string(), 100);
806        cache.insert(TestKey("cold".into()), "value".to_string(), 100);
807        cache.insert(TestKey("warm".into()), "value".to_string(), 100);
808
809        // Access "hot" multiple times
810        for _ in 0..5 {
811            let _ = cache.get(&TestKey("hot".into()));
812        }
813
814        // Access "warm" once
815        let _ = cache.get(&TestKey("warm".into()));
816
817        // "cold" is never accessed after insert
818
819        let usefulness = cache.entries_by_usefulness();
820        assert_eq!(usefulness.len(), 3);
821
822        // "hot" should be first (most useful)
823        assert_eq!(usefulness[0].0.0, "hot");
824    }
825
826    #[test]
827    fn test_pressure_aware_estimate_entry_cost() {
828        let entry = CacheEntry::new("test_value".to_string(), 1000);
829        let cost = UnifiedCache::<TestKey, String>::estimate_entry_cost(&entry);
830
831        // Cost should be at least the value size + overhead
832        assert!(cost >= 1000);
833        assert!(cost <= 1200); // Should be close to 1100 (1000 + 100 overhead)
834    }
835}