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

nordisk/edda

crates/nornir-build-thing/tests/recipe_faithful.rs

//! **Every recipe in the constellation, read in FULL by this reader — or named.**
//!
//! The guard for the 2026-08-30 defect (lane AJ), which was not that a recipe
//! failed to load but that one loaded and lost:
//!
//! ```text
//! nornir-build-thing 0.1.10 over gunnar's committed .nornir/build-variants.toml
//!   REFUSED  unknown field `kernel_requires` … [[build.iso.options.kernel_requires]]
//!   and with that key removed so the load can finish:
//!   DROPS    build.installer.0.data   build.installer.0.target
//!            build.installer.1.data   build.installer.1.target
//! ```
//!
//! Four declarations — the machine each medium is built for, and what `/data`
//! is laid with — read by nobody for four pin generations, with gunnar's own
//! `tests/toml_parity.rs` GREEN over them the whole time. It was green because a
//! parity mirror compares the file against the SAME loader that dropped the
//! rows: two sides that agree about nothing.
//!
//! This test is the half a parity test structurally cannot be. It does not
//! compare the file to a mirror; it asks the loader **what it did not read**
//! ([`nornir_build_thing::schema`], R3) and fails with the path.
//!
//! # Seen RED
//!
//! ```text
//! $ cargo test -p nornir-build-thing --test recipe_faithful
//! ---- every_declared_recipe_is_read_in_full stdout ----
//!   gunnar (…/gunnar/.nornir/build-variants.toml): this reader would SILENTLY
//!   DROP 4 declarations:
//!     build.installer.0.data
//!     build.installer.0.target
//!     build.installer.1.data
//!     build.installer.1.target
//! ```
//! measured by pointing an OLD reader at the same roster
//! (`crates/nornir-build-thing-skew`, which links 0.1.10 and this crate in one
//! process). Against THIS reader the roster is green, which is the claim being
//! made: nothing in the constellation declares a key this pin cannot read.
//!
//! A guard with no subject on disk is a false green, so a missing checkout is a
//! FAILURE here and not a skip — the same rule `vocabulary_compat.rs` follows.

use std::path::{Path, PathBuf};

use nornir_build_thing::schema::{self, RecipeAudit};
use nornir_build_thing::variant::BuildVariantSet;

/// **The roster.** Every repo in the constellation that ships a
/// `.nornir/build-variants.toml`, measured 2026-08-30. A repo that grows one
/// belongs here the day it does: an omission reads as "there is no such recipe",
/// which is the one thing a roster must never read as.
const ROSTER: &[&str] = &[
    "gunnar",
    "korp",
    "holger",
    "skade",
    "nornir",
    "tunnr",
    "znippy",
    "znippy-zoomies",
    "modgunn",
    "ordning",
    "dwarves",
    "edda",
    "knut",
    "Skidbladnir",
    "draupnir",
];

/// The directory the sibling checkouts live in — edda's parent.
fn constellation_root() -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR"))
        .ancestors()
        .nth(3)
        .expect("crates/nornir-build-thing has three ancestors up to ~/git")
        .to_path_buf()
}

#[test]
fn the_roster_has_a_subject_on_disk() {
    let root = constellation_root();
    // Two different defects, named apart: a sibling that was never CLONED here
    // (the release-3 constellation at ~/d-release3/src had no `korp` and no
    // `ordning`), and a checkout that no longer SHIPS a recipe.
    let absent: Vec<&str> = ROSTER.iter().copied().filter(|r| !root.join(r).is_dir()).collect();
    let no_recipe: Vec<&str> = ROSTER
        .iter()
        .copied()
        .filter(|r| root.join(r).is_dir())
        .filter(|r| !root.join(r).join(".nornir/build-variants.toml").is_file())
        .collect();
    assert!(
        absent.is_empty() && no_recipe.is_empty(),
        "the guard's subject is the CONSTELLATION under {}.\n  \
         sibling checkouts ABSENT (clone each beside edda): {absent:?}\n  \
         checked out but WITHOUT .nornir/build-variants.toml: {no_recipe:?}\n\
         A compat guard that skips its missing subjects goes green having read nothing — \
         clone them, or take the repo off the roster with the reason.",
        root.display()
    );
}

