Skip to main content

lens_core/lsp/
hint.rs

1//! LSP Hint Caching System
2//!
3//! Implements 24h TTL caching with invalidation for LSP results
4//! Features:
5//! - Time-based expiration (24h default)
6//! - Size-based LRU eviction
7//! - File change invalidation
8//! - Concurrent access with dashmap
9//! - Metrics tracking
10
11use super::{LspSearchResult, LspServerType};
12use anyhow::Result;
13use dashmap::DashMap;
14use serde::{Deserialize, Serialize};
15use std::path::PathBuf;
16use std::sync::Arc;
17use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
18use tokio::sync::RwLock;
19use tokio::time::interval;
20use tracing::{debug, info, warn};
21
22/// Type of LSP hint for categorization
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
24pub enum HintType {
25    Definition,
26    References,
27    TypeDefinition,
28    Implementation,
29    Declaration,
30    Symbol,
31    Hover,
32    Completion,
33}
34
35impl HintType {
36    pub fn as_str(&self) -> &'static str {
37        match self {
38            HintType::Definition => "definition",
39            HintType::References => "references",
40            HintType::TypeDefinition => "type_definition",
41            HintType::Implementation => "implementation",
42            HintType::Declaration => "declaration",
43            HintType::Symbol => "symbol",
44            HintType::Hover => "hover",
45            HintType::Completion => "completion",
46        }
47    }
48}
49
50/// Cached LSP hint entry
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct CachedHint {
53    pub results: Vec<LspSearchResult>,
54    pub created_at: u64,
55    pub expires_at: u64,
56    pub access_count: u64,
57    pub last_accessed: u64,
58    pub cache_key: String,
59}
60
61impl CachedHint {
62    pub fn new(results: Vec<LspSearchResult>, ttl_seconds: u64, cache_key: String) -> Self {
63        let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
64        Self {
65            results,
66            created_at: now,
67            expires_at: now + ttl_seconds,
68            access_count: 0,
69            last_accessed: now,
70            cache_key,
71        }
72    }
73
74    pub fn is_expired(&self) -> bool {
75        let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
76        now >= self.expires_at
77    }
78
79    pub fn touch(&mut self) {
80        self.access_count += 1;
81        self.last_accessed = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
82    }
83
84    pub fn age_seconds(&self) -> u64 {
85        let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
86        now.saturating_sub(self.created_at)
87    }
88}
89
90/// File modification tracking for cache invalidation
91#[derive(Debug, Clone)]
92pub struct FileTracker {
93    pub path: PathBuf,
94    pub last_modified: SystemTime,
95    pub size: u64,
96}
97
98impl FileTracker {
99    pub async fn new(path: PathBuf) -> Result<Self> {
100        let metadata = tokio::fs::metadata(&path).await?;
101        Ok(Self {
102            path,
103            last_modified: metadata.modified()?,
104            size: metadata.len(),
105        })
106    }
107
108    pub async fn has_changed(&self) -> bool {
109        match tokio::fs::metadata(&self.path).await {
110            Ok(metadata) => {
111                let new_modified = metadata.modified().unwrap_or(UNIX_EPOCH);
112                let new_size = metadata.len();
113                new_modified != self.last_modified || new_size != self.size
114            }
115            Err(_) => true, // File doesn't exist anymore, consider it changed
116        }
117    }
118}
119
120/// Cache statistics
121#[derive(Debug, Default, Clone)]
122pub struct CacheStats {
123    pub hits: u64,
124    pub misses: u64,
125    pub evictions: u64,
126    pub invalidations: u64,
127    pub size: usize,
128    pub memory_usage_bytes: u64,
129    pub avg_lookup_time_ns: u64,
130    pub avg_storage_time_ns: u64,
131    pub hit_rate: f64,
132}
133
134impl CacheStats {
135    pub fn update_hit_rate(&mut self) {
136        let total_requests = self.hits + self.misses;
137        if total_requests > 0 {
138            self.hit_rate = self.hits as f64 / total_requests as f64;
139        }
140    }
141    
142    /// Check if cache meets TODO.md performance targets: ≤+1ms p95 overhead
143    pub fn meets_performance_targets(&self) -> bool {
144        const MAX_LOOKUP_TIME_NS: u64 = 1_000_000; // 1ms in nanoseconds
145        const MIN_HIT_RATE: f64 = 0.7; // 70% minimum hit rate for effectiveness
146        
147        self.avg_lookup_time_ns <= MAX_LOOKUP_TIME_NS && self.hit_rate >= MIN_HIT_RATE
148    }
149}
150
151impl CacheStats {
152    /// Calculate current hit rate
153    pub fn calculate_hit_rate(&self) -> f64 {
154        if self.hits + self.misses == 0 {
155            0.0
156        } else {
157            self.hits as f64 / (self.hits + self.misses) as f64
158        }
159    }
160    
161    /// Update timing metrics with exponential moving average
162    pub fn update_lookup_timing(&mut self, lookup_time_ns: u64) {
163        const ALPHA: f64 = 0.1; // EMA smoothing factor
164        if self.avg_lookup_time_ns == 0 {
165            self.avg_lookup_time_ns = lookup_time_ns;
166        } else {
167            self.avg_lookup_time_ns = ((1.0 - ALPHA) * self.avg_lookup_time_ns as f64 
168                                    + ALPHA * lookup_time_ns as f64) as u64;
169        }
170    }
171    
172    /// Update storage timing metrics
173    pub fn update_storage_timing(&mut self, storage_time_ns: u64) {
174        const ALPHA: f64 = 0.1; // EMA smoothing factor
175        if self.avg_storage_time_ns == 0 {
176            self.avg_storage_time_ns = storage_time_ns;
177        } else {
178            self.avg_storage_time_ns = ((1.0 - ALPHA) * self.avg_storage_time_ns as f64 
179                                      + ALPHA * storage_time_ns as f64) as u64;
180        }
181    }
182}
183
184/// High-performance LSP hint cache
185pub struct HintCache {
186    // Main cache storage
187    cache: Arc<DashMap<String, CachedHint>>,
188    
189    // File modification tracking
190    file_trackers: Arc<RwLock<DashMap<PathBuf, FileTracker>>>,
191    
192    // Configuration
193    max_size: usize,
194    default_ttl_seconds: u64,
195    
196    // Statistics
197    stats: Arc<RwLock<CacheStats>>,
198    
199    // Background tasks
200    cleanup_handle: Option<tokio::task::JoinHandle<()>>,
201    invalidation_handle: Option<tokio::task::JoinHandle<()>>,
202}
203
204impl HintCache {
205    /// Create a new hint cache
206    pub async fn new(ttl_hours: u64) -> Result<Self> {
207        let cache = Arc::new(DashMap::new());
208        let file_trackers = Arc::new(RwLock::new(DashMap::new()));
209        let stats = Arc::new(RwLock::new(CacheStats::default()));
210        
211        let mut hint_cache = Self {
212            cache: cache.clone(),
213            file_trackers,
214            max_size: 10000, // Default max entries
215            default_ttl_seconds: ttl_hours * 3600,
216            stats,
217            cleanup_handle: None,
218            invalidation_handle: None,
219        };
220
221        // Start background cleanup task
222        hint_cache.start_cleanup_task().await;
223        
224        // Start file invalidation task
225        hint_cache.start_invalidation_task().await;
226        
227        info!("LSP hint cache initialized with {}h TTL", ttl_hours);
228        Ok(hint_cache)
229    }
230
231    /// Get cached hints with performance timing
232    pub async fn get(&self, key: &str) -> Result<Option<Vec<LspSearchResult>>> {
233        let start_time = std::time::Instant::now();
234        let result = self.get_internal(key).await;
235        let elapsed_ns = start_time.elapsed().as_nanos() as u64;
236        
237        // Update timing statistics
238        {
239            let mut stats = self.stats.write().await;
240            stats.update_lookup_timing(elapsed_ns);
241            stats.hit_rate = stats.calculate_hit_rate();
242        }
243        
244        result
245    }
246    
247    /// Internal get method without timing overhead
248    async fn get_internal(&self, key: &str) -> Result<Option<Vec<LspSearchResult>>> {
249        let mut stats = self.stats.write().await;
250        
251        match self.cache.get_mut(key) {
252            Some(mut entry) => {
253                if entry.is_expired() {
254                    // Remove expired entry
255                    drop(entry);
256                    self.cache.remove(key);
257                    stats.misses += 1;
258                    debug!("Cache expired for key: {}", key);
259                    Ok(None)
260                } else {
261                    // Update access info and return results
262                    entry.touch();
263                    let results = entry.results.clone();
264                    stats.hits += 1;
265                    debug!("Cache hit for key: {}", key);
266                    Ok(Some(results))
267                }
268            }
269            None => {
270                stats.misses += 1;
271                debug!("Cache miss for key: {}", key);
272                Ok(None)
273            }
274        }
275    }
276
277    /// Set cached hints with performance timing
278    pub async fn set(&self, key: String, results: Vec<LspSearchResult>, ttl_seconds: u64) -> Result<()> {
279        let start_time = std::time::Instant::now();
280        let result = self.set_internal(key, results, ttl_seconds).await;
281        let elapsed_ns = start_time.elapsed().as_nanos() as u64;
282        
283        // Update timing statistics
284        {
285            let mut stats = self.stats.write().await;
286            stats.update_storage_timing(elapsed_ns);
287        }
288        
289        result
290    }
291    
292    /// Internal set method without timing overhead
293    async fn set_internal(&self, key: String, results: Vec<LspSearchResult>, ttl_seconds: u64) -> Result<()> {
294        // Check size limits
295        if self.cache.len() >= self.max_size {
296            self.evict_lru().await;
297        }
298
299        let cached_hint = CachedHint::new(results, ttl_seconds, key.clone());
300        
301        // Track files mentioned in results for invalidation
302        self.track_files_from_results(&cached_hint.results).await;
303        
304        self.cache.insert(key.clone(), cached_hint);
305        
306        // Update stats
307        {
308            let mut stats = self.stats.write().await;
309            stats.size = self.cache.len();
310            stats.memory_usage_bytes = self.estimate_memory_usage();
311        }
312        
313        debug!("Cached hints for key: {}", key);
314        Ok(())
315    }
316
317    /// Invalidate cache entries for a specific file
318    pub async fn invalidate_file(&self, file_path: &PathBuf) -> Result<u64> {
319        let mut invalidated = 0;
320        let file_path_str = file_path.to_string_lossy();
321        
322        // Remove entries that reference this file
323        self.cache.retain(|_key, hint| {
324            let should_keep = !hint.results.iter().any(|result| {
325                result.file_path.contains(&*file_path_str)
326            });
327            
328            if !should_keep {
329                invalidated += 1;
330            }
331            
332            should_keep
333        });
334
335        // Update stats
336        if invalidated > 0 {
337            let mut stats = self.stats.write().await;
338            stats.invalidations += invalidated;
339            stats.size = self.cache.len();
340            debug!("Invalidated {} cache entries for file: {:?}", invalidated, file_path);
341        }
342
343        Ok(invalidated)
344    }
345
346    /// Clear all cache entries
347    pub async fn clear(&self) -> Result<()> {
348        let size = self.cache.len();
349        self.cache.clear();
350        
351        {
352            let mut stats = self.stats.write().await;
353            stats.size = 0;
354            stats.memory_usage_bytes = 0;
355        }
356        
357        info!("Cleared {} cache entries", size);
358        Ok(())
359    }
360
361    /// Get cache statistics
362    pub async fn stats(&self) -> CacheStats {
363        self.stats.read().await.clone()
364    }
365
366    /// Track files from search results for invalidation
367    async fn track_files_from_results(&self, results: &[LspSearchResult]) {
368        let trackers = self.file_trackers.read().await;
369        
370        for result in results {
371            let path = PathBuf::from(&result.file_path);
372            
373            if !trackers.contains_key(&path) {
374                if let Ok(tracker) = FileTracker::new(path.clone()).await {
375                    drop(trackers); // Release read lock
376                    let mut trackers_write = self.file_trackers.write().await;
377                    trackers_write.insert(path, tracker);
378                    return; // Re-acquire read lock on next iteration
379                }
380            }
381        }
382    }
383
384    /// Evict least recently used entries
385    async fn evict_lru(&self) {
386        if self.cache.is_empty() {
387            return;
388        }
389
390        // Find oldest entry by last_accessed time
391        let mut oldest_key: Option<String> = None;
392        let mut oldest_time = u64::MAX;
393
394        for entry in self.cache.iter() {
395            if entry.value().last_accessed < oldest_time {
396                oldest_time = entry.value().last_accessed;
397                oldest_key = Some(entry.key().clone());
398            }
399        }
400
401        if let Some(key) = oldest_key {
402            self.cache.remove(&key);
403            
404            let mut stats = self.stats.write().await;
405            stats.evictions += 1;
406            stats.size = self.cache.len();
407            
408            debug!("Evicted LRU cache entry: {}", key);
409        }
410    }
411
412    /// Start background cleanup task for expired entries
413    async fn start_cleanup_task(&mut self) {
414        let cache = self.cache.clone();
415        let stats = self.stats.clone();
416        
417        let handle = tokio::spawn(async move {
418            let mut interval = interval(Duration::from_secs(300)); // 5 minutes
419            
420            loop {
421                interval.tick().await;
422                
423                let mut expired_keys = Vec::new();
424                
425                // Find expired entries
426                for entry in cache.iter() {
427                    if entry.value().is_expired() {
428                        expired_keys.push(entry.key().clone());
429                    }
430                }
431                
432                // Remove expired entries
433                let mut removed_count = 0;
434                for key in expired_keys {
435                    if cache.remove(&key).is_some() {
436                        removed_count += 1;
437                    }
438                }
439                
440                if removed_count > 0 {
441                    let mut stats_lock = stats.write().await;
442                    stats_lock.size = cache.len();
443                    debug!("Cleaned up {} expired cache entries", removed_count);
444                }
445            }
446        });
447        
448        self.cleanup_handle = Some(handle);
449    }
450
451    /// Start background file invalidation task
452    async fn start_invalidation_task(&mut self) {
453        let cache = self.cache.clone();
454        let file_trackers = self.file_trackers.clone();
455        let stats = self.stats.clone();
456        
457        let handle = tokio::spawn(async move {
458            let mut interval = interval(Duration::from_secs(60)); // 1 minute
459            
460            loop {
461                interval.tick().await;
462                
463                let trackers = file_trackers.read().await;
464                let mut changed_files = Vec::new();
465                
466                // Check for file changes
467                for tracker in trackers.iter() {
468                    if tracker.value().has_changed().await {
469                        changed_files.push(tracker.key().clone());
470                    }
471                }
472                
473                if !changed_files.is_empty() {
474                    drop(trackers); // Release read lock
475                    
476                    let mut total_invalidated = 0;
477                    
478                    for file_path in changed_files {
479                        let file_path_str = file_path.to_string_lossy();
480                        
481                        // Invalidate cache entries for this file
482                        cache.retain(|_key, hint| {
483                            let should_keep = !hint.results.iter().any(|result| {
484                                result.file_path.contains(&*file_path_str)
485                            });
486                            
487                            if !should_keep {
488                                total_invalidated += 1;
489                            }
490                            
491                            should_keep
492                        });
493                        
494                        // Update file tracker
495                        if let Ok(new_tracker) = FileTracker::new(file_path.clone()).await {
496                            let mut trackers_write = file_trackers.write().await;
497                            trackers_write.insert(file_path, new_tracker);
498                        }
499                    }
500                    
501                    if total_invalidated > 0 {
502                        let mut stats_lock = stats.write().await;
503                        stats_lock.invalidations += total_invalidated;
504                        stats_lock.size = cache.len();
505                        
506                        debug!("Invalidated {} cache entries due to file changes", total_invalidated);
507                    }
508                }
509            }
510        });
511        
512        self.invalidation_handle = Some(handle);
513    }
514
515    /// Estimate memory usage (rough calculation)
516    fn estimate_memory_usage(&self) -> u64 {
517        // Rough estimate: each cache entry ~1KB
518        self.cache.len() as u64 * 1024
519    }
520
521    /// Shutdown the cache and background tasks
522    pub async fn shutdown(&self) -> Result<()> {
523        info!("Shutting down LSP hint cache");
524        
525        if let Some(handle) = &self.cleanup_handle {
526            handle.abort();
527        }
528        
529        if let Some(handle) = &self.invalidation_handle {
530            handle.abort();
531        }
532        
533        self.cache.clear();
534        
535        Ok(())
536    }
537}
538
539impl Drop for HintCache {
540    fn drop(&mut self) {
541        debug!("LSP hint cache dropped");
542    }
543}
544
545/// Symbol hint for caching and quick lookups
546#[derive(Debug, Clone, Serialize, Deserialize)]
547pub struct SymbolHint {
548    pub symbol_name: String,
549    pub symbol_kind: String,
550    pub file_path: String,
551    pub line_number: u32,
552    pub column: u32,
553    pub documentation: Option<String>,
554    pub signature: Option<String>,
555    pub confidence: f64,
556}
557
558impl SymbolHint {
559    pub fn new(
560        symbol_name: String,
561        symbol_kind: String,
562        file_path: String,
563        line_number: u32,
564        column: u32,
565        confidence: f64,
566    ) -> Self {
567        Self {
568            symbol_name,
569            symbol_kind,
570            file_path,
571            line_number,
572            column,
573            documentation: None,
574            signature: None,
575            confidence,
576        }
577    }
578}
579
580#[cfg(test)]
581mod tests {
582    use super::*;
583    use tokio::time::sleep;
584
585    #[tokio::test]
586    async fn test_cache_basic_operations() {
587        let cache = HintCache::new(1).await.unwrap(); // 1 hour TTL
588        let key = "test_key".to_string();
589        
590        // Test miss
591        assert!(cache.get(&key).await.unwrap().is_none());
592        
593        // Test set and get
594        let results = vec![LspSearchResult {
595            file_path: "/test/file.rs".to_string(),
596            line_number: 10,
597            column: 5,
598            content: "test content".to_string(),
599            hint_type: HintType::Definition,
600            server_type: LspServerType::Rust,
601            confidence: 0.9,
602            context_lines: None,
603        }];
604        
605        cache.set(key.clone(), results.clone(), 3600).await.unwrap();
606        
607        let cached = cache.get(&key).await.unwrap().unwrap();
608        assert_eq!(cached.len(), 1);
609        assert_eq!(cached[0].file_path, results[0].file_path);
610        
611        // Test stats
612        let stats = cache.stats().await;
613        assert_eq!(stats.hits, 1);
614        assert_eq!(stats.misses, 1);
615        assert_eq!(stats.size, 1);
616    }
617
618    #[tokio::test] 
619    async fn test_cache_expiration() {
620        let cache = HintCache::new(1).await.unwrap();
621        let key = "expiry_test".to_string();
622        
623        let results = vec![LspSearchResult {
624            file_path: "/test/file.rs".to_string(),
625            line_number: 10,
626            column: 5,
627            content: "test content".to_string(),
628            hint_type: HintType::Definition,
629            server_type: LspServerType::Rust,
630            confidence: 0.9,
631            context_lines: None,
632        }];
633        
634        // Set with 1 second TTL
635        cache.set(key.clone(), results, 1).await.unwrap();
636        
637        // Should be available immediately
638        assert!(cache.get(&key).await.unwrap().is_some());
639        
640        // Wait for expiration
641        sleep(Duration::from_secs(2)).await;
642        
643        // Should be expired now
644        assert!(cache.get(&key).await.unwrap().is_none());
645    }
646
647    #[tokio::test]
648    async fn test_lru_eviction() {
649        let mut cache = HintCache::new(1).await.unwrap();
650        cache.max_size = 2; // Set small limit for testing
651        
652        let results = vec![LspSearchResult {
653            file_path: "/test/file.rs".to_string(),
654            line_number: 10,
655            column: 5,
656            content: "test content".to_string(),
657            hint_type: HintType::Definition,
658            server_type: LspServerType::Rust,
659            confidence: 0.9,
660            context_lines: None,
661        }];
662        
663        // Fill cache to capacity
664        cache.set("key1".to_string(), results.clone(), 3600).await.unwrap();
665        sleep(Duration::from_millis(100)).await; // Ensure different timestamps
666        cache.set("key2".to_string(), results.clone(), 3600).await.unwrap();
667        
668        // Access key1 to make key2 the LRU
669        sleep(Duration::from_millis(100)).await; // Ensure different timestamps
670        let _val = cache.get("key1").await.unwrap();
671        assert!(_val.is_some()); // key1 should exist and be accessed
672        
673        // Add another entry, should evict key2
674        sleep(Duration::from_millis(100)).await; // Ensure different timestamps
675        cache.set("key3".to_string(), results, 3600).await.unwrap();
676        
677        // key1 and key3 should exist, key2 should be evicted
678        assert!(cache.get("key1").await.unwrap().is_some());
679        assert!(cache.get("key2").await.unwrap().is_none());
680        assert!(cache.get("key3").await.unwrap().is_some());
681    }
682
683    #[tokio::test]
684    async fn test_hint_type_as_str() {
685        assert_eq!(HintType::Definition.as_str(), "definition");
686        assert_eq!(HintType::References.as_str(), "references");
687        assert_eq!(HintType::TypeDefinition.as_str(), "type_definition");
688        assert_eq!(HintType::Implementation.as_str(), "implementation");
689        assert_eq!(HintType::Declaration.as_str(), "declaration");
690        assert_eq!(HintType::Symbol.as_str(), "symbol");
691        assert_eq!(HintType::Hover.as_str(), "hover");
692        assert_eq!(HintType::Completion.as_str(), "completion");
693    }
694
695    #[tokio::test]
696    async fn test_hint_type_equality() {
697        assert_eq!(HintType::Definition, HintType::Definition);
698        assert_ne!(HintType::Definition, HintType::References);
699    }
700
701    #[tokio::test]
702    async fn test_cached_hint_creation() {
703        let results = vec![LspSearchResult {
704            file_path: "/test/file.rs".to_string(),
705            line_number: 10,
706            column: 5,
707            content: "test content".to_string(),
708            hint_type: HintType::Definition,
709            server_type: LspServerType::Rust,
710            confidence: 0.9,
711            context_lines: None,
712        }];
713        
714        let cache_key = "test_key".to_string();
715        let ttl_seconds = 3600;
716        
717        let cached_hint = CachedHint::new(results.clone(), ttl_seconds, cache_key.clone());
718        
719        assert_eq!(cached_hint.results.len(), 1);
720        assert_eq!(cached_hint.cache_key, cache_key);
721        assert_eq!(cached_hint.access_count, 0);
722        assert!(!cached_hint.is_expired());
723        assert!(cached_hint.age_seconds() < 5); // Should be very recent
724    }
725
726    #[tokio::test]
727    async fn test_cached_hint_expiration() {
728        let results = vec![];
729        let cache_key = "test_key".to_string();
730        
731        // Create hint that expires immediately
732        let mut cached_hint = CachedHint::new(results, 0, cache_key);
733        cached_hint.expires_at = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() - 1;
734        
735        assert!(cached_hint.is_expired());
736    }
737
738    #[tokio::test]
739    async fn test_cached_hint_touch() {
740        let results = vec![];
741        let cache_key = "test_key".to_string();
742        let mut cached_hint = CachedHint::new(results, 3600, cache_key);
743        
744        let initial_access_count = cached_hint.access_count;
745        let initial_last_accessed = cached_hint.last_accessed;
746        
747        // Small delay to ensure different timestamp
748        sleep(Duration::from_millis(10)).await;
749        cached_hint.touch();
750        
751        assert_eq!(cached_hint.access_count, initial_access_count + 1);
752        assert!(cached_hint.last_accessed >= initial_last_accessed);
753    }
754
755    #[tokio::test]
756    async fn test_cached_hint_age() {
757        let results = vec![];
758        let cache_key = "test_key".to_string();
759        let cached_hint = CachedHint::new(results, 3600, cache_key);
760        
761        let age = cached_hint.age_seconds();
762        assert!(age < 5); // Should be very young
763        
764        // Test with manually set created_at
765        let mut old_hint = cached_hint;
766        old_hint.created_at = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() - 100;
767        
768        let old_age = old_hint.age_seconds();
769        assert!(old_age >= 90); // Should be around 100 seconds old
770    }
771
772    #[tokio::test]
773    async fn test_cache_stats_hit_rate() {
774        let mut stats = CacheStats::default();
775        
776        // No requests yet
777        stats.update_hit_rate();
778        assert_eq!(stats.hit_rate, 0.0);
779        
780        // All misses
781        stats.misses = 5;
782        stats.update_hit_rate();
783        assert_eq!(stats.hit_rate, 0.0);
784        
785        // Mixed hits and misses
786        stats.hits = 3;
787        stats.misses = 7;
788        stats.update_hit_rate();
789        assert_eq!(stats.hit_rate, 0.3);
790        
791        // All hits
792        stats.hits = 10;
793        stats.misses = 0;
794        stats.update_hit_rate();
795        assert_eq!(stats.hit_rate, 1.0);
796    }
797
798    #[tokio::test]
799    async fn test_cache_stats_clone() {
800        let mut stats = CacheStats::default();
801        stats.hits = 5;
802        stats.misses = 3;
803        stats.size = 10;
804        
805        let cloned = stats.clone();
806        assert_eq!(cloned.hits, stats.hits);
807        assert_eq!(cloned.misses, stats.misses);
808        assert_eq!(cloned.size, stats.size);
809    }
810
811    #[tokio::test]
812    async fn test_file_tracker_creation() {
813        use tempfile::NamedTempFile;
814        use tokio::io::AsyncWriteExt;
815        
816        let temp_file = NamedTempFile::new().unwrap();
817        let temp_path = temp_file.path().to_path_buf();
818        
819        // Write some content
820        {
821            let mut file = tokio::fs::File::create(&temp_path).await.unwrap();
822            file.write_all(b"test content").await.unwrap();
823        }
824        
825        let tracker = FileTracker::new(temp_path.clone()).await.unwrap();
826        
827        assert_eq!(tracker.path, temp_path);
828        assert!(tracker.size > 0);
829        assert!(!tracker.has_changed().await);
830    }
831
832    #[tokio::test]
833    async fn test_file_tracker_change_detection() {
834        use tempfile::NamedTempFile;
835        use tokio::io::AsyncWriteExt;
836        
837        let temp_file = NamedTempFile::new().unwrap();
838        let temp_path = temp_file.path().to_path_buf();
839        
840        // Create initial file
841        {
842            let mut file = tokio::fs::File::create(&temp_path).await.unwrap();
843            file.write_all(b"initial content").await.unwrap();
844        }
845        
846        let tracker = FileTracker::new(temp_path.clone()).await.unwrap();
847        assert!(!tracker.has_changed().await);
848        
849        // Modify the file
850        sleep(Duration::from_millis(100)).await; // Ensure different timestamp
851        {
852            let mut file = tokio::fs::File::create(&temp_path).await.unwrap();
853            file.write_all(b"modified content").await.unwrap();
854        }
855        
856        assert!(tracker.has_changed().await);
857    }
858
859    #[tokio::test]
860    async fn test_file_tracker_missing_file() {
861        let non_existent_path = PathBuf::from("/definitely/does/not/exist.txt");
862        let tracker = FileTracker::new(non_existent_path.clone()).await;
863        
864        // Should fail to create tracker for non-existent file
865        assert!(tracker.is_err());
866        
867        // For existing tracker, missing file should be detected as changed
868        use tempfile::NamedTempFile;
869        let temp_file = NamedTempFile::new().unwrap();
870        let temp_path = temp_file.path().to_path_buf();
871        
872        let tracker = FileTracker::new(temp_path.clone()).await.unwrap();
873        
874        // Remove the file
875        std::fs::remove_file(&temp_path).unwrap();
876        
877        // Should detect as changed
878        assert!(tracker.has_changed().await);
879    }
880
881    #[tokio::test]
882    async fn test_cache_clear() {
883        let cache = HintCache::new(1).await.unwrap();
884        let results = vec![LspSearchResult {
885            file_path: "/test/file.rs".to_string(),
886            line_number: 10,
887            column: 5,
888            content: "test content".to_string(),
889            hint_type: HintType::Definition,
890            server_type: LspServerType::Rust,
891            confidence: 0.9,
892            context_lines: None,
893        }];
894        
895        // Add some entries
896        cache.set("key1".to_string(), results.clone(), 3600).await.unwrap();
897        cache.set("key2".to_string(), results, 3600).await.unwrap();
898        
899        let stats = cache.stats().await;
900        assert_eq!(stats.size, 2);
901        
902        // Clear cache
903        cache.clear().await.unwrap();
904        
905        let stats_after = cache.stats().await;
906        assert_eq!(stats_after.size, 0);
907        assert_eq!(stats_after.memory_usage_bytes, 0);
908        
909        // Entries should be gone
910        assert!(cache.get("key1").await.unwrap().is_none());
911        assert!(cache.get("key2").await.unwrap().is_none());
912    }
913
914    #[tokio::test]
915    async fn test_cache_invalidate_file() {
916        let cache = HintCache::new(1).await.unwrap();
917        
918        let results_file1 = vec![LspSearchResult {
919            file_path: "/test/file1.rs".to_string(),
920            line_number: 10,
921            column: 5,
922            content: "test content 1".to_string(),
923            hint_type: HintType::Definition,
924            server_type: LspServerType::Rust,
925            confidence: 0.9,
926            context_lines: None,
927        }];
928        
929        let results_file2 = vec![LspSearchResult {
930            file_path: "/test/file2.rs".to_string(),
931            line_number: 20,
932            column: 10,
933            content: "test content 2".to_string(),
934            hint_type: HintType::References,
935            server_type: LspServerType::Rust,
936            confidence: 0.8,
937            context_lines: None,
938        }];
939        
940        // Add entries for different files
941        cache.set("key1".to_string(), results_file1, 3600).await.unwrap();
942        cache.set("key2".to_string(), results_file2, 3600).await.unwrap();
943        
944        // Invalidate file1
945        let file1_path = PathBuf::from("/test/file1.rs");
946        let invalidated = cache.invalidate_file(&file1_path).await.unwrap();
947        
948        assert_eq!(invalidated, 1);
949        
950        // key1 should be gone, key2 should remain
951        assert!(cache.get("key1").await.unwrap().is_none());
952        assert!(cache.get("key2").await.unwrap().is_some());
953        
954        let stats = cache.stats().await;
955        assert_eq!(stats.invalidations, 1);
956        assert_eq!(stats.size, 1);
957    }
958
959    #[tokio::test]
960    async fn test_cache_memory_usage_estimation() {
961        let cache = HintCache::new(1).await.unwrap();
962        
963        let initial_usage = cache.estimate_memory_usage();
964        assert_eq!(initial_usage, 0);
965        
966        let results = vec![LspSearchResult {
967            file_path: "/test/file.rs".to_string(),
968            line_number: 10,
969            column: 5,
970            content: "test content".to_string(),
971            hint_type: HintType::Definition,
972            server_type: LspServerType::Rust,
973            confidence: 0.9,
974            context_lines: None,
975        }];
976        
977        cache.set("key1".to_string(), results, 3600).await.unwrap();
978        
979        let usage_after = cache.estimate_memory_usage();
980        assert_eq!(usage_after, 1024); // 1 entry * 1KB estimate
981    }
982
983    #[tokio::test]
984    async fn test_cache_concurrent_access() {
985        let cache = Arc::new(HintCache::new(1).await.unwrap());
986        
987        let results = vec![LspSearchResult {
988            file_path: "/test/file.rs".to_string(),
989            line_number: 10,
990            column: 5,
991            content: "test content".to_string(),
992            hint_type: HintType::Definition,
993            server_type: LspServerType::Rust,
994            confidence: 0.9,
995            context_lines: None,
996        }];
997        
998        // Concurrent writes
999        let mut handles = vec![];
1000        for i in 0..10 {
1001            let cache = cache.clone();
1002            let results = results.clone();
1003            let handle = tokio::spawn(async move {
1004                cache.set(format!("key{}", i), results, 3600).await
1005            });
1006            handles.push(handle);
1007        }
1008        
1009        for handle in handles {
1010            handle.await.unwrap().unwrap();
1011        }
1012        
1013        let stats = cache.stats().await;
1014        assert_eq!(stats.size, 10);
1015        
1016        // Concurrent reads
1017        let mut handles = vec![];
1018        for i in 0..10 {
1019            let cache = cache.clone();
1020            let handle = tokio::spawn(async move {
1021                cache.get(&format!("key{}", i)).await
1022            });
1023            handles.push(handle);
1024        }
1025        
1026        for handle in handles {
1027            let result = handle.await.unwrap().unwrap();
1028            assert!(result.is_some());
1029        }
1030        
1031        let final_stats = cache.stats().await;
1032        assert_eq!(final_stats.hits, 10);
1033    }
1034
1035    #[tokio::test]
1036    async fn test_symbol_hint_creation() {
1037        let symbol_hint = SymbolHint::new(
1038            "test_function".to_string(),
1039            "function".to_string(),
1040            "/test/file.rs".to_string(),
1041            42,
1042            10,
1043            0.95,
1044        );
1045        
1046        assert_eq!(symbol_hint.symbol_name, "test_function");
1047        assert_eq!(symbol_hint.symbol_kind, "function");
1048        assert_eq!(symbol_hint.file_path, "/test/file.rs");
1049        assert_eq!(symbol_hint.line_number, 42);
1050        assert_eq!(symbol_hint.column, 10);
1051        assert_eq!(symbol_hint.confidence, 0.95);
1052        assert!(symbol_hint.documentation.is_none());
1053        assert!(symbol_hint.signature.is_none());
1054    }
1055
1056    #[tokio::test]
1057    async fn test_symbol_hint_with_optional_fields() {
1058        let mut symbol_hint = SymbolHint::new(
1059            "test_function".to_string(),
1060            "function".to_string(),
1061            "/test/file.rs".to_string(),
1062            42,
1063            10,
1064            0.95,
1065        );
1066        
1067        symbol_hint.documentation = Some("Test function documentation".to_string());
1068        symbol_hint.signature = Some("fn test_function() -> bool".to_string());
1069        
1070        assert!(symbol_hint.documentation.is_some());
1071        assert!(symbol_hint.signature.is_some());
1072        assert_eq!(symbol_hint.documentation.unwrap(), "Test function documentation");
1073    }
1074
1075    #[tokio::test]
1076    async fn test_cache_shutdown() {
1077        let cache = HintCache::new(1).await.unwrap();
1078        
1079        // Add some data
1080        let results = vec![LspSearchResult {
1081            file_path: "/test/file.rs".to_string(),
1082            line_number: 10,
1083            column: 5,
1084            content: "test content".to_string(),
1085            hint_type: HintType::Definition,
1086            server_type: LspServerType::Rust,
1087            confidence: 0.9,
1088            context_lines: None,
1089        }];
1090        
1091        cache.set("key1".to_string(), results, 3600).await.unwrap();
1092        
1093        let stats_before = cache.stats().await;
1094        assert_eq!(stats_before.size, 1);
1095        
1096        // Shutdown should clear cache
1097        cache.shutdown().await.unwrap();
1098        
1099        // Cache should be empty after shutdown
1100        assert_eq!(cache.cache.len(), 0);
1101    }
1102
1103    #[tokio::test]
1104    async fn test_cache_drop() {
1105        let cache = HintCache::new(1).await.unwrap();
1106        
1107        // Test that drop doesn't panic
1108        drop(cache);
1109    }
1110
1111    #[tokio::test]
1112    async fn test_cache_evict_lru_empty_cache() {
1113        let cache = HintCache::new(1).await.unwrap();
1114        
1115        // Should not panic with empty cache
1116        cache.evict_lru().await;
1117        
1118        let stats = cache.stats().await;
1119        assert_eq!(stats.size, 0);
1120        assert_eq!(stats.evictions, 0);
1121    }
1122
1123    #[tokio::test]
1124    async fn test_large_cache_performance() {
1125        let cache = HintCache::new(1).await.unwrap();
1126        let start = Instant::now();
1127        
1128        let results = vec![LspSearchResult {
1129            file_path: "/test/file.rs".to_string(),
1130            line_number: 10,
1131            column: 5,
1132            content: "test content".to_string(),
1133            hint_type: HintType::Definition,
1134            server_type: LspServerType::Rust,
1135            confidence: 0.9,
1136            context_lines: None,
1137        }];
1138        
1139        // Add many entries
1140        for i in 0..1000 {
1141            cache.set(format!("key{}", i), results.clone(), 3600).await.unwrap();
1142        }
1143        
1144        let insert_time = start.elapsed();
1145        assert!(insert_time < Duration::from_secs(5)); // Should be reasonably fast
1146        
1147        // Test retrieval performance
1148        let start = Instant::now();
1149        for i in 0..1000 {
1150            let _result = cache.get(&format!("key{}", i)).await.unwrap();
1151        }
1152        
1153        let retrieval_time = start.elapsed();
1154        assert!(retrieval_time < Duration::from_secs(2)); // Should be fast
1155        
1156        let stats = cache.stats().await;
1157        assert_eq!(stats.hits, 1000);
1158        assert_eq!(stats.size, 1000);
1159    }
1160
1161    #[tokio::test]
1162    async fn test_cache_key_with_special_characters() {
1163        let cache = HintCache::new(1).await.unwrap();
1164        
1165        let results = vec![LspSearchResult {
1166            file_path: "/test/file.rs".to_string(),
1167            line_number: 10,
1168            column: 5,
1169            content: "test content".to_string(),
1170            hint_type: HintType::Definition,
1171            server_type: LspServerType::Rust,
1172            confidence: 0.9,
1173            context_lines: None,
1174        }];
1175        
1176        let special_keys = vec![
1177            "key with spaces".to_string(),
1178            "key:with:colons".to_string(),
1179            "key/with/slashes".to_string(),
1180            "key-with-dashes".to_string(),
1181            "key_with_underscores".to_string(),
1182            "key.with.dots".to_string(),
1183            "key@with@symbols".to_string(),
1184        ];
1185        
1186        for key in special_keys {
1187            cache.set(key.clone(), results.clone(), 3600).await.unwrap();
1188            let retrieved = cache.get(&key).await.unwrap();
1189            assert!(retrieved.is_some());
1190        }
1191    }
1192
1193    #[tokio::test]
1194    async fn test_empty_results_caching() {
1195        let cache = HintCache::new(1).await.unwrap();
1196        let key = "empty_results".to_string();
1197        
1198        // Cache empty results
1199        let empty_results: Vec<LspSearchResult> = vec![];
1200        cache.set(key.clone(), empty_results, 3600).await.unwrap();
1201        
1202        let retrieved = cache.get(&key).await.unwrap().unwrap();
1203        assert!(retrieved.is_empty());
1204        
1205        let stats = cache.stats().await;
1206        assert_eq!(stats.hits, 1);
1207        assert_eq!(stats.size, 1);
1208    }
1209}