Skip to main content

minion_engine/config/
manager.rs

1use std::collections::HashMap;
2
3use crate::config::merge::yaml_to_json;
4use crate::config::StepConfig;
5use crate::workflow::schema::{StepType, WorkflowConfig};
6
7/// Manages 4-layer config resolution
8pub struct ConfigManager {
9    config: WorkflowConfig,
10}
11
12impl ConfigManager {
13    pub fn new(config: WorkflowConfig) -> Self {
14        Self { config }
15    }
16
17    /// Resolve config for a step by merging 4 layers:
18    /// global < type < pattern < step inline
19    pub fn resolve(
20        &self,
21        step_name: &str,
22        step_type: &StepType,
23        step_inline: &HashMap<String, serde_yaml::Value>,
24    ) -> StepConfig {
25        let mut merged = HashMap::new();
26
27        // Layer 1: global
28        for (k, v) in &self.config.global {
29            merged.insert(k.clone(), yaml_to_json(v));
30        }
31
32        // Layer 2: by step type
33        let empty = HashMap::new();
34        let type_config = match step_type {
35            StepType::Agent => &self.config.agent,
36            StepType::Cmd => &self.config.cmd,
37            StepType::Chat => &self.config.chat,
38            StepType::Gate => &self.config.gate,
39            _ => &empty,
40        };
41        for (k, v) in type_config {
42            merged.insert(k.clone(), yaml_to_json(v));
43        }
44
45        // Layer 3: by pattern match on step name
46        for (pattern, values) in &self.config.patterns {
47            if let Ok(re) = regex::Regex::new(pattern) {
48                if re.is_match(step_name) {
49                    for (k, v) in values {
50                        merged.insert(k.clone(), yaml_to_json(v));
51                    }
52                }
53            }
54        }
55
56        // Layer 4: step inline (highest priority)
57        for (k, v) in step_inline {
58            merged.insert(k.clone(), yaml_to_json(v));
59        }
60
61        StepConfig { values: merged }
62    }
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68    use std::time::Duration;
69
70    fn yaml_str(s: &str) -> serde_yaml::Value {
71        serde_yaml::Value::String(s.to_string())
72    }
73
74    #[test]
75    fn test_global_timeout_overridden_by_step_inline() {
76        let mut global = HashMap::new();
77        global.insert("timeout".to_string(), yaml_str("300s"));
78
79        let config = WorkflowConfig {
80            global,
81            ..Default::default()
82        };
83        let manager = ConfigManager::new(config);
84
85        let mut inline = HashMap::new();
86        inline.insert("timeout".to_string(), yaml_str("10s"));
87
88        let resolved = manager.resolve("any_step", &StepType::Agent, &inline);
89        assert_eq!(
90            resolved.get_duration("timeout"),
91            Some(Duration::from_secs(10)),
92            "step inline timeout=10s should override global timeout=300s"
93        );
94    }
95
96    #[test]
97    fn test_pattern_match_sets_model() {
98        let mut pattern_values = HashMap::new();
99        pattern_values.insert("model".to_string(), yaml_str("claude-3-haiku"));
100
101        let mut patterns = HashMap::new();
102        patterns.insert("lint.*".to_string(), pattern_values);
103
104        let config = WorkflowConfig {
105            patterns,
106            ..Default::default()
107        };
108        let manager = ConfigManager::new(config);
109
110        let resolved = manager.resolve("lint_check", &StepType::Agent, &HashMap::new());
111        assert_eq!(
112            resolved.get_str("model"),
113            Some("claude-3-haiku"),
114            "pattern 'lint.*' should match step name 'lint_check'"
115        );
116    }
117
118    #[test]
119    fn test_pattern_no_match() {
120        let mut pattern_values = HashMap::new();
121        pattern_values.insert("model".to_string(), yaml_str("claude-3-haiku"));
122
123        let mut patterns = HashMap::new();
124        patterns.insert("lint.*".to_string(), pattern_values);
125
126        let config = WorkflowConfig {
127            patterns,
128            ..Default::default()
129        };
130        let manager = ConfigManager::new(config);
131
132        let resolved = manager.resolve("test_run", &StepType::Agent, &HashMap::new());
133        assert_eq!(
134            resolved.get_str("model"),
135            None,
136            "pattern 'lint.*' should NOT match step name 'test_run'"
137        );
138    }
139}