/// **R3 over the real declarations.** Every recipe on disk, read in full.
#[test]
fn every_declared_recipe_is_read_in_full() {
    let root = constellation_root();
    let mut problems: Vec<String> = Vec::new();
    let mut read = 0usize;

    for repo in ROSTER {
        let path = root.join(repo).join(".nornir/build-variants.toml");
        let Ok(text) = std::fs::read_to_string(&path) else { continue };
        read += 1;
        let audit = RecipeAudit::of(&text);
        if let Some(why) = &audit.refused {
            problems.push(format!("{repo} ({}): REFUSED — {why}", path.display()));
            continue;
        }
        if !audit.dropped.is_empty() {
            problems.push(format!(
                "{repo} ({}): this reader would SILENTLY DROP {} declaration(s):\n    {}",
                path.display(),
                audit.dropped.len(),
                audit.dropped.join("\n    "),
            ));
        }
    }

    assert!(read >= 3, "read {read} recipes — the roster found nothing to judge");
    assert!(
        problems.is_empty(),
        "the recipes below declare something this reader does not read:\n\n{}\n\n\
         Every one of these is a group of the build that no artifact will carry. \
         Raise the repo's `nornir-build-thing` pin, or change the recipe in Rust \
         and re-emit — never hand-edit the row to match what the loader kept.",
        problems.join("\n\n")
    );
}

/// **R1 over the real declarations.** A recipe that needs more than the floor
/// must SAY so, or an older reader has no way to refuse by name.
#[test]
fn a_recipe_that_needs_a_newer_reader_declares_its_schema() {
    let root = constellation_root();
    let mut problems: Vec<String> = Vec::new();

    for repo in ROSTER {
        let path = root.join(repo).join(".nornir/build-variants.toml");
        let Ok(text) = std::fs::read_to_string(&path) else { continue };
        let Ok(set) = BuildVariantSet::from_toml_str(&text) else { continue };
        let (needs, why) = schema::required_by(&set);
        let declared = schema::declared_schema(&text).unwrap_or(schema::RECIPE_SCHEMA_FLOOR);
        if declared < needs {
            problems.push(format!(
                "{repo} ({}) declares schema {declared} but needs {needs}, because of:\n    {}\n  \
                 An older reader will not refuse it by name — it will drop those rows in \
                 silence, which is measured to happen (published 0.1.10 over gunnar).",
                path.display(),
                why.join("\n    "),
            ));
        }
    }

    assert!(problems.is_empty(), "{}", problems.join("\n\n"));
}

/// **The schema header is not itself a compatibility hazard.**
///
/// R1's whole argument is that the ROOT is the one place every published
/// generation ignores. If the header were ever moved inside `[build]` —
/// `deny_unknown_fields` — the version marker would become the next outage. This
/// pins it where it is: a recipe carrying the header still loads, and the header
/// is not reported as a dropped key.
#[test]
fn the_header_lives_where_every_generation_can_ignore_it() {
    let text = format!(
        "{}\n[build]\noffers = [\"gui\"]\n\n[[build.variant]]\nname = \"app\"\nfeatures = [\"gui\"]\n",
        schema::header_toml(schema::RECIPE_SCHEMA)
    );
    assert!(text.contains("[nornir-build-thing]"), "the header is a ROOT table");
    assert!(
        !text.contains("[build.nornir-build-thing]"),
        "inside [build] it would hit `deny_unknown_fields` on BuildVariantSet and \
         make the version marker the next outage"
    );
    let audit = RecipeAudit::of(&text);
    assert!(audit.is_faithful(), "{audit:?}");
    assert_eq!(audit.declared, Some(schema::RECIPE_SCHEMA));
}