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 PAYLOAD: &str = concat!(
"\nadoption:\n",
" tasks:\n",
" - id: AD-9\n",
" statement: \"The debt this fixture declares\"\n",
" owner: \"a person\"\n",
" until: 2027-06-30\n",
" pairs:\n",
" - path: docs/spec/02-taxonomy-model.md\n",
" rule: language.retired_term.used\n",
);
struct Root {
at: PathBuf,
}
impl Root {
fn new(label: &str) -> Root {
let at = std::env::temp_dir().join(format!(
"headwater-cli-adoption-{}-{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"),
);
without_doctrine(&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");
}
for entry in [
"README.md",
".github/CONTRIBUTING.md",
".github/SECURITY.md",
".github/ISSUE_TEMPLATE/issue.md",
] {
let to = at.join(entry);
std::fs::create_dir_all(to.parent().expect("it has a parent"))
.expect("the directory is there");
std::fs::write(to, "# A stub\n\nThis file stands in for the real one.\n")
.expect("the stub writes");
}
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 lock(&self) -> PathBuf {
self.at.join(".headwater/taxonomy.lock")
}
fn text(&self) -> String {
std::fs::read_to_string(self.lock()).expect("the lock reads")
}
fn write(&self, text: &str) {
std::fs::write(self.lock(), text).expect("the lock writes");
}
fn author(&self) {
self.author_as("AD-9");
}
fn author_as(&self, id: &str) {
self.author_until(id, "2027-06-30");
}
fn author_until(&self, id: &str, until: &str) {
let text = self.text();
assert!(
!text.contains("\nadoption:\n"),
"the fixture starts with no authored block"
);
let payload = PAYLOAD.replace("AD-9", id).replace("2027-06-30", until);
self.write(&text.replacen("\nresolved:\n", &format!("{payload}\nresolved:\n"), 1));
}
fn author_pair(&self, id: &str, until: &str, path: &str, rule: &str) {
let text = self.text();
assert!(
!text.contains("\nadoption:\n"),
"the fixture starts with no authored block"
);
let payload = PAYLOAD
.replace("AD-9", id)
.replace("2027-06-30", until)
.replace("docs/spec/02-taxonomy-model.md", path)
.replace("language.retired_term.used", rule);
self.write(&text.replacen("\nresolved:\n", &format!("{payload}\nresolved:\n"), 1));
}
fn document(&self) -> String {
let at = std::fs::read_dir(self.at.join("docs/spec"))
.expect("the spec shelf is there")
.map(|entry| entry.expect("the entry reads").path())
.next()
.expect("the corpus was built first");
let name = at.file_name().expect("the document has a name");
format!("docs/spec/{}", name.to_string_lossy())
}
fn store(&self) -> Vec<String> {
match std::fs::read_to_string(self.at.join(".headwater/adoption.jsonl")) {
Err(_) => vec![],
Ok(text) => text
.lines()
.filter(|line| !line.trim().is_empty())
.map(str::to_string)
.collect(),
}
}
fn corpus(&self) {
let made = self.run(&["new", "design_spec", "--title", "A scratch part"]);
assert_eq!(
made.code,
Some(0),
"the corpus document scaffolds\n{}{}",
made.out,
made.err
);
let at = std::fs::read_dir(self.at.join("docs/spec"))
.expect("the spec shelf is there")
.map(|entry| entry.expect("the entry reads").path())
.next()
.expect("the scaffolder wrote a document");
let mut text = std::fs::read_to_string(&at).expect("the document reads");
text.push_str(
"\n## A scratch section\n\nThis document is scratch and it does not meet the \
controlled language, because the sentence is deliberately long enough to pass the \
twenty five word limit that the regime sets for a sentence of running prose.\n",
);
std::fs::write(&at, text).expect("the document 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,
}
fn without_doctrine(library: &Path) {
let index = library.join("README.md");
if index.is_file() {
std::fs::remove_file(&index).expect("the library index is removed");
}
for entry in std::fs::read_dir(library).expect("the copied library reads") {
let doctrine = entry.expect("the entry reads").path().join("doctrine.md");
if doctrine.is_file() {
std::fs::remove_file(&doctrine).expect("the doctrine page is removed");
}
}
}
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 a_lock_whose_digest_does_not_match_keeps_its_authored_block() {
let root = Root::new("digest-does-not-match");
root.author();
let before = root.text();
let corrupted = before.replacen("digest: sha256:", "digest: sha256:0000", 1);
assert_ne!(corrupted, before, "the digest line is there to corrupt");
root.write(&corrupted);
let resolved = root.run(&["taxonomy", "resolve"]);
assert_eq!(
resolved.code,
Some(0),
"the printed remedy runs\n{}{}",
resolved.out,
resolved.err
);
let after = root.text();
assert!(
after.contains("id: AD-9")
&& after.contains("owner: \"a person\"")
&& after.contains("until: 2027-06-30"),
"the authored block survived a resolve over a lock that did not read:\n{after}"
);
assert!(
resolved.out.contains("adoption"),
"standard output says what became of the authored block:\n{}",
resolved.out
);
}
#[test]
fn a_lock_that_does_not_parse_stops_a_resolve() {
let root = Root::new("does-not-parse");
root.author();
let broken = format!("{}\n\t- this is not a lock\n", root.text());
root.write(&broken);
let resolved = root.run(&["taxonomy", "resolve"]);
assert_ne!(
resolved.code,
Some(0),
"a lock this engine cannot read stops a rewrite\n{}{}",
resolved.out,
resolved.err
);
assert_eq!(root.text(), broken, "the file the run refused is untouched");
assert!(
resolved.err.contains("adoption"),
"the refusal names what a rewrite would have discarded:\n{}",
resolved.err
);
}
#[test]
fn a_first_resolve_with_no_lock_writes_one() {
let root = Root::new("no-lock-at-all");
std::fs::remove_file(root.lock()).expect("the lock is removed");
let resolved = root.run(&["taxonomy", "resolve"]);
assert_eq!(
resolved.code,
Some(0),
"a first resolve writes a lock\n{}{}",
resolved.out,
resolved.err
);
assert!(root.lock().exists(), "the lock is there");
assert!(
resolved.out.contains("adoption"),
"standard output says there was no block to carry:\n{}",
resolved.out
);
}
#[test]
fn a_lock_a_newer_engine_wrote_stops_a_resolve() {
let root = Root::new("newer-format");
root.author();
let ahead = root.text().replacen(" format: 3\n", " format: 4\n", 1);
assert!(ahead.contains("format: 4"), "the format line moved");
root.write(&ahead);
let resolved = root.run(&["taxonomy", "resolve"]);
assert_ne!(
resolved.code,
Some(0),
"an older engine does not rewrite a newer lock\n{}{}",
resolved.out,
resolved.err
);
assert_eq!(root.text(), ahead, "the file the run refused is untouched");
}
fn ids(text: &str) -> Vec<String> {
let block = match text.split_once("\nadoption:\n") {
Some((_, rest)) => rest.split("\nresolved:\n").next().unwrap_or_default(),
None => return Vec::new(),
};
block
.lines()
.filter_map(|line| line.trim().strip_prefix("- id: "))
.map(|id| id.trim().to_string())
.collect()
}
const FAR: &str = "2035-01-01";
#[test]
fn an_infer_write_keeps_every_task_the_lock_already_declared() {
let root = Root::new("infer-keeps-the-task");
root.author();
root.corpus();
let ran = root.run(&[
"infer",
"--owner",
"a parent test",
"--until",
FAR,
"--write",
]);
assert_eq!(
ran.code,
Some(0),
"the documented remedy runs\n{}{}",
ran.out,
ran.err
);
let after = root.text();
assert!(
after.contains("id: AD-9")
&& after.contains("owner: \"a person\"")
&& after.contains("until: 2027-06-30"),
"the task the lock declared survived `infer --write`:\n{after}"
);
assert!(
after.contains("owner: \"a parent test\""),
"the new payload is in the lock as well:\n{after}"
);
assert!(
ran.out
.contains("carried the adoption block through, 1 task"),
"standard output says what became of the authored block:\n{}",
ran.out
);
}
#[test]
fn the_identifier_a_run_mints_is_one_no_declared_task_holds() {
let root = Root::new("identifier-not-taken");
root.author_as("AD-1");
root.corpus();
let proposed = root.run(&["infer", "--until", FAR]);
assert_eq!(
proposed.code,
Some(0),
"the proposal runs\n{}{}",
proposed.out,
proposed.err
);
assert!(
proposed
.out
.contains("the payload, which --write puts in the lock"),
"the corpus of this fixture raises a finding to declare:\n{}",
proposed.out
);
assert!(
!proposed.out.contains("- id: AD-1\n"),
"the proposal mints no identifier the lock already declares:\n{}",
proposed.out
);
let ran = root.run(&[
"infer",
"--owner",
"a parent test",
"--until",
FAR,
"--write",
]);
assert_eq!(ran.code, Some(0), "the write runs\n{}{}", ran.out, ran.err);
let after = ids(&root.text());
assert_eq!(
after.len(),
2,
"the write added a task beside the declared one: {after:?}"
);
assert_eq!(after[0], "AD-1", "the declared task keeps its identifier");
assert_ne!(
after[1], "AD-1",
"and the new one does not take it: {after:?}"
);
assert!(
proposed.out.contains(&format!("- id: {}\n", after[1])),
"the proposal printed {}, which is what the write used:\n{}",
after[1],
proposed.out
);
}
#[test]
fn a_second_and_a_third_infer_write_change_nothing() {
let root = Root::new("infer-run-twice");
root.author();
root.corpus();
let first = root.run(&[
"infer",
"--owner",
"a parent test",
"--until",
FAR,
"--write",
]);
assert_eq!(
first.code,
Some(0),
"the first write runs\n{}{}",
first.out,
first.err
);
let after_first = ids(&root.text());
assert_eq!(
after_first.len(),
2,
"the first write added one task beside the declared one: {after_first:?}"
);
let second = root.run(&[
"infer",
"--owner",
"a parent test",
"--until",
FAR,
"--write",
]);
assert_eq!(
second.code,
Some(0),
"the second write runs\n{}{}",
second.out,
second.err
);
assert_eq!(
ids(&root.text()),
after_first,
"the second run declares nothing new and removes nothing\n{}",
second.out
);
let third = root.run(&[
"infer",
"--owner",
"a parent test",
"--until",
FAR,
"--write",
]);
assert_eq!(
third.code,
Some(0),
"the third write runs\n{}{}",
third.out,
third.err
);
assert_eq!(
ids(&root.text()),
after_first,
"and so does the third\n{}",
third.out
);
let after = root.text();
assert!(
after.contains("id: AD-9") && after.contains("until: 2027-06-30"),
"the declared task survived all three runs:\n{after}"
);
}
#[test]
fn an_adoption_block_with_no_tasks_stops_an_infer_write() {
let root = Root::new("no-tasks-sequence");
root.author();
root.corpus();
let broken = root.text().replacen(
PAYLOAD,
"\nadoption:\n note: this block declares no tasks\n",
1,
);
assert!(
broken.contains("\nadoption:\n") && !broken.contains("tasks:"),
"the block is there and its tasks sequence is not:\n{broken}"
);
root.write(&broken);
let ran = root.run(&[
"infer",
"--owner",
"a parent test",
"--until",
FAR,
"--write",
]);
assert_ne!(
ran.code,
Some(0),
"a block this run cannot add to stops the write\n{}{}",
ran.out,
ran.err
);
assert_eq!(root.text(), broken, "the file the run refused is untouched");
assert!(
ran.err.contains("adoption"),
"the refusal names what a write would have discarded:\n{}",
ran.err
);
}
#[test]
fn a_hand_edited_adoption_block_does_not_read_as_a_source_that_moved() {
let root = Root::new("hand-edited-block");
root.author();
let resolved = root.run(&["taxonomy", "resolve"]);
assert_eq!(
resolved.code,
Some(0),
"the fixture resolves\n{}{}",
resolved.out,
resolved.err
);
let clean = root.run(&["taxonomy", "resolve", "--check"]);
assert_eq!(
clean.code,
Some(0),
"the case starts from a lock `--check` accepts\n{}{}",
clean.out,
clean.err
);
let canonical = root.text();
let bare = " - id: AD-9\n";
assert!(
canonical.contains(bare),
"the renderer writes the identifier bare:\n{canonical}"
);
root.write(&canonical.replacen(bare, " - id: \"AD-9\"\n", 1));
let ran = root.run(&["taxonomy", "resolve", "--check"]);
assert_eq!(
ran.code,
Some(1),
"the file still differs from the one the sources produce\n{}{}",
ran.out,
ran.err
);
assert!(
ran.err
.contains("carries the taxonomy its sources resolve to"),
"the diagnosis names the half that agrees:\n{}",
ran.err
);
assert!(
ran.err.contains("`adoption` block"),
"and the half that does not:\n{}",
ran.err
);
assert!(
!ran.err.contains("is not what the sources resolve to"),
"nothing about the sources changed:\n{}",
ran.err
);
}
#[test]
fn a_source_that_moved_still_names_itself_and_keeps_the_old_message() {
let root = Root::new("source-moved");
root.author();
let resolved = root.run(&["taxonomy", "resolve"]);
assert_eq!(
resolved.code,
Some(0),
"the fixture resolves\n{}{}",
resolved.out,
resolved.err
);
let source = root
.at
.join(".headwater/packages/headwater-standard/taxonomy.yml");
let mut text = std::fs::read_to_string(&source).expect("the source reads");
text.push_str("\n# A comment this case appended, which moves the bytes and not the result.\n");
std::fs::write(&source, text).expect("the source writes");
let ran = root.run(&["taxonomy", "resolve", "--check"]);
assert_eq!(
ran.code,
Some(1),
"a moved source is still a stale lock\n{}{}",
ran.out,
ran.err
);
assert!(
ran.err.contains("is not what the sources resolve to"),
"the old message stands:\n{}",
ran.err
);
assert!(
ran.err
.contains("taxonomy.yml has changed since the lock was written"),
"and it names the file that moved:\n{}",
ran.err
);
}
#[test]
fn a_block_level_key_this_engine_does_not_read_is_named() {
let root = Root::new("unread-block-key");
root.author();
root.corpus();
let text = root.text();
root.write(&text.replacen(
"\nadoption:\n tasks:\n",
"\nadoption:\n from: 99.0.0\n severity: quiet\n tasks:\n",
1,
));
let ran = root.run(&["check"]);
assert_eq!(
ran.code,
Some(0),
"an unread key is reported and does not fail a run\n{}{}",
ran.out,
ran.err
);
assert!(
ran.out.contains("`from`"),
"the report names the key:\n{}",
ran.out
);
assert!(
ran.out.contains("`severity`"),
"and the second one:\n{}",
ran.out
);
assert!(
ran.out.contains("AD-9"),
"and the tasks beside it are still read:\n{}",
ran.out
);
}
#[test]
fn a_task_level_key_this_engine_does_not_read_refuses_the_task() {
let root = Root::new("unread-task-key");
root.author();
root.corpus();
let text = root.text();
let anchor = " until: 2027-06-30\n";
assert!(text.contains(anchor), "the task states its expiry:\n{text}");
root.write(&text.replacen(anchor, &format!("{anchor} to: 4.0.0\n"), 1));
let ran = root.run(&["check"]);
assert_eq!(
ran.code,
Some(0),
"a refused task is reported and does not fail a run\n{}{}",
ran.out,
ran.err
);
assert!(
ran.out.contains("AD-9 holds nothing"),
"the task is refused by name:\n{}",
ran.out
);
assert!(
ran.out.contains("`to`"),
"and the refusal names the key:\n{}",
ran.out
);
}
#[test]
fn a_comment_in_the_generated_half_is_not_blamed_on_the_authored_block() {
let root = Root::new("comment-in-generated-half");
root.author();
let resolved = root.run(&["taxonomy", "resolve"]);
assert_eq!(
resolved.code,
Some(0),
"the fixture resolves\n{}{}",
resolved.out,
resolved.err
);
let canonical = root.text();
root.write(&canonical.replacen("\nresolved:\n", "\n# somebody added a note\nresolved:\n", 1));
let ran = root.run(&["taxonomy", "resolve", "--check"]);
assert_eq!(
ran.code,
Some(1),
"the file still differs from the one the sources produce\n{}{}",
ran.out,
ran.err
);
assert!(
ran.err.contains("not inside the `adoption` block"),
"the diagnosis places the difference outside the block:\n{}",
ran.err
);
assert!(
!ran.err.contains("where the two differ"),
"and does not blame the block:\n{}",
ran.err
);
}
#[test]
fn a_lock_that_will_not_read_names_a_remedy_that_can_succeed() {
let root = Root::new("unreadable-empty-block");
root.author();
let resolved = root.run(&["taxonomy", "resolve"]);
assert_eq!(resolved.code, Some(0), "the fixture resolves");
let canonical = root.text();
let cut = canonical.find("\nadoption:\n").expect("the block is there") + 1;
let end = canonical[cut..]
.find("\n# The resolved taxonomy")
.expect("the generated half follows it")
+ cut;
root.write(&format!(
"{}adoption:{}",
&canonical[..cut],
&canonical[end..]
));
let ran = root.run(&["taxonomy", "resolve", "--check"]);
assert_eq!(
ran.code,
Some(1),
"a lock that will not read is not a lock\n{}{}",
ran.out,
ran.err
);
assert!(
ran.err.contains("did not read"),
"the run says what is wrong with the file:\n{}",
ran.err
);
assert!(
!ran.err.contains("is not what the sources resolve to"),
"and says nothing about the sources, which it cannot see:\n{}",
ran.err
);
let remedy = root.run(&["taxonomy", "resolve"]);
let refuses = ran.err.contains("refuses this file");
assert_eq!(
remedy.code != Some(0),
refuses,
"the printed remedy and what the remedy does agree\n--check said:\n{}\nresolve said ({:?}):\n{}{}",
ran.err,
remedy.code,
remedy.out,
remedy.err
);
}
#[test]
fn a_lock_whose_digest_does_not_match_is_told_to_resolve() {
let root = Root::new("unreadable-digest");
root.author();
let resolved = root.run(&["taxonomy", "resolve"]);
assert_eq!(resolved.code, Some(0), "the fixture resolves");
root.write(
&root
.text()
.replacen("digest: sha256:", "digest: sha256:0000", 1),
);
let ran = root.run(&["taxonomy", "resolve", "--check"]);
assert_eq!(ran.code, Some(1), "{}{}", ran.out, ran.err);
assert!(
ran.err.contains("did not read"),
"the run says what is wrong with the file:\n{}",
ran.err
);
assert!(
!ran.err.contains("refuses this file"),
"and this is the state a resolve repairs:\n{}",
ran.err
);
let remedy = root.run(&["taxonomy", "resolve"]);
assert_eq!(
remedy.code,
Some(0),
"the printed remedy succeeds\n{}{}",
remedy.out,
remedy.err
);
assert!(
root.text().contains("AD-9"),
"and the authored block survived it"
);
}
#[test]
fn check_strict_exits_zero_with_no_adoption_task_declared() {
let root = Root::new("expired-baseline-absent");
root.corpus();
let checked = root.run(&["check", "--strict"]);
assert_eq!(
checked.code,
Some(0),
"a corpus with no declared task passes a strict run\n{}{}",
checked.out,
checked.err
);
assert!(
!checked.out.contains("adoption.task.expired ("),
"no task exists, so the rule fires zero times (the rule still names \
itself in the catalog of what ran, which is not a finding):\n{}",
checked.out
);
}
#[test]
fn check_strict_is_non_zero_when_a_declared_task_has_lapsed() {
let root = Root::new("expired-lapsed");
root.author_until("AD-9", "2020-01-01");
root.corpus();
let checked = root.run(&["check", "--strict"]);
assert_eq!(
checked.code,
Some(1),
"a lapsed task fails a strict run\n{}{}",
checked.out,
checked.err
);
assert!(
checked.out.contains("adoption.task.expired"),
"the rule fired:\n{}",
checked.out
);
assert!(
checked.out.contains("AD-9"),
"the finding names the task:\n{}",
checked.out
);
assert!(
checked.out.contains("a person"),
"and the owner PAYLOAD declares:\n{}",
checked.out
);
}
#[test]
fn check_strict_exits_zero_when_a_declared_task_is_renewed() {
let root = Root::new("expired-renewed");
root.author_until("AD-9", FAR);
root.corpus();
let checked = root.run(&["check", "--strict"]);
assert_eq!(
checked.code,
Some(0),
"a renewed task passes a strict run, same as the absent arm\n{}{}",
checked.out,
checked.err
);
assert!(
!checked.out.contains("adoption.task.expired ("),
"the rule does not fire on an open task (the rule still names itself \
in the catalog of what ran, which is not a finding):\n{}",
checked.out
);
}
const AT: &str = "2026-08-28";
const RAISED: &str = "language.controlled.not_met";
const UNRAISED: &str = "language.retired_term.used";
#[test]
fn a_recorded_reading_states_an_open_payload_as_open() {
let root = Root::new("decay-open");
root.corpus();
let document = root.document();
root.author_pair("AD-9", "2027-06-30", &document, RAISED);
let ran = root.run(&["taxonomy", "audit", "--now", AT, "--record"]);
assert_eq!(
ran.code,
Some(0),
"the audit gates nothing, so it exits 0 whatever it read\n{}{}",
ran.out,
ran.err
);
let lines = root.store();
assert_eq!(lines.len(), 1, "one invocation is one reading: {lines:?}");
let line = &lines[0];
assert!(
line.contains("\"id\":\"AD-9\""),
"the reading names the task:\n{line}"
);
assert!(
line.contains("\"open\":1"),
"the declared pair still raises its finding:\n{line}"
);
assert!(
line.contains("\"closed\":0"),
"and nothing about it is discharged:\n{line}"
);
assert!(
line.contains("\"held\":1"),
"the task is holding the finding out of the report:\n{line}"
);
assert!(
line.contains("\"until\":\"2027-06-30\""),
"with the expiry a later reader needs to say whether it reached zero in time:\n{line}"
);
assert!(
ran.out.contains("1 pair open"),
"and the section states the same number:\n{}",
ran.out
);
}
#[test]
fn a_recorded_reading_states_a_discharged_payload_as_zero() {
let root = Root::new("decay-zero");
root.corpus();
let document = root.document();
root.author_pair("AD-9", "2027-06-30", &document, UNRAISED);
let ran = root.run(&["taxonomy", "audit", "--now", AT, "--record"]);
assert_eq!(
ran.code,
Some(0),
"the audit gates nothing\n{}{}",
ran.out,
ran.err
);
let lines = root.store();
assert_eq!(lines.len(), 1, "one invocation is one reading: {lines:?}");
let line = &lines[0];
assert!(
line.contains("\"open\":0"),
"the declared pair raises nothing, so the payload stands at zero:\n{line}"
);
assert!(
line.contains("\"closed\":1"),
"and the pair that stopped failing is what shrank it:\n{line}"
);
assert!(
line.contains("\"held\":0"),
"a task at zero holds nothing out of the report:\n{line}"
);
assert!(
ran.out.contains("0 pairs open"),
"and the section states the same number:\n{}",
ran.out
);
assert!(
ran.out.contains("until 2027-06-30"),
"with the expiry beside it, which is what `before the expiry` is read against:\n{}",
ran.out
);
}
#[test]
fn two_lock_digests_are_two_measurements_and_the_report_says_so() {
let root = Root::new("decay-two-locks");
root.corpus();
let document = root.document();
root.author_pair("AD-9", "2027-06-30", &document, RAISED);
let first = root.run(&["taxonomy", "audit", "--now", AT, "--record"]);
assert_eq!(
first.code,
Some(0),
"the first reading is taken\n{}{}",
first.out,
first.err
);
let source = root
.at
.join(".headwater/packages/headwater-standard/taxonomy.yml");
let text = std::fs::read_to_string(&source).expect("the source reads");
let moved = text.replace(
"draft: the document is being written or argued over, and nothing may rely on it",
"draft: the document is being written, and nothing may rely on it",
);
assert_ne!(text, moved, "the guidance this case edits is still there");
std::fs::write(&source, moved).expect("the source writes");
let resolved = root.run(&["taxonomy", "resolve"]);
assert_eq!(
resolved.code,
Some(0),
"the moved source resolves, and the authored block is carried through\n{}{}",
resolved.out,
resolved.err
);
assert!(
root.text().contains("AD-9"),
"the payload survived the resolve, or the second reading is over a different corpus"
);
let second = root.run(&["taxonomy", "audit", "--now", AT, "--record"]);
assert_eq!(
second.code,
Some(0),
"the second reading is taken\n{}{}",
second.out,
second.err
);
let lines = root.store();
assert_eq!(
lines.len(),
2,
"one date and two digests are two readings: {lines:?}"
);
let digest = |line: &str| {
line.split("\"lock\":\"")
.nth(1)
.and_then(|rest| rest.split('"').next())
.expect("a reading names its lock")
.to_string()
};
assert_ne!(
digest(&lines[0]),
digest(&lines[1]),
"the two readings were taken under two taxonomies: {lines:?}"
);
assert!(
second.out.contains("2 taxonomies"),
"the section counts the denominators:\n{}",
second.out
);
assert!(
second.out.contains("not a trend"),
"and refuses to trend across them:\n{}",
second.out
);
}
#[test]
fn a_second_recorded_audit_of_one_tree_at_one_date_adds_nothing() {
let root = Root::new("decay-idempotent");
root.corpus();
let document = root.document();
root.author_pair("AD-9", "2027-06-30", &document, RAISED);
let first = root.run(&["taxonomy", "audit", "--now", AT, "--record"]);
assert_eq!(first.code, Some(0), "{}{}", first.out, first.err);
let after_one = root.store();
let second = root.run(&["taxonomy", "audit", "--now", AT, "--record"]);
assert_eq!(second.code, Some(0), "{}{}", second.out, second.err);
assert_eq!(
root.store(),
after_one,
"the store holds one reading of one tree at one date"
);
assert!(
second.err.contains("already holds"),
"and the run says on standard error that it added nothing:\n{}",
second.err
);
}
#[test]
fn an_audit_without_the_flag_writes_no_reading() {
let root = Root::new("decay-no-flag");
root.corpus();
let document = root.document();
root.author_pair("AD-9", "2027-06-30", &document, RAISED);
let ran = root.run(&["taxonomy", "audit", "--now", AT]);
assert_eq!(ran.code, Some(0), "{}{}", ran.out, ran.err);
assert_eq!(root.store(), Vec::<String>::new(), "nothing was written");
assert!(
ran.out.contains("no reading recorded"),
"and the section says the series is empty rather than saying nothing:\n{}",
ran.out
);
assert!(
ran.out.contains("this run reads"),
"while still stating what this run read:\n{}",
ran.out
);
}
#[test]
fn a_corpus_with_no_declared_payload_records_a_reading_of_none() {
let root = Root::new("decay-no-payload");
root.corpus();
let ran = root.run(&["taxonomy", "audit", "--now", AT, "--record"]);
assert_eq!(ran.code, Some(0), "{}{}", ran.out, ran.err);
let lines = root.store();
assert_eq!(lines.len(), 1, "a run over no payload is still a reading");
assert!(
lines[0].contains("\"tasks\":[]"),
"and it states that the corpus declared none:\n{}",
lines[0]
);
assert!(
ran.out.contains("declares no adoption payload"),
"the section says so in words:\n{}",
ran.out
);
}
#[test]
fn a_refused_task_is_counted_and_the_report_names_the_refusal_as_the_cause() {
let root = Root::new("decay-refused");
root.corpus();
root.author_until("AD-9", "soon");
let checked = root.run(&["check", "--now", AT]);
assert_eq!(
checked.code,
Some(0),
"the lock still reads, and one task in it does not\n{}{}",
checked.out,
checked.err
);
assert!(
checked.out.contains("AD-9 holds nothing"),
"the check layer refuses the task rather than the block:\n{}",
checked.out
);
let ran = root.run(&["taxonomy", "audit", "--now", AT, "--record"]);
assert_eq!(ran.code, Some(0), "{}{}", ran.out, ran.err);
let lines = root.store();
assert_eq!(lines.len(), 1, "one invocation is one reading: {lines:?}");
let line = &lines[0];
assert!(
line.contains("\"refused\":1"),
"the reading counts the task nobody is measuring:\n{line}"
);
assert!(
line.contains("\"tasks\":[]"),
"and it holds no task entry, because the task did not read:\n{line}"
);
assert!(
ran.out.contains("the cause is a refusal rather than an"),
"the section names the cause it actually read:\n{}",
ran.out
);
assert!(
!ran.out
.contains("because the lock declares no adoption payload"),
"and never the cause it did not: this lock declares one\n{}",
ran.out
);
assert!(
ran.out.contains("it could not read 1 task"),
"with the count beside it:\n{}",
ran.out
);
}