1use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
26#[serde(default)]
27pub struct CodaConfig {
28 pub version: u32,
30
31 pub agent: AgentConfig,
33
34 pub checks: Vec<String>,
36
37 pub prompts: PromptsConfig,
39
40 pub git: GitConfig,
42
43 pub review: ReviewConfig,
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
49#[serde(default)]
50pub struct AgentConfig {
51 pub model: String,
53
54 pub max_budget_usd: f64,
56
57 pub max_retries: u32,
59
60 pub max_turns: u32,
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize)]
66#[serde(default)]
67pub struct PromptsConfig {
68 pub extra_dirs: Vec<String>,
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
76#[serde(default)]
77pub struct GitConfig {
78 pub auto_commit: bool,
80
81 pub branch_prefix: String,
84
85 pub base_branch: String,
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize)]
93#[serde(default)]
94pub struct ReviewConfig {
95 pub enabled: bool,
97
98 pub max_review_rounds: u32,
100}
101
102impl Default for CodaConfig {
103 fn default() -> Self {
104 Self {
105 version: 1,
106 agent: AgentConfig::default(),
107 checks: vec![
108 "cargo build".to_string(),
109 "cargo +nightly fmt -- --check".to_string(),
110 "cargo clippy -- -D warnings".to_string(),
111 ],
112 prompts: PromptsConfig::default(),
113 git: GitConfig::default(),
114 review: ReviewConfig::default(),
115 }
116 }
117}
118
119impl Default for AgentConfig {
120 fn default() -> Self {
121 Self {
122 model: "claude-opus-4-6".to_string(),
123 max_budget_usd: 20.0,
124 max_retries: 3,
125 max_turns: 100,
126 }
127 }
128}
129
130impl Default for PromptsConfig {
131 fn default() -> Self {
132 Self {
133 extra_dirs: vec![".coda/prompts".to_string()],
134 }
135 }
136}
137
138impl Default for GitConfig {
139 fn default() -> Self {
140 Self {
141 auto_commit: true,
142 branch_prefix: "feature".to_string(),
143 base_branch: "auto".to_string(),
144 }
145 }
146}
147
148impl Default for ReviewConfig {
149 fn default() -> Self {
150 Self {
151 enabled: true,
152 max_review_rounds: 5,
153 }
154 }
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160
161 #[test]
162 fn test_should_create_default_config() {
163 let config = CodaConfig::default();
164 assert_eq!(config.version, 1);
165 assert_eq!(config.agent.max_budget_usd, 20.0);
166 assert_eq!(config.agent.max_retries, 3);
167 assert_eq!(config.checks.len(), 3);
168 assert!(config.git.auto_commit);
169 assert!(config.review.enabled);
170 }
171
172 #[test]
173 fn test_should_round_trip_yaml_serialization() {
174 let config = CodaConfig::default();
175 let yaml = serde_yaml::to_string(&config).unwrap();
176 let deserialized: CodaConfig = serde_yaml::from_str(&yaml).unwrap();
177 assert_eq!(deserialized.version, config.version);
178 assert_eq!(deserialized.agent.model, config.agent.model);
179 assert_eq!(deserialized.git.branch_prefix, config.git.branch_prefix);
180 }
181
182 #[test]
183 fn test_should_deserialize_custom_config() {
184 let yaml = r#"
185version: 2
186agent:
187 model: "claude-opus-4-20250514"
188 max_budget_usd: 50.0
189 max_retries: 5
190checks:
191 - "npm run build"
192 - "npm run lint"
193prompts:
194 extra_dirs:
195 - ".coda/custom-prompts"
196 - ".prompts"
197git:
198 auto_commit: false
199 branch_prefix: "dev"
200review:
201 enabled: false
202 max_review_rounds: 10
203"#;
204
205 let config: CodaConfig = serde_yaml::from_str(yaml).unwrap();
206 assert_eq!(config.version, 2);
207 assert_eq!(config.agent.model, "claude-opus-4-20250514");
208 assert!((config.agent.max_budget_usd - 50.0).abs() < f64::EPSILON);
209 assert_eq!(config.agent.max_retries, 5);
210 assert_eq!(config.checks.len(), 2);
211 assert_eq!(config.checks[0], "npm run build");
212 assert_eq!(config.prompts.extra_dirs.len(), 2);
213 assert!(!config.git.auto_commit);
214 assert_eq!(config.git.branch_prefix, "dev");
215 assert!(!config.review.enabled);
216 assert_eq!(config.review.max_review_rounds, 10);
217 }
218
219 #[test]
220 fn test_should_deserialize_partial_config_with_defaults() {
221 let yaml = r#"
223version: 1
224agent:
225 model: "claude-sonnet-4-20250514"
226 permission_mode: "auto"
227 max_retries: 3
228prompts:
229 extra_dirs: []
230git:
231 auto_commit: true
232 branch_prefix: "feature"
233 commit_prefix: "feat"
234review:
235 enabled: true
236 checks:
237 - "cargo build"
238 max_review_rounds: 5
239"#;
240
241 let config: CodaConfig = serde_yaml::from_str(yaml).unwrap();
242 assert_eq!(config.version, 1);
243 assert!((config.agent.max_budget_usd - 20.0).abs() < f64::EPSILON);
245 assert_eq!(config.agent.max_retries, 3);
246 assert!(!config.checks.is_empty());
248 assert!(config.review.enabled);
249 }
250
251 #[test]
252 fn test_should_deserialize_minimal_config() {
253 let yaml = "version: 1\n";
254 let config: CodaConfig = serde_yaml::from_str(yaml).unwrap();
255 assert_eq!(config.version, 1);
256 assert_eq!(config.agent.model, "claude-opus-4-6");
257 assert!((config.agent.max_budget_usd - 20.0).abs() < f64::EPSILON);
258 assert!(config.git.auto_commit);
259 }
260}