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/package_kind.rs

//! **HO-T5 — a produced FILE SET is a declarable kind.**
//!
//! Until this test, `ArtifactKind` was `Lib | Exe | Wasm | Kernel | Container |
//! Service | Vm | Iso | Installer`, and a pip wheel was none of them. facett's
//! wheel is real and released — but only through its own `cargo xtask
//! publish-pip`, because `dwarves buildthing facett --variant facet-wrapped-web`
//! had no kind to route on. `facett-ops/tests/release_manifest.rs` in facett says so in
//! its own module docs: *"That needs the forge to have an artifact kind for a
//! **package**, and it does not."*
//!
//! `Wasm` was the tempting near-miss and is the wrong answer: it would make the
//! forge build a bare `.wasm` — not run wasm-bindgen, not build the wheel, not
//! verify it, and not leave a checkable release behind. The distinction the kind
//! exists to draw is that **a package's product is a SET of files**, not one
//! executable: a wheel AND an sdist AND the `SHA256SUMS` a downloader checks them
//! with AND a provenance record over the produced bytes. A kind whose output is
//! "the binary" cannot describe that, which is why `Exe` is not it either.
//!
//! # What is asserted here, and why each arm can fail
//!
//! 1. **The kind round-trips through the EMITTED toml.** Per the 2026-08-27 law
//!    the CODE is the source and `.nornir/build-variants.toml` is emitted to
//!    DESCRIBE, never read back as authority — so the thing that must hold is not
//!    "the file parses" but "what the code declared survives being written down".
//!    `emit::recipe_toml` round-trips internally; this test asserts on the TEXT as
//!    well, because an emission that wrote `kind = "exe"` for a package would still
//!    read back equal to a set that had ALSO lost the kind.
//! 2. **The spelling is `package`,** and it is reachable from the string form both
//!    ways — the `From<String>` a hand-written recipe goes through and the
//!    `as_str` an attestation records. An `ArtifactKind::from` that fell through to
//!    its `_ => Exe` default is the silent failure this guards: a `kind =
//!    "package"` row would build a cargo binary and say nothing.
//! 3. **The cell survives.** `[[build.package]]` carries the file set; a variant
//!    declaring `kind = "package"` with no cell is refused BY NAME.
//! 4. **A package's product is a file set, not one executable** — the two
//!    discriminants (`product_is_not_a_file`, `produces_no_executable`) that decide
//!    whether "exited 0 and produced no artifact" is a bug or the correct outcome.

use nornir_build_thing::emit::recipe_toml;
use nornir_build_thing::package::{PackageBuild, PackageFormat};
use nornir_build_thing::variant::{ArtifactKind, BuildVariant, BuildVariantSet, Profile};

/// The declaration under test: one variant of the new kind, and its cell.
fn a_package_set() -> BuildVariantSet {
    BuildVariantSet {
        variants: vec![BuildVariant {
            name: "facet-wrapped-web".into(),
            kind: ArtifactKind::Package,
            package: Some("facet-wrapped".into()),
            profile: Profile::Release,
            ..BuildVariant::default()
        }],
        packages: vec![PackageBuild {
            name: "facet-wrapped-web".into(),
            format: PackageFormat::Pip,
            distribution: "facet_wrapped_web".into(),
            out_dir: "pkg-facet-wrapped/dist".into(),
            sdist: true,
            ..PackageBuild::default()
        }],
        ..BuildVariantSet::default()
    }
}

/// ① + ②: the kind survives the emission, spelled `package`, and reads back.
#[test]
fn the_package_kind_round_trips_through_the_emitted_toml() {
    let set = a_package_set();
    let text = recipe_toml("# emitted, not source.", &set).expect("the set emits");

    // The TEXT, not only the value: an emission that dropped the kind would read
    // back equal to a set that had also lost it, and this whole law is about the
    // written description matching the code.
    assert!(
        text.contains("kind = \"package\""),
        "the emitted toml does not spell the kind:\n{text}"
    );
    assert!(
        text.contains("[[build.package]]"),
        "the emitted toml carries no package cell:\n{text}"
    );

    let back = BuildVariantSet::from_toml_str(&text).expect("the emission reads back");
    assert_eq!(back, set, "the emission is not the declaration");
    assert_eq!(
        back.kind_of("facet-wrapped-web"),
        Some(ArtifactKind::Package),
        "the variant came back as some other kind"
    );
    assert_eq!(back.packages.len(), 1, "the package cell did not survive");
    assert_eq!(back.packages[0].distribution, "facet_wrapped_web");
    assert!(back.packages[0].sdist, "the sdist requirement did not survive");
}

/// ②, the half that fails silently: the string form, both directions.
#[test]
fn the_string_form_is_package_in_both_directions() {
    assert_eq!(ArtifactKind::Package.as_str(), "package");
    for spelling in ["package", "Package", " package ", "wheel", "pip"] {
        assert_eq!(
            ArtifactKind::from(spelling.to_string()),
            ArtifactKind::Package,
            "`{spelling}` did not read as a package — `From<String>` falls through \
             to Exe, so a `kind = \"{spelling}\"` row would build a cargo binary and \
             say nothing"
        );
    }
}

/// ③: a `kind = "package"` variant with no cell is refused, and it is refused BY
/// NAME. The peer of every other cell kind's contract.
#[test]
fn a_package_variant_without_a_cell_is_refused_by_name() {
    let mut set = a_package_set();
    set.packages.clear();
    let err = set.validate_all().expect_err("a package with no cell must be refused");
    let msg = format!("{err:#}");
    assert!(
        msg.contains("facet-wrapped-web") && msg.to_lowercase().contains("package"),
        "the refusal names neither the variant nor the kind: {msg}"
    );
}

