use std::path::{Path, PathBuf};
use std::process::Command;
fn repository() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../..")
.canonicalize()
.expect("the repository root resolves")
}
const DECLARED: &str = "
identifier_schemes.solution_note_id:
pattern: \"{namespace}-SOL-{slug}\"
namespace: HW
allocation: minted-once
kinds.solution_note:
is_a: governed_document
purpose: rationale
voice: declarative
lifecycle: standard
language: ste_house
identifier: {scheme: solution_note_id}
shelves.solution_notes:
title: The solution notes
path: docs/solution/*.md
homogeneous: true
kind: solution_note
anchors.service_in_a: {resolver: export-repo-a}
anchors.service_in_b: {resolver: export-repo-b}
relations.uses_service_in_a:
family: association
from: [solution_note]
to: [service_in_a]
cardinality: many
created_by: author
relations.uses_service_in_b:
family: association
from: [solution_note]
to: [service_in_b]
cardinality: many
created_by: author
";
const NOTE: &str = "\
---
id: HW-SOL-checkout
title: The checkout path
summary: The checkout path calls one service in each source repository.
status: current
status_since: 2026-09-01
last_verified: 2026-09-01
relations:
uses_service_in_a:
- SVC-1
uses_service_in_b:
- SVC-1
---
# The checkout path
The checkout path calls one service in each repository.
";
fn export(tier: &str) -> String {
format!(
"{{\"version\":\"0.4.0\",\"export_version\":\"0.4.0\",\
\"profile\":{{\"name\":\"full\",\"target\":\"native\",\"filtered\":false}},\
\"graph\":{{\"documents\":[{{\"path\":\"docs/services/svc-1.md\",\"kind\":\"service\",\
\"id\":\"SVC-1\",\"facets\":{{\"tier\":\"{tier}\"}}}}],\"anchors\":[],\"edges\":[]}}}}\n"
)
}
const EXPORT_A: &str = "harvest/repo-a.json";
const EXPORT_B: &str = "harvest/repo-b.json";
struct Root {
at: PathBuf,
}
impl Root {
fn new(label: &str) -> Root {
let at = std::env::temp_dir().join(format!(
"headwater-cli-harvest-{}-{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(headwater_resolve::package::PACKAGES),
&at.join(headwater_resolve::package::PACKAGES),
);
copy(
&repository.join("docs/taxonomies"),
&at.join("docs/taxonomies"),
);
let overlay = std::fs::read_to_string(repository.join(".headwater/overlay.yml"))
.expect("the overlay reads");
assert!(
overlay.contains("\nadd:\n"),
"the overlay opens its additions with `add:`, where the scratch kinds go"
);
std::fs::write(
at.join(".headwater/overlay.yml"),
overlay.replacen("\nadd:\n", &format!("\nadd:\n{DECLARED}\n"), 1),
)
.expect("the overlay writes");
let root = Root { at };
root.write("docs/solution/checkout.md", NOTE);
root.write(EXPORT_A, &export("gold"));
root.write(EXPORT_B, &export("bronze"));
for stub in [
"README.md",
".github/CONTRIBUTING.md",
".github/ISSUE_TEMPLATE/issue.md",
".github/SECURITY.md",
] {
root.write(stub, "# Scratch\n\nThis file is a stub.\n");
}
let consumer = std::fs::read_to_string(repository.join(".headwater/taxonomy.yml"))
.expect("the declaration reads");
let pins = format!(
"\nharvests:\n repo-a:\n at: {EXPORT_A}\n digest: {}\n channel: the tier's nightly harvest job\n resolver: export-repo-a\n repo-b:\n at: {EXPORT_B}\n digest: {}\n channel: the tier's nightly harvest job\n resolver: export-repo-b\n",
root.digest(EXPORT_A),
root.digest(EXPORT_B),
);
root.write(".headwater/taxonomy.yml", &format!("{consumer}{pins}"));
let resolved = root.run(&["taxonomy", "resolve"]);
assert_eq!(
resolved.code,
Some(0),
"the fixture resolves\n{}{}",
resolved.out,
resolved.err
);
root
}
fn write(&self, relative: &str, text: &str) {
let path = self.at.join(relative);
std::fs::create_dir_all(path.parent().expect("it has a parent"))
.expect("the directory is made");
std::fs::write(path, text).expect("the file writes");
}
fn digest(&self, relative: &str) -> String {
headwater_hash::digest(&std::fs::read(self.at.join(relative)).expect("the export reads"))
}
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(),
}
}
fn errors(ran: &Ran) -> Vec<String> {
Root::errors_on(ran, "docs/solution/checkout.md")
}
fn errors_on(ran: &Ran, path: &str) -> Vec<String> {
let header = format!(" {path}");
let mut out: Vec<String> = Vec::new();
let mut open = false;
for line in ran.out.lines() {
if line.starts_with(" ") && !line.starts_with(" ") {
open = line.starts_with(&header) && line.contains("error");
if open {
out.push(line.to_string());
}
} else if open && line.starts_with(" ") {
let last = out.last_mut().expect("a block is open");
last.push(' ');
last.push_str(line.trim());
} else {
open = false;
}
}
out
}
}
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,
}
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");
}
}
}
}
fn unresolved(ran: &Ran) -> Vec<String> {
Root::errors(ran)
.into_iter()
.filter(|block| block.contains("relation.target.unresolved"))
.collect()
}
#[test]
fn an_anchor_into_each_pinned_export_binds_there() {
let root = Root::new("both");
let ran = root.run(&["check", "--strict"]);
assert_eq!(Root::errors(&ran), Vec::<String>::new(), "{ran:?}");
assert_eq!(ran.code, Some(0), "{ran:?}");
assert!(!ran.out.contains("unbound"), "both edges bind: {ran:?}");
}
#[test]
fn a_missing_export_is_a_finding_that_names_its_pin_and_never_a_lookup_elsewhere() {
let root = Root::new("missing");
let warm = root.run(&["check", "--strict"]);
assert_eq!(warm.code, Some(0), "the cache is populated: {warm:?}");
std::fs::remove_file(root.at.join(EXPORT_B)).expect("B's export is removed");
let ran = root.run(&["check", "--strict"]);
assert_ne!(ran.code, Some(0), "the strict gate fails: {ran:?}");
assert!(
!ran.err.starts_with("0 served from cache"),
"the second run reads a warm cache, so the verdict that moved is one a key moved: {}",
ran.err
);
let unresolved = unresolved(&ran);
assert_eq!(unresolved.len(), 1, "only the edge into B: {unresolved:?}");
let finding = &unresolved[0];
assert!(finding.contains("uses_service_in_b"), "{finding}");
assert!(
finding.contains("`repo-b`") && finding.contains(EXPORT_B),
"the finding names B's pin and where it is: {finding}"
);
assert!(
!finding.contains("repo-a") && !finding.contains("which this run does not have"),
"the finding names B's pin, not A's and not a missing resolver: {finding}"
);
assert_eq!(
Root::errors(&ran).len(),
1,
"nothing else is wrong: {ran:?}"
);
}
#[test]
fn an_export_that_is_not_the_pinned_artifact_binds_nothing_and_names_the_pin() {
let root = Root::new("moved");
let warm = root.run(&["check", "--strict"]);
assert_eq!(warm.code, Some(0), "the cache is populated: {warm:?}");
root.write(EXPORT_B, &export("silver"));
let ran = root.run(&["check", "--strict"]);
assert_ne!(ran.code, Some(0), "the strict gate fails: {ran:?}");
let unresolved = unresolved(&ran);
assert_eq!(unresolved.len(), 1, "only the edge into B: {unresolved:?}");
let finding = &unresolved[0];
assert!(finding.contains("uses_service_in_b"), "{finding}");
assert!(
finding.contains("`repo-b`") && finding.contains("not the pinned artifact"),
"the finding names the pin and says the bytes moved: {finding}"
);
assert!(!finding.contains("repo-a"), "{finding}");
}
#[test]
fn an_identifier_only_the_other_export_holds_is_unresolved() {
let root = Root::new("elsewhere");
let pinned = std::fs::read_to_string(root.at.join(".headwater/taxonomy.yml"))
.expect("the declaration reads");
let before = root.digest(EXPORT_A);
root.write(EXPORT_A, &export("gold").replace("\"SVC-1\"", "\"SVC-2\""));
root.write(
".headwater/taxonomy.yml",
&pinned.replace(&before, &root.digest(EXPORT_A)),
);
root.write(
"docs/solution/checkout.md",
&NOTE.replace(
" uses_service_in_b:\n - SVC-1",
" uses_service_in_b:\n - SVC-2",
),
);
let ran = root.run(&["check", "--strict"]);
let unresolved = unresolved(&ran);
assert_eq!(unresolved.len(), 2, "{unresolved:?}");
assert!(
unresolved
.iter()
.any(|finding| finding.contains("uses_service_in_b")
&& finding.contains("`repo-b`")
&& finding.contains("SVC-2")),
"{unresolved:?}"
);
}
#[test]
fn an_export_this_binary_wrote_is_one_the_resolver_reads() {
let root = Root::new("real");
let exported = root.run(&["export", "--format", "json"]);
assert_eq!(exported.code, Some(0), "{exported:?}");
assert!(exported.out.contains("\"HW-SOL-checkout\""), "{exported:?}");
let pinned = std::fs::read_to_string(root.at.join(".headwater/taxonomy.yml"))
.expect("the declaration reads");
let before = root.digest(EXPORT_A);
root.write(EXPORT_A, &exported.out);
root.write(
".headwater/taxonomy.yml",
&pinned.replace(&before, &root.digest(EXPORT_A)),
);
root.write(
"docs/solution/checkout.md",
&NOTE.replace(
" uses_service_in_a:\n - SVC-1",
" uses_service_in_a:\n - HW-SOL-checkout",
),
);
let ran = root.run(&["check", "--strict"]);
assert_eq!(unresolved(&ran), Vec::<String>::new(), "{ran:?}");
assert_eq!(ran.code, Some(0), "{ran:?}");
}
fn declare(root: &Root, from: &str, to: &str) {
let declared = std::fs::read_to_string(root.at.join(".headwater/taxonomy.yml"))
.expect("the declaration reads");
assert_eq!(declared.matches(from).count(), 1, "{declared}");
root.write(".headwater/taxonomy.yml", &declared.replacen(from, to, 1));
}
#[test]
fn two_pins_that_name_one_resolver_refuse_the_run() {
let root = Root::new("one-name");
declare(
&root,
" resolver: export-repo-b\n",
" resolver: export-repo-a\n",
);
let ran = root.run(&["check", "--strict"]);
assert_eq!(ran.code, Some(1), "{ran:?}");
assert!(
ran.err.contains("the resolver set is ambiguous") && ran.err.contains("export-repo-a"),
"the refusal names the resolver two pins share: {ran:?}"
);
assert!(
!ran.out.contains("docs/solution/checkout.md"),
"no report was written over a graph built from half the pins: {ran:?}"
);
}
#[test]
fn a_harvests_entry_that_does_not_read_refuses_the_run() {
let root = Root::new("unreadable");
declare(&root, " resolver: export-repo-b\n", "");
let ran = root.run(&["check", "--strict"]);
assert_eq!(ran.code, Some(1), "{ran:?}");
assert!(
ran.err
.contains("the pinned export declarations did not read")
&& ran.err.contains("harvests.repo-b"),
"the refusal names the entry: {ran:?}"
);
assert!(
!ran.out.contains("docs/solution/checkout.md"),
"no report was written: {ran:?}"
);
}
const PIN_RULE: &str = "harvest.pin.unread";
const EXPORT_C: &str = "harvest/repo-c.json";
#[test]
fn an_unread_pin_that_no_anchor_names_is_a_finding_that_names_the_pin() {
let root = Root::new("unread");
declare(
&root,
" resolver: export-repo-b\n",
&format!(
" resolver: export-repo-b\n repo-c:\n at: {EXPORT_C}\n digest: sha256:0000\n \
resolver: export-repo-c\n"
),
);
let ran = root.run(&["check", "--strict"]);
assert_ne!(ran.code, Some(0), "the strict gate fails: {ran:?}");
let pins: Vec<String> = Root::errors_on(&ran, ".headwater/taxonomy.yml")
.into_iter()
.filter(|block| block.contains(PIN_RULE))
.collect();
assert_eq!(pins.len(), 1, "one finding, for C alone: {ran:?}");
assert!(
pins[0].contains(&format!(
"`harvests.repo-c` pins an export at `{EXPORT_C}` that binds nothing"
)),
"the finding names the pin and where it is: {}",
pins[0]
);
assert!(
!pins[0].contains("`harvests.repo-a`") && !pins[0].contains("`harvests.repo-b`"),
"{}",
pins[0]
);
assert!(pins[0].contains("did not read"), "{}", pins[0]);
assert_eq!(
unresolved(&ran),
Vec::<String>::new(),
"A and B bind: {ran:?}"
);
root.write(EXPORT_C, &export("tin"));
declare(&root, "sha256:0000", &root.digest(EXPORT_C));
let ran = root.run(&["check", "--strict"]);
assert_eq!(
Root::errors_on(&ran, ".headwater/taxonomy.yml"),
Vec::<String>::new(),
"{ran:?}"
);
assert_eq!(ran.code, Some(0), "{ran:?}");
}
#[test]
fn a_pinned_export_joins_the_read_set_and_a_gate_sees_it_move() {
let root = Root::new("read-set");
let read_set = root.at.join("clean.readset");
let read_set = read_set.to_str().expect("the read set path is UTF-8");
let ran = root.run(&["check", "--read-set", read_set]);
assert_eq!(ran.code, Some(0), "{ran:?}");
let recorded = std::fs::read_to_string(read_set).expect("the read set reads");
for (export, digest) in [
(EXPORT_A, root.digest(EXPORT_A)),
(EXPORT_B, root.digest(EXPORT_B)),
] {
assert!(
recorded
.lines()
.any(|line| line.contains(export) && line.contains(&digest)),
"the read set lists `{export}` at {digest}: {recorded}"
);
}
let still = root.run(&["gate", "--read-set", read_set]);
assert!(!still.out.contains(EXPORT_B), "{still:?}");
root.write(EXPORT_B, &export("silver"));
let moved = root.run(&["gate", "--read-set", read_set]);
assert!(
moved.out.contains(EXPORT_B),
"the gate names the export that moved: {moved:?}"
);
}
#[test]
fn an_absent_pinned_export_joins_the_read_set_and_a_gate_never_carries_across_it() {
let root = Root::new("read-set-absent");
let pinned = export("tin");
declare(
&root,
" resolver: export-repo-b\n",
&format!(
" resolver: export-repo-b\n repo-c:\n at: {EXPORT_C}\n digest: {}\n \
resolver: export-repo-c\n",
headwater_hash::digest(pinned.as_bytes())
),
);
let read_set = root.at.join("absent.readset");
let read_set = read_set.to_str().expect("the read set path is UTF-8");
let ran = root.run(&["check", "--read-set", read_set]);
let recorded = std::fs::read_to_string(read_set)
.unwrap_or_else(|error| panic!("the read set is written: {error}: {ran:?}"));
assert!(
recorded.lines().any(|line| line.contains(EXPORT_C)),
"the read set lists the absent export: {recorded}"
);
let unhashed = format!("{EXPORT_C} carried no hash when it was read");
let still = root.run(&["gate", "--read-set", read_set]);
assert!(still.out.contains(&unhashed), "{still:?}");
root.write(EXPORT_C, &pinned);
let appeared = root.run(&["gate", "--read-set", read_set]);
assert!(
appeared.out.contains(&unhashed),
"the gate names the export that appeared: {appeared:?}"
);
}
#[test]
fn a_relative_root_holds_every_pin_inside_it() {
let root = Root::new("relative");
let output = Command::new(env!("CARGO_BIN_EXE_headwater"))
.args(["check", "--strict", "--root", "."])
.current_dir(&root.at)
.output()
.expect("the binary runs");
let ran = Ran {
code: output.status.code(),
out: String::from_utf8_lossy(&output.stdout).into_owned(),
err: String::from_utf8_lossy(&output.stderr).into_owned(),
};
assert_eq!(ran.code, Some(0), "{ran:?}");
assert_eq!(Root::errors(&ran), Vec::<String>::new(), "{ran:?}");
}
#[test]
fn every_unread_pin_is_its_own_finding() {
let root = Root::new("two-unread");
std::fs::remove_file(root.at.join(EXPORT_A)).expect("A's export is removed");
std::fs::remove_file(root.at.join(EXPORT_B)).expect("B's export is removed");
let ran = root.run(&["check", "--strict"]);
let pins: Vec<String> = Root::errors_on(&ran, ".headwater/taxonomy.yml")
.into_iter()
.filter(|block| block.contains(PIN_RULE))
.collect();
assert_eq!(pins.len(), 2, "{ran:?}");
for (pin, at) in [("repo-a", EXPORT_A), ("repo-b", EXPORT_B)] {
let head = format!("`harvests.{pin}` pins an export at `{at}` that binds nothing");
assert!(
pins.iter().any(|block| block.contains(&head)),
"{head}: {pins:?}"
);
}
}
#[test]
fn infer_sees_an_unread_pin() {
let root = Root::new("infer");
std::fs::remove_file(root.at.join(EXPORT_B)).expect("B's export is removed");
let ran = root.run(&["infer", "--owner", "tier-team"]);
assert_eq!(ran.code, Some(0), "{ran:?}");
assert!(ran.out.contains(PIN_RULE), "{ran:?}");
}
#[test]
fn a_pin_with_no_digest_is_a_finding_that_says_what_to_write() {
let root = Root::new("undigested");
let digest = root.digest(EXPORT_B);
declare(&root, &format!(" digest: {digest}\n"), "");
let ran = root.run(&["check", "--strict"]);
assert_ne!(ran.code, Some(0), "{ran:?}");
let pins: Vec<String> = Root::errors_on(&ran, ".headwater/taxonomy.yml")
.into_iter()
.filter(|block| block.contains(PIN_RULE))
.collect();
assert_eq!(pins.len(), 1, "{ran:?}");
assert!(
pins[0].contains("repo-b") && pins[0].contains("harvests.repo-b.digest"),
"{}",
pins[0]
);
}
#[test]
fn a_harvests_block_that_is_not_a_mapping_refuses_the_run() {
let root = Root::new("list");
let declared = std::fs::read_to_string(root.at.join(".headwater/taxonomy.yml"))
.expect("the declaration reads");
let (head, _) = declared
.split_once("\nharvests:\n")
.expect("the fixture declares harvests");
root.write(
".headwater/taxonomy.yml",
&format!("{head}\nharvests:\n - repo-a\n - repo-b\n"),
);
let ran = root.run(&["check", "--strict"]);
assert_eq!(ran.code, Some(1), "{ran:?}");
assert!(
ran.err
.contains("the pinned export declarations did not read")
&& ran.err.contains("`harvests`")
&& ran.err.contains("a sequence"),
"{ran:?}"
);
}
#[cfg(unix)]
#[test]
fn a_pin_that_a_symlink_takes_out_of_the_root_refuses_the_run() {
let root = Root::new("symlink");
let outside = std::env::temp_dir().join(format!(
"headwater-cli-harvest-{}-symlink-outside",
std::process::id()
));
let _ = std::fs::remove_dir_all(&outside);
std::fs::create_dir_all(&outside).expect("the outside directory is made");
for export in [EXPORT_A, EXPORT_B] {
let name = Path::new(export).file_name().expect("it has a name");
std::fs::copy(root.at.join(export), outside.join(name)).expect("the export copies");
}
std::fs::remove_dir_all(root.at.join("harvest")).expect("the directory is removed");
std::os::unix::fs::symlink(&outside, root.at.join("harvest")).expect("the symlink is made");
let ran = root.run(&["check", "--strict"]);
let _ = std::fs::remove_dir_all(&outside);
assert_eq!(ran.code, Some(1), "{ran:?}");
assert!(
ran.err.contains("harvests.repo-a.at") && ran.err.contains(EXPORT_A),
"{ran:?}"
);
}