1use crate::config::paths::Paths;
2use crate::config::AsterMode;
3use fs2::FileExt;
4use keyring::Entry;
5use once_cell::sync::OnceCell;
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8use serde_yaml::Mapping;
9use std::collections::HashMap;
10use std::env;
11use std::ffi::OsString;
12use std::fs::OpenOptions;
13use std::io::Write;
14use std::path::{Path, PathBuf};
15use std::sync::Mutex;
16use thiserror::Error;
17
18const KEYRING_SERVICE: &str = "aster";
19const KEYRING_USERNAME: &str = "secrets";
20pub const CONFIG_YAML_NAME: &str = "config.yaml";
21
22#[derive(Error, Debug)]
23pub enum ConfigError {
24 #[error("Configuration value not found: {0}")]
25 NotFound(String),
26 #[error("Failed to deserialize value: {0}")]
27 DeserializeError(String),
28 #[error("Failed to read config file: {0}")]
29 FileError(#[from] std::io::Error),
30 #[error("Failed to create config directory: {0}")]
31 DirectoryError(String),
32 #[error("Failed to access keyring: {0}")]
33 KeyringError(String),
34 #[error("Failed to lock config file: {0}")]
35 LockError(String),
36}
37
38impl From<serde_json::Error> for ConfigError {
39 fn from(err: serde_json::Error) -> Self {
40 ConfigError::DeserializeError(err.to_string())
41 }
42}
43
44impl From<serde_yaml::Error> for ConfigError {
45 fn from(err: serde_yaml::Error) -> Self {
46 ConfigError::DeserializeError(err.to_string())
47 }
48}
49
50impl From<keyring::Error> for ConfigError {
51 fn from(err: keyring::Error) -> Self {
52 ConfigError::KeyringError(err.to_string())
53 }
54}
55
56pub struct Config {
103 config_path: PathBuf,
104 secrets: SecretStorage,
105 guard: Mutex<()>,
106}
107
108enum SecretStorage {
109 Keyring { service: String },
110 File { path: PathBuf },
111}
112
113static GLOBAL_CONFIG: OnceCell<Config> = OnceCell::new();
115
116impl Default for Config {
117 fn default() -> Self {
118 let config_dir = Paths::config_dir();
119
120 let config_path = config_dir.join(CONFIG_YAML_NAME);
121
122 let secrets = match env::var("ASTER_DISABLE_KEYRING") {
123 Ok(_) => SecretStorage::File {
124 path: config_dir.join("secrets.yaml"),
125 },
126 Err(_) => SecretStorage::Keyring {
127 service: KEYRING_SERVICE.to_string(),
128 },
129 };
130 Config {
131 config_path,
132 secrets,
133 guard: Mutex::new(()),
134 }
135 }
136}
137
138pub trait ConfigValue {
139 const KEY: &'static str;
140 const DEFAULT: &'static str;
141}
142
143macro_rules! config_value {
144 ($key:ident, $type:ty) => {
145 impl Config {
146 paste::paste! {
147 pub fn [<get_ $key:lower>](&self) -> Result<$type, ConfigError> {
148 self.get_param(stringify!($key))
149 }
150 }
151 paste::paste! {
152 pub fn [<set_ $key:lower>](&self, v: impl Into<$type>) -> Result<(), ConfigError> {
153 self.set_param(stringify!($key), &v.into())
154 }
155 }
156 }
157 };
158
159 ($key:ident, $inner:ty, $default:expr) => {
160 paste::paste! {
161 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
162 #[serde(transparent)]
163 pub struct [<$key:camel>]($inner);
164
165 impl ConfigValue for [<$key:camel>] {
166 const KEY: &'static str = stringify!($key);
167 const DEFAULT: &'static str = $default;
168 }
169
170 impl Default for [<$key:camel>] {
171 fn default() -> Self {
172 [<$key:camel>]($default.into())
173 }
174 }
175
176 impl std::ops::Deref for [<$key:camel>] {
177 type Target = $inner;
178
179 fn deref(&self) -> &Self::Target {
180 &self.0
181 }
182 }
183
184 impl std::ops::DerefMut for [<$key:camel>] {
185 fn deref_mut(&mut self) -> &mut Self::Target {
186 &mut self.0
187 }
188 }
189
190 impl std::fmt::Display for [<$key:camel>] {
191 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
192 write!(f, "{:?}", self.0)
193 }
194 }
195
196 impl From<$inner> for [<$key:camel>] {
197 fn from(value: $inner) -> Self {
198 [<$key:camel>](value)
199 }
200 }
201
202 impl From<[<$key:camel>]> for $inner {
203 fn from(value: [<$key:camel>]) -> $inner {
204 value.0
205 }
206 }
207
208 config_value!($key, [<$key:camel>]);
209 }
210 };
211}
212
213fn parse_yaml_content(content: &str) -> Result<Mapping, ConfigError> {
214 serde_yaml::from_str(content).map_err(|e| e.into())
215}
216
217impl Config {
218 pub fn global() -> &'static Config {
223 GLOBAL_CONFIG.get_or_init(Config::default)
224 }
225
226 pub fn new<P: AsRef<Path>>(config_path: P, service: &str) -> Result<Self, ConfigError> {
231 Ok(Config {
232 config_path: config_path.as_ref().to_path_buf(),
233 secrets: SecretStorage::Keyring {
234 service: service.to_string(),
235 },
236 guard: Mutex::new(()),
237 })
238 }
239
240 pub fn new_with_file_secrets<P1: AsRef<Path>, P2: AsRef<Path>>(
245 config_path: P1,
246 secrets_path: P2,
247 ) -> Result<Self, ConfigError> {
248 Ok(Config {
249 config_path: config_path.as_ref().to_path_buf(),
250 secrets: SecretStorage::File {
251 path: secrets_path.as_ref().to_path_buf(),
252 },
253 guard: Mutex::new(()),
254 })
255 }
256
257 pub fn exists(&self) -> bool {
258 self.config_path.exists()
259 }
260
261 pub fn clear(&self) -> Result<(), ConfigError> {
262 Ok(std::fs::remove_file(&self.config_path)?)
263 }
264
265 pub fn path(&self) -> String {
266 self.config_path.to_string_lossy().to_string()
267 }
268
269 fn load(&self) -> Result<Mapping, ConfigError> {
270 if self.config_path.exists() {
271 self.load_values_with_recovery()
272 } else {
273 tracing::info!("Config file doesn't exist, attempting recovery from backup");
275
276 if let Ok(backup_values) = self.try_restore_from_backup() {
277 tracing::info!("Successfully restored config from backup");
278 return Ok(backup_values);
279 }
280
281 tracing::info!("No backup found, creating default configuration");
283
284 let default_config = self.load_init_config_if_exists().unwrap_or_default();
286
287 self.create_and_save_default_config(default_config)
288 }
289 }
290
291 pub fn all_values(&self) -> Result<HashMap<String, Value>, ConfigError> {
292 self.load().map(|m| {
293 HashMap::from_iter(m.into_iter().filter_map(|(k, v)| {
294 k.as_str()
295 .map(|k| k.to_string())
296 .zip(serde_json::to_value(v).ok())
297 }))
298 })
299 }
300
301 fn create_and_save_default_config(
303 &self,
304 default_config: Mapping,
305 ) -> Result<Mapping, ConfigError> {
306 match self.save_values(default_config.clone()) {
308 Ok(_) => {
309 if default_config.is_empty() {
310 tracing::info!("Created fresh empty config file");
311 } else {
312 tracing::info!(
313 "Created fresh config file from init-config.yaml with {} keys",
314 default_config.len()
315 );
316 }
317 Ok(default_config)
318 }
319 Err(write_error) => {
320 tracing::error!("Failed to write default config file: {}", write_error);
321 Ok(default_config)
323 }
324 }
325 }
326
327 fn load_values_with_recovery(&self) -> Result<Mapping, ConfigError> {
328 let file_content = std::fs::read_to_string(&self.config_path)?;
329
330 match parse_yaml_content(&file_content) {
331 Ok(values) => Ok(values),
332 Err(parse_error) => {
333 tracing::warn!(
334 "Config file appears corrupted, attempting recovery: {}",
335 parse_error
336 );
337
338 if let Ok(backup_values) = self.try_restore_from_backup() {
340 tracing::info!("Successfully restored config from backup");
341 return Ok(backup_values);
342 }
343
344 tracing::error!("Could not recover config file, creating fresh default configuration. Original error: {}", parse_error);
346
347 let default_config = self.load_init_config_if_exists().unwrap_or_default();
348
349 self.create_and_save_default_config(default_config)
350 }
351 }
352 }
353
354 fn try_restore_from_backup(&self) -> Result<Mapping, ConfigError> {
355 let backup_paths = self.get_backup_paths();
356
357 for backup_path in backup_paths {
358 if backup_path.exists() {
359 match std::fs::read_to_string(&backup_path) {
360 Ok(backup_content) => {
361 match parse_yaml_content(&backup_content) {
362 Ok(values) => {
363 if let Err(e) = self.save_values(values.clone()) {
365 tracing::warn!(
366 "Failed to restore backup as main config: {}",
367 e
368 );
369 } else {
370 tracing::info!(
371 "Restored config from backup: {:?}",
372 backup_path
373 );
374 }
375 return Ok(values);
376 }
377 Err(e) => {
378 tracing::warn!(
379 "Backup file {:?} is also corrupted: {}",
380 backup_path,
381 e
382 );
383 continue;
384 }
385 }
386 }
387 Err(e) => {
388 tracing::warn!("Could not read backup file {:?}: {}", backup_path, e);
389 continue;
390 }
391 }
392 }
393 }
394
395 Err(ConfigError::NotFound("No valid backup found".to_string()))
396 }
397
398 fn get_backup_paths(&self) -> Vec<PathBuf> {
400 let mut paths = Vec::new();
401
402 if let Some(file_name) = self.config_path.file_name() {
404 let mut backup_name = file_name.to_os_string();
405 backup_name.push(".bak");
406 paths.push(self.config_path.with_file_name(backup_name));
407 }
408
409 for i in 1..=5 {
411 if let Some(file_name) = self.config_path.file_name() {
412 let mut backup_name = file_name.to_os_string();
413 backup_name.push(format!(".bak.{}", i));
414 paths.push(self.config_path.with_file_name(backup_name));
415 }
416 }
417
418 paths
419 }
420
421 fn load_init_config_if_exists(&self) -> Result<Mapping, ConfigError> {
422 load_init_config_from_workspace()
423 }
424
425 fn save_values(&self, values: Mapping) -> Result<(), ConfigError> {
426 self.create_backup_if_needed()?;
428
429 let yaml_value = serde_yaml::to_string(&values)?;
431
432 if let Some(parent) = self.config_path.parent() {
433 std::fs::create_dir_all(parent)
434 .map_err(|e| ConfigError::DirectoryError(e.to_string()))?;
435 }
436
437 let temp_path = self.config_path.with_extension("tmp");
439
440 {
441 let mut file = OpenOptions::new()
442 .write(true)
443 .create(true)
444 .truncate(true)
445 .open(&temp_path)?;
446
447 file.lock_exclusive()
449 .map_err(|e| ConfigError::LockError(e.to_string()))?;
450
451 file.write_all(yaml_value.as_bytes())?;
453 file.sync_all()?;
454
455 }
457
458 std::fs::rename(&temp_path, &self.config_path)?;
460
461 Ok(())
462 }
463
464 pub fn initialize_if_empty(&self, values: Mapping) -> Result<(), ConfigError> {
465 let _guard = self.guard.lock().unwrap();
466 if !self.exists() {
467 self.save_values(values)
468 } else {
469 Ok(())
470 }
471 }
472
473 fn create_backup_if_needed(&self) -> Result<(), ConfigError> {
475 if !self.config_path.exists() {
476 return Ok(());
477 }
478
479 let current_content = std::fs::read_to_string(&self.config_path)?;
481 if parse_yaml_content(¤t_content).is_err() {
482 return Ok(());
484 }
485
486 self.rotate_backups()?;
488
489 if let Some(file_name) = self.config_path.file_name() {
491 let mut backup_name = file_name.to_os_string();
492 backup_name.push(".bak");
493 let backup_path = self.config_path.with_file_name(backup_name);
494
495 if let Err(e) = std::fs::copy(&self.config_path, &backup_path) {
496 tracing::warn!("Failed to create config backup: {}", e);
497 } else {
499 tracing::debug!("Created config backup: {:?}", backup_path);
500 }
501 }
502
503 Ok(())
504 }
505
506 fn rotate_backups(&self) -> Result<(), ConfigError> {
508 if let Some(file_name) = self.config_path.file_name() {
509 for i in (1..5).rev() {
511 let mut current_backup = file_name.to_os_string();
512 current_backup.push(format!(".bak.{}", i));
513 let current_path = self.config_path.with_file_name(¤t_backup);
514
515 let mut next_backup = file_name.to_os_string();
516 next_backup.push(format!(".bak.{}", i + 1));
517 let next_path = self.config_path.with_file_name(&next_backup);
518
519 if current_path.exists() {
520 let _ = std::fs::rename(¤t_path, &next_path);
521 }
522 }
523
524 let mut backup_name = file_name.to_os_string();
526 backup_name.push(".bak");
527 let backup_path = self.config_path.with_file_name(&backup_name);
528
529 if backup_path.exists() {
530 let mut backup_1_name = file_name.to_os_string();
531 backup_1_name.push(".bak.1");
532 let backup_1_path = self.config_path.with_file_name(&backup_1_name);
533 let _ = std::fs::rename(&backup_path, &backup_1_path);
534 }
535 }
536
537 Ok(())
538 }
539
540 pub fn all_secrets(&self) -> Result<HashMap<String, Value>, ConfigError> {
541 match &self.secrets {
542 SecretStorage::Keyring { service } => {
543 let entry = Entry::new(service, KEYRING_USERNAME)?;
544
545 match entry.get_password() {
546 Ok(content) => {
547 let values: HashMap<String, Value> = serde_json::from_str(&content)?;
548 Ok(values)
549 }
550 Err(keyring::Error::NoEntry) => Ok(HashMap::new()),
551 Err(e) => Err(ConfigError::KeyringError(e.to_string())),
552 }
553 }
554 SecretStorage::File { path } => {
555 if path.exists() {
556 let file_content = std::fs::read_to_string(path)?;
557 let yaml_value: serde_yaml::Value = serde_yaml::from_str(&file_content)?;
558 let json_value: Value = serde_json::to_value(yaml_value)?;
559 match json_value {
560 Value::Object(map) => Ok(map.into_iter().collect()),
561 _ => Ok(HashMap::new()),
562 }
563 } else {
564 Ok(HashMap::new())
565 }
566 }
567 }
568 }
569
570 fn parse_env_value(val: &str) -> Result<Value, ConfigError> {
577 if let Ok(json_value) = serde_json::from_str(val) {
579 return Ok(json_value);
580 }
581
582 let trimmed = val.trim();
583
584 match trimmed.to_lowercase().as_str() {
585 "true" => return Ok(Value::Bool(true)),
586 "false" => return Ok(Value::Bool(false)),
587 _ => {}
588 }
589
590 if let Ok(int_val) = trimmed.parse::<i64>() {
591 return Ok(Value::Number(int_val.into()));
592 }
593
594 if let Ok(float_val) = trimmed.parse::<f64>() {
595 if let Some(num) = serde_json::Number::from_f64(float_val) {
596 return Ok(Value::Number(num));
597 }
598 }
599
600 Ok(Value::String(val.to_string()))
601 }
602
603 pub fn get(&self, key: &str, is_secret: bool) -> Result<Value, ConfigError> {
605 if is_secret {
606 self.get_secret(key)
607 } else {
608 self.get_param(key)
609 }
610 }
611
612 pub fn set<V>(&self, key: &str, value: &V, is_secret: bool) -> Result<(), ConfigError>
614 where
615 V: Serialize,
616 {
617 if is_secret {
618 self.set_secret(key, value)
619 } else {
620 self.set_param(key, value)
621 }
622 }
623
624 pub fn get_param<T: for<'de> Deserialize<'de>>(&self, key: &str) -> Result<T, ConfigError> {
641 let env_key = key.to_uppercase();
642 if let Ok(val) = env::var(&env_key) {
643 let value = Self::parse_env_value(&val)?;
644 return Ok(serde_json::from_value(value)?);
645 }
646
647 let values = self.load()?;
648 values
649 .get(key)
650 .ok_or_else(|| ConfigError::NotFound(key.to_string()))
651 .and_then(|v| Ok(serde_yaml::from_value(v.clone())?))
652 }
653
654 pub fn set_param<V: Serialize>(&self, key: &str, value: V) -> Result<(), ConfigError> {
668 let _guard = self.guard.lock().unwrap();
669 let mut values = self.load()?;
670 values.insert(serde_yaml::to_value(key)?, serde_yaml::to_value(value)?);
671 self.save_values(values)
672 }
673
674 pub fn delete(&self, key: &str) -> Result<(), ConfigError> {
688 let _guard = self.guard.lock().unwrap();
690
691 let mut values = self.load()?;
692 values.shift_remove(key);
693
694 self.save_values(values)
695 }
696
697 pub fn get_secret<T: for<'de> Deserialize<'de>>(&self, key: &str) -> Result<T, ConfigError> {
714 let env_key = key.to_uppercase();
716 if let Ok(val) = env::var(&env_key) {
717 let value = Self::parse_env_value(&val)?;
718 return Ok(serde_json::from_value(value)?);
719 }
720
721 let values = self.all_secrets()?;
723 values
724 .get(key)
725 .ok_or_else(|| ConfigError::NotFound(key.to_string()))
726 .and_then(|v| Ok(serde_json::from_value(v.clone())?))
727 }
728
729 pub fn get_secrets(
731 &self,
732 primary: &str,
733 maybe_secret: &[&str],
734 ) -> Result<HashMap<String, String>, ConfigError> {
735 let use_env = env::var(primary.to_uppercase()).is_ok();
736 let get_value = |key: &str| -> Result<String, ConfigError> {
737 if use_env {
738 env::var(key.to_uppercase()).map_err(|_| ConfigError::NotFound(key.to_string()))
739 } else {
740 self.get_secret(key)
741 }
742 };
743
744 let mut result = HashMap::new();
745 result.insert(primary.to_string(), get_value(primary)?);
746 for &key in maybe_secret {
747 if let Ok(v) = get_value(key) {
748 result.insert(key.to_string(), v);
749 }
750 }
751 Ok(result)
752 }
753
754 pub fn set_secret<V>(&self, key: &str, value: &V) -> Result<(), ConfigError>
769 where
770 V: Serialize,
771 {
772 let _guard = self.guard.lock().unwrap();
774
775 let mut values = self.all_secrets()?;
776 values.insert(key.to_string(), serde_json::to_value(value)?);
777
778 match &self.secrets {
779 SecretStorage::Keyring { service } => {
780 let json_value = serde_json::to_string(&values)?;
781 let entry = Entry::new(service, KEYRING_USERNAME)?;
782 entry.set_password(&json_value)?;
783 }
784 SecretStorage::File { path } => {
785 let yaml_value = serde_yaml::to_string(&values)?;
786 std::fs::write(path, yaml_value)?;
787 }
788 };
789 Ok(())
790 }
791
792 pub fn delete_secret(&self, key: &str) -> Result<(), ConfigError> {
803 let _guard = self.guard.lock().unwrap();
805
806 let mut values = self.all_secrets()?;
807 values.remove(key);
808
809 match &self.secrets {
810 SecretStorage::Keyring { service } => {
811 let json_value = serde_json::to_string(&values)?;
812 let entry = Entry::new(service, KEYRING_USERNAME)?;
813 entry.set_password(&json_value)?;
814 }
815 SecretStorage::File { path } => {
816 let yaml_value = serde_yaml::to_string(&values)?;
817 std::fs::write(path, yaml_value)?;
818 }
819 };
820 Ok(())
821 }
822}
823
824config_value!(CLAUDE_CODE_COMMAND, OsString, "claude");
825config_value!(GEMINI_CLI_COMMAND, OsString, "gemini");
826config_value!(CURSOR_AGENT_COMMAND, OsString, "cursor-agent");
827config_value!(CODEX_COMMAND, OsString, "codex");
828config_value!(CODEX_REASONING_EFFORT, String, "high");
829config_value!(CODEX_ENABLE_SKILLS, String, "true");
830config_value!(CODEX_SKIP_GIT_CHECK, String, "false");
831config_value!(CODEX_USE_APP_SERVER, String, "true");
832
833config_value!(ASTER_SEARCH_PATHS, Vec<String>);
834config_value!(ASTER_MODE, AsterMode);
835config_value!(ASTER_PROVIDER, String);
836config_value!(ASTER_MODEL, String);
837config_value!(ASTER_MAX_ACTIVE_AGENTS, usize);
838
839pub fn load_init_config_from_workspace() -> Result<Mapping, ConfigError> {
842 let workspace_root = match std::env::current_exe() {
843 Ok(mut exe_path) => {
844 while let Some(parent) = exe_path.parent() {
845 let cargo_toml = parent.join("Cargo.toml");
846 if cargo_toml.exists() {
847 if let Ok(content) = std::fs::read_to_string(&cargo_toml) {
848 if content.contains("[workspace]") {
849 exe_path = parent.to_path_buf();
850 break;
851 }
852 }
853 }
854 exe_path = parent.to_path_buf();
855 }
856 exe_path
857 }
858 Err(_) => {
859 return Err(ConfigError::FileError(std::io::Error::new(
860 std::io::ErrorKind::NotFound,
861 "Could not determine executable path",
862 )))
863 }
864 };
865
866 let init_config_path = workspace_root.join("init-config.yaml");
867 if !init_config_path.exists() {
868 return Err(ConfigError::NotFound(
869 "init-config.yaml not found".to_string(),
870 ));
871 }
872
873 let init_content = std::fs::read_to_string(&init_config_path)?;
874 parse_yaml_content(&init_content)
875}
876
877#[cfg(test)]
878mod tests {
879 use super::*;
880 use serial_test::serial;
881 use tempfile::NamedTempFile;
882
883 #[test]
884 fn test_basic_config() -> Result<(), ConfigError> {
885 let config = new_test_config();
886
887 config.set_param("test_key", "test_value")?;
889
890 let value: String = config.get_param("test_key")?;
892 assert_eq!(value, "test_value");
893
894 std::env::set_var("TEST_KEY", "env_value");
896 let value: String = config.get_param("test_key")?;
897 assert_eq!(value, "env_value");
898
899 Ok(())
900 }
901
902 #[test]
903 fn test_complex_type() -> Result<(), ConfigError> {
904 #[derive(Deserialize, Debug, PartialEq)]
905 struct TestStruct {
906 field1: String,
907 field2: i32,
908 }
909
910 let config = new_test_config();
911
912 config.set_param(
914 "complex_key",
915 serde_json::json!({
916 "field1": "hello",
917 "field2": 42
918 }),
919 )?;
920
921 let value: TestStruct = config.get_param("complex_key")?;
922 assert_eq!(value.field1, "hello");
923 assert_eq!(value.field2, 42);
924
925 Ok(())
926 }
927
928 #[test]
929 fn test_missing_value() {
930 let config = new_test_config();
931
932 let result: Result<String, ConfigError> = config.get_param("nonexistent_key");
933 assert!(matches!(result, Err(ConfigError::NotFound(_))));
934 }
935
936 #[test]
937 fn test_yaml_formatting() -> Result<(), ConfigError> {
938 let config_file = NamedTempFile::new().unwrap();
939 let secrets_file = NamedTempFile::new().unwrap();
940 let config = Config::new_with_file_secrets(config_file.path(), secrets_file.path())?;
941
942 config.set_param("key1", "value1")?;
943 config.set_param("key2", 42)?;
944
945 let content = std::fs::read_to_string(config_file.path())?;
947 assert!(content.contains("key1: value1"));
948 assert!(content.contains("key2: 42"));
949
950 Ok(())
951 }
952
953 #[test]
954 fn test_value_management() -> Result<(), ConfigError> {
955 let config = new_test_config();
956
957 config.set_param("test_key", "test_value")?;
958 config.set_param("another_key", 42)?;
959 config.set_param("third_key", true)?;
960
961 let _values = config.load()?;
962
963 let result: Result<String, ConfigError> = config.get_param("key");
964 assert!(matches!(result, Err(ConfigError::NotFound(_))));
965
966 Ok(())
967 }
968
969 #[test]
970 fn test_file_based_secrets_management() -> Result<(), ConfigError> {
971 let config = new_test_config();
972
973 config.set_secret("key", &"value")?;
974
975 let value: String = config.get_secret("key")?;
976 assert_eq!(value, "value");
977
978 config.delete_secret("key")?;
979
980 let result: Result<String, ConfigError> = config.get_secret("key");
981 assert!(matches!(result, Err(ConfigError::NotFound(_))));
982
983 Ok(())
984 }
985
986 #[test]
987 #[serial]
988 fn test_secret_management() -> Result<(), ConfigError> {
989 let config = new_test_config();
990
991 config.set_secret("api_key", &Value::String("secret123".to_string()))?;
993 let value: String = config.get_secret("api_key")?;
994 assert_eq!(value, "secret123");
995
996 std::env::set_var("API_KEY", "env_secret");
998 let value: String = config.get_secret("api_key")?;
999 assert_eq!(value, "env_secret");
1000 std::env::remove_var("API_KEY");
1001
1002 config.delete_secret("api_key")?;
1004 let result: Result<String, ConfigError> = config.get_secret("api_key");
1005 assert!(matches!(result, Err(ConfigError::NotFound(_))));
1006
1007 Ok(())
1008 }
1009
1010 #[test]
1011 #[serial]
1012 fn test_multiple_secrets() -> Result<(), ConfigError> {
1013 let config = new_test_config();
1014
1015 config.set_secret("key1", &Value::String("secret1".to_string()))?;
1017 config.set_secret("key2", &Value::String("secret2".to_string()))?;
1018
1019 let value1: String = config.get_secret("key1")?;
1021 let value2: String = config.get_secret("key2")?;
1022 assert_eq!(value1, "secret1");
1023 assert_eq!(value2, "secret2");
1024
1025 config.delete_secret("key1")?;
1027
1028 let result1: Result<String, ConfigError> = config.get_secret("key1");
1030 let value2: String = config.get_secret("key2")?;
1031 assert!(matches!(result1, Err(ConfigError::NotFound(_))));
1032 assert_eq!(value2, "secret2");
1033
1034 Ok(())
1035 }
1036
1037 #[test]
1038 fn test_concurrent_writes() -> Result<(), ConfigError> {
1039 use std::sync::{Arc, Barrier, Mutex};
1040 use std::thread;
1041
1042 let config = Arc::new(new_test_config());
1043 let barrier = Arc::new(Barrier::new(3)); let values = Arc::new(Mutex::new(Mapping::new()));
1045 let mut handles = vec![];
1046
1047 config.save_values(Default::default())?;
1049
1050 for i in 0..3 {
1052 let config = Arc::clone(&config);
1053 let barrier = Arc::clone(&barrier);
1054 let values = Arc::clone(&values);
1055 let handle = thread::spawn(move || -> Result<(), ConfigError> {
1056 barrier.wait();
1058
1059 let mut values = values.lock().unwrap();
1061 values.insert(
1062 serde_yaml::to_value(format!("key{}", i)).unwrap(),
1063 serde_yaml::to_value(format!("value{}", i)).unwrap(),
1064 );
1065
1066 config.save_values(values.clone())?;
1068 Ok(())
1069 });
1070 handles.push(handle);
1071 }
1072
1073 for handle in handles {
1075 handle.join().unwrap()?;
1076 }
1077
1078 let final_values = config.all_values()?;
1080
1081 println!("Final values: {:?}", final_values);
1083
1084 assert_eq!(
1085 final_values.len(),
1086 3,
1087 "Expected 3 values, got {}",
1088 final_values.len()
1089 );
1090
1091 for i in 0..3 {
1092 let key = format!("key{}", i);
1093 let value = format!("value{}", i);
1094 assert!(
1095 final_values.contains_key(&key),
1096 "Missing key {} in final values",
1097 key
1098 );
1099 assert_eq!(
1100 final_values.get(&key).unwrap(),
1101 &Value::String(value),
1102 "Incorrect value for key {}",
1103 key
1104 );
1105 }
1106
1107 Ok(())
1108 }
1109
1110 #[test]
1111 fn test_config_recovery_from_backup() -> Result<(), ConfigError> {
1112 let config_file = NamedTempFile::new().unwrap();
1113 let secrets_file = NamedTempFile::new().unwrap();
1114 let config = Config::new_with_file_secrets(config_file.path(), secrets_file.path())?;
1115
1116 config.set_param("key1", "value1")?;
1118
1119 let backup_paths = config.get_backup_paths();
1121 println!("Backup paths: {:?}", backup_paths);
1122 for (i, path) in backup_paths.iter().enumerate() {
1123 println!("Backup {} exists: {}", i, path.exists());
1124 }
1125
1126 config.set_param("key2", 42)?;
1128
1129 for (i, path) in backup_paths.iter().enumerate() {
1131 println!(
1132 "After second write - Backup {} exists: {}",
1133 i,
1134 path.exists()
1135 );
1136 }
1137
1138 std::fs::write(config_file.path(), "invalid: yaml: content: [unclosed")?;
1140
1141 let recovered_values = config.all_values()?;
1143 println!("Recovered values: {:?}", recovered_values);
1144
1145 assert!(
1147 !recovered_values.is_empty(),
1148 "Should have recovered at least one key"
1149 );
1150
1151 Ok(())
1152 }
1153
1154 #[test]
1155 fn test_config_recovery_creates_fresh_file() -> Result<(), ConfigError> {
1156 let config_file = NamedTempFile::new().unwrap();
1157 let secrets_file = NamedTempFile::new().unwrap();
1158 let config = Config::new_with_file_secrets(config_file.path(), secrets_file.path())?;
1159
1160 std::fs::write(config_file.path(), "invalid: yaml: content: [unclosed")?;
1162
1163 let recovered_values = config.all_values()?;
1165
1166 assert_eq!(recovered_values.len(), 0);
1168
1169 let file_content = std::fs::read_to_string(config_file.path())?;
1171
1172 let parsed: serde_yaml::Value = serde_yaml::from_str(&file_content)?;
1174 assert!(parsed.is_mapping());
1175
1176 let reloaded_values = config.all_values()?;
1178 assert_eq!(reloaded_values.len(), 0);
1179
1180 Ok(())
1181 }
1182
1183 #[test]
1184 fn test_config_file_creation_when_missing() -> Result<(), ConfigError> {
1185 let config_file = NamedTempFile::new().unwrap();
1186 let secrets_file = NamedTempFile::new().unwrap();
1187 let config_path = config_file.path().to_path_buf();
1188 let config = Config::new_with_file_secrets(&config_path, secrets_file.path())?;
1189
1190 std::fs::remove_file(&config_path)?;
1192 assert!(!config_path.exists());
1193
1194 let values = config.all_values()?;
1196
1197 assert_eq!(values.len(), 0);
1199
1200 assert!(config_path.exists());
1202
1203 let file_content = std::fs::read_to_string(&config_path)?;
1205 let parsed: serde_yaml::Value = serde_yaml::from_str(&file_content)?;
1206 assert!(parsed.is_mapping());
1207
1208 let reloaded_values = config.all_values()?;
1210 assert_eq!(reloaded_values.len(), 0);
1211
1212 Ok(())
1213 }
1214
1215 #[test]
1216 fn test_config_recovery_from_backup_when_missing() -> Result<(), ConfigError> {
1217 let config_file = NamedTempFile::new().unwrap();
1218 let secrets_file = NamedTempFile::new().unwrap();
1219 let config_path = config_file.path().to_path_buf();
1220 let config = Config::new_with_file_secrets(&config_path, secrets_file.path())?;
1221
1222 config.set_param("test_key_backup", "backup_value")?;
1224 config.set_param("another_key", 42)?;
1225
1226 let backup_paths = config.get_backup_paths();
1228 let primary_backup = &backup_paths[0]; config.set_param("third_key", true)?;
1232 assert!(primary_backup.exists(), "Backup should exist after writes");
1233
1234 std::fs::remove_file(&config_path)?;
1236 assert!(!config_path.exists());
1237
1238 let recovered_values = config.all_values()?;
1240
1241 assert!(
1243 !recovered_values.is_empty(),
1244 "Should have recovered data from backup"
1245 );
1246
1247 assert!(config_path.exists(), "Main config file should be restored");
1249
1250 if let Ok(backup_value) = config.get_param::<String>("test_key_backup") {
1252 assert_eq!(backup_value, "backup_value");
1254 }
1255 Ok(())
1259 }
1260
1261 #[test]
1262 fn test_atomic_write_prevents_corruption() -> Result<(), ConfigError> {
1263 let config_file = NamedTempFile::new().unwrap();
1264 let secrets_file = NamedTempFile::new().unwrap();
1265 let config = Config::new_with_file_secrets(config_file.path(), secrets_file.path())?;
1266
1267 config.set_param("key1", "value1")?;
1269
1270 assert!(config_file.path().exists());
1272 let content = std::fs::read_to_string(config_file.path())?;
1273 assert!(serde_yaml::from_str::<serde_yaml::Value>(&content).is_ok());
1274
1275 let temp_path = config_file.path().with_extension("tmp");
1277 assert!(!temp_path.exists(), "Temporary file should be cleaned up");
1278
1279 Ok(())
1280 }
1281
1282 #[test]
1283 fn test_backup_rotation() -> Result<(), ConfigError> {
1284 let config = new_test_config();
1285
1286 for i in 1..=7 {
1288 config.set_param("version", i)?;
1289 }
1290
1291 let backup_paths = config.get_backup_paths();
1292
1293 let existing_backups: Vec<_> = backup_paths.iter().filter(|p| p.exists()).collect();
1295 assert!(
1296 existing_backups.len() <= 6,
1297 "Should not exceed backup limit"
1298 ); Ok(())
1301 }
1302
1303 #[test]
1304 fn test_env_var_parsing_strings() -> Result<(), ConfigError> {
1305 let value = Config::parse_env_value("ANTHROPIC")?;
1307 assert_eq!(value, Value::String("ANTHROPIC".to_string()));
1308
1309 let value = Config::parse_env_value("hello world")?;
1311 assert_eq!(value, Value::String("hello world".to_string()));
1312
1313 let value = Config::parse_env_value("\"ANTHROPIC\"")?;
1315 assert_eq!(value, Value::String("ANTHROPIC".to_string()));
1316
1317 let value = Config::parse_env_value("")?;
1319 assert_eq!(value, Value::String("".to_string()));
1320
1321 Ok(())
1322 }
1323
1324 #[test]
1325 fn test_env_var_parsing_numbers() -> Result<(), ConfigError> {
1326 let value = Config::parse_env_value("42")?;
1328 assert_eq!(value, Value::Number(42.into()));
1329
1330 let value = Config::parse_env_value("-123")?;
1331 assert_eq!(value, Value::Number((-123).into()));
1332
1333 let value = Config::parse_env_value("3.41")?;
1335 assert!(matches!(value, Value::Number(_)));
1336 if let Value::Number(n) = value {
1337 assert_eq!(n.as_f64().unwrap(), 3.41);
1338 }
1339
1340 let value = Config::parse_env_value("0.01")?;
1341 assert!(matches!(value, Value::Number(_)));
1342 if let Value::Number(n) = value {
1343 assert_eq!(n.as_f64().unwrap(), 0.01);
1344 }
1345
1346 let value = Config::parse_env_value("0")?;
1348 assert_eq!(value, Value::Number(0.into()));
1349
1350 let value = Config::parse_env_value("0.0")?;
1351 assert!(matches!(value, Value::Number(_)));
1352 if let Value::Number(n) = value {
1353 assert_eq!(n.as_f64().unwrap(), 0.0);
1354 }
1355
1356 let value = Config::parse_env_value(".5")?;
1358 assert!(matches!(value, Value::Number(_)));
1359 if let Value::Number(n) = value {
1360 assert_eq!(n.as_f64().unwrap(), 0.5);
1361 }
1362
1363 let value = Config::parse_env_value(".00001")?;
1364 assert!(matches!(value, Value::Number(_)));
1365 if let Value::Number(n) = value {
1366 assert_eq!(n.as_f64().unwrap(), 0.00001);
1367 }
1368
1369 Ok(())
1370 }
1371
1372 #[test]
1373 fn test_env_var_parsing_booleans() -> Result<(), ConfigError> {
1374 let value = Config::parse_env_value("true")?;
1376 assert_eq!(value, Value::Bool(true));
1377
1378 let value = Config::parse_env_value("True")?;
1379 assert_eq!(value, Value::Bool(true));
1380
1381 let value = Config::parse_env_value("TRUE")?;
1382 assert_eq!(value, Value::Bool(true));
1383
1384 let value = Config::parse_env_value("false")?;
1386 assert_eq!(value, Value::Bool(false));
1387
1388 let value = Config::parse_env_value("False")?;
1389 assert_eq!(value, Value::Bool(false));
1390
1391 let value = Config::parse_env_value("FALSE")?;
1392 assert_eq!(value, Value::Bool(false));
1393
1394 Ok(())
1395 }
1396
1397 #[test]
1398 fn test_env_var_parsing_json() -> Result<(), ConfigError> {
1399 let value = Config::parse_env_value("{\"host\": \"localhost\", \"port\": 8080}")?;
1401 assert!(matches!(value, Value::Object(_)));
1402 if let Value::Object(obj) = value {
1403 assert_eq!(
1404 obj.get("host"),
1405 Some(&Value::String("localhost".to_string()))
1406 );
1407 assert_eq!(obj.get("port"), Some(&Value::Number(8080.into())));
1408 }
1409
1410 let value = Config::parse_env_value("[1, 2, 3]")?;
1412 assert!(matches!(value, Value::Array(_)));
1413 if let Value::Array(arr) = value {
1414 assert_eq!(arr.len(), 3);
1415 assert_eq!(arr[0], Value::Number(1.into()));
1416 assert_eq!(arr[1], Value::Number(2.into()));
1417 assert_eq!(arr[2], Value::Number(3.into()));
1418 }
1419
1420 let value = Config::parse_env_value("null")?;
1422 assert_eq!(value, Value::Null);
1423
1424 Ok(())
1425 }
1426
1427 #[test]
1428 fn test_env_var_parsing_edge_cases() -> Result<(), ConfigError> {
1429 let value = Config::parse_env_value(" 42 ")?;
1431 assert_eq!(value, Value::Number(42.into()));
1432
1433 let value = Config::parse_env_value(" true ")?;
1434 assert_eq!(value, Value::Bool(true));
1435
1436 let value = Config::parse_env_value("123abc")?;
1438 assert_eq!(value, Value::String("123abc".to_string()));
1439
1440 let value = Config::parse_env_value("abc123")?;
1441 assert_eq!(value, Value::String("abc123".to_string()));
1442
1443 let value = Config::parse_env_value("truthy")?;
1445 assert_eq!(value, Value::String("truthy".to_string()));
1446
1447 let value = Config::parse_env_value("falsy")?;
1448 assert_eq!(value, Value::String("falsy".to_string()));
1449
1450 Ok(())
1451 }
1452
1453 #[test]
1454 fn test_env_var_parsing_numeric_edge_cases() -> Result<(), ConfigError> {
1455 let value = Config::parse_env_value("007")?;
1457 assert_eq!(value, Value::Number(7.into()));
1458
1459 let value = Config::parse_env_value("9223372036854775807")?; assert_eq!(value, Value::Number(9223372036854775807i64.into()));
1462
1463 let value = Config::parse_env_value("1e10")?;
1465 assert!(matches!(value, Value::Number(_)));
1466 if let Value::Number(n) = value {
1467 assert_eq!(n.as_f64().unwrap(), 1e10);
1468 }
1469
1470 let value = Config::parse_env_value("inf")?;
1472 assert_eq!(value, Value::String("inf".to_string()));
1473
1474 Ok(())
1475 }
1476
1477 #[test]
1478 fn test_env_var_with_config_integration() -> Result<(), ConfigError> {
1479 let config = new_test_config();
1480
1481 std::env::set_var("PROVIDER", "ANTHROPIC");
1483 let value: String = config.get_param("provider")?;
1484 assert_eq!(value, "ANTHROPIC");
1485
1486 std::env::set_var("PORT", "8080");
1488 let value: i32 = config.get_param("port")?;
1489 assert_eq!(value, 8080);
1490
1491 std::env::set_var("ENABLED", "true");
1493 let value: bool = config.get_param("enabled")?;
1494 assert!(value);
1495
1496 std::env::set_var("CONFIG", "{\"debug\": true, \"level\": 5}");
1498 #[derive(Deserialize, Debug, PartialEq)]
1499 struct TestConfig {
1500 debug: bool,
1501 level: i32,
1502 }
1503 let value: TestConfig = config.get_param("config")?;
1504 assert!(value.debug);
1505 assert_eq!(value.level, 5);
1506
1507 std::env::remove_var("PROVIDER");
1509 std::env::remove_var("PORT");
1510 std::env::remove_var("ENABLED");
1511 std::env::remove_var("CONFIG");
1512
1513 Ok(())
1514 }
1515
1516 #[test]
1517 fn test_env_var_precedence_over_config_file() -> Result<(), ConfigError> {
1518 let config = new_test_config();
1519
1520 config.set_param("test_precedence", "file_value")?;
1522
1523 let value: String = config.get_param("test_precedence")?;
1525 assert_eq!(value, "file_value");
1526
1527 std::env::set_var("TEST_PRECEDENCE", "env_value");
1529
1530 let value: String = config.get_param("test_precedence")?;
1532 assert_eq!(value, "env_value");
1533
1534 std::env::remove_var("TEST_PRECEDENCE");
1536
1537 Ok(())
1538 }
1539
1540 #[test]
1541 fn get_secrets_primary_from_env_uses_env_for_secondary() {
1542 temp_env::with_vars(
1543 [
1544 ("TEST_PRIMARY", Some("primary_env")),
1545 ("TEST_SECONDARY", Some("secondary_env")),
1546 ],
1547 || {
1548 let config = new_test_config();
1549 let secrets = config
1550 .get_secrets("TEST_PRIMARY", &["TEST_SECONDARY"])
1551 .unwrap();
1552
1553 assert_eq!(secrets["TEST_PRIMARY"], "primary_env");
1554 assert_eq!(secrets["TEST_SECONDARY"], "secondary_env");
1555 },
1556 );
1557 }
1558
1559 #[test]
1560 fn get_secrets_primary_from_secret_uses_secret_for_secondary() {
1561 temp_env::with_vars(
1562 [("TEST_PRIMARY", None::<&str>), ("TEST_SECONDARY", None)],
1563 || {
1564 let config = new_test_config();
1565 config
1566 .set_secret("TEST_PRIMARY", &"primary_secret")
1567 .unwrap();
1568 config
1569 .set_secret("TEST_SECONDARY", &"secondary_secret")
1570 .unwrap();
1571
1572 let secrets = config
1573 .get_secrets("TEST_PRIMARY", &["TEST_SECONDARY"])
1574 .unwrap();
1575
1576 assert_eq!(secrets["TEST_PRIMARY"], "primary_secret");
1577 assert_eq!(secrets["TEST_SECONDARY"], "secondary_secret");
1578 },
1579 );
1580 }
1581
1582 #[test]
1583 fn get_secrets_primary_missing_returns_error() {
1584 temp_env::with_vars([("TEST_PRIMARY", None::<&str>)], || {
1585 let config = new_test_config();
1586
1587 let result = config.get_secrets("TEST_PRIMARY", &[]);
1588
1589 assert!(matches!(result, Err(ConfigError::NotFound(_))));
1590 });
1591 }
1592
1593 fn new_test_config() -> Config {
1594 let config_file = NamedTempFile::new().unwrap();
1595 let secrets_file = NamedTempFile::new().unwrap();
1596 Config::new_with_file_secrets(config_file.path(), secrets_file.path()).unwrap()
1597 }
1598}