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