Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/nornir

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();
}