huskies: merge 611_story_extract_settings_service
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
//! 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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user