1use std::collections::HashMap;
9
10use claude_wrapper::types::{Effort, PermissionMode};
11
12use crate::types::{PoolConfig, SlotConfig};
13
14#[derive(Debug, Clone)]
18pub struct ResolvedConfig {
19 pub model: Option<String>,
20 pub permission_mode: PermissionMode,
21 pub max_turns: Option<u32>,
22 pub system_prompt: Option<String>,
23 pub allowed_tools: Vec<String>,
24 pub effort: Option<Effort>,
25 pub mcp_servers: HashMap<String, serde_json::Value>,
28 pub strict_mcp_config: bool,
30}
31
32impl ResolvedConfig {
33 pub fn resolve(global: &PoolConfig, slot: &SlotConfig, task: Option<&SlotConfig>) -> Self {
39 let model = task
40 .and_then(|t| t.model.clone())
41 .or_else(|| slot.model.clone())
42 .or_else(|| global.model.clone());
43
44 let permission_mode = task
45 .and_then(|t| t.permission_mode)
46 .or(slot.permission_mode)
47 .or(global.permission_mode)
48 .unwrap_or(PermissionMode::Plan);
49
50 let max_turns = task
51 .and_then(|t| t.max_turns)
52 .or(slot.max_turns)
53 .or(global.max_turns);
54
55 let system_prompt = task
56 .and_then(|t| t.system_prompt.clone())
57 .or_else(|| slot.system_prompt.clone())
58 .or_else(|| global.system_prompt.clone());
59
60 let effort = task
61 .and_then(|t| t.effort)
62 .or(slot.effort)
63 .or(global.effort);
64
65 let mut allowed_tools = global.allowed_tools.clone();
67 if let Some(ref wt) = slot.allowed_tools {
68 allowed_tools.extend(wt.iter().cloned());
69 }
70 if let Some(task_cfg) = task
71 && let Some(ref tt) = task_cfg.allowed_tools
72 {
73 allowed_tools.extend(tt.iter().cloned());
74 }
75
76 let mut mcp_servers = global.mcp_servers.clone();
79 if let Some(ref slot_servers) = slot.mcp_servers {
80 mcp_servers.extend(slot_servers.iter().map(|(k, v)| (k.clone(), v.clone())));
81 }
82 if let Some(task_cfg) = task
83 && let Some(ref task_servers) = task_cfg.mcp_servers
84 {
85 mcp_servers.extend(task_servers.iter().map(|(k, v)| (k.clone(), v.clone())));
86 }
87
88 let strict_mcp_config = global.strict_mcp_config;
89
90 Self {
91 model,
92 permission_mode,
93 max_turns,
94 system_prompt,
95 allowed_tools,
96 effort,
97 mcp_servers,
98 strict_mcp_config,
99 }
100 }
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106
107 #[test]
108 fn global_defaults() {
109 let global = PoolConfig::default();
110 let slot = SlotConfig::default();
111 let resolved = ResolvedConfig::resolve(&global, &slot, None);
112
113 assert_eq!(resolved.permission_mode, PermissionMode::Plan);
115 assert!(resolved.model.is_none());
116 assert!(resolved.allowed_tools.is_empty());
117 assert!(resolved.mcp_servers.is_empty());
118 assert!(resolved.strict_mcp_config);
119 }
120
121 #[test]
122 fn slot_overrides_global() {
123 let global = PoolConfig {
124 model: Some("haiku".into()),
125 effort: Some(Effort::Low),
126 ..Default::default()
127 };
128 let slot = SlotConfig {
129 model: Some("opus".into()),
130 ..Default::default()
131 };
132 let resolved = ResolvedConfig::resolve(&global, &slot, None);
133
134 assert_eq!(resolved.model.as_deref(), Some("opus"));
135 assert_eq!(resolved.effort, Some(Effort::Low)); }
137
138 #[test]
139 fn task_overrides_slot() {
140 let global = PoolConfig {
141 model: Some("haiku".into()),
142 ..Default::default()
143 };
144 let slot = SlotConfig {
145 model: Some("sonnet".into()),
146 effort: Some(Effort::Medium),
147 ..Default::default()
148 };
149 let task = SlotConfig {
150 effort: Some(Effort::Max),
151 ..Default::default()
152 };
153 let resolved = ResolvedConfig::resolve(&global, &slot, Some(&task));
154
155 assert_eq!(resolved.model.as_deref(), Some("sonnet")); assert_eq!(resolved.effort, Some(Effort::Max)); }
158
159 #[test]
160 fn allowed_tools_merge() {
161 let global = PoolConfig {
162 allowed_tools: vec!["Bash".into(), "Read".into()],
163 ..Default::default()
164 };
165 let slot = SlotConfig {
166 allowed_tools: Some(vec!["Write".into()]),
167 ..Default::default()
168 };
169 let task = SlotConfig {
170 allowed_tools: Some(vec!["Edit".into()]),
171 ..Default::default()
172 };
173 let resolved = ResolvedConfig::resolve(&global, &slot, Some(&task));
174
175 assert_eq!(
176 resolved.allowed_tools,
177 vec!["Bash", "Read", "Write", "Edit"]
178 );
179 }
180
181 #[test]
182 fn mcp_servers_merge() {
183 let global = PoolConfig {
184 mcp_servers: [(
185 "hub".to_string(),
186 serde_json::json!({"type": "http", "url": "http://global/"}),
187 )]
188 .into_iter()
189 .collect(),
190 ..Default::default()
191 };
192 let slot = SlotConfig {
193 mcp_servers: Some(
194 [(
195 "tool".to_string(),
196 serde_json::json!({"type": "stdio", "command": "npx"}),
197 )]
198 .into_iter()
199 .collect(),
200 ),
201 ..Default::default()
202 };
203 let resolved = ResolvedConfig::resolve(&global, &slot, None);
204
205 assert_eq!(resolved.mcp_servers.len(), 2);
206 assert!(resolved.mcp_servers.contains_key("hub"));
207 assert!(resolved.mcp_servers.contains_key("tool"));
208 }
209
210 #[test]
211 fn task_mcp_servers_override_slot() {
212 let global = PoolConfig::default();
213 let slot = SlotConfig {
214 mcp_servers: Some(
215 [(
216 "hub".to_string(),
217 serde_json::json!({"type": "http", "url": "http://slot/"}),
218 )]
219 .into_iter()
220 .collect(),
221 ),
222 ..Default::default()
223 };
224 let task = SlotConfig {
225 mcp_servers: Some(
226 [(
227 "hub".to_string(),
228 serde_json::json!({"type": "http", "url": "http://task/"}),
229 )]
230 .into_iter()
231 .collect(),
232 ),
233 ..Default::default()
234 };
235 let resolved = ResolvedConfig::resolve(&global, &slot, Some(&task));
236
237 assert_eq!(resolved.mcp_servers["hub"]["url"], "http://task/");
238 }
239
240 #[test]
241 fn strict_mcp_config_defaults_true() {
242 let global = PoolConfig::default();
243 let slot = SlotConfig::default();
244 let resolved = ResolvedConfig::resolve(&global, &slot, None);
245 assert!(resolved.strict_mcp_config);
246 }
247
248 #[test]
249 fn strict_mcp_config_can_be_disabled() {
250 let global = PoolConfig {
251 strict_mcp_config: false,
252 ..Default::default()
253 };
254 let slot = SlotConfig::default();
255 let resolved = ResolvedConfig::resolve(&global, &slot, None);
256 assert!(!resolved.strict_mcp_config);
257 }
258}