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