Skip to main content

a3s_code_core/
config.rs

1//! Configuration module for A3S Code
2//!
3//! Provides configuration for:
4//! - LLM providers and models (defaultModel in "provider/model" format, providers)
5//! - Queue configuration (a3s-lane integration)
6//! - Search configuration (a3s-search integration)
7//! - Directories for dynamic skill and agent loading
8//!
9//! Configuration is loaded from HCL files or HCL strings only.
10//! JSON support has been removed.
11
12use crate::error::{CodeError, Result};
13use crate::llm::LlmConfig;
14use crate::memory::MemoryConfig;
15use serde::{Deserialize, Serialize};
16use serde_json::Value as JsonValue;
17use std::path::{Path, PathBuf};
18
19// ============================================================================
20// Provider Configuration
21// ============================================================================
22
23/// Model cost information (per million tokens)
24#[derive(Debug, Clone, Serialize, Deserialize, Default)]
25#[serde(rename_all = "camelCase")]
26pub struct ModelCost {
27    /// Input token cost
28    #[serde(default)]
29    pub input: f64,
30    /// Output token cost
31    #[serde(default)]
32    pub output: f64,
33    /// Cache read cost
34    #[serde(default)]
35    pub cache_read: f64,
36    /// Cache write cost
37    #[serde(default)]
38    pub cache_write: f64,
39}
40
41/// Model limits
42#[derive(Debug, Clone, Serialize, Deserialize, Default)]
43pub struct ModelLimit {
44    /// Maximum context tokens
45    #[serde(default)]
46    pub context: u32,
47    /// Maximum output tokens
48    #[serde(default)]
49    pub output: u32,
50}
51
52/// Model modalities (input/output types)
53#[derive(Debug, Clone, Serialize, Deserialize, Default)]
54pub struct ModelModalities {
55    /// Supported input types
56    #[serde(default)]
57    pub input: Vec<String>,
58    /// Supported output types
59    #[serde(default)]
60    pub output: Vec<String>,
61}
62
63/// Model configuration
64#[derive(Debug, Clone, Serialize, Deserialize)]
65#[serde(rename_all = "camelCase")]
66pub struct ModelConfig {
67    /// Model ID (e.g., "claude-sonnet-4-20250514")
68    pub id: String,
69    /// Display name
70    #[serde(default)]
71    pub name: String,
72    /// Model family (e.g., "claude-sonnet")
73    #[serde(default)]
74    pub family: String,
75    /// Per-model API key override
76    #[serde(default)]
77    pub api_key: Option<String>,
78    /// Per-model base URL override
79    #[serde(default)]
80    pub base_url: Option<String>,
81    /// Supports file attachments
82    #[serde(default)]
83    pub attachment: bool,
84    /// Supports reasoning/thinking
85    #[serde(default)]
86    pub reasoning: bool,
87    /// Supports tool calling
88    #[serde(default = "default_true")]
89    pub tool_call: bool,
90    /// Supports temperature setting
91    #[serde(default = "default_true")]
92    pub temperature: bool,
93    /// Release date
94    #[serde(default)]
95    pub release_date: Option<String>,
96    /// Input/output modalities
97    #[serde(default)]
98    pub modalities: ModelModalities,
99    /// Cost information
100    #[serde(default)]
101    pub cost: ModelCost,
102    /// Token limits
103    #[serde(default)]
104    pub limit: ModelLimit,
105}
106
107fn default_true() -> bool {
108    true
109}
110
111/// Provider configuration
112#[derive(Debug, Clone, Serialize, Deserialize)]
113#[serde(rename_all = "camelCase")]
114pub struct ProviderConfig {
115    /// Provider name (e.g., "anthropic", "openai")
116    pub name: String,
117    /// API key for this provider
118    #[serde(default)]
119    pub api_key: Option<String>,
120    /// Base URL for the API
121    #[serde(default)]
122    pub base_url: Option<String>,
123    /// Available models
124    #[serde(default)]
125    pub models: Vec<ModelConfig>,
126}
127
128impl ProviderConfig {
129    /// Find a model by ID
130    pub fn find_model(&self, model_id: &str) -> Option<&ModelConfig> {
131        self.models.iter().find(|m| m.id == model_id)
132    }
133
134    /// Get the effective API key for a model (model override or provider default)
135    pub fn get_api_key<'a>(&'a self, model: &'a ModelConfig) -> Option<&'a str> {
136        model.api_key.as_deref().or(self.api_key.as_deref())
137    }
138
139    /// Get the effective base URL for a model (model override or provider default)
140    pub fn get_base_url<'a>(&'a self, model: &'a ModelConfig) -> Option<&'a str> {
141        model.base_url.as_deref().or(self.base_url.as_deref())
142    }
143}
144
145// ============================================================================
146// Storage Configuration
147// ============================================================================
148
149/// Session storage backend type
150#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, Hash)]
151#[serde(rename_all = "lowercase")]
152pub enum StorageBackend {
153    /// In-memory storage (no persistence)
154    Memory,
155    /// File-based storage (JSON files)
156    #[default]
157    File,
158    /// Custom external storage (Redis, PostgreSQL, etc.)
159    ///
160    /// Requires a `SessionStore` implementation registered via `SessionManager::with_store()`.
161    /// Use `storage_url` in config to pass connection details.
162    Custom,
163}
164
165// ============================================================================
166// Main Configuration
167// ============================================================================
168
169/// Configuration for A3S Code
170#[derive(Debug, Clone, Serialize, Deserialize, Default)]
171#[serde(rename_all = "camelCase")]
172pub struct CodeConfig {
173    /// Default model in "provider/model" format (e.g., "anthropic/claude-sonnet-4-20250514")
174    #[serde(default, alias = "default_model")]
175    pub default_model: Option<String>,
176
177    /// Provider configurations
178    #[serde(default)]
179    pub providers: Vec<ProviderConfig>,
180
181    /// Session storage backend
182    #[serde(default)]
183    pub storage_backend: StorageBackend,
184
185    /// Sessions directory (for file backend)
186    #[serde(skip_serializing_if = "Option::is_none")]
187    pub sessions_dir: Option<PathBuf>,
188
189    /// Connection URL for custom storage backend (e.g., "redis://localhost:6379", "postgres://user:pass@localhost/a3s")
190    #[serde(default, skip_serializing_if = "Option::is_none")]
191    pub storage_url: Option<String>,
192
193    /// Directories to scan for skill files (*.md with tool definitions)
194    #[serde(default, alias = "skill_dirs")]
195    pub skill_dirs: Vec<PathBuf>,
196
197    /// Directories to scan for agent files (*.yaml or *.md)
198    #[serde(default, alias = "agent_dirs")]
199    pub agent_dirs: Vec<PathBuf>,
200
201    /// Maximum tool execution rounds per turn (default: 25)
202    #[serde(default, alias = "max_tool_rounds")]
203    pub max_tool_rounds: Option<usize>,
204
205    /// Thinking/reasoning budget in tokens
206    #[serde(default, alias = "thinking_budget")]
207    pub thinking_budget: Option<usize>,
208
209    /// Memory system configuration
210    #[serde(default, skip_serializing_if = "Option::is_none")]
211    pub memory: Option<MemoryConfig>,
212
213    /// Queue configuration (a3s-lane integration)
214    #[serde(default, skip_serializing_if = "Option::is_none")]
215    pub queue: Option<crate::queue::SessionQueueConfig>,
216
217    /// Search configuration (a3s-search integration)
218    #[serde(default, skip_serializing_if = "Option::is_none")]
219    pub search: Option<SearchConfig>,
220}
221
222/// Search engine configuration (a3s-search integration)
223#[derive(Debug, Clone, Serialize, Deserialize)]
224#[serde(rename_all = "camelCase")]
225pub struct SearchConfig {
226    /// Default timeout in seconds for all engines
227    #[serde(default = "default_search_timeout")]
228    pub timeout: u64,
229
230    /// Health monitor configuration
231    #[serde(default, skip_serializing_if = "Option::is_none")]
232    pub health: Option<SearchHealthConfig>,
233
234    /// Engine configurations
235    #[serde(default, rename = "engine")]
236    pub engines: std::collections::HashMap<String, SearchEngineConfig>,
237}
238
239/// Search health monitor configuration
240#[derive(Debug, Clone, Serialize, Deserialize)]
241#[serde(rename_all = "camelCase")]
242pub struct SearchHealthConfig {
243    /// Number of consecutive failures before suspending
244    #[serde(default = "default_max_failures")]
245    pub max_failures: u32,
246
247    /// Suspension duration in seconds
248    #[serde(default = "default_suspend_seconds")]
249    pub suspend_seconds: u64,
250}
251
252/// Per-engine search configuration
253#[derive(Debug, Clone, Serialize, Deserialize)]
254#[serde(rename_all = "camelCase")]
255pub struct SearchEngineConfig {
256    /// Whether the engine is enabled
257    #[serde(default = "default_enabled")]
258    pub enabled: bool,
259
260    /// Weight for ranking (higher = more influence)
261    #[serde(default = "default_weight")]
262    pub weight: f64,
263
264    /// Per-engine timeout override in seconds
265    #[serde(skip_serializing_if = "Option::is_none")]
266    pub timeout: Option<u64>,
267}
268
269fn default_search_timeout() -> u64 {
270    10
271}
272
273fn default_max_failures() -> u32 {
274    3
275}
276
277fn default_suspend_seconds() -> u64 {
278    60
279}
280
281fn default_enabled() -> bool {
282    true
283}
284
285fn default_weight() -> f64 {
286    1.0
287}
288
289impl CodeConfig {
290    /// Create a new empty configuration
291    pub fn new() -> Self {
292        Self::default()
293    }
294
295    /// Load configuration from an HCL file.
296    ///
297    /// Only `.hcl` files are supported. JSON support has been removed.
298    pub fn from_file(path: &Path) -> Result<Self> {
299        let content = std::fs::read_to_string(path).map_err(|e| {
300            CodeError::Config(format!(
301                "Failed to read config file {}: {}",
302                path.display(),
303                e
304            ))
305        })?;
306
307        Self::from_hcl(&content).map_err(|e| {
308            CodeError::Config(format!(
309                "Failed to parse HCL config {}: {}",
310                path.display(),
311                e
312            ))
313        })
314    }
315
316    /// Parse configuration from an HCL string.
317    ///
318    /// HCL attributes use `snake_case` which is converted to `camelCase` for
319    /// serde deserialization. Repeated blocks (e.g., `providers`, `models`)
320    /// are collected into JSON arrays.
321    pub fn from_hcl(content: &str) -> Result<Self> {
322        let body: hcl::Body = hcl::from_str(content)
323            .map_err(|e| CodeError::Config(format!("Failed to parse HCL: {}", e)))?;
324        let json_value = hcl_body_to_json(&body);
325        serde_json::from_value(json_value)
326            .map_err(|e| CodeError::Config(format!("Failed to deserialize HCL config: {}", e)))
327    }
328
329    /// Save configuration to a JSON file (used for persistence)
330    ///
331    /// Note: This saves as JSON format. To use HCL format, manually create .hcl files.
332    pub fn save_to_file(&self, path: &Path) -> Result<()> {
333        if let Some(parent) = path.parent() {
334            std::fs::create_dir_all(parent).map_err(|e| {
335                CodeError::Config(format!(
336                    "Failed to create config directory {}: {}",
337                    parent.display(),
338                    e
339                ))
340            })?;
341        }
342
343        let content = serde_json::to_string_pretty(self)
344            .map_err(|e| CodeError::Config(format!("Failed to serialize config: {}", e)))?;
345
346        std::fs::write(path, content).map_err(|e| {
347            CodeError::Config(format!(
348                "Failed to write config file {}: {}",
349                path.display(),
350                e
351            ))
352        })?;
353
354        Ok(())
355    }
356
357    /// Find a provider by name
358    pub fn find_provider(&self, name: &str) -> Option<&ProviderConfig> {
359        self.providers.iter().find(|p| p.name == name)
360    }
361
362    /// Get the default provider configuration (parsed from `default_model` "provider/model" format)
363    pub fn default_provider_config(&self) -> Option<&ProviderConfig> {
364        let default = self.default_model.as_ref()?;
365        let (provider_name, _) = default.split_once('/')?;
366        self.find_provider(provider_name)
367    }
368
369    /// Get the default model configuration (parsed from `default_model` "provider/model" format)
370    pub fn default_model_config(&self) -> Option<(&ProviderConfig, &ModelConfig)> {
371        let default = self.default_model.as_ref()?;
372        let (provider_name, model_id) = default.split_once('/')?;
373        let provider = self.find_provider(provider_name)?;
374        let model = provider.find_model(model_id)?;
375        Some((provider, model))
376    }
377
378    /// Get LlmConfig for the default provider and model
379    ///
380    /// Returns None if default provider/model is not configured or API key is missing.
381    pub fn default_llm_config(&self) -> Option<LlmConfig> {
382        let (provider, model) = self.default_model_config()?;
383        let api_key = provider.get_api_key(model)?;
384        let base_url = provider.get_base_url(model);
385
386        let mut config = LlmConfig::new(&provider.name, &model.id, api_key);
387        if let Some(url) = base_url {
388            config = config.with_base_url(url);
389        }
390        Some(config)
391    }
392
393    /// Get LlmConfig for a specific provider and model
394    ///
395    /// Returns None if provider/model is not found or API key is missing.
396    pub fn llm_config(&self, provider_name: &str, model_id: &str) -> Option<LlmConfig> {
397        let provider = self.find_provider(provider_name)?;
398        let model = provider.find_model(model_id)?;
399        let api_key = provider.get_api_key(model)?;
400        let base_url = provider.get_base_url(model);
401
402        let mut config = LlmConfig::new(&provider.name, &model.id, api_key);
403        if let Some(url) = base_url {
404            config = config.with_base_url(url);
405        }
406        Some(config)
407    }
408
409    /// List all available models across all providers
410    pub fn list_models(&self) -> Vec<(&ProviderConfig, &ModelConfig)> {
411        self.providers
412            .iter()
413            .flat_map(|p| p.models.iter().map(move |m| (p, m)))
414            .collect()
415    }
416
417    /// Add a skill directory
418    pub fn add_skill_dir(mut self, dir: impl Into<PathBuf>) -> Self {
419        self.skill_dirs.push(dir.into());
420        self
421    }
422
423    /// Add an agent directory
424    pub fn add_agent_dir(mut self, dir: impl Into<PathBuf>) -> Self {
425        self.agent_dirs.push(dir.into());
426        self
427    }
428
429    /// Check if any directories are configured
430    pub fn has_directories(&self) -> bool {
431        !self.skill_dirs.is_empty() || !self.agent_dirs.is_empty()
432    }
433
434    /// Check if provider configuration is available
435    pub fn has_providers(&self) -> bool {
436        !self.providers.is_empty()
437    }
438}
439
440// ============================================================================
441// HCL Parsing Helpers
442// ============================================================================
443
444/// Block labels that should be collected into JSON arrays.
445const HCL_ARRAY_BLOCKS: &[&str] = &["providers", "models"];
446
447/// Convert an HCL body into a JSON value with camelCase keys.
448fn hcl_body_to_json(body: &hcl::Body) -> JsonValue {
449    let mut map = serde_json::Map::new();
450
451    // Process attributes (key = value)
452    for attr in body.attributes() {
453        let key = snake_to_camel(attr.key.as_str());
454        let value = hcl_expr_to_json(attr.expr());
455        map.insert(key, value);
456    }
457
458    // Process blocks (repeated structures like `providers { ... }`)
459    for block in body.blocks() {
460        let key = snake_to_camel(block.identifier.as_str());
461        let block_value = hcl_body_to_json(block.body());
462
463        if HCL_ARRAY_BLOCKS.contains(&block.identifier.as_str()) {
464            // Collect into array
465            let arr = map
466                .entry(key)
467                .or_insert_with(|| JsonValue::Array(Vec::new()));
468            if let JsonValue::Array(ref mut vec) = arr {
469                vec.push(block_value);
470            }
471        } else {
472            map.insert(key, block_value);
473        }
474    }
475
476    JsonValue::Object(map)
477}
478
479/// Convert snake_case to camelCase.
480fn snake_to_camel(s: &str) -> String {
481    let mut result = String::with_capacity(s.len());
482    let mut capitalize_next = false;
483    for ch in s.chars() {
484        if ch == '_' {
485            capitalize_next = true;
486        } else if capitalize_next {
487            result.extend(ch.to_uppercase());
488            capitalize_next = false;
489        } else {
490            result.push(ch);
491        }
492    }
493    result
494}
495
496/// Convert an HCL expression to a JSON value.
497fn hcl_expr_to_json(expr: &hcl::Expression) -> JsonValue {
498    match expr {
499        hcl::Expression::String(s) => JsonValue::String(s.clone()),
500        hcl::Expression::Number(n) => {
501            if let Some(i) = n.as_i64() {
502                JsonValue::Number(i.into())
503            } else if let Some(f) = n.as_f64() {
504                serde_json::Number::from_f64(f)
505                    .map(JsonValue::Number)
506                    .unwrap_or(JsonValue::Null)
507            } else {
508                JsonValue::Null
509            }
510        }
511        hcl::Expression::Bool(b) => JsonValue::Bool(*b),
512        hcl::Expression::Null => JsonValue::Null,
513        hcl::Expression::Array(arr) => JsonValue::Array(arr.iter().map(hcl_expr_to_json).collect()),
514        hcl::Expression::Object(obj) => {
515            let map: serde_json::Map<String, JsonValue> = obj
516                .iter()
517                .map(|(k, v)| {
518                    let key = match k {
519                        hcl::ObjectKey::Identifier(id) => snake_to_camel(id.as_str()),
520                        hcl::ObjectKey::Expression(expr) => {
521                            if let hcl::Expression::String(s) = expr {
522                                snake_to_camel(s)
523                            } else {
524                                format!("{:?}", expr)
525                            }
526                        }
527                        _ => format!("{:?}", k),
528                    };
529                    (key, hcl_expr_to_json(v))
530                })
531                .collect();
532            JsonValue::Object(map)
533        }
534        _ => JsonValue::String(format!("{:?}", expr)),
535    }
536}
537
538#[cfg(test)]
539mod tests {
540    use super::*;
541
542    #[test]
543    fn test_config_default() {
544        let config = CodeConfig::default();
545        assert!(config.skill_dirs.is_empty());
546        assert!(config.agent_dirs.is_empty());
547        assert!(config.providers.is_empty());
548        assert!(config.default_model.is_none());
549        assert_eq!(config.storage_backend, StorageBackend::File);
550        assert!(config.sessions_dir.is_none());
551    }
552
553    #[test]
554    fn test_storage_backend_default() {
555        let backend = StorageBackend::default();
556        assert_eq!(backend, StorageBackend::File);
557    }
558
559    #[test]
560    fn test_storage_backend_serde() {
561        // Test serialization
562        let memory = StorageBackend::Memory;
563        let json = serde_json::to_string(&memory).unwrap();
564        assert_eq!(json, "\"memory\"");
565
566        let file = StorageBackend::File;
567        let json = serde_json::to_string(&file).unwrap();
568        assert_eq!(json, "\"file\"");
569
570        // Test deserialization
571        let memory: StorageBackend = serde_json::from_str("\"memory\"").unwrap();
572        assert_eq!(memory, StorageBackend::Memory);
573
574        let file: StorageBackend = serde_json::from_str("\"file\"").unwrap();
575        assert_eq!(file, StorageBackend::File);
576    }
577
578    #[test]
579    fn test_config_with_storage_backend() {
580        let temp_dir = tempfile::tempdir().unwrap();
581        let config_path = temp_dir.path().join("config.hcl");
582
583        std::fs::write(
584            &config_path,
585            r#"
586                storage_backend = "memory"
587                sessions_dir = "/tmp/sessions"
588            "#,
589        )
590        .unwrap();
591
592        let config = CodeConfig::from_file(&config_path).unwrap();
593        assert_eq!(config.storage_backend, StorageBackend::Memory);
594        assert_eq!(config.sessions_dir, Some(PathBuf::from("/tmp/sessions")));
595    }
596
597    #[test]
598    fn test_config_builder() {
599        let config = CodeConfig::new()
600            .add_skill_dir("/tmp/skills")
601            .add_agent_dir("/tmp/agents");
602
603        assert_eq!(config.skill_dirs.len(), 1);
604        assert_eq!(config.agent_dirs.len(), 1);
605    }
606
607    #[test]
608    fn test_find_provider() {
609        let config = CodeConfig {
610            providers: vec![
611                ProviderConfig {
612                    name: "anthropic".to_string(),
613                    api_key: Some("key1".to_string()),
614                    base_url: None,
615                    models: vec![],
616                },
617                ProviderConfig {
618                    name: "openai".to_string(),
619                    api_key: Some("key2".to_string()),
620                    base_url: None,
621                    models: vec![],
622                },
623            ],
624            ..Default::default()
625        };
626
627        assert!(config.find_provider("anthropic").is_some());
628        assert!(config.find_provider("openai").is_some());
629        assert!(config.find_provider("unknown").is_none());
630    }
631
632    #[test]
633    fn test_default_llm_config() {
634        let config = CodeConfig {
635            default_model: Some("anthropic/claude-sonnet-4".to_string()),
636            providers: vec![ProviderConfig {
637                name: "anthropic".to_string(),
638                api_key: Some("test-api-key".to_string()),
639                base_url: Some("https://api.anthropic.com".to_string()),
640                models: vec![ModelConfig {
641                    id: "claude-sonnet-4".to_string(),
642                    name: "Claude Sonnet 4".to_string(),
643                    family: "claude-sonnet".to_string(),
644                    api_key: None,
645                    base_url: None,
646                    attachment: false,
647                    reasoning: false,
648                    tool_call: true,
649                    temperature: true,
650                    release_date: None,
651                    modalities: ModelModalities::default(),
652                    cost: ModelCost::default(),
653                    limit: ModelLimit::default(),
654                }],
655            }],
656            ..Default::default()
657        };
658
659        let llm_config = config.default_llm_config().unwrap();
660        assert_eq!(llm_config.provider, "anthropic");
661        assert_eq!(llm_config.model, "claude-sonnet-4");
662        assert_eq!(llm_config.api_key.expose(), "test-api-key");
663        assert_eq!(
664            llm_config.base_url,
665            Some("https://api.anthropic.com".to_string())
666        );
667    }
668
669    #[test]
670    fn test_model_api_key_override() {
671        let provider = ProviderConfig {
672            name: "openai".to_string(),
673            api_key: Some("provider-key".to_string()),
674            base_url: Some("https://api.openai.com".to_string()),
675            models: vec![
676                ModelConfig {
677                    id: "gpt-4".to_string(),
678                    name: "GPT-4".to_string(),
679                    family: "gpt".to_string(),
680                    api_key: None, // Uses provider key
681                    base_url: None,
682                    attachment: false,
683                    reasoning: false,
684                    tool_call: true,
685                    temperature: true,
686                    release_date: None,
687                    modalities: ModelModalities::default(),
688                    cost: ModelCost::default(),
689                    limit: ModelLimit::default(),
690                },
691                ModelConfig {
692                    id: "custom-model".to_string(),
693                    name: "Custom Model".to_string(),
694                    family: "custom".to_string(),
695                    api_key: Some("model-specific-key".to_string()), // Override
696                    base_url: Some("https://custom.api.com".to_string()), // Override
697                    attachment: false,
698                    reasoning: false,
699                    tool_call: true,
700                    temperature: true,
701                    release_date: None,
702                    modalities: ModelModalities::default(),
703                    cost: ModelCost::default(),
704                    limit: ModelLimit::default(),
705                },
706            ],
707        };
708
709        // Model without override uses provider key
710        let model1 = provider.find_model("gpt-4").unwrap();
711        assert_eq!(provider.get_api_key(model1), Some("provider-key"));
712        assert_eq!(
713            provider.get_base_url(model1),
714            Some("https://api.openai.com")
715        );
716
717        // Model with override uses its own key
718        let model2 = provider.find_model("custom-model").unwrap();
719        assert_eq!(provider.get_api_key(model2), Some("model-specific-key"));
720        assert_eq!(
721            provider.get_base_url(model2),
722            Some("https://custom.api.com")
723        );
724    }
725
726    #[test]
727    fn test_list_models() {
728        let config = CodeConfig {
729            providers: vec![
730                ProviderConfig {
731                    name: "anthropic".to_string(),
732                    api_key: None,
733                    base_url: None,
734                    models: vec![
735                        ModelConfig {
736                            id: "claude-1".to_string(),
737                            name: "Claude 1".to_string(),
738                            family: "claude".to_string(),
739                            api_key: None,
740                            base_url: None,
741                            attachment: false,
742                            reasoning: false,
743                            tool_call: true,
744                            temperature: true,
745                            release_date: None,
746                            modalities: ModelModalities::default(),
747                            cost: ModelCost::default(),
748                            limit: ModelLimit::default(),
749                        },
750                        ModelConfig {
751                            id: "claude-2".to_string(),
752                            name: "Claude 2".to_string(),
753                            family: "claude".to_string(),
754                            api_key: None,
755                            base_url: None,
756                            attachment: false,
757                            reasoning: false,
758                            tool_call: true,
759                            temperature: true,
760                            release_date: None,
761                            modalities: ModelModalities::default(),
762                            cost: ModelCost::default(),
763                            limit: ModelLimit::default(),
764                        },
765                    ],
766                },
767                ProviderConfig {
768                    name: "openai".to_string(),
769                    api_key: None,
770                    base_url: None,
771                    models: vec![ModelConfig {
772                        id: "gpt-4".to_string(),
773                        name: "GPT-4".to_string(),
774                        family: "gpt".to_string(),
775                        api_key: None,
776                        base_url: None,
777                        attachment: false,
778                        reasoning: false,
779                        tool_call: true,
780                        temperature: true,
781                        release_date: None,
782                        modalities: ModelModalities::default(),
783                        cost: ModelCost::default(),
784                        limit: ModelLimit::default(),
785                    }],
786                },
787            ],
788            ..Default::default()
789        };
790
791        let models = config.list_models();
792        assert_eq!(models.len(), 3);
793    }
794
795    #[test]
796    fn test_config_from_file_not_found() {
797        let result = CodeConfig::from_file(Path::new("/nonexistent/config.json"));
798        assert!(result.is_err());
799    }
800
801    #[test]
802    fn test_config_has_directories() {
803        let empty = CodeConfig::default();
804        assert!(!empty.has_directories());
805
806        let with_skills = CodeConfig::new().add_skill_dir("/tmp/skills");
807        assert!(with_skills.has_directories());
808
809        let with_agents = CodeConfig::new().add_agent_dir("/tmp/agents");
810        assert!(with_agents.has_directories());
811    }
812
813    #[test]
814    fn test_config_has_providers() {
815        let empty = CodeConfig::default();
816        assert!(!empty.has_providers());
817
818        let with_providers = CodeConfig {
819            providers: vec![ProviderConfig {
820                name: "test".to_string(),
821                api_key: None,
822                base_url: None,
823                models: vec![],
824            }],
825            ..Default::default()
826        };
827        assert!(with_providers.has_providers());
828    }
829
830    #[test]
831    fn test_storage_backend_equality() {
832        assert_eq!(StorageBackend::Memory, StorageBackend::Memory);
833        assert_eq!(StorageBackend::File, StorageBackend::File);
834        assert_ne!(StorageBackend::Memory, StorageBackend::File);
835    }
836
837    #[test]
838    fn test_storage_backend_serde_custom() {
839        let custom = StorageBackend::Custom;
840        // Custom variant is now serializable
841        let json = serde_json::to_string(&custom).unwrap();
842        assert_eq!(json, "\"custom\"");
843
844        // And deserializable
845        let parsed: StorageBackend = serde_json::from_str("\"custom\"").unwrap();
846        assert_eq!(parsed, StorageBackend::Custom);
847    }
848
849    #[test]
850    fn test_model_cost_default() {
851        let cost = ModelCost::default();
852        assert_eq!(cost.input, 0.0);
853        assert_eq!(cost.output, 0.0);
854        assert_eq!(cost.cache_read, 0.0);
855        assert_eq!(cost.cache_write, 0.0);
856    }
857
858    #[test]
859    fn test_model_cost_serialization() {
860        let cost = ModelCost {
861            input: 3.0,
862            output: 15.0,
863            cache_read: 0.3,
864            cache_write: 3.75,
865        };
866        let json = serde_json::to_string(&cost).unwrap();
867        assert!(json.contains("\"input\":3"));
868        assert!(json.contains("\"output\":15"));
869    }
870
871    #[test]
872    fn test_model_cost_deserialization_missing_fields() {
873        let json = r#"{"input":3.0}"#;
874        let cost: ModelCost = serde_json::from_str(json).unwrap();
875        assert_eq!(cost.input, 3.0);
876        assert_eq!(cost.output, 0.0);
877        assert_eq!(cost.cache_read, 0.0);
878        assert_eq!(cost.cache_write, 0.0);
879    }
880
881    #[test]
882    fn test_model_limit_default() {
883        let limit = ModelLimit::default();
884        assert_eq!(limit.context, 0);
885        assert_eq!(limit.output, 0);
886    }
887
888    #[test]
889    fn test_model_limit_serialization() {
890        let limit = ModelLimit {
891            context: 200000,
892            output: 8192,
893        };
894        let json = serde_json::to_string(&limit).unwrap();
895        assert!(json.contains("\"context\":200000"));
896        assert!(json.contains("\"output\":8192"));
897    }
898
899    #[test]
900    fn test_model_limit_deserialization_missing_fields() {
901        let json = r#"{"context":100000}"#;
902        let limit: ModelLimit = serde_json::from_str(json).unwrap();
903        assert_eq!(limit.context, 100000);
904        assert_eq!(limit.output, 0);
905    }
906
907    #[test]
908    fn test_model_modalities_default() {
909        let modalities = ModelModalities::default();
910        assert!(modalities.input.is_empty());
911        assert!(modalities.output.is_empty());
912    }
913
914    #[test]
915    fn test_model_modalities_serialization() {
916        let modalities = ModelModalities {
917            input: vec!["text".to_string(), "image".to_string()],
918            output: vec!["text".to_string()],
919        };
920        let json = serde_json::to_string(&modalities).unwrap();
921        assert!(json.contains("\"input\""));
922        assert!(json.contains("\"text\""));
923    }
924
925    #[test]
926    fn test_model_modalities_deserialization_missing_fields() {
927        let json = r#"{"input":["text"]}"#;
928        let modalities: ModelModalities = serde_json::from_str(json).unwrap();
929        assert_eq!(modalities.input.len(), 1);
930        assert!(modalities.output.is_empty());
931    }
932
933    #[test]
934    fn test_model_config_serialization() {
935        let config = ModelConfig {
936            id: "gpt-4o".to_string(),
937            name: "GPT-4o".to_string(),
938            family: "gpt-4".to_string(),
939            api_key: Some("sk-test".to_string()),
940            base_url: None,
941            attachment: true,
942            reasoning: false,
943            tool_call: true,
944            temperature: true,
945            release_date: Some("2024-05-13".to_string()),
946            modalities: ModelModalities::default(),
947            cost: ModelCost::default(),
948            limit: ModelLimit::default(),
949        };
950        let json = serde_json::to_string(&config).unwrap();
951        assert!(json.contains("\"id\":\"gpt-4o\""));
952        assert!(json.contains("\"attachment\":true"));
953    }
954
955    #[test]
956    fn test_model_config_deserialization_with_defaults() {
957        let json = r#"{"id":"test-model"}"#;
958        let config: ModelConfig = serde_json::from_str(json).unwrap();
959        assert_eq!(config.id, "test-model");
960        assert_eq!(config.name, "");
961        assert_eq!(config.family, "");
962        assert!(config.api_key.is_none());
963        assert!(!config.attachment);
964        assert!(config.tool_call);
965        assert!(config.temperature);
966    }
967
968    #[test]
969    fn test_model_config_all_optional_fields() {
970        let json = r#"{
971            "id": "claude-sonnet-4",
972            "name": "Claude Sonnet 4",
973            "family": "claude-sonnet",
974            "apiKey": "sk-test",
975            "baseUrl": "https://api.anthropic.com",
976            "attachment": true,
977            "reasoning": true,
978            "toolCall": false,
979            "temperature": false,
980            "releaseDate": "2025-05-14"
981        }"#;
982        let config: ModelConfig = serde_json::from_str(json).unwrap();
983        assert_eq!(config.id, "claude-sonnet-4");
984        assert_eq!(config.name, "Claude Sonnet 4");
985        assert_eq!(config.api_key, Some("sk-test".to_string()));
986        assert_eq!(
987            config.base_url,
988            Some("https://api.anthropic.com".to_string())
989        );
990        assert!(config.attachment);
991        assert!(config.reasoning);
992        assert!(!config.tool_call);
993        assert!(!config.temperature);
994    }
995
996    #[test]
997    fn test_provider_config_serialization() {
998        let provider = ProviderConfig {
999            name: "anthropic".to_string(),
1000            api_key: Some("sk-test".to_string()),
1001            base_url: Some("https://api.anthropic.com".to_string()),
1002            models: vec![],
1003        };
1004        let json = serde_json::to_string(&provider).unwrap();
1005        assert!(json.contains("\"name\":\"anthropic\""));
1006        assert!(json.contains("\"apiKey\":\"sk-test\""));
1007    }
1008
1009    #[test]
1010    fn test_provider_config_deserialization_missing_optional() {
1011        let json = r#"{"name":"openai"}"#;
1012        let provider: ProviderConfig = serde_json::from_str(json).unwrap();
1013        assert_eq!(provider.name, "openai");
1014        assert!(provider.api_key.is_none());
1015        assert!(provider.base_url.is_none());
1016        assert!(provider.models.is_empty());
1017    }
1018
1019    #[test]
1020    fn test_provider_config_find_model() {
1021        let provider = ProviderConfig {
1022            name: "anthropic".to_string(),
1023            api_key: None,
1024            base_url: None,
1025            models: vec![ModelConfig {
1026                id: "claude-sonnet-4".to_string(),
1027                name: "Claude Sonnet 4".to_string(),
1028                family: "claude-sonnet".to_string(),
1029                api_key: None,
1030                base_url: None,
1031                attachment: false,
1032                reasoning: false,
1033                tool_call: true,
1034                temperature: true,
1035                release_date: None,
1036                modalities: ModelModalities::default(),
1037                cost: ModelCost::default(),
1038                limit: ModelLimit::default(),
1039            }],
1040        };
1041
1042        let found = provider.find_model("claude-sonnet-4");
1043        assert!(found.is_some());
1044        assert_eq!(found.unwrap().id, "claude-sonnet-4");
1045
1046        let not_found = provider.find_model("gpt-4o");
1047        assert!(not_found.is_none());
1048    }
1049
1050    #[test]
1051    fn test_provider_config_get_api_key() {
1052        let provider = ProviderConfig {
1053            name: "anthropic".to_string(),
1054            api_key: Some("provider-key".to_string()),
1055            base_url: None,
1056            models: vec![],
1057        };
1058
1059        let model_with_key = ModelConfig {
1060            id: "test".to_string(),
1061            name: "".to_string(),
1062            family: "".to_string(),
1063            api_key: Some("model-key".to_string()),
1064            base_url: None,
1065            attachment: false,
1066            reasoning: false,
1067            tool_call: true,
1068            temperature: true,
1069            release_date: None,
1070            modalities: ModelModalities::default(),
1071            cost: ModelCost::default(),
1072            limit: ModelLimit::default(),
1073        };
1074
1075        let model_without_key = ModelConfig {
1076            id: "test2".to_string(),
1077            name: "".to_string(),
1078            family: "".to_string(),
1079            api_key: None,
1080            base_url: None,
1081            attachment: false,
1082            reasoning: false,
1083            tool_call: true,
1084            temperature: true,
1085            release_date: None,
1086            modalities: ModelModalities::default(),
1087            cost: ModelCost::default(),
1088            limit: ModelLimit::default(),
1089        };
1090
1091        assert_eq!(provider.get_api_key(&model_with_key), Some("model-key"));
1092        assert_eq!(
1093            provider.get_api_key(&model_without_key),
1094            Some("provider-key")
1095        );
1096    }
1097
1098    #[test]
1099    fn test_code_config_default_provider_config() {
1100        let config = CodeConfig {
1101            default_model: Some("anthropic/claude-sonnet-4".to_string()),
1102            providers: vec![ProviderConfig {
1103                name: "anthropic".to_string(),
1104                api_key: Some("sk-test".to_string()),
1105                base_url: None,
1106                models: vec![],
1107            }],
1108            ..Default::default()
1109        };
1110
1111        let provider = config.default_provider_config();
1112        assert!(provider.is_some());
1113        assert_eq!(provider.unwrap().name, "anthropic");
1114    }
1115
1116    #[test]
1117    fn test_code_config_default_model_config() {
1118        let config = CodeConfig {
1119            default_model: Some("anthropic/claude-sonnet-4".to_string()),
1120            providers: vec![ProviderConfig {
1121                name: "anthropic".to_string(),
1122                api_key: Some("sk-test".to_string()),
1123                base_url: None,
1124                models: vec![ModelConfig {
1125                    id: "claude-sonnet-4".to_string(),
1126                    name: "Claude Sonnet 4".to_string(),
1127                    family: "claude-sonnet".to_string(),
1128                    api_key: None,
1129                    base_url: None,
1130                    attachment: false,
1131                    reasoning: false,
1132                    tool_call: true,
1133                    temperature: true,
1134                    release_date: None,
1135                    modalities: ModelModalities::default(),
1136                    cost: ModelCost::default(),
1137                    limit: ModelLimit::default(),
1138                }],
1139            }],
1140            ..Default::default()
1141        };
1142
1143        let result = config.default_model_config();
1144        assert!(result.is_some());
1145        let (provider, model) = result.unwrap();
1146        assert_eq!(provider.name, "anthropic");
1147        assert_eq!(model.id, "claude-sonnet-4");
1148    }
1149
1150    #[test]
1151    fn test_code_config_default_llm_config() {
1152        let config = CodeConfig {
1153            default_model: Some("anthropic/claude-sonnet-4".to_string()),
1154            providers: vec![ProviderConfig {
1155                name: "anthropic".to_string(),
1156                api_key: Some("sk-test".to_string()),
1157                base_url: Some("https://api.anthropic.com".to_string()),
1158                models: vec![ModelConfig {
1159                    id: "claude-sonnet-4".to_string(),
1160                    name: "Claude Sonnet 4".to_string(),
1161                    family: "claude-sonnet".to_string(),
1162                    api_key: None,
1163                    base_url: None,
1164                    attachment: false,
1165                    reasoning: false,
1166                    tool_call: true,
1167                    temperature: true,
1168                    release_date: None,
1169                    modalities: ModelModalities::default(),
1170                    cost: ModelCost::default(),
1171                    limit: ModelLimit::default(),
1172                }],
1173            }],
1174            ..Default::default()
1175        };
1176
1177        let llm_config = config.default_llm_config();
1178        assert!(llm_config.is_some());
1179    }
1180
1181    #[test]
1182    fn test_code_config_list_models() {
1183        let config = CodeConfig {
1184            providers: vec![
1185                ProviderConfig {
1186                    name: "anthropic".to_string(),
1187                    api_key: None,
1188                    base_url: None,
1189                    models: vec![ModelConfig {
1190                        id: "claude-sonnet-4".to_string(),
1191                        name: "".to_string(),
1192                        family: "".to_string(),
1193                        api_key: None,
1194                        base_url: None,
1195                        attachment: false,
1196                        reasoning: false,
1197                        tool_call: true,
1198                        temperature: true,
1199                        release_date: None,
1200                        modalities: ModelModalities::default(),
1201                        cost: ModelCost::default(),
1202                        limit: ModelLimit::default(),
1203                    }],
1204                },
1205                ProviderConfig {
1206                    name: "openai".to_string(),
1207                    api_key: None,
1208                    base_url: None,
1209                    models: vec![ModelConfig {
1210                        id: "gpt-4o".to_string(),
1211                        name: "".to_string(),
1212                        family: "".to_string(),
1213                        api_key: None,
1214                        base_url: None,
1215                        attachment: false,
1216                        reasoning: false,
1217                        tool_call: true,
1218                        temperature: true,
1219                        release_date: None,
1220                        modalities: ModelModalities::default(),
1221                        cost: ModelCost::default(),
1222                        limit: ModelLimit::default(),
1223                    }],
1224                },
1225            ],
1226            ..Default::default()
1227        };
1228
1229        let models = config.list_models();
1230        assert_eq!(models.len(), 2);
1231    }
1232
1233    #[test]
1234    fn test_llm_config_specific_provider_model() {
1235        let model: ModelConfig = serde_json::from_value(serde_json::json!({
1236            "id": "claude-3",
1237            "name": "Claude 3"
1238        }))
1239        .unwrap();
1240
1241        let config = CodeConfig {
1242            providers: vec![ProviderConfig {
1243                name: "anthropic".to_string(),
1244                api_key: Some("sk-test".to_string()),
1245                base_url: None,
1246                models: vec![model],
1247            }],
1248            ..Default::default()
1249        };
1250
1251        let llm = config.llm_config("anthropic", "claude-3");
1252        assert!(llm.is_some());
1253        let llm = llm.unwrap();
1254        assert_eq!(llm.provider, "anthropic");
1255        assert_eq!(llm.model, "claude-3");
1256    }
1257
1258    #[test]
1259    fn test_llm_config_missing_provider() {
1260        let config = CodeConfig::default();
1261        assert!(config.llm_config("nonexistent", "model").is_none());
1262    }
1263
1264    #[test]
1265    fn test_llm_config_missing_model() {
1266        let config = CodeConfig {
1267            providers: vec![ProviderConfig {
1268                name: "anthropic".to_string(),
1269                api_key: Some("sk-test".to_string()),
1270                base_url: None,
1271                models: vec![],
1272            }],
1273            ..Default::default()
1274        };
1275        assert!(config.llm_config("anthropic", "nonexistent").is_none());
1276    }
1277
1278    #[test]
1279    fn test_from_hcl_string() {
1280        let hcl = r#"
1281            default_model = "anthropic/claude-sonnet-4"
1282
1283            providers {
1284                name    = "anthropic"
1285                api_key = "test-key"
1286
1287                models {
1288                    id   = "claude-sonnet-4"
1289                    name = "Claude Sonnet 4"
1290                }
1291            }
1292        "#;
1293
1294        let config = CodeConfig::from_hcl(hcl).unwrap();
1295        assert_eq!(
1296            config.default_model,
1297            Some("anthropic/claude-sonnet-4".to_string())
1298        );
1299        assert_eq!(config.providers.len(), 1);
1300        assert_eq!(config.providers[0].name, "anthropic");
1301        assert_eq!(config.providers[0].models.len(), 1);
1302        assert_eq!(config.providers[0].models[0].id, "claude-sonnet-4");
1303    }
1304
1305    #[test]
1306    fn test_from_hcl_multi_provider() {
1307        let hcl = r#"
1308            default_model = "anthropic/claude-sonnet-4"
1309
1310            providers {
1311                name    = "anthropic"
1312                api_key = "sk-ant-test"
1313
1314                models {
1315                    id   = "claude-sonnet-4"
1316                    name = "Claude Sonnet 4"
1317                }
1318
1319                models {
1320                    id        = "claude-opus-4"
1321                    name      = "Claude Opus 4"
1322                    reasoning = true
1323                }
1324            }
1325
1326            providers {
1327                name    = "openai"
1328                api_key = "sk-test"
1329
1330                models {
1331                    id   = "gpt-4o"
1332                    name = "GPT-4o"
1333                }
1334            }
1335        "#;
1336
1337        let config = CodeConfig::from_hcl(hcl).unwrap();
1338        assert_eq!(config.providers.len(), 2);
1339        assert_eq!(config.providers[0].models.len(), 2);
1340        assert_eq!(config.providers[1].models.len(), 1);
1341        assert_eq!(config.providers[1].name, "openai");
1342    }
1343
1344    #[test]
1345    fn test_snake_to_camel() {
1346        assert_eq!(snake_to_camel("default_model"), "defaultModel");
1347        assert_eq!(snake_to_camel("api_key"), "apiKey");
1348        assert_eq!(snake_to_camel("base_url"), "baseUrl");
1349        assert_eq!(snake_to_camel("name"), "name");
1350        assert_eq!(snake_to_camel("tool_call"), "toolCall");
1351    }
1352
1353    #[test]
1354    fn test_from_file_auto_detect_hcl() {
1355        let temp_dir = tempfile::tempdir().unwrap();
1356        let config_path = temp_dir.path().join("config.hcl");
1357
1358        std::fs::write(
1359            &config_path,
1360            r#"
1361            default_model = "anthropic/claude-sonnet-4"
1362
1363            providers {
1364                name    = "anthropic"
1365                api_key = "test-key"
1366
1367                models {
1368                    id = "claude-sonnet-4"
1369                }
1370            }
1371        "#,
1372        )
1373        .unwrap();
1374
1375        let config = CodeConfig::from_file(&config_path).unwrap();
1376        assert_eq!(
1377            config.default_model,
1378            Some("anthropic/claude-sonnet-4".to_string())
1379        );
1380    }
1381
1382    #[test]
1383    fn test_from_hcl_with_queue_config() {
1384        let hcl = r#"
1385            default_model = "anthropic/claude-sonnet-4"
1386
1387            providers {
1388                name    = "anthropic"
1389                api_key = "test-key"
1390            }
1391
1392            queue {
1393                query_max_concurrency = 20
1394                execute_max_concurrency = 5
1395                enable_metrics = true
1396                enable_dlq = true
1397            }
1398        "#;
1399
1400        let config = CodeConfig::from_hcl(hcl).unwrap();
1401        assert!(config.queue.is_some());
1402        let queue = config.queue.unwrap();
1403        assert_eq!(queue.query_max_concurrency, 20);
1404        assert_eq!(queue.execute_max_concurrency, 5);
1405        assert!(queue.enable_metrics);
1406        assert!(queue.enable_dlq);
1407    }
1408
1409    #[test]
1410    fn test_from_hcl_with_search_config() {
1411        let hcl = r#"
1412            default_model = "anthropic/claude-sonnet-4"
1413
1414            providers {
1415                name    = "anthropic"
1416                api_key = "test-key"
1417            }
1418
1419            search {
1420                timeout = 30
1421
1422                health {
1423                    max_failures = 5
1424                    suspend_seconds = 120
1425                }
1426
1427                engine {
1428                    google {
1429                        enabled = true
1430                        weight = 1.5
1431                    }
1432                    bing {
1433                        enabled = true
1434                        weight = 1.0
1435                        timeout = 15
1436                    }
1437                }
1438            }
1439        "#;
1440
1441        let config = CodeConfig::from_hcl(hcl).unwrap();
1442        assert!(config.search.is_some());
1443        let search = config.search.unwrap();
1444        assert_eq!(search.timeout, 30);
1445        assert!(search.health.is_some());
1446        let health = search.health.unwrap();
1447        assert_eq!(health.max_failures, 5);
1448        assert_eq!(health.suspend_seconds, 120);
1449        assert_eq!(search.engines.len(), 2);
1450        assert!(search.engines.contains_key("google"));
1451        assert!(search.engines.contains_key("bing"));
1452        let google = &search.engines["google"];
1453        assert!(google.enabled);
1454        assert_eq!(google.weight, 1.5);
1455        let bing = &search.engines["bing"];
1456        assert_eq!(bing.timeout, Some(15));
1457    }
1458
1459    #[test]
1460    fn test_from_hcl_with_queue_and_search() {
1461        let hcl = r#"
1462            default_model = "anthropic/claude-sonnet-4"
1463
1464            providers {
1465                name    = "anthropic"
1466                api_key = "test-key"
1467            }
1468
1469            queue {
1470                query_max_concurrency = 10
1471                enable_metrics = true
1472            }
1473
1474            search {
1475                timeout = 20
1476                engine {
1477                    duckduckgo {
1478                        enabled = true
1479                    }
1480                }
1481            }
1482        "#;
1483
1484        let config = CodeConfig::from_hcl(hcl).unwrap();
1485        assert!(config.queue.is_some());
1486        assert!(config.search.is_some());
1487        assert_eq!(config.queue.unwrap().query_max_concurrency, 10);
1488        assert_eq!(config.search.unwrap().timeout, 20);
1489    }
1490
1491    #[test]
1492    fn test_from_hcl_with_advanced_queue_config() {
1493        let hcl = r#"
1494            default_model = "anthropic/claude-sonnet-4"
1495
1496            providers {
1497                name    = "anthropic"
1498                api_key = "test-key"
1499            }
1500
1501            queue {
1502                query_max_concurrency = 20
1503                enable_metrics = true
1504
1505                retry_policy {
1506                    strategy = "exponential"
1507                    max_retries = 5
1508                    initial_delay_ms = 200
1509                }
1510
1511                rate_limit {
1512                    limit_type = "per_second"
1513                    max_operations = 100
1514                }
1515
1516                priority_boost {
1517                    strategy = "standard"
1518                    deadline_ms = 300000
1519                }
1520
1521                pressure_threshold = 50
1522            }
1523        "#;
1524
1525        let config = CodeConfig::from_hcl(hcl).unwrap();
1526        assert!(config.queue.is_some());
1527        let queue = config.queue.unwrap();
1528
1529        assert_eq!(queue.query_max_concurrency, 20);
1530        assert!(queue.enable_metrics);
1531
1532        // Test retry policy
1533        assert!(queue.retry_policy.is_some());
1534        let retry = queue.retry_policy.unwrap();
1535        assert_eq!(retry.strategy, "exponential");
1536        assert_eq!(retry.max_retries, 5);
1537        assert_eq!(retry.initial_delay_ms, 200);
1538
1539        // Test rate limit
1540        assert!(queue.rate_limit.is_some());
1541        let rate = queue.rate_limit.unwrap();
1542        assert_eq!(rate.limit_type, "per_second");
1543        assert_eq!(rate.max_operations, Some(100));
1544
1545        // Test priority boost
1546        assert!(queue.priority_boost.is_some());
1547        let boost = queue.priority_boost.unwrap();
1548        assert_eq!(boost.strategy, "standard");
1549        assert_eq!(boost.deadline_ms, Some(300000));
1550
1551        // Test pressure threshold
1552        assert_eq!(queue.pressure_threshold, Some(50));
1553    }
1554}