use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Mutex;
use io_harness::provider::{CompletionRequest, CompletionResponse, ToolCall};
use io_harness::template::{Template, Templates};
use io_harness::{
run_with, ApproveAll, Policy, Provider, RunOutcome, Store, TaskContract, Verification,
};
use serde_json::json;
#[derive(Default)]
struct MockScript {
at: AtomicUsize,
steps: Vec<Vec<ToolCall>>,
seen: Mutex<Vec<CompletionRequest>>,
}
impl MockScript {
fn scripted(steps: Vec<Vec<ToolCall>>) -> Self {
Self {
steps,
..Default::default()
}
}
fn request(&self, i: usize) -> CompletionRequest {
self.seen.lock().unwrap()[i].clone()
}
}
impl Provider for MockScript {
async fn complete(&self, req: CompletionRequest) -> io_harness::Result<CompletionResponse> {
let i = self.at.fetch_add(1, Ordering::SeqCst);
self.seen.lock().unwrap().push(req);
Ok(CompletionResponse {
tool_calls: self.steps.get(i).cloned().unwrap_or_default(),
..Default::default()
})
}
}
fn call(name: &str, args: serde_json::Value) -> ToolCall {
ToolCall {
name: name.into(),
arguments: args,
}
}
fn tmp() -> tempfile::TempDir {
tempfile::tempdir().unwrap()
}
fn write(path: &Path, body: &str) {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).unwrap();
}
std::fs::write(path, body).unwrap();
}
fn fixtures() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/templates-0.21.0")
}
fn discovered() -> Templates {
Templates::discover(fixtures()).expect("the checked-in fixture directory must discover")
}
#[test]
fn discovery_finds_both_layouts_with_their_descriptions() {
let templates = discovered();
assert_eq!(
templates.names(),
vec!["bugfix", "review", "ship"],
"sorted by name, and notes.txt is not a template"
);
assert_eq!(templates.len(), 3);
assert!(!templates.is_empty());
assert_eq!(
templates.get("bugfix").unwrap().description,
"Fix one failing test and change nothing else.",
"the frontmatter description"
);
assert_eq!(
templates.get("review").unwrap().description,
"Review the diff and say what would break",
"no frontmatter: the first prose line, heading markers stripped"
);
assert!(
templates.get("review").unwrap().body.contains("$ARGUMENTS"),
"a Template holds its body — rendering needs the text"
);
assert!(templates.get("notes").is_none());
}
#[test]
fn the_catalog_carries_names_and_descriptions_but_no_bodies() {
let catalog = discovered().catalog();
assert!(
catalog.contains("- ship: Write one file and stop."),
"got: {catalog}"
);
assert!(
!catalog.contains("{{needle}}"),
"a template body must not be in the catalogue, got: {catalog}"
);
assert_eq!(Templates::none().catalog(), "");
assert!(Templates::none().is_empty());
}
#[test]
fn render_substitutes_placeholders_and_routes_the_remainder_to_arguments() {
let rendered = discovered()
.render(
"bugfix",
&[
("test", "parses_a_crlf_header"),
("note", "it only fails on CI"),
("file", "src/parse.rs"),
("also", "see the flake in #412"),
],
)
.unwrap();
assert_eq!(
rendered,
"Fix the failing test parses_a_crlf_header in src/parse.rs. Change no other test.\n\n\
Extra context: it only fails on CI see the flake in #412",
"every placeholder substituted; the two unmatched arguments joined by one \
space, in the order given"
);
assert!(
!rendered.contains("{{") && !rendered.contains("$ARGUMENTS"),
"no marker may survive rendering, got: {rendered}"
);
}
#[test]
fn arguments_with_no_remainder_expands_to_nothing() {
let rendered = discovered()
.render("bugfix", &[("test", "t"), ("file", "f")])
.unwrap();
assert!(rendered.ends_with("Extra context: "), "got: {rendered:?}");
}
#[test]
fn an_unknown_template_name_is_an_error() {
let err = discovered()
.render("bugfx", &[("test", "t"), ("file", "f")])
.expect_err("a name that is not a template must not render");
let message = err.to_string();
assert!(
matches!(err, io_harness::Error::Config(_)),
"expected a Config error, got {err:?}"
);
assert!(
message.contains("bugfx") && message.contains("bugfix"),
"the error must name what was asked for and what exists, got: {message}"
);
}
#[test]
fn a_placeholder_with_no_argument_is_an_error_not_an_empty_string() {
let err = discovered()
.render("bugfix", &[("test", "parses_a_crlf_header")])
.expect_err("a placeholder with no argument must not render");
let message = err.to_string();
assert!(
matches!(err, io_harness::Error::Config(_)),
"expected a Config error, got {err:?}"
);
assert!(
message.contains("file"),
"the error must name the placeholder that went unfilled, got: {message}"
);
assert!(
message.contains("bugfix"),
"and the template it is in, got: {message}"
);
}
#[test]
fn a_substituted_value_is_never_re_substituted() {
let rendered = discovered()
.render(
"bugfix",
&[
("test", "{{file}}"),
("file", "$ARGUMENTS"),
("rest", "tail"),
],
)
.unwrap();
assert!(
rendered.starts_with("Fix the failing test {{file}} in $ARGUMENTS."),
"an inserted value must be emitted literally, got: {rendered}"
);
assert!(
rendered.ends_with("Extra context: tail"),
"and the real $ARGUMENTS still gets only the unmatched arguments, got: {rendered}"
);
}
#[test]
fn a_missing_or_non_directory_path_is_a_config_error() {
let root = tmp();
let missing = root.path().join("no-such-templates");
let err = Templates::discover(&missing).expect_err("a missing directory must fail");
assert!(
matches!(err, io_harness::Error::Config(ref m) if m.contains(&missing.display().to_string())),
"expected a Config error naming the path, got {err:?}"
);
let file = root.path().join("templates.md");
write(&file, "# not a directory\n");
let err = Templates::discover(&file).expect_err("a file must fail");
assert!(
matches!(err, io_harness::Error::Config(ref m) if m.contains(&file.display().to_string())),
"expected a Config error naming the path, got {err:?}"
);
}
#[test]
fn two_templates_of_the_same_name_are_rejected() {
let dir = tmp();
write(&dir.path().join("a.md"), "---\nname: same\n---\nA\n");
write(
&dir.path().join("b/TEMPLATE.md"),
"---\nname: same\n---\nB\n",
);
let err = Templates::discover(dir.path()).expect_err("a duplicate name must fail");
assert!(
matches!(err, io_harness::Error::Config(ref m) if m.contains("same")),
"expected a Config error naming the duplicate, got {err:?}"
);
}
#[test]
fn a_directory_over_the_template_cap_is_rejected() {
let dir = tmp();
for i in 0..=io_harness::template::MAX_TEMPLATES {
write(&dir.path().join(format!("t-{i:03}.md")), "a line\n");
}
let err = Templates::discover(dir.path()).expect_err("over the cap must fail");
assert!(
matches!(err, io_harness::Error::Config(ref m) if m.contains(&dir.path().display().to_string())),
"expected a Config error naming the directory, got {err:?}"
);
}
#[tokio::test]
async fn a_rendered_prompt_drives_a_real_run_unchanged() {
let root = tmp();
let rendered = discovered()
.render(
"ship",
&[
("file", "out.txt"),
("needle", "SHIPPED"),
("aside", "and stop there"),
],
)
.unwrap();
assert_eq!(
rendered, "Write out.txt so that it contains SHIPPED. and stop there",
"the string under test"
);
let contract = TaskContract::workspace(rendered.clone(), root.path())
.with_verification(Verification::WorkspaceFileContains {
file: "out.txt".into(),
needle: "SHIPPED".into(),
})
.with_max_steps(2);
let provider = MockScript::scripted(vec![vec![call(
"write_file",
json!({ "path": "out.txt", "content": "SHIPPED\n" }),
)]]);
let result = run_with(
&contract,
&provider,
&Store::memory().unwrap(),
&Policy::permissive(),
&ApproveAll,
)
.await
.expect("a rendered prompt is an ordinary goal");
assert!(
provider.request(0).user.contains(&rendered),
"the rendered string must reach the prompt unchanged, got: {}",
provider.request(0).user
);
assert!(
matches!(result.outcome, RunOutcome::Success { .. }),
"the run driven by the rendered prompt must verify, got {:?}",
result.outcome
);
}
#[test]
fn a_template_is_plain_data() {
let templates = discovered();
let ship: &Template = templates.get("ship").unwrap();
assert_eq!(ship.name, "ship");
assert!(ship.body.contains("{{needle}}"));
}