Files
huskies/server/src/service/settings/validate.rs
T

150 lines
5.2 KiB
Rust
Raw Normal View History

//! 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());
}
}