1#![allow(deprecated)]
2use crate::config::{EnvironmentProvider, SystemEnvironmentProvider};
9use crate::{Result, config::Config, error::SubXError};
10use config::{Config as ConfigCrate, ConfigBuilder, Environment, File, builder::DefaultState};
11use log::debug;
12use std::path::{Path, PathBuf};
13use std::sync::{Arc, RwLock};
14
15pub trait ConfigService: Send + Sync {
20 fn get_config(&self) -> Result<Config>;
40
41 fn reload(&self) -> Result<()>;
50
51 fn save_config(&self) -> Result<()>;
60
61 fn save_config_to_file(&self, path: &Path) -> Result<()>;
74
75 fn get_config_file_path(&self) -> Result<PathBuf>;
82
83 fn get_config_value(&self, key: &str) -> Result<String>;
93
94 fn reset_to_defaults(&self) -> Result<()>;
103
104 fn set_config_value(&self, key: &str, value: &str) -> Result<()>;
118}
119
120pub struct ProductionConfigService {
129 config_builder: ConfigBuilder<DefaultState>,
130 cached_config: Arc<RwLock<Option<Config>>>,
131 env_provider: Arc<dyn EnvironmentProvider>,
132}
133
134impl ProductionConfigService {
135 pub fn new() -> Result<Self> {
142 Self::with_env_provider(Arc::new(SystemEnvironmentProvider::new()))
143 }
144
145 pub fn with_env_provider(env_provider: Arc<dyn EnvironmentProvider>) -> Result<Self> {
150 let config_file_path = if let Some(custom_path) = env_provider.get_var("SUBX_CONFIG_PATH") {
152 PathBuf::from(custom_path)
153 } else {
154 Self::user_config_path()
155 };
156
157 let config_builder = ConfigCrate::builder()
158 .add_source(File::with_name("config/default").required(false))
159 .add_source(File::from(config_file_path).required(false))
160 .add_source(Environment::with_prefix("SUBX").separator("_"));
161
162 Ok(Self {
163 config_builder,
164 cached_config: Arc::new(RwLock::new(None)),
165 env_provider,
166 })
167 }
168
169 pub fn with_custom_file(mut self, file_path: PathBuf) -> Result<Self> {
181 self.config_builder = self.config_builder.add_source(File::from(file_path));
182 Ok(self)
183 }
184
185 fn user_config_path() -> PathBuf {
190 dirs::config_dir()
191 .unwrap_or_else(|| PathBuf::from("."))
192 .join("subx")
193 .join("config.toml")
194 }
195
196 fn load_and_validate(&self) -> Result<Config> {
202 debug!("ProductionConfigService: Loading configuration from sources");
203
204 let config_crate = self.config_builder.build_cloned().map_err(|e| {
206 debug!("ProductionConfigService: Config build failed: {}", e);
207 SubXError::config(format!("Failed to build configuration: {}", e))
208 })?;
209
210 let mut app_config = Config::default();
212
213 if let Ok(config) = config_crate.clone().try_deserialize::<Config>() {
215 app_config = config;
216 debug!("ProductionConfigService: Full configuration loaded successfully");
217 } else {
218 debug!("ProductionConfigService: Full deserialization failed, attempting partial load");
219
220 if let Ok(raw_map) = config_crate
222 .try_deserialize::<std::collections::HashMap<String, serde_json::Value>>()
223 {
224 if let Some(ai_section) = raw_map.get("ai") {
226 if let Some(ai_obj) = ai_section.as_object() {
227 if let Some(api_key) = ai_obj.get("apikey").and_then(|v| v.as_str()) {
229 app_config.ai.api_key = Some(api_key.to_string());
230 debug!(
231 "ProductionConfigService: AI API key loaded from SUBX_AI_APIKEY"
232 );
233 }
234 if let Some(provider) = ai_obj.get("provider").and_then(|v| v.as_str()) {
235 app_config.ai.provider = provider.to_string();
236 debug!(
237 "ProductionConfigService: AI provider loaded from SUBX_AI_PROVIDER"
238 );
239 }
240 if let Some(model) = ai_obj.get("model").and_then(|v| v.as_str()) {
241 app_config.ai.model = model.to_string();
242 debug!("ProductionConfigService: AI model loaded from SUBX_AI_MODEL");
243 }
244 if let Some(base_url) = ai_obj.get("base_url").and_then(|v| v.as_str()) {
245 app_config.ai.base_url = base_url.to_string();
246 debug!(
247 "ProductionConfigService: AI base URL loaded from SUBX_AI_BASE_URL"
248 );
249 }
250 }
251 }
252 }
253 }
254
255 if app_config.ai.api_key.is_none() {
258 if let Some(api_key) = self.env_provider.get_var("OPENAI_API_KEY") {
259 debug!("ProductionConfigService: Found OPENAI_API_KEY environment variable");
260 app_config.ai.api_key = Some(api_key);
261 }
262 }
263
264 if let Some(base_url) = self.env_provider.get_var("OPENAI_BASE_URL") {
266 debug!("ProductionConfigService: Found OPENAI_BASE_URL environment variable");
267 app_config.ai.base_url = base_url;
268 }
269
270 crate::config::validator::validate_config(&app_config).map_err(|e| {
272 debug!("ProductionConfigService: Config validation failed: {}", e);
273 SubXError::config(format!("Configuration validation failed: {}", e))
274 })?;
275
276 debug!("ProductionConfigService: Configuration loaded and validated successfully");
277 Ok(app_config)
278 }
279
280 fn validate_and_set_value(&self, config: &mut Config, key: &str, value: &str) -> Result<()> {
284 use crate::config::field_validator;
285
286 field_validator::validate_field(key, value)?;
288
289 self.set_value_internal(config, key, value)?;
291
292 self.validate_configuration(config)?;
294
295 Ok(())
296 }
297
298 fn set_value_internal(&self, config: &mut Config, key: &str, value: &str) -> Result<()> {
300 use crate::config::OverflowStrategy;
301 use crate::config::validation::*;
302 use crate::error::SubXError;
303
304 let parts: Vec<&str> = key.split('.').collect();
305 match parts.as_slice() {
306 ["ai", "provider"] => {
307 config.ai.provider = value.to_string();
308 }
309 ["ai", "api_key"] => {
310 if !value.is_empty() {
311 config.ai.api_key = Some(value.to_string());
312 } else {
313 config.ai.api_key = None;
314 }
315 }
316 ["ai", "model"] => {
317 config.ai.model = value.to_string();
318 }
319 ["ai", "base_url"] => {
320 config.ai.base_url = value.to_string();
321 }
322 ["ai", "max_sample_length"] => {
323 let v = value.parse().unwrap(); config.ai.max_sample_length = v;
325 }
326 ["ai", "temperature"] => {
327 let v = value.parse().unwrap(); config.ai.temperature = v;
329 }
330 ["ai", "max_tokens"] => {
331 let v = value.parse().unwrap(); config.ai.max_tokens = v;
333 }
334 ["ai", "retry_attempts"] => {
335 let v = value.parse().unwrap(); config.ai.retry_attempts = v;
337 }
338 ["ai", "retry_delay_ms"] => {
339 let v = value.parse().unwrap(); config.ai.retry_delay_ms = v;
341 }
342 ["ai", "request_timeout_seconds"] => {
343 let v = value.parse().unwrap(); config.ai.request_timeout_seconds = v;
345 }
346 ["formats", "default_output"] => {
347 config.formats.default_output = value.to_string();
348 }
349 ["formats", "preserve_styling"] => {
350 let v = parse_bool(value)?;
351 config.formats.preserve_styling = v;
352 }
353 ["formats", "default_encoding"] => {
354 config.formats.default_encoding = value.to_string();
355 }
356 ["formats", "encoding_detection_confidence"] => {
357 let v = value.parse().unwrap(); config.formats.encoding_detection_confidence = v;
359 }
360 ["sync", "max_offset_seconds"] => {
361 let v = value.parse().unwrap(); config.sync.max_offset_seconds = v;
363 }
364 ["sync", "default_method"] => {
365 config.sync.default_method = value.to_string();
366 }
367 ["sync", "vad", "enabled"] => {
368 let v = parse_bool(value)?;
369 config.sync.vad.enabled = v;
370 }
371 ["sync", "vad", "sensitivity"] => {
372 let v = value.parse().unwrap(); config.sync.vad.sensitivity = v;
374 }
375 ["sync", "vad", "padding_chunks"] => {
376 let v = value.parse().unwrap(); config.sync.vad.padding_chunks = v;
378 }
379 ["sync", "vad", "min_speech_duration_ms"] => {
380 let v = value.parse().unwrap(); config.sync.vad.min_speech_duration_ms = v;
382 }
383 ["general", "backup_enabled"] => {
384 let v = parse_bool(value)?;
385 config.general.backup_enabled = v;
386 }
387 ["general", "max_concurrent_jobs"] => {
388 let v = value.parse().unwrap(); config.general.max_concurrent_jobs = v;
390 }
391 ["general", "task_timeout_seconds"] => {
392 let v = value.parse().unwrap(); config.general.task_timeout_seconds = v;
394 }
395 ["general", "enable_progress_bar"] => {
396 let v = parse_bool(value)?;
397 config.general.enable_progress_bar = v;
398 }
399 ["general", "worker_idle_timeout_seconds"] => {
400 let v = value.parse().unwrap(); config.general.worker_idle_timeout_seconds = v;
402 }
403 ["parallel", "max_workers"] => {
404 let v = value.parse().unwrap(); config.parallel.max_workers = v;
406 }
407 ["parallel", "task_queue_size"] => {
408 let v = value.parse().unwrap(); config.parallel.task_queue_size = v;
410 }
411 ["parallel", "enable_task_priorities"] => {
412 let v = parse_bool(value)?;
413 config.parallel.enable_task_priorities = v;
414 }
415 ["parallel", "auto_balance_workers"] => {
416 let v = parse_bool(value)?;
417 config.parallel.auto_balance_workers = v;
418 }
419 ["parallel", "overflow_strategy"] => {
420 config.parallel.overflow_strategy = match value {
421 "Block" => OverflowStrategy::Block,
422 "Drop" => OverflowStrategy::Drop,
423 "Expand" => OverflowStrategy::Expand,
424 _ => unreachable!(), };
426 }
427 _ => {
428 return Err(SubXError::config(format!(
429 "Unknown configuration key: {}",
430 key
431 )));
432 }
433 }
434 Ok(())
435 }
436
437 fn validate_configuration(&self, config: &Config) -> Result<()> {
439 use crate::config::validator;
440 validator::validate_config(config)
441 }
442
443 fn save_config_to_file_with_config(
445 &self,
446 path: &std::path::Path,
447 config: &Config,
448 ) -> Result<()> {
449 let toml_content = toml::to_string_pretty(config)
450 .map_err(|e| SubXError::config(format!("TOML serialization error: {}", e)))?;
451 if let Some(parent) = path.parent() {
452 std::fs::create_dir_all(parent).map_err(|e| {
453 SubXError::config(format!("Failed to create config directory: {}", e))
454 })?;
455 }
456 std::fs::write(path, toml_content)
457 .map_err(|e| SubXError::config(format!("Failed to write config file: {}", e)))?;
458 Ok(())
459 }
460}
461
462impl ConfigService for ProductionConfigService {
463 fn get_config(&self) -> Result<Config> {
464 {
466 let cache = self.cached_config.read().unwrap();
467 if let Some(config) = cache.as_ref() {
468 debug!("ProductionConfigService: Returning cached configuration");
469 return Ok(config.clone());
470 }
471 }
472
473 let app_config = self.load_and_validate()?;
475
476 {
478 let mut cache = self.cached_config.write().unwrap();
479 *cache = Some(app_config.clone());
480 }
481
482 Ok(app_config)
483 }
484
485 fn reload(&self) -> Result<()> {
486 debug!("ProductionConfigService: Reloading configuration");
487
488 {
490 let mut cache = self.cached_config.write().unwrap();
491 *cache = None;
492 }
493
494 self.get_config()?;
496
497 debug!("ProductionConfigService: Configuration reloaded successfully");
498 Ok(())
499 }
500
501 fn save_config(&self) -> Result<()> {
502 let _config = self.get_config()?;
503 let path = self.get_config_file_path()?;
504 self.save_config_to_file(&path)
505 }
506
507 fn save_config_to_file(&self, path: &Path) -> Result<()> {
508 let config = self.get_config()?;
509 let toml_content = toml::to_string_pretty(&config)
510 .map_err(|e| SubXError::config(format!("TOML serialization error: {}", e)))?;
511
512 if let Some(parent) = path.parent() {
513 std::fs::create_dir_all(parent).map_err(|e| {
514 SubXError::config(format!("Failed to create config directory: {}", e))
515 })?;
516 }
517
518 std::fs::write(path, toml_content)
519 .map_err(|e| SubXError::config(format!("Failed to write config file: {}", e)))?;
520
521 Ok(())
522 }
523
524 fn get_config_file_path(&self) -> Result<PathBuf> {
525 if let Some(custom) = self.env_provider.get_var("SUBX_CONFIG_PATH") {
527 return Ok(PathBuf::from(custom));
528 }
529
530 let config_dir = dirs::config_dir()
531 .ok_or_else(|| SubXError::config("Unable to determine config directory"))?;
532 Ok(config_dir.join("subx").join("config.toml"))
533 }
534
535 fn get_config_value(&self, key: &str) -> Result<String> {
536 let config = self.get_config()?;
537 let parts: Vec<&str> = key.split('.').collect();
538 match parts.as_slice() {
539 ["ai", "provider"] => Ok(config.ai.provider.clone()),
540 ["ai", "model"] => Ok(config.ai.model.clone()),
541 ["ai", "api_key"] => Ok(config.ai.api_key.clone().unwrap_or_default()),
542 ["ai", "base_url"] => Ok(config.ai.base_url.clone()),
543 ["ai", "max_sample_length"] => Ok(config.ai.max_sample_length.to_string()),
544 ["ai", "temperature"] => Ok(config.ai.temperature.to_string()),
545 ["ai", "max_tokens"] => Ok(config.ai.max_tokens.to_string()),
546 ["ai", "retry_attempts"] => Ok(config.ai.retry_attempts.to_string()),
547 ["ai", "retry_delay_ms"] => Ok(config.ai.retry_delay_ms.to_string()),
548 ["ai", "request_timeout_seconds"] => Ok(config.ai.request_timeout_seconds.to_string()),
549
550 ["formats", "default_output"] => Ok(config.formats.default_output.clone()),
551 ["formats", "default_encoding"] => Ok(config.formats.default_encoding.clone()),
552 ["formats", "preserve_styling"] => Ok(config.formats.preserve_styling.to_string()),
553 ["formats", "encoding_detection_confidence"] => {
554 Ok(config.formats.encoding_detection_confidence.to_string())
555 }
556
557 ["sync", "default_method"] => Ok(config.sync.default_method.clone()),
558 ["sync", "max_offset_seconds"] => Ok(config.sync.max_offset_seconds.to_string()),
559 ["sync", "vad", "enabled"] => Ok(config.sync.vad.enabled.to_string()),
560 ["sync", "vad", "sensitivity"] => Ok(config.sync.vad.sensitivity.to_string()),
561 ["sync", "vad", "padding_chunks"] => Ok(config.sync.vad.padding_chunks.to_string()),
562 ["sync", "vad", "min_speech_duration_ms"] => {
563 Ok(config.sync.vad.min_speech_duration_ms.to_string())
564 }
565
566 ["general", "backup_enabled"] => Ok(config.general.backup_enabled.to_string()),
567 ["general", "max_concurrent_jobs"] => {
568 Ok(config.general.max_concurrent_jobs.to_string())
569 }
570 ["general", "task_timeout_seconds"] => {
571 Ok(config.general.task_timeout_seconds.to_string())
572 }
573 ["general", "enable_progress_bar"] => {
574 Ok(config.general.enable_progress_bar.to_string())
575 }
576 ["general", "worker_idle_timeout_seconds"] => {
577 Ok(config.general.worker_idle_timeout_seconds.to_string())
578 }
579
580 ["parallel", "max_workers"] => Ok(config.parallel.max_workers.to_string()),
581 ["parallel", "task_queue_size"] => Ok(config.parallel.task_queue_size.to_string()),
582 ["parallel", "enable_task_priorities"] => {
583 Ok(config.parallel.enable_task_priorities.to_string())
584 }
585 ["parallel", "auto_balance_workers"] => {
586 Ok(config.parallel.auto_balance_workers.to_string())
587 }
588 ["parallel", "overflow_strategy"] => {
589 Ok(format!("{:?}", config.parallel.overflow_strategy))
590 }
591
592 _ => Err(SubXError::config(format!(
593 "Unknown configuration key: {}",
594 key
595 ))),
596 }
597 }
598
599 fn set_config_value(&self, key: &str, value: &str) -> Result<()> {
600 let mut config = self.get_config()?;
602
603 self.validate_and_set_value(&mut config, key, value)?;
605
606 crate::config::validator::validate_config(&config)?;
608
609 let path = self.get_config_file_path()?;
611 self.save_config_to_file_with_config(&path, &config)?;
612
613 {
615 let mut cache = self.cached_config.write().unwrap();
616 *cache = Some(config);
617 }
618
619 Ok(())
620 }
621
622 fn reset_to_defaults(&self) -> Result<()> {
623 let default_config = Config::default();
624 let path = self.get_config_file_path()?;
625
626 let toml_content = toml::to_string_pretty(&default_config)
627 .map_err(|e| SubXError::config(format!("TOML serialization error: {}", e)))?;
628
629 if let Some(parent) = path.parent() {
630 std::fs::create_dir_all(parent).map_err(|e| {
631 SubXError::config(format!("Failed to create config directory: {}", e))
632 })?;
633 }
634
635 std::fs::write(&path, toml_content)
636 .map_err(|e| SubXError::config(format!("Failed to write config file: {}", e)))?;
637
638 self.reload()
639 }
640}
641
642impl Default for ProductionConfigService {
643 fn default() -> Self {
644 Self::new().expect("Failed to create default ProductionConfigService")
645 }
646}
647
648#[cfg(test)]
649mod tests {
650 use super::*;
651 use crate::config::TestConfigService;
652 use crate::config::TestEnvironmentProvider;
653 use std::sync::Arc;
654
655 #[test]
656 fn test_production_config_service_creation() {
657 let service = ProductionConfigService::new();
658 assert!(service.is_ok());
659 }
660
661 #[test]
662 fn test_production_config_service_with_custom_file() {
663 let service = ProductionConfigService::new()
664 .unwrap()
665 .with_custom_file(PathBuf::from("test.toml"));
666 assert!(service.is_ok());
667 }
668
669 #[test]
670 fn test_production_service_implements_config_service_trait() {
671 let service = ProductionConfigService::new().unwrap();
672
673 let config1 = service.get_config();
675 assert!(config1.is_ok());
676
677 let reload_result = service.reload();
678 assert!(reload_result.is_ok());
679
680 let config2 = service.get_config();
681 assert!(config2.is_ok());
682 }
683
684 #[test]
685 fn test_config_service_with_openai_api_key() {
686 let test_service = TestConfigService::with_ai_settings_and_key(
688 "openai",
689 "gpt-4.1-mini",
690 "sk-test-openai-key-123",
691 );
692
693 let config = test_service.get_config().unwrap();
694 assert_eq!(
695 config.ai.api_key,
696 Some("sk-test-openai-key-123".to_string())
697 );
698 assert_eq!(config.ai.provider, "openai");
699 assert_eq!(config.ai.model, "gpt-4.1-mini");
700 }
701
702 #[test]
703 fn test_config_service_with_custom_base_url() {
704 let mut config = Config::default();
706 config.ai.base_url = "https://custom.openai.endpoint".to_string();
707
708 let test_service = TestConfigService::new(config);
709 let loaded_config = test_service.get_config().unwrap();
710
711 assert_eq!(loaded_config.ai.base_url, "https://custom.openai.endpoint");
712 }
713
714 #[test]
715 fn test_config_service_with_both_openai_settings() {
716 let mut config = Config::default();
718 config.ai.api_key = Some("sk-test-api-key-combined".to_string());
719 config.ai.base_url = "https://api.custom-openai.com".to_string();
720
721 let test_service = TestConfigService::new(config);
722 let loaded_config = test_service.get_config().unwrap();
723
724 assert_eq!(
725 loaded_config.ai.api_key,
726 Some("sk-test-api-key-combined".to_string())
727 );
728 assert_eq!(loaded_config.ai.base_url, "https://api.custom-openai.com");
729 }
730
731 #[test]
732 fn test_config_service_provider_precedence() {
733 let test_service =
735 TestConfigService::with_ai_settings_and_key("openai", "gpt-4.1", "sk-explicit-key");
736
737 let config = test_service.get_config().unwrap();
738 assert_eq!(config.ai.api_key, Some("sk-explicit-key".to_string()));
739 assert_eq!(config.ai.provider, "openai");
740 assert_eq!(config.ai.model, "gpt-4.1");
741 }
742
743 #[test]
744 fn test_config_service_fallback_behavior() {
745 let test_service = TestConfigService::with_defaults();
747 let config = test_service.get_config().unwrap();
748
749 assert_eq!(config.ai.provider, "openai");
751 assert_eq!(config.ai.model, "gpt-4.1-mini");
752 assert_eq!(config.ai.base_url, "https://api.openai.com/v1");
753 assert_eq!(config.ai.api_key, None); }
755
756 #[test]
757 fn test_config_service_reload_functionality() {
758 let test_service = TestConfigService::with_defaults();
760
761 let config1 = test_service.get_config().unwrap();
763 assert_eq!(config1.ai.provider, "openai");
764
765 let reload_result = test_service.reload();
767 assert!(reload_result.is_ok());
768
769 let config2 = test_service.get_config().unwrap();
771 assert_eq!(config2.ai.provider, "openai");
772 }
773
774 #[test]
775 fn test_config_service_custom_base_url_override() {
776 let mut config = Config::default();
778 config.ai.base_url = "https://my-proxy.openai.com/v1".to_string();
779
780 let test_service = TestConfigService::new(config);
781 let loaded_config = test_service.get_config().unwrap();
782
783 assert_eq!(loaded_config.ai.base_url, "https://my-proxy.openai.com/v1");
784 }
785
786 #[test]
787 fn test_config_service_sync_settings() {
788 let test_service = TestConfigService::with_sync_settings(0.8, 45.0);
790 let config = test_service.get_config().unwrap();
791
792 assert_eq!(config.sync.correlation_threshold, 0.8);
793 assert_eq!(config.sync.max_offset_seconds, 45.0);
794 }
795
796 #[test]
797 fn test_config_service_parallel_settings() {
798 let test_service = TestConfigService::with_parallel_settings(8, 200);
800 let config = test_service.get_config().unwrap();
801
802 assert_eq!(config.general.max_concurrent_jobs, 8);
803 assert_eq!(config.parallel.task_queue_size, 200);
804 }
805
806 #[test]
807 fn test_config_service_direct_access() {
808 let test_service = TestConfigService::with_defaults();
810
811 assert_eq!(test_service.config().ai.provider, "openai");
813
814 test_service.config_mut().ai.provider = "modified".to_string();
816 assert_eq!(test_service.config().ai.provider, "modified");
817
818 let config = test_service.get_config().unwrap();
820 assert_eq!(config.ai.provider, "modified");
821 }
822
823 #[test]
824 fn test_production_config_service_openai_api_key_loading() {
825 let mut env_provider = TestEnvironmentProvider::new();
827 env_provider.set_var("OPENAI_API_KEY", "sk-test-openai-key-env");
828
829 env_provider.set_var(
831 "SUBX_CONFIG_PATH",
832 "/tmp/test_config_that_does_not_exist.toml",
833 );
834
835 let service = ProductionConfigService::with_env_provider(Arc::new(env_provider))
836 .expect("Failed to create config service");
837
838 let config = service.get_config().expect("Failed to get config");
839
840 assert_eq!(
841 config.ai.api_key,
842 Some("sk-test-openai-key-env".to_string())
843 );
844 }
845
846 #[test]
847 fn test_production_config_service_openai_base_url_loading() {
848 let mut env_provider = TestEnvironmentProvider::new();
850 env_provider.set_var("OPENAI_BASE_URL", "https://test.openai.com/v1");
851
852 let service = ProductionConfigService::with_env_provider(Arc::new(env_provider))
853 .expect("Failed to create config service");
854
855 let config = service.get_config().expect("Failed to get config");
856
857 assert_eq!(config.ai.base_url, "https://test.openai.com/v1");
858 }
859
860 #[test]
861 fn test_production_config_service_both_openai_env_vars() {
862 let mut env_provider = TestEnvironmentProvider::new();
864 env_provider.set_var("OPENAI_API_KEY", "sk-test-key-both");
865 env_provider.set_var("OPENAI_BASE_URL", "https://both.openai.com/v1");
866
867 env_provider.set_var(
869 "SUBX_CONFIG_PATH",
870 "/tmp/test_config_both_that_does_not_exist.toml",
871 );
872
873 let service = ProductionConfigService::with_env_provider(Arc::new(env_provider))
874 .expect("Failed to create config service");
875
876 let config = service.get_config().expect("Failed to get config");
877
878 assert_eq!(config.ai.api_key, Some("sk-test-key-both".to_string()));
879 assert_eq!(config.ai.base_url, "https://both.openai.com/v1");
880 }
881
882 #[test]
883 fn test_production_config_service_no_openai_env_vars() {
884 let mut env_provider = TestEnvironmentProvider::new(); env_provider.set_var(
889 "SUBX_CONFIG_PATH",
890 "/tmp/test_config_no_openai_that_does_not_exist.toml",
891 );
892
893 let service = ProductionConfigService::with_env_provider(Arc::new(env_provider))
894 .expect("Failed to create config service");
895
896 let config = service.get_config().expect("Failed to get config");
897
898 assert_eq!(config.ai.api_key, None);
900 assert_eq!(config.ai.base_url, "https://api.openai.com/v1"); }
902
903 #[test]
904 fn test_production_config_service_api_key_priority() {
905 let mut env_provider = TestEnvironmentProvider::new();
907 env_provider.set_var("OPENAI_API_KEY", "sk-env-key");
908 env_provider.set_var("SUBX_AI_APIKEY", "sk-config-key");
910
911 let service = ProductionConfigService::with_env_provider(Arc::new(env_provider))
912 .expect("Failed to create config service");
913
914 let config = service.get_config().expect("Failed to get config");
915
916 assert!(config.ai.api_key.is_some());
919 }
920}