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