use pretty_assertions::assert_eq;
use tempfile::TempDir;
use super::*;
#[test]
fn model_selection_rebuilds_behavior_without_losing_retained_instructions() {
let home = TempDir::new().unwrap();
let directory = home.path().join(".rho/model-prompts");
std::fs::create_dir_all(&directory).unwrap();
for mode in ["append", "replace"] {
std::fs::write(
directory.join(format!("{mode}.md")),
format!("---\nprovider: test\nmodel: {mode}\nmode: {mode}\n---\n{mode} behavior"),
)
.unwrap();
}
let mut template = ModelPromptTemplate::new(
Some(home.path()),
"\ncwd\n".into(),
"retained".into(),
vec![PromptSource {
kind: PromptSourceKind::Base,
path: None,
bytes: "retained".len(),
}],
);
template.append_retained("\nagent and MCP instructions");
for (model, behavior) in [
("append", format!("{BASE_SYSTEM_PROMPT}\n\nappend behavior")),
("replace", "replace behavior".into()),
("unmatched", BASE_SYSTEM_PROMPT.into()),
("append", format!("{BASE_SYSTEM_PROMPT}\n\nappend behavior")),
] {
let running = PromptModel::Rho {
provider: "test".into(),
model: model.into(),
};
let built = template.build(&running).unwrap();
assert_eq!(built.text, format!(
"{behavior}\ncwd\nYou are running on {}. Rho can switch this mid-session and tells you when it does.\nretained\nagent and MCP instructions",
running.describe(),
));
assert_eq!(
built
.sources
.iter()
.map(|source| source.bytes)
.sum::<usize>(),
built.text.len()
);
}
}
#[test]
fn retained_suffix_and_mcp_rerender_match_the_built_prompt() {
let running = PromptModel::Rho {
provider: "test".into(),
model: "model".into(),
};
let mut template = ModelPromptTemplate::new(
None,
String::new(),
"retained".into(),
vec![PromptSource {
kind: PromptSourceKind::Base,
path: None,
bytes: "retained".len(),
}],
);
use crate::tools::mcp::report::{
ConnectedServerReport, McpServerReport, McpSessionReport, McpTransportSummary,
};
template.replace_mcp(&McpSessionReport {
servers: vec![McpServerReport::connected(ConnectedServerReport {
identity: "docs".into(),
transport: McpTransportSummary::Stdio {
command: "fixture".into(),
args: Vec::new(),
},
tools: Vec::new(),
instructions: Some("mcp context".into()),
live: Default::default(),
filtered_out_count: 0,
collision_skipped_count: 0,
})],
..Default::default()
});
let mut built = template.build(&running).unwrap();
let suffix = "\n\n# Agent instructions\n\nsession suffix";
built.text.push_str(suffix);
template.append_retained(suffix);
let rerendered = template.render(&running, built.model_prompt.as_ref());
assert_eq!(rerendered.text, built.text);
assert_eq!(
rerendered
.sources
.iter()
.map(|source| source.bytes)
.sum::<usize>(),
built.text.len()
);
}
#[test]
fn project_instruction_reload_replaces_context_and_source_accounting() {
let home = TempDir::new().unwrap();
let project = TempDir::new().unwrap();
std::fs::create_dir(home.path().join(".rho")).unwrap();
let running = PromptModel::Rho {
provider: "test".into(),
model: "model".into(),
};
let make_template = || {
let mut template = super::super::system_prompt_template_with_home_and_models(
&[],
project.path(),
Some(home.path()),
super::super::PromptModels {
running: &running,
advisor: None,
},
);
let skills = "\ncached skill context";
template.append_section(
skills.into(),
vec![PromptSource {
kind: PromptSourceKind::Skills,
path: None,
bytes: skills.len(),
}],
);
template.append_retained("\nretained agent instructions");
template
};
let mut template = make_template();
let mut expected = template.build(&running).unwrap();
for (rules, session, has_instructions) in [
(Some("first"), PromptSession::Different, true),
(Some("updated"), PromptSession::Current, true),
(Some("updated"), PromptSession::Different, true),
(None, PromptSession::Current, true),
(None, PromptSession::Different, false),
] {
for path in [
home.path().join(".rho/AGENTS.md"),
project.path().join("AGENTS.md"),
] {
if let Some(rules) = rules {
std::fs::write(path, rules).unwrap();
} else if path.exists() {
std::fs::remove_file(path).unwrap();
}
}
let switched = template.build(&running).unwrap();
assert_eq!(
(&switched.text, &switched.sources),
(&expected.text, &expected.sources),
);
let refreshed = template.build_for_session(&running, session).unwrap();
match session {
PromptSession::Current => {}
PromptSession::Different => expected = make_template().build(&running).unwrap(),
}
assert_eq!(
(&refreshed.text, &refreshed.sources),
(&expected.text, &expected.sources),
"instruction state: {rules:?}",
);
assert_eq!(
refreshed
.sources
.iter()
.map(|source| source.kind)
.collect::<Vec<_>>(),
if has_instructions {
vec![
PromptSourceKind::Base,
PromptSourceKind::Agents,
PromptSourceKind::Agents,
PromptSourceKind::Skills,
]
} else {
vec![PromptSourceKind::Base, PromptSourceKind::Skills]
},
);
assert_eq!(
refreshed
.sources
.iter()
.map(|source| source.bytes)
.sum::<usize>(),
refreshed.text.len()
);
}
}
#[test]
fn cached_render_and_explicit_reload_have_distinct_file_lifetimes() {
let home = TempDir::new().unwrap();
let project = TempDir::new().unwrap();
let instructions = project.path().join("AGENTS.md");
std::fs::write(&instructions, "original instructions").unwrap();
let directory = home.path().join(".rho/model-prompts");
std::fs::create_dir_all(&directory).unwrap();
let path = directory.join("custom.md");
std::fs::write(&path, "---\nprovider: test\nmodel: model\n---\nfirst").unwrap();
let mut template = ModelPromptTemplate::new(
Some(home.path()),
String::new(),
String::new(),
vec![PromptSource {
kind: PromptSourceKind::Base,
path: None,
bytes: 0,
}],
)
.with_project_instructions(project.path());
let running = PromptModel::Rho {
provider: "test".into(),
model: "model".into(),
};
let original = template.build(&running).unwrap();
std::fs::write(&path, "---\nprovider: test\nmodel: model\n---\nsecond").unwrap();
let cached = template.render(&running, original.model_prompt.as_ref());
assert_eq!(cached.text, original.text);
let reloaded = template.build(&running).unwrap();
assert_eq!(reloaded.model_prompt.as_ref().unwrap().body, "second");
assert_ne!(
&reloaded.model_prompt.as_ref().unwrap().sha256,
&original.model_prompt.as_ref().unwrap().sha256
);
std::fs::write(&path, "invalid frontmatter").unwrap();
assert!(template.build(&running).is_err());
std::fs::write(&instructions, "changed instructions").unwrap();
assert!(template
.build_for_session(&running, PromptSession::Different)
.is_err());
let after_failure = template.render(&running, original.model_prompt.as_ref());
assert_eq!(
(after_failure.text, after_failure.sources),
(original.text, original.sources),
);
}