dwarves-gui/tests/robot_mimir_scan_async.rs
//! **ROBOT-UI: the Mímir pane reads ITSELF on open, and never on the egui thread.**
//!
//! Driven through the REAL widget tree of the shipped thin Lenses pane
//! (`LensPane::thin`) against a mock `Mimir.*` server that HOLDS every reply for a
//! while. What is asserted is applied output: the state the pane publishes to a
//! robot (`pending`, `scanned`, `thin`) and the wall-clock a frame took.
//!
//! Since the jera chain landed the answers are already folded in `lens_*`, so the
//! pane READS on open rather than making the operator press a button — and NO button
//! remains: the `⟳` re-read went with `Scan` at `768983f` (2026-08-22, the owner: "SCAN
//! skulle ju bort och vara en chain i jera som arbetas i background direkt efter
//! populate", quoted at `mimir_lens.rs:2045`). This file asked for `⟳` for a day after
//! the affordance was deliberately removed, which is only possible because nobody ran
//! it — the same disease as a feature-gated test binary printing `ok` with no tests.
//!
//! MEASURED 2026-08-22 on oden, before this: pressing Scan on the shipped pane ran eleven
//! `Mimir.*` RPCs in series ON the egui thread (ChangeRisk 13.4 s, GenericCost 11.2 s …)
//! and the window did not paint for the whole round — read by the operator as "it
//! hangs". Proven RED first by restoring the synchronous path: the click then holds the
//! frame for 11 × reply_delay and `pending` never appears.
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;
struct LensApp(LensPane);
impl Observable for LensApp {
fn state_json(&self) -> serde_json::Value {
Facet::state_json(&self.0)
}
}
/// Each lens reply is held this long; eleven of them in series would be ~4.4 s.
const REPLY_DELAY: Duration = Duration::from_millis(400);
#[test]
fn the_scan_click_returns_the_frame_before_the_server_answers() {
let srv = spawn_mock_mimir(MockMimir {
reply_delay: REPLY_DELAY,
..Default::default()
});
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");
// THE OPEN. One frame, timed: the pane must have STARTED its read and given
// the frame back — no button pressed, nothing waited on.
let t0 = Instant::now();
robot.run(1);
let held = t0.elapsed();
assert_eq!(
robot.state_at("/pending"),
Some(json!(true)),
"the pane must read ITSELF on open — the chain has already folded the answers, so \
making the operator press a button is pure friction (state {})",
robot
.state_at("")
.map(|v| v.to_string())
.unwrap_or_default()
);
assert_eq!(
robot.state_at("/scanned"),
Some(json!(false)),
"not read yet — the server is still holding its reply"
);
assert!(
held < REPLY_DELAY,
"the open frame took {held:?} — longer than ONE server reply ({REPLY_DELAY:?}); the \
read ran on the egui thread"
);
// ── AND NO BUTTON REMAINS. `Scan` meant "fold eleven lenses for 30 s"; `⟳` meant
// "do it again". Both are gone (`768983f`): populate is the base, the folds are
// jera chain jobs behind it, and this pane only READS. `node_id` is used rather
// than a click so the absence is asked of the tree without mutating it.
//
// A bare `is_none()` here would be hollow — an empty AccessKit tree (a pane that
// painted nothing, which is a state this file is otherwise blind to) satisfies it
// just as well. So the absence is asserted BESIDE a control the pane really draws;
// if the tree were empty the second assertion goes red and the first stops being
// quotable.
assert!(
robot.node_id("⟳").is_none(),
"the ⟳ re-read button is BACK in the AccessKit tree — the read is automatic (proven \
by /pending above) and a button that makes the operator wait is exactly what was \
removed"
);
assert!(
robot.label_visible("🔧 fixes"),
"NON-VACUITY: the pane's own `🔧 fixes` checkbox must be on the rendered surface, or \
the ⟳ assertion above proves only that the tree is empty"
);
// Keep painting; the worker lands the result.
let deadline = Instant::now() + Duration::from_secs(30);
while robot.state_at("/pending") == Some(json!(true)) && Instant::now() < deadline {
robot.run(1);
std::thread::sleep(Duration::from_millis(25));
}
assert_eq!(
robot.state_at("/pending"),
Some(json!(false)),
"the fold must land within 30 s"
);
assert_eq!(
robot.state_at("/scanned"),
Some(json!(true)),
"and the pane is scanned once it lands"
);
assert_eq!(
robot.state_at("/thin"),
Some(json!(true)),
"the reading is the server's, not a local fold"
);
}