tests/viz_release.rs
//! Inject-and-assert proof for the 🚀 **Release** view (B3 op-log + B4 lit-up
//! dep-graph) and the ℹ **About** popup (C1) — THE_BIG_PLAN LAW #1: a test that
//! feeds real input and asserts real output, never merely "didn't panic".
//!
//! B3/B4: build the app, inject a concrete `release_events` DAG (two repos, a
//! `znippy` → green dep that `holger` waits on, holger still running), select the
//! Release tab, and assert `state_json()["release"]` exposes:
//! * the component-prefixed op-log lines (`[znippy] test end ok (1.2s)`),
//! * each row's component / op / status, and
//! * the per-component latest status the lit-up graph colours by
//! (`znippy=ok`, `holger=running`).
//!
//! C1: assert `state_json()["about"]` carries SOMETHING real — the client version
//! (`CARGO_PKG_VERSION`), the workspace, the warehouse path, and the mode.
//!
//! A wgpu snapshot (LAW #4) renders the Release view to a committed PNG, matching
//! the egui_kittest 0.33 pattern in `viz_snapshots.rs`.
#![cfg(feature = "viz")]
use std::path::{Path, PathBuf};
use nornir::viz::UrdrThreadsApp;
use nornir::warehouse::release_events::{ReleaseEventRow, phase, status};
/// A deterministic two-repo release DAG: `znippy` tests green, then `holger`
/// (which depends on znippy) starts its gate and is still running. Appended out
/// of `seq` order on purpose — the tab must re-group + re-order by `(run_id, seq)`.
fn fixture_rows() -> Vec<ReleaseEventRow> {
let run = "run-b3-fixture";
let mk = |seq: i64,
comp: &str,
op: &str,
ph: &str,
st: &str,
deps: Option<Vec<&str>>,
ms: Option<i64>,
detail: &str| {
ReleaseEventRow {
run_id: run.into(),
seq,
ts_micros: seq * 1_000,
component: comp.into(),
repo: comp.into(),
op: op.into(),
phase: ph.into(),
status: st.into(),
detail: detail.into(),
depends_on: deps.map(|v| v.into_iter().map(String::from).collect()),
elapsed_ms: ms,
}
};
vec![
// run envelope start (no component lane)
mk(0, run, "run", phase::START, status::RUNNING, None, None, ""),
// znippy: test start → end ok (1200ms) — a dep-graph root (Some([]))
mk(
2,
"znippy",
"test",
phase::END,
status::OK,
Some(vec![]),
Some(1200),
"3 passed",
),
mk(
1,
"znippy",
"test",
phase::START,
status::RUNNING,
Some(vec![]),
None,
"",
),
// holger: gate start, still running, waits on znippy
mk(
3,
"holger",
"gate",
phase::START,
status::RUNNING,
Some(vec!["znippy"]),
None,
"",
),
]
}
fn build_app() -> UrdrThreadsApp {
// Empty local warehouse — the rows are injected, not read from disk.
let warehouse_root =
std::env::temp_dir().join(format!("nornir-viz-release-{}", std::process::id()));
let _ = std::fs::create_dir_all(&warehouse_root);
let mut app = UrdrThreadsApp::with_repos(
warehouse_root,
"holger".to_string(),
PathBuf::new(),
vec!["znippy".to_string(), "holger".to_string()],
);
app.inject_release_events_for_test(fixture_rows());
app
}
#[test]
fn release_view_exposes_op_rows_and_component_status() {
let app = build_app();
let state = app.state_json();
// The Release tab is selected and present in the tab set.
assert_eq!(
state["tab"].as_str(),
Some("Release"),
"tab must be Release"
);
let tabs: Vec<&str> = state["all_tabs"]
.as_array()
.unwrap()
.iter()
.filter_map(|t| t.as_str())
.collect();
assert!(
tabs.contains(&"Release"),
"Release must be in all_tabs; got {tabs:?}"
);
let rel = &state["release"];
assert!(
rel["error"].is_null(),
"release.error must be null, got {}",
rel["error"]
);
assert_eq!(rel["runs"].as_u64(), Some(1), "one run injected");
assert_eq!(rel["selected_run"].as_str(), Some("run-b3-fixture"));
// B3 — the op-log rows: component-prefixed line + component/op/status fields.
let rows = rel["rows"].as_array().expect("release.rows array");
assert_eq!(
rows.len(),
4,
"all 4 injected events surfaced (re-grouped), got {}",
rows.len()
);
// Re-grouped by (run_id, seq): the run envelope first, then znippy start,
// znippy end ok, holger gate start.
let lines: Vec<&str> = rows.iter().filter_map(|r| r["line"].as_str()).collect();
assert!(
lines
.iter()
.any(|l| *l == "[znippy] test end ok (1.2s) — 3 passed"),
"component-prefixed op-log line missing; got {lines:?}"
);
assert!(
lines.iter().any(|l| *l == "[holger] gate start running"),
"holger gate-start line missing; got {lines:?}"
);
// Each row carries the structured fields the lit-up graph + assertions need.
let znippy_end = rows
.iter()
.find(|r| r["component"] == "znippy" && r["phase"] == "end")
.expect("znippy end row");
assert_eq!(znippy_end["op"], "test");
assert_eq!(znippy_end["status"], "ok");
// B4 — per-component LATEST status (what the graph colours by): znippy went
// running→ok so it's ok; holger is still running; the run envelope is NOT a
// node (excluded).
let cs = &rel["component_status"];
assert_eq!(
cs["znippy"].as_str(),
Some("ok"),
"znippy lights green (ok)"
);
assert_eq!(
cs["holger"].as_str(),
Some("running"),
"holger pulses amber (running)"
);
assert!(
cs.get("run-b3-fixture").is_none(),
"run envelope is not a graph node"
);
}
#[test]
fn about_popup_exposes_client_version_endpoint_workspace_warehouse() {
let app = build_app();
let state = app.state_json();
let about = &state["about"];
// Client version is the real crate version, never a placeholder.
assert_eq!(
about["client_version"].as_str(),
Some(env!("CARGO_PKG_VERSION")),
"About must show the real client CARGO_PKG_VERSION"
);
assert_eq!(
about["mode"].as_str(),
Some("local"),
"local-warehouse build"
);
assert_eq!(
about["workspace"].as_str(),
Some("holger"),
"selected workspace shown"
);
// The warehouse path is a real, non-empty path in local mode.
let wh = about["warehouse"].as_str().unwrap_or("");
assert!(
wh.contains("nornir-viz-release-"),
"warehouse path must be the injected root, got {wh:?}"
);
// Connect status is the real local-mode status string.
assert_eq!(about["connect_status"].as_str(), Some("local (no server)"));
}
// ─── wgpu snapshot (LAW #4) ──────────────────────────────────────────────────
#[test]
fn viz_snapshot_release() {
use egui_kittest::Harness;
let mut app = build_app();
// Re-inject after construction (build_app already injected, but the harness
// takes ownership) — keep the same fixture so the PNG is deterministic.
app.inject_release_events_for_test(fixture_rows());
let mut harness = Harness::builder()
.with_size(egui::Vec2::new(1400.0, 900.0))
.wgpu()
.build_ui_state(|ui, app: &mut UrdrThreadsApp| app.draw_ui(ui), app);
for _ in 0..6 {
harness.run_ok();
}
// FUNCTIONAL PROOF on the rendered state.
let state = harness.state().state_json();
assert_eq!(state["tab"].as_str(), Some("Release"));
assert_eq!(
state["release"]["component_status"]["znippy"].as_str(),
Some("ok")
);
// VISUAL PROOF.
let dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/snapshots");
std::fs::create_dir_all(&dir).expect("create tests/snapshots");
let image = harness.render().expect("wgpu render of Release view");
let png_path = dir.join("viz_release.png");
image.save(&png_path).expect("write viz_release.png");
let size = std::fs::metadata(&png_path).map(|m| m.len()).unwrap_or(0);
eprintln!("PROOF: viz_release → {} ({size} bytes)", png_path.display());
assert!(
size > 3_000,
"Release PNG suspiciously small ({size} bytes) — did it render?"
);
// Golden compare (first run writes the golden via save above; only a real
// diff against an existing committed golden fails — mirrors viz_snapshots.rs).
harness.snapshot("viz_release");
if let Err(results) = harness.take_snapshot_results().into_result() {
let inner: Vec<_> = results.into();
for err in &inner {
let msg = err.to_string();
assert!(
msg.contains("Missing snapshot"),
"[viz_release] golden diff: {msg}"
);
}
}
}
// ════════════════════════════════════════════════════════════════════════════
// REGRESSION (task #20 — redb lock-tolerance): a held writer must NOT hide
// committed rows from the viz read path.
//
// The fat viz keeps a durable action-log warehouse handle open for the whole
// app lifetime. Before the fix it `open()`ed the SAME data warehouse the data
// tabs read, holding redb's single-writer lock; every `open_read_only()` then
// fell into the degraded copy-aside snapshot path, which can read mid-write /
// stale state → the 🚀 Release tab showed 0 rows even though the warehouse held
// the seeded events (the original `viz_surface` fat-parity failure).
//
// This test pins the invariant at the warehouse seam (no GUI needed): hold a
// writer handle on a warehouse, seed `release_events`, then read through the
// EXACT viz read path (`open_read_only` + `query_release_events(All)`) and assert
// every committed row comes back.
// ════════════════════════════════════════════════════════════════════════════
#[test]
fn held_writer_does_not_hide_release_rows_from_viz_read_path() {
use nornir::warehouse::iceberg::IcebergWarehouse;
use nornir::warehouse::release_events::{
EventSelector, append_release_events, query_release_events,
};
let root = std::env::temp_dir().join(format!(
"nornir-viz-release-held-{}-{}",
std::process::id(),
uuid::Uuid::new_v4()
));
let _ = std::fs::remove_dir_all(&root);
std::fs::create_dir_all(&root).unwrap();
// Open a writer and KEEP it for the whole test — this is the action-log
// durable sink's exclusive handle (or a live nornir-server). Seed the rows
// through this same writer so they're genuinely committed.
let writer = IcebergWarehouse::open(&root).expect("open writer warehouse");
let rows = fixture_rows();
writer
.block_on(append_release_events(&writer, &rows))
.expect("seed release_events through the held writer");
// The writer stays alive past the read below (NOT dropped), reproducing the
// app-lifetime hold that triggered the bug.
// The viz read path: lock-tolerant read-only open + the All selector, exactly
// as `release_tab::load()` (Src::Local) does.
let wh = IcebergWarehouse::open_read_only(&root)
.expect("open_read_only must succeed even while a writer holds the lock");
// The writer is held, so the read MUST be the degraded copy-aside snapshot
// path (not a plain exclusive open) — that's the exact path the fat viz hits.
assert!(
wh.is_snapshot(),
"expected the lock-tolerant copy-aside snapshot path while a writer is held",
);
let read = wh
.block_on(query_release_events(&wh, &EventSelector::All))
.expect("query_release_events while a writer is held");
assert_eq!(
read.len(),
rows.len(),
"viz read path saw {} of {} committed release_events while a writer held the \
warehouse — the copy-aside snapshot must reflect committed state, not 0 rows",
read.len(),
rows.len(),
);
drop(writer);
let _ = std::fs::remove_dir_all(&root);
}