use crate::{
config::ReviewConfig,
voice::{VoiceConfig, VoiceLoader, principles::principles_addendum},
};
pub fn build_voice_config(config: &ReviewConfig) -> VoiceConfig {
let principles = if config.voice_principles {
Some(principles_addendum().to_string())
} else {
None
};
let (voice_addendum, voice_name) = match config.voice_package.as_deref() {
None | Some("") => (None, None),
Some(name) => {
let loader = VoiceLoader::new();
match loader.load(name) {
Ok(pkg) => {
let addendum = pkg.effective_addendum();
if addendum.is_empty() {
tracing::warn!(
voice = name,
"voice package loaded but effective_addendum is empty; \
treating as no-voice (voice_name=None)"
);
(None, None)
} else {
(Some(addendum), Some(name.to_string()))
}
}
Err(e) => {
tracing::warn!(
voice = name,
error = %e,
"voice package not found; proceeding without voice layer"
);
(None, None)
}
}
}
};
VoiceConfig {
principles,
voice_addendum,
voice_name,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn config_default_voice() -> ReviewConfig {
unsafe {
std::env::remove_var("TRUSTY_REVIEW_VOICE_PACKAGE");
std::env::remove_var("TRUSTY_REVIEW_PRINCIPLES");
}
crate::config::ReviewConfig::from_env_and_file(None, None)
}
#[test]
#[serial_test::serial]
fn build_voice_config_no_voice() {
let mut config = config_default_voice();
config.voice_package = None;
config.voice_principles = true;
let vc = build_voice_config(&config);
assert!(
vc.principles.is_some(),
"principles must be enabled by default"
);
assert!(
!vc.principles.as_deref().unwrap_or("").is_empty(),
"principles must be non-empty"
);
assert!(
vc.voice_addendum.is_none(),
"no voice package → no voice addendum"
);
assert!(vc.voice_name.is_none(), "no voice package → no voice name");
}
#[test]
#[serial_test::serial]
fn build_voice_config_principles_off() {
let mut config = config_default_voice();
config.voice_package = None;
config.voice_principles = false;
let vc = build_voice_config(&config);
assert!(
vc.principles.is_none(),
"principles=false must produce None"
);
assert!(
!vc.has_any_addendum(),
"no layers → has_any_addendum must be false"
);
}
#[test]
#[serial_test::serial]
fn build_voice_config_duetto_bundled() {
let mut config = config_default_voice();
config.voice_package = Some("duetto".to_string());
config.voice_principles = true;
let vc = build_voice_config(&config);
assert!(
vc.voice_addendum.is_some(),
"duetto voice must produce a non-None addendum"
);
assert!(
!vc.voice_addendum.as_deref().unwrap_or("").is_empty(),
"duetto addendum must be non-empty"
);
assert_eq!(
vc.voice_name.as_deref(),
Some("duetto"),
"voice_name must be set to \"duetto\""
);
assert!(
vc.has_any_addendum(),
"duetto + principles must report has_any_addendum=true"
);
}
#[test]
#[serial_test::serial]
fn build_voice_config_unknown_voice_degrades() {
let mut config = config_default_voice();
config.voice_package = Some("nonexistent-voice-xyz".to_string());
config.voice_principles = true;
let vc = build_voice_config(&config);
assert!(
vc.voice_addendum.is_none(),
"unknown voice must degrade to None (not panic)"
);
assert!(
vc.voice_name.is_none(),
"unknown voice must produce None voice_name"
);
assert!(
vc.principles.is_some(),
"principles must remain active even when voice is missing"
);
}
#[test]
#[serial_test::serial]
fn build_voice_config_combined_ordering() {
let mut config = config_default_voice();
config.voice_package = Some("duetto".to_string());
config.voice_principles = true;
let vc = build_voice_config(&config);
let combined = vc.combined_addendum();
let p_pos = combined.find("Review principles").unwrap_or(usize::MAX);
let v_pos = combined
.find("data and control flow")
.or_else(|| combined.find("correctness first"))
.unwrap_or(usize::MAX);
assert!(
p_pos != usize::MAX,
"combined must contain principles heading"
);
assert!(v_pos != usize::MAX, "combined must contain duetto content");
assert!(
p_pos < v_pos,
"principles must come before voice content in combined addendum"
);
}
#[test]
#[serial_test::serial]
fn build_voice_config_empty_addendum_gives_no_voice_name() {
let dir = tempfile::tempdir().expect("tempdir");
let voice_dir = dir.path().join("emptyvoice");
std::fs::create_dir_all(&voice_dir).expect("create voice dir");
std::fs::write(
voice_dir.join("voice.toml"),
r#"
[meta]
name = "emptyvoice"
version = "0.1.0"
[voice]
system_addendum = ""
"#,
)
.expect("write empty voice.toml");
use crate::voice::VoiceLoader;
let loader = VoiceLoader::with_extra_dirs(vec![dir.path().to_path_buf()]);
let pkg = loader.load("emptyvoice").expect("emptyvoice must load");
let addendum = pkg.effective_addendum();
assert!(
addendum.is_empty(),
"emptyvoice effective_addendum must be empty"
);
let (voice_addendum, voice_name) = if addendum.is_empty() {
(None::<String>, None::<String>)
} else {
(Some(addendum), Some("emptyvoice".to_string()))
};
assert!(
voice_addendum.is_none(),
"empty addendum must produce None voice_addendum"
);
assert!(
voice_name.is_none(),
"empty addendum must produce None voice_name (not misleading Some)"
);
}
}