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

nordisk/dwarves

dwarves-gui/tests/robot_host_only.rs

//! **ROBOT-UI: the thin client is HOST-ONLY — it knows only `DWARVES_SERVER`.**
//!
//! The suite-host-embedded-auth LAW: nornir is EMBEDDED in the dwarves server (the host);
//! the UI defers to that host and dials ONLY dwarves (`:7900`). It must NEVER open a
//! socket to nornir (`:7878`) — that port is for EXTERNAL nornir consumers only. This
//! headless robot drives the SHIPPED shell (`dwarves_gui::build_native_app` — the exact
//! builder `main` runs) over the `state_json` seam and proves the contract with NO window:
//!
//!  1. [`shell_comes_up_with_only_the_dwarves_host_and_never_dials_nornir`] — builds the
//!     shell given ONLY a dwarves forge endpoint, with `NORNIR_SERVER` set to a POISON
//!     `:7878` value in the environment to prove the UI ignores it. Asserts: the whole
//!     roster mounts; the Forge pane's endpoint is the dwarves host; and the ENTIRE app
//!     `state_json` (every pane folded in) never contains the poison nornir address —
//!     the UI carries no nornir endpoint at all.
//!  2. [`the_funnel_is_embedded_in_process_not_a_thin_nornir_socket`] — the Funnel pane
//!     renders from its in-process embedded warehouse (no network dial), so it is up with
//!     nornir DOWN and points at no `:7878` server.
//!
//! Each check emits a `nornir-testmatrix` `functional_status` cell under
//! `--features testmatrix` (reuse edda; never a hand-rolled ledger).

use dwarves_gui::build_forge::DeclaredForge;
use dwarves_gui::build_scene::{Forge, Query};
use dwarves_gui::{build_native_app, tab_labels, DWARVES_TABS};
use facett_core::Facet;
use serde_json::json;

/// The dwarves host the thin client is pointed at (the ONE address it knows). A
/// distinctive port so the assertion that it — and ONLY it — reaches the panes is exact.
const DWARVES_HOST: &str = "http://dwarves-host.example:7900";
/// The nornir address the UI must NEVER adopt. Set in the environment as a booby-trap.
const NORNIR_POISON: &str = "http://nornir-poison.example:7878";

#[allow(unused_variables)]
fn matrix(check: &str, ok: bool, detail: &str) {
    #[cfg(feature = "testmatrix")]
    nornir_testmatrix::functional_status("dwarves-gui", check, ok, detail);
}

/// Build the SHIPPED shell exactly as `main` does — the dwarves host is the only endpoint
/// it is given — while `NORNIR_SERVER` is poisoned in the environment. If the client
/// still read `NORNIR_SERVER`, the poison `:7878` would leak into a pane; it must not.
fn host_only_shell() -> dwarves_gui::tabbed::TabbedView {
    // SAFETY: the integration test binary is single-purpose; set the booby-trap before
    // the (env-free) builder runs. `build_native_app` takes NO nornir arg by design.
    unsafe {
        std::env::set_var("NORNIR_SERVER", NORNIR_POISON);
        std::env::set_var("DWARVES_SERVER", DWARVES_HOST);
    }
    build_native_app(
        DWARVES_HOST.to_string(),
        std::path::PathBuf::from("/nonexistent/dwarves-gui/host-only"),
    )
}

