use anyhow::{Context, Result};
use serde_json;
use super::AppConfig;
use crate::spec_ai_config::persistence::Persistence;
const CONFIG_CACHE_KEY: &str = "effective_config";
const POLICIES_CACHE_KEY: &str = "effective_policies";
pub struct ConfigCache {
persistence: Persistence,
}
impl ConfigCache {
pub fn new(persistence: Persistence) -> Self {
Self { persistence }
}
pub fn store_effective_config(&self, config: &AppConfig) -> Result<()> {
let value = serde_json::to_value(config).context("serializing config to JSON")?;
self.persistence
.policy_upsert(CONFIG_CACHE_KEY, &value)
.context("storing effective config in cache")
}
pub fn load_effective_config(&self) -> Result<Option<AppConfig>> {
if let Some(entry) = self.persistence.policy_get(CONFIG_CACHE_KEY)? {
let config: AppConfig =
serde_json::from_value(entry.value).context("deserializing cached config")?;
Ok(Some(config))
} else {
Ok(None)
}
}
pub fn store_effective_policies(&self, policies: &serde_json::Value) -> Result<()> {
self.persistence
.policy_upsert(POLICIES_CACHE_KEY, policies)
.context("storing effective policies in cache")
}
pub fn load_effective_policies(&self) -> Result<Option<serde_json::Value>> {
if let Some(entry) = self.persistence.policy_get(POLICIES_CACHE_KEY)? {
Ok(Some(entry.value))
} else {
Ok(None)
}
}
pub fn has_config_changed(&self, current: &AppConfig) -> Result<bool> {
if let Some(cached) = self.load_effective_config()? {
let current_json =
serde_json::to_value(current).context("serializing current config")?;
let cached_json = serde_json::to_value(&cached).context("serializing cached config")?;
Ok(current_json != cached_json)
} else {
Ok(true)
}
}
pub fn diff_summary(&self, current: &AppConfig) -> Result<Vec<String>> {
let mut changes = Vec::new();
if let Some(cached) = self.load_effective_config()? {
if current.model.provider != cached.model.provider {
changes.push(format!(
"Model provider: {} -> {}",
cached.model.provider, current.model.provider
));
}
if current.model.temperature != cached.model.temperature {
changes.push(format!(
"Temperature: {} -> {}",
cached.model.temperature, current.model.temperature
));
}
if current.logging.level != cached.logging.level {
changes.push(format!(
"Logging level: {} -> {}",
cached.logging.level, current.logging.level
));
}
if current.database.path != cached.database.path {
changes.push(format!(
"Database path: {} -> {}",
cached.database.path.display(),
current.database.path.display()
));
}
if current.agents.len() != cached.agents.len() {
changes.push(format!(
"Number of agents: {} -> {}",
cached.agents.len(),
current.agents.len()
));
}
if current.default_agent != cached.default_agent {
changes.push(format!(
"Default agent: {:?} -> {:?}",
cached.default_agent, current.default_agent
));
}
} else {
changes.push("No cached config found (first run or cache cleared)".to_string());
}
Ok(changes)
}
pub fn clear(&self) -> Result<()> {
self.persistence
.policy_upsert(CONFIG_CACHE_KEY, &serde_json::Value::Null)?;
self.persistence
.policy_upsert(POLICIES_CACHE_KEY, &serde_json::Value::Null)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
fn create_test_config() -> AppConfig {
use crate::spec_ai_config::config::{
AudioConfig, AuthConfig, DatabaseConfig, LoggingConfig, ModelConfig, PluginConfig,
SyncConfig, UiConfig,
};
use std::collections::HashMap;
use std::path::PathBuf;
AppConfig {
database: DatabaseConfig {
path: PathBuf::from("/tmp/test.db"),
},
model: ModelConfig {
provider: "test".to_string(),
model_name: None,
code_model: None,
embeddings_model: None,
api_key_source: None,
temperature: 0.5,
},
ui: UiConfig {
prompt: "> ".to_string(),
theme: "default".to_string(),
},
logging: LoggingConfig {
level: "info".to_string(),
},
audio: AudioConfig::default(),
mesh: crate::spec_ai_config::config::MeshConfig::default(),
plugins: PluginConfig::default(),
sync: SyncConfig::default(),
auth: AuthConfig::default(),
agents: HashMap::new(),
default_agent: None,
}
}
#[test]
fn test_store_and_load_config() {
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().join("test.duckdb");
let persistence = Persistence::new(&db_path).unwrap();
let cache = ConfigCache::new(persistence);
let config = create_test_config();
cache.store_effective_config(&config).unwrap();
let loaded = cache.load_effective_config().unwrap();
assert!(loaded.is_some());
let loaded_config = loaded.unwrap();
assert_eq!(loaded_config.model.provider, "test");
assert_eq!(loaded_config.model.temperature, 0.5);
}
#[test]
fn test_load_nonexistent_config() {
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().join("test.duckdb");
let persistence = Persistence::new(&db_path).unwrap();
let cache = ConfigCache::new(persistence);
let loaded = cache.load_effective_config().unwrap();
assert!(loaded.is_none());
}
#[test]
fn test_store_and_load_policies() {
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().join("test.duckdb");
let persistence = Persistence::new(&db_path).unwrap();
let cache = ConfigCache::new(persistence);
let policies = serde_json::json!({
"allow": ["tool1", "tool2"],
"deny": ["tool3"]
});
cache.store_effective_policies(&policies).unwrap();
let loaded = cache.load_effective_policies().unwrap();
assert!(loaded.is_some());
assert_eq!(loaded.unwrap(), policies);
}
#[test]
fn test_has_config_changed() {
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().join("test.duckdb");
let persistence = Persistence::new(&db_path).unwrap();
let cache = ConfigCache::new(persistence);
let config1 = create_test_config();
assert!(cache.has_config_changed(&config1).unwrap());
cache.store_effective_config(&config1).unwrap();
assert!(!cache.has_config_changed(&config1).unwrap());
let mut config2 = config1.clone();
config2.model.temperature = 0.9;
assert!(cache.has_config_changed(&config2).unwrap());
}
#[test]
fn test_diff_summary() {
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().join("test.duckdb");
let persistence = Persistence::new(&db_path).unwrap();
let cache = ConfigCache::new(persistence);
let mut config1 = create_test_config();
cache.store_effective_config(&config1).unwrap();
config1.model.provider = "new_provider".to_string();
config1.model.temperature = 0.9;
let diff = cache.diff_summary(&config1).unwrap();
assert!(diff.len() >= 2);
assert!(diff.iter().any(|s| s.contains("Model provider")));
assert!(diff.iter().any(|s| s.contains("Temperature")));
}
#[test]
fn test_clear_cache() {
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().join("test.duckdb");
let persistence = Persistence::new(&db_path).unwrap();
let cache = ConfigCache::new(persistence);
let config = create_test_config();
let policies = serde_json::json!({"test": "value"});
cache.store_effective_config(&config).unwrap();
cache.store_effective_policies(&policies).unwrap();
assert!(cache.load_effective_config().unwrap().is_some());
assert!(cache.load_effective_policies().unwrap().is_some());
cache.clear().unwrap();
let _ = cache.load_effective_config();
}
#[test]
fn test_idempotent_store() {
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().join("test.duckdb");
let persistence = Persistence::new(&db_path).unwrap();
let cache = ConfigCache::new(persistence);
let config = create_test_config();
cache.store_effective_config(&config).unwrap();
cache.store_effective_config(&config).unwrap();
cache.store_effective_config(&config).unwrap();
let loaded = cache.load_effective_config().unwrap();
assert!(loaded.is_some());
assert_eq!(loaded.unwrap().model.provider, "test");
}
}