use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct CodaConfig {
pub version: u32,
pub agent: AgentConfig,
pub checks: Vec<String>,
pub prompts: PromptsConfig,
pub git: GitConfig,
pub review: ReviewConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct AgentConfig {
pub model: String,
pub max_budget_usd: f64,
pub max_retries: u32,
pub max_turns: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct PromptsConfig {
pub extra_dirs: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct GitConfig {
pub auto_commit: bool,
pub branch_prefix: String,
pub base_branch: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct ReviewConfig {
pub enabled: bool,
pub max_review_rounds: u32,
}
impl Default for CodaConfig {
fn default() -> Self {
Self {
version: 1,
agent: AgentConfig::default(),
checks: vec![
"cargo build".to_string(),
"cargo +nightly fmt -- --check".to_string(),
"cargo clippy -- -D warnings".to_string(),
],
prompts: PromptsConfig::default(),
git: GitConfig::default(),
review: ReviewConfig::default(),
}
}
}
impl Default for AgentConfig {
fn default() -> Self {
Self {
model: "claude-opus-4-6".to_string(),
max_budget_usd: 20.0,
max_retries: 3,
max_turns: 100,
}
}
}
impl Default for PromptsConfig {
fn default() -> Self {
Self {
extra_dirs: vec![".coda/prompts".to_string()],
}
}
}
impl Default for GitConfig {
fn default() -> Self {
Self {
auto_commit: true,
branch_prefix: "feature".to_string(),
base_branch: "auto".to_string(),
}
}
}
impl Default for ReviewConfig {
fn default() -> Self {
Self {
enabled: true,
max_review_rounds: 5,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_should_create_default_config() {
let config = CodaConfig::default();
assert_eq!(config.version, 1);
assert_eq!(config.agent.max_budget_usd, 20.0);
assert_eq!(config.agent.max_retries, 3);
assert_eq!(config.checks.len(), 3);
assert!(config.git.auto_commit);
assert!(config.review.enabled);
}
#[test]
fn test_should_round_trip_yaml_serialization() {
let config = CodaConfig::default();
let yaml = serde_yaml::to_string(&config).unwrap();
let deserialized: CodaConfig = serde_yaml::from_str(&yaml).unwrap();
assert_eq!(deserialized.version, config.version);
assert_eq!(deserialized.agent.model, config.agent.model);
assert_eq!(deserialized.git.branch_prefix, config.git.branch_prefix);
}
#[test]
fn test_should_deserialize_custom_config() {
let yaml = r#"
version: 2
agent:
model: "claude-opus-4-20250514"
max_budget_usd: 50.0
max_retries: 5
checks:
- "npm run build"
- "npm run lint"
prompts:
extra_dirs:
- ".coda/custom-prompts"
- ".prompts"
git:
auto_commit: false
branch_prefix: "dev"
review:
enabled: false
max_review_rounds: 10
"#;
let config: CodaConfig = serde_yaml::from_str(yaml).unwrap();
assert_eq!(config.version, 2);
assert_eq!(config.agent.model, "claude-opus-4-20250514");
assert!((config.agent.max_budget_usd - 50.0).abs() < f64::EPSILON);
assert_eq!(config.agent.max_retries, 5);
assert_eq!(config.checks.len(), 2);
assert_eq!(config.checks[0], "npm run build");
assert_eq!(config.prompts.extra_dirs.len(), 2);
assert!(!config.git.auto_commit);
assert_eq!(config.git.branch_prefix, "dev");
assert!(!config.review.enabled);
assert_eq!(config.review.max_review_rounds, 10);
}
#[test]
fn test_should_deserialize_partial_config_with_defaults() {
let yaml = r#"
version: 1
agent:
model: "claude-sonnet-4-20250514"
permission_mode: "auto"
max_retries: 3
prompts:
extra_dirs: []
git:
auto_commit: true
branch_prefix: "feature"
commit_prefix: "feat"
review:
enabled: true
checks:
- "cargo build"
max_review_rounds: 5
"#;
let config: CodaConfig = serde_yaml::from_str(yaml).unwrap();
assert_eq!(config.version, 1);
assert!((config.agent.max_budget_usd - 20.0).abs() < f64::EPSILON);
assert_eq!(config.agent.max_retries, 3);
assert!(!config.checks.is_empty());
assert!(config.review.enabled);
}
#[test]
fn test_should_deserialize_minimal_config() {
let yaml = "version: 1\n";
let config: CodaConfig = serde_yaml::from_str(yaml).unwrap();
assert_eq!(config.version, 1);
assert_eq!(config.agent.model, "claude-opus-4-6");
assert!((config.agent.max_budget_usd - 20.0).abs() < f64::EPSILON);
assert!(config.git.auto_commit);
}
}