tests/viz_live_data.rs
//! Comprehensive **live-DATA** functional test for the `nornir viz`
//! (nornir-ui) tab matrix — THE_BIG_PLAN LAW #1 (feed real input, assert
//! real *rendered* output, never merely "didn't panic"), LAW #6 (the component
//! emits the actual data it drew), DoD #3 (verified against real data).
//!
//! ## Why this file exists
//!
//! The pre-existing per-tab tests (`viz_test_pane.rs`, `viz_leaderboard_pane.rs`,
//! `viz_live.rs`, `viz_release.rs`, `viz_snapshots.rs`, `funnel_viz.rs`) each
//! prove ONE tab. The `viz_matrix.rs` "render every tab headless" test only
//! asserts the state has the right *shape* (keys present) — it stays GREEN even
//! when a tab renders **empty** (no rows / no nodes / no lanes). A live audit
//! against the running server found exactly that hole:
//!
//! * `Timeline releases:22 / lanes:0` — releases counted but **no lane mapped**,
//! * `Test` / `DepGraph` / `Release` / `Knowledge` / `Funnel` / `LiveRun` /
//! `Leaderboard` all rendered **empty** — yet every test was green.
//!
//! ## What this test guards
//!
//! For EVERY data-bearing tab it **seeds real data → drives the tab → asserts the
//! rendered `state_json` block is NON-EMPTY** — the actual rows / nodes / cells /
//! lanes, not just that the key exists. A future tab that renders empty (an RPC
//! that returns data the tab fails to MAP, a scoping bug, or genuinely no data)
//! turns THIS test — and therefore the `nornir test` matrix — RED.
//!
//! Seeding route per tab (whichever the codebase already supports):
//! * **Real on-disk Iceberg warehouse** (preferred — proves the warehouse read
//! path): `release_events` → Live Run + Release + Warehouse tables;
//! `test_results` → the warehouse browser; `funnel_events` (via the demo
//! seeder) → Funnel; multiple tables → the Warehouse browser table list.
//! * **`*_for_test` injectors** (the route the codebase exposes for the
//! timeline-derived tabs whose data is built by the release pipeline, not
//! hand-appendable): `inject_timeline_for_test` → Timeline / DepGraph /
//! TimeTravel; `inject_test_results_for_test` → Test grid;
//! `inject_bakeoff_for_test` → Leaderboard; `inject_release_events_for_test`
//! → Release; `inject_live_release_events_for_test` → Live Run.
//!
//! The **lanes-follow-releases** guard (the `22 releases / 0 lanes` bug): the
//! timeline `state_json` exposes only the `releases` scalar (sum of lane nodes),
//! not the lane array — but the 🔗 Dep Graph lays its nodes out from
//! `timeline.lanes` (one node per lane repo). So `dep_graph.node_count > 0`
//! **proves the lanes exist and mapped**: if releases were counted but lanes were
//! empty, the dep-graph would have ZERO nodes (and be `Null`). We assert both
//! `releases > 0` AND `dep_graph.node_count == lane repo count`, which is the
//! precise inverse of the `22 / 0` symptom.
#![cfg(feature = "viz")]
use std::collections::BTreeMap;
use std::path::PathBuf;
use chrono::{TimeZone, Utc};
use nornir::viz::UrdrThreadsApp;
use nornir::viz::model::{Lane, LaneNode, Timeline};
use nornir::warehouse::agent_model_runs::{AgentModelRunRow, append_agent_model_runs};
use nornir::warehouse::dep_graph::{CrossRepoEdge, DepGraphSnapshot};
use nornir::warehouse::iceberg::IcebergWarehouse;
use nornir::warehouse::release_events::{ReleaseEventRow, append_release_events, phase, status};
use nornir::warehouse::test_results::{TestResultRow, append_test_results, status as test_status};
use serde_json::Value;
use uuid::Uuid;
const REPOS: [&str; 3] = ["holger", "knut", "skade"];
// ─── fixtures ────────────────────────────────────────────────────────────────
/// A concrete [`Timeline`] with per-repo swim-lanes (real release nodes), a
/// dep-graph snapshot with cross-repo edges, so Timeline / DepGraph / TimeTravel
/// all have real data to lay out. Three lanes × {3,2,2} nodes = 7 releases.
fn fixture_timeline() -> Timeline {
let ts = |day: u32| Utc.with_ymd_and_hms(2026, 6, day, 12, 0, 0).unwrap();
let rid = |repo: &str, n: u8| {
let h = repo
.bytes()
.fold(0u128, |a, b| a.wrapping_mul(131).wrapping_add(b as u128));
let base = 0x4e6f_726e_6972_0000_0000_0000_0000_0000u128;
Uuid::from_u128(base | (h << 8) | n as u128)
};
let lane_specs: [(&str, &[(u8, &str, &str)]); 3] = [
(
"holger",
&[
(1, "succeeded", "0.3.0"),
(2, "succeeded", "0.3.1"),
(3, "failed_test", "0.3.2"),
],
),
(
"knut",
&[(1, "succeeded", "0.5.0"), (2, "succeeded", "0.5.1")],
),
(
"skade",
&[(1, "succeeded", "0.1.3"), (2, "succeeded", "0.1.4")],
),
];
let mut lanes = Vec::new();
let mut release_order = Vec::new();
let mut release_snapshot = BTreeMap::new();
let mut day = 1u32;
let snapshot_id = Uuid::from_u128(0x4e6f_726e_6972_5f73_6e61_7000_0000_0001u128);
for (repo, nodes) in lane_specs {
let mut lane_nodes = Vec::new();
for (n, st, ver) in nodes {
let id = rid(repo, *n);
release_order.push(id);
release_snapshot.insert(id, snapshot_id);
lane_nodes.push(LaneNode {
release_id: id,
timestamp: ts(day),
sha: format!("{repo}{n:02}deadbeef"),
branch: "main".to_string(),
dirty: false,
gate_status: st.to_string(),
tests_passed: 12 + *n as u32,
tests_failed: if *st == "failed_test" { 1 } else { 0 },
published_versions: vec![(repo.to_string(), ver.to_string())],
});
day += 1;
}
lanes.push(Lane {
repo: repo.to_string(),
nodes: lane_nodes,
});
}
// skade → knut → holger cross-repo edges.
let edges = vec![
CrossRepoEdge {
from: "knut".into(),
to: "skade".into(),
via: ["skade-katalog".into()].into_iter().collect(),
kind: Default::default(),
},
CrossRepoEdge {
from: "holger".into(),
to: "knut".into(),
via: ["knut-core".into()].into_iter().collect(),
kind: Default::default(),
},
CrossRepoEdge {
from: "holger".into(),
to: "skade".into(),
via: ["skade".into()].into_iter().collect(),
kind: Default::default(),
},
];
let snapshot = DepGraphSnapshot {
snapshot_id,
workspace_name: "holger".to_string(),
timestamp: ts(3),
edges,
};
let mut snapshots = BTreeMap::new();
snapshots.insert(snapshot_id, snapshot.clone());
Timeline {
workspace_name: "holger".to_string(),
lanes,
release_order,
release_snapshot,
snapshots,
latest_snapshot: Some(snapshot),
bench_history: BTreeMap::new(),
}
}
/// `test_results` rows: 3 repos × multiple aspects × 1 run each — a populated
/// grid (repos>0, aspects>0, rows>0, with at least one red cell).
fn fixture_test_rows() -> Vec<TestResultRow> {
let mk = |repo: &str, aspect: &str, name: &str, st: &str, metric: f64| TestResultRow {
run_id: format!("{repo}-r1"),
repo: repo.into(),
suite: repo.into(),
test_name: name.into(),
status: st.into(),
duration_ms: 4.0,
ts_micros: 300,
message: String::new(),
aspect: aspect.into(),
metric,
};
vec![
mk("holger", "build", "build", test_status::PASS, 0.0),
mk("holger", "unit", "a", test_status::PASS, 0.0),
mk("holger", "coverage", "cov", test_status::PASS, 88.0),
mk("knut", "build", "build", test_status::PASS, 0.0),
mk("knut", "unit", "b", test_status::FAIL, 0.0),
mk("knut", "coverage", "cov", test_status::PASS, 70.0),
mk("skade", "build", "build", test_status::PASS, 0.0),
mk("skade", "unit", "c", test_status::PASS, 0.0),
mk("skade", "coverage", "cov", test_status::PASS, 92.0),
]
}
/// A deterministic one-run release DAG (run envelope + a test + a gate), so the
/// Release op-log + Live Run pane both have real rows to render.
fn fixture_release_rows(run: &str) -> Vec<ReleaseEventRow> {
let mk = |seq: i64, comp: &str, op: &str, ph: &str, st: &str, ms: Option<i64>, detail: &str| {
ReleaseEventRow {
run_id: run.into(),
seq,
ts_micros: 1_000_000 + seq * 1_000,
component: comp.into(),
repo: comp.into(),
op: op.into(),
phase: ph.into(),
status: st.into(),
detail: detail.into(),
depends_on: None,
elapsed_ms: ms,
}
};
vec![
mk(0, run, "run", phase::START, status::RUNNING, None, "holger"),
mk(1, "znippy", "test", phase::START, status::RUNNING, None, ""),
mk(
2,
"znippy",
"test",
phase::END,
status::OK,
Some(1200),
"3 passed",
),
mk(3, "holger", "gate", phase::START, status::RUNNING, None, ""),
]
}
/// A bake-off `agent_model_runs` run across three models, so the Leaderboard has
/// a ranked board to render.
fn fixture_bakeoff_rows() -> Vec<AgentModelRunRow> {
let mk = |model: &str, tps: f64, score: f64, ok: bool| AgentModelRunRow {
run_id: "bake-1".into(),
ts_micros: 200,
agent: "local-llm".into(),
model: model.into(),
prompt_id: "capital-fr".into(),
prompt: "Capital of France?".into(),
output: "Paris".into(),
latency_ms: 100.0,
tokens_in: 12,
tokens_out: 18,
tokens_per_s: tps,
score,
ok,
error: None,
cost_usd: 0.0,
mcp_tool_calls: 0,
};
vec![
mk("beta", 300.0, 0.9, true),
mk("alpha", 100.0, 0.9, true),
mk("gamma", 500.0, 0.5, true),
]
}
fn build_app(warehouse_root: PathBuf) -> UrdrThreadsApp {
UrdrThreadsApp::with_repos(
warehouse_root,
"holger".to_string(),
PathBuf::new(),
REPOS.iter().map(|s| s.to_string()).collect(),
)
}
fn tmp_warehouse(tag: &str) -> PathBuf {
let root =
std::env::temp_dir().join(format!("nornir-viz-livedata-{tag}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&root);
std::fs::create_dir_all(&root).unwrap();
root
}
// ─── per-tab non-empty guards ────────────────────────────────────────────────
/// 🗓 Timeline + 🔗 Dep Graph + ⏳ Time Travel: inject a real multi-lane timeline
/// with a dep-graph snapshot, then assert the RENDERED data is non-empty.
///
/// This is the `22 releases / 0 lanes` guard: we assert both `releases > 0` AND
/// that the dep-graph — which lays its nodes out FROM `timeline.lanes` (one node
/// per lane repo) — has `node_count == lane repo count`. If releases were counted
/// but lanes were empty (the live bug), `dep_graph` would be `Null`/0 nodes.
#[test]
fn timeline_depgraph_timetravel_render_non_empty() {
let mut app = build_app(tmp_warehouse("tl"));
let tl = fixture_timeline();
let expected_releases: u64 = tl.lanes.iter().map(|l| l.nodes.len() as u64).sum();
let pinned = tl.release_order[0];
let matched = app.inject_timeline_for_test(
"Timeline",
tl,
REPOS.iter().map(|s| s.to_string()).collect(),
);
assert!(matched, "Timeline must be a known tab");
let state = app.state_json();
// Timeline: releases mapped into lanes (the bug is releases>0 but lanes==0).
assert_eq!(state["tab"].as_str(), Some("Timeline"));
let releases = state["timeline"]["releases"].as_u64().unwrap_or(0);
assert!(
state["timeline"]["error"].is_null(),
"timeline.error must be null, got {}",
state["timeline"]["error"]
);
assert!(
releases > 0,
"Timeline must render releases, got {releases}"
);
assert_eq!(
releases, expected_releases,
"every injected release surfaced"
);
// The empty placeholder must NOT be present when data is present.
let placeholders: Vec<&str> = state["placeholders"]
.as_array()
.unwrap()
.iter()
.filter_map(|p| p.as_str())
.collect();
assert!(
!placeholders.contains(&"no releases recorded yet"),
"timeline has releases but the empty placeholder is present: {placeholders:?}"
);
// Dep Graph: nodes laid out FROM the lanes — this is the lanes-follow-releases
// proof. node_count==0 would mean lanes were empty (the `0 lanes` symptom).
let dg = &state["dep_graph"];
assert!(
dg.is_object(),
"dep_graph must be a non-null object (lanes present), got {dg}"
);
let node_count = dg["node_count"].as_u64().unwrap_or(0);
assert!(
node_count > 0,
"dep_graph must have nodes (lanes mapped), got {node_count}"
);
assert_eq!(
node_count,
REPOS.len() as u64,
"one dep-graph node per lane repo (lanes followed releases)"
);
let nodes = dg["nodes"].as_array().expect("dep_graph nodes array");
assert!(!nodes.is_empty(), "dep_graph nodes array non-empty");
let edges = dg["edges"].as_array().expect("dep_graph edges array");
assert!(
!edges.is_empty(),
"dep_graph must render the cross-repo edges, got {}",
edges.len()
);
assert!(
dg["direct_edges"].as_u64().unwrap_or(0) > 0,
"at least one direct edge classified"
);
// Time Travel: pin the cursor, switch to graph view, assert the time-pinned
// graph is rendered with the lanes' nodes.
app.set_timetravel_for_test(0, true, true);
let tt = &app.state_json()["timetravel"];
assert_eq!(
tt["view"].as_str(),
Some("graph"),
"deps reel in graph view"
);
assert!(
tt["release_count"].as_u64().unwrap_or(0) > 0,
"time travel has releases"
);
assert_eq!(
tt["pinned_release"].as_str(),
Some(pinned.to_string().as_str()),
"cursor pinned"
);
let ttg = &tt["graph"];
assert!(ttg.is_object(), "time-pinned graph rendered, got {ttg}");
assert!(
ttg["node_count"].as_u64().unwrap_or(0) > 0,
"time-pinned graph has nodes"
);
}
/// 🧪 Test grid: inject a real `test_results` matrix, assert the rendered grid is
/// non-empty (repos>0, aspects>0, rows>0).
/// 🚀 Release op-log: inject real `release_events`, assert the rendered op rows
/// are non-empty.
#[test]
fn release_oplog_renders_non_empty() {
let mut app = build_app(tmp_warehouse("rel"));
app.inject_release_events_for_test(fixture_release_rows("run-rel"));
let state = app.state_json();
assert_eq!(state["tab"].as_str(), Some("Release"));
let rel = &state["release"];
assert!(
rel["error"].is_null(),
"release.error must be null, got {}",
rel["error"]
);
assert!(
rel["runs"].as_u64().unwrap_or(0) > 0,
"release must render >=1 run"
);
let rows = rel["rows"].as_array().expect("release.rows array");
assert!(
!rows.is_empty(),
"Release op-log must render rows, got {}",
rows.len()
);
let comp_status = rel["component_status"]
.as_object()
.expect("component_status map");
assert!(
!comp_status.is_empty(),
"per-component status must be rendered, got {comp_status:?}"
);
}
/// 📡 Live Run: inject real `release_events`, assert the hydrated run + rendered
/// event lines are non-empty.
#[test]
fn live_run_renders_non_empty() {
let mut app = build_app(tmp_warehouse("live-inject"));
app.inject_live_release_events_for_test(fixture_release_rows("run-live"));
let state = app.state_json();
assert_eq!(state["tab"].as_str(), Some("LiveRun"));
let live = &state["live"];
assert!(
live["error"].is_null(),
"live.error must be null, got {}",
live["error"]
);
assert_eq!(
live["hydrated_run_id"].as_str(),
Some("run-live"),
"a run hydrated"
);
assert_eq!(live["run_id"].as_str(), Some("run-live"));
assert!(
live["events"].as_u64().unwrap_or(0) > 0,
"live must render events, got {}",
live["events"]
);
let lines = live["lines"].as_array().expect("live.lines array");
assert!(
!lines.is_empty(),
"Live Run must render event lines, got {}",
lines.len()
);
}
/// 📡 Live Run + 🚀 Release + 🗂 Warehouse, all from a **real on-disk warehouse**
/// (the preferred route — proves the warehouse read path, not just injection).
/// Seeds `release_events` + `test_results` + `agent_model_runs` to a temp Iceberg
/// warehouse, points the app at it, and asserts:
/// * Live Run hydrates from the warehouse (non-empty run + lines),
/// * the Warehouse browser lists the seeded tables (tables>0).
#[test]
fn warehouse_backed_tabs_render_non_empty() {
let warehouse_root = tmp_warehouse("wh");
let wh = IcebergWarehouse::open(&warehouse_root).expect("open warehouse");
wh.block_on(append_release_events(&wh, &fixture_release_rows("run-wh")))
.expect("append release_events");
// test_results is partitioned by `repo` — single-partition appends only, so
// seed one batch per repo (still a real on-disk table for the browser to list).
for repo in REPOS {
let rows: Vec<TestResultRow> = fixture_test_rows()
.into_iter()
.filter(|r| r.repo == repo)
.collect();
wh.block_on(append_test_results(&wh, &rows))
.expect("append test_results");
}
wh.block_on(append_agent_model_runs(&wh, &fixture_bakeoff_rows()))
.expect("append agent_model_runs");
drop(wh); // release the writer lock so the read-only hydrate opens cleanly.
let mut app = build_app(warehouse_root.clone());
// 📡 Live Run, hydrated from the REAL warehouse release_events table.
app.live_hydrate_from_warehouse_for_test();
let live = &app.state_json()["live"];
assert_eq!(
live["hydrated_from_warehouse"],
Value::Bool(true),
"Live Run hydrated from warehouse"
);
assert!(
live["error"].is_null(),
"warehouse hydrate must succeed, got {}",
live["error"]
);
assert_eq!(
live["hydrated_run_id"].as_str(),
Some("run-wh"),
"run read back from warehouse"
);
assert!(
live["events"].as_u64().unwrap_or(0) > 0,
"warehouse-sourced events non-empty"
);
assert!(
!live["lines"].as_array().unwrap().is_empty(),
"warehouse-sourced lines non-empty"
);
// 🗂 Warehouse browser: drive every tab headless (the Warehouse draw lists the
// tables), then assert the seeded tables surface (tables > 0).
app.render_all_tabs_headless();
let wh_state = app.state_json();
let tables = wh_state["warehouse_tables"]
.as_array()
.expect("warehouse_tables array");
assert!(
!tables.is_empty(),
"Warehouse browser must list the seeded tables, got {tables:?}"
);
let names: Vec<&str> = tables.iter().filter_map(|t| t.as_str()).collect();
assert!(
names.iter().any(|n| n.contains("release_events")),
"release_events table must be listed; got {names:?}"
);
let _ = std::fs::remove_dir_all(&warehouse_root);
}
/// 🗂 Funnel: seed real `funnel_events` via the demo seeder on a **real on-disk
/// warehouse** (the route the codebase exposes — the funnel DAG is materialised
/// by `run_demo`, not hand-appendable), then assert the rendered funnel plan is
/// non-empty (plan_count>0, nodes>0).
#[test]
fn funnel_renders_non_empty() {
// The demo's git-root resolution wants `<tmp>/ws/.nornir/warehouse` so the
// fake repos land under `<tmp>/git/…` (mirrors funnel_viz.rs).
let tmp = tempfile::tempdir().unwrap();
let ws = tmp.path().join("ws");
let warehouse = ws.join(".nornir").join("warehouse");
std::fs::create_dir_all(&warehouse).unwrap();
let mut app =
UrdrThreadsApp::with_repos(warehouse, "funnel-livedata".to_string(), ws.clone(), vec![]);
let size = 3;
app.funnel_tab_for_test()
.run_demo_for_test(size)
.expect("seed funnel demo");
let state = app.state_json();
let funnel = &state["funnel"];
assert!(
funnel["error"].is_null(),
"funnel.error must be null, got {}",
funnel["error"]
);
assert!(
funnel["plan_count"].as_u64().unwrap_or(0) > 0,
"Funnel must render a plan, got plan_count 0"
);
let plans = funnel["plans"].as_array().expect("funnel.plans array");
assert!(
!plans.is_empty(),
"Funnel must render plans, got {}",
plans.len()
);
let plan = &plans[0];
assert!(
plan["nodes"].as_u64().unwrap_or(0) > 0,
"funnel plan must render nodes, got {}",
plan["nodes"]
);
assert_eq!(
plan["nodes"].as_u64(),
Some(size as u64),
"one node per seeded repo"
);
assert!(
plan["edges"].as_u64().unwrap_or(0) > 0,
"funnel DAG must render dep edges"
);
assert!(
plan["ready_count"].as_u64().unwrap_or(0) > 0,
"at least one node is Ready"
);
}
/// 📚 Knowledge: the map is the code room's. A workspace nothing was consumed
/// into renders the reader's `not_consumed` — never `crates: 0`, which would
/// read as an empty workspace.
#[test]
fn knowledge_renders_not_consumed_never_an_empty_map() {
let warehouse_root = tmp_warehouse("kn-wh");
let mut app = UrdrThreadsApp::with_repos(
warehouse_root,
"holger".to_string(),
std::env::temp_dir(),
REPOS.iter().map(|s| s.to_string()).collect(),
);
app.knowledge_load_for_test();
let kn = &app.state_json()["knowledge"];
assert_eq!(kn["state"], "unavailable", "{kn}");
assert!(kn["note"].as_str().is_some_and(|n| n.contains("[not_consumed]")), "{kn}");
assert!(kn.get("crates").is_none(), "no empty crate count: {kn}");
}