use std::path::{Path, PathBuf};
use std::process::Command;
const SECTIONS: [&str; 8] = [
"Synopsis",
"Description",
"Preconditions",
"Options",
"Exit status",
"Environment",
"Files",
"See also",
];
const OMITTED: &str = "Exit status";
fn declared_sections(lock: &str) -> Vec<String> {
let loaded = headwater_yaml::load(lock).expect("the lock is YAML this engine reads");
let mut at = loaded
.value
.as_map()
.expect("the lock is a mapping")
.get("resolved")
.expect("the lock carries a resolved taxonomy");
for key in ["kinds", "interface_contract", "sections", "require"] {
at = at
.value
.as_map()
.unwrap_or_else(|| panic!("`{key}` sits under a mapping"))
.get(key)
.unwrap_or_else(|| panic!("the resolved taxonomy declares `{key}`"));
}
at.value
.as_seq()
.expect("`sections.require` is a sequence")
.iter()
.map(|item| {
item.value
.as_scalar()
.expect("a required section is a scalar")
.text
.clone()
})
.collect()
}
fn repository() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../..")
.canonicalize()
.expect("the repository root resolves")
}
struct Root {
at: PathBuf,
}
impl Root {
fn new(label: &str) -> Root {
let at = std::env::temp_dir().join(format!(
"headwater-cli-interface-{}-{label}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&at);
std::fs::create_dir_all(&at).expect("the root is made");
let repository = repository();
copy(&repository.join("packages"), &at.join("packages"));
copy(
&repository.join("docs/taxonomies"),
&at.join("docs/taxonomies"),
);
for name in ["taxonomy.yml", "overlay.yml"] {
let to = at.join(".headwater").join(name);
std::fs::create_dir_all(to.parent().expect("it has a parent"))
.expect("the declaration directory is there");
std::fs::copy(repository.join(".headwater").join(name), to)
.expect("the declaration copies");
}
let root = Root { at };
let resolved = root.run(&["taxonomy", "resolve"]);
assert_eq!(
resolved.code,
Some(0),
"the fixture resolves\n{}{}",
resolved.out,
resolved.err
);
root
}
fn contract(&self, slug: &str, sections: &[&str], under: &str) -> String {
let relative = format!("docs/interfaces/{slug}.md");
let path = self.at.join(&relative);
std::fs::create_dir_all(path.parent().expect("it has a parent"))
.expect("the shelf directory is there");
let mut text = format!(
"---\n\
id: HW-IFACE-{slug}\n\
status: draft\n\
status_since: 2026-08-16\n\
summary: \"What this verb takes, what it prints, and what its exit status means.\"\n\
last_verified: 2026-08-16\n\
title: \"{slug}\"\n\
---\n\n\
# {slug}\n"
);
for section in sections {
text.push_str(&format!("\n## {section}\n\n{under}\n"));
}
std::fs::write(&path, text).expect("the contract writes");
relative
}
fn remove(&self, relative: &str) {
std::fs::remove_file(self.at.join(relative)).expect("the contract is removed");
}
fn source_file(&self, relative: &str) {
let path = self.at.join(relative);
std::fs::create_dir_all(path.parent().expect("it has a parent"))
.expect("the source directory is there");
std::fs::write(&path, "// a file the anchor resolver finds\n").expect("the source writes");
}
fn run(&self, arguments: &[&str]) -> Ran {
let output = Command::new(env!("CARGO_BIN_EXE_headwater"))
.args(arguments)
.arg("--root")
.arg(&self.at)
.output()
.expect("the binary runs");
Ran {
code: output.status.code(),
out: String::from_utf8_lossy(&output.stdout).into_owned(),
err: String::from_utf8_lossy(&output.stderr).into_owned(),
}
}
}
impl Drop for Root {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.at);
}
}
#[derive(Debug)]
struct Ran {
code: Option<i32>,
out: String,
err: String,
}
impl Ran {
fn section_findings(&self) -> Vec<&str> {
self.out
.lines()
.filter(|line| line.contains("section.required.missing (OB-SECT-1)"))
.collect()
}
fn flowed(&self) -> String {
flowed(&self.out)
}
}
fn flowed(text: &str) -> String {
text.split_whitespace().collect::<Vec<&str>>().join(" ")
}
fn copy(from: &Path, to: &Path) {
std::fs::create_dir_all(to).expect("the directory is there");
for entry in std::fs::read_dir(from).expect("the fixture directory reads") {
let entry = entry.expect("the entry reads");
let target = to.join(entry.file_name());
match entry.file_type().expect("the file type reads").is_dir() {
true => copy(&entry.path(), &target),
false => {
std::fs::copy(entry.path(), &target).expect("the fixture copies");
}
}
}
}
#[test]
fn the_declaration_requires_eight_headings_in_the_order_it_writes_them() {
let root = Root::new("eight-headings");
let lock = std::fs::read_to_string(root.at.join(".headwater/taxonomy.lock"))
.expect("the resolve wrote a lock");
let declared = declared_sections(&lock);
let left: Vec<&str> = SECTIONS
.iter()
.copied()
.filter(|section| !declared.iter().any(|held| held == section))
.collect();
let arrived: Vec<&str> = declared
.iter()
.map(String::as_str)
.filter(|section| !SECTIONS.contains(section))
.collect();
assert!(
left.is_empty(),
"`interface_contract` no longer requires {}. Eight headings are declared in \
`.headwater/overlay.yml`, seven of them from man-pages(7) and `Preconditions` from \
Design by Contract, and every contract under `docs/interfaces/` is written to them. \
Removing one is a change to what this kind promises a reader, and it belongs in the \
pull request that makes it rather than here",
left.join(", ")
);
assert!(
arrived.is_empty(),
"`interface_contract` now requires {}, which this case does not know about. A heading \
added to the contract is owed by every document already on the shelf, so add it to \
`SECTIONS` in the same change that adds it to the declaration",
arrived.join(", ")
);
assert_eq!(
declared, SECTIONS,
"the eight headings are declared in the order a contract is read in"
);
}
#[test]
fn the_constructor_writes_a_contract_that_a_strict_run_passes() {
let root = Root::new("constructor");
let wrote = root.run(&["new", "interface_contract", "--title", "headwater check"]);
assert_eq!(
wrote.code,
Some(0),
"the kind can be constructed\n{}{}",
wrote.out,
wrote.err
);
assert!(
wrote
.out
.contains("wrote docs/interfaces/headwater-check.md"),
"the shelf and the layout name the file:\n{}",
wrote.out
);
let text = std::fs::read_to_string(root.at.join("docs/interfaces/headwater-check.md"))
.expect("the document reads");
let written: Vec<&str> = text
.lines()
.filter_map(|line| line.strip_prefix("## "))
.collect();
assert_eq!(written, SECTIONS, "the document carries the contract");
assert!(
text.contains("id: HW-IFACE-headwater-check"),
"the scheme mints an identifier:\n{text}"
);
let checked = root.run(&["check", "--strict"]);
assert_eq!(
checked.code,
Some(0),
"a scaffolded contract passes a strict run\n{}{}",
checked.out,
checked.err
);
assert!(
checked.out.contains(" 0 findings"),
"and it passes with nothing reported:\n{}",
checked.out
);
let explained = root.run(&["explain", "docs/interfaces/headwater-check.md"]);
assert!(
explained.out.contains("kind interface_contract")
&& explained.out.contains(
"requires the facets status, status_since, summary, last_verified, title"
)
&& explained.out.contains("requires the sections")
&& explained.out.contains("Environment, Files, See also"),
"`explain` prints the kind, its facets and its sections:\n{}",
explained.out
);
}
#[test]
fn a_contract_missing_one_heading_is_reported_by_its_name_and_its_file() {
let root = Root::new("missing-one");
let complete = root.contract("headwater-route", &SECTIONS, "One sentence of description.");
let short: Vec<&str> = SECTIONS
.iter()
.copied()
.filter(|section| *section != OMITTED)
.collect();
assert_eq!(short.len(), SECTIONS.len() - 1, "one heading is left out");
let incomplete = root.contract("headwater-explain", &short, "One sentence of description.");
let checked = root.run(&["check"]);
assert_eq!(
checked.section_findings().len(),
1,
"one finding, and it is about the document that is short one heading:\n{}",
checked.out
);
assert!(
checked.flowed().contains(&flowed(&format!(
" {incomplete} ✗ error\n \
section.required.missing (OB-SECT-1): `interface_contract` requires the section \
`{OMITTED}`, and no heading of this document says so\n \
fix: add a `{OMITTED}` heading to {incomplete}, with the content the kind is for"
))),
"the finding names the rule, the kind, the heading and the file:\n{}",
checked.out
);
assert!(
!checked
.section_findings()
.iter()
.any(|line| line.contains(&complete)),
"the complete contract is in no finding:\n{}",
checked.out
);
let strict = root.run(&["check", "--strict"]);
assert_eq!(
strict.code,
Some(1),
"a missing section fails a strict run\n{}{}",
strict.out,
strict.err
);
root.remove(&incomplete);
let again = root.run(&["check", "--strict"]);
assert_eq!(
again.code,
Some(0),
"the complete contract alone passes\n{}{}",
again.out,
again.err
);
assert!(
again.section_findings().is_empty() && again.out.contains(" 0 findings"),
"and it passes with nothing reported:\n{}",
again.out
);
}
#[test]
fn the_contract_reads_a_heading_and_never_its_order_or_its_content() {
let root = Root::new("what-it-does-not-read");
let reversed: Vec<&str> = SECTIONS.iter().copied().rev().collect();
let out_of_order = root.contract("headwater-generate", &reversed, "One sentence.");
let hollow = root.contract("headwater-import", &SECTIONS, "TODO write this section.");
let short: Vec<&str> = SECTIONS
.iter()
.copied()
.filter(|section| *section != OMITTED)
.collect();
let control = root.contract("headwater-export", &short, "One sentence.");
let checked = root.run(&["check"]);
let findings = checked.section_findings();
assert_eq!(
findings.len(),
1,
"the rule ran, and it reported the control and nothing else:\n{}",
checked.out
);
assert!(
checked.out.contains(&format!(" {control} ✗ error")),
"the one finding is about the document that is short a heading:\n{}",
checked.out
);
assert!(
!checked.out.contains(&format!(" {out_of_order} ✗ error"))
&& !checked.out.contains(&format!(" {hollow} ✗ error")),
"the order of the headings and the words under them reach no rule:\n{}",
checked.out
);
}
#[test]
fn a_governs_edge_binds_on_existence_and_reports_a_path_that_is_not_there() {
let root = Root::new("governs");
root.source_file("engine/crates/route/src/lib.rs");
let relative = "docs/interfaces/headwater-route.md";
let path = root.at.join(relative);
std::fs::create_dir_all(path.parent().expect("it has a parent"))
.expect("the shelf directory is there");
let mut text = String::from(
"---\n\
id: HW-IFACE-headwater-route\n\
status: draft\n\
status_since: 2026-08-16\n\
summary: \"What this verb takes, what it prints, and what its exit status means.\"\n\
last_verified: 2026-08-16\n\
title: \"headwater route\"\n\
relations:\n \
governs:\n \
- engine/crates/route/src/lib.rs\n \
- engine/crates/route/src/nowhere.rs\n\
---\n\n\
# headwater route\n",
);
for section in SECTIONS {
text.push_str(&format!("\n## {section}\n\nOne sentence of description.\n"));
}
std::fs::write(&path, text).expect("the contract writes");
let checked = root.run(&["check"]);
assert!(
!checked
.out
.contains("relation.endpoint.not_permitted (OB-REL-2)"),
"`governs` admits an `interface_contract` as its source:\n{}",
checked.out
);
assert!(
!checked
.out
.contains("relation.reciprocity.missing (OB-REL-1)"),
"`governs` declares no reciprocal, so neither end owes one:\n{}",
checked.out
);
let unresolved: Vec<&str> = checked
.out
.lines()
.filter(|line| line.contains("relation.target.unresolved (OB-REL-4)"))
.collect();
assert_eq!(
unresolved.len(),
1,
"the path that is on the tree binds and the other does not:\n{}",
checked.out
);
assert!(
checked.flowed().contains(&flowed(
"`HW-IFACE-headwater-route` declares `governs: \
engine/crates/route/src/nowhere.rs`"
)),
"the finding names the edge that resolved to nothing:\n{}",
unresolved[0]
);
let explained = root.run(&["explain", relative]);
assert!(
explained
.out
.contains("to engine/crates/route/src/lib.rs governs"),
"the bound edge is on the document:\n{}",
explained.out
);
}