150 lines
5.2 KiB
Rust
150 lines
5.2 KiB
Rust
|
|
//! 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());
|
||
|
|
}
|
||
|
|
}
|