1use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct DelegationConfig {
18 #[serde(default)]
20 pub claude_enabled: bool,
21
22 #[serde(default = "default_delegation_model", alias = "claudeModel")]
24 pub claude_model: String,
25
26 #[serde(default = "default_max_turns", alias = "maxTurns")]
28 pub max_turns: u32,
29
30 #[serde(default = "default_max_tokens", alias = "maxTokens")]
32 pub max_tokens: u32,
33
34 #[serde(default, alias = "claudeFlowEnabled")]
36 pub claude_flow_enabled: bool,
37
38 #[serde(default)]
40 pub rules: Vec<DelegationRule>,
41
42 #[serde(default, alias = "excludedTools")]
44 pub excluded_tools: Vec<String>,
45}
46
47fn default_delegation_model() -> String {
48 "claude-sonnet-4-20250514".into()
49}
50
51fn default_max_turns() -> u32 {
52 10
53}
54
55fn default_max_tokens() -> u32 {
56 4096
57}
58
59impl Default for DelegationConfig {
60 fn default() -> Self {
61 Self {
62 claude_enabled: true, claude_model: default_delegation_model(),
64 max_turns: default_max_turns(),
65 max_tokens: default_max_tokens(),
66 claude_flow_enabled: false, rules: Vec::new(),
68 excluded_tools: Vec::new(),
69 }
70 }
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct DelegationRule {
78 pub pattern: String,
80
81 pub target: DelegationTarget,
83}
84
85#[non_exhaustive]
93#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
94#[serde(rename_all = "snake_case")]
95pub enum DelegationTarget {
96 #[serde(alias = "Local")]
98 Local,
99 #[serde(alias = "Claude")]
101 Claude,
102 #[serde(alias = "Flow")]
104 Flow,
105 #[serde(alias = "Auto")]
107 #[default]
108 Auto,
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114
115 #[test]
116 fn delegation_config_defaults() {
117 let cfg = DelegationConfig::default();
118 assert!(cfg.claude_enabled); assert_eq!(cfg.claude_model, "claude-sonnet-4-20250514");
120 assert_eq!(cfg.max_turns, 10);
121 assert_eq!(cfg.max_tokens, 4096);
122 assert!(!cfg.claude_flow_enabled);
123 assert!(cfg.rules.is_empty());
124 assert!(cfg.excluded_tools.is_empty());
125 }
126
127 #[test]
128 fn delegation_config_serde_roundtrip() {
129 let cfg = DelegationConfig {
130 claude_enabled: true,
131 claude_model: "claude-opus-4-20250514".into(),
132 max_turns: 5,
133 max_tokens: 2048,
134 claude_flow_enabled: true,
135 rules: vec![
136 DelegationRule {
137 pattern: r"(?i)deploy".into(),
138 target: DelegationTarget::Flow,
139 },
140 DelegationRule {
141 pattern: r"(?i)simple.*query".into(),
142 target: DelegationTarget::Local,
143 },
144 ],
145 excluded_tools: vec!["shell_exec".into()],
146 };
147
148 let json = serde_json::to_string(&cfg).unwrap();
149 let restored: DelegationConfig = serde_json::from_str(&json).unwrap();
150
151 assert_eq!(restored.claude_enabled, cfg.claude_enabled);
152 assert_eq!(restored.claude_model, cfg.claude_model);
153 assert_eq!(restored.max_turns, cfg.max_turns);
154 assert_eq!(restored.max_tokens, cfg.max_tokens);
155 assert_eq!(restored.claude_flow_enabled, cfg.claude_flow_enabled);
156 assert_eq!(restored.rules.len(), 2);
157 assert_eq!(restored.rules[0].pattern, r"(?i)deploy");
158 assert_eq!(restored.rules[0].target, DelegationTarget::Flow);
159 assert_eq!(restored.rules[1].target, DelegationTarget::Local);
160 assert_eq!(restored.excluded_tools, vec!["shell_exec"]);
161 }
162
163 #[test]
164 fn delegation_config_from_empty_json() {
165 let cfg: DelegationConfig = serde_json::from_str("{}").unwrap();
166 assert!(!cfg.claude_enabled);
167 assert_eq!(cfg.claude_model, "claude-sonnet-4-20250514");
168 assert_eq!(cfg.max_turns, 10);
169 assert_eq!(cfg.max_tokens, 4096);
170 assert!(!cfg.claude_flow_enabled);
171 assert!(cfg.rules.is_empty());
172 assert!(cfg.excluded_tools.is_empty());
173 }
174
175 #[test]
176 fn delegation_config_camel_case_aliases() {
177 let json = r#"{
178 "claudeModel": "test-model",
179 "maxTurns": 3,
180 "maxTokens": 1024,
181 "claudeFlowEnabled": true,
182 "excludedTools": ["dangerous_tool"]
183 }"#;
184 let cfg: DelegationConfig = serde_json::from_str(json).unwrap();
185 assert_eq!(cfg.claude_model, "test-model");
186 assert_eq!(cfg.max_turns, 3);
187 assert_eq!(cfg.max_tokens, 1024);
188 assert!(cfg.claude_flow_enabled);
189 assert_eq!(cfg.excluded_tools, vec!["dangerous_tool"]);
190 }
191
192 #[test]
193 fn delegation_target_serializes_snake_case() {
194 let targets = [
195 (DelegationTarget::Local, "\"local\""),
196 (DelegationTarget::Claude, "\"claude\""),
197 (DelegationTarget::Flow, "\"flow\""),
198 (DelegationTarget::Auto, "\"auto\""),
199 ];
200 for (target, expected_json) in &targets {
201 let json = serde_json::to_string(target).unwrap();
202 assert_eq!(&json, expected_json);
203 let restored: DelegationTarget = serde_json::from_str(&json).unwrap();
204 assert_eq!(restored, *target);
205 }
206 }
207
208 #[test]
209 fn delegation_target_deserializes_legacy_pascal_case() {
210 let cases = [
212 ("\"Local\"", DelegationTarget::Local),
213 ("\"Claude\"", DelegationTarget::Claude),
214 ("\"Flow\"", DelegationTarget::Flow),
215 ("\"Auto\"", DelegationTarget::Auto),
216 ];
217 for (json, expected) in &cases {
218 let restored: DelegationTarget = serde_json::from_str(json).unwrap();
219 assert_eq!(restored, *expected);
220 }
221 }
222
223 #[test]
224 fn delegation_target_default_is_auto() {
225 assert_eq!(DelegationTarget::default(), DelegationTarget::Auto);
226 }
227}