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

//! **ROBOT-UI: the step console renders all FIVE step states from a fixture DAG,
//! without a forge** (wave 3, gap #4).
//!
//! | claim | RED when |
//! |---|---|
//! | the hosted console leaf says, by name, that nothing is planned | an empty console that reads like a finished one |
//! | a staged plan draws every step in DAG order with its state's own line | two states share a line, or a step is dropped |
//! | `queued · running in container <image@digest> · cached from ledger · done with receipt sha+fragment · refused: <reason>` are each ON SCREEN | a state is only in the data |
//! | the expectation (cold / cached) and its source are drawn BEFORE any run | a figure without a source |
//!
//! The plan is DATA through the shipped `update_json` door (`stage_steps`), so no
//! forge, no runner, no container is needed to prove the console tells the truth about
//! each state. RED BEFORE GREEN: on the parent commit the console leaf had no step
//! table and `stage_steps` was not a message.

#![cfg(not(target_arch = "wasm32"))]

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

use dwarves_gui::build_scene::{Act, Axis, AxisPick, BuildScene, Forge, Menu, Query, Report};
use dwarves_gui::build_steps::{Expectation, Step, StepPlan, StepRole, StepState};
use dwarves_gui::build_thing_scene::CONSOLE;
use dwarves_gui::tabbed::LeafPanel;
use nornir_build_thing::variant::ArtifactKind;

/// A forge that resolves nothing and runs nothing — the console must not need one.
struct NoForge;

impl Forge for NoForge {
    fn axis(&mut self, _pick: &AxisPick) -> Axis {
        Axis::Absent
    }
    fn resolve(&mut self, _q: &Query) -> Result<Menu, String> {
        Err("ingen smedja i den här roboten".into())
    }
    fn act(&mut self, _act: Act) {}
    fn poll(&mut self) -> Report {
        Report::default()
    }
}

struct App(BuildScene);

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

/// The console LEAF, hosted — the shape the ⚒ tab mounts it in.
fn console_leaf() -> RobotSession<'static, App> {
    let scene = BuildScene::new(Box::new(NoForge)).hosted();
    let harness = Harness::builder()
        .with_size(nornir_robotui::egui::Vec2::new(2400.0, 1200.0))
        .build_ui_state(
            |ui, app: &mut App| {
                app.0.ui_leaf(CONSOLE, ui);
            },
            App(scene),
        );
    let mut robot = RobotSession::new(harness, "dwarves-gui");
    robot.run(2);
    robot
}

/// The fixture DAG: five steps, one per state, donor-first with the consumer last.
fn fixture() -> StepPlan {
    let mut step =
        |coordinate: &str, role: StepRole, typ: ArtifactKind, state: StepState, ms: u64| {
            let mut s = Step::queued(coordinate, role, typ, vec![format!("--build {coordinate}")]);
            s.state = state;
            s.elapsed_ms = ms;
            s
        };
    StepPlan {
        product: Some("orten".into()),
        variant: "orten-installer".into(),
        edge_path: "kernel".into(),
        coordinate: "tunnr/tunnr-kernel".into(),
        cli: "dwarves buildthing orten --variant orten-installer --build kernel --opt kernel.preset=nuked".into(),
        steps: vec![
            step("tunnr/tunnr-kernel", StepRole::Donor, ArtifactKind::Kernel, StepState::Cached { key: "3f9a1c2b4d5e6f708192a3b4c5d6e7f8".into() }, 145_000),
            step("edda/edda-fragments", StepRole::Donor, ArtifactKind::Lib, StepState::Done { sha: "c9a4fc6c1122334455667788".into(), fragment: Some("d437b9195566778899aabbcc".into()) }, 323_000),
            step("orten/orten-toolchain", StepRole::Donor, ArtifactKind::Container, StepState::Running { image: "nordisk-cargo-toolchain@sha256:9e8d7c6b5a4f".into() }, 61_000),
            step("orten/orten-ui-wasm", StepRole::Donor, ArtifactKind::Wasm, StepState::Refused { why: "bilden saknar std för wasm32 — rustup target add wasm32-unknown-unknown i toolchain-containern".into() }, 0),
            step("orten-installer", StepRole::Consumer, ArtifactKind::Installer, StepState::Queued, 0),
        ],
        settings: Default::default(),
        params: Vec::new(),
    }
}

/// **Nothing planned ⇒ the console says so.** Never a blank pane, never a spinner.
#[test]
fn an_empty_console_says_nothing_is_planned() {
    let robot = console_leaf();
    assert_eq!(robot.state_at("/steps"), Some(Value::Null));
    assert!(robot.label_visible(
        "stegkonsol: ingen kant vald — BYGG <kant> i nodens meny planerar DAG:en här"
    ));
}

