Skip to main content

trae_agent_rs_core/config/
cache.rs

1//! Configuration cache for storing user preferences
2
3use serde::{Deserialize, Serialize};
4use std::path::{Path, PathBuf};
5use tokio::fs;
6use crate::error::Result;
7
8/// Configuration cache to store user's provider selection
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct ConfigCache {
11    /// Last selected provider
12    pub selected_provider: Option<String>,
13    
14    /// Timestamp of last selection
15    pub last_updated: Option<u64>,
16    
17    /// Cache version for compatibility
18    pub version: u32,
19}
20
21impl ConfigCache {
22    /// Create a new empty cache
23    pub fn new() -> Self {
24        Self {
25            selected_provider: None,
26            last_updated: None,
27            version: 1,
28        }
29    }
30    
31    /// Get the default cache file path
32    pub fn default_cache_path() -> PathBuf {
33        let mut path = dirs::config_dir()
34            .unwrap_or_else(|| PathBuf::from("."));
35        path.push("trae-agent");
36        path.push("cache.json");
37        path
38    }
39    
40    /// Load cache from file
41    pub async fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
42        let path = path.as_ref();
43        
44        if !path.exists() {
45            return Ok(Self::new());
46        }
47        
48        let content = fs::read_to_string(path).await?;
49        let cache: ConfigCache = serde_json::from_str(&content)
50            .unwrap_or_else(|_| Self::new());
51            
52        Ok(cache)
53    }
54    
55    /// Save cache to file
56    pub async fn save<P: AsRef<Path>>(&self, path: P) -> Result<()> {
57        let path = path.as_ref();
58        
59        // Create parent directory if it doesn't exist
60        if let Some(parent) = path.parent() {
61            fs::create_dir_all(parent).await?;
62        }
63        
64        let content = serde_json::to_string_pretty(self)?;
65        fs::write(path, content).await?;
66        
67        Ok(())
68    }
69    
70    /// Update the selected provider
71    pub fn set_selected_provider(&mut self, provider: String) {
72        self.selected_provider = Some(provider);
73        self.last_updated = Some(
74            std::time::SystemTime::now()
75                .duration_since(std::time::UNIX_EPOCH)
76                .unwrap_or_default()
77                .as_secs()
78        );
79    }
80    
81    /// Get the selected provider
82    pub fn get_selected_provider(&self) -> Option<&String> {
83        self.selected_provider.as_ref()
84    }
85    
86    /// Check if the cache is expired (older than 30 days)
87    pub fn is_expired(&self) -> bool {
88        if let Some(last_updated) = self.last_updated {
89            let now = std::time::SystemTime::now()
90                .duration_since(std::time::UNIX_EPOCH)
91                .unwrap_or_default()
92                .as_secs();
93            
94            let thirty_days = 30 * 24 * 60 * 60; // 30 days in seconds
95            now - last_updated > thirty_days
96        } else {
97            true
98        }
99    }
100    
101    /// Clear the cache
102    pub fn clear(&mut self) {
103        self.selected_provider = None;
104        self.last_updated = None;
105    }
106}
107
108impl Default for ConfigCache {
109    fn default() -> Self {
110        Self::new()
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117    use tempfile::tempdir;
118    
119    #[tokio::test]
120    async fn test_cache_save_load() {
121        let temp_dir = tempdir().unwrap();
122        let cache_path = temp_dir.path().join("test_cache.json");
123        
124        let mut cache = ConfigCache::new();
125        cache.set_selected_provider("openai".to_string());
126        
127        // Save cache
128        cache.save(&cache_path).await.unwrap();
129        
130        // Load cache
131        let loaded_cache = ConfigCache::load(&cache_path).await.unwrap();
132        
133        assert_eq!(loaded_cache.selected_provider, Some("openai".to_string()));
134        assert!(loaded_cache.last_updated.is_some());
135    }
136    
137    #[tokio::test]
138    async fn test_cache_load_nonexistent() {
139        let temp_dir = tempdir().unwrap();
140        let cache_path = temp_dir.path().join("nonexistent.json");
141        
142        let cache = ConfigCache::load(&cache_path).await.unwrap();
143        
144        assert_eq!(cache.selected_provider, None);
145        assert_eq!(cache.last_updated, None);
146    }
147    
148    #[test]
149    fn test_cache_expiry() {
150        let mut cache = ConfigCache::new();
151        
152        // New cache should be expired
153        assert!(cache.is_expired());
154        
155        // Set provider (this sets current timestamp)
156        cache.set_selected_provider("anthropic".to_string());
157        
158        // Should not be expired now
159        assert!(!cache.is_expired());
160        
161        // Manually set old timestamp
162        cache.last_updated = Some(0); // Unix epoch
163        
164        // Should be expired now
165        assert!(cache.is_expired());
166    }
167}