#![cfg(all(feature = "viz", feature = "server"))]
use std::collections::BTreeMap;
use std::net::TcpListener;
use std::path::PathBuf;
use std::process::{Child, Command};
use std::time::{Duration, Instant};
use egui_kittest::Harness;
use nornir::autonom::atom_coverage_rows;
use nornir::viz::control::VizCommand;
use nornir::viz::UrdrThreadsApp;
use nornir::warehouse::iceberg::IcebergWarehouse;
use nornir::warehouse::surface_coverage::{
append_surface_coverage, query_surface_coverage, CoverageSelector, CoverageSummary, Verdict,
};
use nornir::viz::surfaces::tab_pane_ids;
use nornir_robotui::{all_atoms, error_atoms};
use nornir_testmatrix::discover::DiscoveredAtom;
use uuid::Uuid;
const TABS: &[&str] = &[
"Nornir", "Timeline", "DepGraph", "CallGraph", "Architecture", "Metro", "Helix", "Bloodstream",
"Funnel", "TimeTravel", "LiveRun", "Release", "Knowledge", "Warehouse", "WarehouseDeck", "Mcp",
"Search", "Gates", "Bench", "Test", "Leaderboard", "Security", "Dwarfs", "Holger", "Manual",
"Claude", "Models",
];
fn registry_tab_to_walk_tab(section: &str) -> String {
match section {
"test" => "Test",
"bench" => "Bench",
"release" => "Release",
"nornir" => "Nornir",
other => {
let mut c = other.chars();
return match c.next() {
Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
None => String::new(),
};
}
}
.to_string()
}
fn free_port() -> u16 {
TcpListener::bind("127.0.0.1:0").unwrap().local_addr().unwrap().port()
}
struct EmptyServer {
child: Child,
root: PathBuf,
endpoint: String,
token: String,
}
impl Drop for EmptyServer {
fn drop(&mut self) {
let _ = self.child.kill();
let _ = self.child.wait();
let _ = std::fs::remove_dir_all(&self.root);
}
}
fn spawn_empty_server() -> EmptyServer {
let token = "atom-coverage-token-0123456789abcd";
let port = free_port();
let endpoint = format!("http://127.0.0.1:{port}");
let root = std::env::temp_dir()
.join(format!("nornir-atom-cov-{}-{}", std::process::id(), Uuid::new_v4()));
let _ = std::fs::remove_dir_all(&root);
let server_home = root.join("home");
std::fs::create_dir_all(server_home.join(".nornir").join("workspaces")).unwrap();
let child = Command::new(env!("CARGO_BIN_EXE_nornir-server"))
.env("NORNIR_SERVER_ADDR", format!("127.0.0.1:{port}"))
.env("NORNIR_SERVER_TOKEN", token)
.env("HOME", &server_home)
.env("NORNIR_POLL", "3600s")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()
.expect("spawn nornir-server");
let srv = EmptyServer { child, root, endpoint: endpoint.clone(), token: token.to_string() };
let deadline = Instant::now() + Duration::from_secs(45);
loop {
if nornir::viz::remote::list_workspaces(&endpoint, token).is_ok() {
break;
}
assert!(Instant::now() < deadline, "empty server never came up");
std::thread::sleep(Duration::from_millis(200));
}
srv
}
fn remote_empty_app(srv: &EmptyServer) -> UrdrThreadsApp {
UrdrThreadsApp::with_remote(
srv.endpoint.clone(),
srv.token.clone(),
String::new(),
PathBuf::new(),
Vec::new(),
)
}
fn walk_atoms(
harness: &mut Harness<'_, UrdrThreadsApp>,
) -> (Vec<DiscoveredAtom>, BTreeMap<String, Vec<String>>) {
let mut batch: Vec<DiscoveredAtom> = Vec::new();
let mut by_tab: BTreeMap<String, Vec<String>> = BTreeMap::new();
for tab in TABS {
harness.state_mut().apply_command(&VizCommand {
tab: Some((*tab).to_string()),
..Default::default()
});
for _ in 0..4 {
let _ = harness.run_ok();
}
let clean = error_atoms(harness).is_empty();
let atoms = all_atoms(harness);
let labels = by_tab.entry((*tab).to_string()).or_default();
for a in atoms {
labels.push(a.text.clone());
batch.push(DiscoveredAtom {
tab: (*tab).to_string(),
label: a.text,
role: a.role,
ran: true,
clean,
});
}
}
(batch, by_tab)
}
#[test]
fn atom_rows_write_read_and_autonom_counts_them() {
let srv = spawn_empty_server();
let mut harness: Harness<'_, UrdrThreadsApp> = Harness::builder()
.with_size(egui::Vec2::new(1400.0, 1000.0))
.build_state(|ctx, app: &mut UrdrThreadsApp| app.draw_ui(ctx), remote_empty_app(&srv));
let (atoms, _by_tab) = walk_atoms(&mut harness);
assert!(!atoms.is_empty(), "the walk discovered atoms across the tabs");
let dir = tempfile::tempdir().unwrap();
let wh = IcebergWarehouse::open(dir.path()).unwrap();
let run_id = format!("atom-run-{}", Uuid::new_v4());
let ts = chrono::Utc::now().timestamp_micros();
let rows = atom_coverage_rows(&run_id, "nornir", &atoms, ts);
assert!(!rows.is_empty(), "produced one ui_atom row per discovered atom");
for r in &rows {
assert_eq!(r.kind, "ui_atom", "row kind tags the atom surface");
assert!(r.id.contains('/'), "id is `<tab>/<label>`: {}", r.id);
assert_eq!(
r.surface_key,
format!("ui_atom:{}@na", r.id),
"surface_key follows the SurfaceNode::key_str convention",
);
}
wh.block_on(append_surface_coverage(&wh, &rows)).unwrap();
let back = wh
.block_on(query_surface_coverage(&wh, &CoverageSelector::Run(run_id.clone())))
.unwrap();
assert_eq!(back.len(), rows.len(), "every atom row read back from the warehouse");
assert!(back.iter().all(|r| r.kind == "ui_atom"), "the read-back rows are ui_atom rows");
let covered = back.iter().filter(|r| r.verdict() == Verdict::Covered).count();
let missing = back.iter().filter(|r| r.verdict() == Verdict::Missing).count();
assert!(covered >= 1, "at least one atom in a clean tab is covered");
assert_eq!(
missing, 0,
"no atom is missing — every walked tab is error-atom-clean (S2 green)",
);
let summary = CoverageSummary::from_rows(&back);
assert_eq!(summary.total, back.len(), "the gate roll-up counts every ui_atom row");
assert_eq!(summary.covered, covered, "covered atoms counted by the gate summary");
assert!(summary.green, "no missing atom => the atom slice of the gate is green");
}
#[test]
fn atom_coverage_tabs_match_pane_registry() {
let from_registry: Vec<String> = tab_pane_ids().iter().map(|s| s.to_string()).collect();
let from_const: Vec<String> = TABS.iter().map(|s| s.to_string()).collect();
assert_eq!(
from_const, from_registry,
"atom_coverage TABS drifted from surfaces::tab_pane_ids() — the registry is \
the canonical surface enumerator (S3c); update TABS to match"
);
}
#[test]
fn button_registry_stations_are_rendered_atoms() {
let srv = spawn_empty_server();
let mut harness: Harness<'_, UrdrThreadsApp> = Harness::builder()
.with_size(egui::Vec2::new(1400.0, 1000.0))
.build_state(|ctx, app: &mut UrdrThreadsApp| app.draw_ui(ctx), remote_empty_app(&srv));
let (_atoms, by_tab) = walk_atoms(&mut harness);
let tab_tokens = |walk_tab: &str| -> std::collections::BTreeSet<String> {
by_tab
.get(walk_tab)
.into_iter()
.flatten()
.flat_map(|label| {
label
.split(|c: char| !c.is_alphanumeric())
.filter(|t| !t.is_empty())
.map(|t| t.to_lowercase())
})
.collect()
};
let mut drift: Vec<String> = Vec::new();
for btn in nornir::arch::metro::button_registry() {
let walk_tab = registry_tab_to_walk_tab(btn.tab);
let tokens = tab_tokens(&walk_tab);
let id_tokens: Vec<String> = btn
.id
.split('_')
.filter(|t| t.len() >= 3)
.map(|t| t.to_lowercase())
.collect();
let found = id_tokens.iter().any(|t| tokens.contains(t));
if !found {
drift.push(format!(
"button `{}` (tab `{}` -> walk `{}`, rpc `{}`) has NO matching rendered atom \
(id tokens {:?} not in tab atom tokens)",
btn.id, btn.tab, walk_tab, btn.rpc, id_tokens
));
}
}
assert!(
drift.is_empty(),
"button_registry drift — these registry stations don't correspond to any \
rendered atom (the metro map would draw a phantom station):\n{}",
drift.join("\n"),
);
}