Skip to main content

llm_optimizer_processor/actuator/
configuration.rs

1//! Configuration Manager for Actuator
2//!
3//! This module provides configuration management capabilities including:
4//! - Configuration application and reversion
5//! - Snapshot creation and restoration
6//! - Configuration versioning
7//! - Validation before application
8//! - Audit logging for compliance
9//! - Dry-run mode for safe testing
10
11use async_trait::async_trait;
12use chrono::{DateTime, Utc};
13use serde::{Deserialize, Serialize};
14use std::collections::HashMap;
15use tracing::{debug, error, info, warn};
16
17use super::config::ConfigurationManagementConfig;
18use super::error::{ActuatorError, ActuatorResult};
19use super::traits::ConfigurationManager;
20use super::types::ConfigurationSnapshot;
21use crate::decision::ConfigChange;
22
23/// Configuration Manager implementation
24pub struct ConfigurationManagerImpl {
25    /// Configuration
26    config: ConfigurationManagementConfig,
27
28    /// Current configuration values
29    current_config: HashMap<String, serde_json::Value>,
30
31    /// Configuration snapshots (snapshot_id -> snapshot)
32    snapshots: HashMap<String, ConfigurationSnapshot>,
33
34    /// Configuration history (version -> changes)
35    history: Vec<ConfigurationVersion>,
36
37    /// Current version number
38    current_version: u64,
39
40    /// Audit log entries
41    audit_log: Vec<AuditLogEntry>,
42
43    /// Dry-run mode flag
44    dry_run_mode: bool,
45
46    /// Configuration backends for different config types
47    backends: HashMap<String, Box<dyn ConfigurationBackend>>,
48}
49
50/// Configuration version entry
51#[derive(Debug, Clone, Serialize, Deserialize)]
52struct ConfigurationVersion {
53    /// Version number
54    version: u64,
55
56    /// Timestamp
57    timestamp: DateTime<Utc>,
58
59    /// Changes applied in this version
60    changes: Vec<ConfigChange>,
61
62    /// Who applied the changes
63    applied_by: String,
64
65    /// Description
66    description: String,
67
68    /// Snapshot ID before this version (for rollback)
69    snapshot_id: String,
70}
71
72/// Audit log entry
73#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct AuditLogEntry {
75    /// Entry ID
76    pub id: String,
77
78    /// Timestamp
79    pub timestamp: DateTime<Utc>,
80
81    /// Operation type (apply, revert, snapshot, restore)
82    pub operation: String,
83
84    /// User/system that performed the operation
85    pub actor: String,
86
87    /// Changes involved
88    pub changes: Vec<ConfigChange>,
89
90    /// Success or failure
91    pub success: bool,
92
93    /// Error message if failed
94    pub error: Option<String>,
95
96    /// Additional metadata
97    pub metadata: HashMap<String, serde_json::Value>,
98}
99
100/// Configuration diff entry
101#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct ConfigurationDiff {
103    /// Path to the configuration
104    pub path: String,
105
106    /// Old value (if any)
107    pub old_value: Option<serde_json::Value>,
108
109    /// New value
110    pub new_value: serde_json::Value,
111
112    /// Type of change (add, update, delete)
113    pub change_type: DiffType,
114}
115
116/// Type of configuration change
117#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
118pub enum DiffType {
119    /// Adding a new configuration
120    Add,
121    /// Updating existing configuration
122    Update,
123    /// Deleting configuration
124    Delete,
125}
126
127/// Trait for configuration backends
128#[async_trait]
129pub trait ConfigurationBackend: Send + Sync {
130    /// Get configuration value
131    async fn get(&self, path: &str) -> ActuatorResult<Option<serde_json::Value>>;
132
133    /// Set configuration value
134    async fn set(&mut self, path: &str, value: serde_json::Value) -> ActuatorResult<()>;
135
136    /// Delete configuration value
137    async fn delete(&mut self, path: &str) -> ActuatorResult<()>;
138
139    /// Validate configuration
140    async fn validate(&self, path: &str, value: &serde_json::Value) -> ActuatorResult<()>;
141
142    /// Get all configuration values
143    async fn get_all(&self) -> ActuatorResult<HashMap<String, serde_json::Value>>;
144}
145
146/// In-memory configuration backend (for testing and simple cases)
147#[derive(Debug, Clone)]
148pub struct InMemoryBackend {
149    config: HashMap<String, serde_json::Value>,
150}
151
152impl InMemoryBackend {
153    pub fn new() -> Self {
154        Self {
155            config: HashMap::new(),
156        }
157    }
158
159    pub fn with_initial_config(config: HashMap<String, serde_json::Value>) -> Self {
160        Self { config }
161    }
162}
163
164impl Default for InMemoryBackend {
165    fn default() -> Self {
166        Self::new()
167    }
168}
169
170#[async_trait]
171impl ConfigurationBackend for InMemoryBackend {
172    async fn get(&self, path: &str) -> ActuatorResult<Option<serde_json::Value>> {
173        Ok(self.config.get(path).cloned())
174    }
175
176    async fn set(&mut self, path: &str, value: serde_json::Value) -> ActuatorResult<()> {
177        self.config.insert(path.to_string(), value);
178        Ok(())
179    }
180
181    async fn delete(&mut self, path: &str) -> ActuatorResult<()> {
182        self.config.remove(path);
183        Ok(())
184    }
185
186    async fn validate(&self, path: &str, value: &serde_json::Value) -> ActuatorResult<()> {
187        // Basic validation - check that value is not null
188        if value.is_null() {
189            return Err(ActuatorError::ValidationFailed(format!(
190                "Configuration value at '{}' cannot be null",
191                path
192            )));
193        }
194
195        // Path-specific validation
196        if path.contains("..") || path.starts_with('/') {
197            return Err(ActuatorError::ValidationFailed(format!(
198                "Invalid configuration path: '{}'",
199                path
200            )));
201        }
202
203        Ok(())
204    }
205
206    async fn get_all(&self) -> ActuatorResult<HashMap<String, serde_json::Value>> {
207        Ok(self.config.clone())
208    }
209}
210
211impl ConfigurationManagerImpl {
212    /// Create a new Configuration Manager
213    pub fn new(config: ConfigurationManagementConfig) -> Self {
214        Self {
215            dry_run_mode: config.enable_dry_run,
216            config,
217            current_config: HashMap::new(),
218            snapshots: HashMap::new(),
219            history: Vec::new(),
220            current_version: 0,
221            audit_log: Vec::new(),
222            backends: HashMap::new(),
223        }
224    }
225
226    /// Register a configuration backend
227    pub fn register_backend(&mut self, name: impl Into<String>, backend: Box<dyn ConfigurationBackend>) {
228        self.backends.insert(name.into(), backend);
229    }
230
231    /// Get backend for a configuration type
232    fn get_backend_mut(&mut self, config_type: &str) -> ActuatorResult<&mut Box<dyn ConfigurationBackend>> {
233        self.backends
234            .get_mut(config_type)
235            .ok_or_else(|| ActuatorError::ConfigError(format!(
236                "No backend registered for config type: {}",
237                config_type
238            )))
239    }
240
241    /// Set dry-run mode
242    pub fn set_dry_run(&mut self, enabled: bool) {
243        self.dry_run_mode = enabled;
244    }
245
246    /// Get dry-run mode status
247    pub fn is_dry_run(&self) -> bool {
248        self.dry_run_mode
249    }
250
251    /// Calculate diff between current config and proposed changes
252    pub fn calculate_diff(&self, changes: &[ConfigChange]) -> Vec<ConfigurationDiff> {
253        let mut diffs = Vec::new();
254
255        for change in changes {
256            let diff_type = if change.old_value.is_none() {
257                DiffType::Add
258            } else if change.new_value.is_null() {
259                DiffType::Delete
260            } else {
261                DiffType::Update
262            };
263
264            diffs.push(ConfigurationDiff {
265                path: change.path.clone(),
266                old_value: change.old_value.clone(),
267                new_value: change.new_value.clone(),
268                change_type: diff_type,
269            });
270        }
271
272        diffs
273    }
274
275    /// Get configuration history
276    pub fn get_history(&self) -> &[ConfigurationVersion] {
277        &self.history
278    }
279
280    /// Get audit log
281    pub fn get_audit_log(&self) -> &[AuditLogEntry] {
282        &self.audit_log
283    }
284
285    /// Get snapshot by ID
286    pub fn get_snapshot(&self, snapshot_id: &str) -> Option<&ConfigurationSnapshot> {
287        self.snapshots.get(snapshot_id)
288    }
289
290    /// List all snapshots
291    pub fn list_snapshots(&self) -> Vec<&ConfigurationSnapshot> {
292        let mut snapshots: Vec<_> = self.snapshots.values().collect();
293        snapshots.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
294        snapshots
295    }
296
297    /// Cleanup old snapshots
298    fn cleanup_old_snapshots(&mut self) {
299        if self.snapshots.len() > self.config.max_snapshots {
300            // Keep only the most recent snapshots
301            let mut snapshot_list: Vec<_> = self.snapshots.iter()
302                .map(|(id, snap)| (id.clone(), snap.timestamp))
303                .collect();
304
305            snapshot_list.sort_by(|a, b| b.1.cmp(&a.1));
306
307            // Remove oldest snapshots
308            for (id, _) in snapshot_list.iter().skip(self.config.max_snapshots) {
309                self.snapshots.remove(id);
310                debug!("Removed old snapshot: {}", id);
311            }
312        }
313    }
314
315    /// Add audit log entry
316    fn add_audit_log(
317        &mut self,
318        operation: impl Into<String>,
319        actor: impl Into<String>,
320        changes: Vec<ConfigChange>,
321        success: bool,
322        error: Option<String>,
323    ) {
324        if !self.config.enable_audit_log {
325            return;
326        }
327
328        let entry = AuditLogEntry {
329            id: uuid::Uuid::new_v4().to_string(),
330            timestamp: Utc::now(),
331            operation: operation.into(),
332            actor: actor.into(),
333            changes,
334            success,
335            error,
336            metadata: HashMap::new(),
337        };
338
339        self.audit_log.push(entry);
340        debug!("Added audit log entry: {} - success: {}", self.audit_log.last().unwrap().operation, success);
341    }
342
343    /// Validate all changes
344    async fn validate_changes(&mut self, changes: &[ConfigChange]) -> ActuatorResult<()> {
345        if !self.config.enable_validation {
346            return Ok(());
347        }
348
349        for change in changes {
350            // Get backend for this config type
351            let config_type = format!("{:?}", change.config_type);
352
353            // If backend exists, use it for validation
354            if let Ok(backend) = self.get_backend_mut(&config_type) {
355                backend.validate(&change.path, &change.new_value).await?;
356            } else {
357                // Fallback to basic validation
358                self.basic_validation(change)?;
359            }
360        }
361
362        Ok(())
363    }
364
365    /// Basic validation when no backend is available
366    fn basic_validation(&self, change: &ConfigChange) -> ActuatorResult<()> {
367        // Check path is not empty
368        if change.path.is_empty() {
369            return Err(ActuatorError::ValidationFailed(
370                "Configuration path cannot be empty".to_string()
371            ));
372        }
373
374        // Check for invalid characters in path
375        if change.path.contains("..") {
376            return Err(ActuatorError::ValidationFailed(
377                format!("Invalid path contains '..': {}", change.path)
378            ));
379        }
380
381        Ok(())
382    }
383
384    /// Apply changes to backends
385    async fn apply_to_backends(&mut self, changes: &[ConfigChange]) -> ActuatorResult<()> {
386        for change in changes {
387            let config_type = format!("{:?}", change.config_type);
388
389            if let Ok(backend) = self.get_backend_mut(&config_type) {
390                if change.new_value.is_null() {
391                    backend.delete(&change.path).await?;
392                } else {
393                    backend.set(&change.path, change.new_value.clone()).await?;
394                }
395            }
396
397            // Update current config
398            if change.new_value.is_null() {
399                self.current_config.remove(&change.path);
400            } else {
401                self.current_config.insert(change.path.clone(), change.new_value.clone());
402            }
403        }
404
405        Ok(())
406    }
407
408    /// Revert changes from backends
409    async fn revert_from_backends(&mut self, changes: &[ConfigChange]) -> ActuatorResult<()> {
410        for change in changes {
411            let config_type = format!("{:?}", change.config_type);
412
413            if let Ok(backend) = self.get_backend_mut(&config_type) {
414                if let Some(old_value) = &change.old_value {
415                    backend.set(&change.path, old_value.clone()).await?;
416                } else {
417                    backend.delete(&change.path).await?;
418                }
419            }
420
421            // Update current config
422            if let Some(old_value) = &change.old_value {
423                self.current_config.insert(change.path.clone(), old_value.clone());
424            } else {
425                self.current_config.remove(&change.path);
426            }
427        }
428
429        Ok(())
430    }
431}
432
433#[async_trait]
434impl ConfigurationManager for ConfigurationManagerImpl {
435    async fn apply_config(&mut self, changes: &[ConfigChange]) -> ActuatorResult<()> {
436        info!("Applying {} configuration changes (dry-run: {})", changes.len(), self.dry_run_mode);
437
438        // Validate configuration if enabled
439        if let Err(e) = self.validate_changes(changes).await {
440            error!("Configuration validation failed: {}", e);
441            self.add_audit_log("apply_config", "system", changes.to_vec(), false, Some(e.to_string()));
442            return Err(e);
443        }
444
445        // In dry-run mode, just validate and return
446        if self.dry_run_mode {
447            info!("Dry-run mode: Configuration changes validated successfully");
448            self.add_audit_log("apply_config_dry_run", "system", changes.to_vec(), true, None);
449            return Ok(());
450        }
451
452        // Create snapshot before applying if versioning is enabled
453        let snapshot_id = if self.config.enable_versioning {
454            let snapshot = self.create_snapshot().await?;
455            snapshot.id
456        } else {
457            String::new()
458        };
459
460        // Apply changes
461        match self.apply_to_backends(changes).await {
462            Ok(_) => {
463                info!("Configuration changes applied successfully");
464
465                // Update version history
466                if self.config.enable_versioning {
467                    self.current_version += 1;
468                    self.history.push(ConfigurationVersion {
469                        version: self.current_version,
470                        timestamp: Utc::now(),
471                        changes: changes.to_vec(),
472                        applied_by: "system".to_string(),
473                        description: format!("Applied {} changes", changes.len()),
474                        snapshot_id,
475                    });
476                }
477
478                self.add_audit_log("apply_config", "system", changes.to_vec(), true, None);
479                Ok(())
480            }
481            Err(e) => {
482                error!("Failed to apply configuration changes: {}", e);
483                self.add_audit_log("apply_config", "system", changes.to_vec(), false, Some(e.to_string()));
484
485                // Try to restore from snapshot if available
486                if self.config.enable_versioning && !snapshot_id.is_empty() {
487                    warn!("Attempting to restore from snapshot due to error");
488                    if let Err(restore_err) = self.restore_snapshot(&snapshot_id).await {
489                        error!("Failed to restore snapshot after error: {}", restore_err);
490                    }
491                }
492
493                Err(e)
494            }
495        }
496    }
497
498    async fn revert_config(&mut self, changes: &[ConfigChange]) -> ActuatorResult<()> {
499        info!("Reverting {} configuration changes (dry-run: {})", changes.len(), self.dry_run_mode);
500
501        // In dry-run mode, just validate and return
502        if self.dry_run_mode {
503            info!("Dry-run mode: Configuration revert validated successfully");
504            self.add_audit_log("revert_config_dry_run", "system", changes.to_vec(), true, None);
505            return Ok(());
506        }
507
508        // Revert changes
509        match self.revert_from_backends(changes).await {
510            Ok(_) => {
511                info!("Configuration changes reverted successfully");
512                self.add_audit_log("revert_config", "system", changes.to_vec(), true, None);
513                Ok(())
514            }
515            Err(e) => {
516                error!("Failed to revert configuration changes: {}", e);
517                self.add_audit_log("revert_config", "system", changes.to_vec(), false, Some(e.to_string()));
518                Err(e)
519            }
520        }
521    }
522
523    async fn create_snapshot(&mut self) -> ActuatorResult<ConfigurationSnapshot> {
524        debug!("Creating configuration snapshot");
525
526        let snapshot = ConfigurationSnapshot {
527            id: uuid::Uuid::new_v4().to_string(),
528            timestamp: Utc::now(),
529            config: self.current_config.clone(),
530            deployment_id: None,
531            version: self.current_version,
532            tags: vec!["auto".to_string()],
533        };
534
535        if !self.dry_run_mode {
536            self.snapshots.insert(snapshot.id.clone(), snapshot.clone());
537            self.cleanup_old_snapshots();
538            info!("Created configuration snapshot: {}", snapshot.id);
539        }
540
541        Ok(snapshot)
542    }
543
544    async fn restore_snapshot(&mut self, snapshot_id: &str) -> ActuatorResult<()> {
545        info!("Restoring configuration from snapshot: {} (dry-run: {})", snapshot_id, self.dry_run_mode);
546
547        let snapshot = self.snapshots.get(snapshot_id)
548            .ok_or_else(|| ActuatorError::ConfigError(format!(
549                "Snapshot not found: {}",
550                snapshot_id
551            )))?
552            .clone();
553
554        // In dry-run mode, just verify snapshot exists
555        if self.dry_run_mode {
556            info!("Dry-run mode: Snapshot restore validated successfully");
557            return Ok(());
558        }
559
560        // Calculate changes needed to restore
561        let mut changes = Vec::new();
562
563        // Add all snapshot values
564        for (path, value) in &snapshot.config {
565            let old_value = self.current_config.get(path).cloned();
566            changes.push(ConfigChange {
567                config_type: crate::decision::ConfigType::Other,
568                path: path.clone(),
569                old_value,
570                new_value: value.clone(),
571                description: format!("Restore from snapshot {}", snapshot_id),
572            });
573        }
574
575        // Remove values not in snapshot
576        for path in self.current_config.keys() {
577            if !snapshot.config.contains_key(path) {
578                changes.push(ConfigChange {
579                    config_type: crate::decision::ConfigType::Other,
580                    path: path.clone(),
581                    old_value: self.current_config.get(path).cloned(),
582                    new_value: serde_json::Value::Null,
583                    description: format!("Remove during snapshot {} restore", snapshot_id),
584                });
585            }
586        }
587
588        // Apply the restore changes
589        match self.apply_to_backends(&changes).await {
590            Ok(_) => {
591                info!("Successfully restored configuration from snapshot: {}", snapshot_id);
592                self.add_audit_log("restore_snapshot", "system", changes, true, None);
593                Ok(())
594            }
595            Err(e) => {
596                error!("Failed to restore snapshot: {}", e);
597                self.add_audit_log("restore_snapshot", "system", changes, false, Some(e.to_string()));
598                Err(e)
599            }
600        }
601    }
602
603    async fn get_config(&self) -> ActuatorResult<HashMap<String, serde_json::Value>> {
604        Ok(self.current_config.clone())
605    }
606
607    async fn validate_config(&self, changes: &[ConfigChange]) -> ActuatorResult<()> {
608        debug!("Validating {} configuration changes", changes.len());
609
610        // Clone self to avoid mutable borrow issues
611        let mut temp_manager = ConfigurationManagerImpl {
612            config: self.config.clone(),
613            current_config: self.current_config.clone(),
614            snapshots: HashMap::new(),
615            history: Vec::new(),
616            current_version: self.current_version,
617            audit_log: Vec::new(),
618            dry_run_mode: false,
619            backends: HashMap::new(),
620        };
621
622        // Register backends (note: we can't clone trait objects, so validation may be limited)
623        temp_manager.validate_changes(changes).await
624    }
625}
626
627#[cfg(test)]
628mod tests {
629    use super::*;
630    use crate::decision::ConfigType;
631
632    fn create_test_manager() -> ConfigurationManagerImpl {
633        let config = ConfigurationManagementConfig {
634            enable_versioning: true,
635            enable_validation: true,
636            enable_audit_log: true,
637            snapshot_interval: std::time::Duration::from_secs(3600),
638            max_snapshots: 10,
639            enable_dry_run: false,
640        };
641
642        let mut manager = ConfigurationManagerImpl::new(config);
643
644        // Register in-memory backend for testing
645        manager.register_backend("Model", Box::new(InMemoryBackend::new()));
646        manager.register_backend("Cache", Box::new(InMemoryBackend::new()));
647        manager.register_backend("RateLimit", Box::new(InMemoryBackend::new()));
648
649        manager
650    }
651
652    fn create_test_change(path: &str, value: serde_json::Value) -> ConfigChange {
653        ConfigChange {
654            config_type: ConfigType::Model,
655            path: path.to_string(),
656            old_value: None,
657            new_value: value,
658            description: format!("Test change for {}", path),
659        }
660    }
661
662    #[tokio::test]
663    async fn test_apply_config_basic() {
664        let mut manager = create_test_manager();
665
666        let changes = vec![
667            create_test_change("model.name", serde_json::json!("gpt-4")),
668            create_test_change("model.temperature", serde_json::json!(0.7)),
669        ];
670
671        let result = manager.apply_config(&changes).await;
672        assert!(result.is_ok(), "Failed to apply config: {:?}", result);
673
674        let config = manager.get_config().await.unwrap();
675        assert_eq!(config.get("model.name"), Some(&serde_json::json!("gpt-4")));
676        assert_eq!(config.get("model.temperature"), Some(&serde_json::json!(0.7)));
677    }
678
679    #[tokio::test]
680    async fn test_revert_config() {
681        let mut manager = create_test_manager();
682
683        // Apply initial config
684        let changes = vec![
685            create_test_change("model.name", serde_json::json!("gpt-4")),
686        ];
687        manager.apply_config(&changes).await.unwrap();
688
689        // Create revert changes
690        let revert_changes = vec![
691            ConfigChange {
692                config_type: ConfigType::Model,
693                path: "model.name".to_string(),
694                old_value: Some(serde_json::json!("gpt-4")),
695                new_value: serde_json::json!("gpt-3.5-turbo"),
696                description: "Revert to previous model".to_string(),
697            },
698        ];
699
700        let result = manager.revert_config(&revert_changes).await;
701        assert!(result.is_ok(), "Failed to revert config: {:?}", result);
702
703        let config = manager.get_config().await.unwrap();
704        assert_eq!(config.get("model.name"), Some(&serde_json::json!("gpt-3.5-turbo")));
705    }
706
707    #[tokio::test]
708    async fn test_create_snapshot() {
709        let mut manager = create_test_manager();
710
711        // Apply some config
712        let changes = vec![
713            create_test_change("model.name", serde_json::json!("gpt-4")),
714            create_test_change("cache.enabled", serde_json::json!(true)),
715        ];
716        manager.apply_config(&changes).await.unwrap();
717
718        // Create snapshot
719        let snapshot = manager.create_snapshot().await.unwrap();
720        assert!(!snapshot.id.is_empty());
721        assert_eq!(snapshot.config.len(), 2);
722        assert_eq!(snapshot.version, 1);
723    }
724
725    #[tokio::test]
726    async fn test_restore_snapshot() {
727        let mut manager = create_test_manager();
728
729        // Apply initial config
730        let changes1 = vec![
731            create_test_change("model.name", serde_json::json!("gpt-4")),
732        ];
733        manager.apply_config(&changes1).await.unwrap();
734
735        // Create snapshot
736        let snapshot = manager.create_snapshot().await.unwrap();
737        let snapshot_id = snapshot.id.clone();
738
739        // Apply more changes
740        let changes2 = vec![
741            create_test_change("model.name", serde_json::json!("gpt-3.5-turbo")),
742            create_test_change("model.temperature", serde_json::json!(0.9)),
743        ];
744        manager.apply_config(&changes2).await.unwrap();
745
746        // Restore snapshot
747        manager.restore_snapshot(&snapshot_id).await.unwrap();
748
749        let config = manager.get_config().await.unwrap();
750        assert_eq!(config.get("model.name"), Some(&serde_json::json!("gpt-4")));
751        assert!(!config.contains_key("model.temperature"));
752    }
753
754    #[tokio::test]
755    async fn test_configuration_versioning() {
756        let mut manager = create_test_manager();
757
758        // Apply multiple changes
759        let changes1 = vec![create_test_change("model.name", serde_json::json!("gpt-4"))];
760        manager.apply_config(&changes1).await.unwrap();
761
762        let changes2 = vec![create_test_change("cache.enabled", serde_json::json!(true))];
763        manager.apply_config(&changes2).await.unwrap();
764
765        let changes3 = vec![create_test_change("rate_limit.max_rps", serde_json::json!(100))];
766        manager.apply_config(&changes3).await.unwrap();
767
768        // Check version
769        assert_eq!(manager.current_version, 3);
770
771        // Check history
772        let history = manager.get_history();
773        assert_eq!(history.len(), 3);
774        assert_eq!(history[0].version, 1);
775        assert_eq!(history[1].version, 2);
776        assert_eq!(history[2].version, 3);
777    }
778
779    #[tokio::test]
780    async fn test_validation_failure() {
781        let mut manager = create_test_manager();
782
783        let changes = vec![
784            ConfigChange {
785                config_type: ConfigType::Model,
786                path: "".to_string(), // Invalid empty path
787                old_value: None,
788                new_value: serde_json::json!("value"),
789                description: "Invalid change".to_string(),
790            },
791        ];
792
793        let result = manager.apply_config(&changes).await;
794        assert!(result.is_err());
795    }
796
797    #[tokio::test]
798    async fn test_dry_run_mode() {
799        let mut manager = create_test_manager();
800        manager.set_dry_run(true);
801
802        let changes = vec![
803            create_test_change("model.name", serde_json::json!("gpt-4")),
804        ];
805
806        let result = manager.apply_config(&changes).await;
807        assert!(result.is_ok());
808
809        // Config should not be applied in dry-run mode
810        let config = manager.get_config().await.unwrap();
811        assert!(!config.contains_key("model.name"));
812
813        // But validation should have occurred
814        let audit_log = manager.get_audit_log();
815        assert!(!audit_log.is_empty());
816        assert_eq!(audit_log[0].operation, "apply_config_dry_run");
817    }
818
819    #[tokio::test]
820    async fn test_audit_logging() {
821        let mut manager = create_test_manager();
822
823        let changes = vec![
824            create_test_change("model.name", serde_json::json!("gpt-4")),
825        ];
826
827        manager.apply_config(&changes).await.unwrap();
828
829        let audit_log = manager.get_audit_log();
830        assert_eq!(audit_log.len(), 1);
831        assert_eq!(audit_log[0].operation, "apply_config");
832        assert!(audit_log[0].success);
833        assert_eq!(audit_log[0].changes.len(), 1);
834    }
835
836    #[tokio::test]
837    async fn test_configuration_diff() {
838        let manager = create_test_manager();
839
840        let changes = vec![
841            create_test_change("model.name", serde_json::json!("gpt-4")),
842            ConfigChange {
843                config_type: ConfigType::Model,
844                path: "model.temperature".to_string(),
845                old_value: Some(serde_json::json!(0.5)),
846                new_value: serde_json::json!(0.7),
847                description: "Update temperature".to_string(),
848            },
849        ];
850
851        let diffs = manager.calculate_diff(&changes);
852        assert_eq!(diffs.len(), 2);
853        assert_eq!(diffs[0].change_type, DiffType::Add);
854        assert_eq!(diffs[1].change_type, DiffType::Update);
855    }
856
857    #[tokio::test]
858    async fn test_snapshot_cleanup() {
859        let mut manager = create_test_manager();
860
861        // Create more snapshots than max_snapshots
862        for i in 0..15 {
863            let changes = vec![
864                create_test_change(&format!("key_{}", i), serde_json::json!(i)),
865            ];
866            manager.apply_config(&changes).await.unwrap();
867        }
868
869        // Should only keep max_snapshots (10)
870        assert!(manager.snapshots.len() <= manager.config.max_snapshots);
871    }
872
873    #[tokio::test]
874    async fn test_list_snapshots() {
875        let mut manager = create_test_manager();
876
877        // Create multiple snapshots
878        for i in 0..3 {
879            let changes = vec![
880                create_test_change(&format!("key_{}", i), serde_json::json!(i)),
881            ];
882            manager.apply_config(&changes).await.unwrap();
883        }
884
885        let snapshots = manager.list_snapshots();
886        assert_eq!(snapshots.len(), 3);
887
888        // Should be sorted by timestamp (newest first)
889        for i in 0..snapshots.len() - 1 {
890            assert!(snapshots[i].timestamp >= snapshots[i + 1].timestamp);
891        }
892    }
893
894    #[tokio::test]
895    async fn test_validate_config_only() {
896        let manager = create_test_manager();
897
898        let valid_changes = vec![
899            create_test_change("model.name", serde_json::json!("gpt-4")),
900        ];
901
902        let result = manager.validate_config(&valid_changes).await;
903        assert!(result.is_ok());
904
905        let invalid_changes = vec![
906            ConfigChange {
907                config_type: ConfigType::Model,
908                path: "".to_string(),
909                old_value: None,
910                new_value: serde_json::json!("value"),
911                description: "Invalid".to_string(),
912            },
913        ];
914
915        let result = manager.validate_config(&invalid_changes).await;
916        assert!(result.is_err());
917    }
918
919    #[tokio::test]
920    async fn test_get_snapshot() {
921        let mut manager = create_test_manager();
922
923        let changes = vec![
924            create_test_change("model.name", serde_json::json!("gpt-4")),
925        ];
926        manager.apply_config(&changes).await.unwrap();
927
928        let snapshot = manager.create_snapshot().await.unwrap();
929        let snapshot_id = snapshot.id.clone();
930
931        let retrieved = manager.get_snapshot(&snapshot_id);
932        assert!(retrieved.is_some());
933        assert_eq!(retrieved.unwrap().id, snapshot_id);
934
935        let not_found = manager.get_snapshot("nonexistent");
936        assert!(not_found.is_none());
937    }
938
939    #[tokio::test]
940    async fn test_backend_not_found() {
941        let mut manager = create_test_manager();
942
943        // Remove all backends
944        manager.backends.clear();
945
946        let changes = vec![
947            create_test_change("model.name", serde_json::json!("gpt-4")),
948        ];
949
950        // Should still work with basic validation
951        let result = manager.apply_config(&changes).await;
952        // This will fail because no backend is registered
953        assert!(result.is_err());
954    }
955}