1mod scm_types;
50use scm_types::{cosine_similarity, fnv1a_64, xorshift64, Slot};
51pub use scm_types::{
52 ScmCacheConfig, ScmCacheEntry, ScmCacheError, ScmCacheHit, ScmCacheKey, ScmCacheStats,
53 ScmEvictionStrategy,
54};
55
56#[derive(Debug)]
66pub struct SemanticCacheManager {
67 config: ScmCacheConfig,
68 slots: Vec<Slot>,
69 next_id: u64,
70 embedding_dim: Option<usize>,
72 total_insertions: u64,
74 exact_hits: u64,
75 semantic_hits: u64,
76 misses: u64,
77 evictions: u64,
78 semantic_similarity_sum: f64,
80 rng_state: u64,
82}
83
84impl SemanticCacheManager {
85 pub fn new(config: ScmCacheConfig) -> Self {
87 Self {
88 slots: Vec::with_capacity(config.max_entries.min(4096)),
89 next_id: 1,
90 embedding_dim: None,
91 total_insertions: 0,
92 exact_hits: 0,
93 semantic_hits: 0,
94 misses: 0,
95 evictions: 0,
96 semantic_similarity_sum: 0.0,
97 rng_state: 0xDEAD_BEEF_CAFE_0001,
98 config,
99 }
100 }
101
102 pub fn insert(
118 &mut self,
119 key: ScmCacheKey,
120 result: Vec<u8>,
121 ttl_us: Option<u64>,
122 ) -> Result<u64, ScmCacheError> {
123 if let Some(dim) = self.embedding_dim {
125 if key.embedding.len() != dim {
126 return Err(ScmCacheError::DimensionMismatch {
127 expected: dim,
128 got: key.embedding.len(),
129 });
130 }
131 } else if !key.embedding.is_empty() {
132 self.embedding_dim = Some(key.embedding.len());
133 }
134
135 let effective_ttl = ttl_us.or(self.config.ttl_us);
136 let entry = ScmCacheEntry {
137 result,
138 inserted_at: 0, last_accessed: 0,
140 access_count: 0,
141 ttl_us: effective_ttl,
142 similarity_score: 1.0,
143 key,
144 };
145
146 let entry_bytes = entry.byte_size();
147 if entry_bytes > self.config.max_bytes {
148 return Err(ScmCacheError::EntryTooLarge(entry_bytes));
149 }
150
151 let current_bytes = self.current_bytes();
153 if current_bytes + entry_bytes > self.config.max_bytes
154 || self.slots.len() >= self.config.max_entries
155 {
156 let needed = entry_bytes;
157 self.evict_to_fit_internal(needed)?;
158 }
159
160 let id = self.next_id;
161 self.next_id += 1;
162 self.slots.push(Slot { id, entry });
163 self.total_insertions += 1;
164 Ok(id)
165 }
166
167 pub fn insert_at(
170 &mut self,
171 key: ScmCacheKey,
172 result: Vec<u8>,
173 ttl_us: Option<u64>,
174 inserted_at: u64,
175 ) -> Result<u64, ScmCacheError> {
176 let id = self.insert(key, result, ttl_us)?;
177 if let Some(slot) = self.slots.iter_mut().find(|s| s.id == id) {
179 slot.entry.inserted_at = inserted_at;
180 slot.entry.last_accessed = inserted_at;
181 }
182 Ok(id)
183 }
184
185 pub fn lookup(
198 &mut self,
199 query_text: &str,
200 embedding: &[f64],
201 current_ts: u64,
202 ) -> Result<ScmCacheHit, ScmCacheError> {
203 if let Some(dim) = self.embedding_dim {
205 if !embedding.is_empty() && embedding.len() != dim {
206 return Err(ScmCacheError::DimensionMismatch {
207 expected: dim,
208 got: embedding.len(),
209 });
210 }
211 }
212
213 let query_hash = fnv1a_64(query_text.as_bytes());
214
215 self.slots.retain(|s| !s.entry.is_expired(current_ts));
217
218 let mut best_exact: Option<usize> = None;
220 let mut best_semantic: Option<(usize, f64)> = None;
221
222 for (i, slot) in self.slots.iter().enumerate() {
223 if slot.entry.key.query_hash == query_hash && slot.entry.key.query_text == query_text {
225 best_exact = Some(i);
226 break; }
228 let sim = cosine_similarity(embedding, &slot.entry.key.embedding);
230 if sim >= self.config.semantic_threshold {
231 match best_semantic {
232 Some((_, prev_sim)) if sim <= prev_sim => {}
233 _ => best_semantic = Some((i, sim)),
234 }
235 }
236 }
237
238 if let Some(idx) = best_exact {
239 let slot = &mut self.slots[idx];
240 slot.entry.access_count += 1;
241 slot.entry.last_accessed = current_ts;
242 self.exact_hits += 1;
243 return Ok(ScmCacheHit::Exact { entry_id: slot.id });
244 }
245
246 if let Some((idx, sim)) = best_semantic {
247 let slot = &mut self.slots[idx];
248 slot.entry.access_count += 1;
249 slot.entry.last_accessed = current_ts;
250 let entry_id = slot.id;
251 let original_query = slot.entry.key.query_text.clone();
252 self.semantic_hits += 1;
253 self.semantic_similarity_sum += sim;
254 return Ok(ScmCacheHit::Semantic {
255 entry_id,
256 similarity: sim,
257 original_query,
258 });
259 }
260
261 self.misses += 1;
262 Ok(ScmCacheHit::Miss)
263 }
264
265 pub fn get_entry(&self, entry_id: u64) -> Result<&ScmCacheEntry, ScmCacheError> {
271 self.slots
272 .iter()
273 .find(|s| s.id == entry_id)
274 .map(|s| &s.entry)
275 .ok_or(ScmCacheError::EntryNotFound(entry_id))
276 }
277
278 pub fn invalidate(&mut self, entry_id: u64) -> Result<(), ScmCacheError> {
284 let pos = self
285 .slots
286 .iter()
287 .position(|s| s.id == entry_id)
288 .ok_or(ScmCacheError::EntryNotFound(entry_id))?;
289 self.slots.swap_remove(pos);
290 Ok(())
291 }
292
293 pub fn invalidate_similar(&mut self, embedding: &[f64], threshold: f64) -> Vec<u64> {
296 let mut removed = Vec::new();
297 let mut i = 0;
298 while i < self.slots.len() {
299 let sim = cosine_similarity(embedding, &self.slots[i].entry.key.embedding);
300 if sim > threshold {
301 removed.push(self.slots[i].id);
302 self.slots.swap_remove(i);
303 } else {
304 i += 1;
305 }
306 }
307 removed
308 }
309
310 pub fn expire_ttl(&mut self, current_ts: u64) -> Vec<u64> {
312 let mut removed = Vec::new();
313 let mut i = 0;
314 while i < self.slots.len() {
315 if self.slots[i].entry.is_expired(current_ts) {
316 removed.push(self.slots[i].id);
317 self.slots.swap_remove(i);
318 } else {
319 i += 1;
320 }
321 }
322 removed
323 }
324
325 pub fn evict_to_fit(&mut self, needed_bytes: usize) -> Vec<u64> {
333 let current = self.current_bytes();
334 if current + needed_bytes <= self.config.max_bytes
335 && self.slots.len() < self.config.max_entries
336 {
337 return Vec::new();
338 }
339 let mut removed = Vec::new();
340 while !self.slots.is_empty()
341 && (self.current_bytes() + needed_bytes > self.config.max_bytes
342 || self.slots.len() >= self.config.max_entries)
343 {
344 if let Some(victim) = self.pick_victim() {
345 let id = self.slots[victim].id;
346 self.slots.swap_remove(victim);
347 self.evictions += 1;
348 removed.push(id);
349 } else {
350 break;
351 }
352 }
353 removed
354 }
355
356 pub fn semantic_neighbors(&self, embedding: &[f64], top_k: usize) -> Vec<(u64, f64)> {
359 if top_k == 0 || self.slots.is_empty() {
360 return Vec::new();
361 }
362 let mut scores: Vec<(u64, f64)> = self
363 .slots
364 .iter()
365 .map(|s| {
366 let sim = cosine_similarity(embedding, &s.entry.key.embedding);
367 (s.id, sim)
368 })
369 .collect();
370 scores.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
372 scores.truncate(top_k);
373 scores
374 }
375
376 pub fn cluster_stats(&self) -> Vec<(f64, usize)> {
386 if self.slots.is_empty() {
387 return Vec::new();
388 }
389 let embeddings: Vec<&Vec<f64>> =
391 self.slots.iter().map(|s| &s.entry.key.embedding).collect();
392 let n = embeddings.len();
393 let mut assigned = vec![false; n];
394 let mut clusters: Vec<(f64, usize)> = Vec::new();
395
396 for seed in 0..n {
397 if assigned[seed] {
398 continue;
399 }
400 let mut members: Vec<usize> = vec![seed];
402 assigned[seed] = true;
403 for candidate in (seed + 1)..n {
404 if assigned[candidate] {
405 continue;
406 }
407 let sim = cosine_similarity(embeddings[seed], embeddings[candidate]);
408 if 1.0 - sim <= self.config.cluster_radius {
410 members.push(candidate);
411 assigned[candidate] = true;
412 }
413 }
414 let avg_sim = if members.len() == 1 {
416 1.0
417 } else {
418 let mut sum = 0.0;
419 let mut count = 0usize;
420 for i in 0..members.len() {
421 for j in (i + 1)..members.len() {
422 sum += cosine_similarity(embeddings[members[i]], embeddings[members[j]]);
423 count += 1;
424 }
425 }
426 if count == 0 {
427 1.0
428 } else {
429 sum / count as f64
430 }
431 };
432 clusters.push((avg_sim, members.len()));
433 }
434 clusters
435 }
436
437 pub fn stats(&self) -> ScmCacheStats {
439 let total_queries = self.exact_hits + self.semantic_hits + self.misses;
440 let semantic_hit_rate = if total_queries == 0 {
441 0.0
442 } else {
443 self.semantic_hits as f64 / total_queries as f64
444 };
445 let avg_similarity_on_semantic_hit = if self.semantic_hits == 0 {
446 0.0
447 } else {
448 self.semantic_similarity_sum / self.semantic_hits as f64
449 };
450 ScmCacheStats {
451 total_insertions: self.total_insertions,
452 exact_hits: self.exact_hits,
453 semantic_hits: self.semantic_hits,
454 misses: self.misses,
455 evictions: self.evictions,
456 current_entries: self.slots.len(),
457 current_bytes: self.current_bytes(),
458 semantic_hit_rate,
459 avg_similarity_on_semantic_hit,
460 }
461 }
462
463 fn current_bytes(&self) -> usize {
469 self.slots.iter().map(|s| s.entry.byte_size()).sum()
470 }
471
472 fn evict_to_fit_internal(&mut self, needed_bytes: usize) -> Result<(), ScmCacheError> {
474 let max_iters = self.slots.len() + 1; let mut iters = 0;
476 while !self.slots.is_empty()
477 && (self.current_bytes() + needed_bytes > self.config.max_bytes
478 || self.slots.len() >= self.config.max_entries)
479 {
480 if iters >= max_iters {
481 return Err(ScmCacheError::CacheAtCapacity);
482 }
483 match self.pick_victim() {
484 Some(victim) => {
485 self.slots.swap_remove(victim);
486 self.evictions += 1;
487 }
488 None => return Err(ScmCacheError::CacheAtCapacity),
489 }
490 iters += 1;
491 }
492 Ok(())
493 }
494
495 fn pick_victim(&mut self) -> Option<usize> {
498 if self.slots.is_empty() {
499 return None;
500 }
501 let strategy = self.config.strategy.clone();
502 match strategy {
503 ScmEvictionStrategy::LRU => self.victim_lru(),
504 ScmEvictionStrategy::LFU => self.victim_lfu(),
505 ScmEvictionStrategy::TTLFirst => self.victim_ttl_first(),
506 ScmEvictionStrategy::SemanticCluster => self.victim_semantic_cluster(),
507 ScmEvictionStrategy::HybridScore(w) => self.victim_hybrid(w),
508 }
509 }
510
511 fn victim_lru(&self) -> Option<usize> {
513 self.slots
514 .iter()
515 .enumerate()
516 .min_by_key(|(_, s)| s.entry.last_accessed)
517 .map(|(i, _)| i)
518 }
519
520 fn victim_lfu(&self) -> Option<usize> {
522 self.slots
523 .iter()
524 .enumerate()
525 .min_by_key(|(_, s)| s.entry.access_count)
526 .map(|(i, _)| i)
527 }
528
529 fn victim_ttl_first(&self) -> Option<usize> {
532 let ttl_victim = self
534 .slots
535 .iter()
536 .enumerate()
537 .filter_map(|(i, s)| {
538 s.entry.ttl_us.map(|ttl| {
539 let remaining = ttl
540 .saturating_sub(s.entry.last_accessed.saturating_sub(s.entry.inserted_at));
541 (i, remaining)
542 })
543 })
544 .min_by_key(|(_, rem)| *rem)
545 .map(|(i, _)| i);
546
547 ttl_victim.or_else(|| self.victim_lru())
548 }
549
550 fn victim_semantic_cluster(&mut self) -> Option<usize> {
553 if self.slots.is_empty() {
554 return None;
555 }
556 let n = self.slots.len();
557 let embeddings: Vec<&Vec<f64>> =
558 self.slots.iter().map(|s| &s.entry.key.embedding).collect();
559
560 let mut cluster_id = vec![usize::MAX; n];
562 let mut cluster_seeds: Vec<usize> = Vec::new();
563
564 for seed in 0..n {
565 if cluster_id[seed] != usize::MAX {
566 continue;
567 }
568 let cid = cluster_seeds.len();
569 cluster_seeds.push(seed);
570 cluster_id[seed] = cid;
571 for candidate in (seed + 1)..n {
572 if cluster_id[candidate] != usize::MAX {
573 continue;
574 }
575 let sim = cosine_similarity(embeddings[seed], embeddings[candidate]);
576 if 1.0 - sim <= self.config.cluster_radius {
577 cluster_id[candidate] = cid;
578 }
579 }
580 }
581
582 let num_clusters = cluster_seeds.len();
584 let mut sizes = vec![0usize; num_clusters];
585 for &cid in &cluster_id {
586 if cid < num_clusters {
587 sizes[cid] += 1;
588 }
589 }
590
591 let (largest_cid, _) = sizes
593 .iter()
594 .enumerate()
595 .max_by_key(|(_, &s)| s)
596 .unwrap_or((0, &0));
597
598 cluster_id
600 .iter()
601 .enumerate()
602 .filter(|(_, &cid)| cid == largest_cid)
603 .min_by_key(|(i, _)| self.slots[*i].entry.last_accessed)
604 .map(|(i, _)| i)
605 }
606
607 fn victim_hybrid(&mut self, weight: f64) -> Option<usize> {
610 if self.slots.is_empty() {
611 return None;
612 }
613 let weight = weight.clamp(0.0, 1.0);
614
615 let max_ts = self.slots.iter().map(|s| s.entry.last_accessed).max()?;
617 let max_ts = max_ts.max(1); let max_cnt = self
619 .slots
620 .iter()
621 .map(|s| s.entry.access_count)
622 .max()
623 .unwrap_or(1)
624 .max(1);
625
626 let mut best_idx = 0;
627 let mut best_score = f64::MAX;
628
629 for (i, slot) in self.slots.iter().enumerate() {
630 let recency = slot.entry.last_accessed as f64 / max_ts as f64;
631 let frequency = slot.entry.access_count as f64 / max_cnt as f64;
632 let score = weight * recency + (1.0 - weight) * frequency;
633 if score < best_score
634 || (score == best_score && {
635 let noise = xorshift64(&mut self.rng_state) & 1;
637 noise == 0
638 })
639 {
640 best_score = score;
641 best_idx = i;
642 }
643 }
644 Some(best_idx)
645 }
646}
647
648pub type ScmKeyAlias = ScmCacheKey;
654pub type ScmEntryAlias = ScmCacheEntry;
656pub type ScmStatsAlias = ScmCacheStats;
658pub type ScmHitAlias = ScmCacheHit;
660pub type ScmErrorAlias = ScmCacheError;
662
663#[cfg(test)]
668mod tests {
669 use super::*;
670
671 fn default_config() -> ScmCacheConfig {
674 ScmCacheConfig {
675 max_entries: 16,
676 max_bytes: 1024 * 1024,
677 semantic_threshold: 0.90,
678 ttl_us: None,
679 strategy: ScmEvictionStrategy::LRU,
680 cluster_radius: 0.15,
681 }
682 }
683
684 fn make_key(text: &str, emb: Vec<f64>) -> ScmCacheKey {
685 ScmCacheKey::new(text.to_string(), emb)
686 }
687
688 fn unit_vec(dim: usize, hot: usize) -> Vec<f64> {
689 let mut v = vec![0.0; dim];
690 if hot < dim {
691 v[hot] = 1.0;
692 }
693 v
694 }
695
696 fn uniform_vec(dim: usize, val: f64) -> Vec<f64> {
697 vec![val; dim]
698 }
699
700 fn make_mgr() -> SemanticCacheManager {
701 SemanticCacheManager::new(default_config())
702 }
703
704 #[test]
706 fn test_insert_returns_id() {
707 let mut mgr = make_mgr();
708 let key = make_key("hello", vec![1.0, 0.0, 0.0]);
709 let id = mgr.insert(key, b"result".to_vec(), None).expect("insert");
710 assert!(id >= 1);
711 }
712
713 #[test]
715 fn test_insert_monotonic_ids() {
716 let mut mgr = make_mgr();
717 let id1 = mgr
718 .insert(make_key("a", vec![1.0, 0.0]), b"r".to_vec(), None)
719 .expect("a");
720 let id2 = mgr
721 .insert(make_key("b", vec![0.0, 1.0]), b"r".to_vec(), None)
722 .expect("b");
723 assert!(id2 > id1);
724 }
725
726 #[test]
728 fn test_exact_hit() {
729 let mut mgr = make_mgr();
730 let emb = vec![1.0, 0.0, 0.0];
731 let id = mgr
732 .insert(make_key("exact query", emb.clone()), b"res".to_vec(), None)
733 .expect("insert");
734 let hit = mgr.lookup("exact query", &emb, 0).expect("lookup");
735 assert_eq!(hit, ScmCacheHit::Exact { entry_id: id });
736 }
737
738 #[test]
740 fn test_semantic_hit_above_threshold() {
741 let mut mgr = make_mgr();
742 let emb = vec![1.0, 0.0, 0.0];
744 let id = mgr
745 .insert(make_key("what is ipfs", emb.clone()), b"res".to_vec(), None)
746 .expect("insert");
747 let query_emb = vec![0.999, 0.04, 0.0];
749 let hit = mgr.lookup("what is ipfs?", &query_emb, 0).expect("lookup");
750 match hit {
751 ScmCacheHit::Semantic {
752 entry_id,
753 similarity,
754 ..
755 } => {
756 assert_eq!(entry_id, id);
757 assert!(similarity >= 0.90);
758 }
759 other => panic!("expected Semantic hit, got {:?}", other),
760 }
761 }
762
763 #[test]
765 fn test_semantic_hit_below_threshold() {
766 let mut mgr = make_mgr();
767 mgr.insert(
768 make_key("topic A", vec![1.0, 0.0, 0.0]),
769 b"r".to_vec(),
770 None,
771 )
772 .expect("insert");
773 let hit = mgr.lookup("topic B", &[0.0, 1.0, 0.0], 0).expect("lookup");
775 assert_eq!(hit, ScmCacheHit::Miss);
776 }
777
778 #[test]
780 fn test_miss_on_empty_cache() {
781 let mut mgr = make_mgr();
782 let hit = mgr.lookup("anything", &[1.0, 0.0], 0).expect("lookup");
783 assert_eq!(hit, ScmCacheHit::Miss);
784 }
785
786 #[test]
788 fn test_ttl_expiration_during_lookup() {
789 let mut mgr = make_mgr();
790 let emb = vec![1.0, 0.0, 0.0];
792 mgr.insert_at(
793 make_key("expiring", emb.clone()),
794 b"r".to_vec(),
795 Some(100),
796 0,
797 )
798 .expect("insert");
799 let hit = mgr.lookup("expiring", &emb, 200).expect("lookup");
801 assert_eq!(hit, ScmCacheHit::Miss);
802 assert_eq!(mgr.slots.len(), 0);
803 }
804
805 #[test]
807 fn test_ttl_entry_alive_before_expiry() {
808 let mut mgr = make_mgr();
809 let emb = vec![1.0, 0.0, 0.0];
810 let id = mgr
811 .insert_at(make_key("live", emb.clone()), b"r".to_vec(), Some(1000), 0)
812 .expect("insert");
813 let hit = mgr.lookup("live", &emb, 50).expect("lookup");
815 assert_eq!(hit, ScmCacheHit::Exact { entry_id: id });
816 }
817
818 #[test]
820 fn test_expire_ttl_explicit() {
821 let mut mgr = make_mgr();
822 let emb = vec![1.0, 0.0];
823 let id1 = mgr
824 .insert_at(make_key("q1", emb.clone()), b"r1".to_vec(), Some(50), 0)
825 .expect("i1");
826 let id2 = mgr
827 .insert_at(make_key("q2", vec![0.0, 1.0]), b"r2".to_vec(), None, 0)
828 .expect("i2");
829 let removed = mgr.expire_ttl(100);
830 assert!(removed.contains(&id1));
831 assert!(!removed.contains(&id2));
832 assert_eq!(mgr.slots.len(), 1);
833 }
834
835 #[test]
837 fn test_eviction_lru() {
838 let config = ScmCacheConfig {
839 max_entries: 3,
840 max_bytes: 1024 * 1024,
841 semantic_threshold: 0.90,
842 ttl_us: None,
843 strategy: ScmEvictionStrategy::LRU,
844 cluster_radius: 0.15,
845 };
846 let mut mgr = SemanticCacheManager::new(config);
847
848 let id1 = mgr
850 .insert_at(make_key("q1", unit_vec(4, 0)), b"r".to_vec(), None, 100)
851 .expect("i1");
852 let _id2 = mgr
853 .insert_at(make_key("q2", unit_vec(4, 1)), b"r".to_vec(), None, 200)
854 .expect("i2");
855 let _id3 = mgr
856 .insert_at(make_key("q3", unit_vec(4, 2)), b"r".to_vec(), None, 300)
857 .expect("i3");
858
859 let _id4 = mgr
861 .insert_at(make_key("q4", unit_vec(4, 3)), b"r".to_vec(), None, 400)
862 .expect("i4");
863
864 assert_eq!(mgr.slots.len(), 3);
865 assert!(mgr.get_entry(id1).is_err(), "id1 should have been evicted");
866 }
867
868 #[test]
870 fn test_eviction_lfu() {
871 let config = ScmCacheConfig {
872 max_entries: 3,
873 max_bytes: 1024 * 1024,
874 semantic_threshold: 0.90,
875 ttl_us: None,
876 strategy: ScmEvictionStrategy::LFU,
877 cluster_radius: 0.15,
878 };
879 let mut mgr = SemanticCacheManager::new(config);
880
881 let id1 = mgr
882 .insert(make_key("rare", unit_vec(4, 0)), b"r".to_vec(), None)
883 .expect("i1");
884 let _id2 = mgr
885 .insert(make_key("common1", unit_vec(4, 1)), b"r".to_vec(), None)
886 .expect("i2");
887 let _id3 = mgr
888 .insert(make_key("common2", unit_vec(4, 2)), b"r".to_vec(), None)
889 .expect("i3");
890
891 for _ in 0..5 {
893 let _ = mgr.lookup("common1", &unit_vec(4, 1), 0);
894 let _ = mgr.lookup("common2", &unit_vec(4, 2), 0);
895 }
896
897 let _id4 = mgr
899 .insert(make_key("new", unit_vec(4, 3)), b"r".to_vec(), None)
900 .expect("i4");
901
902 assert_eq!(mgr.slots.len(), 3);
903 assert!(mgr.get_entry(id1).is_err(), "id1 (LFU) should be evicted");
904 }
905
906 #[test]
908 fn test_eviction_ttl_first() {
909 let config = ScmCacheConfig {
910 max_entries: 3,
911 max_bytes: 1024 * 1024,
912 semantic_threshold: 0.90,
913 ttl_us: None,
914 strategy: ScmEvictionStrategy::TTLFirst,
915 cluster_radius: 0.15,
916 };
917 let mut mgr = SemanticCacheManager::new(config);
918
919 let id_short = mgr
920 .insert(
921 make_key("short-ttl", unit_vec(4, 0)),
922 b"r".to_vec(),
923 Some(10),
924 )
925 .expect("short");
926 let _id_long = mgr
927 .insert(
928 make_key("long-ttl", unit_vec(4, 1)),
929 b"r".to_vec(),
930 Some(10_000),
931 )
932 .expect("long");
933 let _id_none = mgr
934 .insert(make_key("no-ttl", unit_vec(4, 2)), b"r".to_vec(), None)
935 .expect("none");
936
937 let _id4 = mgr
939 .insert(make_key("q4", unit_vec(4, 3)), b"r".to_vec(), None)
940 .expect("i4");
941
942 assert_eq!(mgr.slots.len(), 3);
943 assert!(
944 mgr.get_entry(id_short).is_err(),
945 "short-TTL entry should be evicted first"
946 );
947 }
948
949 #[test]
951 fn test_eviction_semantic_cluster() {
952 let config = ScmCacheConfig {
953 max_entries: 4,
954 max_bytes: 1024 * 1024,
955 semantic_threshold: 0.50,
956 ttl_us: None,
957 strategy: ScmEvictionStrategy::SemanticCluster,
958 cluster_radius: 0.10, };
960 let mut mgr = SemanticCacheManager::new(config);
961
962 let base = vec![1.0_f64, 0.001, 0.0];
964 mgr.insert(make_key("a1", base.clone()), b"r".to_vec(), None)
965 .expect("a1");
966 mgr.insert(make_key("a2", vec![1.0, 0.002, 0.0]), b"r".to_vec(), None)
967 .expect("a2");
968 mgr.insert(make_key("a3", vec![1.0, 0.003, 0.0]), b"r".to_vec(), None)
969 .expect("a3");
970 mgr.insert(make_key("b1", vec![0.0, 1.0, 0.0]), b"r".to_vec(), None)
971 .expect("b1");
972
973 mgr.insert(make_key("new", vec![0.0, 0.0, 1.0]), b"r".to_vec(), None)
975 .expect("new");
976
977 assert_eq!(mgr.slots.len(), 4);
978 let remaining_a: Vec<_> = mgr
981 .slots
982 .iter()
983 .filter(|s| {
984 let t = &s.entry.key.query_text;
985 t == "a1" || t == "a2" || t == "a3"
986 })
987 .collect();
988 assert!(
989 remaining_a.len() < 3,
990 "One of the cluster-A entries should have been evicted"
991 );
992 }
993
994 #[test]
996 fn test_eviction_hybrid_weight_one() {
997 let config = ScmCacheConfig {
998 max_entries: 3,
999 max_bytes: 1024 * 1024,
1000 semantic_threshold: 0.90,
1001 ttl_us: None,
1002 strategy: ScmEvictionStrategy::HybridScore(1.0),
1003 cluster_radius: 0.15,
1004 };
1005 let mut mgr = SemanticCacheManager::new(config);
1006
1007 let id_old = mgr
1008 .insert_at(make_key("old", unit_vec(4, 0)), b"r".to_vec(), None, 1)
1009 .expect("old");
1010 let _id_mid = mgr
1011 .insert_at(make_key("mid", unit_vec(4, 1)), b"r".to_vec(), None, 50)
1012 .expect("mid");
1013 let _id_new = mgr
1014 .insert_at(make_key("new", unit_vec(4, 2)), b"r".to_vec(), None, 100)
1015 .expect("new");
1016
1017 let _id4 = mgr
1019 .insert_at(make_key("q4", unit_vec(4, 3)), b"r".to_vec(), None, 200)
1020 .expect("i4");
1021
1022 assert_eq!(mgr.slots.len(), 3);
1023 assert!(
1024 mgr.get_entry(id_old).is_err(),
1025 "oldest entry should be evicted with weight=1.0"
1026 );
1027 }
1028
1029 #[test]
1031 fn test_eviction_hybrid_weight_zero() {
1032 let config = ScmCacheConfig {
1033 max_entries: 3,
1034 max_bytes: 1024 * 1024,
1035 semantic_threshold: 0.90,
1036 ttl_us: None,
1037 strategy: ScmEvictionStrategy::HybridScore(0.0),
1038 cluster_radius: 0.15,
1039 };
1040 let mut mgr = SemanticCacheManager::new(config);
1041
1042 let id_rare = mgr
1043 .insert(make_key("rare", unit_vec(4, 0)), b"r".to_vec(), None)
1044 .expect("rare");
1045 let _id_freq1 = mgr
1046 .insert(make_key("freq1", unit_vec(4, 1)), b"r".to_vec(), None)
1047 .expect("freq1");
1048 let _id_freq2 = mgr
1049 .insert(make_key("freq2", unit_vec(4, 2)), b"r".to_vec(), None)
1050 .expect("freq2");
1051
1052 for _ in 0..10 {
1054 let _ = mgr.lookup("freq1", &unit_vec(4, 1), 0);
1055 let _ = mgr.lookup("freq2", &unit_vec(4, 2), 0);
1056 }
1057
1058 let _id4 = mgr
1060 .insert(make_key("q4", unit_vec(4, 3)), b"r".to_vec(), None)
1061 .expect("i4");
1062
1063 assert_eq!(mgr.slots.len(), 3);
1064 assert!(
1065 mgr.get_entry(id_rare).is_err(),
1066 "least-frequent entry should be evicted with weight=0.0"
1067 );
1068 }
1069
1070 #[test]
1072 fn test_invalidate() {
1073 let mut mgr = make_mgr();
1074 let id = mgr
1075 .insert(make_key("q", vec![1.0, 0.0]), b"r".to_vec(), None)
1076 .expect("insert");
1077 mgr.invalidate(id).expect("invalidate");
1078 assert!(mgr.get_entry(id).is_err());
1079 }
1080
1081 #[test]
1083 fn test_invalidate_unknown_id() {
1084 let mut mgr = make_mgr();
1085 let err = mgr.invalidate(999).unwrap_err();
1086 assert_eq!(err, ScmCacheError::EntryNotFound(999));
1087 }
1088
1089 #[test]
1091 fn test_invalidate_similar() {
1092 let mut mgr = make_mgr();
1093 let id1 = mgr
1094 .insert(make_key("a", vec![1.0, 0.0, 0.0]), b"r".to_vec(), None)
1095 .expect("a");
1096 let id2 = mgr
1097 .insert(make_key("b", vec![0.999, 0.044, 0.0]), b"r".to_vec(), None)
1098 .expect("b");
1099 let id3 = mgr
1100 .insert(make_key("c", vec![0.0, 1.0, 0.0]), b"r".to_vec(), None)
1101 .expect("c");
1102
1103 let removed = mgr.invalidate_similar(&[1.0, 0.0, 0.0], 0.95);
1105 assert!(removed.contains(&id1));
1106 assert!(removed.contains(&id2));
1107 assert!(!removed.contains(&id3));
1108 assert_eq!(mgr.slots.len(), 1);
1109 }
1110
1111 #[test]
1113 fn test_invalidate_similar_no_match() {
1114 let mut mgr = make_mgr();
1115 mgr.insert(make_key("a", vec![0.0, 1.0, 0.0]), b"r".to_vec(), None)
1116 .expect("a");
1117 let removed = mgr.invalidate_similar(&[1.0, 0.0, 0.0], 0.99);
1118 assert!(removed.is_empty());
1119 }
1120
1121 #[test]
1123 fn test_semantic_neighbors_top_k() {
1124 let mut mgr = make_mgr();
1125 for i in 0..8 {
1126 let mut emb = vec![0.0_f64; 8];
1127 emb[i] = 1.0;
1128 mgr.insert(make_key(&format!("q{i}"), emb), b"r".to_vec(), None)
1129 .expect("insert");
1130 }
1131 let query = unit_vec(8, 0);
1133 let neighbors = mgr.semantic_neighbors(&query, 3);
1134 assert_eq!(neighbors.len(), 3);
1135 assert!((neighbors[0].1 - 1.0).abs() < 1e-9);
1137 for w in neighbors.windows(2) {
1139 assert!(w[0].1 >= w[1].1);
1140 }
1141 }
1142
1143 #[test]
1145 fn test_semantic_neighbors_k_larger_than_cache() {
1146 let mut mgr = make_mgr();
1147 mgr.insert(make_key("only", vec![1.0, 0.0]), b"r".to_vec(), None)
1148 .expect("insert");
1149 let neighbors = mgr.semantic_neighbors(&[1.0, 0.0], 100);
1150 assert_eq!(neighbors.len(), 1);
1151 }
1152
1153 #[test]
1155 fn test_semantic_neighbors_empty() {
1156 let mgr = make_mgr();
1157 let neighbors = mgr.semantic_neighbors(&[1.0, 0.0], 5);
1158 assert!(neighbors.is_empty());
1159 }
1160
1161 #[test]
1163 fn test_cluster_stats_empty() {
1164 let mgr = make_mgr();
1165 let stats = mgr.cluster_stats();
1166 assert!(stats.is_empty());
1167 }
1168
1169 #[test]
1171 fn test_cluster_stats_single() {
1172 let mut mgr = make_mgr();
1173 mgr.insert(make_key("q", vec![1.0, 0.0]), b"r".to_vec(), None)
1174 .expect("insert");
1175 let stats = mgr.cluster_stats();
1176 assert_eq!(stats.len(), 1);
1177 assert_eq!(stats[0].1, 1); }
1179
1180 #[test]
1182 fn test_cluster_stats_two_clusters() {
1183 let config = ScmCacheConfig {
1184 cluster_radius: 0.05, ..default_config()
1186 };
1187 let mut mgr = SemanticCacheManager::new(config);
1188 mgr.insert(make_key("a1", vec![1.0, 0.0, 0.0]), b"r".to_vec(), None)
1190 .expect("a1");
1191 mgr.insert(make_key("a2", vec![1.0, 0.001, 0.0]), b"r".to_vec(), None)
1192 .expect("a2");
1193 mgr.insert(make_key("b1", vec![0.0, 1.0, 0.0]), b"r".to_vec(), None)
1195 .expect("b1");
1196 mgr.insert(make_key("b2", vec![0.001, 1.0, 0.0]), b"r".to_vec(), None)
1197 .expect("b2");
1198
1199 let stats = mgr.cluster_stats();
1200 assert_eq!(stats.len(), 2);
1202 for (_, size) in &stats {
1203 assert_eq!(*size, 2);
1204 }
1205 }
1206
1207 #[test]
1209 fn test_cluster_stats_similarity_bound() {
1210 let mut mgr = SemanticCacheManager::new(ScmCacheConfig {
1211 cluster_radius: 0.20,
1212 ..default_config()
1213 });
1214 mgr.insert(make_key("x", vec![1.0, 0.0]), b"r".to_vec(), None)
1215 .expect("x");
1216 let stats = mgr.cluster_stats();
1217 assert!(!stats.is_empty());
1218 for (sim, _) in &stats {
1219 assert!(*sim >= -1.0 && *sim <= 1.0);
1220 }
1221 }
1222
1223 #[test]
1225 fn test_dimension_mismatch_insert() {
1226 let mut mgr = make_mgr();
1227 mgr.insert(make_key("q1", vec![1.0, 0.0, 0.0]), b"r".to_vec(), None)
1228 .expect("first insert");
1229 let err = mgr
1230 .insert(make_key("q2", vec![1.0, 0.0]), b"r".to_vec(), None)
1231 .unwrap_err();
1232 assert!(matches!(
1233 err,
1234 ScmCacheError::DimensionMismatch {
1235 expected: 3,
1236 got: 2
1237 }
1238 ));
1239 }
1240
1241 #[test]
1243 fn test_dimension_mismatch_lookup() {
1244 let mut mgr = make_mgr();
1245 mgr.insert(make_key("q1", vec![1.0, 0.0, 0.0]), b"r".to_vec(), None)
1246 .expect("insert");
1247 let err = mgr.lookup("q2", &[1.0, 0.0], 0).unwrap_err();
1248 assert!(matches!(
1249 err,
1250 ScmCacheError::DimensionMismatch {
1251 expected: 3,
1252 got: 2
1253 }
1254 ));
1255 }
1256
1257 #[test]
1259 fn test_capacity_entry_count() {
1260 let config = ScmCacheConfig {
1261 max_entries: 5,
1262 max_bytes: 1024 * 1024,
1263 semantic_threshold: 0.90,
1264 ttl_us: None,
1265 strategy: ScmEvictionStrategy::LRU,
1266 cluster_radius: 0.15,
1267 };
1268 let mut mgr = SemanticCacheManager::new(config);
1269 for i in 0..10usize {
1270 let mut emb = vec![0.0; 10];
1271 emb[i % 10] = 1.0;
1272 mgr.insert(make_key(&format!("q{i}"), emb), b"r".to_vec(), None)
1273 .expect("insert");
1274 }
1275 assert!(mgr.slots.len() <= 5);
1276 }
1277
1278 #[test]
1280 fn test_capacity_bytes() {
1281 let config = ScmCacheConfig {
1282 max_entries: 1000,
1283 max_bytes: 512, semantic_threshold: 0.90,
1285 ttl_us: None,
1286 strategy: ScmEvictionStrategy::LRU,
1287 cluster_radius: 0.15,
1288 };
1289 let mut mgr = SemanticCacheManager::new(config);
1290 for i in 0..20usize {
1292 let mut emb = vec![0.0; 2];
1293 emb[i % 2] = 1.0;
1294 let _ = mgr.insert(make_key(&format!("q{i}"), emb), b"r".to_vec(), None);
1295 }
1296 assert!(mgr.current_bytes() <= 512 + 512); }
1298
1299 #[test]
1301 fn test_stats_exact_hits() {
1302 let mut mgr = make_mgr();
1303 let emb = vec![1.0, 0.0];
1304 let id = mgr
1305 .insert(make_key("q", emb.clone()), b"r".to_vec(), None)
1306 .expect("insert");
1307 for _ in 0..3 {
1308 let hit = mgr.lookup("q", &emb, 0).expect("lookup");
1309 assert_eq!(hit, ScmCacheHit::Exact { entry_id: id });
1310 }
1311 let s = mgr.stats();
1312 assert_eq!(s.exact_hits, 3);
1313 assert_eq!(s.semantic_hits, 0);
1314 assert_eq!(s.misses, 0);
1315 }
1316
1317 #[test]
1319 fn test_stats_semantic_hits() {
1320 let mut mgr = make_mgr();
1321 mgr.insert(
1322 make_key("original", vec![1.0, 0.0, 0.0]),
1323 b"r".to_vec(),
1324 None,
1325 )
1326 .expect("insert");
1327 let _ = mgr.lookup("different text", &[0.999, 0.044, 0.0], 0);
1329 let s = mgr.stats();
1330 assert_eq!(s.semantic_hits, 1);
1331 assert!(s.avg_similarity_on_semantic_hit > 0.0);
1332 }
1333
1334 #[test]
1336 fn test_stats_misses() {
1337 let mut mgr = make_mgr();
1338 mgr.insert(make_key("q", vec![1.0, 0.0]), b"r".to_vec(), None)
1339 .expect("insert");
1340 let _ = mgr.lookup("totally different", &[0.0, 1.0], 0);
1341 let s = mgr.stats();
1342 assert_eq!(s.misses, 1);
1343 }
1344
1345 #[test]
1347 fn test_stats_evictions() {
1348 let config = ScmCacheConfig {
1349 max_entries: 2,
1350 ..default_config()
1351 };
1352 let mut mgr = SemanticCacheManager::new(config);
1353 mgr.insert(make_key("a", unit_vec(3, 0)), b"r".to_vec(), None)
1354 .expect("a");
1355 mgr.insert(make_key("b", unit_vec(3, 1)), b"r".to_vec(), None)
1356 .expect("b");
1357 mgr.insert(make_key("c", unit_vec(3, 2)), b"r".to_vec(), None)
1358 .expect("c");
1359 assert_eq!(mgr.stats().evictions, 1);
1360 }
1361
1362 #[test]
1364 fn test_stats_total_insertions() {
1365 let mut mgr = make_mgr();
1366 for i in 0..5usize {
1367 let mut e = vec![0.0; 5];
1368 e[i] = 1.0;
1369 mgr.insert(make_key(&format!("q{i}"), e), b"r".to_vec(), None)
1370 .expect("insert");
1371 }
1372 assert_eq!(mgr.stats().total_insertions, 5);
1373 }
1374
1375 #[test]
1377 fn test_stats_semantic_hit_rate() {
1378 let mut mgr = make_mgr();
1379 mgr.insert(make_key("q", vec![1.0, 0.0, 0.0]), b"r".to_vec(), None)
1380 .expect("insert");
1381 let _ = mgr.lookup("close", &[0.999, 0.044, 0.0], 0);
1383 let _ = mgr.lookup("far", &[0.0, 1.0, 0.0], 0);
1384 let s = mgr.stats();
1385 assert!((s.semantic_hit_rate - 0.5).abs() < 1e-9);
1387 }
1388
1389 #[test]
1391 fn test_stats_avg_similarity() {
1392 let mut mgr = make_mgr();
1393 mgr.insert(make_key("q", vec![1.0, 0.0, 0.0]), b"r".to_vec(), None)
1394 .expect("insert");
1395 let _ = mgr.lookup("close1", &[0.999, 0.044, 0.0], 0);
1397 let _ = mgr.lookup("close2", &[0.999, 0.044, 0.0], 0);
1398 let s = mgr.stats();
1399 assert!(s.avg_similarity_on_semantic_hit > 0.0);
1400 assert!(s.avg_similarity_on_semantic_hit <= 1.0);
1401 }
1402
1403 #[test]
1405 fn test_get_entry() {
1406 let mut mgr = make_mgr();
1407 let payload = b"important data".to_vec();
1408 let id = mgr
1409 .insert(make_key("q", vec![1.0, 0.0]), payload.clone(), None)
1410 .expect("insert");
1411 let entry = mgr.get_entry(id).expect("get_entry");
1412 assert_eq!(entry.result, payload);
1413 }
1414
1415 #[test]
1417 fn test_get_entry_not_found() {
1418 let mgr = make_mgr();
1419 assert!(matches!(
1420 mgr.get_entry(42),
1421 Err(ScmCacheError::EntryNotFound(42))
1422 ));
1423 }
1424
1425 #[test]
1427 fn test_access_count_updated() {
1428 let mut mgr = make_mgr();
1429 let emb = vec![1.0, 0.0];
1430 let id = mgr
1431 .insert(make_key("q", emb.clone()), b"r".to_vec(), None)
1432 .expect("insert");
1433 assert_eq!(mgr.get_entry(id).expect("entry").access_count, 0);
1434 let _ = mgr.lookup("q", &emb, 0);
1435 let _ = mgr.lookup("q", &emb, 1);
1436 assert_eq!(mgr.get_entry(id).expect("entry").access_count, 2);
1437 }
1438
1439 #[test]
1441 fn test_last_accessed_updated() {
1442 let mut mgr = make_mgr();
1443 let emb = vec![1.0, 0.0];
1444 let id = mgr
1445 .insert(make_key("q", emb.clone()), b"r".to_vec(), None)
1446 .expect("insert");
1447 let _ = mgr.lookup("q", &emb, 999);
1448 assert_eq!(mgr.get_entry(id).expect("entry").last_accessed, 999);
1449 }
1450
1451 #[test]
1453 fn test_entry_too_large() {
1454 let config = ScmCacheConfig {
1455 max_bytes: 10, ..default_config()
1457 };
1458 let mut mgr = SemanticCacheManager::new(config);
1459 let err = mgr
1460 .insert(
1461 make_key("q", vec![1.0]),
1462 vec![0u8; 200], None,
1464 )
1465 .unwrap_err();
1466 assert!(matches!(err, ScmCacheError::EntryTooLarge(_)));
1467 }
1468
1469 #[test]
1471 fn test_evict_to_fit_returns_ids() {
1472 let config = ScmCacheConfig {
1474 max_entries: 100,
1475 max_bytes: 512, ..default_config()
1477 };
1478 let mut mgr = SemanticCacheManager::new(config);
1479 for i in 0..5usize {
1480 let mut e = vec![0.0; 5];
1481 e[i] = 1.0;
1482 let _ = mgr.insert(make_key(&format!("q{i}"), e), b"r".to_vec(), None);
1483 }
1484 let removed = mgr.evict_to_fit(mgr.config.max_bytes + 1);
1486 assert!(!removed.is_empty());
1487 }
1488
1489 #[test]
1491 fn test_evict_to_fit_noop() {
1492 let mut mgr = make_mgr();
1493 mgr.insert(make_key("q", vec![1.0, 0.0]), b"r".to_vec(), None)
1494 .expect("insert");
1495 let removed = mgr.evict_to_fit(0);
1496 assert!(removed.is_empty());
1497 }
1498
1499 #[test]
1501 fn test_fnv1a_consistency() {
1502 let h1 = fnv1a_64(b"hello world");
1503 let h2 = fnv1a_64(b"hello world");
1504 assert_eq!(h1, h2);
1505 let h3 = fnv1a_64(b"Hello World");
1506 assert_ne!(h1, h3);
1507 }
1508
1509 #[test]
1511 fn test_cosine_similarity_edge_cases() {
1512 assert_eq!(cosine_similarity(&[], &[]), 0.0);
1513 assert_eq!(cosine_similarity(&[0.0, 0.0], &[1.0, 0.0]), 0.0); assert!((cosine_similarity(&[1.0, 0.0], &[1.0, 0.0]) - 1.0).abs() < 1e-9);
1515 assert!((cosine_similarity(&[1.0, 0.0], &[0.0, 1.0])).abs() < 1e-9);
1516 assert_eq!(cosine_similarity(&[1.0, 0.0], &[1.0, 0.0, 0.0]), 0.0);
1518 }
1519
1520 #[test]
1522 fn test_xorshift64_distinct() {
1523 let mut state = 0x1234_5678_9ABC_DEF0u64;
1524 let v1 = xorshift64(&mut state);
1525 let v2 = xorshift64(&mut state);
1526 assert_ne!(v1, v2);
1527 }
1528
1529 #[test]
1531 fn test_cache_key_hash() {
1532 let k = ScmCacheKey::new("test".to_string(), vec![]);
1533 assert_eq!(k.query_hash, fnv1a_64(b"test"));
1534 }
1535
1536 #[test]
1538 fn test_cache_entry_is_expired() {
1539 let key = ScmCacheKey::new("q".to_string(), vec![1.0]);
1540 let entry = ScmCacheEntry {
1541 key,
1542 result: vec![],
1543 inserted_at: 0,
1544 last_accessed: 0,
1545 access_count: 0,
1546 ttl_us: Some(100),
1547 similarity_score: 1.0,
1548 };
1549 assert!(!entry.is_expired(99));
1550 assert!(entry.is_expired(100));
1551 assert!(entry.is_expired(200));
1552 }
1553
1554 #[test]
1556 fn test_cache_entry_no_ttl() {
1557 let key = ScmCacheKey::new("q".to_string(), vec![]);
1558 let entry = ScmCacheEntry {
1559 key,
1560 result: vec![],
1561 inserted_at: 0,
1562 last_accessed: 0,
1563 access_count: 0,
1564 ttl_us: None,
1565 similarity_score: 1.0,
1566 };
1567 assert!(!entry.is_expired(u64::MAX));
1568 }
1569
1570 #[test]
1572 fn test_semantic_best_match() {
1573 let mut mgr = make_mgr();
1574 mgr.insert(
1576 make_key("good", vec![0.98, 0.2, 0.0]),
1577 b"good".to_vec(),
1578 None,
1579 )
1580 .expect("good");
1581 mgr.insert(make_key("ok", vec![0.92, 0.4, 0.0]), b"ok".to_vec(), None)
1582 .expect("ok");
1583 let hit = mgr.lookup("query", &[1.0, 0.0, 0.0], 0).expect("lookup");
1585 match hit {
1586 ScmCacheHit::Semantic { original_query, .. } => {
1587 assert_eq!(original_query, "good");
1588 }
1589 other => panic!("expected Semantic hit, got {:?}", other),
1590 }
1591 }
1592
1593 #[test]
1595 fn test_uniform_embeddings_similarity() {
1596 let a = uniform_vec(64, 0.5);
1597 let b = uniform_vec(64, 0.3);
1598 assert!((cosine_similarity(&a, &b) - 1.0).abs() < 1e-6);
1600 }
1601
1602 #[test]
1604 fn test_evict_to_fit_respects_bytes() {
1605 let config = ScmCacheConfig {
1606 max_entries: 1000,
1607 max_bytes: 2048,
1608 ..default_config()
1609 };
1610 let mut mgr = SemanticCacheManager::new(config);
1611 for i in 0..10usize {
1612 let mut e = vec![0.0; 2];
1613 e[i % 2] = 1.0;
1614 let _ = mgr.insert(make_key(&format!("q{i}"), e), vec![0u8; 50], None);
1615 }
1616 let current = mgr.current_bytes();
1617 let freed_target = current / 2;
1619 let removed = mgr.evict_to_fit(freed_target + mgr.config.max_bytes);
1620 assert!(!removed.is_empty());
1621 }
1622
1623 #[test]
1625 fn test_default_config() {
1626 let config = ScmCacheConfig::default();
1627 let mut mgr = SemanticCacheManager::new(config);
1628 let id = mgr
1629 .insert(make_key("q", vec![1.0, 0.0]), b"r".to_vec(), None)
1630 .expect("insert");
1631 assert!(id >= 1);
1632 }
1633
1634 #[test]
1636 fn test_cache_error_display() {
1637 assert!(ScmCacheError::EntryTooLarge(100)
1638 .to_string()
1639 .contains("100"));
1640 assert!(ScmCacheError::CacheAtCapacity
1641 .to_string()
1642 .contains("capacity"));
1643 assert!(ScmCacheError::EntryNotFound(7).to_string().contains("7"));
1644 assert!(ScmCacheError::DimensionMismatch {
1645 expected: 3,
1646 got: 2
1647 }
1648 .to_string()
1649 .contains("mismatch"));
1650 assert!(ScmCacheError::TtlExpired(5).to_string().contains("5"));
1651 }
1652
1653 #[test]
1655 fn test_semantic_hit_original_query() {
1656 let mut mgr = make_mgr();
1657 let original = "the original query text";
1658 mgr.insert(make_key(original, vec![1.0, 0.0, 0.0]), b"r".to_vec(), None)
1659 .expect("insert");
1660 let hit = mgr
1661 .lookup("a different phrasing", &[0.999, 0.044, 0.0], 0)
1662 .expect("lookup");
1663 match hit {
1664 ScmCacheHit::Semantic { original_query, .. } => {
1665 assert_eq!(original_query, original);
1666 }
1667 other => panic!("expected Semantic, got {:?}", other),
1668 }
1669 }
1670
1671 #[test]
1673 fn test_large_scale_capacity() {
1674 let config = ScmCacheConfig {
1675 max_entries: 100,
1676 max_bytes: 64 * 1024 * 1024,
1677 semantic_threshold: 0.90,
1678 ttl_us: None,
1679 strategy: ScmEvictionStrategy::LRU,
1680 cluster_radius: 0.20,
1681 };
1682 let mut mgr = SemanticCacheManager::new(config);
1683 let mut state = 0xFEED_FACE_DEAD_BEEF_u64;
1684 for i in 0..1000usize {
1685 let dim = 16;
1686 let emb: Vec<f64> = (0..dim)
1687 .map(|_| (xorshift64(&mut state) >> 11) as f64 / (1u64 << 53) as f64 * 2.0 - 1.0)
1688 .collect();
1689 let _ = mgr.insert(make_key(&format!("stress-{i}"), emb), vec![0u8; 32], None);
1690 }
1691 assert!(mgr.slots.len() <= 100);
1692 }
1693
1694 #[test]
1696 fn test_insert_explicit_ttl() {
1697 let mut mgr = make_mgr(); let id = mgr
1699 .insert_at(make_key("q", vec![1.0, 0.0]), b"r".to_vec(), Some(50), 0)
1700 .expect("insert");
1701 let entry = mgr.get_entry(id).expect("get");
1702 assert_eq!(entry.ttl_us, Some(50));
1703 }
1704
1705 #[test]
1707 fn test_insert_inherits_config_ttl() {
1708 let config = ScmCacheConfig {
1709 ttl_us: Some(1000),
1710 ..default_config()
1711 };
1712 let mut mgr = SemanticCacheManager::new(config);
1713 let id = mgr
1714 .insert(make_key("q", vec![1.0, 0.0]), b"r".to_vec(), None)
1715 .expect("insert");
1716 let entry = mgr.get_entry(id).expect("get");
1717 assert_eq!(entry.ttl_us, Some(1000));
1718 }
1719}