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 let Some(api_key) = self.env_provider.get_var("OPENROUTER_API_KEY") {
257 debug!("ProductionConfigService: Found OPENROUTER_API_KEY environment variable");
258 app_config.ai.provider = "openrouter".to_string();
259 app_config.ai.api_key = Some(api_key);
260 }
261
262 if app_config.ai.api_key.is_none() {
265 if let Some(api_key) = self.env_provider.get_var("OPENAI_API_KEY") {
266 debug!("ProductionConfigService: Found OPENAI_API_KEY environment variable");
267 app_config.ai.api_key = Some(api_key);
268 }
269 }
270
271 if let Some(base_url) = self.env_provider.get_var("OPENAI_BASE_URL") {
273 debug!("ProductionConfigService: Found OPENAI_BASE_URL environment variable");
274 app_config.ai.base_url = base_url;
275 }
276
277 if let Some(api_key) = self.env_provider.get_var("AZURE_OPENAI_API_KEY") {
279 debug!("ProductionConfigService: Found AZURE_OPENAI_API_KEY environment variable");
280 app_config.ai.provider = "azure-openai".to_string();
281 app_config.ai.api_key = Some(api_key);
282 }
283 if let Some(endpoint) = self.env_provider.get_var("AZURE_OPENAI_ENDPOINT") {
284 debug!("ProductionConfigService: Found AZURE_OPENAI_ENDPOINT environment variable");
285 app_config.ai.base_url = endpoint;
286 }
287 if let Some(version) = self.env_provider.get_var("AZURE_OPENAI_API_VERSION") {
288 debug!("ProductionConfigService: Found AZURE_OPENAI_API_VERSION environment variable");
289 app_config.ai.api_version = Some(version);
290 }
291 if let Some(deployment) = self.env_provider.get_var("AZURE_OPENAI_DEPLOYMENT_ID") {
293 debug!(
294 "ProductionConfigService: Found AZURE_OPENAI_DEPLOYMENT_ID environment variable"
295 );
296 app_config.ai.model = deployment;
297 }
298
299 crate::config::validator::validate_config(&app_config).map_err(|e| {
301 debug!("ProductionConfigService: Config validation failed: {e}");
302 SubXError::config(format!("Configuration validation failed: {e}"))
303 })?;
304
305 debug!("ProductionConfigService: Configuration loaded and validated successfully");
306 Ok(app_config)
307 }
308
309 fn validate_and_set_value(&self, config: &mut Config, key: &str, value: &str) -> Result<()> {
313 use crate::config::field_validator;
314
315 field_validator::validate_field(key, value)?;
317
318 self.set_value_internal(config, key, value)?;
320
321 self.validate_configuration(config)?;
323
324 Ok(())
325 }
326
327 fn set_value_internal(&self, config: &mut Config, key: &str, value: &str) -> Result<()> {
329 use crate::config::OverflowStrategy;
330 use crate::config::validation::*;
331 use crate::error::SubXError;
332
333 let parts: Vec<&str> = key.split('.').collect();
334 match parts.as_slice() {
335 ["ai", "provider"] => {
336 config.ai.provider = value.to_string();
337 }
338 ["ai", "api_key"] => {
339 if !value.is_empty() {
340 config.ai.api_key = Some(value.to_string());
341 } else {
342 config.ai.api_key = None;
343 }
344 }
345 ["ai", "model"] => {
346 config.ai.model = value.to_string();
347 }
348 ["ai", "base_url"] => {
349 config.ai.base_url = value.to_string();
350 }
351 ["ai", "max_sample_length"] => {
352 let v = value.parse().unwrap(); config.ai.max_sample_length = v;
354 }
355 ["ai", "temperature"] => {
356 let v = value.parse().unwrap(); config.ai.temperature = v;
358 }
359 ["ai", "max_tokens"] => {
360 let v = value.parse().unwrap(); config.ai.max_tokens = v;
362 }
363 ["ai", "retry_attempts"] => {
364 let v = value.parse().unwrap(); config.ai.retry_attempts = v;
366 }
367 ["ai", "retry_delay_ms"] => {
368 let v = value.parse().unwrap(); config.ai.retry_delay_ms = v;
370 }
371 ["ai", "request_timeout_seconds"] => {
372 let v = value.parse().unwrap(); config.ai.request_timeout_seconds = v;
374 }
375 ["ai", "api_version"] => {
376 if !value.is_empty() {
377 config.ai.api_version = Some(value.to_string());
378 } else {
379 config.ai.api_version = None;
380 }
381 }
382 ["formats", "default_output"] => {
383 config.formats.default_output = value.to_string();
384 }
385 ["formats", "preserve_styling"] => {
386 let v = parse_bool(value)?;
387 config.formats.preserve_styling = v;
388 }
389 ["formats", "default_encoding"] => {
390 config.formats.default_encoding = value.to_string();
391 }
392 ["formats", "encoding_detection_confidence"] => {
393 let v = value.parse().unwrap(); config.formats.encoding_detection_confidence = v;
395 }
396 ["sync", "max_offset_seconds"] => {
397 let v = value.parse().unwrap(); config.sync.max_offset_seconds = v;
399 }
400 ["sync", "default_method"] => {
401 config.sync.default_method = value.to_string();
402 }
403 ["sync", "vad", "enabled"] => {
404 let v = parse_bool(value)?;
405 config.sync.vad.enabled = v;
406 }
407 ["sync", "vad", "sensitivity"] => {
408 let v = value.parse().unwrap(); config.sync.vad.sensitivity = v;
410 }
411 ["sync", "vad", "padding_chunks"] => {
412 let v = value.parse().unwrap(); config.sync.vad.padding_chunks = v;
414 }
415 ["sync", "vad", "min_speech_duration_ms"] => {
416 let v = value.parse().unwrap(); config.sync.vad.min_speech_duration_ms = v;
418 }
419 ["general", "backup_enabled"] => {
420 let v = parse_bool(value)?;
421 config.general.backup_enabled = v;
422 }
423 ["general", "max_concurrent_jobs"] => {
424 let v = value.parse().unwrap(); config.general.max_concurrent_jobs = v;
426 }
427 ["general", "task_timeout_seconds"] => {
428 let v = value.parse().unwrap(); config.general.task_timeout_seconds = v;
430 }
431 ["general", "enable_progress_bar"] => {
432 let v = parse_bool(value)?;
433 config.general.enable_progress_bar = v;
434 }
435 ["general", "worker_idle_timeout_seconds"] => {
436 let v = value.parse().unwrap(); config.general.worker_idle_timeout_seconds = v;
438 }
439 ["parallel", "max_workers"] => {
440 let v = value.parse().unwrap(); config.parallel.max_workers = v;
442 }
443 ["parallel", "task_queue_size"] => {
444 let v = value.parse().unwrap(); config.parallel.task_queue_size = v;
446 }
447 ["parallel", "enable_task_priorities"] => {
448 let v = parse_bool(value)?;
449 config.parallel.enable_task_priorities = v;
450 }
451 ["parallel", "auto_balance_workers"] => {
452 let v = parse_bool(value)?;
453 config.parallel.auto_balance_workers = v;
454 }
455 ["parallel", "overflow_strategy"] => {
456 config.parallel.overflow_strategy = match value {
457 "Block" => OverflowStrategy::Block,
458 "Drop" => OverflowStrategy::Drop,
459 "Expand" => OverflowStrategy::Expand,
460 _ => unreachable!(), };
462 }
463 _ => {
464 return Err(SubXError::config(format!(
465 "Unknown configuration key: {key}"
466 )));
467 }
468 }
469 Ok(())
470 }
471
472 fn validate_configuration(&self, config: &Config) -> Result<()> {
474 use crate::config::validator;
475 validator::validate_config(config)
476 }
477
478 fn save_config_to_file_with_config(
480 &self,
481 path: &std::path::Path,
482 config: &Config,
483 ) -> Result<()> {
484 let toml_content = toml::to_string_pretty(config)
485 .map_err(|e| SubXError::config(format!("TOML serialization error: {e}")))?;
486 if let Some(parent) = path.parent() {
487 std::fs::create_dir_all(parent).map_err(|e| {
488 SubXError::config(format!("Failed to create config directory: {e}"))
489 })?;
490 }
491 std::fs::write(path, toml_content)
492 .map_err(|e| SubXError::config(format!("Failed to write config file: {e}")))?;
493 Ok(())
494 }
495}
496
497impl ConfigService for ProductionConfigService {
498 fn get_config(&self) -> Result<Config> {
499 {
501 let cache = self.cached_config.read().unwrap();
502 if let Some(config) = cache.as_ref() {
503 debug!("ProductionConfigService: Returning cached configuration");
504 return Ok(config.clone());
505 }
506 }
507
508 let app_config = self.load_and_validate()?;
510
511 {
513 let mut cache = self.cached_config.write().unwrap();
514 *cache = Some(app_config.clone());
515 }
516
517 Ok(app_config)
518 }
519
520 fn reload(&self) -> Result<()> {
521 debug!("ProductionConfigService: Reloading configuration");
522
523 {
525 let mut cache = self.cached_config.write().unwrap();
526 *cache = None;
527 }
528
529 self.get_config()?;
531
532 debug!("ProductionConfigService: Configuration reloaded successfully");
533 Ok(())
534 }
535
536 fn save_config(&self) -> Result<()> {
537 let _config = self.get_config()?;
538 let path = self.get_config_file_path()?;
539 self.save_config_to_file(&path)
540 }
541
542 fn save_config_to_file(&self, path: &Path) -> Result<()> {
543 let config = self.get_config()?;
544 let toml_content = toml::to_string_pretty(&config)
545 .map_err(|e| SubXError::config(format!("TOML serialization error: {e}")))?;
546
547 if let Some(parent) = path.parent() {
548 std::fs::create_dir_all(parent).map_err(|e| {
549 SubXError::config(format!("Failed to create config directory: {e}"))
550 })?;
551 }
552
553 std::fs::write(path, toml_content)
554 .map_err(|e| SubXError::config(format!("Failed to write config file: {e}")))?;
555
556 Ok(())
557 }
558
559 fn get_config_file_path(&self) -> Result<PathBuf> {
560 if let Some(custom) = self.env_provider.get_var("SUBX_CONFIG_PATH") {
562 return Ok(PathBuf::from(custom));
563 }
564
565 let config_dir = dirs::config_dir()
566 .ok_or_else(|| SubXError::config("Unable to determine config directory"))?;
567 Ok(config_dir.join("subx").join("config.toml"))
568 }
569
570 fn get_config_value(&self, key: &str) -> Result<String> {
571 let config = self.get_config()?;
572 let parts: Vec<&str> = key.split('.').collect();
573 match parts.as_slice() {
574 ["ai", "provider"] => Ok(config.ai.provider.clone()),
575 ["ai", "model"] => Ok(config.ai.model.clone()),
576 ["ai", "api_key"] => Ok(config.ai.api_key.clone().unwrap_or_default()),
577 ["ai", "base_url"] => Ok(config.ai.base_url.clone()),
578 ["ai", "max_sample_length"] => Ok(config.ai.max_sample_length.to_string()),
579 ["ai", "temperature"] => Ok(config.ai.temperature.to_string()),
580 ["ai", "max_tokens"] => Ok(config.ai.max_tokens.to_string()),
581 ["ai", "retry_attempts"] => Ok(config.ai.retry_attempts.to_string()),
582 ["ai", "retry_delay_ms"] => Ok(config.ai.retry_delay_ms.to_string()),
583 ["ai", "request_timeout_seconds"] => Ok(config.ai.request_timeout_seconds.to_string()),
584
585 ["formats", "default_output"] => Ok(config.formats.default_output.clone()),
586 ["formats", "default_encoding"] => Ok(config.formats.default_encoding.clone()),
587 ["formats", "preserve_styling"] => Ok(config.formats.preserve_styling.to_string()),
588 ["formats", "encoding_detection_confidence"] => {
589 Ok(config.formats.encoding_detection_confidence.to_string())
590 }
591
592 ["sync", "default_method"] => Ok(config.sync.default_method.clone()),
593 ["sync", "max_offset_seconds"] => Ok(config.sync.max_offset_seconds.to_string()),
594 ["sync", "vad", "enabled"] => Ok(config.sync.vad.enabled.to_string()),
595 ["sync", "vad", "sensitivity"] => Ok(config.sync.vad.sensitivity.to_string()),
596 ["sync", "vad", "padding_chunks"] => Ok(config.sync.vad.padding_chunks.to_string()),
597 ["sync", "vad", "min_speech_duration_ms"] => {
598 Ok(config.sync.vad.min_speech_duration_ms.to_string())
599 }
600
601 ["general", "backup_enabled"] => Ok(config.general.backup_enabled.to_string()),
602 ["general", "max_concurrent_jobs"] => {
603 Ok(config.general.max_concurrent_jobs.to_string())
604 }
605 ["general", "task_timeout_seconds"] => {
606 Ok(config.general.task_timeout_seconds.to_string())
607 }
608 ["general", "enable_progress_bar"] => {
609 Ok(config.general.enable_progress_bar.to_string())
610 }
611 ["general", "worker_idle_timeout_seconds"] => {
612 Ok(config.general.worker_idle_timeout_seconds.to_string())
613 }
614
615 ["parallel", "max_workers"] => Ok(config.parallel.max_workers.to_string()),
616 ["parallel", "task_queue_size"] => Ok(config.parallel.task_queue_size.to_string()),
617 ["parallel", "enable_task_priorities"] => {
618 Ok(config.parallel.enable_task_priorities.to_string())
619 }
620 ["parallel", "auto_balance_workers"] => {
621 Ok(config.parallel.auto_balance_workers.to_string())
622 }
623 ["parallel", "overflow_strategy"] => {
624 Ok(format!("{:?}", config.parallel.overflow_strategy))
625 }
626
627 _ => Err(SubXError::config(format!(
628 "Unknown configuration key: {}",
629 key
630 ))),
631 }
632 }
633
634 fn set_config_value(&self, key: &str, value: &str) -> Result<()> {
635 let mut config = self.get_config()?;
637
638 self.validate_and_set_value(&mut config, key, value)?;
640
641 crate::config::validator::validate_config(&config)?;
643
644 let path = self.get_config_file_path()?;
646 self.save_config_to_file_with_config(&path, &config)?;
647
648 {
650 let mut cache = self.cached_config.write().unwrap();
651 *cache = Some(config);
652 }
653
654 Ok(())
655 }
656
657 fn reset_to_defaults(&self) -> Result<()> {
658 let default_config = Config::default();
659 let path = self.get_config_file_path()?;
660
661 let toml_content = toml::to_string_pretty(&default_config)
662 .map_err(|e| SubXError::config(format!("TOML serialization error: {}", e)))?;
663
664 if let Some(parent) = path.parent() {
665 std::fs::create_dir_all(parent).map_err(|e| {
666 SubXError::config(format!("Failed to create config directory: {}", e))
667 })?;
668 }
669
670 std::fs::write(&path, toml_content)
671 .map_err(|e| SubXError::config(format!("Failed to write config file: {}", e)))?;
672
673 self.reload()
674 }
675}
676
677impl Default for ProductionConfigService {
678 fn default() -> Self {
679 Self::new().expect("Failed to create default ProductionConfigService")
680 }
681}
682
683#[cfg(test)]
684mod tests {
685 use super::*;
686 use crate::config::TestConfigService;
687 use crate::config::TestEnvironmentProvider;
688 use std::sync::Arc;
689
690 #[test]
691 fn test_production_config_service_creation() {
692 let service = ProductionConfigService::new();
693 assert!(service.is_ok());
694 }
695
696 #[test]
697 fn test_production_config_service_with_custom_file() {
698 let service = ProductionConfigService::new()
699 .unwrap()
700 .with_custom_file(PathBuf::from("test.toml"));
701 assert!(service.is_ok());
702 }
703
704 #[test]
705 fn test_production_service_implements_config_service_trait() {
706 let service = ProductionConfigService::new().unwrap();
707
708 let config1 = service.get_config();
710 assert!(config1.is_ok());
711
712 let reload_result = service.reload();
713 assert!(reload_result.is_ok());
714
715 let config2 = service.get_config();
716 assert!(config2.is_ok());
717 }
718
719 #[test]
720 fn test_production_config_service_openrouter_api_key_loading() {
721 use crate::config::TestEnvironmentProvider;
722 use std::sync::Arc;
723
724 let mut env_provider = TestEnvironmentProvider::new();
725 env_provider.set_var("OPENROUTER_API_KEY", "test-openrouter-key");
726 env_provider.set_var("SUBX_CONFIG_PATH", "/tmp/test_config_openrouter.toml");
727
728 let service = ProductionConfigService::with_env_provider(Arc::new(env_provider))
729 .expect("Failed to create config service");
730
731 let config = service.get_config().expect("Failed to get config");
732
733 assert_eq!(config.ai.api_key, Some("test-openrouter-key".to_string()));
734 }
735
736 #[test]
737 fn test_config_service_with_openai_api_key() {
738 let test_service = TestConfigService::with_ai_settings_and_key(
740 "openai",
741 "gpt-4.1-mini",
742 "sk-test-openai-key-123",
743 );
744
745 let config = test_service.get_config().unwrap();
746 assert_eq!(
747 config.ai.api_key,
748 Some("sk-test-openai-key-123".to_string())
749 );
750 assert_eq!(config.ai.provider, "openai");
751 assert_eq!(config.ai.model, "gpt-4.1-mini");
752 }
753
754 #[test]
755 fn test_config_service_with_custom_base_url() {
756 let mut config = Config::default();
758 config.ai.base_url = "https://custom.openai.endpoint".to_string();
759
760 let test_service = TestConfigService::new(config);
761 let loaded_config = test_service.get_config().unwrap();
762
763 assert_eq!(loaded_config.ai.base_url, "https://custom.openai.endpoint");
764 }
765
766 #[test]
767 fn test_config_service_with_both_openai_settings() {
768 let mut config = Config::default();
770 config.ai.api_key = Some("sk-test-api-key-combined".to_string());
771 config.ai.base_url = "https://api.custom-openai.com".to_string();
772
773 let test_service = TestConfigService::new(config);
774 let loaded_config = test_service.get_config().unwrap();
775
776 assert_eq!(
777 loaded_config.ai.api_key,
778 Some("sk-test-api-key-combined".to_string())
779 );
780 assert_eq!(loaded_config.ai.base_url, "https://api.custom-openai.com");
781 }
782
783 #[test]
784 fn test_config_service_provider_precedence() {
785 let test_service =
787 TestConfigService::with_ai_settings_and_key("openai", "gpt-4.1", "sk-explicit-key");
788
789 let config = test_service.get_config().unwrap();
790 assert_eq!(config.ai.api_key, Some("sk-explicit-key".to_string()));
791 assert_eq!(config.ai.provider, "openai");
792 assert_eq!(config.ai.model, "gpt-4.1");
793 }
794
795 #[test]
796 fn test_config_service_fallback_behavior() {
797 let test_service = TestConfigService::with_defaults();
799 let config = test_service.get_config().unwrap();
800
801 assert_eq!(config.ai.provider, "openai");
803 assert_eq!(config.ai.model, "gpt-4.1-mini");
804 assert_eq!(config.ai.base_url, "https://api.openai.com/v1");
805 assert_eq!(config.ai.api_key, None); }
807
808 #[test]
809 fn test_config_service_reload_functionality() {
810 let test_service = TestConfigService::with_defaults();
812
813 let config1 = test_service.get_config().unwrap();
815 assert_eq!(config1.ai.provider, "openai");
816
817 let reload_result = test_service.reload();
819 assert!(reload_result.is_ok());
820
821 let config2 = test_service.get_config().unwrap();
823 assert_eq!(config2.ai.provider, "openai");
824 }
825
826 #[test]
827 fn test_config_service_custom_base_url_override() {
828 let mut config = Config::default();
830 config.ai.base_url = "https://my-proxy.openai.com/v1".to_string();
831
832 let test_service = TestConfigService::new(config);
833 let loaded_config = test_service.get_config().unwrap();
834
835 assert_eq!(loaded_config.ai.base_url, "https://my-proxy.openai.com/v1");
836 }
837
838 #[test]
839 fn test_config_service_sync_settings() {
840 let test_service = TestConfigService::with_sync_settings(0.8, 45.0);
842 let config = test_service.get_config().unwrap();
843
844 assert_eq!(config.sync.correlation_threshold, 0.8);
845 assert_eq!(config.sync.max_offset_seconds, 45.0);
846 }
847
848 #[test]
849 fn test_config_service_parallel_settings() {
850 let test_service = TestConfigService::with_parallel_settings(8, 200);
852 let config = test_service.get_config().unwrap();
853
854 assert_eq!(config.general.max_concurrent_jobs, 8);
855 assert_eq!(config.parallel.task_queue_size, 200);
856 }
857
858 #[test]
859 fn test_config_service_direct_access() {
860 let test_service = TestConfigService::with_defaults();
862
863 assert_eq!(test_service.config().ai.provider, "openai");
865
866 test_service.config_mut().ai.provider = "modified".to_string();
868 assert_eq!(test_service.config().ai.provider, "modified");
869
870 let config = test_service.get_config().unwrap();
872 assert_eq!(config.ai.provider, "modified");
873 }
874
875 #[test]
876 fn test_production_config_service_openai_api_key_loading() {
877 let mut env_provider = TestEnvironmentProvider::new();
879 env_provider.set_var("OPENAI_API_KEY", "sk-test-openai-key-env");
880
881 env_provider.set_var(
883 "SUBX_CONFIG_PATH",
884 "/tmp/test_config_that_does_not_exist.toml",
885 );
886
887 let service = ProductionConfigService::with_env_provider(Arc::new(env_provider))
888 .expect("Failed to create config service");
889
890 let config = service.get_config().expect("Failed to get config");
891
892 assert_eq!(
893 config.ai.api_key,
894 Some("sk-test-openai-key-env".to_string())
895 );
896 }
897
898 #[test]
899 fn test_production_config_service_openai_base_url_loading() {
900 let mut env_provider = TestEnvironmentProvider::new();
902 env_provider.set_var("OPENAI_BASE_URL", "https://test.openai.com/v1");
903
904 let service = ProductionConfigService::with_env_provider(Arc::new(env_provider))
905 .expect("Failed to create config service");
906
907 let config = service.get_config().expect("Failed to get config");
908
909 assert_eq!(config.ai.base_url, "https://test.openai.com/v1");
910 }
911
912 #[test]
913 fn test_production_config_service_both_openai_env_vars() {
914 let mut env_provider = TestEnvironmentProvider::new();
916 env_provider.set_var("OPENAI_API_KEY", "sk-test-key-both");
917 env_provider.set_var("OPENAI_BASE_URL", "https://both.openai.com/v1");
918
919 env_provider.set_var(
921 "SUBX_CONFIG_PATH",
922 "/tmp/test_config_both_that_does_not_exist.toml",
923 );
924
925 let service = ProductionConfigService::with_env_provider(Arc::new(env_provider))
926 .expect("Failed to create config service");
927
928 let config = service.get_config().expect("Failed to get config");
929
930 assert_eq!(config.ai.api_key, Some("sk-test-key-both".to_string()));
931 assert_eq!(config.ai.base_url, "https://both.openai.com/v1");
932 }
933
934 #[test]
935 fn test_production_config_service_no_openai_env_vars() {
936 let mut env_provider = TestEnvironmentProvider::new(); env_provider.set_var(
941 "SUBX_CONFIG_PATH",
942 "/tmp/test_config_no_openai_that_does_not_exist.toml",
943 );
944
945 let service = ProductionConfigService::with_env_provider(Arc::new(env_provider))
946 .expect("Failed to create config service");
947
948 let config = service.get_config().expect("Failed to get config");
949
950 assert_eq!(config.ai.api_key, None);
952 assert_eq!(config.ai.base_url, "https://api.openai.com/v1"); }
954
955 #[test]
956 fn test_production_config_service_api_key_priority() {
957 let mut env_provider = TestEnvironmentProvider::new();
959 env_provider.set_var("OPENAI_API_KEY", "sk-env-key");
960 env_provider.set_var("SUBX_AI_APIKEY", "sk-config-key");
962
963 let service = ProductionConfigService::with_env_provider(Arc::new(env_provider))
964 .expect("Failed to create config service");
965
966 let config = service.get_config().expect("Failed to get config");
967
968 assert!(config.ai.api_key.is_some());
971 }
972}