//! Robot-verified proof for **holger-server-ui demo mode** (`holger-server-ui --demo`).
//!
//! The demo is NOT a static assert — it (1) FILLS a dev znippy store and (2) drives
//! the REAL `HolgerUiApp` through its views via the scripted walk. Here we run the
//! real per-frame loop headlessly (a persistent `egui::Context` fed an INJECTED
//! clock via `RawInput.time`, FC-7) and assert the tour walks ≥7 distinct views,
//! cycles all 5 Tools panes, lights the 3D graph, re-themes the look, and emits a
//! state TRANSITION per beat — all read back as DATA from `state_json()`.
//!
//! Gated on `demo`, not on `gui`: the tour drives an IN-PROCESS `LocalHolger` over
//! a znippy store it fills itself, which is what the thin-client rule keeps out of
//! the shipped `gui` tier. Run this with `--features gui-embedded`.
#![cfg(feature = "demo")]
use std::collections::BTreeSet;
use std::sync::Arc;
use holger_server_ui::app::HolgerUiApp;
use holger_server_ui::data::UiData;
/// Run one real frame at injected time `t_secs` (drives `demo_tick` off the egui
/// clock, exactly as eframe would), then return the app's observable state.
fn frame(app: &mut HolgerUiApp, ctx: &egui::Context, t_secs: f64) -> serde_json::Value {
let input = egui::RawInput { time: Some(t_secs), ..Default::default() };
// `Context::run` hands the closure a `&Context`, not a `&mut Ui` — this line
// was written against the older egui where it did, so it named a `ui` that has
// not existed in this scope for as long as the file has. It never compiled, and
// because the whole file is `#![cfg(feature = "demo")]` and nothing ever built
// that feature, nothing ever said so. `run_ui` is the Ui-closure form this wants —
// and as of egui 0.35 it is the only one: `Context::run` is REMOVED, not merely
// deprecated as it was in 0.34.
let _ = ctx.run_ui(input, |ui| app.draw_ui(ui));
app.state_json()
}
#[test]
fn demo_walk_robot_drives_every_view_offline() {
// (1) FILL the store — offline-safe (no network in CI), bundled fixture.
let store_dir =
std::env::temp_dir().join(format!("holger-demo-walk-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&store_dir);
let fill = holger_server_ui::demo::fill_demo_store(&store_dir, false).expect("fill demo store");
assert_eq!(fill.repos.len(), 2, "two demo repos");
assert_eq!(fill.packages.len(), 12, "the curated set landed");
// (2) Build the real app over the filled in-process store + arm the walk.
let data = UiData::new(Arc::new(fill.local_holger())).expect("view-model");
let mut app = HolgerUiApp::new(data);
let beat_ms = 500u64;
app.enable_demo(beat_ms);
assert!(app.demo_active(), "demo armed");
// The server behind the UI really is populated (repo roster has both repos).
let start = app.state_json();
assert_eq!(start["repo_count"].as_u64(), Some(2), "roster shows both repos");
// ── DRIVE the real frame loop with an injected clock ──────────────────────
let mut tabs: BTreeSet<String> = BTreeSet::new();
let mut tools: BTreeSet<String> = BTreeSet::new();
let mut looks: BTreeSet<String> = BTreeSet::new();
let mut graph_lit = false;
let mut transitions = 0usize;
let mut last_fp = String::new();
// Frames denser than beats (200ms vs 500ms) so each beat lands in its own
// frame; run well past one full pass.
let dt = 0.2_f64;
let frames = ((beat_ms as f64 / 1000.0) * 30.0 / dt) as usize + 40;
for i in 0..frames {
let st = frame(&mut app, &egui::Context::default(), i as f64 * dt);
// NOTE: a fresh Context per frame keeps egui state minimal, but the
// injected RawInput.time still drives demo_tick deterministically.
if let Some(t) = st["tab"].as_str() {
tabs.insert(t.to_string());
}
if let Some(t) = st["tool"].as_str() {
tools.insert(t.to_string());
}
if let Some(l) = st["look"].as_str() {
looks.insert(l.to_string());
}
if st["graph"]["selected_node"].as_u64().is_some() {
graph_lit = true;
}
let fp = format!(
"tab={}|tool={}|look={}|repo_sel={}|gsel={}",
st["tab"],
st["tool"],
st["look"],
st["repos"]["selected"],
st["graph"]["selected_node"],
);
if fp != last_fp {
transitions += 1;
}
last_fp = fp;
}
// (a) ≥7 distinct VIEWS walked.
assert!(
tabs.len() >= 7,
"the demo robot-drove ≥7 distinct tabs, got {}: {tabs:?}",
tabs.len()
);
for want in ["repos", "browse", "archive", "artifact", "graph", "tools", "status"] {
assert!(tabs.contains(want), "the tour landed on {want}: {tabs:?}");
}
// (b) every Tools pane was visited.
for want in ["holger-ds", "migrate", "security", "deploy", "airgap"] {
assert!(tools.contains(want), "the tour cycled Tool {want}: {tools:?}");
}
// (c) the 3D graph was lit (a node selected → glow/shockwave/dim).
assert!(graph_lit, "the tour selected a graph node (neon glow)");
// (d) MOTION as DATA: the look re-themed (>1 distinct) and the tour produced
// many state transitions (it's ALIVE, not static).
assert!(looks.len() >= 2, "the tour re-themed the look: {looks:?}");
assert!(
transitions >= 12,
"the tour produced many state transitions, got {transitions}"
);
let _ = std::fs::remove_dir_all(&store_dir);
}