1use 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
23pub struct ConfigurationManagerImpl {
25 config: ConfigurationManagementConfig,
27
28 current_config: HashMap<String, serde_json::Value>,
30
31 snapshots: HashMap<String, ConfigurationSnapshot>,
33
34 history: Vec<ConfigurationVersion>,
36
37 current_version: u64,
39
40 audit_log: Vec<AuditLogEntry>,
42
43 dry_run_mode: bool,
45
46 backends: HashMap<String, Box<dyn ConfigurationBackend>>,
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
52struct ConfigurationVersion {
53 version: u64,
55
56 timestamp: DateTime<Utc>,
58
59 changes: Vec<ConfigChange>,
61
62 applied_by: String,
64
65 description: String,
67
68 snapshot_id: String,
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct AuditLogEntry {
75 pub id: String,
77
78 pub timestamp: DateTime<Utc>,
80
81 pub operation: String,
83
84 pub actor: String,
86
87 pub changes: Vec<ConfigChange>,
89
90 pub success: bool,
92
93 pub error: Option<String>,
95
96 pub metadata: HashMap<String, serde_json::Value>,
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct ConfigurationDiff {
103 pub path: String,
105
106 pub old_value: Option<serde_json::Value>,
108
109 pub new_value: serde_json::Value,
111
112 pub change_type: DiffType,
114}
115
116#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
118pub enum DiffType {
119 Add,
121 Update,
123 Delete,
125}
126
127#[async_trait]
129pub trait ConfigurationBackend: Send + Sync {
130 async fn get(&self, path: &str) -> ActuatorResult<Option<serde_json::Value>>;
132
133 async fn set(&mut self, path: &str, value: serde_json::Value) -> ActuatorResult<()>;
135
136 async fn delete(&mut self, path: &str) -> ActuatorResult<()>;
138
139 async fn validate(&self, path: &str, value: &serde_json::Value) -> ActuatorResult<()>;
141
142 async fn get_all(&self) -> ActuatorResult<HashMap<String, serde_json::Value>>;
144}
145
146#[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 if value.is_null() {
189 return Err(ActuatorError::ValidationFailed(format!(
190 "Configuration value at '{}' cannot be null",
191 path
192 )));
193 }
194
195 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 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 pub fn register_backend(&mut self, name: impl Into<String>, backend: Box<dyn ConfigurationBackend>) {
228 self.backends.insert(name.into(), backend);
229 }
230
231 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 pub fn set_dry_run(&mut self, enabled: bool) {
243 self.dry_run_mode = enabled;
244 }
245
246 pub fn is_dry_run(&self) -> bool {
248 self.dry_run_mode
249 }
250
251 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 pub fn get_history(&self) -> &[ConfigurationVersion] {
277 &self.history
278 }
279
280 pub fn get_audit_log(&self) -> &[AuditLogEntry] {
282 &self.audit_log
283 }
284
285 pub fn get_snapshot(&self, snapshot_id: &str) -> Option<&ConfigurationSnapshot> {
287 self.snapshots.get(snapshot_id)
288 }
289
290 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 fn cleanup_old_snapshots(&mut self) {
299 if self.snapshots.len() > self.config.max_snapshots {
300 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 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 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 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 let config_type = format!("{:?}", change.config_type);
352
353 if let Ok(backend) = self.get_backend_mut(&config_type) {
355 backend.validate(&change.path, &change.new_value).await?;
356 } else {
357 self.basic_validation(change)?;
359 }
360 }
361
362 Ok(())
363 }
364
365 fn basic_validation(&self, change: &ConfigChange) -> ActuatorResult<()> {
367 if change.path.is_empty() {
369 return Err(ActuatorError::ValidationFailed(
370 "Configuration path cannot be empty".to_string()
371 ));
372 }
373
374 if change.path.contains("..") {
376 return Err(ActuatorError::ValidationFailed(
377 format!("Invalid path contains '..': {}", change.path)
378 ));
379 }
380
381 Ok(())
382 }
383
384 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 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 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 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 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 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 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 match self.apply_to_backends(changes).await {
462 Ok(_) => {
463 info!("Configuration changes applied successfully");
464
465 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 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 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 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 if self.dry_run_mode {
556 info!("Dry-run mode: Snapshot restore validated successfully");
557 return Ok(());
558 }
559
560 let mut changes = Vec::new();
562
563 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 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 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 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 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 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 let changes = vec![
685 create_test_change("model.name", serde_json::json!("gpt-4")),
686 ];
687 manager.apply_config(&changes).await.unwrap();
688
689 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 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 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 let changes1 = vec![
731 create_test_change("model.name", serde_json::json!("gpt-4")),
732 ];
733 manager.apply_config(&changes1).await.unwrap();
734
735 let snapshot = manager.create_snapshot().await.unwrap();
737 let snapshot_id = snapshot.id.clone();
738
739 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 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 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 assert_eq!(manager.current_version, 3);
770
771 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(), 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 let config = manager.get_config().await.unwrap();
811 assert!(!config.contains_key("model.name"));
812
813 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 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 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 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 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 manager.backends.clear();
945
946 let changes = vec![
947 create_test_change("model.name", serde_json::json!("gpt-4")),
948 ];
949
950 let result = manager.apply_config(&changes).await;
952 assert!(result.is_err());
954 }
955}