crates/gunnar-ui-recipe/tests/the_console_product.rs
//! **The console is the product crate `gunnar-ui`** (lane GUN-PRODUCTS, owner 2026-10-30:
//! Gunnar gets Holger's product shape).
//!
//! Gunnar's three products are one crate each: `gunnar` (the server), `gunnar-ui` (this
//! console) and `gunnar-cli` (the command-line tool). `cargo install gunnar-ui` installs
//! one program, `gunnar-ui`: a thin bin over the `gunnar-ui-server` library, with the
//! console bundle built in when one was built (`cargo xtask console`). Its extras are the
//! named features every Gunnar product carries; the default is AIRGAP (no extra, no
//! outbound surface). The Build Thing row `gunnar-ui` builds exactly what
//! `cargo install gunnar-ui` builds.
use std::collections::BTreeSet;
use gunnar_ui_recipe::{declared, repo_root};
/// The named extras every Gunnar product carries (`gunnar`, `gunnar-ui`, `gunnar-cli`).
const EXTRAS: [&str; 5] = ["public-site", "mcp", "engine-gix", "online", "federation"];
fn manifest(rel: &str) -> toml::Table {
let path = repo_root().join(rel).join("Cargo.toml");
std::fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("{} is missing ({e}): `cargo install gunnar-ui` installs nothing", path.display()))
.parse()
.unwrap_or_else(|e| panic!("{}: {e}", path.display()))
}
fn strings(v: Option<&toml::Value>) -> BTreeSet<String> {
v.and_then(toml::Value::as_array)
.map(|a| a.iter().filter_map(|x| x.as_str().map(str::to_string)).collect())
.unwrap_or_default()
}
/// The features a feature turns on, transitively, within the one package.
fn closure(features: &toml::Table, of: &str) -> BTreeSet<String> {
let mut seen = BTreeSet::new();
let mut todo = vec![of.to_string()];
while let Some(f) = todo.pop() {
if !seen.insert(f.clone()) {
continue;
}
for g in strings(features.get(&f)) {
if features.contains_key(&g) {
todo.push(g);
}
}
}
seen
}
#[test]
fn the_console_product_is_one_crate_with_one_program() {
let m = manifest("crates/gunnar-ui");
let package = m["package"].as_table().expect("[package]");
assert_eq!(package["name"].as_str(), Some("gunnar-ui"), "crates/gunnar-ui is the package `gunnar-ui`");
assert_eq!(package.get("publish").and_then(toml::Value::as_bool), Some(true), "gunnar-ui says `publish = true` by name");
for key in ["description", "license", "repository", "readme"] {
assert!(package.contains_key(key), "gunnar-ui's [package] has no `{key}`: crates.io refuses or shows nothing");
}
let bins: Vec<&str> = m["bin"].as_array().expect("[[bin]]").iter().filter_map(|b| b["name"].as_str()).collect();
assert_eq!(bins, ["gunnar-ui"], "`cargo install gunnar-ui` installs exactly one program, `gunnar-ui`");
}
#[test]
fn the_console_carries_the_named_extras_and_an_airgap_default() {
let m = manifest("crates/gunnar-ui");
let features = m["features"].as_table().expect("[features]");
assert!(strings(features.get("default")).is_empty(), "the console's default is AIRGAP: no extra in `default`");
for extra in EXTRAS.iter().chain(&["full"]) {
assert!(features.contains_key(*extra), "gunnar-ui has no named extra `{extra}`");
}
let full = closure(features, "full");
for extra in EXTRAS {
assert!(full.contains(extra), "`full` does not turn on `{extra}`");
}
assert!(!full.contains("metered"), "`full` is every OPEN extra; the paid half is the estate's");
// The public face and the MCP connector are their own crates behind the console's
// own default-off features (the slim rule).
assert!(strings(features.get("public-site")).contains("gunnar-ui-server/public-site"));
assert!(strings(features.get("mcp")).contains("gunnar-ui-server/mcp"));
}
#[test]
fn every_crate_the_console_needs_may_be_published() {
for rel in ["crates/gunnar-ui", "crates/gunnar-ui-server", "crates/gunnar-public-site", "crates/gunnar-mcp"] {
let m = manifest(rel);
let publish = m["package"].get("publish");
let ok = match publish {
Some(toml::Value::Boolean(b)) => *b,
Some(toml::Value::Table(t)) => t.get("workspace").and_then(toml::Value::as_bool) == Some(true) && workspace_publishes(),
_ => true,
};
assert!(ok, "{rel} may not be published, and `cargo install gunnar-ui` needs it from crates.io");
}
}
fn workspace_publishes() -> bool {
let root: toml::Table = std::fs::read_to_string(repo_root().join("Cargo.toml")).unwrap().parse().unwrap();
root["workspace"]["package"].get("publish").and_then(toml::Value::as_bool).unwrap_or(true)
}
#[test]
fn the_ui_row_is_the_cargo_install_line() {
let set = declared();
let v = set.variants.iter().find(|v| v.name == "gunnar-ui").expect("the Build Thing has a `gunnar-ui` row");
assert_eq!(v.package.as_deref(), Some("gunnar-ui"));
assert_eq!(v.bin.as_deref(), Some("gunnar-ui"));
assert!(v.features.is_empty() && !v.no_default_features, "`cargo install gunnar-ui` keeps the defaults and names no feature");
assert_eq!(v.target, None, "`cargo install` builds for the host");
assert_eq!(v.suites, ["MVP"], "the console row is in Gunnar's MVP suite");
for v in &set.variants {
assert_eq!(v.suites, ["MVP"], "row `{}`: gunnar-ui's one suite is MVP, as gunnar's", v.name);
}
}