Skip to main content

asupersync/atp/cache/
mod.rs

1//! ATP cache and seeding system.
2//!
3//! Implements verified object graph caching for teams, CI, datasets, and artifact distribution.
4//! Provides cache indexing by manifest and grant, eviction policies that preserve proof/journal
5//! invariants, and trust boundaries that respect capabilities and prevent ambient data leaks.
6
7pub mod policy;
8pub mod storage;
9pub mod trust;
10
11use crate::atp::identity::IdentityError;
12use serde::{Deserialize, Serialize};
13use std::collections::HashMap;
14use std::path::PathBuf;
15use std::time::{Duration, SystemTime};
16
17/// Cache entry identifier combining manifest and chunk information.
18#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
19pub struct CacheKey {
20    /// Manifest root hash that authorizes this content.
21    pub manifest_hash: String,
22    /// Content hash of the cached chunk/object.
23    pub content_hash: String,
24    /// Grant scope that authorizes access to this content.
25    pub grant_scope: Option<String>,
26}
27
28impl CacheKey {
29    /// Create a new cache key for verified content.
30    #[must_use]
31    pub fn new(manifest_hash: String, content_hash: String, grant_scope: Option<String>) -> Self {
32        Self {
33            manifest_hash,
34            content_hash,
35            grant_scope,
36        }
37    }
38
39    /// Get a stable string representation for indexing.
40    #[must_use]
41    pub fn as_index_key(&self) -> String {
42        let mut index_key = String::new();
43        index_key.push_str("v1|");
44        push_index_key_part(&mut index_key, 'm', &self.manifest_hash);
45        push_index_key_part(&mut index_key, 'c', &self.content_hash);
46        match &self.grant_scope {
47            Some(scope) => push_index_key_part(&mut index_key, 's', scope),
48            None => index_key.push('n'),
49        }
50        index_key
51    }
52
53    /// Whether the grant scope declares encrypted-at-rest content.
54    #[must_use]
55    pub fn declares_encrypted_content(&self) -> bool {
56        self.grant_scope
57            .as_deref()
58            .is_some_and(scope_declares_encrypted_content)
59    }
60}
61
62fn push_index_key_part(index_key: &mut String, label: char, value: &str) {
63    index_key.push(label);
64    index_key.push(':');
65    index_key.push_str(&value.len().to_string());
66    index_key.push(':');
67    index_key.push_str(value);
68    index_key.push('|');
69}
70
71/// Cached content entry with metadata and access tracking.
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct CacheEntry {
74    /// Cache key identifying this entry.
75    pub key: CacheKey,
76    /// Size of cached content in bytes.
77    pub size_bytes: u64,
78    /// When this entry was first cached.
79    pub created_at: SystemTime,
80    /// When this entry was last accessed.
81    pub last_accessed: SystemTime,
82    /// Number of times this entry has been accessed.
83    pub access_count: u64,
84    /// Time-to-live for this entry.
85    pub ttl: Duration,
86    /// Whether this content is encrypted.
87    pub encrypted: bool,
88    /// Storage location (file path, in-memory, etc.).
89    pub storage_location: StorageLocation,
90    /// Verification status and proof metadata.
91    pub verification: VerificationMetadata,
92}
93
94/// Storage location for cached content.
95#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
96pub enum StorageLocation {
97    /// Stored in a file on disk.
98    File(PathBuf),
99    /// Stored in memory (for small, hot content) with lookup key.
100    Memory(String),
101    /// Stored in external location (relay, CDN, etc.).
102    External(String),
103}
104
105fn scope_declares_encrypted_content(scope: &str) -> bool {
106    scope
107        .split(|ch: char| !(ch.is_ascii_alphanumeric() || ch == '-'))
108        .any(|token| {
109            matches!(
110                token.to_ascii_lowercase().as_str(),
111                "encrypted" | "ciphertext" | "e2e" | "end-to-end" | "sealed"
112            )
113        })
114}
115
116fn content_has_encrypted_envelope(content: &[u8]) -> bool {
117    let Ok(value) = serde_json::from_slice::<serde_json::Value>(content) else {
118        return false;
119    };
120    let Some(object) = value.as_object() else {
121        return false;
122    };
123
124    let has_ciphertext = object.contains_key("ciphertext") || object.contains_key("encrypted_data");
125    let has_nonce = object.contains_key("nonce") || object.contains_key("iv");
126    let has_tag = object.contains_key("tag") || object.contains_key("auth_tag");
127    let has_algorithm = object
128        .get("algorithm")
129        .or_else(|| object.get("cipher"))
130        .and_then(serde_json::Value::as_str)
131        .is_some_and(|algorithm| {
132            let algorithm = algorithm.to_ascii_lowercase();
133            algorithm.contains("aes") || algorithm.contains("chacha") || algorithm.contains("gcm")
134        });
135
136    has_ciphertext && has_nonce && has_tag && has_algorithm
137}
138
139fn derive_cache_entry_encryption_status(key: &CacheKey, content: &[u8]) -> bool {
140    key.declares_encrypted_content() || content_has_encrypted_envelope(content)
141}
142
143/// Verification metadata for cached content.
144#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct VerificationMetadata {
146    /// Whether content hash has been verified.
147    pub content_verified: bool,
148    /// Whether manifest signature has been verified.
149    pub manifest_verified: bool,
150    /// Proof bundle location if available.
151    pub proof_location: Option<String>,
152    /// Verification timestamp.
153    pub verified_at: Option<SystemTime>,
154}
155
156/// Cache configuration and policies.
157#[derive(Debug, Clone, Serialize, Deserialize)]
158pub struct CacheConfig {
159    /// Maximum total cache size in bytes.
160    pub max_size_bytes: u64,
161    /// Maximum number of entries.
162    pub max_entries: usize,
163    /// Default TTL for new entries.
164    pub default_ttl: Duration,
165    /// Eviction policy to use when cache is full.
166    pub eviction_policy: EvictionPolicy,
167    /// Whether to allow plaintext content in shared caches.
168    pub allow_plaintext_shared: bool,
169    /// Storage root directory for file-based cache.
170    pub storage_root: PathBuf,
171    /// Whether to enable cache compression.
172    pub compression_enabled: bool,
173}
174
175impl Default for CacheConfig {
176    fn default() -> Self {
177        Self {
178            max_size_bytes: 1_073_741_824, // 1 GiB
179            max_entries: 10_000,
180            default_ttl: Duration::from_secs(24 * 60 * 60), // 24 hours
181            eviction_policy: EvictionPolicy::LeastRecentlyUsed,
182            allow_plaintext_shared: false, // Secure by default
183            storage_root: PathBuf::from(".cache"),
184            compression_enabled: true,
185        }
186    }
187}
188
189/// Cache eviction policies.
190#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
191#[serde(rename_all = "snake_case")]
192pub enum EvictionPolicy {
193    /// Evict least recently used entries first.
194    LeastRecentlyUsed,
195    /// Evict least frequently used entries first.
196    LeastFrequentlyUsed,
197    /// Evict entries with shortest remaining TTL first.
198    ShortestTtl,
199    /// Evict largest entries first to free most space.
200    LargestFirst,
201    /// Hybrid policy considering size, age, and access frequency.
202    Hybrid,
203}
204
205/// Cache metrics and statistics.
206#[derive(Debug, Default, Clone, Serialize, Deserialize)]
207pub struct CacheMetrics {
208    /// Number of cache hits.
209    pub hits: u64,
210    /// Number of cache misses.
211    pub misses: u64,
212    /// Number of entries evicted.
213    pub evictions: u64,
214    /// Number of verification failures.
215    pub verification_failures: u64,
216    /// Total bytes stored.
217    pub total_bytes: u64,
218    /// Number of entries.
219    pub entry_count: usize,
220    /// Cache hit ratio (0.0 to 1.0).
221    pub hit_ratio: f64,
222}
223
224impl CacheMetrics {
225    /// Update hit ratio based on current hits and misses.
226    pub fn update_hit_ratio(&mut self) {
227        let total = self.hits + self.misses;
228        self.hit_ratio = if total > 0 {
229            self.hits as f64 / total as f64
230        } else {
231            0.0
232        };
233    }
234
235    /// Record a cache hit.
236    pub fn record_hit(&mut self) {
237        self.hits += 1;
238        self.update_hit_ratio();
239    }
240
241    /// Record a cache miss.
242    pub fn record_miss(&mut self) {
243        self.misses += 1;
244        self.update_hit_ratio();
245    }
246
247    /// Record an eviction.
248    pub fn record_eviction(&mut self, size_bytes: u64) {
249        self.evictions += 1;
250        self.total_bytes = self.total_bytes.saturating_sub(size_bytes);
251        self.entry_count = self.entry_count.saturating_sub(1);
252    }
253}
254
255/// ATP cache implementation.
256#[derive(Debug)]
257pub struct AtpCache {
258    /// Cache configuration.
259    config: CacheConfig,
260    /// Cache entry index by cache key.
261    entries: HashMap<String, CacheEntry>,
262    /// Byte storage for entries whose storage location is in-memory.
263    memory_storage: HashMap<String, Vec<u8>>,
264    /// LRU tracking for eviction policy.
265    access_order: Vec<String>,
266    /// Cache metrics and statistics.
267    metrics: CacheMetrics,
268    /// Trust boundary policy.
269    trust_policy: trust::TrustPolicy,
270}
271
272impl AtpCache {
273    /// Create a new ATP cache with the given configuration.
274    pub fn new(config: CacheConfig) -> Self {
275        Self {
276            config,
277            entries: HashMap::new(),
278            memory_storage: HashMap::new(),
279            access_order: Vec::new(),
280            metrics: CacheMetrics::default(),
281            trust_policy: trust::TrustPolicy::default(),
282        }
283    }
284
285    /// Get cached content if available and authorized.
286    pub fn get(&mut self, key: &CacheKey) -> Result<Option<Vec<u8>>, CacheError> {
287        let index_key = key.as_index_key();
288
289        // Atomically check TTL and remove if expired to prevent TOCTOU races
290        let entry = if let Some(entry) = self.entries.get(&index_key) {
291            // Check TTL atomically with entry access
292            let elapsed = entry.created_at.elapsed().unwrap_or(Duration::MAX);
293            if elapsed > entry.ttl {
294                if let Some(entry) = self.entries.remove(&index_key) {
295                    self.remove_expired_entry(&index_key, entry);
296                }
297                self.metrics.record_miss();
298                return Ok(None);
299            }
300            entry
301        } else {
302            self.metrics.record_miss();
303            return Ok(None);
304        };
305
306        // Check trust policy
307        self.trust_policy.check_access(key)?;
308
309        // Clone storage location to avoid borrow conflict
310        let storage_location = entry.storage_location.clone();
311
312        // Load content from storage
313        let content = match &storage_location {
314            StorageLocation::File(path) => match std::fs::read(path) {
315                Ok(content) => Some(content),
316                Err(_) => {
317                    // File missing, remove from cache
318                    self.remove(key)?;
319                    self.metrics.record_miss();
320                    None
321                }
322            },
323            StorageLocation::Memory(memory_key) => match self.memory_storage.get(memory_key) {
324                Some(content) => Some(content.clone()),
325                None => {
326                    self.remove(key)?;
327                    self.metrics.record_miss();
328                    None
329                }
330            },
331            StorageLocation::External(location) => {
332                let content = retrieve_external_cache_location(location)?;
333                Some(content)
334            }
335        };
336
337        if let Some(content) = content {
338            self.update_access(&index_key);
339            self.metrics.record_hit();
340            Ok(Some(content))
341        } else {
342            Ok(None)
343        }
344    }
345
346    /// Store content in cache with verification.
347    pub fn put(&mut self, key: CacheKey, content: &[u8]) -> Result<(), CacheError> {
348        // Verify content hash matches
349        let actual_hash = self.compute_content_hash(content);
350        if actual_hash != key.content_hash {
351            return Err(CacheError::VerificationFailed(
352                "Content hash mismatch".to_string(),
353            ));
354        }
355
356        let encrypted = derive_cache_entry_encryption_status(&key, content);
357
358        // Check trust policy for storage before mutating cache state.
359        self.trust_policy.check_storage(&key, encrypted)?;
360
361        let index_key = key.as_index_key();
362        let size_bytes = content.len() as u64;
363        let replaced_entry = self.entries.get(&index_key).cloned();
364
365        // Check if we need to evict entries
366        if replaced_entry.is_none() {
367            self.ensure_space_for(size_bytes)?;
368        }
369
370        // Choose storage location
371        let storage_location = if size_bytes < 64 * 1024 {
372            // Small content in memory - generate memory key from cache key
373            let memory_key = index_key.clone();
374            self.memory_storage
375                .insert(memory_key.clone(), content.to_vec());
376            StorageLocation::Memory(memory_key)
377        } else {
378            // Store in file
379            let filename = format!("{}.cache", actual_hash);
380            let path = self.config.storage_root.join(filename);
381
382            // Create directory if needed
383            if let Some(parent) = path.parent() {
384                std::fs::create_dir_all(parent).map_err(|e| CacheError::Storage(e.to_string()))?;
385            }
386
387            // Write content to file
388            std::fs::write(&path, content).map_err(|e| CacheError::Storage(e.to_string()))?;
389
390            StorageLocation::File(path)
391        };
392
393        // Create cache entry
394        let now = SystemTime::now();
395        let entry = CacheEntry {
396            key: key.clone(),
397            size_bytes,
398            created_at: now,
399            last_accessed: now,
400            access_count: 0,
401            ttl: self.config.default_ttl,
402            encrypted,
403            storage_location: storage_location.clone(),
404            verification: VerificationMetadata {
405                content_verified: true,
406                manifest_verified: false,
407                proof_location: None,
408                verified_at: Some(now),
409            },
410        };
411
412        // Store entry
413        self.entries.insert(index_key.clone(), entry);
414        self.access_order.retain(|k| k != &index_key);
415        self.access_order.push(index_key);
416
417        // Update metrics
418        if let Some(replaced_entry) = replaced_entry {
419            if replaced_entry.storage_location != storage_location {
420                self.remove_storage_location(&replaced_entry.storage_location);
421            }
422            self.metrics.total_bytes = self
423                .metrics
424                .total_bytes
425                .saturating_sub(replaced_entry.size_bytes)
426                .saturating_add(size_bytes);
427        } else {
428            self.metrics.total_bytes = self.metrics.total_bytes.saturating_add(size_bytes);
429            self.metrics.entry_count = self.metrics.entry_count.saturating_add(1);
430        }
431
432        Ok(())
433    }
434
435    /// Remove an entry from the cache.
436    pub fn remove(&mut self, key: &CacheKey) -> Result<(), CacheError> {
437        let index_key = key.as_index_key();
438
439        if let Some(entry) = self.entries.remove(&index_key) {
440            // Remove from access order
441            self.access_order.retain(|k| k != &index_key);
442
443            // Remove backing data if stored in this cache instance.
444            self.remove_storage_location(&entry.storage_location);
445
446            // Update metrics
447            self.metrics.record_eviction(entry.size_bytes);
448        }
449
450        Ok(())
451    }
452
453    /// Get cache metrics.
454    #[must_use]
455    pub const fn metrics(&self) -> &CacheMetrics {
456        &self.metrics
457    }
458
459    /// Compute content hash for verification.
460    fn compute_content_hash(&self, content: &[u8]) -> String {
461        use sha2::{Digest, Sha256};
462
463        let mut hasher = Sha256::new();
464        hasher.update(content);
465        hex::encode(hasher.finalize())
466    }
467
468    /// Update access tracking for LRU eviction.
469    fn update_access(&mut self, index_key: &str) {
470        // Move to end of access order
471        self.access_order.retain(|k| k != index_key);
472        self.access_order.push(index_key.to_string());
473
474        // Update entry access count
475        if let Some(entry) = self.entries.get_mut(index_key) {
476            entry.last_accessed = SystemTime::now();
477            entry.access_count = entry.access_count.saturating_add(1);
478        }
479    }
480
481    /// Ensure space for new content by evicting if necessary.
482    fn ensure_space_for(&mut self, size_bytes: u64) -> Result<(), CacheError> {
483        // Check if we need to evict
484        while (self.metrics.total_bytes.saturating_add(size_bytes) > self.config.max_size_bytes)
485            || (self.metrics.entry_count >= self.config.max_entries)
486        {
487            if self.access_order.is_empty() {
488                return Err(CacheError::InsufficientSpace);
489            }
490
491            // Evict oldest entry (LRU)
492            let to_evict = self.access_order.remove(0);
493            if let Some(entry) = self.entries.remove(&to_evict) {
494                // Remove backing data if needed
495                self.remove_storage_location(&entry.storage_location);
496
497                self.metrics.record_eviction(entry.size_bytes);
498            }
499        }
500
501        Ok(())
502    }
503
504    fn remove_expired_entry(&mut self, index_key: &str, entry: CacheEntry) {
505        self.remove_storage_location(&entry.storage_location);
506        self.access_order.retain(|k| k != index_key);
507        self.metrics.record_eviction(entry.size_bytes);
508    }
509
510    fn remove_storage_location(&mut self, storage_location: &StorageLocation) {
511        match storage_location {
512            StorageLocation::File(path) => {
513                let _ = std::fs::remove_file(path);
514            }
515            StorageLocation::Memory(memory_key) => {
516                self.memory_storage.remove(memory_key);
517            }
518            StorageLocation::External(_) => {}
519        }
520    }
521}
522
523fn retrieve_external_cache_location(location: &str) -> Result<Vec<u8>, CacheError> {
524    if let Some(path) = location.strip_prefix("file://") {
525        return std::fs::read(path).map_err(|error| {
526            CacheError::External(format!(
527                "failed to read external file cache location: {error}"
528            ))
529        });
530    }
531    let path = PathBuf::from(location);
532    if path.is_absolute() {
533        return std::fs::read(path).map_err(|error| {
534            CacheError::External(format!("failed to read external cache path: {error}"))
535        });
536    }
537    Err(CacheError::External(format!(
538        "external cache location requires a configured backend: {location}"
539    )))
540}
541
542/// Cache operation errors.
543#[derive(Debug, thiserror::Error)]
544pub enum CacheError {
545    #[error("Storage error: {0}")]
546    Storage(String),
547
548    #[error("Verification failed: {0}")]
549    VerificationFailed(String),
550
551    #[error("Trust policy violation: {0}")]
552    TrustViolation(String),
553
554    #[error("External cache error: {0}")]
555    External(String),
556
557    #[error("Insufficient cache space")]
558    InsufficientSpace,
559
560    #[error("Identity error: {0}")]
561    Identity(#[from] IdentityError),
562}
563
564#[cfg(test)]
565mod tests {
566    use super::*;
567    use std::time::Duration;
568
569    #[test]
570    fn cache_key_index_key_generation() {
571        let key = CacheKey::new(
572            "manifest123".to_string(),
573            "content456".to_string(),
574            Some("scope789".to_string()),
575        );
576        assert_eq!(
577            key.as_index_key(),
578            "v1|m:11:manifest123|c:10:content456|s:8:scope789|"
579        );
580
581        let key_no_scope = CacheKey::new("manifest123".to_string(), "content456".to_string(), None);
582        assert_eq!(
583            key_no_scope.as_index_key(),
584            "v1|m:11:manifest123|c:10:content456|n"
585        );
586    }
587
588    #[test]
589    fn cache_key_index_key_is_not_delimiter_collision_prone() {
590        let scoped = CacheKey::new("a".to_string(), "b".to_string(), Some("c".to_string()));
591        let unscoped_with_delimiter = CacheKey::new("a".to_string(), "b:c".to_string(), None);
592
593        assert_ne!(
594            scoped.as_index_key(),
595            unscoped_with_delimiter.as_index_key()
596        );
597    }
598
599    #[test]
600    fn cache_metrics_hit_ratio_calculation() {
601        let mut metrics = CacheMetrics::default();
602
603        metrics.record_hit();
604        metrics.record_hit();
605        metrics.record_miss();
606
607        assert_eq!(metrics.hits, 2);
608        assert_eq!(metrics.misses, 1);
609        assert!((metrics.hit_ratio - 0.6667).abs() < 0.001);
610    }
611
612    #[test]
613    fn cache_config_defaults() {
614        let config = CacheConfig::default();
615        assert_eq!(config.max_size_bytes, 1_073_741_824);
616        assert!(!config.allow_plaintext_shared);
617        assert_eq!(config.eviction_policy, EvictionPolicy::LeastRecentlyUsed);
618    }
619
620    #[test]
621    fn cache_basic_put_get() {
622        let mut cache = AtpCache::new(CacheConfig::default());
623        let key = CacheKey::new(
624            "manifest123".to_string(),
625            "d2d2d2d2d2d2d2d2".to_string(), // Intentionally invalid content hash
626            None,
627        );
628        let content = b"test content";
629
630        // This will fail due to hash mismatch, but tests the interface
631        let result = cache.put(key.clone(), content);
632        assert!(result.is_err()); // Should fail due to hash verification
633    }
634
635    fn sha256_hex(content: &[u8]) -> String {
636        use sha2::{Digest, Sha256};
637
638        hex::encode(Sha256::digest(content))
639    }
640
641    #[test]
642    fn cache_put_existing_key_does_not_duplicate_metrics_or_lru_entries() {
643        let mut config = CacheConfig::default();
644        config.max_entries = 1;
645        let mut cache = AtpCache::new(config);
646        let content = b"stable cache content";
647        let key = CacheKey::new("manifest123".to_string(), sha256_hex(content), None);
648
649        cache.put(key.clone(), content).unwrap();
650        cache.put(key.clone(), content).unwrap();
651
652        assert_eq!(cache.metrics().entry_count, 1);
653        assert_eq!(cache.metrics().total_bytes, content.len() as u64);
654        assert_eq!(cache.access_order.len(), 1);
655        assert_eq!(cache.get(&key).unwrap().as_deref(), Some(&content[..]));
656    }
657
658    #[test]
659    fn cache_entry_encryption_status_is_derived_from_scope_or_envelope() {
660        let encrypted_key = CacheKey::new(
661            "manifest123".to_string(),
662            "content456".to_string(),
663            Some("team:engineering:encrypted".to_string()),
664        );
665        assert!(derive_cache_entry_encryption_status(
666            &encrypted_key,
667            b"plaintext"
668        ));
669
670        let envelope_key = CacheKey::new("manifest123".to_string(), "content456".to_string(), None);
671        let envelope = br#"{
672            "algorithm": "aes-256-gcm",
673            "nonce": "000000000000000000000000",
674            "ciphertext": "deadbeef",
675            "tag": "cafebabe"
676        }"#;
677        assert!(derive_cache_entry_encryption_status(
678            &envelope_key,
679            envelope
680        ));
681
682        let plaintext_key =
683            CacheKey::new("manifest123".to_string(), "content456".to_string(), None);
684        assert!(!derive_cache_entry_encryption_status(
685            &plaintext_key,
686            b"plaintext"
687        ));
688    }
689
690    #[test]
691    fn cache_ttl_toctou_fix() {
692        let mut cache = AtpCache::new(CacheConfig::default());
693
694        // Create a cache key
695        let key = CacheKey::new(
696            "manifest123".to_string(),
697            "d2d2d2d2d2d2d2d2".to_string(), // Intentionally invalid content hash
698            None,
699        );
700
701        // Put some content with very short TTL
702        let content = b"test content";
703
704        // Manually add an expired entry to test TTL check
705        let expired_entry = CacheEntry {
706            key: key.clone(),
707            size_bytes: content.len() as u64,
708            created_at: SystemTime::now() - Duration::from_secs(3600), // 1 hour ago
709            last_accessed: SystemTime::now(),
710            access_count: 1,
711            ttl: Duration::from_secs(60), // 1 minute TTL (expired)
712            encrypted: true,
713            storage_location: StorageLocation::Memory("test".to_string()),
714            verification: VerificationMetadata {
715                content_verified: true,
716                manifest_verified: true,
717                proof_location: None,
718                verified_at: Some(SystemTime::now()),
719            },
720        };
721
722        // Insert expired entry directly
723        cache.entries.insert(key.as_index_key(), expired_entry);
724        cache.access_order.push(key.as_index_key());
725        cache
726            .memory_storage
727            .insert("test".to_string(), content.to_vec());
728        cache.metrics.total_bytes = content.len() as u64;
729        cache.metrics.entry_count = 1;
730        assert_eq!(cache.entries.len(), 1);
731
732        // Try to get expired entry - should be atomically removed
733        let result = cache.get(&key);
734        assert!(result.is_ok());
735        assert!(result.unwrap().is_none()); // Should return None for expired entry
736
737        // Entry should be removed from cache
738        assert_eq!(cache.entries.len(), 0);
739        assert_eq!(cache.access_order.len(), 0);
740        assert_eq!(cache.metrics().total_bytes, 0);
741        assert_eq!(cache.metrics().entry_count, 0);
742    }
743
744    #[test]
745    fn cache_eviction_on_size_limit() {
746        let mut config = CacheConfig::default();
747        config.max_size_bytes = 100; // Very small cache
748
749        let cache = AtpCache::new(config);
750        assert_eq!(cache.metrics().total_bytes, 0);
751        assert_eq!(cache.metrics().entry_count, 0);
752    }
753
754    // Golden Artifact Tests for ATP Cache Serialization Stability
755
756    #[test]
757    fn golden_cache_config_default_serialization() {
758        let config = CacheConfig::default();
759        assert_eq!(
760            serde_json::to_value(&config).unwrap(),
761            serde_json::json!({
762                "max_size_bytes": 1_073_741_824_u64,
763                "max_entries": 10_000,
764                "default_ttl": {
765                    "secs": 86_400,
766                    "nanos": 0,
767                },
768                "eviction_policy": "least_recently_used",
769                "allow_plaintext_shared": false,
770                "storage_root": ".cache",
771                "compression_enabled": true,
772            })
773        );
774    }
775
776    #[test]
777    fn golden_cache_config_custom_serialization() {
778        use std::path::PathBuf;
779
780        let config = CacheConfig {
781            max_size_bytes: 512 * 1024 * 1024, // 512 MiB
782            max_entries: 5_000,
783            default_ttl: Duration::from_secs(12 * 60 * 60), // 12 hours
784            eviction_policy: EvictionPolicy::Hybrid,
785            allow_plaintext_shared: true,
786            storage_root: PathBuf::from("/var/cache/atp"),
787            compression_enabled: false,
788        };
789        assert_eq!(
790            serde_json::to_value(&config).unwrap(),
791            serde_json::json!({
792                "max_size_bytes": 536_870_912_u64,
793                "max_entries": 5_000,
794                "default_ttl": {
795                    "secs": 43_200,
796                    "nanos": 0,
797                },
798                "eviction_policy": "hybrid",
799                "allow_plaintext_shared": true,
800                "storage_root": "/var/cache/atp",
801                "compression_enabled": false,
802            })
803        );
804    }
805
806    #[test]
807    fn golden_cache_key_serialization() {
808        // Test cache key with scope
809        let key_with_scope = CacheKey::new(
810            "sha256:a1b2c3d4e5f6g7h8".to_string(),
811            "sha256:1234567890abcdef".to_string(),
812            Some("team:engineering".to_string()),
813        );
814        assert_eq!(
815            serde_json::to_value(&key_with_scope).unwrap(),
816            serde_json::json!({
817                "manifest_hash": "sha256:a1b2c3d4e5f6g7h8",
818                "content_hash": "sha256:1234567890abcdef",
819                "grant_scope": "team:engineering",
820            })
821        );
822
823        // Test cache key without scope
824        let key_no_scope = CacheKey::new(
825            "sha256:fedcba0987654321".to_string(),
826            "sha256:abcdef1234567890".to_string(),
827            None,
828        );
829        assert_eq!(
830            serde_json::to_value(&key_no_scope).unwrap(),
831            serde_json::json!({
832                "manifest_hash": "sha256:fedcba0987654321",
833                "content_hash": "sha256:abcdef1234567890",
834                "grant_scope": null,
835            })
836        );
837    }
838
839    #[test]
840    fn golden_eviction_policy_serialization() {
841        let policies = vec![
842            EvictionPolicy::LeastRecentlyUsed,
843            EvictionPolicy::LeastFrequentlyUsed,
844            EvictionPolicy::ShortestTtl,
845            EvictionPolicy::LargestFirst,
846            EvictionPolicy::Hybrid,
847        ];
848
849        assert_eq!(
850            serde_json::to_value(&policies).unwrap(),
851            serde_json::json!([
852                "least_recently_used",
853                "least_frequently_used",
854                "shortest_ttl",
855                "largest_first",
856                "hybrid",
857            ])
858        );
859    }
860
861    #[test]
862    fn golden_cache_metrics_serialization() {
863        let mut metrics = CacheMetrics::default();
864        metrics.hits = 1500;
865        metrics.misses = 300;
866        metrics.evictions = 25;
867        metrics.verification_failures = 2;
868        metrics.total_bytes = 1024 * 1024; // 1 MiB
869        metrics.entry_count = 150;
870        metrics.update_hit_ratio();
871
872        assert_eq!(
873            serde_json::to_value(&metrics).unwrap(),
874            serde_json::json!({
875                "hits": 1_500,
876                "misses": 300,
877                "evictions": 25,
878                "verification_failures": 2,
879                "total_bytes": 1_048_576_u64,
880                "entry_count": 150,
881                "hit_ratio": 0.8333333333333334,
882            })
883        );
884    }
885}