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

nordisk/gunnar-ui

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