/// ④: the two discriminants that decide whether "produced nothing" is a bug.
///
/// A package IS files on disk — so it is NOT exempt from `produced_something()`
/// the way a container image is, and a green build that wrote no wheel must read
/// as the failure it is. That is the exact defect
/// [`ArtifactKind::product_is_not_a_file`]'s docs record for
/// `gunnar-appliance-iso`: an exemption written for containers, applied to a kind
/// whose product is a file, and a build that produced nothing exiting 0.
#[test]
fn a_package_is_a_file_set_on_disk_and_is_not_exempt_from_producing_one() {
    assert!(
        !ArtifactKind::Package.product_is_not_a_file(),
        "a wheel is a FILE — exempting a package from `produced_something()` is how \
         a build that wrote nothing exits 0"
    );
    assert!(
        !ArtifactKind::Package.is_cargo(),
        "`cargo build` does not build a wheel — routing a package through the cargo \
         leg would build the library and call it a release"
    );
    assert!(
        !ArtifactKind::Package.is_service_shaped(),
        "a package has no fat/thin shape"
    );
    // THE one that fails silently. dwarves' `produced_something()` is
    // `!artifacts.is_empty() || (produced_ref && product_is_not_a_file) ||
    // produces_no_executable()`, so a `true` here is a kind whose build reports
    // GREEN having written nothing at all.
    assert!(
        !ArtifactKind::Package.produces_no_executable(),
        "a package that produced no files FAILED — answering `true` here makes \
         `produced_something()` green for a build that wrote nothing, which is the \
         gunnar-appliance-iso defect arriving by a new road"
    );
}

/// The kind's row in every table that must have one. These lists were HAND-WRITTEN
/// per kind before this row and would have stayed green with `Package` missing from
/// all of them — the hollow-guard shape LAW 2 names. They are driven off
/// `ArtifactKind::ALL` now, so this test is the witness that ALL really holds it.
#[test]
fn the_new_kind_has_a_row_in_every_table_keyed_by_kind() {
    assert!(
        ArtifactKind::ALL.contains(&ArtifactKind::Package),
        "ALL does not name the kind, so every table derived from it silently skips it"
    );
    // A verb row must EXIST (it may legitimately be empty — a wheel is not run and
    // not booted); the failure being guarded is `actions_for` falling through its
    // `unwrap_or_default` for a kind nobody gave a row.
    assert!(
        nornir_build_thing::verbs::VERBS
            .iter()
            .any(|(k, _)| *k == ArtifactKind::Package),
        "no verb row: `actions_for` would answer the empty list by ACCIDENT"
    );
    assert!(
        nornir_build_thing::verbs::actions_for(ArtifactKind::Package).is_empty(),
        "a wheel is not run and not booted — pip installs it"
    );
    assert_eq!(
        nornir_build_thing::contract::PaneKind::for_artifact(ArtifactKind::Package),
        nornir_build_thing::contract::PaneKind::None,
        "there is nothing to run: the pane shows the file set and its receipt"
    );
}

/// **R1/R2 — the emission SAYS which reader it needs, and an older one refuses by
/// name instead of by serde.**
///
/// [`BuildVariantSet`] is `deny_unknown_fields`, so a reader that predates
/// `[[build.package]]` refuses the WHOLE recipe with `unknown field \`package\``,
/// at a line number in a file the operator did not write, naming a word they
/// spelled correctly. That is the exact failure `schema`'s module docs record for
/// gunnar's `kernel_requires`, and R2 exists to convert it into "upgrade
/// nornir-build-thing to >= 0.1.13, because this declares [[build.package]]".
///
/// It only works if the header is EMITTED. A schema bump with a `required_by` that
/// never asks for it is a version number nobody writes down — so the assertion is
/// on the emitted TEXT.
#[test]
fn a_recipe_with_a_package_declares_the_schema_that_can_read_it() {
    use nornir_build_thing::schema::{first_reader_of, required_by, RECIPE_SCHEMA_FLOOR};

    let set = a_package_set();
    let (schema, why) = required_by(&set);
    assert!(
        schema > RECIPE_SCHEMA_FLOOR,
        "a package cell left the recipe at the floor, so no header is emitted and \
         an old reader meets a serde error instead of a named refusal"
    );
    assert_eq!(schema, 3, "the package vocabulary is schema 3");
    assert!(
        why.iter().any(|w| w.contains("[[build.package]]") && w.contains("facet-wrapped-web")),
        "the reason does not name the cell that asked for it: {why:?}"
    );
    assert_eq!(first_reader_of(3), "0.1.13", "R2 would name a reader without the vocabulary");

    // And it is IN the file.
    let text = recipe_toml("# emitted, not source.", &set).expect("emit");
    assert!(
        text.contains("[nornir-build-thing]") && text.contains("schema = 3"),
        "the emission carries no schema header:\n{text}"
    );

    // The floor is not disturbed: a set with no package emits no header at all, so
    // every recipe in the constellation keeps loading in readers back to 0.1.3.
    let plain = BuildVariantSet {
        variants: vec![BuildVariant { name: "solo".into(), ..BuildVariant::default() }],
        ..BuildVariantSet::default()
    };
    let (s, _) = required_by(&plain);
    assert_eq!(s, RECIPE_SCHEMA_FLOOR, "an unrelated recipe was pushed off the floor");
    assert!(
        !recipe_toml("# h", &plain).unwrap().contains("[nornir-build-thing]"),
        "a recipe that declares nothing new grew a header"
    );
}