#[test]
fn shell_comes_up_with_only_the_dwarves_host_and_never_dials_nornir() {
    let mut app = host_only_shell();

    // (a) The whole roster mounts given only the dwarves host — the client needs no
    //     second address to come up.
    let s = Facet::state_json(&app);
    let tabs: Vec<String> = s["tabs"]
        .as_array()
        .expect("tabs array")
        .iter()
        .map(|t| t.as_str().unwrap().to_string())
        .collect();
    let roster_ok = tabs == tab_labels();
    assert!(
        roster_ok,
        "the shell mounts the full roster with only DWARVES_SERVER: {tabs:?}"
    );

    // (b) The Forge pane is pointed at the dwarves host — the one address the UI knows.
    Facet::update_json(&mut app, &json!({ "set_tab": "Forge" }).to_string());
    let forge = Facet::state_json(&app);
    let forge_endpoint = forge["forge"]["endpoint"].as_str().unwrap_or_default();
    let forge_ok = forge_endpoint == DWARVES_HOST;
    assert!(
        forge_ok,
        "Forge dials the dwarves host `{DWARVES_HOST}`, got `{forge_endpoint}`"
    );

    // (c) THE LAW: fold EVERY pane in and prove the poison nornir `:7878` address appears
    //     NOWHERE — the UI holds no nornir endpoint. Visit each tab so its pane state is
    //     materialised, then scan the entire serialized shell for the booby-trap.
    for (_key, label) in DWARVES_TABS.iter() {
        Facet::update_json(&mut app, &json!({ "set_tab": label }).to_string());
    }
    let whole = Facet::state_json(&app).to_string();
    let no_nornir = !whole.contains("nornir-poison.example") && !whole.contains(":7878");
    assert!(
        no_nornir,
        "the UI must never carry a nornir `:7878` endpoint — poison leaked into state_json"
    );

    let ok = roster_ok && forge_ok && no_nornir;
    matrix(
        "host_only_shell_never_dials_nornir",
        ok,
        "shell built with ONLY the dwarves host (NORNIR_SERVER poisoned): full roster \
         mounts, Forge dials the dwarves host, and no `:7878`/nornir endpoint appears \
         anywhere in the shell's state_json — the thin client is host-only",
    );
}

/// The side-by-side checkout dir (the constellation): two levels up from this crate —
/// `dwarves-gui` → the dwarves repo → the constellation dir that holds the suite siblings.
fn constellation_root() -> std::path::PathBuf {
    std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .parent()
        .and_then(std::path::Path::parent)
        .map(std::path::PathBuf::from)
        .unwrap_or_else(|| std::path::PathBuf::from("."))
}

