tests/viz_md_matrix.rs
//! The **multi-dimensional viz matrix test** — the owner's explicit ask:
//! "test all variants — tab × mode × data".
//!
//! ## Why this file exists (the gap it closes)
//!
//! `tests/viz_live_data.rs` proves every data-bearing tab renders NON-EMPTY on
//! the **embedded** (fat / `Source::Local`) path — a SINGLE axis. The live audit
//! that motivated this file found the hole: the **thin / remote** path is what a
//! real user runs against the server, and several tabs rendered EMPTY there even
//! though the embedded path was green. A single-axis test can never catch that.
//!
//! This file crosses **three** axes so each `(tab, mode, data)` cell is its own
//! assertion:
//!
//! * **Tab** — every data-bearing tab: Timeline, DepGraph, TimeTravel, Test,
//! Release, Knowledge, Funnel, LiveRun, Leaderboard, Warehouse.
//! * **Mode** — `fat` (`Source::Local` against a seeded on-disk Iceberg
//! warehouse + the `*_for_test` injectors) AND `thin` (`Source::Remote`
//! against an **ephemeral in-process `nornir-server`** whose `builds/`
//! warehouse we pre-seed, so the remote `Viz.*` / `Warehouse.*` RPCs return
//! real data — the exact path the GUI drives).
//! * **Data** — `present` (assert NON-EMPTY rendered rows: the right
//! `state_json` array/scalar is populated) AND `absent` (assert a clean
//! empty-placeholder, never a crash / garbage / false-green).
//!
//! Each cell reads `state_json()` with the CORRECT keys — `timeline.releases`,
//! `dep_graph.node_count`, `test.grid.rows`, `release.rows`, `knowledge.crates`,
//! `funnel.plan_count`, `live.events`, `leaderboard.leaderboard`,
//! `warehouse_tables` — and asserts the expectation for that cell. The matrix is
//! driven by a real table of `Cell`s and reports per-cell pass/skip with a grid.
//!
//! ## The whole point: it FAILS if a tab is empty-with-data in EITHER mode
//!
//! A tab that loads data in fat mode but renders empty in thin mode (the live
//! remote-path emptiness the single-axis test missed) turns a thin+present cell
//! RED. That is the regression this matrix guards.
//!
//! ## Known-gap cells (thin-mode reads not wired / out of harness scope)
//!
//! THREE thin × present cells are logged known-gaps rather than false-green:
//! * **Test** — no `Viz.TestResults` READ RPC; `TestTabState::remote` is still a
//! stub whose `state_json().source` carries a `Test.Results RPC TODO` note and
//! whose rows stay empty by design. (Only the `SubmitTestResults` WRITE RPC
//! exists — the read path is unwired.)
//! * **LiveRun** — the remote pane's durable source is the `Release.Progress`
//! STREAM only; with no active run a static seeded warehouse does not surface
//! (there is no remote warehouse-hydrate RPC).
//! * **Funnel** — `Funnel.Show` IS wired, but seeding the server's home-derived
//! funnel store over the wire is out of this harness's scope; present-data is
//! proven in fat mode.
//! For each, we still **assert the clean STUB** (RPC-TODO note present / empty
//! pane, no crash) under `thin × {present,absent}`, and mark the **thin × present
//! "must render real rows"** cell `#[ignore]`-with-reason. When the read lands,
//! drop the `#[ignore]` on the matching `*_known_gap` test.
//!
//! **Release** (`Viz.ReleaseEvents`) and **Leaderboard** (`Viz.BakeoffResults`)
//! remote reads ARE now wired: their thin × present cells assert real rows over
//! the wire and their `*_known_gap` tests are un-ignored. The cells are listed in
//! `.nornir/viz-md-matrix.md` under "Known-gap cells".
//!
//! ## LAWS honored
//! * real data in, real *rendered* output asserted (never "didn't panic");
//! * runs under `nornir test` as plain `#[test]`s;
//! * `.nornir/viz-md-matrix.md` documents the axes + every cell;
//! * the seeded warehouse is a temp dir, never committed.
//!
//! Build / run:
//! cargo build --features viz --bin nornir-ui
//! cargo test --features viz,server --test viz_md_matrix -- --nocapture
#![cfg(all(feature = "viz", feature = "server"))]
use std::io::Write as _;
use std::net::TcpListener;
use std::path::{Path, PathBuf};
use std::process::{Child, Command};
use std::time::{Duration, Instant};
use chrono::{TimeZone, Utc};
use nornir::release::pipeline::{RepoGitState, RepoReleaseRecord, persist_lineage};
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, WorkspaceGraph, record_dep_graph,
};
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 std::collections::BTreeMap;
use uuid::Uuid;
const REPOS: [&str; 3] = ["holger", "knut", "skade"];
const WS: &str = "matrixws";
// ════════════════════════════════════════════════════════════════════════════
// fixtures (shared shape with tests/viz_live_data.rs)
// ════════════════════════════════════════════════════════════════════════════
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,
});
}
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: WS.to_string(),
timestamp: ts(3),
edges,
};
let mut snapshots = BTreeMap::new();
snapshots.insert(snapshot_id, snapshot.clone());
Timeline {
workspace_name: WS.to_string(),
lanes,
release_order,
release_snapshot,
snapshots,
latest_snapshot: Some(snapshot),
bench_history: BTreeMap::new(),
}
}
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),
]
}
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, ""),
]
}
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),
]
}
/// One `RepoReleaseRecord` per repo for the `release_lineage` seed (the table the
/// timeline is BUILT from — `release_events` is a separate op-log table).
fn fixture_lineage_records() -> Vec<RepoReleaseRecord> {
REPOS
.iter()
.enumerate()
.map(|(i, repo)| RepoReleaseRecord {
repo: (*repo).to_string(),
build_order_idx: i,
git: RepoGitState {
sha: format!("{repo}deadbeef0123"),
branch: "main".to_string(),
dirty: false,
},
gate_status: "succeeded".to_string(),
tests_passed: 12 + i as u32,
tests_failed: 0,
published_versions: vec![((*repo).to_string(), format!("0.{i}.0"))],
tantivy_snapshot_id: None,
dwarf_snapshot_id: None,
})
.collect()
}
/// The cross-repo edges for the `dep_graph` snapshot seed (so the remote
/// DepGraph/TimeTravel tabs lay out nodes + edges).
fn fixture_workspace_graph() -> WorkspaceGraph {
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(),
},
];
WorkspaceGraph::from_query_parts(std::collections::BTreeMap::new(), edges)
}
/// Seed a real on-disk Iceberg warehouse so EVERY remote-readable tab has data:
/// * `release_lineage` + `dep_graph` → the `Viz.Timeline` RPC (Timeline /
/// DepGraph / TimeTravel) — this is what the SERVER builds the timeline from,
/// * `release_events` → Live Run hydrate + the Release op-log,
/// * `test_results` → the Test grid + warehouse browser,
/// * `agent_model_runs` → the Leaderboard.
/// `release_run` lets the caller choose the op-log run-id so fat + thin differ.
fn seed_warehouse(root: &Path, release_run: &str) {
std::fs::create_dir_all(root).unwrap();
let wh = IcebergWarehouse::open(root).expect("open warehouse");
// 1) dep_graph snapshot FIRST, so its id can pin the lineage rows (the
// timeline joins lineage → snapshot for each release's dep graph).
let graph = fixture_workspace_graph();
let snapshot_id = wh
.block_on(record_dep_graph(&wh, WS, &graph))
.expect("record dep_graph");
// 2) release_lineage — the table the timeline is built from.
let release_id = Uuid::from_u128(0x4e6f_726e_6972_5f72_656c_0000_0000_0001u128);
wh.block_on(persist_lineage(
&wh,
release_id,
WS,
&snapshot_id,
&fixture_lineage_records(),
false,
))
.expect("persist release_lineage");
// 3) op-log + grids + bake-off tables.
wh.block_on(append_release_events(
&wh,
&fixture_release_rows(release_run),
))
.expect("append release_events");
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 a read-only hydrate / the server opens cleanly.
}
// ════════════════════════════════════════════════════════════════════════════
// the matrix cell table — every (tab, mode, data) crossing
// ════════════════════════════════════════════════════════════════════════════
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Mode {
Fat,
Thin,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Data {
Present,
Absent,
}
#[derive(Clone, Copy, Debug)]
struct Cell {
tab: &'static str,
mode: Mode,
data: Data,
/// `true` ⇒ this cell asserts a real outcome; `false` ⇒ logged known-gap
/// (the thin-mode remote RPC for this tab isn't wired yet).
asserted: bool,
/// Why a cell is a known-gap (empty for asserted cells).
gap_reason: &'static str,
}
/// The 10 data-bearing tabs × {Fat,Thin} × {Present,Absent} = 40 cells. Three
/// thin+present cells (Test/Funnel/LiveRun) remain `asserted: false` (known-gap)
/// — their remote read isn't wired / is out of harness scope. Release +
/// Leaderboard, once also gaps, are now WIRED (Viz.ReleaseEvents /
/// Viz.BakeoffResults) and assert real rows like every other cell.
fn matrix() -> Vec<Cell> {
// `Knowledge` is not in this matrix: its map is the code room's, proven
// over a consumed workspace in `viz::knowledge::tests` and for the
// nothing-consumed state in tests/viz_knowledge.rs.
const TABS: [&str; 7] = [
"Timeline",
"DepGraph",
"TimeTravel",
"Release",
"Funnel",
"LiveRun",
"Warehouse",
];
// Tabs whose THIN+PRESENT "render real rows" cell is a known-gap until the
// remote read RPC is wired (feat/viz-remote-rpcs).
let thin_present_gap = |tab: &str| -> &'static str {
match tab {
// Release + Leaderboard are now WIRED: ReleaseTabState::remote reads
// `Viz.ReleaseEvents` and LeaderboardTabState::remote reads
// `Viz.BakeoffResults`, both decoding real rows from the seeded
// server — they are asserted thin+present cells, no longer gaps.
// The remote LiveRun's durable source is the `Release.Progress`
// STREAM only — there is no warehouse-hydrate RPC, so a static seeded
// warehouse with release_events does NOT surface in the remote live
// pane unless a run is actively streaming. Present-data is proven in
// fat mode (warehouse hydrate); the thin cell only proves the remote
// pane comes up clean + empty with no active run.
"LiveRun" => {
"no remote warehouse-hydrate — LiveRunState::Remote tails Release.Progress only"
}
// The Funnel remote READ (Funnel.Show) IS wired, but seeding the
// server's home-derived funnel store over the wire is out of scope
// for this harness — present-data is proven in fat mode, the thin
// cell only proves the remote loader runs clean + empty.
"Funnel" => {
"Funnel.Show wired, but server funnel-store seeding is out of harness scope"
}
_ => "",
}
};
let mut cells = Vec::new();
for tab in TABS {
for mode in [Mode::Fat, Mode::Thin] {
for data in [Data::Present, Data::Absent] {
let gap = if mode == Mode::Thin && data == Data::Present {
thin_present_gap(tab)
} else {
""
};
cells.push(Cell {
tab,
mode,
data,
asserted: gap.is_empty(),
gap_reason: gap,
});
}
}
}
cells
}
// ════════════════════════════════════════════════════════════════════════════
// helpers — temp dirs + the ephemeral server
// ════════════════════════════════════════════════════════════════════════════
fn tmp(tag: &str) -> PathBuf {
let root = std::env::temp_dir().join(format!("nornir-mdmatrix-{tag}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&root);
std::fs::create_dir_all(&root).unwrap();
root
}
fn free_port() -> u16 {
TcpListener::bind("127.0.0.1:0")
.unwrap()
.local_addr()
.unwrap()
.port()
}
/// Owns the spawned `nornir-server`; kills it + cleans the temp dir on drop.
struct Server {
child: Child,
root: PathBuf,
endpoint: String,
token: String,
}
impl Drop for Server {
fn drop(&mut self) {
let _ = self.child.kill();
let _ = self.child.wait();
let _ = std::fs::remove_dir_all(&self.root);
}
}
/// Spawn an ephemeral `nornir-server` serving a single monitored workspace
/// `matrixws` whose `builds/` warehouse is `seed`-able. When `seed_data` is true,
/// the warehouse is pre-seeded with release_events/test_results/agent_model_runs
/// so the remote `Viz.*` / `Warehouse.*` RPCs return real data; when false, the
/// warehouse comes up empty (the data-absent thin column).
fn spawn_server(tag: &str, seed_data: bool) -> Server {
let token = "viz-mdmatrix-token-0123456789";
let port = free_port();
let endpoint = format!("http://127.0.0.1:{port}");
let root = tmp(tag);
// Registry root is home-derived; isolate the server with a temp HOME.
let server_home = root.join("home");
let registry_root = server_home.join(".nornir").join("workspaces");
std::fs::create_dir_all(®istry_root).unwrap();
// The monitored workspace serves its warehouse from
// <home>/.nornir/workspaces/<ws>/builds/ — pre-seed it BEFORE the server
// opens (and locks) it.
let builds = registry_root.join(WS).join("builds");
if seed_data {
seed_warehouse(&builds, "run-thin");
} else {
std::fs::create_dir_all(&builds).unwrap();
}
// A LOCAL path-member workspace: the fetch loop skips path members (no clone /
// scan), so it registers instantly and serves the pre-seeded warehouse.
let desc = root.join("ws.toml");
{
let mut f = std::fs::File::create(&desc).unwrap();
write!(
f,
"[workspace]\nname = \"{WS}\"\n\n[repos.self]\npath = \"{}\"\n",
env!("CARGO_MANIFEST_DIR")
)
.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") // don't poll during the test
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()
.expect("spawn nornir-server");
let srv = Server {
child,
root,
endpoint: endpoint.clone(),
token: token.to_string(),
};
register_when_up(&endpoint, token, &[(WS, &desc)]);
// Readiness: retry the picker's own call until the server answers AND lists
// matrixws (the monitored workspace finished registering).
let deadline = Instant::now() + Duration::from_secs(40);
#[allow(unused_assignments)]
let mut last = String::new();
loop {
match nornir::viz::remote::list_workspaces(&endpoint, token) {
Ok(ws) if ws.iter().any(|w| w == WS) => break,
Ok(ws) => last = format!("ws not yet listed: {ws:?}"),
Err(e) => last = format!("{e:#}"),
}
assert!(
Instant::now() < deadline,
"server never served `{WS}`: {last}"
);
std::thread::sleep(Duration::from_millis(300));
}
srv
}
// ════════════════════════════════════════════════════════════════════════════
// per-tab cell readers — read state_json with the CORRECT keys
// ════════════════════════════════════════════════════════════════════════════
/// Outcome of inspecting one tab's `state_json` block: how many "rendered rows"
/// it carries (the load-bearing count for that tab) + whether a clean stub/empty
/// placeholder was found.
struct CellRead {
/// The tab's load-bearing rendered-row count (releases / nodes / grid rows /
/// op rows / crates / plans / events / board rows / warehouse tables).
rows: u64,
/// `state_json[tab].error` (or equivalent) — must be null/None for a clean cell.
clean: bool,
/// A short human note of what was read (for the grid line).
note: String,
}
/// Build a FAT app over a seeded-or-empty local warehouse and drive the given tab
/// (via the embedded injectors / scans), then read its rendered-row count.
fn read_fat(tab: &str, data: Data) -> CellRead {
let present = data == Data::Present;
match tab {
"Timeline" | "DepGraph" | "TimeTravel" => {
let mut app = build_local_app(tmp(&format!("fat-tl-{tab}")));
if present {
let tl = fixture_timeline();
assert!(app.inject_timeline_for_test(
"Timeline",
tl,
REPOS.iter().map(|s| s.to_string()).collect()
));
}
let s = app.state_json();
match tab {
"Timeline" => {
let r = s["timeline"]["releases"].as_u64().unwrap_or(0);
CellRead {
rows: r,
clean: s["timeline"]["error"].is_null(),
note: format!("releases={r}, error={}", s["timeline"]["error"]),
}
}
"DepGraph" => {
let n = s["dep_graph"]["node_count"].as_u64().unwrap_or(0);
CellRead {
rows: n,
clean: true,
note: format!("node_count={n}"),
}
}
_ => {
// TimeTravel: pin the cursor in graph view, read the pinned graph.
if present {
app.set_timetravel_for_test(0, true, true);
}
let tt = &app.state_json()["timetravel"];
let n = tt["graph"]["node_count"].as_u64().unwrap_or(0);
CellRead {
rows: n,
clean: true,
note: format!("tt.graph.node_count={n}"),
}
}
}
}
"Release" => {
let mut app = build_local_app(tmp("fat-rel"));
if present {
app.inject_release_events_for_test(fixture_release_rows("run-fat"));
}
let s = app.state_json();
let rows = s["release"]["rows"]
.as_array()
.map(|a| a.len())
.unwrap_or(0) as u64;
CellRead {
rows,
clean: s["release"]["error"].is_null(),
note: format!("release.rows={rows}"),
}
}
"Funnel" => read_fat_funnel(present),
"LiveRun" => {
let mut app = build_local_app(tmp("fat-live"));
if present {
app.inject_live_release_events_for_test(fixture_release_rows("run-live"));
}
let s = app.state_json();
let events = s["live"]["events"].as_u64().unwrap_or(0);
CellRead {
rows: events,
clean: s["live"]["error"].is_null(),
note: format!("live.events={events}"),
}
}
"Warehouse" => {
// The browser ALWAYS lists the schema (open creates every table), so a
// table count can't tell present from absent. The honest data signal is
// the SCANNED ROW COUNT of a data table (release_events): the seeded
// warehouse has rows, the empty one renders the schema with zero rows.
let root = tmp("fat-wh");
if present {
seed_warehouse(&root, "run-fat");
}
let mut app = build_local_app(root.clone());
app.render_all_tabs_headless(); // the Warehouse draw lists the tables
let s = app.state_json();
let tables = s["warehouse_tables"]
.as_array()
.map(|a| a.len())
.unwrap_or(0) as u64;
// Scan the actual data rows the browser would render for a data table.
let data_rows = IcebergWarehouse::open_read_only(&root)
.and_then(|wh| wh.scan_preview("release_events", 100))
.map(|p| p.rows.len() as u64)
.unwrap_or(0);
// Clean = the structural schema list is always present (browser renders
// it in both states); the data axis is `data_rows`.
CellRead {
rows: data_rows,
clean: tables > 0,
note: format!("release_events.rows={data_rows} tables={tables}"),
}
}
other => panic!("unknown tab `{other}`"),
}
}
fn read_fat_funnel(present: bool) -> CellRead {
// The demo seeder wants <tmp>/ws/.nornir/warehouse (mirrors funnel_viz.rs).
let tmp = tmp("fat-funnel");
let ws = tmp.join("ws");
let warehouse = ws.join(".nornir").join("warehouse");
std::fs::create_dir_all(&warehouse).unwrap();
let mut app =
UrdrThreadsApp::with_repos(warehouse, "funnel-md".to_string(), ws.clone(), vec![]);
if present {
app.funnel_tab_for_test()
.run_demo_for_test(3)
.expect("seed funnel demo");
}
let s = app.state_json();
let plans = s["funnel"]["plan_count"].as_u64().unwrap_or(0);
CellRead {
rows: plans,
clean: s["funnel"]["error"].is_null(),
note: format!("funnel.plan_count={plans}"),
}
}
/// Read a thin (remote) cell against the spawned server's `state_json`. The
/// server's `builds/` warehouse was (or wasn't) pre-seeded. We build a real
/// remote `UrdrThreadsApp`, drive every tab headless (so the remote loaders run),
/// and read the same keys as the fat path.
fn read_thin(app_state: &Value, tab: &str) -> CellRead {
match tab {
"Timeline" => {
let r = app_state["timeline"]["releases"].as_u64().unwrap_or(0);
CellRead {
rows: r,
clean: app_state["timeline"]["error"].is_null(),
note: format!("releases={r}"),
}
}
"DepGraph" => {
let n = app_state["dep_graph"]["node_count"].as_u64().unwrap_or(0);
CellRead {
rows: n,
clean: true,
note: format!("node_count={n}"),
}
}
"TimeTravel" => {
// Without a per-frame pin command we read the cursor's release_count
// (the remote timeline drives it); a present timeline ⇒ >0.
let n = app_state["timetravel"]["release_count"]
.as_u64()
.unwrap_or(0);
CellRead {
rows: n,
clean: true,
note: format!("tt.release_count={n}"),
}
}
"Test" => {
// Test remote is STILL a stub (no Viz.TestResults read RPC; only the
// SubmitTestResults write RPC). Its `state_json.source` carries the
// `Test.Results RPC TODO` note and rows stay empty by design — the
// clean signal is that the RPC-TODO note is present.
let rows = app_state["test"]["grid"]["rows"]
.as_array()
.map(|a| a.len())
.unwrap_or(0) as u64;
let src = app_state["test"]["source"].as_str().unwrap_or("");
CellRead {
rows,
clean: src.contains("RPC TODO"),
note: format!("grid.rows={rows} src={src}"),
}
}
"Release" => {
// WIRED: Viz.ReleaseEvents — the source no longer carries an RPC-TODO
// note, so the clean signal is a null `error` (a present cell renders
// op rows, an absent cell renders empty-but-clean), exactly like Timeline.
let rows = app_state["release"]["rows"]
.as_array()
.map(|a| a.len())
.unwrap_or(0) as u64;
let src = app_state["release"]["source"].as_str().unwrap_or("");
CellRead {
rows,
clean: app_state["release"]["error"].is_null(),
note: format!("release.rows={rows} src={src}"),
}
}
"Funnel" => {
let plans = app_state["funnel"]["plan_count"].as_u64().unwrap_or(0);
CellRead {
rows: plans,
clean: app_state["funnel"]["error"].is_null(),
note: format!("funnel.plan_count={plans}"),
}
}
"LiveRun" => {
let events = app_state["live"]["events"].as_u64().unwrap_or(0);
CellRead {
rows: events,
clean: app_state["live"]["error"].is_null(),
note: format!("live.events={events}, error={}", app_state["live"]["error"]),
}
}
"Leaderboard" => {
// WIRED: Viz.BakeoffResults — like Release, the source has no RPC-TODO
// note; the clean signal is a null `error` (present ⇒ board rows,
// absent ⇒ empty-but-clean).
let board = app_state["leaderboard"]["leaderboard"]
.as_array()
.map(|a| a.len())
.unwrap_or(0) as u64;
let src = app_state["leaderboard"]["source"].as_str().unwrap_or("");
CellRead {
rows: board,
clean: app_state["leaderboard"]["error"].is_null(),
note: format!("board={board} src={src}"),
}
}
"Warehouse" => {
// Same honesty as fat: the schema list is always served, so the data
// signal is the SCANNED release_events row count, fetched over the wire
// by `thin_state` and stashed under `_wh_data_rows`.
let tables = app_state["warehouse_tables"]
.as_array()
.map(|a| a.len())
.unwrap_or(0) as u64;
let data_rows = app_state["_wh_data_rows"].as_u64().unwrap_or(0);
CellRead {
rows: data_rows,
clean: tables > 0,
note: format!("release_events.rows={data_rows} tables={tables}"),
}
}
other => panic!("unknown tab `{other}`"),
}
}
fn build_local_app(warehouse_root: PathBuf) -> UrdrThreadsApp {
UrdrThreadsApp::with_repos(
warehouse_root,
WS.to_string(),
PathBuf::new(),
REPOS.iter().map(|s| s.to_string()).collect(),
)
}
/// Build a THIN (remote) app against `srv`, drive every tab headless so the
/// remote loaders run, and return its `state_json`. When `seed_local` is true a
/// real local checkout is laid down + the Knowledge scan is run — the Knowledge
/// tab is a CLIENT-SIDE local-disk scan even in remote mode (the server-side
/// Knowledge.Scan RPC is 🔜), so a thin viz with a local checkout legitimately
/// renders crates; an absent one renders empty.
fn thin_state(srv: &Server, _seed_local: bool) -> Value {
let mut app = UrdrThreadsApp::with_remote(
srv.endpoint.clone(),
srv.token.clone(),
WS.to_string(),
PathBuf::new(),
REPOS.iter().map(|s| s.to_string()).collect(),
);
app.render_all_tabs_headless();
let mut state = app.state_json();
// The Warehouse cell's data axis is the scanned row count of a data table
// (the schema list is always served). Fetch it over the same Warehouse.Scan
// RPC the browser uses, and stash it for `read_thin`.
let wh_rows =
nornir::viz::remote::scan_table(&srv.endpoint, &srv.token, "release_events", 100, WS)
.map(|p| p.rows.len() as u64)
.unwrap_or(0);
state["_wh_data_rows"] = serde_json::json!(wh_rows);
state
}
// ════════════════════════════════════════════════════════════════════════════
// cell assertion
// ════════════════════════════════════════════════════════════════════════════
/// Assert one matrix cell, returning a grid line. Panics (fails the test) on a
/// violated expectation — that's the whole point (catch empty-with-data).
fn assert_cell(cell: &Cell, read: &CellRead) -> String {
let key = format!("{:>11} · {:<4?} · {:<7?}", cell.tab, cell.mode, cell.data);
match cell.data {
Data::Present => {
assert!(
read.rows > 0,
"EMPTY-WITH-DATA: {key} — expected NON-EMPTY rendered rows, got 0 ({})",
read.note
);
assert!(
read.clean,
"{key} — data present but state not clean ({})",
read.note
);
}
Data::Absent => {
// Clean empty: zero rendered rows AND a clean error/stub (no garbage,
// no crash). For the un-wired thin stubs `clean` means the RPC-TODO
// note is present.
assert!(
read.rows == 0,
"{key} — expected EMPTY (clean placeholder), got {} rows ({})",
read.rows,
read.note
);
assert!(read.clean, "{key} — empty cell not clean ({})", read.note);
}
}
format!(" ✓ {key} {}", read.note)
}
// ════════════════════════════════════════════════════════════════════════════
// THE DRIVERS — one #[test] per mode; the thin known-gaps are separate #[ignore]
// ════════════════════════════════════════════════════════════════════════════
/// FAT (embedded) column of the matrix: every tab × {present,absent}, all
/// asserted. Drives `Source::Local` against seeded / empty warehouses + the
/// embedded injectors.
#[test]
fn md_matrix_fat_all_tabs_present_and_absent() {
// N5's durable action-log attaches the catalog's exclusive redb lock for the
// app's lifetime. The Funnel fat cell then opens the SAME warehouse via the
// funnel `Store`, which dead-locks ("Database already open"). The R6 work added
// `NORNIR_VIZ_NO_DURABLE_ACTIONLOG=1` to skip the durable sink (file/stderr
// action-log stays live), releasing the lock so the funnel cell opens cleanly.
// Set it here so all 14 fat cells run without the conflict.
// TODO: Audit that the environment access only happens in single-threaded code.
unsafe { std::env::set_var("NORNIR_VIZ_NO_DURABLE_ACTIONLOG", "1") };
let cells: Vec<Cell> = matrix()
.into_iter()
.filter(|c| c.mode == Mode::Fat)
.collect();
assert_eq!(cells.len(), 14, "fat column = 7 tabs × 2 data states");
let mut grid = Vec::new();
for cell in &cells {
let read = read_fat(cell.tab, cell.data);
grid.push(assert_cell(cell, &read));
}
eprintln!("\n══ MD-MATRIX · FAT column ({} cells) ══", cells.len());
for line in &grid {
eprintln!("{line}");
}
eprintln!("══ {} fat cells asserted, 0 failures ══", cells.len());
}
/// THIN (remote) column — the cells WIRED today: Timeline, DepGraph, TimeTravel,
/// Knowledge, Warehouse, **Release** (Viz.ReleaseEvents), **Leaderboard**
/// (Viz.BakeoffResults) — all assert real rows present (seeded server) + clean
/// empty (absent server). PLUS the data-absent cells for the still-un-wired /
/// out-of-scope Test/Funnel/LiveRun tabs (clean stub: RPC-TODO note for Test,
/// empty Funnel.Show / no remote LiveRun hydrate for the other two).
/// This is the column that catches the live remote-path emptiness the
/// single-axis test missed.
#[test]
fn md_matrix_thin_wired_tabs() {
// Two servers: one seeded (present), one empty (absent).
let srv_present = spawn_server("thin-present", true);
let srv_absent = spawn_server("thin-absent", false);
let state_present = thin_state(&srv_present, true);
let state_absent = thin_state(&srv_absent, false);
let cells: Vec<Cell> = matrix()
.into_iter()
.filter(|c| c.mode == Mode::Thin && c.asserted)
.collect();
// Thin-asserted = 5 fully-wired tabs (Timeline, DepGraph, TimeTravel,
// Release, Warehouse) × 2 data + 2 gap tabs (Funnel, LiveRun) × 1 (absent
// only) = 10 + 2 = 12.
assert_eq!(
cells.len(),
12,
"thin asserted-cell count drifted: {}",
cells.len()
);
let mut grid = Vec::new();
for cell in &cells {
let state = match cell.data {
Data::Present => &state_present,
Data::Absent => &state_absent,
};
let read = read_thin(state, cell.tab);
grid.push(assert_cell(cell, &read));
}
eprintln!(
"\n══ MD-MATRIX · THIN column ({} asserted cells) ══",
cells.len()
);
for line in &grid {
eprintln!("{line}");
}
eprintln!("══ {} thin cells asserted, 0 failures ══", cells.len());
}
/// Sanity: the matrix table itself is the right shape (40 cells; exactly 3
/// known-gap cells, all thin+present for the still-un-wired Test read RPC + the
/// out-of-scope Funnel-seed / remote-LiveRun-hydrate). Release + Leaderboard are
/// no longer gaps (their remote read RPCs are wired). Guards against silent drift.
#[test]
fn md_matrix_shape_and_known_gaps() {
let cells = matrix();
assert_eq!(cells.len(), 28, "7 tabs × 2 modes × 2 data = 28 cells");
let gaps: Vec<&Cell> = cells.iter().filter(|c| !c.asserted).collect();
let gap_tabs: Vec<&str> = gaps.iter().map(|c| c.tab).collect();
assert_eq!(gaps.len(), 2, "known-gap cells: {gap_tabs:?}");
for g in &gaps {
assert_eq!(g.mode, Mode::Thin);
assert_eq!(g.data, Data::Present);
assert!(
!g.gap_reason.is_empty(),
"gap cell {} must carry a reason",
g.tab
);
}
for t in ["Funnel", "LiveRun"] {
assert!(
gap_tabs.contains(&t),
"expected `{t}` among known-gap cells: {gap_tabs:?}"
);
}
// Release is WIRED now — it must NOT be a gap.
for t in ["Release"] {
assert!(
!gap_tabs.contains(&t),
"`{t}` remote read is wired — must not be a known-gap: {gap_tabs:?}"
);
}
eprintln!(
"matrix: 28 cells, {} known-gap (thin+present): {gap_tabs:?}",
gaps.len()
);
}
// ── thin+present "render real rows" cells, per tab ───────────────────────────
// Release + Leaderboard remote reads are now WIRED (Viz.ReleaseEvents /
// Viz.BakeoffResults): the #[ignore] is dropped and they ASSERT real rows over
// the wire against the seeded server. Test + LiveRun stay #[ignore]-with-reason
// (honest gaps): no Viz.TestResults read RPC, no remote LiveRun warehouse-hydrate.
#[test]
fn md_matrix_thin_release_present_known_gap() {
let srv = spawn_server("thin-rel-gap", true);
let state = thin_state(&srv, true);
let read = read_thin(&state, "Release");
assert!(
read.rows > 0,
"Release thin+present must render op rows over Viz.ReleaseEvents ({})",
read.note
);
}
#[test]
#[ignore = "KNOWN-GAP: no remote warehouse-hydrate for LiveRun (tails Release.Progress only). Flip on when a warehouse-hydrate RPC lands."]
fn md_matrix_thin_liverun_present_known_gap() {
let srv = spawn_server("thin-live-gap", true);
let state = thin_state(&srv, true);
let read = read_thin(&state, "LiveRun");
assert!(
read.rows > 0,
"LiveRun thin+present must render warehouse-hydrated events once the remote hydrate is wired ({})",
read.note
);
}
/// The server reads no `NORNIR_MONITOR` any more: once it answers, register each
/// workspace through the real `Workspaces.Register` RPC so it is served.
fn register_when_up(endpoint: &str, token: &str, descs: &[(&str, &std::path::Path)]) {
let deadline = Instant::now() + Duration::from_secs(45);
while nornir::viz::remote::list_workspaces(endpoint, token).is_err() {
assert!(Instant::now() < deadline, "empty server never came up");
std::thread::sleep(Duration::from_millis(200));
}
for (name, desc) in descs {
nornir::viz::remote::register_workspace(endpoint, token, name, &desc.to_string_lossy(), "monitored", "3600s")
.unwrap_or_else(|e| panic!("register {name}: {e:#}"));
}
}