Skip to main content

aster/config/
base.rs

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
56/// Configuration management for aster.
57///
58/// This module provides a flexible configuration system that supports:
59/// - Dynamic configuration keys
60/// - Multiple value types through serde deserialization
61/// - Environment variable overrides
62/// - YAML-based configuration file storage
63/// - Hot reloading of configuration changes
64/// - Secure secret storage in system keyring
65///
66/// Configuration values are loaded with the following precedence:
67/// 1. Environment variables (exact key match)
68/// 2. Configuration file (~/.config/aster/config.yaml by default)
69///
70/// Secrets are loaded with the following precedence:
71/// 1. Environment variables (exact key match)
72/// 2. System keyring (which can be disabled with ASTER_DISABLE_KEYRING)
73/// 3. If the keyring is disabled, secrets are stored in a secrets file
74///    (~/.config/aster/secrets.yaml by default)
75///
76/// # Examples
77///
78/// ```no_run
79/// use aster::config::Config;
80/// use serde::Deserialize;
81///
82/// // Get a string value
83/// let config = Config::global();
84/// let api_key: String = config.get_param("OPENAI_API_KEY").unwrap();
85///
86/// // Get a complex type
87/// #[derive(Deserialize)]
88/// struct ServerConfig {
89///     host: String,
90///     port: u16,
91/// }
92///
93/// let server_config: ServerConfig = config.get_param("server").unwrap();
94/// ```
95///
96/// # Naming Convention
97/// we recommend snake_case for keys, and will convert to UPPERCASE when
98/// checking for environment overrides. e.g. openai_api_key will check for an
99/// environment variable OPENAI_API_KEY
100///
101/// For aster-specific configuration, consider prefixing with "aster_" to avoid conflicts.
102pub 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
113// Global instance
114static 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    /// Get the global configuration instance.
219    ///
220    /// This will initialize the configuration with the default path (~/.config/aster/config.yaml)
221    /// if it hasn't been initialized yet.
222    pub fn global() -> &'static Config {
223        GLOBAL_CONFIG.get_or_init(Config::default)
224    }
225
226    /// Create a new configuration instance with custom paths
227    ///
228    /// This is primarily useful for testing or for applications that need
229    /// to manage multiple configuration files.
230    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    /// Create a new configuration instance with custom paths
241    ///
242    /// This is primarily useful for testing or for applications that need
243    /// to manage multiple configuration files.
244    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            // Config file doesn't exist, try to recover from backup first
274            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            // No backup available, create a default config
282            tracing::info!("No backup found, creating default configuration");
283
284            // Try to load from init-config.yaml if it exists, otherwise use empty config
285            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    // Helper method to create and save default config with consistent logging
302    fn create_and_save_default_config(
303        &self,
304        default_config: Mapping,
305    ) -> Result<Mapping, ConfigError> {
306        // Try to write the default config to disk
307        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                // Even if we can't write to disk, return config so app can still run
322                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                // Try to recover from backup
339                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                // Last resort: create a fresh default config file
345                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                                // Successfully parsed backup, restore it as the main config
364                                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    // Get list of backup file paths in order of preference
399    fn get_backup_paths(&self) -> Vec<PathBuf> {
400        let mut paths = Vec::new();
401
402        // Primary backup (created by backup_config endpoint)
403        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        // Timestamped backups
410        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        // Create backup before writing new config
427        self.create_backup_if_needed()?;
428
429        // Convert to YAML for storage
430        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        // Write to a temporary file first for atomic operation
438        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            // Acquire an exclusive lock
448            file.lock_exclusive()
449                .map_err(|e| ConfigError::LockError(e.to_string()))?;
450
451            // Write the contents using the same file handle
452            file.write_all(yaml_value.as_bytes())?;
453            file.sync_all()?;
454
455            // Unlock is handled automatically when file is dropped
456        }
457
458        // Atomically replace the original file
459        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    // Create backup of current config file if it exists and is valid
474    fn create_backup_if_needed(&self) -> Result<(), ConfigError> {
475        if !self.config_path.exists() {
476            return Ok(());
477        }
478
479        // Check if current config is valid before backing it up
480        let current_content = std::fs::read_to_string(&self.config_path)?;
481        if parse_yaml_content(&current_content).is_err() {
482            // Don't back up corrupted files
483            return Ok(());
484        }
485
486        // Rotate existing backups
487        self.rotate_backups()?;
488
489        // Create new backup
490        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                // Don't fail the entire operation if backup fails
498            } else {
499                tracing::debug!("Created config backup: {:?}", backup_path);
500            }
501        }
502
503        Ok(())
504    }
505
506    // Rotate backup files to keep the most recent ones
507    fn rotate_backups(&self) -> Result<(), ConfigError> {
508        if let Some(file_name) = self.config_path.file_name() {
509            // Move .bak.4 to .bak.5, .bak.3 to .bak.4, etc.
510            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(&current_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(&current_path, &next_path);
521                }
522            }
523
524            // Move .bak to .bak.1
525            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    /// Parse an environment variable value into a JSON Value.
571    ///
572    /// This function tries to intelligently parse environment variable values:
573    /// 1. First attempts JSON parsing (for structured data)
574    /// 2. If that fails, tries primitive type parsing for common cases
575    /// 3. Falls back to string if nothing else works
576    fn parse_env_value(val: &str) -> Result<Value, ConfigError> {
577        // First try JSON parsing - this handles quoted strings, objects, arrays, etc.
578        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    // check all possible places for a parameter
604    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    // save a parameter in the appropriate location based on if it's secret or not
613    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    /// Get a configuration value (non-secret).
625    ///
626    /// This will attempt to get the value from:
627    /// 1. Environment variable with the exact key name
628    /// 2. Configuration file
629    ///
630    /// The value will be deserialized into the requested type. This works with
631    /// both simple types (String, i32, etc.) and complex types that implement
632    /// serde::Deserialize.
633    ///
634    /// # Errors
635    ///
636    /// Returns a ConfigError if:
637    /// - The key doesn't exist in either environment or config file
638    /// - The value cannot be deserialized into the requested type
639    /// - There is an error reading the config file
640    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    /// Set a configuration value in the config file (non-secret).
655    ///
656    /// This will immediately write the value to the config file. The value
657    /// can be any type that can be serialized to JSON/YAML.
658    ///
659    /// Note that this does not affect environment variables - those can only
660    /// be set through the system environment.
661    ///
662    /// # Errors
663    ///
664    /// Returns a ConfigError if:
665    /// - There is an error reading or writing the config file
666    /// - There is an error serializing the value
667    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    /// Delete a configuration value in the config file.
675    ///
676    /// This will immediately write the value to the config file. The value
677    /// can be any type that can be serialized to JSON/YAML.
678    ///
679    /// Note that this does not affect environment variables - those can only
680    /// be set through the system environment.
681    ///
682    /// # Errors
683    ///
684    /// Returns a ConfigError if:
685    /// - There is an error reading or writing the config file
686    /// - There is an error serializing the value
687    pub fn delete(&self, key: &str) -> Result<(), ConfigError> {
688        // Lock before reading to prevent race condition.
689        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    /// Get a secret value.
698    ///
699    /// This will attempt to get the value from:
700    /// 1. Environment variable with the exact key name
701    /// 2. System keyring
702    ///
703    /// The value will be deserialized into the requested type. This works with
704    /// both simple types (String, i32, etc.) and complex types that implement
705    /// serde::Deserialize.
706    ///
707    /// # Errors
708    ///
709    /// Returns a ConfigError if:
710    /// - The key doesn't exist in either environment or keyring
711    /// - The value cannot be deserialized into the requested type
712    /// - There is an error accessing the keyring
713    pub fn get_secret<T: for<'de> Deserialize<'de>>(&self, key: &str) -> Result<T, ConfigError> {
714        // First check environment variables (convert to uppercase)
715        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        // Then check keyring
722        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    /// Get secrets. If primary is in env, use env for all keys. Otherwise use secret storage.
730    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    /// Set a secret value in the system keyring.
755    ///
756    /// This will store the value in a single JSON object in the system keyring,
757    /// alongside any other secrets. The value can be any type that can be
758    /// serialized to JSON.
759    ///
760    /// Note that this does not affect environment variables - those can only
761    /// be set through the system environment.
762    ///
763    /// # Errors
764    ///
765    /// Returns a ConfigError if:
766    /// - There is an error accessing the keyring
767    /// - There is an error serializing the value
768    pub fn set_secret<V>(&self, key: &str, value: &V) -> Result<(), ConfigError>
769    where
770        V: Serialize,
771    {
772        // Lock before reading to prevent race condition.
773        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    /// Delete a secret from the system keyring.
793    ///
794    /// This will remove the specified key from the JSON object in the system keyring.
795    /// Other secrets will remain unchanged.
796    ///
797    /// # Errors
798    ///
799    /// Returns a ConfigError if:
800    /// - There is an error accessing the keyring
801    /// - There is an error serializing the remaining values
802    pub fn delete_secret(&self, key: &str) -> Result<(), ConfigError> {
803        // Lock before reading to prevent race condition.
804        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
839/// Load init-config.yaml from workspace root if it exists.
840/// This function is shared between the config recovery and the init_config endpoint.
841pub 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        // Set a simple string value
888        config.set_param("test_key", "test_value")?;
889
890        // Test simple string retrieval
891        let value: String = config.get_param("test_key")?;
892        assert_eq!(value, "test_value");
893
894        // Test with environment variable override
895        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        // Set a complex value
913        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        // Read the file directly to check YAML formatting
946        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        // Test setting and getting a simple secret
992        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        // Test environment variable override
997        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        // Test deleting a secret
1003        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        // Set multiple secrets
1016        config.set_secret("key1", &Value::String("secret1".to_string()))?;
1017        config.set_secret("key2", &Value::String("secret2".to_string()))?;
1018
1019        // Verify both exist
1020        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        // Delete one secret
1026        config.delete_secret("key1")?;
1027
1028        // Verify key1 is gone but key2 remains
1029        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)); // For 3 concurrent threads
1044        let values = Arc::new(Mutex::new(Mapping::new()));
1045        let mut handles = vec![];
1046
1047        // Initialize with empty values
1048        config.save_values(Default::default())?;
1049
1050        // Spawn 3 threads that will try to write simultaneously
1051        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                // Wait for all threads to reach this point
1057                barrier.wait();
1058
1059                // Get the lock and update values
1060                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                // Write all values
1067                config.save_values(values.clone())?;
1068                Ok(())
1069            });
1070            handles.push(handle);
1071        }
1072
1073        // Wait for all threads to complete
1074        for handle in handles {
1075            handle.join().unwrap()?;
1076        }
1077
1078        // Verify all values were written correctly
1079        let final_values = config.all_values()?;
1080
1081        // Print the final values for debugging
1082        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        // Create a valid config first
1117        config.set_param("key1", "value1")?;
1118
1119        // Verify the backup was created by the first write
1120        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        // Make another write to ensure backup is created
1127        config.set_param("key2", 42)?;
1128
1129        // Check again
1130        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        // Corrupt the main config file
1139        std::fs::write(config_file.path(), "invalid: yaml: content: [unclosed")?;
1140
1141        // Try to load values - should recover from backup
1142        let recovered_values = config.all_values()?;
1143        println!("Recovered values: {:?}", recovered_values);
1144
1145        // Should have recovered the data
1146        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        // Create a corrupted config file with no backup
1161        std::fs::write(config_file.path(), "invalid: yaml: content: [unclosed")?;
1162
1163        // Try to load values - should create a fresh default config
1164        let recovered_values = config.all_values()?;
1165
1166        // Should return empty config
1167        assert_eq!(recovered_values.len(), 0);
1168
1169        // Verify that a clean config file was written to disk
1170        let file_content = std::fs::read_to_string(config_file.path())?;
1171
1172        // Should be valid YAML (empty object)
1173        let parsed: serde_yaml::Value = serde_yaml::from_str(&file_content)?;
1174        assert!(parsed.is_mapping());
1175
1176        // Should be able to load it again without issues
1177        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        // Delete the file to simulate it not existing
1191        std::fs::remove_file(&config_path)?;
1192        assert!(!config_path.exists());
1193
1194        // Try to load values - should create a fresh default config file
1195        let values = config.all_values()?;
1196
1197        // Should return empty config
1198        assert_eq!(values.len(), 0);
1199
1200        // Verify that the config file was created
1201        assert!(config_path.exists());
1202
1203        // Verify that it's valid YAML
1204        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        // Should be able to load it again without issues
1209        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        // First, create a config with some data
1223        config.set_param("test_key_backup", "backup_value")?;
1224        config.set_param("another_key", 42)?;
1225
1226        // Verify the backup was created
1227        let backup_paths = config.get_backup_paths();
1228        let primary_backup = &backup_paths[0]; // .bak file
1229
1230        // Make sure we have a backup by doing another write
1231        config.set_param("third_key", true)?;
1232        assert!(primary_backup.exists(), "Backup should exist after writes");
1233
1234        // Now delete the main config file to simulate it being lost
1235        std::fs::remove_file(&config_path)?;
1236        assert!(!config_path.exists());
1237
1238        // Try to load values - should recover from backup
1239        let recovered_values = config.all_values()?;
1240
1241        // Should have recovered the data from backup
1242        assert!(
1243            !recovered_values.is_empty(),
1244            "Should have recovered data from backup"
1245        );
1246
1247        // Verify the main config file was restored
1248        assert!(config_path.exists(), "Main config file should be restored");
1249
1250        // Verify we can load the data (using a key that won't conflict with env vars)
1251        if let Ok(backup_value) = config.get_param::<String>("test_key_backup") {
1252            // If we recovered the key, great!
1253            assert_eq!(backup_value, "backup_value");
1254        }
1255        // Note: Due to back up rotation, we might not get the exact same data,
1256        // but we should get some data back
1257
1258        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        // Set initial values
1268        config.set_param("key1", "value1")?;
1269
1270        // Verify the config file exists and is valid
1271        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        // The temp file should not exist after successful write
1276        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        // Create multiple versions to test rotation
1287        for i in 1..=7 {
1288            config.set_param("version", i)?;
1289        }
1290
1291        let backup_paths = config.get_backup_paths();
1292
1293        // Should have backups but not more than our limit
1294        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        ); // .bak + .bak.1 through .bak.5
1299
1300        Ok(())
1301    }
1302
1303    #[test]
1304    fn test_env_var_parsing_strings() -> Result<(), ConfigError> {
1305        // Test unquoted strings
1306        let value = Config::parse_env_value("ANTHROPIC")?;
1307        assert_eq!(value, Value::String("ANTHROPIC".to_string()));
1308
1309        // Test strings with spaces
1310        let value = Config::parse_env_value("hello world")?;
1311        assert_eq!(value, Value::String("hello world".to_string()));
1312
1313        // Test JSON quoted strings
1314        let value = Config::parse_env_value("\"ANTHROPIC\"")?;
1315        assert_eq!(value, Value::String("ANTHROPIC".to_string()));
1316
1317        // Test empty string
1318        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        // Test integers
1327        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        // Test floats
1334        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        // Test zero
1347        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        // Test numbers starting with decimal point
1357        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        // Test true variants
1375        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        // Test false variants
1385        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        // Test JSON objects
1400        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        // Test JSON arrays
1411        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        // Test JSON null
1421        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        // Test whitespace handling
1430        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        // Test strings that look like numbers but aren't
1437        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        // Test strings that look like booleans but aren't
1444        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        // Test leading zeros (should be treated as integers, not octal)
1456        let value = Config::parse_env_value("007")?;
1457        assert_eq!(value, Value::Number(7.into()));
1458
1459        // Test large numbers
1460        let value = Config::parse_env_value("9223372036854775807")?; // i64::MAX
1461        assert_eq!(value, Value::Number(9223372036854775807i64.into()));
1462
1463        // Test scientific notation (JSON parsing should handle this correctly)
1464        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        // Test infinity (should be treated as string)
1471        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        // Test string environment variable (the original issue case)
1482        std::env::set_var("PROVIDER", "ANTHROPIC");
1483        let value: String = config.get_param("provider")?;
1484        assert_eq!(value, "ANTHROPIC");
1485
1486        // Test number environment variable
1487        std::env::set_var("PORT", "8080");
1488        let value: i32 = config.get_param("port")?;
1489        assert_eq!(value, 8080);
1490
1491        // Test boolean environment variable
1492        std::env::set_var("ENABLED", "true");
1493        let value: bool = config.get_param("enabled")?;
1494        assert!(value);
1495
1496        // Test JSON object environment variable
1497        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        // Clean up
1508        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        // Set value in config file
1521        config.set_param("test_precedence", "file_value")?;
1522
1523        // Verify file value is returned when no env var
1524        let value: String = config.get_param("test_precedence")?;
1525        assert_eq!(value, "file_value");
1526
1527        // Set environment variable
1528        std::env::set_var("TEST_PRECEDENCE", "env_value");
1529
1530        // Environment variable should take precedence
1531        let value: String = config.get_param("test_precedence")?;
1532        assert_eq!(value, "env_value");
1533
1534        // Clean up
1535        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}