#![cfg(feature = "gui")]
use std::sync::Arc;
use egui_kittest::Harness;
use holger_ui::app::HolgerUiApp;
use holger_ui::data::UiData;
use nornir_robotui::{status, Action, Assert, Observable, RobotSession, Scenario, Step};
use serde_json::{json, Value};
use server_lib::exposed::fast_routes::FastRoutes;
use server_lib::LocalHolger;
use traits::{
ArchiveInfo, ArtifactEntry, ArtifactFormat, ArtifactId, HolgerObject, RepositoryBackendTrait,
};
struct StubRepo {
name: &'static str,
writable: bool,
}
impl StubRepo {
fn ro(name: &'static str) -> Self {
Self { name, writable: false }
}
fn rw(name: &'static str) -> Self {
Self { name, writable: true }
}
}
impl RepositoryBackendTrait for StubRepo {
fn name(&self) -> &str {
self.name
}
fn format(&self) -> ArtifactFormat {
ArtifactFormat::Rust
}
fn is_writable(&self) -> bool {
self.writable
}
fn fetch(&self, _id: &ArtifactId) -> anyhow::Result<Option<Vec<u8>>> {
Ok(None)
}
fn put(&self, _id: &ArtifactId, _data: &[u8]) -> anyhow::Result<()> {
Ok(())
}
fn list(&self, _name_filter: Option<&str>, _limit: usize) -> anyhow::Result<Vec<ArtifactEntry>> {
Ok(Vec::new())
}
fn archive_files(&self, _prefix: Option<&str>) -> anyhow::Result<Vec<String>> {
Ok(Vec::new())
}
fn archive_info(&self) -> anyhow::Result<ArchiveInfo> {
Ok(ArchiveInfo {
file_count: 0,
total_uncompressed_bytes: 0,
archive_path: self.name.to_string(),
})
}
fn handle_http2_request(
&self,
_method: &str,
_suburl: &str,
_body: &[u8],
) -> anyhow::Result<(u16, Vec<(String, String)>, Vec<u8>)> {
Ok((404, Vec::new(), Vec::new()))
}
}
struct DataRepo {
name: &'static str,
vuln: bool,
}
impl RepositoryBackendTrait for DataRepo {
fn name(&self) -> &str {
self.name
}
fn format(&self) -> ArtifactFormat {
ArtifactFormat::Rust
}
fn is_writable(&self) -> bool {
true
}
fn fetch(&self, id: &ArtifactId) -> anyhow::Result<Option<Vec<u8>>> {
if id.name == "serde" && id.version == "1.0.0" {
Ok(Some(b"SERDE_CRATE_BYTES".to_vec()))
} else {
Ok(None)
}
}
fn put(&self, _id: &ArtifactId, _data: &[u8]) -> anyhow::Result<()> {
Ok(())
}
fn list(&self, name_filter: Option<&str>, _limit: usize) -> anyhow::Result<Vec<ArtifactEntry>> {
let all = vec![
ArtifactEntry {
id: ArtifactId { namespace: None, name: "serde".into(), version: "1.0.0".into() },
size_bytes: 17,
content_type: "application/octet-stream".into(),
},
ArtifactEntry {
id: ArtifactId { namespace: None, name: "tokio".into(), version: "1.2.0".into() },
size_bytes: 9,
content_type: "application/octet-stream".into(),
},
];
Ok(match name_filter {
Some(f) => all.into_iter().filter(|e| e.id.name.contains(f)).collect(),
None => all,
})
}
fn archive_files(&self, prefix: Option<&str>) -> anyhow::Result<Vec<String>> {
let files = if self.vuln {
vec![
"crates/serde-1.0.0.crate".to_string(),
"crates/openssl-0.10.55.crate".to_string(),
"index/config.json".to_string(),
]
} else {
vec![
"crates/serde-1.0.0.crate".to_string(),
"crates/tokio-1.2.0.crate".to_string(),
"index/config.json".to_string(),
]
};
Ok(match prefix {
Some(p) => files.into_iter().filter(|f| f.starts_with(p)).collect(),
None => files,
})
}
fn archive_info(&self) -> anyhow::Result<ArchiveInfo> {
Ok(ArchiveInfo {
file_count: 3,
total_uncompressed_bytes: 4242,
archive_path: self.name.to_string(),
})
}
fn handle_http2_request(
&self,
_method: &str,
_suburl: &str,
_body: &[u8],
) -> anyhow::Result<(u16, Vec<(String, String)>, Vec<u8>)> {
Ok((404, Vec::new(), Vec::new()))
}
}
fn data_app(vuln: bool) -> HolgerUiApp {
let repos: Vec<(String, Arc<dyn RepositoryBackendTrait>)> =
vec![("rust-data".to_string(), Arc::new(DataRepo { name: "rust-data", vuln }))];
app_over(repos)
}
fn seeded_app() -> HolgerUiApp {
let repos: Vec<(String, Arc<dyn RepositoryBackendTrait>)> = vec![
("rust-dev".to_string(), Arc::new(StubRepo::rw("rust-dev"))),
("cache".to_string(), Arc::new(StubRepo::ro("cache"))),
("sparring".to_string(), Arc::new(StubRepo::ro("sparring"))),
];
app_over(repos)
}
fn app_over(repos: Vec<(String, Arc<dyn RepositoryBackendTrait>)>) -> HolgerUiApp {
let holger: Arc<dyn HolgerObject> = Arc::new(LocalHolger::new(FastRoutes::new(repos)));
let data = UiData::new(holger).expect("runtime");
let mut app = HolgerUiApp::new(data);
app.skip_splash();
app
}
struct UiApp(HolgerUiApp);
impl Observable for UiApp {
fn state_json(&self) -> Value {
self.0.state_json()
}
}
#[test]
fn robot_drives_tabs_tools_and_static_mode() {
let harness = Harness::builder()
.with_size(egui::Vec2::new(1100.0, 720.0))
.build_state(|ctx, app: &mut UiApp| app.0.draw_ui(ctx), UiApp(seeded_app()));
let mut robot = RobotSession::new(harness, "holger-ui");
let scenario = Scenario::new("holger-ui-robot")
.step(
Step::new("repos populated (LAW2: no empty data)").assert(Assert::State {
pointer: "/repo_count".into(),
expected: json!(3),
}),
)
.step(
Step::new("click Graph → tab == graph")
.action(Action::ClickLabel("Graph".into()))
.assert(Assert::State {
pointer: "/tab".into(),
expected: json!("graph"),
}),
)
.step(
Step::new("3D graph built from real repos (LAW2)").assert(Assert::State {
pointer: "/graph_built".into(),
expected: json!(true),
}),
)
.step(Step::new("graph drew widgets").assert(Assert::Drew))
.step(
Step::new("click Tools → tab == tools")
.action(Action::ClickLabel("Tools".into()))
.assert(Assert::State {
pointer: "/tab".into(),
expected: json!("tools"),
}),
)
.step(
Step::new("select Deploy (Skíðblaðnir) → tool == deploy")
.action(Action::ClickLabel("4 · deploy · Skíðblaðnir".into()))
.assert(Assert::State {
pointer: "/tool".into(),
expected: json!("deploy"),
}),
)
.step(
Step::new("engage static silent-running → static_mode == true")
.action(Action::ClickLabel("⚓ static".into()))
.assert(Assert::State {
pointer: "/static_mode".into(),
expected: json!(true),
}),
)
.step(
Step::new("writes disabled in static → write_enabled == false").assert(Assert::State {
pointer: "/write_enabled".into(),
expected: json!(false),
}),
);
let rows = robot.run_scenario(&scenario);
assert_eq!(rows.len(), 8, "one matrix row per robot step");
for r in &rows {
assert_eq!(
r.status,
status::PASS,
"robot step {:?} should pass; message: {}",
r.test_name,
r.message
);
}
}
#[test]
fn robot_drives_lineage_tab() {
let harness = Harness::builder()
.with_size(egui::Vec2::new(1100.0, 720.0))
.build_state(|ctx, app: &mut UiApp| app.0.draw_ui(ctx), UiApp(seeded_app()));
let mut robot = RobotSession::new(harness, "holger-ui");
let scenario = Scenario::new("holger-ui-lineage-robot")
.step(
Step::new("open Lineage → tab == lineage")
.action(Action::ClickLabel("Lineage".into()))
.assert(Assert::State { pointer: "/tab".into(), expected: json!("lineage") }),
)
.step(
Step::new("default mode is the rotating 3D fill")
.assert(Assert::State { pointer: "/lineage_mode".into(), expected: json!("graph3d") }),
)
.step(Step::new("3D cache-fill graph drew").assert(Assert::Drew))
.step(
Step::new("toggle time-helix companion → mode == helix")
.action(Action::ClickLabel("🌀 time helix".into()))
.assert(Assert::State { pointer: "/lineage_mode".into(), expected: json!("helix") }),
)
.step(Step::new("time-helix drew").assert(Assert::Drew))
.step(
Step::new("back to 3D fill → mode == graph3d")
.action(Action::ClickLabel("◉ 3D fill".into()))
.assert(Assert::State { pointer: "/lineage_mode".into(), expected: json!("graph3d") }),
)
.step(Step::new("cache kept filling (drew again)").assert(Assert::Drew));
let rows = robot.run_scenario(&scenario);
assert_eq!(rows.len(), 7, "one matrix row per robot step");
for r in &rows {
assert_eq!(r.status, status::PASS, "robot step {:?} should pass; message: {}", r.test_name, r.message);
}
let filled = robot.app().0.state_json();
let events = filled["lineage"]["total_events"].as_u64().unwrap_or(0);
let artifacts = filled["lineage"]["nodes"]
.as_array()
.map(|a| a.iter().filter(|n| n["kind"] == "artifact").count())
.unwrap_or(0);
assert!(events >= 1, "the live cache-fill produced arrivals: total_events={events}");
assert!(artifacts >= 1, "cached artifact nodes popped in: {artifacts}");
}
#[test]
fn robot_drives_tools_1_3_5_panes() {
let harness = Harness::builder()
.with_size(egui::Vec2::new(1100.0, 720.0))
.build_state(|ctx, app: &mut UiApp| app.0.draw_ui(ctx), UiApp(seeded_app()));
let mut robot = RobotSession::new(harness, "holger-ui");
let scenario = Scenario::new("holger-ui-tools-robot")
.step(
Step::new("open Tools")
.action(Action::ClickLabel("Tools".into()))
.assert(Assert::State { pointer: "/tab".into(), expected: json!("tools") }),
)
.step(
Step::new("select Tool #1 holger-ds → tool == holger-ds")
.action(Action::ClickLabel("1 · holger-ds".into()))
.assert(Assert::State { pointer: "/tool".into(), expected: json!("holger-ds") }),
)
.step(
Step::new("holger-ds default scale == micro")
.assert(Assert::State { pointer: "/holger_ds/scale".into(), expected: json!("micro") }),
)
.step(
Step::new("select Tool #3 security → tool == security")
.action(Action::ClickLabel("3 · security · Móðguðr".into()))
.assert(Assert::State { pointer: "/tool".into(), expected: json!("security") }),
)
.step(
Step::new("scan selected repo → Pass verdict over the empty corpus")
.action(Action::ClickLabel("Scan selected repo".into()))
.assert(Assert::State { pointer: "/security/decision".into(), expected: json!("pass") }),
)
.step(
Step::new("select Tool #5 airgap → tool == airgap")
.action(Action::ClickLabel("5 · airgap populate".into()))
.assert(Assert::State { pointer: "/tool".into(), expected: json!("airgap") }),
)
.step(
Step::new("airgap default target == holger")
.assert(Assert::State { pointer: "/airgap/target".into(), expected: json!("holger") }),
)
.step(
Step::new("select Tool #2 migrate → tool == migrate")
.action(Action::ClickLabel("2 · migrate / suck".into()))
.assert(Assert::State { pointer: "/tool".into(), expected: json!("migrate") }),
)
.step(
Step::new("migrate default source == nexus")
.assert(Assert::State { pointer: "/migrate/source".into(), expected: json!("nexus") }),
);
let rows = robot.run_scenario(&scenario);
assert_eq!(rows.len(), 9, "one matrix row per robot step");
for r in &rows {
assert_eq!(r.status, status::PASS, "robot step {:?}: {}", r.test_name, r.message);
}
}
#[test]
fn robot_runs_each_tool_real_action_and_sees_version() {
let harness = Harness::builder()
.with_size(egui::Vec2::new(1100.0, 720.0))
.build_state(|ctx, app: &mut UiApp| app.0.draw_ui(ctx), UiApp(seeded_app()));
let mut robot = RobotSession::new(harness, "holger-ui");
let v = json!(holger_ui::app::HOLGER_UI_VERSION);
assert!(
holger_ui::app::HOLGER_UI_VERSION.chars().next().map(|c| c.is_ascii_digit()).unwrap_or(false),
"version looks like a version: {}",
holger_ui::app::HOLGER_UI_VERSION
);
let scenario = Scenario::new("holger-ui-run-tools")
.step(
Step::new("splash carries a version string")
.assert(Assert::State { pointer: "/version".into(), expected: v }),
)
.step(
Step::new("open Tools")
.action(Action::ClickLabel("Tools".into()))
.assert(Assert::State { pointer: "/tab".into(), expected: json!("tools") }),
)
.step(
Step::new("select holger-ds")
.action(Action::ClickLabel("1 · holger-ds".into()))
.assert(Assert::State { pointer: "/tool".into(), expected: json!("holger-ds") }),
)
.step(
Step::new("Run benchmark → a REAL throughput result")
.action(Action::ClickLabel("Run benchmark… (over the package protocol)".into()))
.assert(Assert::State { pointer: "/holger_ds/result/ran".into(), expected: json!(true) }),
)
.step(
Step::new("select migrate")
.action(Action::ClickLabel("2 · migrate / suck".into()))
.assert(Assert::State { pointer: "/tool".into(), expected: json!("migrate") }),
)
.step(
Step::new("Run migrate → connector invoked, clear url validation")
.action(Action::ClickLabel("Run migrate… (via holger-agent)".into()))
.assert(Assert::State { pointer: "/migrate/result/ran".into(), expected: json!(false) }),
)
.step(
Step::new("select security")
.action(Action::ClickLabel("3 · security · Móðguðr".into()))
.assert(Assert::State { pointer: "/tool".into(), expected: json!("security") }),
)
.step(
Step::new("Scan → Pass verdict over the empty corpus")
.action(Action::ClickLabel("Scan selected repo".into()))
.assert(Assert::State { pointer: "/security/decision".into(), expected: json!("pass") }),
)
.step(
Step::new("select airgap")
.action(Action::ClickLabel("5 · airgap populate".into()))
.assert(Assert::State { pointer: "/tool".into(), expected: json!("airgap") }),
)
.step(
Step::new("Run populate (dry-run) → a real plan/result")
.action(Action::ClickLabel("Run populate… (dry-run plan)".into()))
.assert(Assert::State { pointer: "/airgap/run/ran".into(), expected: json!(true) }),
);
let rows = robot.run_scenario(&scenario);
assert_eq!(rows.len(), 10, "one matrix row per robot step");
for r in &rows {
assert_eq!(r.status, status::PASS, "robot step {:?}: {}", r.test_name, r.message);
}
let s = robot.state();
assert!(
s["migrate"]["result"]["error"].as_str().unwrap_or("").contains("url"),
"migrate surfaced a clear url-required message: {s}"
);
assert!(
s["holger_ds"]["result"]["ops"].as_u64().unwrap_or(0) > 0,
"holger-ds completed GETs over the package protocol: {s}"
);
assert!(
s["airgap"]["run"]["plan"].as_str().unwrap_or("").contains("airgap populate"),
"airgap produced a real plan: {s}"
);
}
#[test]
fn robot_clicks_repo_node_and_lights_its_subgraph() {
let harness = Harness::builder()
.with_size(egui::Vec2::new(1100.0, 720.0))
.build_state(|ctx, app: &mut UiApp| app.0.draw_ui(ctx), UiApp(seeded_app()));
let mut robot = RobotSession::new(harness, "holger-ui");
robot.click_label("Graph").expect("Graph tab is clickable");
robot.step();
let s = robot.state();
assert_eq!(s["graph_built"], json!(true), "graph built from real repos: {s}");
assert_eq!(
s["graph"]["effects_policy"],
json!("full"),
"the beauty is ON — DecorPolicy::Full: {s}"
);
assert_eq!(s["graph"]["palette"], json!("neon"), "neon palette active: {s}");
let nodes = s["graph"]["nodes"].as_u64().unwrap_or(0);
assert!(nodes >= 4, "server + 3 repositories (≥4 nodes): {s}");
let kinds = s["graph"]["kinds"].as_array().expect("per-node kind palette");
assert_eq!(kinds.len() as u64, nodes, "one kind per node: {s}");
assert_eq!(kinds[0], json!("server"), "node 0 is the holger server anchor: {s}");
assert!(
s["graph"]["selected_node"].is_null(),
"no selection yet → calm default: {s}"
);
assert_eq!(s["graph"]["highlighted_count"], json!(0), "nothing lit yet: {s}");
assert_eq!(s["graph"]["dimmed_alpha"], json!(1.0), "nothing dimmed yet: {s}");
robot.app_mut().0.select_graph_node(1);
robot.step();
let s = robot.state();
assert_eq!(s["graph"]["selected_node"], json!(1), "the repo node is selected: {s}");
let sel_label = s["graph"]["selected_label"].as_str().unwrap_or("");
assert!(
["rust-dev", "cache", "sparring"].contains(&sel_label),
"the selected node is a seeded repository, got {sel_label:?}: {s}"
);
assert!(
s["graph"]["selected_kind"].is_string(),
"the selected node carries a neon-palette kind: {s}"
);
let lit = s["graph"]["highlighted_neighbours"]
.as_array()
.expect("the lit subgraph is an array");
assert!(!lit.is_empty(), "clicking a repo lights a NON-EMPTY subgraph: {s}");
assert!(
s["graph"]["highlighted_count"].as_u64().unwrap_or(0) >= 1,
"≥1 neighbour lit: {s}"
);
assert!(
s["graph"]["dimmed_alpha"].as_f64().unwrap_or(1.0) < 1.0,
"selecting a repo dims the unrelated rest: {s}"
);
}
#[test]
fn robot_static_mode_reflects_server_profile() {
let ro: Vec<(String, Arc<dyn RepositoryBackendTrait>)> = vec![
("locked-a".to_string(), Arc::new(StubRepo::ro("locked-a"))),
("locked-b".to_string(), Arc::new(StubRepo::ro("locked-b"))),
];
let app = app_over(ro);
let s = app.state_json();
assert_eq!(s["static_mode"], json!(true), "all-read-only server starts static: {s}");
assert_eq!(s["profile"]["read_only"], json!(true), "server profile is read-only: {s}");
assert_eq!(s["profile"]["loaded"], json!(true), "profile was fetched: {s}");
assert!(
s["profile"]["label"].as_str().unwrap_or("").contains("SILENT RUNNING"),
"profile carries the silent-running label: {s}"
);
assert_eq!(s["profile"]["writable_repo_count"], json!(0), "no writable repos: {s}");
assert_eq!(s["look"], json!("skade_vinter"), "static look engaged from the profile: {s}");
let rw: Vec<(String, Arc<dyn RepositoryBackendTrait>)> = vec![
("live".to_string(), Arc::new(StubRepo::rw("live"))),
("locked".to_string(), Arc::new(StubRepo::ro("locked"))),
];
let app = app_over(rw);
let s = app.state_json();
assert_eq!(s["static_mode"], json!(false), "a writable server starts dynamic: {s}");
assert_eq!(s["profile"]["read_only"], json!(false), "server profile is dynamic: {s}");
assert_eq!(s["profile"]["writable_repo_count"], json!(1), "one writable repo: {s}");
}
#[test]
fn robot_static_mode_disables_other_tools() {
let harness = Harness::builder()
.with_size(egui::Vec2::new(1100.0, 720.0))
.build_state(|ctx, app: &mut UiApp| app.0.draw_ui(ctx), UiApp(seeded_app()));
let mut robot = RobotSession::new(harness, "holger-ui");
robot.click_label("Tools").expect("Tools tab");
robot.step();
robot.click_label("1 · holger-ds").expect("holger-ds selectable (dynamic)");
robot.step();
assert_eq!(robot.state()["tool"], json!("holger-ds"), "tool == holger-ds");
robot.click_label("⚓ static").expect("static toggle");
robot.step();
assert_eq!(robot.state()["static_mode"], json!(true), "static engaged");
assert_eq!(robot.state()["look"], json!("skade_vinter"), "winter look engaged");
for label in ["2 · migrate / suck", "3 · security · Móðguðr", "4 · deploy · Skíðblaðnir", "5 · airgap populate"] {
let _ = robot.click_label(label);
robot.step();
assert_eq!(
robot.state()["tool"],
json!("holger-ds"),
"tool unchanged after swallowed click on disabled {label:?}"
);
}
robot.click_label("⚓ static").expect("static toggle off");
robot.step();
assert_eq!(robot.state()["static_mode"], json!(false), "static disengaged");
robot.click_label("2 · migrate / suck").expect("migrate selectable again (dynamic)");
robot.step();
assert_eq!(robot.state()["tool"], json!("migrate"), "migrate selectable once dynamic");
}
#[test]
fn robot_repo_row_selection_propagates() {
let harness = Harness::builder()
.with_size(egui::Vec2::new(1100.0, 720.0))
.build_state(|ctx, app: &mut UiApp| app.0.draw_ui(ctx), UiApp(seeded_app()));
let mut robot = RobotSession::new(harness, "holger-ui");
robot.click_label("Repos").expect("Repos tab");
robot.step();
assert_eq!(robot.state()["tab"], json!("repos"));
assert_eq!(robot.state()["repos"]["selected"], json!(0), "first repo selected by default");
robot.app_mut().0.select_repo(2);
robot.step();
assert_eq!(robot.state()["repos"]["selected"], json!(2), "selecting sparring → index 2");
assert_eq!(robot.state()["repos"]["repos"][2]["name"], json!("sparring"), "index 2 is sparring");
robot.click_label("Tools").expect("Tools");
robot.step();
robot.click_label("3 · security · Móðguðr").expect("security");
robot.step();
robot.click_label("Scan selected repo").expect("scan");
robot.step();
assert_eq!(
robot.state()["security"]["repository"],
json!("sparring"),
"the scan targets the row the operator selected"
);
}
#[test]
fn robot_status_tab_nav_and_refresh() {
let harness = Harness::builder()
.with_size(egui::Vec2::new(1100.0, 720.0))
.build_state(|ctx, app: &mut UiApp| app.0.draw_ui(ctx), UiApp(seeded_app()));
let mut robot = RobotSession::new(harness, "holger-ui");
robot.click_label("Graph").expect("Graph tab");
robot.step();
robot.click_label("Status").expect("Status tab");
robot.step();
assert_eq!(robot.state()["tab"], json!("status"));
robot.click_label("⟳ refresh").expect("refresh button");
robot.step();
let s = robot.state();
assert_eq!(s["status"]["loaded"], json!(true), "health loaded: {s}");
assert!(s["status"]["version"].as_str().map(|v| !v.is_empty()).unwrap_or(false), "version present: {s}");
assert_eq!(s["status"]["error"], json!(null), "no error on a healthy server: {s}");
assert_eq!(s["status"]["status"], json!("ok"), "status ok: {s}");
}
#[test]
fn robot_upload_writable_reflects_selected_repo() {
let harness = Harness::builder()
.with_size(egui::Vec2::new(1100.0, 720.0))
.build_state(|ctx, app: &mut UiApp| app.0.draw_ui(ctx), UiApp(seeded_app()));
let mut robot = RobotSession::new(harness, "holger-ui");
robot.click_label("Upload").expect("Upload tab");
robot.step();
assert_eq!(robot.state()["tab"], json!("upload"));
assert_eq!(robot.state()["upload"]["writable"], json!(false), "read-only repo → upload disabled");
robot.app_mut().0.select_repo(1);
robot.step();
let s = robot.state();
assert_eq!(s["repos"]["repos"][1]["name"], json!("rust-dev"), "index 1 is rust-dev");
assert_eq!(s["upload"]["writable"], json!(true), "writable repo → upload enabled: {s}");
}
#[test]
fn robot_static_mode_redirects_upload_to_status() {
let harness = Harness::builder()
.with_size(egui::Vec2::new(1100.0, 720.0))
.build_state(|ctx, app: &mut UiApp| app.0.draw_ui(ctx), UiApp(seeded_app()));
let mut robot = RobotSession::new(harness, "holger-ui");
robot.click_label("Upload").expect("Upload tab");
robot.step();
assert_eq!(robot.state()["tab"], json!("upload"));
robot.click_label("⚓ static").expect("static toggle");
robot.step();
assert_eq!(robot.state()["static_mode"], json!(true));
assert_eq!(robot.state()["tab"], json!("status"), "static mode forces Upload → Status");
let _ = robot.click_label("Upload");
robot.step();
assert_eq!(robot.state()["tab"], json!("status"), "disabled Upload nav stays on Status");
}
#[test]
fn robot_look_preset_switcher_and_static_override() {
let harness = Harness::builder()
.with_size(egui::Vec2::new(1100.0, 720.0))
.build_state(|ctx, app: &mut UiApp| app.0.draw_ui(ctx), UiApp(seeded_app()));
let mut robot = RobotSession::new(harness, "holger-ui");
assert_eq!(robot.state()["look"], json!("dark"), "default look");
robot.click_label("device").expect("device preset label");
robot.step();
assert_eq!(robot.state()["look"], json!("device"), "clicking device switches the look");
robot.click_label("⚓ static").expect("static toggle");
robot.step();
assert_eq!(robot.state()["look"], json!("skade_vinter"), "static overrides the preset");
robot.click_label("⚓ static").expect("static toggle off");
robot.step();
assert_eq!(robot.state()["look"], json!("device"), "manual preset restored after static");
}
#[test]
fn robot_tool_config_selectables_change_state() {
let harness = Harness::builder()
.with_size(egui::Vec2::new(1100.0, 720.0))
.build_state(|ctx, app: &mut UiApp| app.0.draw_ui(ctx), UiApp(seeded_app()));
let mut robot = RobotSession::new(harness, "holger-ui");
robot.click_label("Tools").expect("Tools");
robot.step();
robot.click_label("1 · holger-ds").expect("holger-ds");
robot.step();
robot.click_label("large").expect("large scale");
robot.step();
assert_eq!(robot.state()["holger_ds"]["scale"], json!("large"), "scale → large");
robot.click_label("2 · migrate / suck").expect("migrate");
robot.step();
robot.click_label("artifactory").expect("artifactory source");
robot.step();
assert_eq!(robot.state()["migrate"]["source"], json!("artifactory"), "source → artifactory");
robot.click_label("5 · airgap populate").expect("airgap");
robot.step();
robot.click_label("nexus").expect("nexus target");
robot.step();
assert_eq!(robot.state()["airgap"]["target"], json!("nexus"), "target → nexus");
robot.click_label("oidc").expect("oidc auth");
robot.step();
assert_eq!(robot.state()["airgap"]["auth"], json!("oidc"), "auth → oidc");
}
#[test]
fn robot_airgap_validate_can_run_state() {
let harness = Harness::builder()
.with_size(egui::Vec2::new(1100.0, 720.0))
.build_state(|ctx, app: &mut UiApp| app.0.draw_ui(ctx), UiApp(seeded_app()));
let mut robot = RobotSession::new(harness, "holger-ui");
robot.click_label("Tools").expect("Tools");
robot.step();
robot.click_label("5 · airgap populate").expect("airgap");
robot.step();
let s = robot.state();
assert_eq!(s["airgap"]["dry_run"], json!(true), "demo default is dry-run: {s}");
assert_eq!(s["airgap"]["can_run"], json!(true), "dry-run plan can run: {s}");
assert_eq!(s["airgap"]["validate_error"], json!(null), "no validation error by default: {s}");
{
let app = robot.app_mut();
app.0.airgap_set_dry_run(false);
}
robot.step();
let s = robot.state();
assert_eq!(s["airgap"]["can_run"], json!(false), "push with no creds cannot run: {s}");
assert!(
s["airgap"]["validate_error"].as_str().map(|e| !e.is_empty()).unwrap_or(false),
"the red branch carries a validation error: {s}"
);
}
#[test]
fn robot_startup_splash_dismisses_on_click() {
let repos: Vec<(String, Arc<dyn RepositoryBackendTrait>)> = vec![
("rust-dev".to_string(), Arc::new(StubRepo::rw("rust-dev"))),
];
let holger: Arc<dyn HolgerObject> = Arc::new(LocalHolger::new(FastRoutes::new(repos)));
let data = UiData::new(holger).expect("runtime");
let app = HolgerUiApp::new(data); let harness = Harness::builder()
.with_size(egui::Vec2::new(1100.0, 720.0))
.build_state(|ctx, app: &mut UiApp| app.0.draw_ui(ctx), UiApp(app));
let mut robot = RobotSession::new(harness, "holger-ui");
robot.step();
assert_eq!(robot.state()["splash_done"], json!(false), "splash shows on startup");
assert!(!robot.label_visible("Status"), "tabs hidden behind the splash");
let pos = egui::Pos2::new(120.0, 120.0);
let m = egui::Modifiers::default();
robot.inject_events([
egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed: true, modifiers: m },
egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed: false, modifiers: m },
]);
robot.step();
robot.step();
assert_eq!(robot.state()["splash_done"], json!(true), "splash dismissed on click");
assert!(robot.label_visible("Status"), "tabs visible after dismiss");
}
#[test]
fn robot_artifact_fetch_found_and_not_found() {
let harness = Harness::builder()
.with_size(egui::Vec2::new(1100.0, 720.0))
.build_state(|ctx, app: &mut UiApp| app.0.draw_ui(ctx), UiApp(data_app(false)));
let mut robot = RobotSession::new(harness, "holger-ui");
robot.click_label("Artifact").expect("Artifact tab");
robot.step();
assert_eq!(robot.state()["tab"], json!("artifact"));
robot.app_mut().0.set_artifact_query("rust-data", "serde", "1.0.0");
robot.click_label("fetch").expect("fetch button");
robot.step();
let s = robot.state();
assert_eq!(s["artifact"]["name"], json!("serde"), "fetched name: {s}");
assert_eq!(s["artifact"]["found"], json!(true), "serde found: {s}");
assert!(s["artifact"]["size_bytes"].as_u64().unwrap_or(0) > 0, "non-zero size: {s}");
assert_eq!(s["artifact"]["error"], json!(null), "no transport error: {s}");
robot.app_mut().0.set_artifact_query("rust-data", "ghost", "9.9.9");
robot.click_label("fetch").expect("fetch button");
robot.step();
let s = robot.state();
assert_eq!(s["artifact"]["found"], json!(false), "ghost not found: {s}");
assert_eq!(s["artifact"]["error"], json!(null), "clean NOT_FOUND, not an error: {s}");
}
#[test]
fn robot_browse_tab_refresh_lists_artifacts() {
let harness = Harness::builder()
.with_size(egui::Vec2::new(1100.0, 720.0))
.build_state(|ctx, app: &mut UiApp| app.0.draw_ui(ctx), UiApp(data_app(false)));
let mut robot = RobotSession::new(harness, "holger-ui");
robot.click_label("Browse").expect("Browse tab");
robot.step();
assert_eq!(robot.state()["tab"], json!("browse"));
robot.click_label("⟳ refresh").expect("refresh button");
robot.step();
let s = robot.state();
assert_eq!(s["browse"]["repository"], json!("rust-data"), "browse targets the selected repo: {s}");
let entries = s["browse"]["entries"].as_array().expect("entries array");
assert_eq!(entries.len(), 2, "serde + tokio listed: {s}");
assert!(entries.iter().any(|e| e["name"] == json!("serde")), "serde row: {s}");
assert!(entries.iter().any(|e| e["name"] == json!("tokio")), "tokio row: {s}");
}
#[test]
fn robot_archive_tab_refresh_stats_and_files() {
let harness = Harness::builder()
.with_size(egui::Vec2::new(1100.0, 720.0))
.build_state(|ctx, app: &mut UiApp| app.0.draw_ui(ctx), UiApp(data_app(false)));
let mut robot = RobotSession::new(harness, "holger-ui");
robot.click_label("Archive").expect("Archive tab");
robot.step();
assert_eq!(robot.state()["tab"], json!("archive"));
robot.click_label("⟳ refresh").expect("refresh button");
robot.step();
let s = robot.state();
assert_eq!(s["archive"]["file_count"], json!(3), "archive file_count: {s}");
assert_eq!(s["archive"]["total_uncompressed_bytes"], json!(4242), "archive bytes: {s}");
let files = s["archive"]["files"].as_array().expect("files array");
assert_eq!(files.len(), 3, "three archive entries: {s}");
robot.click_label("Graph").expect("Graph tab");
robot.step();
let s = robot.state();
let kinds = s["graph"]["kinds"].as_array().expect("kinds array");
assert!(kinds.iter().any(|k| k == &json!("artifact")), "graph grew artifact nodes: {s}");
}
#[test]
fn robot_security_scan_block_verdict() {
let harness = Harness::builder()
.with_size(egui::Vec2::new(1100.0, 720.0))
.build_state(|ctx, app: &mut UiApp| app.0.draw_ui(ctx), UiApp(data_app(true)));
let mut robot = RobotSession::new(harness, "holger-ui");
robot.click_label("Tools").expect("Tools");
robot.step();
robot.click_label("3 · security · Móðguðr").expect("security");
robot.step();
robot.click_label("Scan selected repo").expect("scan");
robot.step();
let s = robot.state();
assert_eq!(s["security"]["decision"], json!("block"), "vuln corpus blocks: {s}");
assert!(s["security"]["block"].as_u64().unwrap_or(0) >= 1, "≥1 blocked artifact: {s}");
assert_eq!(s["security"]["error"], json!(null), "block is a verdict, not a transport error: {s}");
let findings = s["security"]["findings"].as_array().expect("findings array");
let openssl = findings.iter().find(|f| f["name"] == json!("openssl")).expect("openssl finding");
let ids = openssl["ids"].as_array().expect("ids array");
assert!(
ids.iter().any(|i| i.as_str().map(|s| s.contains("RUSTSEC-2023-0044")).unwrap_or(false)),
"openssl finding carries the advisory id: {s}"
);
}
#[test]
fn robot_deploys_selected_repo_sealed_bundle() {
let harness = Harness::builder()
.with_size(egui::Vec2::new(1100.0, 720.0))
.build_state(|ctx, app: &mut UiApp| app.0.draw_ui(ctx), UiApp(data_app(false)));
let mut robot = RobotSession::new(harness, "holger-ui");
let scenario = Scenario::new("holger-ui-deploy")
.step(
Step::new("open Tools")
.action(Action::ClickLabel("Tools".into()))
.assert(Assert::State { pointer: "/tab".into(), expected: json!("tools") }),
)
.step(
Step::new("select Deploy (Skíðblaðnir) → tool == deploy")
.action(Action::ClickLabel("4 · deploy · Skíðblaðnir".into()))
.assert(Assert::State { pointer: "/tool".into(), expected: json!("deploy") }),
)
.step(
Step::new("pick the nexus target")
.action(Action::ClickLabel("nexus".into()))
.assert(Assert::State { pointer: "/deploy/target".into(), expected: json!("nexus") }),
)
.step(
Step::new("Deploy to test… → a REAL airgap deploy plan over the sealed bundle")
.action(Action::ClickLabel("Deploy to test…".into()))
.assert(Assert::State { pointer: "/deploy/result/ran".into(), expected: json!(true) }),
);
let rows = robot.run_scenario(&scenario);
assert_eq!(rows.len(), 4, "one matrix row per robot step");
for r in &rows {
assert_eq!(r.status, status::PASS, "robot step {:?}: {}", r.test_name, r.message);
}
let s = robot.state();
assert_eq!(s["deploy"]["result"]["file_count"], json!(3), "sealed bundle file count: {s}");
assert_eq!(s["deploy"]["result"]["target"], json!("nexus"), "deployed to the picked target: {s}");
assert!(
s["deploy"]["result"]["plan"].as_str().unwrap_or("").contains("airgap populate"),
"deploy produced a real airgap-engine plan: {s}"
);
assert!(
s["deploy"]["result"]["plan"].as_str().unwrap_or("").contains("push-only"),
"deploy carries the already-sealed bundle (push-only): {s}"
);
assert_eq!(s["deploy"]["result"]["error"], json!(null), "a clean deploy plan, no error: {s}");
}
#[test]
fn robot_opens_about_and_dispatches_the_jera_demo() {
let harness = Harness::builder()
.with_size(egui::Vec2::new(1100.0, 720.0))
.build_state(|ctx, app: &mut UiApp| app.0.draw_ui(ctx), UiApp(seeded_app()));
let mut robot = RobotSession::new(harness, "holger-ui");
let scenario = Scenario::new("holger-ui-about-and-demo")
.step(
Step::new("open About → popup open + identity reported")
.action(Action::ClickLabel("ℹ About".into()))
.assert(Assert::State { pointer: "/about/open".into(), expected: json!(true) }),
)
.step(
Step::new("About reports the wordmark identity")
.assert(Assert::State { pointer: "/about/wordmark".into(), expected: json!("holger") }),
)
.step(
Step::new("dismiss About with Close")
.action(Action::ClickLabel("Close".into()))
.assert(Assert::State { pointer: "/about/open".into(), expected: json!(false) }),
)
.step(
Step::new("open Tools")
.action(Action::ClickLabel("Tools".into()))
.assert(Assert::State { pointer: "/tab".into(), expected: json!("tools") }),
)
.step(
Step::new("select the demo appliance tool")
.action(Action::ClickLabel("6 · demo appliance".into()))
.assert(Assert::State { pointer: "/tool".into(), expected: json!("demo") }),
)
.step(
Step::new("Start demo appliance → dispatched as a jera job")
.action(Action::ClickLabel("Start demo appliance… (as a jera job)".into()))
.assert(Assert::State { pointer: "/demo_appliance/dispatched".into(), expected: json!(true) }),
);
let rows = robot.run_scenario(&scenario);
assert_eq!(rows.len(), 6, "one matrix row per robot step");
for r in &rows {
assert_eq!(r.status, status::PASS, "robot step {:?}: {}", r.test_name, r.message);
}
let s = robot.state();
let job_id = s["demo_appliance"]["job_id"].as_str().unwrap_or("");
assert!(!job_id.is_empty(), "the demo dispatch recorded a jera job id: {s}");
assert!(
s["demo_appliance"]["image"].as_str().unwrap_or("").contains("registry"),
"the dispatched demo container is the pinned registry image: {s}"
);
let fw = s["about"]["framework"].as_array().expect("framework list");
assert!(
fw.iter().any(|c| c.as_str().unwrap_or("").contains("jera")),
"About names the shared jera job manager: {s}"
);
}