use std::path::{Path, PathBuf};
use std::process::{Command, Output};
fn repository() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../..")
.canonicalize()
.expect("the repository root resolves")
}
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");
}
}
}
}
const DOCUMENT: &str = "docs/decisions/0001-a-document-show-prints.md";
const IDENTIFIER: &str = "HW-DR-0001";
fn document() -> Vec<u8> {
let front = "---\nid: HW-DR-0001\ntitle: A document show prints\nstatus: current\nstatus_since: 2026-09-27\nlast_verified: 2026-09-27\nsummary: A fixture for show, whose body a text round trip would change.\n---\n";
let body = "\r\n# A document show prints\r\n\r\n## Context\r\n\r\nThe caf\u{e9} line ends in CRLF.\r\n\r\n## Decision\r\n\r\nThe last line has no newline.";
[front.as_bytes(), body.as_bytes()].concat()
}
struct Root {
at: PathBuf,
}
impl Root {
fn new(label: &str) -> Root {
let at =
std::env::temp_dir().join(format!("headwater-cli-show-{}-{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("taxonomy-source/headwater-standard"),
&at.join(".headwater/packages/headwater-standard"),
);
repoint_bundles(&at.join(".headwater/packages/headwater-standard"));
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 path = at.join(DOCUMENT);
std::fs::create_dir_all(path.parent().expect("it has a parent"))
.expect("the shelf is made");
std::fs::write(&path, document()).expect("the document writes");
let root = Root { at };
let resolved = root.run(&["taxonomy", "resolve"]);
assert_eq!(
resolved.status.code(),
Some(0),
"the fixture resolves\n{}",
String::from_utf8_lossy(&resolved.stderr)
);
root
}
fn run(&self, arguments: &[&str]) -> Output {
Command::new(env!("CARGO_BIN_EXE_headwater"))
.args(arguments)
.arg("--root")
.arg(&self.at)
.output()
.expect("the binary runs")
}
fn run_inside(&self, arguments: &[&str]) -> Output {
Command::new(env!("CARGO_BIN_EXE_headwater"))
.args(arguments)
.arg("--root")
.arg(".")
.current_dir(&self.at)
.output()
.expect("the binary runs")
}
}
impl Drop for Root {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.at);
}
}
#[test]
fn show_writes_the_bytes_on_disk_for_an_identifier_and_for_its_path() {
let root = Root::new("bytes");
let on_disk = std::fs::read(root.at.join(DOCUMENT)).expect("the document reads");
assert_eq!(on_disk, document(), "the fixture wrote what it meant to");
let explained = root.run(&["explain", IDENTIFIER]);
assert_eq!(
explained.status.code(),
Some(0),
"the fixture document is typed and carries its identifier: {}",
String::from_utf8_lossy(&explained.stderr)
);
for target in [IDENTIFIER, DOCUMENT] {
let shown = root.run(&["show", target]);
assert_eq!(
shown.status.code(),
Some(0),
"`show {target}`: {}",
String::from_utf8_lossy(&shown.stderr)
);
assert!(
shown.stderr.is_empty(),
"`show {target}` writes nothing on standard error: {}",
String::from_utf8_lossy(&shown.stderr)
);
assert!(
shown.stdout == on_disk,
"`show {target}` writes the bytes on disk and nothing else:\n got {:?}\n want {:?}",
shown.stdout,
on_disk
);
}
for target in ["HW-DR-9999", "HW-DR-004", "engine/nowhere/at-all.rs"] {
let shown = root.run(&["show", target]);
let explained = root.run(&["explain", target]);
assert_eq!(
shown.status.code(),
Some(1),
"`show {target}` refuses: {}",
String::from_utf8_lossy(&shown.stderr)
);
assert!(
shown.stdout.is_empty(),
"`show {target}` writes nothing on standard output"
);
assert_eq!(
explained.status.code(),
Some(1),
"`explain {target}` refuses too"
);
assert_eq!(
String::from_utf8_lossy(&shown.stderr),
String::from_utf8_lossy(&explained.stderr),
"`show` and `explain` refuse `{target}` in the same sentence, because one resolver answers both"
);
}
}
#[test]
fn show_writes_bytes_that_are_not_utf_8_unchanged() {
let root = Root::new("not-utf-8");
let path = "docs/notes/not-utf-8.md";
let bytes: &[u8] = b"\xff\xfe# Not UTF-8\r\n\r\nA byte \xe9 alone, and \xc3 cut short\r\n";
let at = root.at.join(path);
std::fs::create_dir_all(at.parent().expect("it has a parent")).expect("the shelf is made");
std::fs::write(&at, bytes).expect("the document writes");
let shown = root.run(&["show", path]);
assert_eq!(
shown.status.code(),
Some(0),
"the census carries the file as a row: {}",
String::from_utf8_lossy(&shown.stderr)
);
assert!(
shown.stdout == bytes,
"`show` writes bytes that are not UTF-8 unchanged:\n got {:?}\n want {:?}",
shown.stdout,
bytes
);
}
#[cfg(unix)]
#[test]
fn show_refuses_a_symlink_and_prints_nothing_of_its_target() {
let root = Root::new("symlink");
let outside = root.at.with_extension("outside");
std::fs::write(&outside, b"a secret outside the root\n").expect("the target writes");
let path = "docs/decisions/9998-link.md";
std::os::unix::fs::symlink(&outside, root.at.join(path)).expect("the link is made");
let shown = root.run(&["show", path]);
let _ = std::fs::remove_file(&outside);
assert_eq!(
shown.status.code(),
Some(1),
"`show` refuses a symlink: {}",
String::from_utf8_lossy(&shown.stderr)
);
assert!(
shown.stdout.is_empty(),
"`show` prints no byte of the link's target: {:?}",
String::from_utf8_lossy(&shown.stdout)
);
assert!(
String::from_utf8_lossy(&shown.stderr).contains("is a symlink"),
"the refusal names the link: {}",
String::from_utf8_lossy(&shown.stderr)
);
}
#[cfg(unix)]
#[test]
fn explain_refuses_a_symlink_row_and_prints_nothing() {
let root = Root::new("explain-symlink");
let outside = root.at.with_extension("outside");
std::fs::write(&outside, b"a secret outside the root\n").expect("the target writes");
let path = "docs/decisions/9998-link.md";
std::os::unix::fs::symlink(&outside, root.at.join(path)).expect("the link is made");
for args in [&["explain", path][..], &["explain", path, "--json"]] {
let explained = root.run(args);
let stderr = String::from_utf8_lossy(&explained.stderr)
.split_whitespace()
.collect::<Vec<_>>()
.join(" ");
assert_eq!(
explained.status.code(),
Some(1),
"{args:?} refuses a symlink row: {stderr}"
);
assert!(
explained.stdout.is_empty(),
"{args:?} prints nothing of a symlink row: {:?}",
String::from_utf8_lossy(&explained.stdout)
);
assert!(
stderr.contains("is a symlink to") && stderr.contains("so `explain` prints nothing"),
"{args:?} names the row and why: {stderr}"
);
}
let _ = std::fs::remove_file(&outside);
let untyped = "docs/decisions/9997-untyped.md";
std::fs::write(root.at.join(untyped), "# No front matter\n").expect("the file writes");
for args in [&["explain", untyped][..], &["explain", untyped, "--json"]] {
let explained = root.run(args);
assert_eq!(
explained.status.code(),
Some(0),
"{args:?} explains an untyped document: {}",
String::from_utf8_lossy(&explained.stderr)
);
assert!(
String::from_utf8_lossy(&explained.stdout).contains(untyped),
"{args:?} names the untyped document: {:?}",
String::from_utf8_lossy(&explained.stdout)
);
}
}
#[test]
fn a_bare_show_names_what_it_takes() {
let root = Root::new("bare");
let shown = root.run(&["show"]);
assert_eq!(shown.status.code(), Some(1));
assert!(shown.stdout.is_empty());
assert!(
String::from_utf8_lossy(&shown.stderr).contains("`show` takes a path or an identifier"),
"{}",
String::from_utf8_lossy(&shown.stderr)
);
}
fn repoint_bundles(package: &Path) {
let up = "../".repeat(headwater_resolve::package::PACKAGES.split('/').count() + 1);
let manifest = package.join(headwater_resolve::package::MANIFEST);
let text = std::fs::read_to_string(&manifest).expect("the scratch manifest reads");
let from = " bundles: ../../docs/taxonomies";
assert!(text.contains(from), "the authored manifest states `{from}`");
let to = format!(" bundles: {up}docs/taxonomies");
std::fs::write(&manifest, text.replace(from, &to)).expect("the scratch manifest writes");
}
type Run = fn(&Root, &[&str]) -> Output;
const RUNS: [(&str, Run); 2] = [
("--root <absolute>", Root::run),
("--root . from inside", Root::run_inside),
];
#[test]
fn explain_and_show_resolve_every_spelling_of_a_path_inside_the_repository() {
let root = Root::new("spellings");
let on_disk = std::fs::read(root.at.join(DOCUMENT)).expect("the document reads");
let plain = root.run(&["explain", DOCUMENT]);
assert_eq!(plain.status.code(), Some(0), "the plain path resolves");
let spellings = [
format!("./{DOCUMENT}"),
format!("docs/../{DOCUMENT}"),
root.at.join(DOCUMENT).display().to_string(),
];
for target in &spellings {
for (how, run) in RUNS {
let explained = run(&root, &["explain", target]);
assert_eq!(
explained.status.code(),
Some(0),
"`explain {target}` with {how}: {}",
String::from_utf8_lossy(&explained.stderr)
);
assert_eq!(
String::from_utf8_lossy(&explained.stdout),
String::from_utf8_lossy(&plain.stdout),
"`explain {target}` with {how} explains the document the plain path names"
);
let shown = run(&root, &["show", target]);
assert_eq!(
shown.status.code(),
Some(0),
"`show {target}` with {how}: {}",
String::from_utf8_lossy(&shown.stderr)
);
assert!(
shown.stdout == on_disk,
"`show {target}` with {how} writes the bytes on disk"
);
}
}
}
#[test]
fn explain_and_show_refuse_a_path_outside_the_repository_as_outside_it() {
let root = Root::new("outside");
let beside = root
.at
.parent()
.expect("the root has a parent")
.join("outside.md")
.display()
.to_string();
for target in ["/etc/passwd", "../../outside.md", beside.as_str()] {
for (how, run) in RUNS {
for arguments in [
vec!["explain", target],
vec!["explain", target, "--json"],
vec!["show", target],
] {
let refused = run(&root, &arguments);
let asked = arguments.join(" ");
let stderr = String::from_utf8_lossy(&refused.stderr);
assert_eq!(
refused.status.code(),
Some(1),
"`{asked}` with {how} refuses: {stderr}"
);
assert!(
refused.stdout.is_empty(),
"`{asked}` with {how} writes nothing on standard output"
);
assert!(
stderr.contains("outside this repository"),
"`{asked}` with {how} says it is outside this repository: {stderr}"
);
assert!(
!stderr.contains("no document written there"),
"`{asked}` with {how} is not read as a corpus path: {stderr}"
);
}
}
}
}
#[cfg(unix)]
#[test]
fn every_path_route_refuses_a_path_outside_the_repository_in_one_sentence() {
use std::io::Write;
let root = Root::new("one-sentence");
let elsewhere = root.at.with_extension("elsewhere");
let _ = std::fs::remove_dir_all(&elsewhere);
std::fs::create_dir_all(&elsewhere).expect("the outside directory is made");
std::fs::write(elsewhere.join("x.md"), b"a file outside the root\n")
.expect("the outside file writes");
std::os::unix::fs::symlink(&elsewhere, root.at.join("escape")).expect("the link is made");
let nowhere = root.at.with_extension("nowhere");
let links = root.at.with_extension("o");
let _ = std::fs::remove_dir_all(&nowhere);
let _ = std::fs::remove_dir_all(&links);
std::fs::create_dir_all(&links).expect("the links directory is made");
std::os::unix::fs::symlink(nowhere.join("dir"), root.at.join("docs/dang"))
.expect("the dangling link is made");
std::os::unix::fs::symlink(root.at.join("docs"), links.join("Docs"))
.expect("the link into the root is made");
let through = links.join("Docs/dang/new.md").display().to_string();
let targets = [
"escape/x.md",
"./escape/x.md",
"/etc/passwd",
"../outside.md",
"docs/dang/new.md",
through.as_str(),
];
let mut failures: Vec<String> = Vec::new();
for target in targets {
let sentence = headwater_query::outside_text(target);
assert_eq!(
sentence,
format!("`{target}` is outside this repository, or is not a path it can read"),
"the shared sentence is the documented one"
);
for (how, run) in RUNS {
for verb in ["explain", "show"] {
let refused = run(&root, &[verb, target]);
let stderr = String::from_utf8_lossy(&refused.stderr).into_owned();
if refused.status.code() != Some(1)
|| !refused.stdout.is_empty()
|| stderr != format!("headwater: {sentence}\n")
{
failures.push(format!("`{verb} {target}` with {how}: {stderr:?}"));
}
}
}
let mut input = String::from(
"{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{}}\n",
);
let tools = [
("explain", "target"),
("related", "target"),
("governing_docs_for_path", "path"),
];
for (at, (tool, key)) in tools.iter().enumerate() {
input.push_str(&format!(
"{{\"jsonrpc\":\"2.0\",\"id\":{},\"method\":\"tools/call\",\"params\":\
{{\"name\":\"{tool}\",\"arguments\":{{\"{key}\":\"{target}\"}}}}}}\n",
at + 2
));
}
let mut child = Command::new(env!("CARGO_BIN_EXE_headwater"))
.args(["mcp", "--now", "2026-09-28", "--root"])
.arg(&root.at)
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::null())
.spawn()
.expect("the binary runs");
child
.stdin
.take()
.expect("the standard input is piped")
.write_all(input.as_bytes())
.expect("the requests write");
let served = child.wait_with_output().expect("the server ends");
let stdout = String::from_utf8_lossy(&served.stdout).into_owned();
for (at, (tool, _)) in tools.iter().enumerate() {
let id = format!("\"id\":{},", at + 2);
let line = stdout.lines().find(|line| line.contains(&id)).unwrap_or("");
let text = line
.split("\"text\":\"")
.nth(1)
.and_then(|rest| rest.split('"').next())
.unwrap_or("")
.replace("\\n", "\n");
if text != format!("{sentence}\n") {
failures.push(format!("MCP `{tool} {target}`: {text:?}"));
}
}
}
let _ = std::fs::remove_dir_all(&elsewhere);
let _ = std::fs::remove_dir_all(&links);
assert!(
failures.is_empty(),
"each route refuses in the one sentence:\n{}",
failures.join("\n")
);
}
#[test]
fn a_dot_slash_spelling_of_an_identifier_is_a_path_and_is_refused_as_one() {
let root = Root::new("dot-identifier");
let target = format!("./{IDENTIFIER}");
for (how, run) in RUNS {
for verb in ["explain", "show"] {
let refused = run(&root, &[verb, &target]);
let stderr = String::from_utf8_lossy(&refused.stderr);
assert_eq!(
refused.status.code(),
Some(1),
"`{verb} {target}` with {how} is not the identifier: {stderr}"
);
assert!(refused.stdout.is_empty(), "`{verb} {target}` with {how}");
assert!(
stderr.contains("is outside every corpus root this repository declares"),
"`{verb} {target}` with {how} reads as a path: {stderr}"
);
}
}
}
#[test]
fn an_absolute_path_with_no_file_under_a_relative_root_is_a_path_of_this_corpus() {
let root = Root::new("absolute-missing");
let target = root
.at
.join("docs/decisions/0002-not-written.md")
.display()
.to_string();
let refused = root.run_inside(&["explain", &target]);
let stderr = String::from_utf8_lossy(&refused.stderr);
assert_eq!(refused.status.code(), Some(1), "{stderr}");
assert!(
stderr.contains("is a path of this corpus, with no document written there yet"),
"`explain {target}` with `--root .` reads as a corpus path: {stderr}"
);
}
#[cfg(unix)]
#[test]
fn an_absolute_target_finds_its_document_under_a_root_reached_through_a_symlink() {
let root = Root::new("linked-root");
let link = root.at.with_file_name(format!(
"{}-link",
root.at
.file_name()
.expect("the root has a name")
.to_string_lossy()
));
let _ = std::fs::remove_file(&link);
std::os::unix::fs::symlink(&root.at, &link).expect("the link is made");
let target = root.at.join(DOCUMENT).display().to_string();
let shown = Command::new(env!("CARGO_BIN_EXE_headwater"))
.args(["show", &target, "--root"])
.arg(&link)
.output()
.expect("the binary runs");
let _ = std::fs::remove_file(&link);
assert_eq!(
shown.status.code(),
Some(0),
"`show {target}` under a linked root: {}",
String::from_utf8_lossy(&shown.stderr)
);
assert!(shown.stdout == document(), "the bytes on disk");
}
#[cfg(unix)]
#[test]
fn an_absolute_path_through_a_link_into_the_root_finds_its_document() {
let root = Root::new("link-into");
let link = root.at.with_file_name(format!(
"{}-decisions",
root.at
.file_name()
.expect("the root has a name")
.to_string_lossy()
));
let _ = std::fs::remove_file(&link);
std::os::unix::fs::symlink(root.at.join("docs/decisions"), &link).expect("the link is made");
let present = Path::new(DOCUMENT)
.file_name()
.expect("the document has a name");
let target = link.join(present).display().to_string();
let shown = root.run(&["show", &target]);
let missing = link.join("0002-not-written.md").display().to_string();
let refused = root.run(&["explain", &missing]);
let _ = std::fs::remove_file(&link);
assert_eq!(
shown.status.code(),
Some(0),
"`show {target}`: {}",
String::from_utf8_lossy(&shown.stderr)
);
assert!(shown.stdout == document(), "the bytes on disk");
let stderr = String::from_utf8_lossy(&refused.stderr);
assert_eq!(refused.status.code(), Some(1), "{stderr}");
assert!(
stderr.contains("is a path of this corpus, with no document written there yet"),
"`explain {missing}` through a link into the root: {stderr}"
);
}
#[cfg(unix)]
#[test]
fn a_link_into_the_root_finds_its_document_under_a_linked_or_relative_root() {
let root = Root::new("link-into-roots");
let name = root
.at
.file_name()
.expect("the root has a name")
.to_string_lossy()
.to_string();
let into = root.at.with_file_name(format!("{name}-decisions"));
let via = root.at.with_file_name(format!("{name}-via"));
let _ = std::fs::remove_file(&into);
let _ = std::fs::remove_file(&via);
std::os::unix::fs::symlink(root.at.join("docs/decisions"), &into).expect("the link in");
std::os::unix::fs::symlink(&root.at, &via).expect("the linked root");
let present = Path::new(DOCUMENT)
.file_name()
.expect("the document has a name");
let target = into.join(present).display().to_string();
let missing = into.join("0002-not-written.md").display().to_string();
let under_link = |verb: &str, path: &str| {
Command::new(env!("CARGO_BIN_EXE_headwater"))
.args([verb, path, "--root"])
.arg(&via)
.output()
.expect("the binary runs")
};
let under_parent = |verb: &str, path: &str| {
Command::new(env!("CARGO_BIN_EXE_headwater"))
.args([verb, path, "--root", ".."])
.current_dir(root.at.join("docs"))
.output()
.expect("the binary runs")
};
let shown = [
("--root <link>", under_link("show", &target)),
("--root ..", under_parent("show", &target)),
];
let refused = [
("--root <link>", under_link("explain", &missing)),
("--root ..", under_parent("explain", &missing)),
];
let _ = std::fs::remove_file(&into);
let _ = std::fs::remove_file(&via);
for (how, shown) in &shown {
assert_eq!(
shown.status.code(),
Some(0),
"`show {target}` with {how}: {}",
String::from_utf8_lossy(&shown.stderr)
);
assert!(shown.stdout == document(), "the bytes on disk, with {how}");
}
for (how, refused) in &refused {
let stderr = String::from_utf8_lossy(&refused.stderr);
assert_eq!(refused.status.code(), Some(1), "with {how}: {stderr}");
assert!(
stderr.contains("is a path of this corpus, with no document written there yet"),
"`explain {missing}` with {how}: {stderr}"
);
}
}
#[test]
fn an_absolute_path_that_leaves_the_root_and_comes_back_finds_its_document() {
let root = Root::new("back-in");
let name = root.at.file_name().expect("the root has a name");
let target = root
.at
.join("..")
.join(name)
.join(DOCUMENT)
.display()
.to_string();
let shown = root.run(&["show", &target]);
assert_eq!(
shown.status.code(),
Some(0),
"`show {target}`: {}",
String::from_utf8_lossy(&shown.stderr)
);
assert!(shown.stdout == document(), "the bytes on disk");
}
#[cfg(unix)]
#[test]
fn an_absolute_path_with_no_file_typed_through_a_linked_root_is_a_path_of_this_corpus() {
let root = Root::new("linked-missing");
let link = root.at.with_file_name(format!(
"{}-link",
root.at
.file_name()
.expect("the root has a name")
.to_string_lossy()
));
let _ = std::fs::remove_file(&link);
std::os::unix::fs::symlink(&root.at, &link).expect("the link is made");
let target = link
.join("docs/decisions/0002-not-written.md")
.display()
.to_string();
let refused = Command::new(env!("CARGO_BIN_EXE_headwater"))
.args(["explain", &target, "--root", "."])
.current_dir(&link)
.output()
.expect("the binary runs");
let _ = std::fs::remove_file(&link);
let stderr = String::from_utf8_lossy(&refused.stderr);
assert_eq!(refused.status.code(), Some(1), "{stderr}");
assert!(refused.stdout.is_empty(), "nothing on stdout: {stderr}");
assert!(
stderr.contains("is a path of this corpus, with no document written there yet"),
"`explain {target}` through a linked root reads as a corpus path: {stderr}"
);
}