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