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