fn machine_with_prompt_templates(
state_body: &str,
templates: &[(&str, &str)],
) -> (tempfile::TempDir, rhei_validator::StateMachine) {
let dir = tempfile::tempdir().expect("tmpdir");
let states_path = dir.path().join("states.yaml");
let yaml = format!(
r#"
name: prompt-demo
version: 1
states:
review:
description: Review
initial: true
{state_body}
done:
description: Done
final: true
transitions:
- from: review
to: done
"#
);
fs::write(&states_path, yaml).expect("write states.yaml");
let templates_dir = dir.path().join("prompt_templates");
fs::create_dir_all(&templates_dir).expect("create prompt_templates");
for (name, body) in templates {
fs::write(templates_dir.join(name), body).expect("write prompt template");
}
let machine = rhei_validator::StateMachine::from_yaml_file(&states_path)
.expect("machine should load with its prompt templates");
(dir, machine)
}
const REVIEW_BODY: &str = r#" prompt_template:
name: artifact-review
values:
review_role: API reviewer
findings_path: reports/findings.md
instructions: Then summarize what you found.
"#;
#[test]
fn state_instructions_compose_template_then_inline_text() {
let (_dir, machine) = machine_with_prompt_templates(
REVIEW_BODY,
&[("artifact-review.md", "You are a {review_role}.\nWrite to {findings_path}.\n")],
);
assert_eq!(
state_instructions(&machine, "review"),
"You are a API reviewer.\nWrite to reports/findings.md.\n\nThen summarize what you found.",
"`rhei complete` rewrites see the same composed prompt the agent ran with"
);
assert_eq!(state_instructions(&machine, "done"), "");
assert_eq!(state_instructions(&machine, "no-such-state"), "");
}
#[test]
fn state_personality_is_inline_only_and_empty_becomes_none() {
let (_dir, machine) = machine_with_prompt_templates(
" prompt_template: artifact-review\n personality: Be terse.\n",
&[("artifact-review.md", "Review it.\n")],
);
assert_eq!(state_personality(&machine, "review").as_deref(), Some("Be terse."));
assert_eq!(state_personality(&machine, "done"), None);
assert_eq!(state_personality(&machine, "no-such-state"), None);
}
#[test]
fn render_state_machine_text_lists_templates_and_state_selection() {
let (_dir, machine) = machine_with_prompt_templates(
REVIEW_BODY,
&[("artifact-review.md", "Review it.\n"), ("triage.md", "Triage it.\n")],
);
let rendered = render_state_machine_text(&machine);
assert!(rendered.contains("Prompt templates: artifact-review, triage"), "{rendered}");
assert!(rendered.contains("Prompt template: artifact-review"), "{rendered}");
}
#[test]
fn render_state_machine_json_carries_templates_and_reference() {
let (_dir, machine) = machine_with_prompt_templates(
REVIEW_BODY,
&[("artifact-review.md", "You are a {review_role}.\nWrite to {findings_path}.\n")],
);
let rendered = render_state_machine_json(&machine).expect("render JSON");
let json: serde_json::Value = serde_json::from_str(&rendered).expect("parse JSON");
assert_eq!(
json["prompt_templates"]["artifact-review"]["instructions"],
"You are a {review_role}.\nWrite to {findings_path}.\n"
);
let review = json["states"]
.as_array()
.expect("states array")
.iter()
.find(|state| state["name"] == "review")
.expect("review state");
assert_eq!(review["prompt_template"]["name"], "artifact-review");
assert_eq!(review["prompt_template"]["values"]["review_role"], "API reviewer");
}
#[test]
fn viz_machine_exposes_the_composed_instructions() {
let (_dir, machine) = machine_with_prompt_templates(
REVIEW_BODY,
&[("artifact-review.md", "You are a {review_role}.\nWrite to {findings_path}.\n")],
);
let flattened = rhei_viz::flatten_machine(&machine);
let review = flattened
.states
.iter()
.find(|state| state.name == "review")
.expect("review state");
assert_eq!(
review.instructions.as_deref(),
Some(
"You are a API reviewer.\nWrite to reports/findings.md.\n\nThen summarize what you found."
),
"the inspector shows the prompt the agent actually receives"
);
}
fn canonical_tempdir() -> (tempfile::TempDir, PathBuf) {
let dir = tempfile::tempdir().expect("tmpdir");
let root = dir.path().canonicalize().expect("canonicalize tmpdir");
(dir, root)
}
#[test]
fn watch_plan_covers_an_existing_prompt_templates_directory_recursively() {
let (_guard, dir) = canonical_tempdir();
let dir = dir.as_path();
let plan = dir.join("plan.rhei.md");
fs::write(&plan, "# Rhei: Watch\n").expect("plan");
let states = dir.join("states.yaml");
fs::write(&states, "name: x\nversion: 1\nstates: {}\ntransitions: []\n").expect("states");
let templates_dir = dir.join("prompt_templates");
fs::create_dir_all(&templates_dir).expect("prompt_templates");
fs::write(templates_dir.join("review.md"), "Review it.\n").expect("template");
let plan_out = validation_watch_plan(&plan, Some(&states));
assert!(
plan_out.roots.iter().any(|root| root.mode == RecursiveMode::Recursive
&& paths_equivalent(&root.path, &templates_dir)),
"an existing prompt_templates/ is watched recursively: {:?}",
plan_out.roots
);
assert!(
should_revalidate(
&Event {
kind: EventKind::Modify(notify::event::ModifyKind::Data(
notify::event::DataChange::Content,
)),
paths: vec![templates_dir.join("review.md")],
attrs: Default::default(),
},
&plan_out.targets,
),
"editing a prompt file revalidates"
);
}
#[test]
fn watch_plan_still_matches_prompt_files_when_the_directory_is_absent() {
let (_guard, dir) = canonical_tempdir();
let plan = dir.join("plan.rhei.md");
fs::write(&plan, "# Rhei: Watch\n").expect("plan");
let states = dir.join("states.yaml");
fs::write(&states, "name: x\nversion: 1\nstates: {}\ntransitions: []\n").expect("states");
let plan_out = validation_watch_plan(&plan, Some(&states));
assert!(
should_revalidate(
&Event {
kind: EventKind::Create(notify::event::CreateKind::Folder),
paths: vec![dir.join("prompt_templates")],
attrs: Default::default(),
},
&plan_out.targets,
),
"creating prompt_templates/ revalidates and re-plans"
);
}