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_build_scene_empty_root.rs

//! **ROBOT-UI: an empty constellation root says so, instead of saying "not found".**
//!
//! MEASURED 2026-08-22 on the shipped ui: it was launched with `cwd =
//! ~/git` and no `$DWARVES_CONSTELLATION`, so
//! `ConstellationBuilds::default_root()` took the PARENT — `~` —
//! which declares no build space at all. The roster was empty. Typing `gunnar`
//! and pressing `sök` answered "ingen deklaration under ~ nämner
//! \"gunnar\"", which is TRUE, and useless: it is the same sentence a root
//! holding forty checkouts gives for a misspelt artifact, and only one of those
//! two is the operator's own fault. The log said nothing about where it had
//! looked.
//!
//! The distinction this file holds is the whole point (LAW 2): "no evidence of
//! the artifact" and "no evidence of anything" must not read alike. So it drives
//! the real widgets against TWO staged roots — one empty, one with a checkout
//! that declares a build space — and requires the two answers to differ.
//!
//! A note on the red proof: asserting only that the empty root's message
//! contains `$DWARVES_CONSTELLATION` would pass on a scene that printed that
//! remedy for EVERY miss. The populated root's arm is what makes the empty
//! root's arm mean something.

use facett_core::Facet;
use nornir_robotui::egui_kittest::Harness;
use nornir_robotui::{Observable, RobotSession};

use dwarves_gui::build_forge::DeclaredForge;
use dwarves_gui::build_scene::BuildScene;

struct SceneApp(BuildScene);

impl Observable for SceneApp {
    fn state_json(&self) -> serde_json::Value {
        Facet::state_json(&self.0)
    }
}

/// A root with nothing in it at all — the `~` case.
fn empty_root() -> tempfile::TempDir {
    tempfile::tempdir().expect("tempdir")
}

/// A root holding one checkout that DOES declare a build space — the control.
/// Its artifact is deliberately not the one the test asks for, so the only
/// difference between the two arms is whether the ROSTER is empty.
fn populated_root() -> tempfile::TempDir {
    let dir = tempfile::tempdir().expect("tempdir");
    let repo = dir.path().join("orten");
    std::fs::create_dir_all(repo.join(".nornir")).expect("mkdir");
    std::fs::write(
        repo.join("Cargo.toml"),
        "[package]\nname = \"orten\"\nversion = \"1.0.0\"\nedition = \"2021\"\n\n[dependencies]\n",
    )
    .expect("manifest");
    std::fs::write(
        repo.join("Cargo.lock"),
        "version = 4\n\n[[package]]\nname = \"orten\"\nversion = \"1.0.0\"\n",
    )
    .expect("lockfile");
    std::fs::write(
        repo.join(".nornir").join("build-variants.toml"),
        r#"
[build]
base_suite = "ORIGINAL"

[[build.variant]]
name = "orten-server"
role = "server"
default = true
suites = ["ORIGINAL"]
package = "orten"
bin = "orten"
kind = "exe"
features = ["server"]
profile = "release"
"#,
    )
    .expect("declaration");
    dir
}

fn session(root: &std::path::Path) -> RobotSession<'_, SceneApp> {
    let scene = BuildScene::new(Box::new(DeclaredForge::at(root.to_path_buf())));
    let harness = Harness::builder()
        .with_size(nornir_robotui::egui::Vec2::new(1400.0, 1000.0))
        .build_ui_state(
            |ui, app: &mut SceneApp| Facet::ui(&mut app.0, ui),
            SceneApp(scene),
        );
    let mut robot = RobotSession::new(harness, "dwarves-gui");
    robot.run(2);
    robot
}

/// Type an artifact name into the real field and press the real button.
fn search(robot: &mut RobotSession<'_, SceneApp>, name: &str) -> String {
    robot
        .type_into("artefakt", name)
        .expect("the artefakt field");
    robot.click_label("sök").expect("the sök button");
    robot.run(2);
    robot
        .state_at("/error")
        .and_then(|v| v.as_str().map(str::to_string))
        .unwrap_or_default()
}

#[test]
fn an_empty_constellation_root_names_itself_and_the_variable_that_fixes_it() {
    let dir = empty_root();
    let root = dir.path().to_path_buf();
    let mut robot = session(&root);
    let why = search(&mut robot, "gunnar");

    assert!(!why.is_empty(), "the search reported nothing at all");
    assert!(
        why.contains(&root.display().to_string()),
        "the refusal does not name the root it looked in: {why}"
    );
    assert!(
        why.contains("0 repon deklarerar ett byggrum"),
        "the refusal does not say the roster is EMPTY — it reads as 'that artifact is not \
         here', which is the defect: {why}"
    );
    assert!(
        why.contains("$DWARVES_CONSTELLATION"),
        "the refusal names no remedy: {why}"
    );
    // The message is the one the operator sees, not only a state field.
    assert!(
        robot.label_visible(&why),
        "the refusal is in the state but not on the screen"
    );
}

/// The arm that keeps the arm above honest: with a real roster, a miss is a
/// MISS — it must NOT reach for `$DWARVES_CONSTELLATION`, and it must say how
/// many checkouts it did look through.
#[test]
fn a_populated_root_reports_a_miss_as_a_miss_and_counts_what_it_searched() {
    let dir = populated_root();
    let mut robot = session(dir.path());
    let why = search(&mut robot, "gunnar");

    assert!(!why.is_empty(), "the search reported nothing at all");
    assert!(
        !why.contains("$DWARVES_CONSTELLATION"),
        "a root with a real roster blamed the environment variable — the remedy is printed \
         unconditionally, so the empty-root arm proves nothing: {why}"
    );
    assert!(
        !why.contains("0 repon"),
        "a root with one declaring checkout reported an empty roster: {why}"
    );
    assert!(
        why.contains("gunnar"),
        "a miss must name what was searched for: {why}"
    );
    assert!(
        why.contains("1 repo(n) deklarerar ett byggrum"),
        "the miss does not say how many checkouts were actually searched, so an operator \
         cannot tell a small roster from a complete one: {why}"
    );
}

/// The forge's own roster is what both messages are derived from — asserted
/// directly, so a message that happened to say "0" while the roster was full
/// (or the reverse) cannot pass.
#[test]
fn the_roster_the_messages_speak_for_is_the_one_the_forge_actually_read() {
    let empty = empty_root();
    assert!(
        DeclaredForge::at(empty.path().to_path_buf())
            .declared_repos()
            .is_empty(),
        "the empty root was not empty, so its arm above is about nothing"
    );
    let full = populated_root();
    assert_eq!(
        DeclaredForge::at(full.path().to_path_buf()).declared_repos(),
        vec!["orten".to_string()],
        "the control root did not declare the checkout the arm above counts"
    );
}