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

//! **ROBOT-UI: no lens blocks another on the Lenses tab.**
//!
//! Measured 2026-10-16 on dwarf: the tab ran its lens RPCs in series and `?`-aborted
//! the whole reading on the first failure, with a 20 s client bound per RPC — a cold
//! room (28 s) or `world_impact` (39 s) blanked every lens, and so did one forge
//! answering `Unimplemented`. Here the mock `Mimir` server fails one lens and holds
//! another far longer than the rest; driven through the real widget tree of the
//! shipped thin pane, every other lens must be on screen with its own answer, the
//! failed one must say its error, the slow one must say it is still waiting — and a
//! lens the server first answers `queued` must be asked again until it lands.
use std::time::{Duration, Instant};

use dwarves_gui::mimir_lens::LensPane;
use facett_core::Facet;
use nornir_robotui::egui_kittest::Harness;
use nornir_robotui::{Observable, RobotSession};
use nornir_warehouse_embed::testkit::{spawn_mock_mimir, MockMimir};
use serde_json::{json, Value};

struct LensApp(LensPane);

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

fn answered(lens: &str) -> String {
    json!({
        "kind": lens,
        "worst": [{ "repo": "nornir", "subject": format!("{lens}-finding") }],
        "feeds": [],
        "evidence_note": format!("{lens}: computed"),
        "lens_meta": { "state": "complete", "lens": lens },
    })
    .to_string()
}

fn state_of(robot: &RobotSession<LensApp>, key: &str) -> String {
    robot
        .state_at(&format!("/{key}/lens_meta/state"))
        .and_then(|v| v.as_str().map(str::to_string))
        .unwrap_or_default()
}

#[test]
fn one_lens_failing_and_one_slow_leave_every_other_lens_shown() {
    let mut mock = MockMimir {
        zombie_features: answered("zombie_features"),
        change_risk: answered("change_risk"),
        villa: answered("villa"),
        anomaly: answered("anomaly"),
        world_impact: answered("world_impact"),
        trust: answered("trust"),
        ..Default::default()
    };
    // churn: the server refuses it; world_impact: held for a minute; villa: the
    // server first says `queued` twice, then answers.
    mock.lens_fail
        .insert("churn".into(), "the fold panicked".into());
    mock.lens_delay
        .insert("world_impact".into(), Duration::from_secs(60));
    mock.queued_first.lock().unwrap().insert("villa".into(), 2);
    let srv = spawn_mock_mimir(mock);
    let pane = LensPane::thin(&srv.endpoint, "", "nordisk");
    let harness = Harness::builder()
        .with_size(nornir_robotui::egui::Vec2::new(1200.0, 900.0))
        .build_ui_state(
            |ui, app: &mut LensApp| Facet::ui(&mut app.0, ui),
            LensApp(pane),
        );
    let mut robot = RobotSession::new(harness, "dwarves-gui");

    // paint until every lens but the slow one has its answer (or 20 s)
    let deadline = Instant::now() + Duration::from_secs(20);
    loop {
        robot.run(1);
        let done = ["zombie", "risk", "villa", "anomaly", "trust"]
            .iter()
            .all(|k| state_of(&robot, k) == "complete")
            && state_of(&robot, "churn") == "error";
        if done || Instant::now() > deadline {
            break;
        }
        std::thread::sleep(Duration::from_millis(25));
    }
    let all = robot.state_at("").unwrap_or(Value::Null);

    for key in ["zombie", "risk", "villa", "anomaly", "trust"] {
        assert_eq!(
            state_of(&robot, key),
            "complete",
            "`{key}` must be shown while another lens failed and one is slow: {all}"
        );
    }
    assert_eq!(
        robot.state_at("/zombie/worst/0/subject"),
        Some(json!("zombie_features-finding")),
        "the answer itself is on screen, not just a state"
    );
    assert_eq!(state_of(&robot, "churn"), "error", "{all}");
    let churn_note = robot
        .state_at("/churn/evidence_note")
        .and_then(|v| v.as_str().map(str::to_string))
        .unwrap_or_default();
    assert!(
        churn_note.contains("the fold panicked"),
        "the failed lens says its own error: {churn_note}"
    );
    assert!(
        ["pending", "queued", "running"].contains(&state_of(&robot, "world_impact").as_str()),
        "the slow lens says it is still waiting: {all}"
    );
    assert_eq!(
        robot.state_at("/pending"),
        Some(json!(true)),
        "the reading is still in flight for the slow lens"
    );
    assert!(
        robot.state_at("/error").map_or(true, |e| e.is_null()),
        "one lens failing is that lens's error, not the tab's: {all}"
    );
    // the queued lens was asked again until it landed
    assert!(
        srv_calls_for_villa(&all),
        "villa was answered `queued` twice and must have been asked again"
    );
}

fn srv_calls_for_villa(all: &Value) -> bool {
    all.pointer("/villa/worst/0/subject") == Some(&json!("villa-finding"))
}