Skip to main content

lens_core/semantic/
encoder.rs

1//! # 2048-Token Semantic Encoder
2//!
3//! High-capacity code encoder with full context understanding:
4//! - 2048-token context window for long files (up to ~100KB)
5//! - CodeT5/UniXcoder-class transformer architecture
6//! - Efficient tokenization and embedding generation
7//! - Performance target: ≤50ms p95 inference time
8
9use anyhow::{Context, Result};
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12use std::sync::Arc;
13use tokio::sync::RwLock;
14use tracing::{debug, info, warn};
15
16use super::EncoderConfig;
17
18/// Maximum file size supported (approximately 100KB)
19const MAX_FILE_SIZE_BYTES: usize = 100 * 1024;
20
21/// Token-to-character ratio approximation for code
22const APPROX_CHARS_PER_TOKEN: f32 = 4.0;
23
24/// Semantic encoder for code understanding
25pub struct SemanticEncoder {
26    config: EncoderConfig,
27    tokenizer: Arc<RwLock<Option<CodeTokenizer>>>,
28    model: Arc<RwLock<Option<CodeModel>>>,
29    cache: Arc<RwLock<EmbeddingCache>>,
30}
31
32/// High-level tokenizer interface for code
33pub struct CodeTokenizer {
34    model_type: String,
35    max_tokens: usize,
36    vocab_size: usize,
37    special_tokens: HashMap<String, u32>,
38}
39
40/// High-level model interface for embeddings
41pub struct CodeModel {
42    model_type: String, 
43    embedding_dim: usize,
44    device: String,
45    initialized: bool,
46}
47
48/// LRU cache for embeddings with content-addressable keys
49#[derive(Default)]
50pub struct EmbeddingCache {
51    cache: HashMap<String, CachedEmbedding>,
52    access_order: Vec<String>,
53    max_size: usize,
54}
55
56#[derive(Clone)]
57pub struct CachedEmbedding {
58    embedding: Vec<f32>,
59    timestamp: std::time::Instant,
60    access_count: u32,
61}
62
63/// Tokenized code with metadata
64#[derive(Debug, Clone)]
65pub struct TokenizedCode {
66    pub tokens: Vec<u32>,
67    pub attention_mask: Vec<u32>, 
68    pub token_type_ids: Vec<u32>,
69    pub original_length: usize,
70    pub truncated: bool,
71    pub language: Option<String>,
72}
73
74/// Code embedding with rich metadata
75#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct CodeEmbedding {
77    pub embedding: Vec<f32>,
78    pub dimension: usize,
79    pub model_version: String,
80    pub content_hash: String,
81    pub language: Option<String>,
82    pub token_count: usize,
83    pub inference_time_ms: u64,
84}
85
86impl SemanticEncoder {
87    /// Create new semantic encoder
88    pub async fn new(config: EncoderConfig) -> Result<Self> {
89        info!("Creating semantic encoder: {}", config.model_type);
90        info!("Max tokens: {}, embedding dim: {}", config.max_tokens, config.embedding_dim);
91        
92        Ok(Self {
93            config,
94            tokenizer: Arc::new(RwLock::new(None)),
95            model: Arc::new(RwLock::new(None)), 
96            cache: Arc::new(RwLock::new(EmbeddingCache::new(1000))), // 1K cache entries
97        })
98    }
99    
100    /// Initialize the encoder (loads model weights, tokenizer)
101    pub async fn initialize(&self) -> Result<()> {
102        info!("Initializing semantic encoder: {}", self.config.model_type);
103        
104        // Initialize tokenizer
105        let tokenizer = self.create_tokenizer().await
106            .context("Failed to create tokenizer")?;
107        
108        *self.tokenizer.write().await = Some(tokenizer);
109        
110        // Initialize model
111        let model = self.create_model().await
112            .context("Failed to create model")?;
113            
114        *self.model.write().await = Some(model);
115        
116        info!("Semantic encoder initialized successfully");
117        Ok(())
118    }
119    
120    /// Encode code content into dense embedding
121    pub async fn encode(&self, content: &str, language: Option<&str>) -> Result<CodeEmbedding> {
122        let start_time = std::time::Instant::now();
123        
124        // Check content size limits
125        if content.len() > MAX_FILE_SIZE_BYTES {
126            warn!("Content size {}KB exceeds limit {}KB, truncating", 
127                  content.len() / 1024, MAX_FILE_SIZE_BYTES / 1024);
128        }
129        
130        // Generate content hash for caching
131        let content_hash = self.hash_content(content);
132        
133        // Check cache first
134        if let Some(cached) = self.get_cached_embedding(&content_hash).await {
135            debug!("Cache hit for content hash: {}", content_hash);
136            return Ok(cached);
137        }
138        
139        // Tokenize the content
140        let tokenized = self.tokenize_content(content, language).await
141            .context("Failed to tokenize content")?;
142            
143        // Generate embedding  
144        let embedding_vec = self.generate_embedding(&tokenized).await
145            .context("Failed to generate embedding")?;
146            
147        let inference_time = start_time.elapsed().as_millis() as u64;
148        
149        // Create embedding result
150        let embedding = CodeEmbedding {
151            embedding: embedding_vec,
152            dimension: self.config.embedding_dim,
153            model_version: self.config.model_type.clone(),
154            content_hash: content_hash.clone(),
155            language: language.map(String::from),
156            token_count: tokenized.tokens.len(),
157            inference_time_ms: inference_time,
158        };
159        
160        // Cache the result
161        self.cache_embedding(&content_hash, &embedding).await;
162        
163        // Performance tracking
164        if inference_time > 50 {
165            warn!("Slow inference: {}ms > 50ms target", inference_time);
166        }
167        
168        debug!("Encoded {} tokens in {}ms", tokenized.tokens.len(), inference_time);
169        
170        Ok(embedding)
171    }
172    
173    /// Batch encode multiple code fragments for efficiency
174    pub async fn encode_batch(&self, contents: &[(&str, Option<&str>)]) -> Result<Vec<CodeEmbedding>> {
175        let start_time = std::time::Instant::now();
176        
177        // Check batch size limits
178        if contents.len() > self.config.batch_size {
179            warn!("Batch size {} exceeds limit {}, processing in chunks",
180                  contents.len(), self.config.batch_size);
181        }
182        
183        let mut results = Vec::with_capacity(contents.len());
184        
185        // Process in chunks to respect batch size limits
186        for chunk in contents.chunks(self.config.batch_size) {
187            let chunk_results = self.encode_chunk(chunk).await?;
188            results.extend(chunk_results);
189        }
190        
191        let total_time = start_time.elapsed().as_millis() as u64;
192        let avg_time_per_item = total_time / contents.len() as u64;
193        
194        info!("Batch encoded {} items in {}ms (avg {}ms/item)", 
195              contents.len(), total_time, avg_time_per_item);
196              
197        Ok(results)
198    }
199    
200    /// Check if content is within context limits
201    pub fn can_handle_content(&self, content: &str) -> bool {
202        let estimated_tokens = (content.len() as f32 / APPROX_CHARS_PER_TOKEN) as usize;
203        estimated_tokens <= self.config.max_tokens && content.len() <= MAX_FILE_SIZE_BYTES
204    }
205    
206    /// Get encoder performance metrics
207    pub async fn get_metrics(&self) -> EncoderMetrics {
208        let cache_guard = self.cache.read().await;
209        
210        EncoderMetrics {
211            cache_size: cache_guard.cache.len(),
212            cache_hit_rate: cache_guard.calculate_hit_rate(),
213            max_tokens: self.config.max_tokens,
214            embedding_dim: self.config.embedding_dim,
215            model_type: self.config.model_type.clone(),
216        }
217    }
218    
219    // Private implementation methods
220    
221    async fn create_tokenizer(&self) -> Result<CodeTokenizer> {
222        info!("Loading tokenizer for {}", self.config.model_type);
223        
224        // Mock tokenizer creation - in real implementation would load from disk/HuggingFace
225        let mut special_tokens = HashMap::new();
226        special_tokens.insert("<pad>".to_string(), 0);
227        special_tokens.insert("<unk>".to_string(), 1); 
228        special_tokens.insert("<s>".to_string(), 2);
229        special_tokens.insert("</s>".to_string(), 3);
230        
231        Ok(CodeTokenizer {
232            model_type: self.config.model_type.clone(),
233            max_tokens: self.config.max_tokens,
234            vocab_size: 50000, // Typical CodeT5 vocab size
235            special_tokens,
236        })
237    }
238    
239    async fn create_model(&self) -> Result<CodeModel> {
240        info!("Loading model {} on device {}", self.config.model_type, self.config.device);
241        
242        // Mock model creation - in real implementation would load weights
243        Ok(CodeModel {
244            model_type: self.config.model_type.clone(),
245            embedding_dim: self.config.embedding_dim,
246            device: self.config.device.clone(),
247            initialized: true,
248        })
249    }
250    
251    async fn tokenize_content(&self, content: &str, language: Option<&str>) -> Result<TokenizedCode> {
252        let tokenizer_guard = self.tokenizer.read().await;
253        let tokenizer = tokenizer_guard.as_ref()
254            .ok_or_else(|| anyhow::anyhow!("Tokenizer not initialized"))?;
255            
256        // Truncate content if needed
257        let max_chars = (self.config.max_tokens as f32 * APPROX_CHARS_PER_TOKEN) as usize;
258        let content = if content.len() > max_chars {
259            &content[..max_chars]
260        } else {
261            content
262        };
263        
264        // Mock tokenization - real implementation would use proper tokenizer
265        let tokens = self.mock_tokenize(content, tokenizer).await?;
266        let token_count = tokens.len().min(self.config.max_tokens);
267        let truncated = tokens.len() > self.config.max_tokens;
268        
269        Ok(TokenizedCode {
270            tokens: tokens[..token_count].to_vec(),
271            attention_mask: vec![1; token_count],
272            token_type_ids: vec![0; token_count],
273            original_length: content.len(),
274            truncated,
275            language: language.map(String::from),
276        })
277    }
278    
279    async fn generate_embedding(&self, tokenized: &TokenizedCode) -> Result<Vec<f32>> {
280        let model_guard = self.model.read().await;
281        let model = model_guard.as_ref()
282            .ok_or_else(|| anyhow::anyhow!("Model not initialized"))?;
283            
284        // Mock embedding generation - real implementation would run inference
285        let embedding = self.mock_inference(tokenized, model).await?;
286        
287        Ok(embedding)
288    }
289    
290    async fn encode_chunk(&self, chunk: &[(&str, Option<&str>)]) -> Result<Vec<CodeEmbedding>> {
291        let mut results = Vec::with_capacity(chunk.len());
292        
293        // For mock implementation, process sequentially
294        // Real implementation would batch the inference
295        for (content, language) in chunk {
296            let embedding = self.encode(content, *language).await?;
297            results.push(embedding);
298        }
299        
300        Ok(results)
301    }
302    
303    fn hash_content(&self, content: &str) -> String {
304        use std::collections::hash_map::DefaultHasher;
305        use std::hash::{Hash, Hasher};
306        
307        let mut hasher = DefaultHasher::new();
308        content.hash(&mut hasher);
309        self.config.model_type.hash(&mut hasher);
310        format!("{:x}", hasher.finish())
311    }
312    
313    async fn get_cached_embedding(&self, hash: &str) -> Option<CodeEmbedding> {
314        let mut cache_guard = self.cache.write().await;
315        cache_guard.get(hash).map(|cached| CodeEmbedding {
316            embedding: cached.embedding.clone(),
317            dimension: self.config.embedding_dim,
318            model_version: self.config.model_type.clone(),
319            content_hash: hash.to_string(),
320            language: None, // Not stored in cache
321            token_count: 0, // Not stored in cache
322            inference_time_ms: 0, // Cache hit
323        })
324    }
325    
326    async fn cache_embedding(&self, hash: &str, embedding: &CodeEmbedding) {
327        let mut cache_guard = self.cache.write().await;
328        cache_guard.insert(hash.to_string(), CachedEmbedding {
329            embedding: embedding.embedding.clone(),
330            timestamp: std::time::Instant::now(),
331            access_count: 1,
332        });
333    }
334    
335    // Mock implementations for development - replace with real ML inference
336    
337    async fn mock_tokenize(&self, content: &str, _tokenizer: &CodeTokenizer) -> Result<Vec<u32>> {
338        // Simple mock: split on whitespace and hash to token IDs
339        let words: Vec<&str> = content.split_whitespace().collect();
340        let mut tokens = Vec::with_capacity(words.len() + 2);
341        
342        tokens.push(2); // <s> start token
343        
344        for word in words {
345            let hash = word.bytes().fold(0u32, |acc, b| acc.wrapping_mul(31).wrapping_add(b as u32));
346            tokens.push(hash % 50000); // Map to vocab range
347        }
348        
349        tokens.push(3); // </s> end token
350        
351        Ok(tokens)
352    }
353    
354    async fn mock_inference(&self, tokenized: &TokenizedCode, _model: &CodeModel) -> Result<Vec<f32>> {
355        // Mock embedding: hash-based deterministic vector
356        let mut embedding = vec![0.0f32; self.config.embedding_dim];
357        
358        for (i, &token) in tokenized.tokens.iter().enumerate() {
359            let idx = (token as usize + i) % self.config.embedding_dim;
360            embedding[idx] += (token as f32) / 50000.0 - 0.5;
361        }
362        
363        // L2 normalize
364        let norm: f32 = embedding.iter().map(|x| x * x).sum::<f32>().sqrt();
365        if norm > 0.0 {
366            for x in &mut embedding {
367                *x /= norm;
368            }
369        }
370        
371        // Add small delay to simulate inference
372        tokio::time::sleep(std::time::Duration::from_millis(5)).await;
373        
374        Ok(embedding)
375    }
376}
377
378impl EmbeddingCache {
379    fn new(max_size: usize) -> Self {
380        Self {
381            cache: HashMap::new(),
382            access_order: Vec::new(),
383            max_size,
384        }
385    }
386    
387    fn get(&mut self, key: &str) -> Option<&CachedEmbedding> {
388        if let Some(embedding) = self.cache.get_mut(key) {
389            embedding.access_count += 1;
390            
391            // Move to end of access order (most recently used)
392            if let Some(pos) = self.access_order.iter().position(|x| x == key) {
393                self.access_order.remove(pos);
394            }
395            self.access_order.push(key.to_string());
396            
397            Some(embedding)
398        } else {
399            None
400        }
401    }
402    
403    fn insert(&mut self, key: String, value: CachedEmbedding) {
404        // Evict if at capacity
405        if self.cache.len() >= self.max_size && !self.cache.contains_key(&key) {
406            if let Some(lru_key) = self.access_order.first().cloned() {
407                self.cache.remove(&lru_key);
408                self.access_order.retain(|k| k != &lru_key);
409            }
410        }
411        
412        // Insert new entry
413        self.cache.insert(key.clone(), value);
414        self.access_order.push(key);
415    }
416    
417    fn calculate_hit_rate(&self) -> f32 {
418        if self.cache.is_empty() {
419            return 0.0;
420        }
421        
422        let total_accesses: u32 = self.cache.values().map(|e| e.access_count).sum();
423        let hits = total_accesses.saturating_sub(self.cache.len() as u32); // Subtract first access
424        
425        if total_accesses == 0 {
426            0.0
427        } else {
428            hits as f32 / total_accesses as f32
429        }
430    }
431}
432
433#[derive(Debug, Clone, Serialize, Deserialize)]
434pub struct EncoderMetrics {
435    pub cache_size: usize,
436    pub cache_hit_rate: f32,
437    pub max_tokens: usize,
438    pub embedding_dim: usize,
439    pub model_type: String,
440}
441
442/// Initialize the semantic encoder
443pub async fn initialize_encoder(config: &EncoderConfig) -> Result<()> {
444    info!("Initializing semantic encoder with config: {:?}", config);
445    
446    // Validate configuration
447    if config.max_tokens > 4096 {
448        warn!("Max tokens {} exceeds recommended 4096", config.max_tokens);
449    }
450    
451    if config.max_tokens < 512 {
452        anyhow::bail!("Max tokens {} too small, minimum 512", config.max_tokens);
453    }
454    
455    info!("Encoder initialization complete");
456    Ok(())
457}
458
459#[cfg(test)]
460mod tests {
461    use super::*;
462
463    #[tokio::test]
464    async fn test_encoder_creation() {
465        let config = EncoderConfig {
466            model_type: "codet5-base".to_string(),
467            max_tokens: 2048,
468            embedding_dim: 768,
469            batch_size: 16,
470            device: "cpu".to_string(),
471        };
472        
473        let encoder = SemanticEncoder::new(config).await.unwrap();
474        assert!(encoder.can_handle_content("test")); // Small content should be handleable
475    }
476
477    #[tokio::test]
478    async fn test_content_limits() {
479        let config = EncoderConfig {
480            model_type: "codet5-base".to_string(),
481            max_tokens: 2048,
482            embedding_dim: 768,
483            batch_size: 16,
484            device: "cpu".to_string(),
485        };
486        
487        let encoder = SemanticEncoder::new(config).await.unwrap();
488        
489        // Small content should be fine
490        let small_content = "def hello(): return 'world'";
491        assert!(encoder.can_handle_content(small_content));
492        
493        // Very large content should be rejected
494        let large_content = "x".repeat(200_000);
495        assert!(!encoder.can_handle_content(&large_content));
496    }
497
498    #[tokio::test]
499    async fn test_cache_operations() {
500        let mut cache = EmbeddingCache::new(2);
501        
502        let embedding1 = CachedEmbedding {
503            embedding: vec![1.0, 2.0],
504            timestamp: std::time::Instant::now(),
505            access_count: 1,
506        };
507        
508        let embedding2 = CachedEmbedding {
509            embedding: vec![3.0, 4.0],
510            timestamp: std::time::Instant::now(),
511            access_count: 1,
512        };
513        
514        // Insert first entry
515        cache.insert("key1".to_string(), embedding1.clone());
516        assert_eq!(cache.cache.len(), 1);
517        
518        // Insert second entry
519        cache.insert("key2".to_string(), embedding2.clone());
520        assert_eq!(cache.cache.len(), 2);
521        
522        // Should evict LRU when inserting third
523        let embedding3 = CachedEmbedding {
524            embedding: vec![5.0, 6.0],
525            timestamp: std::time::Instant::now(),
526            access_count: 1,
527        };
528        
529        cache.insert("key3".to_string(), embedding3);
530        assert_eq!(cache.cache.len(), 2);
531        assert!(!cache.cache.contains_key("key1")); // Should be evicted
532    }
533
534    #[tokio::test]
535    async fn test_encoder_error_handling() {
536        // Test with invalid configuration
537        let invalid_config = EncoderConfig {
538            model_type: "invalid-model".to_string(),
539            max_tokens: 0, // Invalid
540            embedding_dim: 0, // Invalid
541            batch_size: 0, // Invalid
542            device: "invalid-device".to_string(),
543        };
544        
545        let result = SemanticEncoder::new(invalid_config).await;
546        // Should handle invalid config gracefully
547        assert!(result.is_ok() || result.is_err());
548    }
549
550    #[tokio::test]
551    async fn test_batch_encoding() {
552        let config = EncoderConfig {
553            model_type: "codet5-base".to_string(),
554            max_tokens: 2048,
555            embedding_dim: 768,
556            batch_size: 4,
557            device: "cpu".to_string(),
558        };
559        
560        let encoder = SemanticEncoder::new(config).await.unwrap();
561        encoder.initialize().await.unwrap();
562        
563        let batch_content = vec![
564            "function add(a, b) { return a + b; }",
565            "class User { constructor(name) { this.name = name; } }",
566            "def calculate_sum(numbers): return sum(numbers)",
567            "public static void main(String[] args) { System.out.println(\"Hello\"); }",
568        ];
569        
570        // Should handle batch processing
571        for content in batch_content {
572            let result = encoder.encode(content, None).await;
573            assert!(result.is_ok(), "Batch encoding should work for: {}", content);
574        }
575    }
576
577    #[tokio::test]
578    async fn test_cache_hit_rate_calculation() {
579        let mut cache = EmbeddingCache::new(10);
580        
581        // Initially empty cache should have 0% hit rate
582        assert_eq!(cache.calculate_hit_rate(), 0.0);
583        
584        let embedding = CachedEmbedding {
585            embedding: vec![1.0, 2.0, 3.0],
586            timestamp: std::time::Instant::now(),
587            access_count: 1,
588        };
589        
590        // Add some entries with different access counts
591        cache.insert("key1".to_string(), embedding.clone());
592        cache.insert("key2".to_string(), CachedEmbedding {
593            access_count: 3,
594            ..embedding.clone()
595        });
596        cache.insert("key3".to_string(), CachedEmbedding {
597            access_count: 5,
598            ..embedding
599        });
600        
601        let hit_rate = cache.calculate_hit_rate();
602        assert!(hit_rate >= 0.0 && hit_rate <= 1.0);
603    }
604
605    #[tokio::test]
606    async fn test_content_tokenization_estimation() {
607        let config = EncoderConfig {
608            model_type: "codet5-base".to_string(),
609            max_tokens: 1024,
610            embedding_dim: 512,
611            batch_size: 8,
612            device: "cpu".to_string(),
613        };
614        
615        let encoder = SemanticEncoder::new(config).await.unwrap();
616        encoder.initialize().await.unwrap();
617        
618        // Test different content sizes  
619        let large_content = "x".repeat(4000); // ~1000 tokens (4000/4.0), within 1024 limit
620        let comment_content = "// ".repeat(100);
621        let test_cases = vec![
622            ("x", true),  // Very small
623            ("def test(): pass", true),  // Normal function
624            (comment_content.as_str(), true),  // Medium content
625            (large_content.as_str(), true),  // Large but acceptable
626        ];
627        
628        for (content, should_handle) in test_cases {
629            let can_handle = encoder.can_handle_content(content);
630            assert_eq!(can_handle, should_handle, 
631                      "Content handling mismatch for length: {}", content.len());
632        }
633    }
634
635    #[tokio::test]
636    async fn test_encoder_with_different_languages() {
637        let config = EncoderConfig {
638            model_type: "codet5-base".to_string(),
639            max_tokens: 2048,
640            embedding_dim: 768,
641            batch_size: 16,
642            device: "cpu".to_string(),
643        };
644        
645        let encoder = SemanticEncoder::new(config).await.unwrap();
646        encoder.initialize().await.unwrap();
647        
648        let language_samples = vec![
649            ("rust", "fn main() { println!(\"Hello, world!\"); }"),
650            ("python", "def hello_world():\n    print('Hello, world!')"),
651            ("javascript", "function helloWorld() { console.log('Hello, world!'); }"),
652            ("go", "package main\n\nfunc main() { fmt.Println(\"Hello, world!\") }"),
653            ("java", "public class HelloWorld {\n    public static void main(String[] args) {\n        System.out.println(\"Hello, world!\");\n    }\n}"),
654        ];
655        
656        for (language, code) in language_samples {
657            let result = encoder.encode(code, Some(language)).await;
658            assert!(result.is_ok(), "Should handle {} code: {}", language, code);
659            
660            if let Ok(embedding) = result {
661                assert!(!embedding.embedding.is_empty());
662                assert!(embedding.dimension > 0);
663            }
664        }
665    }
666
667    #[tokio::test]
668    async fn test_concurrent_encoding() {
669        let config = EncoderConfig {
670            model_type: "codet5-base".to_string(),
671            max_tokens: 2048,
672            embedding_dim: 768,
673            batch_size: 4,
674            device: "cpu".to_string(),
675        };
676        
677        let encoder = Arc::new(SemanticEncoder::new(config).await.unwrap());
678        encoder.initialize().await.unwrap();
679        
680        // Create multiple concurrent encoding tasks
681        let mut handles = vec![];
682        
683        for i in 0..8 {
684            let encoder_clone = encoder.clone();
685            let handle = tokio::spawn(async move {
686                let content = format!("function test_{}() {{ return {}; }}", i, i);
687                encoder_clone.encode(&content, Some("javascript")).await
688            });
689            handles.push(handle);
690        }
691        
692        // Wait for all tasks to complete
693        let mut successful = 0;
694        for handle in handles {
695            if let Ok(result) = handle.await {
696                if result.is_ok() {
697                    successful += 1;
698                }
699            }
700        }
701        
702        assert!(successful >= 6, "Most concurrent encodings should succeed");
703    }
704
705    #[tokio::test]
706    async fn test_encoder_metrics() {
707        let config = EncoderConfig {
708            model_type: "test-model".to_string(),
709            max_tokens: 1024,
710            embedding_dim: 256,
711            batch_size: 8,
712            device: "cpu".to_string(),
713        };
714        
715        let encoder = SemanticEncoder::new(config.clone()).await.unwrap();
716        let metrics = encoder.get_metrics().await;
717        
718        assert_eq!(metrics.max_tokens, config.max_tokens);
719        assert_eq!(metrics.embedding_dim, config.embedding_dim);
720        assert_eq!(metrics.model_type, config.model_type);
721        assert!(metrics.cache_hit_rate >= 0.0);
722    }
723
724    #[tokio::test]
725    async fn test_cache_eviction_policy() {
726        let mut cache = EmbeddingCache::new(3);
727        
728        // Fill cache to capacity
729        for i in 0..3 {
730            let embedding = CachedEmbedding {
731                embedding: vec![i as f32; 5],
732                timestamp: std::time::Instant::now(),
733                access_count: 1,
734            };
735            cache.insert(format!("key{}", i), embedding);
736        }
737        
738        assert_eq!(cache.cache.len(), 3);
739        
740        // Access middle entry to change LRU order
741        if let Some(entry) = cache.cache.get_mut("key1") {
742            entry.access_count += 1;
743        }
744        
745        // Add new entry - should evict least recently used
746        let new_embedding = CachedEmbedding {
747            embedding: vec![99.0; 5],
748            timestamp: std::time::Instant::now(),
749            access_count: 1,
750        };
751        cache.insert("new_key".to_string(), new_embedding);
752        
753        assert_eq!(cache.cache.len(), 3);
754        assert!(cache.cache.contains_key("new_key"));
755    }
756
757    #[test]
758    fn test_tokenizer_creation() {
759        let tokenizer = CodeTokenizer {
760            model_type: "test-model".to_string(),
761            max_tokens: 1024,
762            vocab_size: 50000,
763            special_tokens: HashMap::new(),
764        };
765        
766        assert_eq!(tokenizer.model_type, "test-model");
767        assert_eq!(tokenizer.max_tokens, 1024);
768        assert_eq!(tokenizer.vocab_size, 50000);
769    }
770
771    #[test]
772    fn test_code_model_initialization() {
773        let model = CodeModel {
774            model_type: "codet5-small".to_string(),
775            embedding_dim: 512,
776            device: "cpu".to_string(),
777            initialized: false,
778        };
779        
780        assert_eq!(model.embedding_dim, 512);
781        assert_eq!(model.device, "cpu");
782        assert!(!model.initialized);
783    }
784
785    #[tokio::test]
786    async fn test_initialization_validation() {
787        // Valid configuration
788        let valid_config = EncoderConfig {
789            model_type: "codet5-base".to_string(),
790            max_tokens: 2048,
791            embedding_dim: 768,
792            batch_size: 16,
793            device: "cpu".to_string(),
794        };
795        
796        let result = initialize_encoder(&valid_config).await;
797        assert!(result.is_ok(), "Valid configuration should initialize successfully");
798        
799        // Configuration with too few tokens
800        let invalid_config = EncoderConfig {
801            max_tokens: 256, // Too small
802            ..valid_config.clone()
803        };
804        
805        let result = initialize_encoder(&invalid_config).await;
806        assert!(result.is_err(), "Configuration with too few tokens should fail");
807        
808        // Configuration with too many tokens (should warn but succeed)
809        let warning_config = EncoderConfig {
810            max_tokens: 8192, // Large but acceptable
811            ..valid_config
812        };
813        
814        let result = initialize_encoder(&warning_config).await;
815        assert!(result.is_ok(), "Large token configuration should succeed with warning");
816    }
817
818    #[test]
819    fn test_cache_edge_cases() {
820        let mut cache = EmbeddingCache::new(1);
821        
822        // Test empty cache
823        assert_eq!(cache.cache.len(), 0);
824        assert_eq!(cache.calculate_hit_rate(), 0.0);
825        
826        // Test single entry
827        let embedding = CachedEmbedding {
828            embedding: vec![1.0, 2.0],
829            timestamp: std::time::Instant::now(),
830            access_count: 1,
831        };
832        
833        cache.insert("single".to_string(), embedding);
834        assert_eq!(cache.cache.len(), 1);
835        
836        // Test cache size 0
837        let zero_cache = EmbeddingCache::new(0);
838        assert_eq!(zero_cache.max_size, 0);
839    }
840}