/// ROBOT — **the ⚒ Build Thing scene resolves in the host-only shell, and refuses
/// honestly when there is nothing to resolve.**
///
/// The owner's target, carried over from the pane this replaces: the ⚒ tab must not come
/// up blank in the thin/host-only viewer.
///  (a) a staged constellation RESOLVES — a real recipe coordinate and a real matrix;
///  (b) a thin container with no staged checkouts REFUSES by name, never blank;
///  (c) the shipped host-only shell mounts the scene and drives the same two paths.
#[test]
fn the_build_thing_scene_resolves_and_refuses_honestly_in_the_host_only_shell() {
    // (a) STAGED path — the side-by-side checkouts this machine holds.
    let root = constellation_root();
    let mut forge = DeclaredForge::at(root.clone());
    let declared: Vec<String> = forge.declared_repos();
    eprintln!(
        "─── ⚒ Build Thing scene (constellation {}) ───",
        root.display()
    );
    eprintln!("declared     : {declared:?}");
    let mut resolved_any = false;
    for repo in &declared {
        if let Ok(menu) = forge.resolve(&Query::Artifact(repo.clone())) {
            let cols = menu.rows.first().map(|r| r.targets.len()).unwrap_or(0);
            eprintln!(
                "  · {repo}: {} rad(er) × {cols} kolumn(er) — {}",
                menu.rows.len(),
                menu.coordinate
            );
            resolved_any = true;
        }
    }
    eprintln!("─── end scene ───");
    assert!(
        resolved_any,
        "no staged checkout resolved to a matrix under {}",
        root.display()
    );

    // (b1) NAME MISS — a populated root that declares no such artifact owes the NAME.
    //      This is where "refuses BY NAME" is a meaningful claim, and it is asserted
    //      against the same forge that resolved in (a), so the refusal cannot be a
    //      constant.
    let miss = forge
        .resolve(&Query::Artifact("no-such-artifact-anywhere".to_string()))
        .expect_err("a populated root must not resolve a name nobody declares");
    eprintln!("─── ⚒ name-miss refusal ───\n{miss}\n─── end ───");
    assert!(
        miss.contains("no-such-artifact-anywhere") && miss.contains(&root.display().to_string()),
        "a populated root refused without naming WHAT was asked and WHERE it looked: {miss}"
    );

    // (b2) THIN path — no staged checkouts at all. This refusal does NOT owe the
    //      artifact name, and that is deliberate: `1859b82` ("an empty constellation
    //      root says so, instead of saying 'not found'") split the two cases apart
    //      because "ingen deklaration nämner app" is equally true of a root holding 40
    //      checkouts and a root holding none, and only one of those is the operator's
    //      fault. What an empty root owes is the ROOT it searched and the VARIABLE that
    //      fixes it. This test kept demanding the artifact name and had been red since,
    //      while `robot_build_scene_empty_root.rs` — the guard written FOR the new
    //      wording — was green the whole time.
    let thin = std::path::PathBuf::from("/nonexistent/thin-viewer/constellation");
    let mut empty = DeclaredForge::at(thin.clone());
    let why = empty
        .resolve(&Query::Artifact("app".to_string()))
        .expect_err("an empty constellation cannot resolve anything");
    eprintln!("─── ⚒ thin refusal ───\n{why}\n─── end ───");
    assert!(
        why.contains(&thin.display().to_string()),
        "an empty constellation refused without naming the root it searched: {why}"
    );
    assert!(
        why.contains("DWARVES_CONSTELLATION"),
        "an empty constellation refused without naming the variable that fixes it — a \
         refusal with no next step is the blank pane this test exists to prevent: {why}"
    );
    // …and it must NOT read as "that artifact does not exist", which is the exact
    // misdiagnosis `1859b82` removed. The two refusals are distinguishable by
    // construction: the name-miss above carries the name, this one does not.
    assert!(
        !why.contains("app"),
        "the empty-root refusal blames the ARTIFACT again — it is the ROOT that is empty, \
         and conflating the two is what sent the operator hunting for a missing recipe: {why}"
    );

    // (c) THE SHIPPED SHELL — the ⚒ tab is mounted and drives the same two paths.
    let mut app = host_only_shell();
    Facet::update_json(&mut app, &json!({ "set_tab": "Build Thing" }).to_string());
    let s = Facet::state_json(&app);
    let bt = &s["build-thing"];
    assert_eq!(
        bt["component"], "buildscene",
        "the ⚒ scene is mounted in the shell: {s}"
    );
    Facet::update_json(&mut app, &json!({ "artifact": "" }).to_string());
    Facet::update_json(&mut app, &json!({ "resolve": true }).to_string());
    let bt = Facet::state_json(&app)["build-thing"].clone();
    let shell_never_blank = bt["error"].as_str().map(|e| !e.is_empty()).unwrap_or(false);
    assert!(
        shell_never_blank,
        "the ⚒ scene in the host-only shell said nothing at all: {bt}"
    );

    matrix(
        "build_scene_resolves_and_refuses_honestly_host_only",
        resolved_any && shell_never_blank,
        "the ⚒ Build Thing SCENE resolves a staged constellation to a real recipe coordinate and \
         matrix, tells its TWO refusals apart (a populated root that declares no such name says \
         the NAME and the ROOT; an empty root says the ROOT and $DWARVES_CONSTELLATION and never \
         blames the artifact), and is mounted + answering in the shipped host-only shell — \
         driven over state_json, never blank",
    );
}

#[test]
fn the_funnel_is_embedded_in_process_not_a_thin_nornir_socket() {
    let mut app = host_only_shell();

    // The Funnel renders from its in-process embedded warehouse — up with nornir DOWN,
    // and carrying no `:7878` server address.
    Facet::update_json(&mut app, &json!({ "set_tab": "Funnel" }).to_string());
    let s = Facet::state_json(&app);
    let funnel = &s["funnel"];
    let present = funnel.is_object();
    assert!(
        present,
        "the Funnel pane renders (embedded) with only the dwarves host: {s}"
    );
    let no_nornir = !funnel.to_string().contains(":7878")
        && !funnel.to_string().contains("nornir-poison.example");
    assert!(
        no_nornir,
        "the Funnel is embedded in-process and dials no nornir `:7878`: {funnel}"
    );

    let ok = present && no_nornir;
    matrix(
        "funnel_embedded_no_nornir_socket",
        ok,
        "the Funnel pane renders from its in-process embedded warehouse (up with nornir \
         DOWN) and holds no `:7878` nornir server address",
    );
}