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