//! Pure validation logic for project settings — no side effects. //! //! All functions in this module are pure: given the same input, they always //! return the same output, and they never perform any I/O. use super::{Error, project::ProjectSettings}; /// Validate the incoming [`ProjectSettings`] before writing to disk. /// /// # Errors /// - [`Error::Validation`] if any field value is invalid. pub fn validate_project_settings(s: &ProjectSettings) -> Result<(), Error> { match s.default_qa.as_str() { "server" | "agent" | "human" => {} other => { return Err(Error::Validation(format!( "Invalid default_qa value '{other}'. Must be one of: server, agent, human" ))); } } Ok(()) } // ── Tests ───────────────────────────────────────────────────────────────────── #[cfg(test)] mod tests { use super::*; use crate::config::ProjectConfig; use crate::service::settings::project::settings_from_config; fn make_settings(default_qa: &str) -> ProjectSettings { let cfg = ProjectConfig { default_qa: default_qa.to_string(), ..Default::default() }; settings_from_config(&cfg) } // ── Valid cases ─────────────────────────────────────────────────────────── #[test] fn accepts_server_qa_mode() { assert!(validate_project_settings(&make_settings("server")).is_ok()); } #[test] fn accepts_agent_qa_mode() { assert!(validate_project_settings(&make_settings("agent")).is_ok()); } #[test] fn accepts_human_qa_mode() { assert!(validate_project_settings(&make_settings("human")).is_ok()); } #[test] fn accepts_all_qa_modes() { for mode in &["server", "agent", "human"] { let result = validate_project_settings(&make_settings(mode)); assert!(result.is_ok(), "qa mode '{mode}' should be valid"); } } // ── Invalid cases ───────────────────────────────────────────────────────── #[test] fn rejects_empty_qa_mode() { let err = validate_project_settings(&make_settings("")).unwrap_err(); assert!(matches!(err, Error::Validation(_))); } #[test] fn rejects_unknown_qa_mode() { let err = validate_project_settings(&make_settings("robot")).unwrap_err(); assert!(matches!(err, Error::Validation(ref msg) if msg.contains("robot"))); } #[test] fn rejects_uppercase_qa_mode() { let err = validate_project_settings(&make_settings("Server")).unwrap_err(); assert!(matches!(err, Error::Validation(_))); } #[test] fn rejects_partial_qa_mode() { let err = validate_project_settings(&make_settings("serv")).unwrap_err(); assert!(matches!(err, Error::Validation(_))); } #[test] fn rejects_qa_mode_with_trailing_space() { let err = validate_project_settings(&make_settings("server ")).unwrap_err(); assert!(matches!(err, Error::Validation(_))); } #[test] fn error_message_contains_invalid_value() { let err = validate_project_settings(&make_settings("bad_mode")).unwrap_err(); if let Error::Validation(msg) = err { assert!( msg.contains("bad_mode"), "error message should include the bad value" ); assert!( msg.contains("server") && msg.contains("agent") && msg.contains("human"), "error message should list valid values" ); } else { panic!("expected ValidationError"); } } // ── Settings with other fields set ─────────────────────────────────────── #[test] fn valid_settings_with_all_optional_fields_set() { let s = ProjectSettings { default_qa: "agent".to_string(), default_coder_model: Some("opus".to_string()), max_coders: Some(4), max_retries: 5, base_branch: Some("main".to_string()), rate_limit_notifications: false, timezone: Some("UTC".to_string()), rendezvous: Some("ws://host:3001/crdt-sync".to_string()), watcher_sweep_interval_secs: 30, watcher_done_retention_secs: 3600, }; assert!(validate_project_settings(&s).is_ok()); } #[test] fn valid_settings_with_no_optional_fields() { let s = ProjectSettings { default_qa: "human".to_string(), default_coder_model: None, max_coders: None, max_retries: 2, base_branch: None, rate_limit_notifications: true, timezone: None, rendezvous: None, watcher_sweep_interval_secs: 60, watcher_done_retention_secs: 14400, }; assert!(validate_project_settings(&s).is_ok()); } }