oxify_model/
cache.rs

1//! Cache Management Module
2//!
3//! This module provides comprehensive caching utilities for workflow execution,
4//! including cache key generation, TTL management, invalidation strategies,
5//! and cache warming capabilities.
6//!
7//! # Features
8//!
9//! - **Cache Key Generation:** Deterministic key generation for LLM prompts, code, and retrieval
10//! - **TTL Management:** Time-based expiration with customizable policies
11//! - **Invalidation Strategies:** Pattern-based and dependency-based invalidation
12//! - **Cache Warming:** Preload frequently used results
13//! - **Cache Statistics:** Track hit rates, misses, and performance metrics
14//!
15//! # Example
16//!
17//! ```rust
18//! use oxify_model::cache::{CacheKeyGenerator, CacheConfig, CachePolicy};
19//! use std::time::Duration;
20//!
21//! // Generate cache key for LLM prompt
22//! let key = CacheKeyGenerator::llm_prompt_key(
23//!     "gpt-4",
24//!     "Summarize this text",
25//!     &[("temperature", "0.7")],
26//! );
27//!
28//! // Configure cache policy
29//! let config = CacheConfig::default()
30//!     .with_ttl(Duration::from_secs(3600))
31//!     .with_max_size(1000);
32//! ```
33
34use serde::{Deserialize, Serialize};
35use std::collections::{HashMap, HashSet};
36use std::time::{Duration, SystemTime};
37
38/// Cache policy defining caching behavior
39#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
40pub enum CachePolicy {
41    /// No caching
42    NoCache,
43    /// Cache with time-to-live
44    Ttl(Duration),
45    /// Cache until explicitly invalidated
46    Indefinite,
47    /// Cache with least-recently-used eviction
48    Lru {
49        max_size: usize,
50        ttl: Option<Duration>,
51    },
52    /// Cache with least-frequently-used eviction
53    Lfu {
54        max_size: usize,
55        ttl: Option<Duration>,
56    },
57}
58
59impl Default for CachePolicy {
60    fn default() -> Self {
61        CachePolicy::Ttl(Duration::from_secs(3600)) // 1 hour default
62    }
63}
64
65/// Cache invalidation strategy
66#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
67pub enum InvalidationStrategy {
68    /// Invalidate all cache entries
69    All,
70    /// Invalidate entries matching a pattern
71    Pattern(String),
72    /// Invalidate entries for specific node IDs
73    NodeIds(Vec<String>),
74    /// Invalidate entries older than duration
75    OlderThan(Duration),
76    /// Invalidate entries by tags
77    Tags(Vec<String>),
78    /// Invalidate entries with specific prefix
79    Prefix(String),
80}
81
82/// Cache configuration
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct CacheConfig {
85    /// Default cache policy
86    pub policy: CachePolicy,
87    /// Enable cache warming on startup
88    pub enable_warming: bool,
89    /// Maximum cache size in bytes (0 = unlimited)
90    pub max_size_bytes: u64,
91    /// Enable cache compression
92    pub enable_compression: bool,
93    /// Invalidation strategy
94    pub invalidation_strategy: InvalidationStrategy,
95    /// Enable cache statistics tracking
96    pub enable_stats: bool,
97}
98
99impl Default for CacheConfig {
100    fn default() -> Self {
101        Self {
102            policy: CachePolicy::default(),
103            enable_warming: false,
104            max_size_bytes: 0,
105            enable_compression: false,
106            invalidation_strategy: InvalidationStrategy::OlderThan(Duration::from_secs(86400)), // 24 hours
107            enable_stats: true,
108        }
109    }
110}
111
112impl CacheConfig {
113    /// Set the TTL duration
114    pub fn with_ttl(mut self, ttl: Duration) -> Self {
115        self.policy = CachePolicy::Ttl(ttl);
116        self
117    }
118
119    /// Set the maximum cache size
120    pub fn with_max_size(mut self, max_size: usize) -> Self {
121        self.policy = match self.policy {
122            CachePolicy::Lru { ttl, .. } => CachePolicy::Lru { max_size, ttl },
123            CachePolicy::Lfu { ttl, .. } => CachePolicy::Lfu { max_size, ttl },
124            _ => CachePolicy::Lru {
125                max_size,
126                ttl: Some(Duration::from_secs(3600)),
127            },
128        };
129        self
130    }
131
132    /// Enable cache warming
133    pub fn with_warming(mut self, enable: bool) -> Self {
134        self.enable_warming = enable;
135        self
136    }
137
138    /// Set maximum cache size in bytes
139    pub fn with_max_bytes(mut self, max_bytes: u64) -> Self {
140        self.max_size_bytes = max_bytes;
141        self
142    }
143
144    /// Enable compression
145    pub fn with_compression(mut self, enable: bool) -> Self {
146        self.enable_compression = enable;
147        self
148    }
149
150    /// Set invalidation strategy
151    pub fn with_invalidation(mut self, strategy: InvalidationStrategy) -> Self {
152        self.invalidation_strategy = strategy;
153        self
154    }
155}
156
157/// Cache entry metadata
158#[derive(Debug, Clone, Serialize, Deserialize)]
159pub struct CacheEntry {
160    /// Cache key
161    pub key: String,
162    /// Cached value (serialized)
163    pub value: Vec<u8>,
164    /// Creation timestamp
165    pub created_at: SystemTime,
166    /// Last access timestamp
167    pub last_accessed: SystemTime,
168    /// Access count
169    pub access_count: u64,
170    /// Size in bytes
171    pub size_bytes: usize,
172    /// Tags for categorization
173    pub tags: Vec<String>,
174    /// TTL duration
175    pub ttl: Option<Duration>,
176}
177
178impl CacheEntry {
179    /// Check if the entry has expired
180    pub fn is_expired(&self) -> bool {
181        if let Some(ttl) = self.ttl {
182            if let Ok(elapsed) = self.created_at.elapsed() {
183                return elapsed > ttl;
184            }
185        }
186        false
187    }
188
189    /// Update access metadata
190    pub fn record_access(&mut self) {
191        self.last_accessed = SystemTime::now();
192        self.access_count += 1;
193    }
194
195    /// Get age of the entry
196    pub fn age(&self) -> Duration {
197        self.created_at.elapsed().unwrap_or(Duration::from_secs(0))
198    }
199}
200
201/// Cache key generator utilities
202pub struct CacheKeyGenerator;
203
204impl CacheKeyGenerator {
205    /// Generate cache key for LLM prompt
206    ///
207    /// Creates a deterministic cache key based on:
208    /// - Model name
209    /// - Prompt text
210    /// - Configuration parameters (temperature, max_tokens, etc.)
211    pub fn llm_prompt_key(model: &str, prompt: &str, params: &[(&str, &str)]) -> String {
212        use std::collections::BTreeMap;
213
214        // Sort params for deterministic key generation
215        let sorted_params: BTreeMap<_, _> = params.iter().map(|(k, v)| (*k, *v)).collect();
216        let params_str = sorted_params
217            .iter()
218            .map(|(k, v)| format!("{}={}", k, v))
219            .collect::<Vec<_>>()
220            .join("&");
221
222        format!(
223            "llm:{}:{}:{}",
224            model,
225            Self::hash_string(prompt),
226            Self::hash_string(&params_str)
227        )
228    }
229
230    /// Generate cache key for code execution
231    pub fn code_execution_key(
232        runtime: &str,
233        code: &str,
234        input_vars: &HashMap<String, String>,
235    ) -> String {
236        use std::collections::BTreeMap;
237
238        let sorted_vars: BTreeMap<_, _> = input_vars.iter().collect();
239        let vars_str = sorted_vars
240            .iter()
241            .map(|(k, v)| format!("{}={}", k, Self::hash_string(v)))
242            .collect::<Vec<_>>()
243            .join("&");
244
245        format!(
246            "code:{}:{}:{}",
247            runtime,
248            Self::hash_string(code),
249            Self::hash_string(&vars_str)
250        )
251    }
252
253    /// Generate cache key for vector retrieval
254    pub fn vector_retrieval_key(
255        collection: &str,
256        query: &str,
257        top_k: usize,
258        filters: &HashMap<String, String>,
259    ) -> String {
260        use std::collections::BTreeMap;
261
262        let sorted_filters: BTreeMap<_, _> = filters.iter().collect();
263        let filters_str = sorted_filters
264            .iter()
265            .map(|(k, v)| format!("{}={}", k, v))
266            .collect::<Vec<_>>()
267            .join("&");
268
269        format!(
270            "vector:{}:{}:{}:{}",
271            collection,
272            Self::hash_string(query),
273            top_k,
274            Self::hash_string(&filters_str)
275        )
276    }
277
278    /// Generate cache key for workflow execution
279    pub fn workflow_execution_key(
280        workflow_id: &str,
281        version: &str,
282        inputs: &HashMap<String, String>,
283    ) -> String {
284        use std::collections::BTreeMap;
285
286        let sorted_inputs: BTreeMap<_, _> = inputs.iter().collect();
287        let inputs_str = sorted_inputs
288            .iter()
289            .map(|(k, v)| format!("{}={}", k, Self::hash_string(v)))
290            .collect::<Vec<_>>()
291            .join("&");
292
293        format!(
294            "workflow:{}:{}:{}",
295            workflow_id,
296            version,
297            Self::hash_string(&inputs_str)
298        )
299    }
300
301    /// Hash a string to create a shorter, deterministic identifier
302    fn hash_string(s: &str) -> String {
303        use std::collections::hash_map::DefaultHasher;
304        use std::hash::{Hash, Hasher};
305
306        let mut hasher = DefaultHasher::new();
307        s.hash(&mut hasher);
308        format!("{:x}", hasher.finish())
309    }
310}
311
312/// Cache statistics
313#[derive(Debug, Clone, Default, Serialize, Deserialize)]
314pub struct CacheStats {
315    /// Total cache hits
316    pub hits: u64,
317    /// Total cache misses
318    pub misses: u64,
319    /// Total evictions
320    pub evictions: u64,
321    /// Total entries in cache
322    pub entry_count: usize,
323    /// Total size in bytes
324    pub total_bytes: u64,
325    /// Hit rate percentage
326    pub hit_rate: f64,
327    /// Average access count per entry
328    pub avg_access_count: f64,
329}
330
331impl CacheStats {
332    /// Calculate hit rate
333    pub fn calculate_hit_rate(&mut self) {
334        let total = self.hits + self.misses;
335        self.hit_rate = if total > 0 {
336            (self.hits as f64 / total as f64) * 100.0
337        } else {
338            0.0
339        };
340    }
341
342    /// Record a cache hit
343    pub fn record_hit(&mut self) {
344        self.hits += 1;
345        self.calculate_hit_rate();
346    }
347
348    /// Record a cache miss
349    pub fn record_miss(&mut self) {
350        self.misses += 1;
351        self.calculate_hit_rate();
352    }
353
354    /// Record an eviction
355    pub fn record_eviction(&mut self) {
356        self.evictions += 1;
357    }
358
359    /// Update entry statistics
360    pub fn update_stats(&mut self, entry_count: usize, total_bytes: u64, total_accesses: u64) {
361        self.entry_count = entry_count;
362        self.total_bytes = total_bytes;
363        self.avg_access_count = if entry_count > 0 {
364            total_accesses as f64 / entry_count as f64
365        } else {
366            0.0
367        };
368    }
369}
370
371/// Cache warming configuration
372#[derive(Debug, Clone, Serialize, Deserialize)]
373pub struct CacheWarmingConfig {
374    /// Enable automatic warming on startup
375    pub enabled: bool,
376    /// Node IDs to warm
377    pub node_ids: Vec<String>,
378    /// Maximum entries to warm
379    pub max_entries: usize,
380    /// Warming strategy
381    pub strategy: WarmingStrategy,
382}
383
384/// Cache warming strategy
385#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
386pub enum WarmingStrategy {
387    /// Warm most frequently accessed entries
388    MostFrequent,
389    /// Warm most recently accessed entries
390    MostRecent,
391    /// Warm entries matching specific patterns
392    Pattern(Vec<String>),
393    /// Warm all entries
394    All,
395}
396
397impl Default for CacheWarmingConfig {
398    fn default() -> Self {
399        Self {
400            enabled: false,
401            node_ids: Vec::new(),
402            max_entries: 100,
403            strategy: WarmingStrategy::MostFrequent,
404        }
405    }
406}
407
408/// Cache invalidation plan
409#[derive(Debug, Clone, Serialize, Deserialize)]
410pub struct InvalidationPlan {
411    /// Strategy to use
412    pub strategy: InvalidationStrategy,
413    /// Estimated entries to invalidate
414    pub estimated_count: usize,
415    /// Tags affected
416    pub affected_tags: Vec<String>,
417    /// Node IDs affected
418    pub affected_nodes: Vec<String>,
419}
420
421impl InvalidationPlan {
422    /// Create a new invalidation plan
423    pub fn new(strategy: InvalidationStrategy) -> Self {
424        Self {
425            strategy,
426            estimated_count: 0,
427            affected_tags: Vec::new(),
428            affected_nodes: Vec::new(),
429        }
430    }
431
432    /// Add affected tags
433    pub fn with_tags(mut self, tags: Vec<String>) -> Self {
434        self.affected_tags = tags;
435        self
436    }
437
438    /// Add affected node IDs
439    pub fn with_nodes(mut self, nodes: Vec<String>) -> Self {
440        self.affected_nodes = nodes;
441        self
442    }
443
444    /// Set estimated count
445    pub fn with_count(mut self, count: usize) -> Self {
446        self.estimated_count = count;
447        self
448    }
449}
450
451/// Cache manager for workflow caching
452#[derive(Debug, Clone, Serialize, Deserialize)]
453pub struct CacheManager {
454    /// Cache configuration
455    pub config: CacheConfig,
456    /// Cache statistics
457    pub stats: CacheStats,
458    /// Cache entries
459    entries: HashMap<String, CacheEntry>,
460    /// Tag index for fast lookups
461    tag_index: HashMap<String, HashSet<String>>,
462}
463
464impl CacheManager {
465    /// Create a new cache manager
466    pub fn new(config: CacheConfig) -> Self {
467        Self {
468            config,
469            stats: CacheStats::default(),
470            entries: HashMap::new(),
471            tag_index: HashMap::new(),
472        }
473    }
474
475    /// Get a cache entry
476    pub fn get(&mut self, key: &str) -> Option<&CacheEntry> {
477        if let Some(entry) = self.entries.get_mut(key) {
478            if entry.is_expired() {
479                self.stats.record_miss();
480                self.entries.remove(key);
481                return None;
482            }
483            entry.record_access();
484            self.stats.record_hit();
485            self.entries.get(key)
486        } else {
487            self.stats.record_miss();
488            None
489        }
490    }
491
492    /// Put a cache entry
493    pub fn put(&mut self, mut entry: CacheEntry) {
494        // Check size limits
495        if self.config.max_size_bytes > 0 {
496            let total_size = self.total_size();
497            if total_size + entry.size_bytes as u64 > self.config.max_size_bytes {
498                self.evict_entries(entry.size_bytes);
499            }
500        }
501
502        // Update tag index
503        for tag in &entry.tags {
504            self.tag_index
505                .entry(tag.clone())
506                .or_default()
507                .insert(entry.key.clone());
508        }
509
510        // Set TTL from policy if not specified
511        if entry.ttl.is_none() {
512            entry.ttl = match &self.config.policy {
513                CachePolicy::Ttl(duration) => Some(*duration),
514                CachePolicy::Lru { ttl, .. } | CachePolicy::Lfu { ttl, .. } => *ttl,
515                _ => None,
516            };
517        }
518
519        self.entries.insert(entry.key.clone(), entry);
520        self.update_stats();
521    }
522
523    /// Invalidate cache entries based on strategy
524    pub fn invalidate(&mut self, strategy: &InvalidationStrategy) -> usize {
525        let keys_to_remove: Vec<String> = match strategy {
526            InvalidationStrategy::All => self.entries.keys().cloned().collect(),
527
528            InvalidationStrategy::Pattern(pattern) => self
529                .entries
530                .keys()
531                .filter(|k| k.contains(pattern))
532                .cloned()
533                .collect(),
534
535            InvalidationStrategy::NodeIds(node_ids) => self
536                .entries
537                .keys()
538                .filter(|k| node_ids.iter().any(|nid| k.contains(nid)))
539                .cloned()
540                .collect(),
541
542            InvalidationStrategy::OlderThan(duration) => self
543                .entries
544                .iter()
545                .filter(|(_, entry)| entry.age() > *duration)
546                .map(|(k, _)| k.clone())
547                .collect(),
548
549            InvalidationStrategy::Tags(tags) => {
550                let mut keys = HashSet::new();
551                for tag in tags {
552                    if let Some(tag_keys) = self.tag_index.get(tag) {
553                        keys.extend(tag_keys.iter().cloned());
554                    }
555                }
556                keys.into_iter().collect()
557            }
558
559            InvalidationStrategy::Prefix(prefix) => self
560                .entries
561                .keys()
562                .filter(|k| k.starts_with(prefix))
563                .cloned()
564                .collect(),
565        };
566
567        let count = keys_to_remove.len();
568        for key in keys_to_remove {
569            self.remove(&key);
570        }
571        count
572    }
573
574    /// Remove a specific entry
575    pub fn remove(&mut self, key: &str) {
576        if let Some(entry) = self.entries.remove(key) {
577            // Remove from tag index
578            for tag in &entry.tags {
579                if let Some(tag_keys) = self.tag_index.get_mut(tag) {
580                    tag_keys.remove(key);
581                    if tag_keys.is_empty() {
582                        self.tag_index.remove(tag);
583                    }
584                }
585            }
586            self.stats.record_eviction();
587        }
588        self.update_stats();
589    }
590
591    /// Evict entries to make space
592    fn evict_entries(&mut self, needed_space: usize) {
593        let strategy = &self.config.policy;
594        match strategy {
595            CachePolicy::Lru { .. } => self.evict_lru(needed_space),
596            CachePolicy::Lfu { .. } => self.evict_lfu(needed_space),
597            _ => self.evict_oldest(needed_space),
598        }
599    }
600
601    /// Evict least recently used entries
602    fn evict_lru(&mut self, needed_space: usize) {
603        let mut entries: Vec<_> = self.entries.iter().collect();
604        entries.sort_by_key(|(_, e)| e.last_accessed);
605
606        let mut freed = 0;
607        let mut to_remove = Vec::new();
608
609        for (key, entry) in entries {
610            to_remove.push(key.clone());
611            freed += entry.size_bytes;
612            if freed >= needed_space {
613                break;
614            }
615        }
616
617        for key in to_remove {
618            self.remove(&key);
619        }
620    }
621
622    /// Evict least frequently used entries
623    fn evict_lfu(&mut self, needed_space: usize) {
624        let mut entries: Vec<_> = self.entries.iter().collect();
625        entries.sort_by_key(|(_, e)| e.access_count);
626
627        let mut freed = 0;
628        let mut to_remove = Vec::new();
629
630        for (key, entry) in entries {
631            to_remove.push(key.clone());
632            freed += entry.size_bytes;
633            if freed >= needed_space {
634                break;
635            }
636        }
637
638        for key in to_remove {
639            self.remove(&key);
640        }
641    }
642
643    /// Evict oldest entries
644    fn evict_oldest(&mut self, needed_space: usize) {
645        let mut entries: Vec<_> = self.entries.iter().collect();
646        entries.sort_by_key(|(_, e)| e.created_at);
647
648        let mut freed = 0;
649        let mut to_remove = Vec::new();
650
651        for (key, entry) in entries {
652            to_remove.push(key.clone());
653            freed += entry.size_bytes;
654            if freed >= needed_space {
655                break;
656            }
657        }
658
659        for key in to_remove {
660            self.remove(&key);
661        }
662    }
663
664    /// Get total cache size in bytes
665    fn total_size(&self) -> u64 {
666        self.entries.values().map(|e| e.size_bytes as u64).sum()
667    }
668
669    /// Update statistics
670    fn update_stats(&mut self) {
671        let total_accesses: u64 = self.entries.values().map(|e| e.access_count).sum();
672        self.stats
673            .update_stats(self.entries.len(), self.total_size(), total_accesses);
674    }
675
676    /// Get cache statistics
677    pub fn get_stats(&self) -> &CacheStats {
678        &self.stats
679    }
680
681    /// Clear all cache entries
682    pub fn clear(&mut self) {
683        let count = self.entries.len();
684        self.entries.clear();
685        self.tag_index.clear();
686        for _ in 0..count {
687            self.stats.record_eviction();
688        }
689        self.update_stats();
690    }
691}
692
693impl Default for CacheManager {
694    fn default() -> Self {
695        Self::new(CacheConfig::default())
696    }
697}
698
699#[cfg(test)]
700mod tests {
701    use super::*;
702
703    #[test]
704    fn test_cache_config_default() {
705        let config = CacheConfig::default();
706        assert!(config.enable_stats);
707        assert!(!config.enable_warming);
708        assert_eq!(config.max_size_bytes, 0);
709    }
710
711    #[test]
712    fn test_cache_config_builder() {
713        let config = CacheConfig::default()
714            .with_ttl(Duration::from_secs(1800))
715            .with_max_size(500)
716            .with_warming(true)
717            .with_compression(true);
718
719        assert!(config.enable_warming);
720        assert!(config.enable_compression);
721        assert!(matches!(config.policy, CachePolicy::Lru { .. }));
722    }
723
724    #[test]
725    fn test_llm_prompt_key_generation() {
726        let key1 = CacheKeyGenerator::llm_prompt_key(
727            "gpt-4",
728            "Hello world",
729            &[("temperature", "0.7"), ("max_tokens", "100")],
730        );
731
732        let key2 = CacheKeyGenerator::llm_prompt_key(
733            "gpt-4",
734            "Hello world",
735            &[("max_tokens", "100"), ("temperature", "0.7")],
736        );
737
738        // Keys should be the same regardless of param order
739        assert_eq!(key1, key2);
740        assert!(key1.starts_with("llm:gpt-4:"));
741    }
742
743    #[test]
744    fn test_code_execution_key_generation() {
745        let mut vars = HashMap::new();
746        vars.insert("x".to_string(), "10".to_string());
747        vars.insert("y".to_string(), "20".to_string());
748
749        let key = CacheKeyGenerator::code_execution_key("rust", "fn main() {}", &vars);
750        assert!(key.starts_with("code:rust:"));
751    }
752
753    #[test]
754    fn test_vector_retrieval_key_generation() {
755        let mut filters = HashMap::new();
756        filters.insert("category".to_string(), "tech".to_string());
757
758        let key = CacheKeyGenerator::vector_retrieval_key("docs", "search query", 10, &filters);
759        assert!(key.starts_with("vector:docs:"));
760        assert!(key.contains(":10:"));
761    }
762
763    #[test]
764    fn test_workflow_execution_key_generation() {
765        let mut inputs = HashMap::new();
766        inputs.insert("user_id".to_string(), "123".to_string());
767
768        let key = CacheKeyGenerator::workflow_execution_key("workflow-1", "v1.0.0", &inputs);
769        assert!(key.starts_with("workflow:workflow-1:v1.0.0:"));
770    }
771
772    #[test]
773    fn test_cache_entry_expiration() {
774        let entry = CacheEntry {
775            key: "test".to_string(),
776            value: vec![1, 2, 3],
777            created_at: SystemTime::now() - Duration::from_secs(3700),
778            last_accessed: SystemTime::now(),
779            access_count: 1,
780            size_bytes: 3,
781            tags: vec![],
782            ttl: Some(Duration::from_secs(3600)), // 1 hour
783        };
784
785        assert!(entry.is_expired());
786    }
787
788    #[test]
789    fn test_cache_entry_not_expired() {
790        let entry = CacheEntry {
791            key: "test".to_string(),
792            value: vec![1, 2, 3],
793            created_at: SystemTime::now(),
794            last_accessed: SystemTime::now(),
795            access_count: 1,
796            size_bytes: 3,
797            tags: vec![],
798            ttl: Some(Duration::from_secs(3600)),
799        };
800
801        assert!(!entry.is_expired());
802    }
803
804    #[test]
805    fn test_cache_stats_hit_rate() {
806        let mut stats = CacheStats::default();
807
808        stats.record_hit();
809        stats.record_hit();
810        stats.record_miss();
811
812        assert_eq!(stats.hits, 2);
813        assert_eq!(stats.misses, 1);
814        assert!((stats.hit_rate - 66.666).abs() < 0.01);
815    }
816
817    #[test]
818    fn test_cache_manager_put_and_get() {
819        let config = CacheConfig::default();
820        let mut manager = CacheManager::new(config);
821
822        let entry = CacheEntry {
823            key: "test-key".to_string(),
824            value: vec![1, 2, 3, 4],
825            created_at: SystemTime::now(),
826            last_accessed: SystemTime::now(),
827            access_count: 0,
828            size_bytes: 4,
829            tags: vec!["test".to_string()],
830            ttl: Some(Duration::from_secs(3600)),
831        };
832
833        manager.put(entry);
834
835        let retrieved = manager.get("test-key");
836        assert!(retrieved.is_some());
837        assert_eq!(retrieved.unwrap().value, vec![1, 2, 3, 4]);
838        assert_eq!(manager.stats.hits, 1);
839    }
840
841    #[test]
842    fn test_cache_manager_miss() {
843        let config = CacheConfig::default();
844        let mut manager = CacheManager::new(config);
845
846        let result = manager.get("nonexistent");
847        assert!(result.is_none());
848        assert_eq!(manager.stats.misses, 1);
849    }
850
851    #[test]
852    fn test_cache_invalidation_all() {
853        let config = CacheConfig::default();
854        let mut manager = CacheManager::new(config);
855
856        for i in 0..5 {
857            let entry = CacheEntry {
858                key: format!("key-{}", i),
859                value: vec![i as u8],
860                created_at: SystemTime::now(),
861                last_accessed: SystemTime::now(),
862                access_count: 0,
863                size_bytes: 1,
864                tags: vec![],
865                ttl: None,
866            };
867            manager.put(entry);
868        }
869
870        let count = manager.invalidate(&InvalidationStrategy::All);
871        assert_eq!(count, 5);
872        assert_eq!(manager.entries.len(), 0);
873    }
874
875    #[test]
876    fn test_cache_invalidation_pattern() {
877        let config = CacheConfig::default();
878        let mut manager = CacheManager::new(config);
879
880        let entry1 = CacheEntry {
881            key: "llm:gpt-4:test".to_string(),
882            value: vec![1],
883            created_at: SystemTime::now(),
884            last_accessed: SystemTime::now(),
885            access_count: 0,
886            size_bytes: 1,
887            tags: vec![],
888            ttl: None,
889        };
890
891        let entry2 = CacheEntry {
892            key: "code:rust:test".to_string(),
893            value: vec![2],
894            created_at: SystemTime::now(),
895            last_accessed: SystemTime::now(),
896            access_count: 0,
897            size_bytes: 1,
898            tags: vec![],
899            ttl: None,
900        };
901
902        manager.put(entry1);
903        manager.put(entry2);
904
905        let count = manager.invalidate(&InvalidationStrategy::Pattern("llm:".to_string()));
906        assert_eq!(count, 1);
907        assert_eq!(manager.entries.len(), 1);
908    }
909
910    #[test]
911    fn test_cache_invalidation_tags() {
912        let config = CacheConfig::default();
913        let mut manager = CacheManager::new(config);
914
915        let entry1 = CacheEntry {
916            key: "key1".to_string(),
917            value: vec![1],
918            created_at: SystemTime::now(),
919            last_accessed: SystemTime::now(),
920            access_count: 0,
921            size_bytes: 1,
922            tags: vec!["tag1".to_string()],
923            ttl: None,
924        };
925
926        let entry2 = CacheEntry {
927            key: "key2".to_string(),
928            value: vec![2],
929            created_at: SystemTime::now(),
930            last_accessed: SystemTime::now(),
931            access_count: 0,
932            size_bytes: 1,
933            tags: vec!["tag2".to_string()],
934            ttl: None,
935        };
936
937        manager.put(entry1);
938        manager.put(entry2);
939
940        let count = manager.invalidate(&InvalidationStrategy::Tags(vec!["tag1".to_string()]));
941        assert_eq!(count, 1);
942        assert!(manager.get("key2").is_some());
943    }
944
945    #[test]
946    fn test_cache_invalidation_prefix() {
947        let config = CacheConfig::default();
948        let mut manager = CacheManager::new(config);
949
950        let entry1 = CacheEntry {
951            key: "workflow:123:v1".to_string(),
952            value: vec![1],
953            created_at: SystemTime::now(),
954            last_accessed: SystemTime::now(),
955            access_count: 0,
956            size_bytes: 1,
957            tags: vec![],
958            ttl: None,
959        };
960
961        let entry2 = CacheEntry {
962            key: "llm:gpt-4:test".to_string(),
963            value: vec![2],
964            created_at: SystemTime::now(),
965            last_accessed: SystemTime::now(),
966            access_count: 0,
967            size_bytes: 1,
968            tags: vec![],
969            ttl: None,
970        };
971
972        manager.put(entry1);
973        manager.put(entry2);
974
975        let count = manager.invalidate(&InvalidationStrategy::Prefix("workflow:".to_string()));
976        assert_eq!(count, 1);
977        assert!(manager.get("llm:gpt-4:test").is_some());
978    }
979
980    #[test]
981    fn test_cache_manager_clear() {
982        let config = CacheConfig::default();
983        let mut manager = CacheManager::new(config);
984
985        for i in 0..3 {
986            let entry = CacheEntry {
987                key: format!("key-{}", i),
988                value: vec![i as u8],
989                created_at: SystemTime::now(),
990                last_accessed: SystemTime::now(),
991                access_count: 0,
992                size_bytes: 1,
993                tags: vec![],
994                ttl: None,
995            };
996            manager.put(entry);
997        }
998
999        manager.clear();
1000        assert_eq!(manager.entries.len(), 0);
1001        assert_eq!(manager.stats.evictions, 3);
1002    }
1003
1004    #[test]
1005    fn test_cache_lru_eviction() {
1006        let config = CacheConfig::default().with_max_bytes(10);
1007        let mut manager = CacheManager::new(config);
1008
1009        // Add entries that exceed max size
1010        for i in 0..5 {
1011            let entry = CacheEntry {
1012                key: format!("key-{}", i),
1013                value: vec![i as u8],
1014                created_at: SystemTime::now(),
1015                last_accessed: SystemTime::now() - Duration::from_secs((5 - i) as u64),
1016                access_count: 0,
1017                size_bytes: 3,
1018                tags: vec![],
1019                ttl: None,
1020            };
1021            manager.put(entry);
1022        }
1023
1024        // Should have evicted some entries to stay under limit
1025        assert!(manager.total_size() <= 10);
1026    }
1027
1028    #[test]
1029    fn test_invalidation_plan() {
1030        let plan = InvalidationPlan::new(InvalidationStrategy::All)
1031            .with_tags(vec!["tag1".to_string()])
1032            .with_nodes(vec!["node1".to_string()])
1033            .with_count(10);
1034
1035        assert_eq!(plan.estimated_count, 10);
1036        assert_eq!(plan.affected_tags.len(), 1);
1037        assert_eq!(plan.affected_nodes.len(), 1);
1038    }
1039}