/// **Five states, five lines, all on screen, in DAG order — from data, without a forge.**
#[test]
fn the_console_renders_all_five_step_states_from_a_fixture_dag() {
    let mut robot = console_leaf();
    let plan = fixture();
    let msg = json!({ "stage_steps": serde_json::to_value(&plan).unwrap() }).to_string();
    robot.harness_mut().state_mut().0.update_json(&msg);
    robot.run(3);

    let steps = robot.state_at("/steps").expect("the plan is staged");
    assert_eq!(steps["cli"], json!(plan.cli));
    assert!(
        robot.label_visible(&plan.cli),
        "the CLI line is on the console"
    );
    let rows = steps["steps"].as_array().expect("steps");
    assert_eq!(rows.len(), 5);
    let words: Vec<&str> = rows.iter().map(|r| r["state"].as_str().unwrap()).collect();
    assert_eq!(
        words,
        ["cached", "done", "running", "refused", "queued"],
        "every state, DAG order kept"
    );
    assert_eq!(rows[4]["role"], json!("konsument"), "the consumer is last");
    assert!(
        rows[..4].iter().all(|r| r["role"] == json!("donator")),
        "donors first"
    );

    // Every state's own line, on screen — read off the state so the assertion is on
    // the words the console draws, not on words this test invented.
    for (i, s) in plan.steps.iter().enumerate() {
        let line = s.state.line();
        assert_eq!(rows[i]["line"], json!(line));
        assert!(
            robot.label_visible(&line),
            "step {i} `{}` draws its state line: {line}",
            s.coordinate
        );
    }
    assert!(robot.label_visible("köad"));
    assert!(robot.label_visible("kör i container nordisk-cargo-toolchain@sha256:9e8d7c6b5a4f"));
    assert!(robot.label_visible("cachad ur ledgern 3f9a1c2b4d5e…"));
    assert!(robot.label_visible("klar · kvitto sha c9a4fc6c1122… · fragment d437b9195566…"));
    assert!(robot.label_visible("vägrad: bilden saknar std för wasm32 — rustup target add wasm32-unknown-unknown i toolchain-containern"));

    // The state's data beside the word.
    assert_eq!(
        rows[0]["detail"]["key"],
        json!("3f9a1c2b4d5e6f708192a3b4c5d6e7f8")
    );
    assert_eq!(rows[1]["detail"]["sha"], json!("c9a4fc6c1122334455667788"));
    assert_eq!(
        rows[1]["detail"]["fragment"],
        json!("d437b9195566778899aabbcc")
    );
    assert_eq!(
        rows[2]["detail"]["image"],
        json!("nordisk-cargo-toolchain@sha256:9e8d7c6b5a4f")
    );
    assert!(rows[3]["detail"]["why"]
        .as_str()
        .unwrap()
        .contains("wasm32"));
    assert_eq!(rows[4]["detail"], Value::Null);
    // Elapsed as the fixture states it; the console's own clock never started.
    assert_eq!(rows[0]["elapsed_ms"], json!(145_000));
    assert!(robot.label_visible("145.0s"));
    assert_eq!(steps["elapsed_ms"], json!(0));

    // The expectation, drawn before any run, with its source — by typ.
    let kernel = Expectation::for_typ(ArtifactKind::Kernel);
    assert_eq!(rows[0]["expect"]["cold"], json!(kernel.cold));
    assert_eq!(rows[0]["expect"]["cached"], json!(kernel.cached));
    assert!(robot.label_visible(&format!("{} / {}", kernel.cold, kernel.cached)));
    assert!(
        robot.label_visible(&kernel.source),
        "the expectation names its source on screen"
    );
    assert!(
        kernel.cached.contains("145 s"),
        "the plan's own figure: {}",
        kernel.cached
    );
    assert_eq!(steps["receipt"], Value::Null, "no verb fired ⇒ no receipt");
}

/// A message that is not a plan is refused by name and stages nothing.
#[test]
fn a_message_that_is_not_a_plan_stages_nothing() {
    let mut robot = console_leaf();
    robot
        .harness_mut()
        .state_mut()
        .0
        .update_json(r#"{"stage_steps": {"steps": "nej"}}"#);
    robot.run(2);
    assert_eq!(robot.state_at("/steps"), Some(Value::Null));
    assert!(
        robot.state_at("/menu/note").is_none_or(|v| v.is_null())
            || robot.state_at("/menu").is_some()
    );
}