use std::path::PathBuf;
#[derive(Debug, Clone)]
pub struct TemplateLoader {
prompt_dir: Option<PathBuf>,
}
impl TemplateLoader {
#[must_use]
pub fn new(prompt_dir: Option<impl Into<PathBuf>>) -> Self {
Self {
prompt_dir: prompt_dir.map(Into::into),
}
}
#[must_use]
pub fn load(&self, name: &str) -> Option<String> {
if let Some(ref dir) = self.prompt_dir {
let path = dir.join(format!("{name}.prompt.txt"));
match std::fs::read_to_string(&path) {
Ok(content) => {
tracing::debug!(name, path = %path.display(), "Loaded prompt template from filesystem");
return Some(content);
},
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
},
Err(e) => {
tracing::warn!(name, path = %path.display(), error = %e, "Failed to read prompt template file, using compiled-in default");
},
}
}
crate::YAML_PROMPTS
.iter()
.find(|(n, _)| *n == name)
.map(|(_, content)| (*content).to_owned())
}
#[must_use]
pub fn has_override_dir(&self) -> bool {
self.prompt_dir.is_some()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn loader_without_dir_returns_compiled_in_template() {
let loader = TemplateLoader::new(None::<PathBuf>);
let result = loader.load("__nonexistent_template_42__");
assert!(result.is_none(), "unknown template should return None");
}
#[test]
fn loader_without_dir_has_no_override_dir() {
let loader = TemplateLoader::new(None::<PathBuf>);
assert!(!loader.has_override_dir());
}
#[test]
fn loader_with_dir_has_override_dir() {
let loader = TemplateLoader::new(Some("/some/dir"));
assert!(loader.has_override_dir());
}
#[test]
fn loader_with_nonexistent_dir_falls_back_to_compiled_in() {
let loader = TemplateLoader::new(Some("/tmp/nonexistent_pisovereign_prompts_dir_xyz987"));
let result = loader.load("__nonexistent_template_42__");
assert!(result.is_none());
}
#[test]
fn loader_reads_filesystem_override_when_file_exists() {
let dir = std::env::temp_dir().join("ai_tokenopt_template_loader_test");
std::fs::create_dir_all(&dir).expect("create tmp dir");
let file_path = dir.join("my_prompt.prompt.txt");
std::fs::write(&file_path, "custom content").expect("write file");
let loader = TemplateLoader::new(Some(&dir));
let result = loader.load("my_prompt");
std::fs::remove_file(&file_path).ok();
std::fs::remove_dir(&dir).ok();
assert_eq!(result.as_deref(), Some("custom content"));
}
#[test]
fn filesystem_override_takes_priority_over_compiled_in() {
let dir = std::env::temp_dir().join("ai_tokenopt_template_override_test");
std::fs::create_dir_all(&dir).expect("create tmp dir");
let name = crate::YAML_PROMPTS
.first()
.map_or("test_override", |(n, _)| *n);
let file_path = dir.join(format!("{name}.prompt.txt"));
std::fs::write(&file_path, "filesystem_override_content").expect("write file");
let loader = TemplateLoader::new(Some(&dir));
let result = loader.load(name);
std::fs::remove_file(&file_path).ok();
std::fs::remove_dir(&dir).ok();
assert_eq!(
result.as_deref(),
Some("filesystem_override_content"),
"filesystem override must take priority over compiled-in template"
);
}
}