tests/docs_claims.rs
//! The `nornir:claim:` gate on the real render path (sweep-25-22).
//!
//! The renderer used to be a formatter — `.nornir/*.md` prose copied through
//! with no check, so `.nornir/design.md` carried "stripping those lines from
//! the generator is a pending code change" for TWO MONTHS after
//! `0c240f9`/`9ea97c4` removed the `NORNIR_MONITOR`/`NORNIR_POLL` wiring,
//! while `src/bin/nornir.rs`'s own unit test was green asserting the removal.
//! These tests prove the gate that closes that: every render entry point
//! evaluates doc claims against the tree and REFUSES — artifact untouched —
//! when one no longer holds. Red proof (2026-08-25): with the stale paragraph's
//! own assertion encoded as
//! `present "Environment=NORNIR_MONITOR" in src/bin/nornir.rs`, the gate on
//! this repo refused with `claimed PRESENT but nothing under
//! src/bin/nornir.rs contains the needle` — the two-month-old real defect,
//! caught by the mechanism before the prose was fixed.
use std::fs;
use std::path::Path;
use nornir::docs;
/// A minimal repo the render path accepts: a `.nornir/` source set and a
/// `src/` tree for claims to speak about.
fn fixture(root: &Path) {
fs::create_dir_all(root.join(".nornir")).unwrap();
fs::create_dir_all(root.join("src")).unwrap();
fs::write(
root.join("src/lib.rs"),
"pub fn shipped() {}\n// the OLD_WIRING was removed\n",
)
.unwrap();
}
fn ctx(root: &Path) -> docs::Ctx<'_> {
docs::Ctx::new(root, root, None)
}
#[test]
fn a_repo_without_claims_renders_exactly_as_before() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
fixture(root);
fs::write(root.join(".nornir/README.md"), "# fixture\n\nplain prose\n").unwrap();
let reports = docs::render_all(&docs::RepoLayout::new(root), &ctx(root)).unwrap();
assert_eq!(reports.len(), 1, "README should render");
let out = fs::read_to_string(root.join("README.md")).unwrap();
assert!(out.contains("plain prose"));
}
#[test]
fn render_all_refuses_a_false_claim_and_leaves_the_committed_artifact_untouched() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
fixture(root);
let layout = docs::RepoLayout::new(root);
// First render with a TRUE claim — both verbs — goes green and writes.
fs::write(
root.join(".nornir/README.md"),
"# fixture\n\
<!-- nornir:claim: absent \"OLD_WIRING was reintroduced\" in src -->\n\
<!-- nornir:claim: present \"pub fn shipped\" in src/lib.rs -->\n\
the wiring is gone\n",
)
.unwrap();
docs::render_all(&layout, &ctx(root)).unwrap();
let committed = fs::read_to_string(root.join("README.md")).unwrap();
// The tree changes under the doc: the claimed-absent needle appears.
fs::write(
root.join("src/lib.rs"),
"pub fn shipped() {}\n// OLD_WIRING was reintroduced\n",
)
.unwrap();
// The very next render REFUSES — not a warning, an Err — and the
// committed artifact is byte-for-byte what it was (SK190: refusing is
// fine; silently emitting is not).
let err = docs::render_all(&layout, &ctx(root))
.unwrap_err()
.to_string();
assert!(err.contains("docs claims REFUSED"), "{err}");
assert!(err.contains(".nornir/README.md:2"), "{err}");
assert!(err.contains("claimed ABSENT"), "{err}");
let after = fs::read_to_string(root.join("README.md")).unwrap();
assert_eq!(committed, after, "refusal must not touch the artifact");
}
#[test]
fn render_sources_in_place_refuses_too_and_the_source_survives() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
fixture(root);
let src = "# design\n<!-- nornir:claim: present \"fn removed_in_june\" in src/lib.rs -->\n";
fs::write(root.join(".nornir/design.md"), src).unwrap();
let layout = docs::RepoLayout::new(root);
let err = docs::render_sources_in_place(&layout, &ctx(root))
.unwrap_err()
.to_string();
assert!(err.contains("claimed PRESENT"), "{err}");
let after = fs::read_to_string(root.join(".nornir/design.md")).unwrap();
assert_eq!(src, after, "refusal must not touch the source");
}
#[test]
fn a_malformed_claim_is_an_error_never_a_silent_pass() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
fixture(root);
// Verb the gate does not know — treating it as satisfied would make the
// whole mechanism worthless.
fs::write(
root.join(".nornir/README.md"),
"# fixture\n<!-- nornir:claim: verified \"anything\" in src -->\n",
)
.unwrap();
let layout = docs::RepoLayout::new(root);
let err = docs::render_all(&layout, &ctx(root))
.unwrap_err()
.to_string();
assert!(err.contains("MALFORMED"), "{err}");
assert!(err.contains("unknown verb `verified`"), "{err}");
assert!(
!root.join("README.md").exists(),
"nothing may be rendered past a marker the gate does not recognise"
);
}
/// THE LIVE GUARD: every claim in this repo's own `.nornir/*.md` must hold
/// against this tree. This is the test that goes red the day a doc's claimed
/// sentence and the code diverge again — the check the June
/// `NORNIR_MONITOR`/`NORNIR_POLL` removal never had.
#[test]
fn every_claim_in_this_repos_docs_holds_against_this_tree() {
let root = Path::new(env!("CARGO_MANIFEST_DIR"));
if let Err(e) = docs::gate_doc_claims(root) {
panic!("{e}");
}
}
// ── Parser-level coverage (moved from `src/docs/claims.rs`'s cfg(test) module;
// see the note at the bottom of that file). Everything asserted here is on the
// pub surface: `docs::claims::{scan, gate_repo}`.
use nornir::docs::claims::{self, ClaimKind};
use std::path::PathBuf;
fn doc() -> PathBuf {
PathBuf::from(".nornir/design.md")
}
#[test]
fn parses_both_verbs_with_line_numbers() {
let text = "prose\n<!-- nornir:claim: absent \"FOO\" in src -->\n\
more\n<!-- nornir:claim: present \"a b\" in src/lib.rs -->\n";
let parsed = claims::scan(&doc(), text).unwrap();
assert_eq!(parsed.len(), 2);
assert_eq!(parsed[0].kind, ClaimKind::Absent);
assert_eq!(parsed[0].needle, "FOO");
assert_eq!(parsed[0].path, "src");
assert_eq!(parsed[0].line, 2);
assert_eq!(parsed[1].kind, ClaimKind::Present);
assert_eq!(parsed[1].needle, "a b"); // spaces survive quoting
assert_eq!(parsed[1].line, 4);
}
#[test]
fn no_marker_means_no_claims() {
assert!(
claims::scan(&doc(), "plain prose, no markers")
.unwrap()
.is_empty()
);
}
#[test]
fn unknown_verb_is_an_error_not_a_pass() {
let err = claims::scan(&doc(), "<!-- nornir:claim: exists \"X\" in src -->")
.unwrap_err()
.to_string();
assert!(err.contains("unknown verb `exists`"), "{err}");
assert!(err.contains("never a silent pass"), "{err}");
}
#[test]
fn malformed_variants_all_refuse() {
for bad in [
"<!-- nornir:claim: -->", // empty
"<!-- nornir:claim: absent FOO in src -->", // unquoted
"<!-- nornir:claim: absent \"FOO in src -->", // unterminated
"<!-- nornir:claim: absent \"\" in src -->", // empty needle
"<!-- nornir:claim: absent \"FOO\" src -->", // missing `in`
"<!-- nornir:claim: absent \"FOO\" in -->", // empty path
"<!-- nornir:claim: absent \"FOO\" in /etc -->", // absolute
"<!-- nornir:claim: absent \"FOO\" in ../up -->", // escapes root
"<!-- nornir:claim: absent \"FOO\" index -->", // `in` not a token
] {
let err = claims::scan(&doc(), bad);
assert!(err.is_err(), "should refuse: {bad}");
let msg = err.unwrap_err().to_string();
assert!(msg.contains("MALFORMED"), "{bad} → {msg}");
}
}
#[test]
fn a_path_the_tree_does_not_have_is_a_failure_for_both_verbs() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
fs::create_dir_all(root.join(".nornir")).unwrap();
for verb in ["absent", "present"] {
fs::write(
root.join(".nornir/design.md"),
format!("<!-- nornir:claim: {verb} \"X\" in src/typo.rs -->\n"),
)
.unwrap();
let err = claims::gate_repo(root).unwrap_err().to_string();
assert!(err.contains("does not exist"), "{verb}: {err}");
}
}
#[test]
fn dir_search_recurses_and_skips_git_and_target() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
fs::create_dir_all(root.join(".nornir")).unwrap();
fs::create_dir_all(root.join("src/deep/deeper")).unwrap();
fs::create_dir_all(root.join("src/.git")).unwrap();
fs::create_dir_all(root.join("src/target")).unwrap();
fs::write(root.join("src/deep/deeper/x.rs"), "NEEDLE_A").unwrap();
fs::write(root.join("src/.git/blob"), "NEEDLE_B").unwrap();
fs::write(root.join("src/target/out"), "NEEDLE_B").unwrap();
fs::write(
root.join(".nornir/design.md"),
"<!-- nornir:claim: present \"NEEDLE_A\" in src -->\n\
<!-- nornir:claim: absent \"NEEDLE_B\" in src -->\n",
)
.unwrap();
claims::gate_repo(root).unwrap();
}