use std::path::{Path, PathBuf};
use std::process::Command;
mod common;
use common::pin;
fn repository() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../..")
.canonicalize()
.expect("the repository root resolves")
}
fn fixtures() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("fixtures/migration")
}
const CANDIDATE: [(&[&str], &[&str]); 3] = [
(
&[
" - {value: draft, role: initial}",
" - {value: current, role: live}",
" - {value: superseded, role: terminal-retained}",
" - {value: deprecated, role: terminal-retained}",
" - {value: discharged, role: terminal-retained}",
],
&[
" - {value: outline, role: initial}",
" - {value: settled, role: live}",
" - {value: provisional}",
" - {value: superseded, role: terminal-retained}",
" - {value: discharged, role: terminal-retained}",
],
),
(
&[
" draft: the document is being written or argued over, and nothing may rely on it",
" current: the document states what holds now, and a reader may rely on it",
" superseded: a later document replaced this one, and the succession edge names it",
" deprecated: the document is no longer to be relied on, and nothing replaced it",
" discharged: the document recorded something the corpus owed, the corpus paid it, and the record is kept as the receipt",
],
&[
" outline: the document is being written or argued over, and nothing may rely on it",
" settled: the document states what holds now and nobody is arguing with it",
" provisional: the document states what holds now and the argument is not closed",
" superseded: a later document replaced this one, and the succession edge names it",
" discharged: the document recorded something the corpus owed, the corpus paid it, and the record is kept as the receipt",
],
),
(
&[
" initial: draft",
" transitions: {draft: [current, deprecated], current: [superseded, deprecated, discharged]}",
],
&[
" initial: outline",
" transitions: {outline: [provisional, settled], provisional: [settled, superseded], settled: [superseded, discharged]}",
],
),
];
const KIND: [(&[&str], &[&str]); 2] = [
(&[" decision:"], &[" ruling:"]),
(&["kind: decision,"], &["kind: ruling,"]),
];
const BUNDLE_KIND: [(&str, &str); 3] = [
(" kinds.decision_register:", " kinds.ruling_register:"),
(
" kinds: [design_spec, decision_register, obligation_register]",
" kinds: [design_spec, ruling_register, obligation_register]",
),
(
" from: [design_spec, decision_register, obligation_register]",
" from: [design_spec, ruling_register, obligation_register]",
),
];
fn register_payload(target: &str) -> String {
format!(
"# SPDX-License-Identifier: Apache-2.0\n\nmigration:\n format: 1\n from: \">=1 <2\"\n \
to: \">=2 <3\"\n\nsteps:\n - subject: kind\n from: decision_register\n to: \
[{target}]\n because: >-\n The register of settled decisions and the decisions \
themselves were one\n kind, and a reader of the shelf could not tell which one a \
document was.\n"
)
}
const FACET: [(&[&str], &[&str]); 1] = [(
&[
" title:",
" role: name",
" type: string",
" required: false",
" volatility: stable",
],
&[
" title:",
" role: name",
" type: string",
" required: false",
" volatility: stable",
" audience:",
" type: string",
" required: false",
" volatility: stable",
],
)];
const REQUIRES_AUDIENCE: [(&str, &str); 1] = [(
" kinds.decision.facets:\n require: [title]",
" kinds.decision.facets:\n require: [title, audience]",
)];
fn inert_payload() -> String {
"# SPDX-License-Identifier: Apache-2.0\n\nmigration:\n format: 1\n from: \">=1 <2\"\n to: \
\">=2 <3\"\n\nsteps:\n - subject: facet_value\n facet: audience\n from: audience\n \
to: []\n task: >-\n Say who each decision is written for.\n because: >-\n A \
reader could not tell which decisions were written for them.\n"
.to_string()
}
fn address() -> Vec<(&'static [&'static str], &'static [&'static str])> {
let mut edits: Vec<(&[&str], &[&str])> = CANDIDATE.to_vec();
edits.push((&[" specification:"], &[" norm:"]));
edits.push((&["kind: specification"], &["kind: norm"]));
edits
}
const ENTRY: &str = "\n kinds.specification.identifier: {scheme: spec_id}\n";
struct Root {
at: PathBuf,
}
impl Root {
fn new(label: &str) -> Root {
let at = std::env::temp_dir().join(format!(
"headwater-cli-migration-{}-{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"),
);
without_doctrine(&at.join("docs/taxonomies"));
without_templates(&at.join("docs/taxonomies"));
copy(&fixtures().join("docs"), &at.join("docs"));
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");
}
pin(&at, "1.0.0");
let root = Root { at };
let resolved = root.run(&["taxonomy", "resolve"]);
assert_eq!(resolved.code, Some(0), "the fixture resolves: {resolved:?}");
root
}
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 publish(&self, name: &str) -> Ran {
let out = self.at.join("released").join(name);
self.run(&["taxonomy", "publish", "--out", out.to_str().expect("utf-8")])
}
fn released(&self, name: &str) -> PathBuf {
self.at.join("released").join(name)
}
fn candidate(&self, payload: Option<&str>) {
self.candidate_of(&CANDIDATE, payload);
}
fn candidate_of(&self, edits: &[(&[&str], &[&str])], payload: Option<&str>) {
let taxonomy = self
.at
.join(".headwater/packages/headwater-standard/taxonomy.yml");
let mut text = std::fs::read_to_string(&taxonomy).expect("the taxonomy reads");
for (from, to) in edits {
let (from, to) = (from.join("\n"), to.join("\n"));
assert!(text.contains(&from), "the base still carries `{from}`");
text = text.replacen(&from, &to, 1);
}
assert!(text.contains("version: 1.0.0"), "the base is at 1.0.0");
std::fs::write(
&taxonomy,
text.replacen("version: 1.0.0", "version: 2.0.0", 1),
)
.expect("the taxonomy writes");
let manifest = self
.at
.join(".headwater/packages/headwater-standard/package.yml");
let mut text = std::fs::read_to_string(&manifest).expect("the manifest reads");
text = text.replacen("version: 1.0.0", "version: 2.0.0", 1);
if let Some(payload) = payload {
let bundles = bundles_line();
assert!(
text.contains(&bundles),
"the scratch manifest states `{bundles}`, which `repoint_bundles` wrote"
);
text = text.replacen(&bundles, &format!("{bundles}\n migrations: migrations"), 1);
let directory = self
.at
.join(".headwater/packages/headwater-standard/migrations");
std::fs::create_dir_all(&directory).expect("the payload directory is there");
std::fs::write(directory.join("1-to-2.yml"), payload).expect("the payload writes");
}
std::fs::write(&manifest, text).expect("the manifest writes");
}
fn in_bundle(&self, bundle: &str, edits: &[(&str, &str)]) {
let at = self
.at
.join("docs/taxonomies")
.join(bundle)
.join("bundle.yml");
let mut text = std::fs::read_to_string(&at).expect("the bundle reads");
for (from, to) in edits {
assert!(text.contains(from), "the bundle still carries `{from}`");
text = text.replacen(from, to, 1);
}
std::fs::write(&at, text).expect("the bundle writes");
}
fn diff(&self, name: &str) -> Ran {
let at = self.released(name);
self.run(&[
"taxonomy",
"diff",
at.to_str().expect("utf-8"),
"--now",
"2026-08-01",
])
}
fn migrate(&self, name: &str, flags: &[&str]) -> Ran {
let at = self.released(name);
let mut arguments = vec![
"taxonomy",
"migrate",
at.to_str().expect("utf-8"),
"--now",
"2026-08-01",
];
arguments.extend_from_slice(flags);
self.run(&arguments)
}
fn read(&self, path: &str) -> String {
std::fs::read_to_string(self.at.join(path)).expect("the document reads")
}
fn register(&self) -> &'static str {
let path = "docs/spec/09-the-register.md";
let at = self.at.join(path);
std::fs::create_dir_all(at.parent().expect("it has a parent"))
.expect("the shelf directory is there");
std::fs::write(&at, REGISTER).expect("the document writes");
path
}
fn takes(&self, version: &str) {
let at = self.at.join(".headwater/taxonomy.yml");
let text = std::fs::read_to_string(&at).expect("the consumer declaration reads");
assert!(
text.contains("\n version: 1.0.0\n"),
"the declaration pins 1.0.0 to start with:\n{text}"
);
std::fs::write(
&at,
text.replacen(
"\n version: 1.0.0\n",
&format!("\n version: {version}\n"),
1,
),
)
.expect("the consumer declaration writes");
let resolved = self.run(&["taxonomy", "resolve"]);
assert_eq!(
resolved.code,
Some(0),
"the corpus resolves against the version it now takes: {resolved:?}"
);
}
fn heads(&self, version: &str) {
let at = self.at.join(".headwater/taxonomy.lock");
let text = std::fs::read_to_string(&at).expect("the lock reads");
let was = "\n version: 1.0.0\n sources:\n";
assert!(text.contains(was), "the lock header names 1.0.0:\n{text}");
std::fs::write(
&at,
text.replacen(was, &format!("\n version: {version}\n sources:\n"), 1),
)
.expect("the lock writes");
}
fn addresses(&self, entry: &str) {
let at = self.at.join(".headwater/overlay.yml");
let text = std::fs::read_to_string(&at).expect("the overlay reads");
assert!(text.starts_with("# SPDX"), "the overlay is the copied one");
let (before, after) = text
.split_once("\nadd:\n")
.expect("it declares an `add` block");
std::fs::write(&at, format!("{before}\nadd:\n{entry}{after}")).expect("the overlay writes");
let resolved = self.run(&["taxonomy", "resolve"]);
assert_eq!(
resolved.code,
Some(0),
"the overlay with the entry resolves against the base: {resolved:?}"
);
}
fn without(&self, line: &str) {
let at = self.at.join(".headwater/overlay.yml");
let text = std::fs::read_to_string(&at).expect("the overlay reads");
assert!(text.contains(line), "the overlay still carries `{line}`");
std::fs::write(&at, text.replacen(line, "", 1)).expect("the overlay writes");
let resolved = self.run(&["taxonomy", "resolve"]);
assert_eq!(
resolved.code,
Some(0),
"the overlay without the line resolves: {resolved:?}"
);
}
fn read_only(&self, path: &str) {
let at = self.at.join(path);
let mut permissions = std::fs::metadata(&at)
.expect("the document is there")
.permissions();
permissions.set_readonly(true);
std::fs::set_permissions(&at, permissions).expect("the document locks");
}
}
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 dimension(&self, name: &str) -> String {
self.out
.lines()
.find_map(|line| line.trim_start().strip_prefix(name))
.map(|rest| rest.trim().to_string())
.unwrap_or_else(|| panic!("the report names `{name}`: {self:?}"))
}
}
fn without_templates(at: &Path) {
for entry in std::fs::read_dir(at).expect("the copied library reads") {
let entry = entry.expect("the entry reads");
let templates = entry.path().join("templates");
if templates.is_dir() {
std::fs::remove_dir_all(&templates).expect("the templates directory is removed");
}
}
}
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");
}
}
}
}
fn payload() -> String {
std::fs::read_to_string(fixtures().join("1-to-2.yml")).expect("the committed payload reads")
}
fn addresses() -> String {
std::fs::read_to_string(fixtures().join("addresses-1-to-2.yml"))
.expect("the committed payload reads")
}
#[test]
fn the_payload_names_every_document_whose_validity_moved() {
let root = Root::new("names-every-document");
assert_eq!(root.publish("1.0.0").code, Some(0));
root.candidate(Some(&payload()));
let published = root.publish("2.0.0");
assert_eq!(published.code, Some(0), "{published:?}");
let ran = root.diff("2.0.0");
assert_eq!(ran.code, Some(0), "{ran:?}");
assert_eq!(
ran.dimension("instance_validity"),
"BROKEN, 3 failed / 3 skipped"
);
assert_eq!(
ran.dimension("consequence"),
"BROKEN, 3 failed / 3 skipped",
"the superset dimension tells the same six apart: {ran:?}"
);
assert!(
ran.out.contains(
"3 of the 3 documents a dimension reports as moved lie under a step of this payload"
),
"{ran:?}"
);
for line in [
"a closed choice the author settles, among `settled`, `provisional`",
"2 documents of this corpus carry the old value",
"mechanical, and it becomes `outline`",
"1 document of this corpus carries the old value",
"a re-statement, and nothing replaces the old value",
"no document of this corpus carries the old value, so this step is a no-op here",
"3 steps, 1 mechanical and 2 judgment-bearing",
"remedies instance_validity, consequence",
] {
assert!(
ran.out.contains(line),
"the report states `{line}`: {ran:?}"
);
}
}
#[test]
fn a_document_no_step_names_is_named() {
let root = Root::new("names-what-is-uncovered");
assert_eq!(root.publish("1.0.0").code, Some(0));
let full = payload();
let step = [
" - subject: facet_value",
" facet: status",
" from: draft",
" to: [outline]",
]
.join("\n");
assert!(full.contains(&step), "the committed payload carries it");
let (before, rest) = full.split_once(&step).expect("the step is there");
let after = rest
.split_once(" # A re-statement")
.expect("the step after it is there")
.1;
root.candidate(Some(&format!("{before} # A re-statement{after}")));
let published = root.publish("2.0.0");
assert_eq!(published.code, Some(0), "{published:?}");
let ran = root.diff("2.0.0");
assert_eq!(ran.code, Some(0), "{ran:?}");
assert_eq!(
ran.dimension("instance_validity"),
"BROKEN, 3 failed / 3 skipped",
"the corpus and the candidate are the ones the case above measured: {ran:?}"
);
assert!(
ran.out.contains(
"1 of the 3 documents a dimension reports as moved lie under no step of this payload"
),
"{ran:?}"
);
assert!(
ran.out
.contains("docs/decisions/0003-the-document-still-being-written.md"),
"the uncovered document is named: {ran:?}"
);
}
#[test]
fn a_closed_choice_with_no_task_stops_the_publish() {
let root = Root::new("choice-with-no-task");
let full = payload();
let task = [
" task: >-",
" Say whether the argument this document makes is closed. A document that a",
" reader may rely on and still argue with is `provisional`. One that nobody",
" is arguing with is `settled`.",
"",
]
.join("\n");
assert!(full.contains(&task), "the committed payload carries it");
root.candidate(Some(&full.replacen(&task, "", 1)));
let ran = root.publish("2.0.0");
assert_eq!(ran.code, Some(1), "{ran:?}");
assert!(ran.err.contains("nothing was published"), "{ran:?}");
assert!(
ran.err
.contains("a choice with no `task` beside it is a question nobody was asked"),
"{ran:?}"
);
assert!(
!root.released("2.0.0").exists(),
"a refused publish writes no artifact"
);
}
#[test]
fn a_source_the_taxonomy_still_declares_stops_the_publish() {
let root = Root::new("source-still-stands");
let full = payload();
let step = ["from: draft", " to: [outline]"].join("\n");
assert!(full.contains(&step), "the committed payload carries it");
root.candidate(Some(&full.replacen(
&step,
&["from: superseded", " to: [outline]"].join("\n"),
1,
)));
let ran = root.publish("2.0.0");
assert_eq!(ran.code, Some(1), "{ran:?}");
assert!(
ran.err.contains(
"still declares that facet_value, so the step renames something that did \
not move"
),
"{ran:?}"
);
}
#[test]
fn the_release_record_covers_the_payload() {
let root = Root::new("record-covers-it");
root.candidate(Some(&payload()));
assert_eq!(root.publish("2.0.0").code, Some(0));
let at = root.released("2.0.0");
assert!(at.join("migrations/1-to-2.yml").is_file(), "it is carried");
let record = std::fs::read_to_string(at.join("release.yml")).expect("the record reads");
assert!(
record.contains("path: migrations/1-to-2.yml"),
"the record names it: {record}"
);
std::fs::write(at.join("migrations/1-to-2.yml"), "migration: {}\n").expect("the edit writes");
let ran = root.diff("2.0.0");
assert_eq!(ran.code, Some(1), "{ran:?}");
assert!(
ran.err
.contains("the artifact is not what its own release record says it is"),
"{ran:?}"
);
}
#[test]
fn a_major_upgrade_with_no_payload_is_reported() {
let root = Root::new("no-payload-at-all");
assert_eq!(root.publish("1.0.0").code, Some(0));
root.candidate(None);
assert_eq!(root.publish("2.0.0").code, Some(0));
let ran = root.diff("2.0.0");
assert_eq!(ran.code, Some(0), "{ran:?}");
assert!(
ran.out.contains(
"the artifact ships no migration payload for 1.0.0 to 2.0.0, and 3 documents stopped \
validating"
),
"{ran:?}"
);
}
#[test]
fn a_kind_step_is_accounted_against_classification() {
let root = Root::new("kind-step");
root.without(" kinds.decision.language: ste_house\n");
assert_eq!(root.publish("1.0.0").code, Some(0));
let payload =
std::fs::read_to_string(fixtures().join("kinds-1-to-2.yml")).expect("the payload reads");
root.candidate_of(&KIND, Some(&payload));
let published = root.publish("2.0.0");
assert_eq!(published.code, Some(0), "{published:?}");
let ran = root.diff("2.0.0");
assert_eq!(ran.code, Some(0), "{ran:?}");
assert_eq!(ran.dimension("classification"), "BROKEN, 3 of them");
assert_eq!(
ran.dimension("instance_validity"),
"preserved",
"the other half contributes nothing here, or this case proves neither: {ran:?}"
);
for line in [
"kind decision",
"mechanical, and it becomes `ruling`",
"remedies classification",
"3 documents of this corpus carry the old value",
"3 of the 3 documents a dimension reports as moved lie under a step of this payload",
] {
assert!(
ran.out.contains(line),
"the report states `{line}`: {ran:?}"
);
}
}
#[test]
fn a_rename_of_a_kind_a_bundle_declares_publishes() {
let root = Root::new("bundle-kind-renamed");
root.in_bundle("design-spec", &BUNDLE_KIND);
let payload = format!(
"{}\n - subject: overlay_address\n from: kinds.decision_register\n to: \
[kinds.ruling_register]\n because: >-\n An overlay entry under the old address \
adds the old kind back rather than\n failing to address anything.\n",
register_payload("ruling_register")
);
root.candidate_of(&[], Some(&payload));
let ran = root.publish("2.0.0");
assert_eq!(ran.code, Some(0), "{ran:?}");
}
#[test]
fn a_target_no_bundle_declares_stops_the_publish() {
let root = Root::new("bundle-kind-no-target");
root.in_bundle("design-spec", &BUNDLE_KIND);
root.candidate_of(&[], Some(®ister_payload("no_source_declares_this")));
let ran = root.publish("2.0.0");
assert_eq!(ran.code, Some(1), "{ran:?}");
assert!(ran.err.contains("nothing was published"), "{ran:?}");
assert!(
ran.err.contains("nor any bundle it ships"),
"the message names the set the target was held against: {ran:?}"
);
assert!(
!root.released("2.0.0").exists(),
"a refused publish writes no artifact"
);
}
#[test]
fn a_source_only_a_bundle_declares_is_not_the_publishers_to_refuse() {
let root = Root::new("bundle-source-stands");
root.candidate_of(&[], Some(®ister_payload("specification")));
let ran = root.publish("2.0.0");
assert_eq!(ran.code, Some(0), "{ran:?}");
assert!(
std::fs::read_to_string(
root.released("2.0.0")
.join("bundles/design-spec/bundle.yml")
)
.expect("the published bundle reads")
.contains("kinds.decision_register:"),
"the artifact still declares the kind the step says moved"
);
}
fn two_document_payload() -> String {
let full = payload();
let choice = [
" to: [settled, provisional]",
" task: >-",
" Say whether the argument this document makes is closed. A document that a",
" reader may rely on and still argue with is `provisional`. One that nobody",
" is arguing with is `settled`.",
]
.join("\n");
assert!(full.contains(&choice), "the committed payload carries it");
full.replacen(&choice, " to: [settled]", 1)
}
#[test]
fn the_mechanical_step_writes_and_a_judgment_step_writes_nothing() {
let root = Root::new("apply-writes-the-mechanical-half");
assert_eq!(root.publish("1.0.0").code, Some(0));
root.candidate(Some(&payload()));
assert_eq!(root.publish("2.0.0").code, Some(0));
let live = root.read("docs/decisions/0001-the-live-document.md");
let second = root.read("docs/decisions/0002-the-second-live-document.md");
let written = root.read("docs/decisions/0003-the-document-still-being-written.md");
assert!(written.contains("status: draft"), "the fixture is at draft");
let ran = root.migrate("2.0.0", &["--apply"]);
assert_eq!(ran.code, Some(0), "{ran:?}");
assert!(ran.out.contains("wrote 1 value in 2 files"), "{ran:?}");
assert!(
root.read("docs/decisions/0003-the-document-still-being-written.md")
.contains("status: outline"),
"the mechanical step wrote its one target"
);
assert_eq!(
root.read("docs/decisions/0001-the-live-document.md"),
live,
"a closed choice is settled by an author, and `--apply` wrote this document"
);
assert_eq!(
root.read("docs/decisions/0002-the-second-live-document.md"),
second,
"a closed choice is settled by an author, and `--apply` wrote this document"
);
assert!(
ran.out.contains("2 tasks for an author"),
"the judgment half is emitted as a task list: {ran:?}"
);
assert!(
ran.out
.contains("Say whether the argument this document makes is closed"),
"the task carries its own text: {ran:?}"
);
}
#[test]
fn a_run_without_apply_writes_no_document() {
let root = Root::new("apply-is-required");
assert_eq!(root.publish("1.0.0").code, Some(0));
root.candidate(Some(&payload()));
assert_eq!(root.publish("2.0.0").code, Some(0));
let before = root.read("docs/decisions/0003-the-document-still-being-written.md");
let ran = root.migrate("2.0.0", &[]);
assert_eq!(ran.code, Some(0), "{ran:?}");
assert!(
ran.out
.contains("1 value in 2 files would be written. Nothing was: pass `--apply`"),
"{ran:?}"
);
assert_eq!(
root.read("docs/decisions/0003-the-document-still-being-written.md"),
before
);
}
#[test]
fn a_document_that_cannot_be_written_leaves_every_other_document_as_it_was() {
let root = Root::new("half-written");
assert_eq!(root.publish("1.0.0").code, Some(0));
root.candidate(Some(&two_document_payload()));
assert_eq!(root.publish("2.0.0").code, Some(0));
let planned = root.migrate("2.0.0", &[]);
assert_eq!(planned.code, Some(0), "{planned:?}");
assert!(
planned.out.contains("3 values in 4 files would be written"),
"{planned:?}"
);
let first = "docs/decisions/0001-the-live-document.md";
let second = "docs/decisions/0002-the-second-live-document.md";
let third = "docs/decisions/0003-the-document-still-being-written.md";
let before = root.read(first);
let after = root.read(third);
root.read_only(second);
let ran = root.migrate("2.0.0", &["--apply"]);
assert_eq!(ran.code, Some(1), "{ran:?}");
assert!(
ran.err.contains(second),
"the refusal names the file it could not write: {ran:?}"
);
assert!(
ran.err
.contains("a file of this migration cannot be written, so none was"),
"{ran:?}"
);
assert_eq!(
root.read(first),
before,
"the document before the unwritable one is exactly what it was"
);
assert_eq!(root.read(third), after, "and so is the document after it");
assert!(
root.read(second).contains("status: current"),
"the unwritable document did not move either"
);
}
#[test]
fn a_kind_the_shelf_carries_is_named_and_no_document_is_written() {
let root = Root::new("kind-by-placement");
assert_eq!(root.publish("1.0.0").code, Some(0));
let payload =
std::fs::read_to_string(fixtures().join("kinds-1-to-2.yml")).expect("the payload reads");
root.candidate_of(&KIND, Some(&payload));
assert_eq!(root.publish("2.0.0").code, Some(0));
let before = root.read("docs/decisions/0001-the-live-document.md");
let ran = root.migrate("2.0.0", &["--apply"]);
assert_eq!(ran.code, Some(0), "{ran:?}");
assert!(
ran.out.contains(
"takes its kind from `decisions`, which is homogeneous, so the document declares \
the kind nowhere. The remedy is to move the file"
),
"{ran:?}"
);
assert!(
ran.out.contains("wrote 0 values in 1 file"),
"no document moved, and the lock still records the migration: {ran:?}"
);
assert_eq!(
root.read("docs/decisions/0001-the-live-document.md"),
before
);
}
#[test]
fn apply_records_the_migration_state_in_the_lock() {
let root = Root::new("records-the-migration-state");
let digest = root
.read(".headwater/taxonomy.yml")
.lines()
.find_map(|line| line.trim().strip_prefix("digest: "))
.expect("the fixture's consumer declaration pins a digest")
.to_string();
assert_eq!(root.publish("1.0.0").code, Some(0));
root.candidate(Some(&payload()));
assert_eq!(root.publish("2.0.0").code, Some(0));
let ran = root.migrate("2.0.0", &["--apply"]);
assert_eq!(ran.code, Some(0), "{ran:?}");
assert!(
ran.out
.contains("the lock records this migration on `--apply`"),
"the run states it: {ran:?}"
);
let lock =
std::fs::read_to_string(root.at.join(".headwater/taxonomy.lock")).expect("the lock reads");
assert!(
lock.contains(&format!(
"adoption:\n from:\n version: 1.0.0\n digest: \"{digest}\"\n to: 2.0.0\n"
)),
"the block states what this run measured: {lock}"
);
}
const NO_PIN_REMEDY: &str = " `.headwater/taxonomy.yml` pins no digest, so this run cannot write \
a verifiable `adoption.from` (HW-DR-0046). Take the digest the \
publisher states and write it as `taxonomy.digest` in \
`.headwater/taxonomy.yml`, by hand or with `headwater taxonomy vendor \
<dir-or-location> --expect <digest>`, and a later run of this verb \
records the migration. Every other file below is still written on \
`--apply`";
#[test]
fn apply_with_no_pinned_digest_writes_no_migration_state() {
let root = Root::new("no-digest-no-lock-write");
let consumer = root.read(".headwater/taxonomy.yml");
assert!(
consumer.contains("digest: "),
"the fixture pins one to start with"
);
let unpinned = consumer
.lines()
.filter(|line| !line.trim_start().starts_with("digest: "))
.collect::<Vec<_>>()
.join("\n")
+ "\n";
std::fs::write(root.at.join(".headwater/taxonomy.yml"), unpinned)
.expect("the consumer declaration writes");
let published = root.publish("1.0.0");
assert_eq!(published.code, Some(0));
let at = published
.out
.find("sha256:")
.expect("the publish prints the digest it wrote");
let digest = published.out[at..at + "sha256:".len() + 64].to_string();
root.candidate(Some(&payload()));
assert_eq!(root.publish("2.0.0").code, Some(0));
let before =
std::fs::read_to_string(root.at.join(".headwater/taxonomy.lock")).expect("the lock reads");
let ran = root.migrate("2.0.0", &["--apply"]);
assert_eq!(ran.code, Some(0), "{ran:?}");
assert!(
ran.out.contains(NO_PIN_REMEDY),
"the run states why, and the whole sentence stands: {ran:?}"
);
assert!(
ran.out.contains("wrote 1 value in 1 file"),
"every other file is still written: {ran:?}"
);
assert_eq!(
std::fs::read_to_string(root.at.join(".headwater/taxonomy.lock")).expect("the lock reads"),
before,
"and the lock is byte for byte what it was"
);
let maintained = root.at.join(".headwater/packages/headwater-standard");
let aside = root.at.join("maintained-source");
std::fs::rename(&maintained, &aside).expect("the maintained source moves aside");
let vendored = root.run(&[
"taxonomy",
"vendor",
root.released("1.0.0").to_str().expect("utf-8"),
"--expect",
&digest,
]);
assert_eq!(vendored.code, Some(0), "the check passes: {vendored:?}");
assert!(
root.read(".headwater/taxonomy.yml")
.contains(&format!(" digest: {digest}\n")),
"and it wrote the pin: {}",
root.read(".headwater/taxonomy.yml")
);
std::fs::remove_dir_all(&maintained).expect("the vendored artifact goes");
std::fs::rename(&aside, &maintained).expect("the maintained source comes back");
let after = root.migrate("2.0.0", &["--apply"]);
assert_eq!(after.code, Some(0), "{after:?}");
assert!(
!after.out.contains("pins no digest"),
"the run has a pin now: {after:?}"
);
let lock =
std::fs::read_to_string(root.at.join(".headwater/taxonomy.lock")).expect("the lock reads");
assert!(
lock.contains(&digest),
"and a later run of this verb records the migration, which is what the remedy \
promises: {lock}"
);
}
#[test]
fn an_apply_leaves_an_adoption_block_that_infer_can_add_to() {
let root = Root::new("apply-then-infer");
assert_eq!(root.publish("1.0.0").code, Some(0));
root.candidate_of(&FACET, Some(&inert_payload()));
root.in_bundle("decision-record", &REQUIRES_AUDIENCE);
assert_eq!(root.publish("2.0.0").code, Some(0));
let ran = root.migrate("2.0.0", &["--apply"]);
assert_eq!(ran.code, Some(0), "{ran:?}");
let written = root.read(".headwater/taxonomy.lock");
assert!(
written.contains("adoption:") && written.contains(" to: 2.0.0\n"),
"the run wrote the migration state: {written}"
);
assert!(
written.contains(" tasks: []\n"),
"and it wrote the empty standing list the next verb adds to: {written}"
);
root.takes("2.0.0");
let checked = root.run(&["check", "--now", "2026-08-01"]);
assert!(
checked.out.contains("0 pairs open"),
"the check layer reads the block as an empty inventory: {checked:?}"
);
let inferred = root.run(&[
"infer",
"--owner",
"tester",
"--until",
"2026-12-06",
"--now",
"2026-08-01",
"--write",
]);
assert_eq!(
inferred.code,
Some(0),
"the verb spec 7 names next can add to the block --apply left: {inferred:?}"
);
let lock = root.read(".headwater/taxonomy.lock");
assert!(
lock.contains("owner: tester"),
"the pairs this run derived are in the lock: {lock}"
);
assert!(
lock.contains("rule: facet.required.missing"),
"and the rule they are held under is the one the upgrade broke: {lock}"
);
}
#[test]
fn an_apply_repairs_a_block_that_declares_no_tasks() {
let root = Root::new("repairs-a-tasks-less-block");
assert_eq!(root.publish("1.0.0").code, Some(0));
root.candidate(Some(&payload()));
assert_eq!(root.publish("2.0.0").code, Some(0));
let lock = root.at.join(".headwater/taxonomy.lock");
let before = std::fs::read_to_string(&lock).expect("the lock reads");
assert!(
!before.contains("\nadoption:\n"),
"the fixture declares none to start with"
);
std::fs::write(
&lock,
before.replacen(
"\n# The resolved taxonomy",
"\nadoption:\n note: a key this engine does not read\n\n# The resolved taxonomy",
1,
),
)
.expect("the lock writes");
let ran = root.migrate("2.0.0", &["--apply"]);
assert_eq!(ran.code, Some(0), "{ran:?}");
let after = std::fs::read_to_string(&lock).expect("the lock reads");
assert!(
after.contains(" tasks: []\n"),
"the run added the empty sequence the next verb adds to: {after}"
);
assert!(
after.contains(" note: \"a key this engine does not read\"\n"),
"and it carried the key it does not read through: {after}"
);
}
#[test]
fn apply_carries_a_standing_task_list_through() {
let root = Root::new("carries-a-standing-list");
assert_eq!(root.publish("1.0.0").code, Some(0));
root.candidate(Some(&payload()));
assert_eq!(root.publish("2.0.0").code, Some(0));
let lock = root.at.join(".headwater/taxonomy.lock");
let before = std::fs::read_to_string(&lock).expect("the lock reads");
let standing = "\nadoption:\n tasks:\n - id: AD-1\n owner: a person\n until: \
2027-01-01\n pairs: []\n\n# The resolved taxonomy";
std::fs::write(
&lock,
before.replacen("\n# The resolved taxonomy", standing, 1),
)
.expect("the lock writes");
let ran = root.migrate("2.0.0", &["--apply"]);
assert_eq!(ran.code, Some(0), "{ran:?}");
let after = std::fs::read_to_string(&lock).expect("the lock reads");
assert!(
after.contains(
" tasks:\n - id: AD-1\n owner: \"a person\"\n until: 2027-01-01\n"
),
"the standing task is exactly what it was: {after}"
);
assert!(
!after.contains("tasks: []"),
"and nothing wrote an empty list over it: {after}"
);
}
#[test]
fn a_break_no_step_reaches_names_infer_rather_than_a_payload() {
let root = Root::new("no-step-reaches-it");
assert_eq!(root.publish("1.0.0").code, Some(0));
root.candidate_of(&FACET, None);
root.in_bundle("decision-record", &REQUIRES_AUDIENCE);
assert_eq!(root.publish("2.0.0").code, Some(0));
let ran = root.diff("2.0.0");
assert_eq!(ran.code, Some(0), "{ran:?}");
assert!(
ran.out.contains(
"the artifact ships no migration payload for 1.0.0 to 2.0.0, and 3 \
documents stopped validating. No step can express this break"
),
"the sentence names what cannot be written: {ran:?}"
);
assert!(
ran.out.contains(
"`headwater infer --owner <name> --write` records the breakage as adoption \
debt, and `headwater check` then reports it as migration-pending"
),
"and it names the route that is reachable: {ran:?}"
);
assert!(
!ran.out.contains("Spec 2 makes a major version ship one"),
"and it does not also ask for the file it just said cannot be written: {ran:?}"
);
}
#[test]
fn a_break_a_step_reaches_still_asks_for_a_payload() {
let root = Root::new("a-step-reaches-it");
assert_eq!(root.publish("1.0.0").code, Some(0));
root.candidate(None);
assert_eq!(root.publish("2.0.0").code, Some(0));
let ran = root.diff("2.0.0");
assert_eq!(ran.code, Some(0), "{ran:?}");
assert!(
ran.out.contains("Spec 2 makes a major version ship one"),
"a break the vocabulary reaches is a payload the publisher owes: {ran:?}"
);
assert!(
!ran.out.contains("No step can express this break"),
"and the other sentence is not printed beside it: {ran:?}"
);
}
#[test]
fn a_classification_only_break_still_asks_for_a_payload() {
let root = Root::new("classification-only-break");
root.without(" kinds.decision.language: ste_house\n");
assert_eq!(root.publish("1.0.0").code, Some(0));
root.candidate_of(&KIND, None);
assert_eq!(root.publish("2.0.0").code, Some(0));
let ran = root.diff("2.0.0");
assert_eq!(ran.code, Some(0), "{ran:?}");
assert_eq!(
ran.dimension("classification"),
"BROKEN, 3 of them",
"the break is the rename: {ran:?}"
);
assert_eq!(
ran.dimension("instance_validity"),
"preserved",
"and no rule moved, or the empty set this case is about is not empty: {ran:?}"
);
assert!(
ran.out.contains(
"the artifact ships no migration payload for 1.0.0 to 2.0.0, and 3 documents \
stopped validating. Spec 2 makes a major version ship one"
),
"a kind step reaches this break, so the publisher owes the file: {ran:?}"
);
assert!(
!ran.out.contains("No step can express this break"),
"and nothing tells the publisher the vocabulary cannot reach a rename of a kind: {ran:?}"
);
}
#[test]
fn an_artifact_with_no_payload_for_the_transition_is_refused() {
let root = Root::new("no-payload");
assert_eq!(root.publish("1.0.0").code, Some(0));
root.candidate(None);
assert_eq!(root.publish("2.0.0").code, Some(0));
let ran = root.migrate("2.0.0", &["--apply"]);
assert_eq!(ran.code, Some(1), "{ran:?}");
assert!(
ran.err
.contains("the artifact ships no migration payload for 1.0.0 to 2.0.0"),
"{ran:?}"
);
assert!(
!ran.err.contains("headwater --help"),
"the artifact is this repository's package and it ships no payload for the transition. \
No command line reads a payload that is not there:\n{}",
ran.err
);
}
#[test]
fn an_add_over_a_path_the_new_base_dropped_is_named_with_the_entry_that_makes_it() {
let root = Root::new("resurrection-is-named");
root.addresses(ENTRY);
assert_eq!(root.publish("1.0.0").code, Some(0));
root.candidate_of(&address(), None);
assert_eq!(root.publish("2.0.0").code, Some(0));
let ran = root.diff("2.0.0");
assert_eq!(
ran.code,
Some(0),
"the candidate resolves, which is what makes the state quiet: {ran:?}"
);
assert_eq!(
ran.dimension("addressability"),
"BROKEN, 1 of them",
"{ran:?}"
);
for line in [
"add.kinds.specification.identifier in .headwater/overlay.yml",
"makes `kinds.specification` rather than reaching into it",
"an address into `kinds.specification`, which the taxonomy under it declared",
] {
assert!(
ran.out.contains(line),
"the report states `{line}`: {ran:?}"
);
}
assert!(
ran.out.contains("this change requires a major version"),
"spec 2: any dimension broken forces a major: {ran:?}"
);
}
#[test]
fn an_overlay_address_the_new_base_still_declares_is_preserved() {
let root = Root::new("address-still-reaches");
root.addresses(ENTRY);
assert_eq!(root.publish("1.0.0").code, Some(0));
root.candidate(None);
assert_eq!(root.publish("2.0.0").code, Some(0));
let ran = root.diff("2.0.0");
assert_eq!(ran.code, Some(0), "{ran:?}");
assert_eq!(
ran.dimension("addressability"),
"preserved",
"the candidate moves three facet values and no address: {ran:?}"
);
assert_eq!(
ran.dimension("instance_validity"),
"BROKEN, 3 failed / 3 skipped",
"and the run did measure something, or this case proves nothing: {ran:?}"
);
}
#[test]
fn a_step_over_an_address_reports_the_overlay_entries_it_reaches() {
let root = Root::new("address-step-reports");
root.addresses(ENTRY);
assert_eq!(root.publish("1.0.0").code, Some(0));
root.candidate_of(&address(), Some(&addresses()));
let published = root.publish("2.0.0");
assert_eq!(published.code, Some(0), "{published:?}");
let ran = root.diff("2.0.0");
assert_eq!(ran.code, Some(0), "{ran:?}");
for line in [
"overlay_address kinds.specification",
"mechanical, and it becomes `kinds.norm`",
"remedies addressability",
"1 entry of this repository's overlay is addressed at or under it",
] {
assert!(
ran.out.contains(line),
"the report states `{line}`: {ran:?}"
);
}
}
#[test]
fn a_step_over_an_address_the_new_taxonomy_still_declares_stops_the_publish() {
let root = Root::new("address-source-stands");
root.addresses(ENTRY);
assert_eq!(root.publish("1.0.0").code, Some(0));
let payload = addresses().replacen(
" from: kinds.specification\n to: [kinds.norm]",
" from: kinds.decision\n to: [kinds.norm]",
1,
);
root.candidate_of(&address(), Some(&payload));
let published = root.publish("2.0.0");
assert_eq!(published.code, Some(1), "{published:?}");
assert!(
published
.err
.contains("the taxonomy this publishes still declares that overlay_address"),
"{published:?}"
);
}
#[test]
fn a_step_over_something_that_is_not_an_address_is_refused_when_the_payload_is_read() {
let root = Root::new("address-unparseable");
root.addresses(ENTRY);
assert_eq!(root.publish("1.0.0").code, Some(0));
let payload =
addresses().replacen("from: kinds.specification", "from: kinds..specification", 1);
root.candidate_of(&address(), Some(&payload));
let published = root.publish("2.0.0");
assert_eq!(published.code, Some(1), "{published:?}");
assert!(
published
.err
.contains("is not an address into a taxonomy, and an `overlay_address` step moves one"),
"{published:?}"
);
}
#[test]
fn apply_rewrites_the_overlay_address_beside_the_document() {
let root = Root::new("address-apply");
root.addresses(ENTRY);
assert_eq!(root.publish("1.0.0").code, Some(0));
root.candidate_of(&address(), Some(&addresses()));
assert_eq!(root.publish("2.0.0").code, Some(0));
assert!(
root.read(".headwater/overlay.yml")
.contains("kinds.specification.identifier: {scheme: spec_id}"),
"the entry is the one the fixture added"
);
let ran = root.migrate("2.0.0", &["--apply"]);
assert_eq!(ran.code, Some(0), "{ran:?}");
assert!(ran.out.contains("wrote 2 values in 3 files"), "{ran:?}");
assert!(
ran.out.contains(
".headwater/overlay.yml add.kinds.specification.identifier becomes \
`kinds.norm.identifier`"
),
"{ran:?}"
);
let overlay = root.read(".headwater/overlay.yml");
assert!(
overlay.contains("kinds.norm.identifier: {scheme: spec_id}"),
"the address moved and the value beside it did not: {overlay}"
);
assert!(
!overlay.contains("kinds.specification"),
"no old address survives: {overlay}"
);
assert!(
overlay.contains("kinds.decision_register.identifier:"),
"an address the step does not reach is untouched: {overlay}"
);
assert!(
root.read("docs/decisions/0003-the-document-still-being-written.md")
.contains("status: outline"),
"the document half of the same write set landed"
);
}
#[test]
fn an_unwritable_overlay_leaves_the_document_beside_it_untouched() {
let root = Root::new("address-apply-refuses");
root.addresses(ENTRY);
assert_eq!(root.publish("1.0.0").code, Some(0));
root.candidate_of(&address(), Some(&addresses()));
assert_eq!(root.publish("2.0.0").code, Some(0));
let document = root.read("docs/decisions/0003-the-document-still-being-written.md");
let overlay = root.read(".headwater/overlay.yml");
root.read_only(".headwater/overlay.yml");
let ran = root.migrate("2.0.0", &["--apply"]);
assert_eq!(ran.code, Some(1), "{ran:?}");
assert!(
ran.err
.contains("a file of this migration cannot be written, so none was"),
"{ran:?}"
);
assert!(ran.err.contains(".headwater/overlay.yml"), "{ran:?}");
assert_eq!(
root.read("docs/decisions/0003-the-document-still-being-written.md"),
document,
"the document is in the same write set as the overlay, so neither moved"
);
assert_eq!(root.read(".headwater/overlay.yml"), overlay);
}
const REGISTER: &str = "---\nid: HW-REG-the-register\ntitle: The register of settled \
decisions\nstatus: current\nstatus_since: 2026-08-01\nlast_verified: \
2026-08-01\nsummary: The one document of this scratch corpus that \
records what the corpus settled.\ndoc_type: \
decision_register\nsequence: 9\nprovenance:\n warrant: asserted\n \
agency: model\n evidence_basis: unevidenced\n---\n\n# 9 — The register \
of settled decisions\n\nThis document takes its kind from the `doc_type` \
key above.\n\nA bundle of the package declares that kind, and the base \
package declares no such kind.\n";
const STANDS: &str = "the taxonomy this artifact gives this repository still declares the old \
value, so this step does not complete here";
const NO_OP: &str = "no document of this corpus carries the old value, so this step is a no-op \
here";
#[test]
fn a_step_whose_source_this_selection_still_declares_is_reported_by_diff() {
let root = Root::new("bundle-source-stands-diff");
assert_eq!(root.publish("1.0.0").code, Some(0));
root.candidate_of(&[], Some(®ister_payload("specification")));
assert_eq!(root.publish("2.0.0").code, Some(0));
let ran = root.diff("2.0.0");
assert_eq!(ran.code, Some(0), "{ran:?}");
assert!(
ran.out.contains("kind decision_register"),
"the report reached the step: {ran:?}"
);
assert!(
ran.out.contains("remedies classification"),
"the report accounted the step: {ran:?}"
);
assert!(ran.out.contains(STANDS), "{ran:?}");
assert!(
!ran.out.contains(NO_OP),
"the no-op sentence is what this state was being told as: {ran:?}"
);
}
#[test]
fn a_step_whose_source_this_selection_no_longer_declares_keeps_its_sentence() {
let root = Root::new("bundle-source-moved-diff");
assert_eq!(root.publish("1.0.0").code, Some(0));
root.in_bundle("design-spec", &BUNDLE_KIND);
root.candidate_of(&[], Some(®ister_payload("ruling_register")));
assert_eq!(root.publish("2.0.0").code, Some(0));
let ran = root.diff("2.0.0");
assert_eq!(ran.code, Some(0), "{ran:?}");
assert!(
ran.out.contains("kind decision_register"),
"the report reached the step: {ran:?}"
);
assert!(ran.out.contains(NO_OP), "{ran:?}");
assert!(
!ran.out.contains(STANDS),
"the source moved for this selection: {ran:?}"
);
}
#[test]
fn an_apply_refuses_a_step_whose_source_this_selection_still_declares() {
let root = Root::new("bundle-source-stands-apply");
let path = root.register();
assert_eq!(root.publish("1.0.0").code, Some(0));
root.candidate_of(&[], Some(®ister_payload("specification")));
assert_eq!(root.publish("2.0.0").code, Some(0));
let before = root.read(path);
assert!(
before.contains("doc_type: decision_register"),
"the document takes the bundle-declared kind"
);
let ran = root.migrate("2.0.0", &["--apply"]);
assert_eq!(ran.code, Some(1), "{ran:?}");
assert!(
ran.out.contains("kind decision_register"),
"the run reached the step: {ran:?}"
);
assert!(ran.out.contains(STANDS), "{ran:?}");
assert!(
ran.err.contains("would rewrite"),
"the refusal says what it stopped: {ran:?}"
);
assert_eq!(root.read(path), before, "no byte of the document moved");
assert!(
!root.read(".headwater/taxonomy.lock").contains("adoption:"),
"a refused run records no migration: {ran:?}"
);
}
#[test]
fn an_apply_writes_a_bundle_declared_kind_the_artifact_really_moved() {
let root = Root::new("bundle-source-moved-apply");
let path = root.register();
assert_eq!(root.publish("1.0.0").code, Some(0));
root.in_bundle("design-spec", &BUNDLE_KIND);
root.candidate_of(&[], Some(®ister_payload("ruling_register")));
assert_eq!(root.publish("2.0.0").code, Some(0));
let ran = root.migrate("2.0.0", &["--apply"]);
assert_eq!(ran.code, Some(0), "{ran:?}");
assert!(
root.read(path).contains("doc_type: ruling_register"),
"the step wrote the one key it names: {ran:?}"
);
assert!(
!ran.out.contains(STANDS),
"the source moved for this selection: {ran:?}"
);
}
#[test]
fn a_lock_already_on_the_artifacts_version_is_refused() {
let root = Root::new("lock-already-on-the-version");
assert_eq!(root.publish("1.0.0").code, Some(0));
root.candidate(Some(&payload()));
assert_eq!(root.publish("2.0.0").code, Some(0));
root.heads("2.0.0");
for flags in [&[][..], &["--apply"][..]] {
let ran = root.migrate("2.0.0", flags);
assert_eq!(ran.code, Some(1), "{ran:?}");
assert!(
ran.err.contains("the lock already takes 2.0.0"),
"the refusal names the lock and the version it holds: {ran:?}"
);
assert!(
ran.err.contains("no earlier version to migrate from"),
"and says what that leaves this run nothing to do: {ran:?}"
);
assert!(
!ran.err.contains("Spec 2 makes a major version ship one"),
"the publisher shipped a sound artifact: {ran:?}"
);
assert!(
!ran.err.contains("headwater infer"),
"and no remedy records debt for a break that does not exist: {ran:?}"
);
}
}
#[test]
fn the_ordinary_upgrade_is_not_refused() {
let root = Root::new("ordinary-upgrade-still-runs");
assert_eq!(root.publish("1.0.0").code, Some(0));
root.candidate(Some(&payload()));
assert_eq!(root.publish("2.0.0").code, Some(0));
let ran = root.migrate("2.0.0", &[]);
assert_eq!(ran.code, Some(0), "{ran:?}");
assert!(
ran.out.contains("from 1.0.0 to 2.0.0"),
"the report still names the pair it read off the lock: {ran:?}"
);
assert!(
ran.out.contains("1-to-2.yml"),
"and the payload it selected: {ran:?}"
);
}
#[test]
fn an_artifact_below_the_locked_version_is_refused() {
let root = Root::new("artifact-below-the-lock");
assert_eq!(root.publish("1.0.0").code, Some(0));
root.candidate(Some(&payload()));
assert_eq!(root.publish("2.0.0").code, Some(0));
root.heads("3.0.0");
let ran = root.migrate("2.0.0", &[]);
assert_eq!(ran.code, Some(1), "{ran:?}");
assert!(
ran.err
.contains("the lock takes 3.0.0 and this artifact publishes 2.0.0"),
"the refusal names both sides: {ran:?}"
);
assert!(
ran.err.contains("runs forward only"),
"and why that is not a migration: {ran:?}"
);
assert!(!ran.err.contains("headwater infer"), "{ran:?}");
}
fn repoint_bundles(package: &std::path::Path) {
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}`");
std::fs::write(&manifest, text.replace(from, &bundles_line()))
.expect("the scratch manifest writes");
}
fn bundles_line() -> String {
let up = "../".repeat(headwater_resolve::package::PACKAGES.split('/').count() + 1);
format!(" bundles: {up}docs/taxonomies")
}