1use serde::{Deserialize, Serialize};
7use thiserror::Error;
8use zeroize::Zeroize;
9
10#[derive(Error, Debug)]
15#[non_exhaustive]
16pub enum ConfigError {
17 #[error("Failed to load config: {0}")]
18 LoadError(String),
19 #[error("Invalid configuration: {0}")]
20 ValidationError(String),
21 #[error("Missing required environment variable: {0}")]
22 #[allow(dead_code)]
23 MissingEnvVar(String),
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
32#[serde(rename_all = "lowercase")]
33#[non_exhaustive]
34pub enum LLMProvider {
35 #[default]
36 LiteLLM,
37 OpenRouter,
38 Ollama,
39 OpenAI,
40 Anthropic,
41 #[serde(rename = "openai-compatible")]
43 OpenAICompatible,
44 #[serde(rename = "azure")]
46 Azure,
47}
48
49#[derive(Debug, Clone, Deserialize, Default)]
55#[non_exhaustive]
56pub struct Config {
57 #[serde(default)]
59 pub llm: LLMConfig,
60
61 #[serde(default)]
63 pub llms: Vec<LLMConfig>,
64
65 #[serde(default)]
67 pub ravenfabric: RavenFabricConfig,
68
69 #[serde(default)]
71 pub security: SecurityConfig,
72
73 #[serde(default)]
75 pub runtime: RuntimeConfig,
76
77 #[serde(default)]
79 pub telemetry: TelemetryConfig,
80
81 #[serde(default)]
83 pub scheduler: SchedulerConfig,
84
85 #[serde(default)]
87 pub web_search: WebSearchConfig,
88
89 #[serde(default)]
91 pub heartbeat: crate::heartbeat::HeartbeatConfig,
92
93 #[serde(default)]
95 pub swarm: crate::swarm::SwarmConfig,
96
97 #[serde(default)]
99 pub mcp: McpConfig,
100
101 #[serde(default)]
103 pub browser: BrowserConfig,
104
105 #[serde(default)]
107 pub load: crate::load::LoadConfig,
108
109 #[serde(default = "default_web_policy")]
111 pub web_policy: crate::web_policy::WebAccessPolicy,
112}
113
114#[derive(Debug, Clone, Deserialize, Default)]
132#[non_exhaustive]
133pub struct McpConfig {
134 #[serde(default)]
136 pub servers: Vec<McpServerConfig>,
137}
138
139#[derive(Debug, Clone, Deserialize)]
144#[non_exhaustive]
145pub struct McpServerConfig {
146 pub name: String,
148 #[serde(default)]
151 pub command: String,
152 #[serde(default)]
154 pub args: Vec<String>,
155 #[serde(default)]
157 pub env: std::collections::HashMap<String, String>,
158 #[serde(default)]
161 pub url: String,
162}
163
164#[derive(Debug, Clone, Deserialize)]
169#[non_exhaustive]
170pub struct WebSearchConfig {
171 #[serde(default = "default_search_endpoint")]
173 pub endpoint: String,
174
175 #[serde(default = "default_search_engine")]
177 pub engine: String,
178
179 #[serde(default = "default_search_max_results")]
181 pub max_results: usize,
182
183 #[serde(default = "default_true")]
185 pub fetch_content: bool,
186}
187
188impl Default for WebSearchConfig {
189 fn default() -> Self {
190 Self {
191 endpoint: default_search_endpoint(),
192 engine: default_search_engine(),
193 max_results: default_search_max_results(),
194 fetch_content: default_true(),
195 }
196 }
197}
198
199#[derive(Debug, Clone, Deserialize)]
207#[non_exhaustive]
208pub struct BrowserConfig {
209 #[serde(default = "default_browser_cdp_url")]
211 pub cdp_url: String,
212
213 #[serde(default = "default_browser_timeout")]
215 pub request_timeout: u64,
216}
217
218impl Default for BrowserConfig {
219 fn default() -> Self {
220 Self {
221 cdp_url: default_browser_cdp_url(),
222 request_timeout: default_browser_timeout(),
223 }
224 }
225}
226
227fn default_browser_cdp_url() -> String {
228 "http://127.0.0.1:9222".to_string()
229}
230
231fn default_browser_timeout() -> u64 {
232 30000
233}
234
235fn default_web_policy() -> crate::web_policy::WebAccessPolicy {
236 crate::web_policy::WebAccessPolicy::disabled()
239}
240
241fn default_search_endpoint() -> String {
242 "https://searx.be".to_string()
243}
244
245fn default_search_engine() -> String {
246 "duckduckgo".to_string()
247}
248
249fn default_search_max_results() -> usize {
250 5
251}
252
253fn default_otel_disabled() -> bool {
254 true
255}
256
257#[derive(Debug, Clone, Deserialize, Default)]
262#[non_exhaustive]
263pub struct SchedulerConfig {
264 #[serde(default)]
266 pub triggers: Vec<crate::scheduler::TriggerConfig>,
267}
268
269#[derive(Debug, Clone, Deserialize, Default)]
274#[non_exhaustive]
275pub struct TelemetryConfig {
276 #[serde(default)]
278 pub otel_endpoint: Option<String>,
279
280 #[serde(default)]
282 pub otel_service_name: Option<String>,
283
284 #[serde(default = "default_otel_disabled")]
286 pub otel_disabled: bool,
287}
288
289#[derive(Debug, Clone, Deserialize)]
295#[non_exhaustive]
296pub struct LLMConfig {
297 #[serde(default)]
299 pub provider: LLMProvider,
300
301 #[serde(default)]
303 pub endpoint: String,
304
305 #[serde(default = "default_model")]
307 pub model: String,
308
309 #[serde(default)]
311 pub api_key: Option<String>,
312
313 #[serde(default = "default_timeout")]
315 pub timeout_secs: u64,
316
317 #[serde(default = "default_system_prompt")]
319 pub system_prompt: String,
320
321 #[serde(default)]
323 pub token_budget: Option<u32>,
324
325 #[serde(default = "default_retry_max")]
327 pub retry_max: u32,
328
329 #[serde(default = "default_retry_base_delay")]
331 pub retry_base_delay_ms: u64,
332
333 #[serde(default = "default_retry_max_delay")]
335 pub retry_max_delay_ms: u64,
336}
337
338pub fn default_retry_max() -> u32 {
339 3
340}
341pub fn default_retry_base_delay() -> u64 {
342 100
343}
344pub fn default_retry_max_delay() -> u64 {
345 10000
346}
347
348pub fn default_system_prompt() -> String {
349 "You are RavenClaws, a lightweight autonomous agent. \
350 Be concise, efficient, and secure. Always validate inputs and outputs. \
351 When you have completed the task, prefix your final answer with FINAL: \
352 so the system knows the task is done."
353 .to_string()
354}
355
356#[derive(Debug, Clone, Deserialize)]
361#[non_exhaustive]
362pub struct RavenFabricConfig {
363 #[serde(default)]
365 pub endpoint: Option<String>,
366
367 #[serde(default)]
369 pub agent_id: Option<String>,
370
371 #[serde(default = "default_true")]
373 pub remote_exec: bool,
374
375 #[serde(default)]
377 #[allow(dead_code)]
378 pub allowed_hosts: Vec<String>,
379}
380
381impl Default for RavenFabricConfig {
382 fn default() -> Self {
383 Self {
384 endpoint: None,
385 agent_id: None,
386 remote_exec: default_true(),
387 allowed_hosts: Vec::new(),
388 }
389 }
390}
391
392#[derive(Debug, Clone, Deserialize)]
397#[non_exhaustive]
398pub struct SecurityConfig {
399 #[serde(default = "default_true")]
401 pub require_tls: bool,
402
403 #[serde(default = "default_token_lifetime")]
407 pub token_lifetime_secs: u64,
408
409 #[serde(default = "default_true")]
411 #[allow(dead_code)]
412 pub audit_log: bool,
413
414 #[serde(default = "default_true")]
418 #[allow(dead_code)]
419 pub prompt_injection_protection: bool,
420}
421
422impl Default for SecurityConfig {
423 fn default() -> Self {
424 Self {
425 require_tls: default_true(),
426 token_lifetime_secs: default_token_lifetime(),
427 audit_log: default_true(),
428 prompt_injection_protection: default_true(),
429 }
430 }
431}
432
433#[derive(Debug, Clone, Deserialize)]
438#[non_exhaustive]
439pub struct RuntimeConfig {
440 #[serde(default = "default_workdir")]
442 #[allow(dead_code)]
443 pub workdir: String,
444
445 #[serde(default = "default_max_agents")]
447 #[allow(dead_code)]
448 pub max_agents: usize,
449
450 #[serde(default = "default_health_interval")]
452 #[allow(dead_code)]
453 pub health_interval_secs: u64,
454
455 #[serde(default)]
457 pub host: Option<String>,
458
459 #[serde(default = "default_server_port")]
461 pub port: u16,
462
463 #[serde(default)]
468 #[allow(dead_code)]
469 pub checkpoint_dir: Option<String>,
470
471 #[serde(default = "default_checkpoint_interval")]
475 #[allow(dead_code)]
476 pub checkpoint_interval: usize,
477}
478
479fn default_checkpoint_interval() -> usize {
480 1
481}
482
483impl Default for RuntimeConfig {
484 fn default() -> Self {
485 Self {
486 workdir: default_workdir(),
487 max_agents: default_max_agents(),
488 health_interval_secs: default_health_interval(),
489 host: None,
490 port: default_server_port(),
491 checkpoint_dir: None,
492 checkpoint_interval: 1,
493 }
494 }
495}
496
497fn default_model() -> String {
498 "gpt-4o-mini".to_string()
499}
500
501fn default_timeout() -> u64 {
502 30
503}
504
505fn default_true() -> bool {
506 true
507}
508
509fn default_token_lifetime() -> u64 {
510 3600
511}
512
513fn default_workdir() -> String {
514 "/tmp/ravenclaws-workdir".to_string()
515}
516
517fn default_max_agents() -> usize {
518 10
519}
520
521fn default_health_interval() -> u64 {
522 60
523}
524
525fn default_server_port() -> u16 {
526 8080
527}
528
529impl Default for LLMConfig {
530 fn default() -> Self {
531 Self {
532 provider: LLMProvider::LiteLLM,
533 endpoint: String::new(),
534 model: default_model(),
535 api_key: None,
536 timeout_secs: default_timeout(),
537 system_prompt: default_system_prompt(),
538 token_budget: None,
539 retry_max: default_retry_max(),
540 retry_base_delay_ms: default_retry_base_delay(),
541 retry_max_delay_ms: default_retry_max_delay(),
542 }
543 }
544}
545
546impl Drop for LLMConfig {
548 fn drop(&mut self) {
549 if let Some(ref mut key) = self.api_key {
550 key.zeroize();
551 }
552 }
553}
554
555impl Config {
556 pub fn load(config_path: Option<&str>) -> Result<Self, ConfigError> {
558 dotenvy::dotenv().ok();
560
561 let mut config_builder = config::Config::builder();
562
563 if let Some(path) = config_path {
565 config_builder =
566 config_builder.add_source(config::File::with_name(path).required(false));
567 }
568
569 let ravenclaws_llms = std::env::var("RAVENCLAWS__LLMS").ok();
574 if ravenclaws_llms.is_some() {
575 std::env::remove_var("RAVENCLAWS__LLMS");
576 }
577
578 config_builder = config_builder
579 .add_source(config::Environment::with_prefix("RAVENCLAW").separator("__"));
580
581 let config = config_builder
582 .build()
583 .map_err(|e| ConfigError::LoadError(e.to_string()))?;
584
585 let mut cfg: Config = config
586 .try_deserialize()
587 .map_err(|e| ConfigError::LoadError(e.to_string()))?;
588
589 if let Some(ref val) = ravenclaws_llms {
591 std::env::set_var("RAVENCLAWS__LLMS", val);
592 }
593
594 if let Ok(key) = std::env::var("LITELLM_API_KEY") {
597 cfg.llm.api_key = Some(key);
598 }
599 if let Ok(provider) = std::env::var("RAVENCLAWS__LLM__PROVIDER") {
600 cfg.llm.provider = match provider.to_lowercase().as_str() {
601 "openrouter" => LLMProvider::OpenRouter,
602 "ollama" => LLMProvider::Ollama,
603 "openai" => LLMProvider::OpenAI,
604 "anthropic" => LLMProvider::Anthropic,
605 _ => LLMProvider::LiteLLM,
606 };
607 }
608 if let Ok(endpoint) = std::env::var("RAVENCLAWS__LLM__ENDPOINT") {
609 cfg.llm.endpoint = endpoint;
610 }
611 if let Ok(model) = std::env::var("RAVENCLAWS__LLM__MODEL") {
612 cfg.llm.model = model;
613 }
614
615 if let Ok(keys) = std::env::var("RAVENCLAWS__LLMS") {
619 if let Ok(llms) = serde_json::from_str::<Vec<LLMConfig>>(&keys) {
621 cfg.llms = llms;
622 }
623 }
624
625 if let Ok(endpoint) = std::env::var("RAVENFABRIC_ENDPOINT") {
626 cfg.ravenfabric.endpoint = Some(endpoint);
627 }
628
629 cfg.validate()?;
631
632 Ok(cfg)
633 }
634
635 fn validate(&self) -> Result<(), ConfigError> {
637 if !self.llm.endpoint.is_empty() {
639 self.validate_llm_config(&self.llm)?;
640 }
641
642 for (i, llm) in self.llms.iter().enumerate() {
644 self.validate_llm_config(llm)
645 .map_err(|e| ConfigError::ValidationError(format!("LLM[{}]: {}", i, e)))?;
646 }
647
648 if self.llm.endpoint.is_empty() && self.llms.is_empty() {
650 return Err(ConfigError::ValidationError(
651 "At least one LLM provider must be configured (llm or llms)".to_string(),
652 ));
653 }
654
655 Ok(())
656 }
657
658 fn validate_llm_config(&self, llm: &LLMConfig) -> Result<(), ConfigError> {
659 if llm.endpoint.is_empty()
660 && llm.provider != LLMProvider::OpenAI
661 && llm.provider != LLMProvider::OpenRouter
662 && llm.provider != LLMProvider::Anthropic
663 {
664 return Err(ConfigError::ValidationError(
666 "LLM endpoint is required for this provider".to_string(),
667 ));
668 }
669
670 if self.security.require_tls
671 && !llm.endpoint.is_empty()
672 && !llm.endpoint.starts_with("https://")
673 && !llm.endpoint.contains("localhost")
674 && !llm.endpoint.contains("127.0.0.1")
675 && !llm.endpoint.contains("0.0.0.0")
676 {
677 return Err(ConfigError::ValidationError(
678 "TLS required but endpoint is not HTTPS".to_string(),
679 ));
680 }
681
682 Ok(())
683 }
684}
685
686#[cfg(test)]
687mod tests {
688 use super::*;
689 use serial_test::serial;
690
691 #[test]
692 #[serial(env_test)]
693 fn test_default_config() {
694 std::env::set_var("LITELLM_API_KEY", "test-key");
695 std::env::set_var("RAVENCLAWS__LLM__ENDPOINT", "http://localhost:4000");
696
697 let config = Config::load(None).unwrap();
698 assert_eq!(config.llm.model, "gpt-4o-mini");
699 assert_eq!(config.llm.timeout_secs, 30);
700 assert!(config.security.require_tls);
704
705 std::env::remove_var("LITELLM_API_KEY");
707 std::env::remove_var("RAVENCLAWS__LLM__ENDPOINT");
708 }
709
710 #[test]
711 fn test_llm_provider_default() {
712 assert_eq!(LLMProvider::default(), LLMProvider::LiteLLM);
713 }
714
715 #[test]
716 fn test_llm_provider_serde() {
717 let json = r#""litellm""#;
718 let provider: LLMProvider = serde_json::from_str(json).unwrap();
719 assert_eq!(provider, LLMProvider::LiteLLM);
720
721 let json = r#""openai""#;
722 let provider: LLMProvider = serde_json::from_str(json).unwrap();
723 assert_eq!(provider, LLMProvider::OpenAI);
724
725 let json = r#""ollama""#;
726 let provider: LLMProvider = serde_json::from_str(json).unwrap();
727 assert_eq!(provider, LLMProvider::Ollama);
728
729 let json = r#""openrouter""#;
730 let provider: LLMProvider = serde_json::from_str(json).unwrap();
731 assert_eq!(provider, LLMProvider::OpenRouter);
732 }
733
734 #[test]
735 fn test_llm_config_default() {
736 let config = LLMConfig::default();
737 assert_eq!(config.provider, LLMProvider::LiteLLM);
738 assert_eq!(config.model, "gpt-4o-mini");
739 assert_eq!(config.timeout_secs, 30);
740 assert!(config.api_key.is_none());
741 assert!(config.endpoint.is_empty());
742 assert!(config.system_prompt.contains("RavenClaws"));
743 }
744
745 #[test]
746 fn test_system_prompt_custom() {
747 let mut config = LLMConfig::default();
748 config.system_prompt = "You are a helpful coding assistant.".to_string();
749 assert_eq!(config.system_prompt, "You are a helpful coding assistant.");
750 }
751
752 #[test]
753 fn test_validate_missing_endpoint() {
754 let config = Config {
755 llm: LLMConfig {
756 provider: LLMProvider::LiteLLM,
757 endpoint: String::new(),
758 model: "gpt-4o-mini".to_string(),
759 api_key: None,
760 timeout_secs: 30,
761 system_prompt: default_system_prompt(),
762 token_budget: None,
763 retry_max: 3,
764 retry_base_delay_ms: 100,
765 retry_max_delay_ms: 10000,
766 },
767 llms: vec![],
768 ravenfabric: RavenFabricConfig::default(),
769 security: SecurityConfig {
770 require_tls: false,
771 token_lifetime_secs: 3600,
772 audit_log: false,
773 prompt_injection_protection: false,
774 },
775 runtime: RuntimeConfig::default(),
776 telemetry: TelemetryConfig::default(),
777 scheduler: SchedulerConfig::default(),
778 web_search: WebSearchConfig::default(),
779 heartbeat: crate::heartbeat::HeartbeatConfig::default(),
780 mcp: McpConfig::default(),
781 swarm: crate::swarm::SwarmConfig::default(),
782 browser: BrowserConfig::default(),
783 load: crate::load::LoadConfig::default(),
784 web_policy: crate::web_policy::WebAccessPolicy::default(),
785 };
786
787 let result = config.validate();
788 assert!(result.is_err());
789 assert!(result
790 .unwrap_err()
791 .to_string()
792 .contains("At least one LLM provider"));
793 }
794
795 #[test]
796 fn test_validate_tls_required() {
797 let config = Config {
798 llm: LLMConfig {
799 provider: LLMProvider::LiteLLM,
800 endpoint: "http://example.com:4000".to_string(),
801 model: "gpt-4o-mini".to_string(),
802 api_key: Some("key".to_string()),
803 timeout_secs: 30,
804 system_prompt: default_system_prompt(),
805 token_budget: None,
806 retry_max: 3,
807 retry_base_delay_ms: 100,
808 retry_max_delay_ms: 10000,
809 },
810 llms: vec![],
811 ravenfabric: RavenFabricConfig::default(),
812 security: SecurityConfig {
813 require_tls: true,
814 token_lifetime_secs: 3600,
815 audit_log: false,
816 prompt_injection_protection: false,
817 },
818 runtime: RuntimeConfig::default(),
819 telemetry: TelemetryConfig::default(),
820 scheduler: SchedulerConfig::default(),
821 web_search: WebSearchConfig::default(),
822 heartbeat: crate::heartbeat::HeartbeatConfig::default(),
823 mcp: McpConfig::default(),
824 swarm: crate::swarm::SwarmConfig::default(),
825 browser: BrowserConfig::default(),
826 load: crate::load::LoadConfig::default(),
827 web_policy: crate::web_policy::WebAccessPolicy::default(),
828 };
829
830 let result = config.validate();
831 assert!(result.is_err());
832 let err = result.unwrap_err().to_string();
833 assert!(err.contains("TLS required"));
834 }
835
836 #[test]
837 fn test_validate_tls_localhost_allowed() {
838 let config = Config {
839 llm: LLMConfig {
840 provider: LLMProvider::LiteLLM,
841 endpoint: "http://localhost:4000".to_string(),
842 model: "gpt-4o-mini".to_string(),
843 api_key: Some("key".to_string()),
844 timeout_secs: 30,
845 system_prompt: default_system_prompt(),
846 token_budget: None,
847 retry_max: 3,
848 retry_base_delay_ms: 100,
849 retry_max_delay_ms: 10000,
850 },
851 llms: vec![],
852 ravenfabric: RavenFabricConfig::default(),
853 security: SecurityConfig {
854 require_tls: true,
855 token_lifetime_secs: 3600,
856 audit_log: false,
857 prompt_injection_protection: false,
858 },
859 runtime: RuntimeConfig::default(),
860 telemetry: TelemetryConfig::default(),
861 scheduler: SchedulerConfig::default(),
862 web_search: WebSearchConfig::default(),
863 heartbeat: crate::heartbeat::HeartbeatConfig::default(),
864 mcp: McpConfig::default(),
865 swarm: crate::swarm::SwarmConfig::default(),
866 browser: BrowserConfig::default(),
867 load: crate::load::LoadConfig::default(),
868 web_policy: crate::web_policy::WebAccessPolicy::default(),
869 };
870
871 let result = config.validate();
872 assert!(result.is_ok());
873 }
874
875 #[test]
876 fn test_validate_openai_no_endpoint_needed() {
877 let config = Config {
878 llm: LLMConfig {
879 provider: LLMProvider::OpenAI,
880 endpoint: String::new(),
881 model: "gpt-4o".to_string(),
882 api_key: Some("sk-key".to_string()),
883 timeout_secs: 30,
884 system_prompt: default_system_prompt(),
885 token_budget: None,
886 retry_max: 3,
887 retry_base_delay_ms: 100,
888 retry_max_delay_ms: 10000,
889 },
890 llms: vec![],
891 ravenfabric: RavenFabricConfig::default(),
892 security: SecurityConfig {
893 require_tls: false,
894 token_lifetime_secs: 3600,
895 audit_log: false,
896 prompt_injection_protection: false,
897 },
898 runtime: RuntimeConfig::default(),
899 telemetry: TelemetryConfig::default(),
900 scheduler: SchedulerConfig::default(),
901 web_search: WebSearchConfig::default(),
902 heartbeat: crate::heartbeat::HeartbeatConfig::default(),
903 mcp: McpConfig::default(),
904 swarm: crate::swarm::SwarmConfig::default(),
905 browser: BrowserConfig::default(),
906 load: crate::load::LoadConfig::default(),
907 web_policy: crate::web_policy::WebAccessPolicy::default(),
908 };
909
910 let result = config.validate();
915 assert!(result.is_err()); }
917
918 #[test]
919 fn test_validate_multi_provider() {
920 let config = Config {
921 llm: LLMConfig::default(),
922 llms: vec![LLMConfig {
923 provider: LLMProvider::Ollama,
924 endpoint: "http://localhost:11434".to_string(),
925 model: "llama3.1".to_string(),
926 api_key: None,
927 timeout_secs: 60,
928 system_prompt: default_system_prompt(),
929 token_budget: None,
930 retry_max: 3,
931 retry_base_delay_ms: 100,
932 retry_max_delay_ms: 10000,
933 }],
934 ravenfabric: RavenFabricConfig::default(),
935 security: SecurityConfig {
936 require_tls: false,
937 token_lifetime_secs: 3600,
938 audit_log: false,
939 prompt_injection_protection: false,
940 },
941 runtime: RuntimeConfig::default(),
942 telemetry: TelemetryConfig::default(),
943 scheduler: SchedulerConfig::default(),
944 web_search: WebSearchConfig::default(),
945 heartbeat: crate::heartbeat::HeartbeatConfig::default(),
946 mcp: McpConfig::default(),
947 swarm: crate::swarm::SwarmConfig::default(),
948 browser: BrowserConfig::default(),
949 load: crate::load::LoadConfig::default(),
950 web_policy: crate::web_policy::WebAccessPolicy::default(),
951 };
952
953 let result = config.validate();
954 assert!(result.is_ok());
955 }
956
957 #[test]
958 fn test_ravenfabric_config_default() {
959 let config = RavenFabricConfig::default();
960 assert!(config.endpoint.is_none());
961 assert!(config.agent_id.is_none());
962 assert!(config.remote_exec);
963 assert!(config.allowed_hosts.is_empty());
964 }
965
966 #[test]
967 fn test_security_config_default() {
968 let config = SecurityConfig::default();
969 assert!(config.require_tls);
970 assert_eq!(config.token_lifetime_secs, 3600);
971 assert!(config.audit_log);
972 }
973
974 #[test]
975 fn test_runtime_config_default() {
976 let config = RuntimeConfig::default();
977 assert_eq!(config.workdir, "/tmp/ravenclaws-workdir");
978 assert_eq!(config.max_agents, 10);
979 assert_eq!(config.health_interval_secs, 60);
980 }
981
982 #[test]
983 fn test_config_error_display() {
984 let err = ConfigError::LoadError("file not found".to_string());
985 assert_eq!(format!("{}", err), "Failed to load config: file not found");
986
987 let err = ConfigError::ValidationError("bad field".to_string());
988 assert_eq!(format!("{}", err), "Invalid configuration: bad field");
989
990 let err = ConfigError::MissingEnvVar("API_KEY".to_string());
991 assert_eq!(
992 format!("{}", err),
993 "Missing required environment variable: API_KEY"
994 );
995 }
996
997 #[test]
998 fn test_validate_openrouter_no_endpoint_needed() {
999 let config = Config {
1000 llm: LLMConfig {
1001 provider: LLMProvider::OpenRouter,
1002 endpoint: String::new(),
1003 model: "anthropic/claude-sonnet-4-20250514".to_string(),
1004 api_key: Some("or-key".to_string()),
1005 timeout_secs: 30,
1006 system_prompt: default_system_prompt(),
1007 token_budget: None,
1008 retry_max: 3,
1009 retry_base_delay_ms: 100,
1010 retry_max_delay_ms: 10000,
1011 },
1012 llms: vec![],
1013 ravenfabric: RavenFabricConfig::default(),
1014 security: SecurityConfig {
1015 require_tls: false,
1016 token_lifetime_secs: 3600,
1017 audit_log: false,
1018 prompt_injection_protection: false,
1019 },
1020 runtime: RuntimeConfig::default(),
1021 telemetry: TelemetryConfig::default(),
1022 scheduler: SchedulerConfig::default(),
1023 web_search: WebSearchConfig::default(),
1024 heartbeat: crate::heartbeat::HeartbeatConfig::default(),
1025 mcp: McpConfig::default(),
1026 swarm: crate::swarm::SwarmConfig::default(),
1027 browser: BrowserConfig::default(),
1028 load: crate::load::LoadConfig::default(),
1029 web_policy: crate::web_policy::WebAccessPolicy::default(),
1030 };
1031
1032 let result = config.validate();
1035 assert!(result.is_err());
1036 }
1037
1038 #[test]
1039 fn test_validate_ollama_needs_endpoint() {
1040 let config = Config {
1041 llm: LLMConfig {
1042 provider: LLMProvider::Ollama,
1043 endpoint: String::new(),
1044 model: "llama3.1".to_string(),
1045 api_key: None,
1046 timeout_secs: 30,
1047 system_prompt: default_system_prompt(),
1048 token_budget: None,
1049 retry_max: 3,
1050 retry_base_delay_ms: 100,
1051 retry_max_delay_ms: 10000,
1052 },
1053 llms: vec![],
1054 ravenfabric: RavenFabricConfig::default(),
1055 security: SecurityConfig {
1056 require_tls: false,
1057 token_lifetime_secs: 3600,
1058 audit_log: false,
1059 prompt_injection_protection: false,
1060 },
1061 runtime: RuntimeConfig::default(),
1062 telemetry: TelemetryConfig::default(),
1063 scheduler: SchedulerConfig::default(),
1064 web_search: WebSearchConfig::default(),
1065 heartbeat: crate::heartbeat::HeartbeatConfig::default(),
1066 mcp: McpConfig::default(),
1067 swarm: crate::swarm::SwarmConfig::default(),
1068 browser: BrowserConfig::default(),
1069 load: crate::load::LoadConfig::default(),
1070 web_policy: crate::web_policy::WebAccessPolicy::default(),
1071 };
1072
1073 let result = config.validate();
1074 assert!(result.is_err());
1075 let err = result.unwrap_err().to_string();
1076 assert!(err.contains("At least one LLM provider"));
1077 }
1078
1079 #[test]
1080 fn test_validate_tls_localhost_ip_allowed() {
1081 let config = Config {
1082 llm: LLMConfig {
1083 provider: LLMProvider::LiteLLM,
1084 endpoint: "http://127.0.0.1:4000".to_string(),
1085 model: "gpt-4o-mini".to_string(),
1086 api_key: Some("key".to_string()),
1087 timeout_secs: 30,
1088 system_prompt: default_system_prompt(),
1089 token_budget: None,
1090 retry_max: 3,
1091 retry_base_delay_ms: 100,
1092 retry_max_delay_ms: 10000,
1093 },
1094 llms: vec![],
1095 ravenfabric: RavenFabricConfig::default(),
1096 security: SecurityConfig {
1097 require_tls: true,
1098 token_lifetime_secs: 3600,
1099 audit_log: false,
1100 prompt_injection_protection: false,
1101 },
1102 runtime: RuntimeConfig::default(),
1103 telemetry: TelemetryConfig::default(),
1104 scheduler: SchedulerConfig::default(),
1105 web_search: WebSearchConfig::default(),
1106 heartbeat: crate::heartbeat::HeartbeatConfig::default(),
1107 mcp: McpConfig::default(),
1108 swarm: crate::swarm::SwarmConfig::default(),
1109 browser: BrowserConfig::default(),
1110 load: crate::load::LoadConfig::default(),
1111 web_policy: crate::web_policy::WebAccessPolicy::default(),
1112 };
1113
1114 let result = config.validate();
1115 assert!(result.is_ok());
1116 }
1117
1118 #[test]
1119 fn test_validate_tls_wildcard_allowed() {
1120 let config = Config {
1121 llm: LLMConfig {
1122 provider: LLMProvider::LiteLLM,
1123 endpoint: "http://0.0.0.0:4000".to_string(),
1124 model: "gpt-4o-mini".to_string(),
1125 api_key: Some("key".to_string()),
1126 timeout_secs: 30,
1127 system_prompt: default_system_prompt(),
1128 token_budget: None,
1129 retry_max: 3,
1130 retry_base_delay_ms: 100,
1131 retry_max_delay_ms: 10000,
1132 },
1133 llms: vec![],
1134 ravenfabric: RavenFabricConfig::default(),
1135 security: SecurityConfig {
1136 require_tls: true,
1137 token_lifetime_secs: 3600,
1138 audit_log: false,
1139 prompt_injection_protection: false,
1140 },
1141 runtime: RuntimeConfig::default(),
1142 telemetry: TelemetryConfig::default(),
1143 scheduler: SchedulerConfig::default(),
1144 web_search: WebSearchConfig::default(),
1145 heartbeat: crate::heartbeat::HeartbeatConfig::default(),
1146 mcp: McpConfig::default(),
1147 swarm: crate::swarm::SwarmConfig::default(),
1148 browser: BrowserConfig::default(),
1149 load: crate::load::LoadConfig::default(),
1150 web_policy: crate::web_policy::WebAccessPolicy::default(),
1151 };
1152
1153 let result = config.validate();
1154 assert!(result.is_ok());
1155 }
1156
1157 #[test]
1158 fn test_validate_multi_provider_with_tls() {
1159 let config = Config {
1160 llm: LLMConfig::default(),
1161 llms: vec![
1162 LLMConfig {
1163 provider: LLMProvider::Ollama,
1164 endpoint: "http://localhost:11434".to_string(),
1165 model: "llama3.1".to_string(),
1166 api_key: None,
1167 timeout_secs: 60,
1168 system_prompt: default_system_prompt(),
1169 token_budget: None,
1170 retry_max: 3,
1171 retry_base_delay_ms: 100,
1172 retry_max_delay_ms: 10000,
1173 },
1174 LLMConfig {
1175 provider: LLMProvider::LiteLLM,
1176 endpoint: "https://litellm.example.com:4000".to_string(),
1177 model: "gpt-4o-mini".to_string(),
1178 api_key: Some("key".to_string()),
1179 timeout_secs: 30,
1180 system_prompt: default_system_prompt(),
1181 token_budget: None,
1182 retry_max: 3,
1183 retry_base_delay_ms: 100,
1184 retry_max_delay_ms: 10000,
1185 },
1186 ],
1187 ravenfabric: RavenFabricConfig::default(),
1188 security: SecurityConfig {
1189 require_tls: true,
1190 token_lifetime_secs: 3600,
1191 audit_log: false,
1192 prompt_injection_protection: false,
1193 },
1194 runtime: RuntimeConfig::default(),
1195 telemetry: TelemetryConfig::default(),
1196 scheduler: SchedulerConfig::default(),
1197 web_search: WebSearchConfig::default(),
1198 heartbeat: crate::heartbeat::HeartbeatConfig::default(),
1199 mcp: McpConfig::default(),
1200 swarm: crate::swarm::SwarmConfig::default(),
1201 browser: BrowserConfig::default(),
1202 load: crate::load::LoadConfig::default(),
1203 web_policy: crate::web_policy::WebAccessPolicy::default(),
1204 };
1205
1206 let result = config.validate();
1207 assert!(result.is_ok());
1208 }
1209
1210 #[test]
1211 fn test_validate_multi_provider_tls_failure() {
1212 let config = Config {
1213 llm: LLMConfig::default(),
1214 llms: vec![LLMConfig {
1215 provider: LLMProvider::LiteLLM,
1216 endpoint: "http://example.com:4000".to_string(),
1217 model: "gpt-4o-mini".to_string(),
1218 api_key: Some("key".to_string()),
1219 timeout_secs: 30,
1220 system_prompt: default_system_prompt(),
1221 token_budget: None,
1222 retry_max: 3,
1223 retry_base_delay_ms: 100,
1224 retry_max_delay_ms: 10000,
1225 }],
1226 ravenfabric: RavenFabricConfig::default(),
1227 security: SecurityConfig {
1228 require_tls: true,
1229 token_lifetime_secs: 3600,
1230 audit_log: false,
1231 prompt_injection_protection: false,
1232 },
1233 runtime: RuntimeConfig::default(),
1234 telemetry: TelemetryConfig::default(),
1235 scheduler: SchedulerConfig::default(),
1236 web_search: WebSearchConfig::default(),
1237 heartbeat: crate::heartbeat::HeartbeatConfig::default(),
1238 mcp: McpConfig::default(),
1239 swarm: crate::swarm::SwarmConfig::default(),
1240 browser: BrowserConfig::default(),
1241 load: crate::load::LoadConfig::default(),
1242 web_policy: crate::web_policy::WebAccessPolicy::default(),
1243 };
1244
1245 let result = config.validate();
1246 assert!(result.is_err());
1247 let err = result.unwrap_err().to_string();
1248 assert!(err.contains("TLS required"));
1249 }
1250
1251 #[test]
1252 fn test_ravenfabric_config_custom() {
1253 let config = RavenFabricConfig {
1254 endpoint: Some("https://fabric.example.com:8443".to_string()),
1255 agent_id: Some("agent-01".to_string()),
1256 remote_exec: false,
1257 allowed_hosts: vec!["10.0.0.0/8".to_string()],
1258 };
1259 assert_eq!(config.endpoint.unwrap(), "https://fabric.example.com:8443");
1260 assert_eq!(config.agent_id.unwrap(), "agent-01");
1261 assert!(!config.remote_exec);
1262 assert_eq!(config.allowed_hosts.len(), 1);
1263 }
1264
1265 #[test]
1266 fn test_security_config_custom() {
1267 let config = SecurityConfig {
1268 require_tls: false,
1269 token_lifetime_secs: 7200,
1270 audit_log: false,
1271 prompt_injection_protection: false,
1272 };
1273 assert!(!config.require_tls);
1274 assert_eq!(config.token_lifetime_secs, 7200);
1275 assert!(!config.audit_log);
1276 }
1277
1278 #[test]
1279 fn test_runtime_config_custom() {
1280 let config = RuntimeConfig {
1281 workdir: "/data".to_string(),
1282 max_agents: 5,
1283 health_interval_secs: 120,
1284 host: Some("127.0.0.1".to_string()),
1285 port: 9090,
1286 checkpoint_dir: None,
1287 checkpoint_interval: 1,
1288 };
1289 assert_eq!(config.workdir, "/data");
1290 assert_eq!(config.max_agents, 5);
1291 assert_eq!(config.health_interval_secs, 120);
1292 assert_eq!(config.host, Some("127.0.0.1".to_string()));
1293 assert_eq!(config.port, 9090);
1294 }
1295
1296 #[test]
1297 fn test_llm_config_custom() {
1298 let config = LLMConfig {
1299 provider: LLMProvider::OpenAI,
1300 endpoint: String::new(),
1301 model: "gpt-4o".to_string(),
1302 api_key: Some("sk-test".to_string()),
1303 timeout_secs: 120,
1304 system_prompt: default_system_prompt(),
1305 token_budget: None,
1306 retry_max: 3,
1307 retry_base_delay_ms: 100,
1308 retry_max_delay_ms: 10000,
1309 };
1310 assert_eq!(config.provider, LLMProvider::OpenAI);
1311 assert_eq!(config.model, "gpt-4o");
1312 assert_eq!(config.timeout_secs, 120);
1313 assert_eq!(config.api_key.clone().unwrap(), "sk-test");
1314 }
1315
1316 #[test]
1317 fn test_llm_provider_serde_invalid_fallback() {
1318 let json = r#""unknown_provider""#;
1320 let provider: LLMProvider = serde_json::from_str(json).unwrap_or_default();
1321 assert_eq!(provider, LLMProvider::LiteLLM);
1322 }
1323
1324 #[test]
1325 #[serial(env_test)]
1326 fn test_config_load_with_env_overrides() {
1327 std::env::set_var("RAVENCLAWS__LLM__ENDPOINT", "http://localhost:4000");
1329 std::env::set_var("RAVENCLAWS__LLM__MODEL", "gpt-4o");
1330 std::env::set_var("LITELLM_API_KEY", "env-key");
1331
1332 let config = Config::load(None).unwrap();
1333 assert_eq!(config.llm.endpoint, "http://localhost:4000");
1334 assert_eq!(config.llm.model, "gpt-4o");
1335 assert_eq!(config.llm.api_key.clone().unwrap(), "env-key");
1336
1337 std::env::remove_var("RAVENCLAWS__LLM__ENDPOINT");
1339 std::env::remove_var("RAVENCLAWS__LLM__MODEL");
1340 std::env::remove_var("LITELLM_API_KEY");
1341 }
1342
1343 #[test]
1344 #[serial(env_test)]
1345 fn test_config_load_with_llms_json_env() {
1346 let llms_json = r#"[{"provider":"ollama","endpoint":"http://localhost:11434","model":"llama3.1","timeout_secs":60}]"#;
1347 std::env::set_var("RAVENCLAWS__LLMS", llms_json);
1348 std::env::set_var("LITELLM_API_KEY", "dummy");
1349 std::env::set_var("RAVENCLAWS__LLM__ENDPOINT", "http://localhost:4000");
1350
1351 let config = Config::load(None).unwrap();
1352 assert_eq!(config.llms.len(), 1);
1353 assert_eq!(config.llms[0].provider, LLMProvider::Ollama);
1354 assert_eq!(config.llms[0].endpoint, "http://localhost:11434");
1355 assert_eq!(config.llms[0].model, "llama3.1");
1356 assert_eq!(config.llms[0].timeout_secs, 60);
1357
1358 std::env::remove_var("RAVENCLAWS__LLMS");
1360 std::env::remove_var("LITELLM_API_KEY");
1361 std::env::remove_var("RAVENCLAWS__LLM__ENDPOINT");
1362 }
1363
1364 #[test]
1365 #[serial(env_test)]
1366 fn test_config_load_with_ravenfabric_env() {
1367 std::env::set_var("RAVENFABRIC_ENDPOINT", "https://fabric.example.com:8443");
1368 std::env::set_var("LITELLM_API_KEY", "dummy");
1369 std::env::set_var("RAVENCLAWS__LLM__ENDPOINT", "http://localhost:4000");
1370
1371 let config = Config::load(None).unwrap();
1372 assert_eq!(
1373 config.ravenfabric.endpoint.unwrap(),
1374 "https://fabric.example.com:8443"
1375 );
1376
1377 std::env::remove_var("RAVENFABRIC_ENDPOINT");
1379 std::env::remove_var("LITELLM_API_KEY");
1380 std::env::remove_var("RAVENCLAWS__LLM__ENDPOINT");
1381 }
1382
1383 #[test]
1384 #[serial(env_test)]
1385 fn test_config_load_with_provider_env() {
1386 std::env::set_var("RAVENCLAWS__LLM__PROVIDER", "openai");
1388 std::env::set_var("RAVENCLAWS__LLM__ENDPOINT", "https://api.openai.com");
1389 std::env::set_var("LITELLM_API_KEY", "dummy");
1390
1391 let config = Config::load(None).unwrap();
1392 assert_eq!(config.llm.provider, LLMProvider::OpenAI);
1393
1394 std::env::remove_var("RAVENCLAWS__LLM__PROVIDER");
1396 std::env::remove_var("RAVENCLAWS__LLM__ENDPOINT");
1397 std::env::remove_var("LITELLM_API_KEY");
1398 }
1399
1400 #[test]
1401 fn test_config_load_with_provider_env_fallback() {
1402 let mapped = match "unknown" {
1405 "openrouter" => LLMProvider::OpenRouter,
1406 "ollama" => LLMProvider::Ollama,
1407 "openai" => LLMProvider::OpenAI,
1408 _ => LLMProvider::LiteLLM,
1409 };
1410 assert_eq!(mapped, LLMProvider::LiteLLM);
1411
1412 let mapped = match "" {
1414 "openrouter" => LLMProvider::OpenRouter,
1415 "ollama" => LLMProvider::Ollama,
1416 "openai" => LLMProvider::OpenAI,
1417 _ => LLMProvider::LiteLLM,
1418 };
1419 assert_eq!(mapped, LLMProvider::LiteLLM);
1420 }
1421
1422 #[test]
1423 fn test_validate_openai_with_endpoint() {
1424 let config = Config {
1425 llm: LLMConfig {
1426 provider: LLMProvider::OpenAI,
1427 endpoint: "https://api.openai.com".to_string(),
1428 model: "gpt-4o".to_string(),
1429 api_key: Some("sk-key".to_string()),
1430 timeout_secs: 30,
1431 system_prompt: default_system_prompt(),
1432 token_budget: None,
1433 retry_max: 3,
1434 retry_base_delay_ms: 100,
1435 retry_max_delay_ms: 10000,
1436 },
1437 llms: vec![],
1438 ravenfabric: RavenFabricConfig::default(),
1439 security: SecurityConfig {
1440 require_tls: false,
1441 token_lifetime_secs: 3600,
1442 audit_log: false,
1443 prompt_injection_protection: false,
1444 },
1445 runtime: RuntimeConfig::default(),
1446 telemetry: TelemetryConfig::default(),
1447 scheduler: SchedulerConfig::default(),
1448 web_search: WebSearchConfig::default(),
1449 heartbeat: crate::heartbeat::HeartbeatConfig::default(),
1450 mcp: McpConfig::default(),
1451 swarm: crate::swarm::SwarmConfig::default(),
1452 browser: BrowserConfig::default(),
1453 load: crate::load::LoadConfig::default(),
1454 web_policy: crate::web_policy::WebAccessPolicy::default(),
1455 };
1456
1457 let result = config.validate();
1458 assert!(result.is_ok());
1459 }
1460
1461 #[test]
1462 fn test_validate_openrouter_with_endpoint() {
1463 let config = Config {
1464 llm: LLMConfig {
1465 provider: LLMProvider::OpenRouter,
1466 endpoint: "https://openrouter.ai/api".to_string(),
1467 model: "anthropic/claude-sonnet-4-20250514".to_string(),
1468 api_key: Some("or-key".to_string()),
1469 timeout_secs: 30,
1470 system_prompt: default_system_prompt(),
1471 token_budget: None,
1472 retry_max: 3,
1473 retry_base_delay_ms: 100,
1474 retry_max_delay_ms: 10000,
1475 },
1476 llms: vec![],
1477 ravenfabric: RavenFabricConfig::default(),
1478 security: SecurityConfig {
1479 require_tls: false,
1480 token_lifetime_secs: 3600,
1481 audit_log: false,
1482 prompt_injection_protection: false,
1483 },
1484 runtime: RuntimeConfig::default(),
1485 telemetry: TelemetryConfig::default(),
1486 scheduler: SchedulerConfig::default(),
1487 web_search: WebSearchConfig::default(),
1488 heartbeat: crate::heartbeat::HeartbeatConfig::default(),
1489 mcp: McpConfig::default(),
1490 swarm: crate::swarm::SwarmConfig::default(),
1491 browser: BrowserConfig::default(),
1492 load: crate::load::LoadConfig::default(),
1493 web_policy: crate::web_policy::WebAccessPolicy::default(),
1494 };
1495
1496 let result = config.validate();
1497 assert!(result.is_ok());
1498 }
1499
1500 #[test]
1501 fn test_validate_https_endpoint_with_tls() {
1502 let config = Config {
1503 llm: LLMConfig {
1504 provider: LLMProvider::LiteLLM,
1505 endpoint: "https://api.example.com:4000".to_string(),
1506 model: "gpt-4o-mini".to_string(),
1507 api_key: Some("key".to_string()),
1508 timeout_secs: 30,
1509 system_prompt: default_system_prompt(),
1510 token_budget: None,
1511 retry_max: 3,
1512 retry_base_delay_ms: 100,
1513 retry_max_delay_ms: 10000,
1514 },
1515 llms: vec![],
1516 ravenfabric: RavenFabricConfig::default(),
1517 security: SecurityConfig {
1518 require_tls: true,
1519 token_lifetime_secs: 3600,
1520 audit_log: false,
1521 prompt_injection_protection: false,
1522 },
1523 runtime: RuntimeConfig::default(),
1524 telemetry: TelemetryConfig::default(),
1525 scheduler: SchedulerConfig::default(),
1526 web_search: WebSearchConfig::default(),
1527 heartbeat: crate::heartbeat::HeartbeatConfig::default(),
1528 mcp: McpConfig::default(),
1529 swarm: crate::swarm::SwarmConfig::default(),
1530 browser: BrowserConfig::default(),
1531 load: crate::load::LoadConfig::default(),
1532 web_policy: crate::web_policy::WebAccessPolicy::default(),
1533 };
1534
1535 let result = config.validate();
1536 assert!(result.is_ok());
1537 }
1538
1539 #[test]
1540 #[serial(env_test)]
1541 fn test_config_load_with_nonexistent_file() {
1542 std::env::set_var("LITELLM_API_KEY", "test-key");
1545 std::env::set_var("RAVENCLAWS__LLM__ENDPOINT", "http://localhost:4000");
1546
1547 let result = Config::load(Some("/tmp/nonexistent/ravenclaws.toml"));
1548 assert!(result.is_ok());
1549
1550 std::env::remove_var("LITELLM_API_KEY");
1551 std::env::remove_var("RAVENCLAWS__LLM__ENDPOINT");
1552 }
1553
1554 #[test]
1555 fn test_config_error_missing_env_var_display() {
1556 let err = ConfigError::MissingEnvVar("DATABASE_URL".to_string());
1557 assert_eq!(
1558 format!("{}", err),
1559 "Missing required environment variable: DATABASE_URL"
1560 );
1561 }
1562
1563 #[test]
1564 fn test_llm_config_deserialize() {
1565 let json = r#"{
1566 "provider": "openai",
1567 "endpoint": "https://api.openai.com",
1568 "model": "gpt-4o",
1569 "api_key": "sk-test",
1570 "timeout_secs": 120
1571 }"#;
1572 let config: LLMConfig = serde_json::from_str(json).unwrap();
1573
1574 assert_eq!(config.provider, LLMProvider::OpenAI);
1575 assert_eq!(config.endpoint, "https://api.openai.com");
1576 assert_eq!(config.model, "gpt-4o");
1577 assert_eq!(config.timeout_secs, 120);
1578 }
1579
1580 #[test]
1581 fn test_security_config_serde_defaults() {
1582 let json = r#"{}"#;
1584 let config: SecurityConfig = serde_json::from_str(json).unwrap();
1585 assert!(config.require_tls);
1586 assert_eq!(config.token_lifetime_secs, 3600);
1587 assert!(config.audit_log);
1588 }
1589
1590 #[test]
1591 fn test_runtime_config_serde_defaults() {
1592 let json = r#"{}"#;
1593 let config: RuntimeConfig = serde_json::from_str(json).unwrap();
1594 assert_eq!(config.workdir, "/tmp/ravenclaws-workdir");
1595 assert_eq!(config.max_agents, 10);
1596 assert_eq!(config.health_interval_secs, 60);
1597 }
1598
1599 #[test]
1600 fn test_ravenfabric_config_serde_defaults() {
1601 let json = r#"{}"#;
1602 let config: RavenFabricConfig = serde_json::from_str(json).unwrap();
1603 assert!(config.endpoint.is_none());
1604 assert!(config.agent_id.is_none());
1605 assert!(config.remote_exec);
1606 assert!(config.allowed_hosts.is_empty());
1607 }
1608
1609 #[test]
1610 fn test_validate_ollama_with_endpoint_succeeds() {
1611 let config = Config {
1612 llm: LLMConfig {
1613 provider: LLMProvider::Ollama,
1614 endpoint: "http://localhost:11434".to_string(),
1615 model: "llama3.1".to_string(),
1616 api_key: None,
1617 timeout_secs: 60,
1618 system_prompt: default_system_prompt(),
1619 token_budget: None,
1620 retry_max: 3,
1621 retry_base_delay_ms: 100,
1622 retry_max_delay_ms: 10000,
1623 },
1624 llms: vec![],
1625 ravenfabric: RavenFabricConfig::default(),
1626 security: SecurityConfig {
1627 require_tls: false,
1628 token_lifetime_secs: 3600,
1629 audit_log: false,
1630 prompt_injection_protection: false,
1631 },
1632 runtime: RuntimeConfig::default(),
1633 telemetry: TelemetryConfig::default(),
1634 scheduler: SchedulerConfig::default(),
1635 web_search: WebSearchConfig::default(),
1636 heartbeat: crate::heartbeat::HeartbeatConfig::default(),
1637 mcp: McpConfig::default(),
1638 swarm: crate::swarm::SwarmConfig::default(),
1639 browser: BrowserConfig::default(),
1640 load: crate::load::LoadConfig::default(),
1641 web_policy: crate::web_policy::WebAccessPolicy::default(),
1642 };
1643
1644 let result = config.validate();
1645 assert!(result.is_ok());
1646 }
1647
1648 #[test]
1649 fn test_validate_openrouter_with_endpoint_succeeds() {
1650 let config = Config {
1651 llm: LLMConfig {
1652 provider: LLMProvider::OpenRouter,
1653 endpoint: "https://openrouter.ai/api".to_string(),
1654 model: "anthropic/claude-sonnet-4-20250514".to_string(),
1655 api_key: Some("or-key".to_string()),
1656 timeout_secs: 30,
1657 system_prompt: default_system_prompt(),
1658 token_budget: None,
1659 retry_max: 3,
1660 retry_base_delay_ms: 100,
1661 retry_max_delay_ms: 10000,
1662 },
1663 llms: vec![],
1664 ravenfabric: RavenFabricConfig::default(),
1665 security: SecurityConfig {
1666 require_tls: false,
1667 token_lifetime_secs: 3600,
1668 audit_log: false,
1669 prompt_injection_protection: false,
1670 },
1671 runtime: RuntimeConfig::default(),
1672 telemetry: TelemetryConfig::default(),
1673 scheduler: SchedulerConfig::default(),
1674 web_search: WebSearchConfig::default(),
1675 heartbeat: crate::heartbeat::HeartbeatConfig::default(),
1676 mcp: McpConfig::default(),
1677 swarm: crate::swarm::SwarmConfig::default(),
1678 browser: BrowserConfig::default(),
1679 load: crate::load::LoadConfig::default(),
1680 web_policy: crate::web_policy::WebAccessPolicy::default(),
1681 };
1682
1683 let result = config.validate();
1684 assert!(result.is_ok());
1685 }
1686
1687 #[test]
1688 fn test_validate_litellm_with_empty_endpoint_fails() {
1689 let config = Config {
1690 llm: LLMConfig {
1691 provider: LLMProvider::LiteLLM,
1692 endpoint: String::new(),
1693 model: "gpt-4o-mini".to_string(),
1694 api_key: Some("key".to_string()),
1695 timeout_secs: 30,
1696 system_prompt: default_system_prompt(),
1697 token_budget: None,
1698 retry_max: 3,
1699 retry_base_delay_ms: 100,
1700 retry_max_delay_ms: 10000,
1701 },
1702 llms: vec![],
1703 ravenfabric: RavenFabricConfig::default(),
1704 security: SecurityConfig {
1705 require_tls: false,
1706 token_lifetime_secs: 3600,
1707 audit_log: false,
1708 prompt_injection_protection: false,
1709 },
1710 runtime: RuntimeConfig::default(),
1711 telemetry: TelemetryConfig::default(),
1712 scheduler: SchedulerConfig::default(),
1713 web_search: WebSearchConfig::default(),
1714 heartbeat: crate::heartbeat::HeartbeatConfig::default(),
1715 mcp: McpConfig::default(),
1716 swarm: crate::swarm::SwarmConfig::default(),
1717 browser: BrowserConfig::default(),
1718 load: crate::load::LoadConfig::default(),
1719 web_policy: crate::web_policy::WebAccessPolicy::default(),
1720 };
1721
1722 let result = config.validate();
1723 assert!(result.is_err());
1724 assert!(result
1725 .unwrap_err()
1726 .to_string()
1727 .contains("At least one LLM provider"));
1728 }
1729
1730 #[test]
1731 fn test_llm_provider_serde_serialize() {
1732 let provider = LLMProvider::OpenAI;
1733 let json = serde_json::to_string(&provider).unwrap();
1734 assert_eq!(json, r#""openai""#);
1735
1736 let provider = LLMProvider::Ollama;
1737 let json = serde_json::to_string(&provider).unwrap();
1738 assert_eq!(json, r#""ollama""#);
1739
1740 let provider = LLMProvider::OpenRouter;
1741 let json = serde_json::to_string(&provider).unwrap();
1742 assert_eq!(json, r#""openrouter""#);
1743
1744 let provider = LLMProvider::LiteLLM;
1745 let json = serde_json::to_string(&provider).unwrap();
1746 assert_eq!(json, r#""litellm""#);
1747 }
1748}