tests/viz_live.rs
//! Inject-and-assert proof for the 📡 **Live Run** view (C3) — THE_BIG_PLAN
//! LAW #1: a test that feeds real input and asserts real output, never merely
//! "didn't panic".
//!
//! C3 changes the Live Run pane's **durable source** from `/tmp` to the
//! warehouse `release_events` table (LAW persist-to-warehouse-stream-not-tmp).
//! These tests prove it two ways:
//!
//! 1. `live_view_hydrates_from_warehouse_release_events` — seed `release_events`
//! rows into a **real on-disk warehouse**, point the app at it, hydrate the
//! Live Run pane (the durable read path it runs on load/reload), and assert
//! `state_json()["live"]` exposes the run + its rendered event lines read
//! back **from the warehouse** (so a run survives a reboot / is visible to
//! other clients). No `/tmp` log file exists — the warehouse IS the source.
//!
//! 2. `live_view_inject_exposes_run_header_and_lines` — the lighter
//! inject-only path (no warehouse on disk) asserting the same `state_json`
//! contract, mirroring `viz_release.rs`.
#![cfg(feature = "viz")]
use std::path::PathBuf;
use nornir::viz::UrdrThreadsApp;
use nornir::warehouse::iceberg::IcebergWarehouse;
use nornir::warehouse::release_events::{ReleaseEventRow, append_release_events, phase, status};
/// A deterministic one-run release DAG: `znippy` tests green, `holger` (gate)
/// still running, wrapped in a `run` envelope. Appended out of `seq` order on
/// purpose — the hydrate must re-order by `seq` for the newest run.
fn fixture_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![
// run envelope start — detail carries the workspace label.
mk(0, run, "run", phase::START, status::RUNNING, None, "holger"),
// znippy test start → end ok (1200ms), appended out of order.
mk(
2,
"znippy",
"test",
phase::END,
status::OK,
Some(1200),
"3 passed",
),
mk(1, "znippy", "test", phase::START, status::RUNNING, None, ""),
// holger gate, still running.
mk(3, "holger", "gate", phase::START, status::RUNNING, None, ""),
]
}
#[test]
fn live_view_hydrates_from_warehouse_release_events() {
// 1. Seed release_events into a REAL on-disk warehouse (the durable source).
let warehouse_root =
std::env::temp_dir().join(format!("nornir-viz-live-wh-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&warehouse_root);
std::fs::create_dir_all(&warehouse_root).unwrap();
let wh = IcebergWarehouse::open(&warehouse_root).expect("open warehouse");
wh.block_on(append_release_events(
&wh,
&fixture_rows("run-c3-warehouse"),
))
.expect("append release_events");
// Drop the writer handle so the read-only hydrate opens cleanly.
drop(wh);
// No /tmp live log file exists — the warehouse is the ONLY source here, which
// is exactly the post-reboot / other-client situation C3 must survive.
// 2. Point the app at that warehouse and hydrate the Live Run pane the way a
// load/reload does (the real durable read path, not an inject).
let mut app = UrdrThreadsApp::with_repos(
warehouse_root.clone(),
"holger".to_string(),
PathBuf::new(),
vec!["znippy".to_string(), "holger".to_string()],
);
app.live_hydrate_from_warehouse_for_test();
// 3. Assert the rendered Live Run state came from the warehouse.
let state = app.state_json();
assert_eq!(
state["tab"].as_str(),
Some("LiveRun"),
"tab must be LiveRun"
);
let live = &state["live"];
assert_eq!(
live["hydrated_from_warehouse"], true,
"Live Run must hydrate from the warehouse, not /tmp; got {live}"
);
assert!(
live["error"].is_null(),
"warehouse hydrate must succeed, got error {}",
live["error"]
);
assert_eq!(
live["hydrated_run_id"].as_str(),
Some("run-c3-warehouse"),
"the run read back from release_events"
);
// The durable source is the warehouse path, NOT a /tmp file.
let durable = live["durable_source"].as_str().unwrap_or("");
assert!(
durable.starts_with("warehouse ") && durable.contains("nornir-viz-live-wh-"),
"durable_source must be the warehouse, got {durable:?}"
);
// The run header reduced from the warehouse rows.
assert_eq!(live["run_id"].as_str(), Some("run-c3-warehouse"));
assert_eq!(
live["workspace"].as_str(),
Some("holger"),
"run/start detail → workspace"
);
assert_eq!(
live["current_repo"].as_str(),
Some("holger"),
"last phase boundary's repo"
);
// The seeded rows surface as the pane's rendered event lines (re-ordered by
// seq): znippy test start, znippy test end ok (1.2s), holger gate start.
let lines: Vec<&str> = live["lines"]
.as_array()
.unwrap()
.iter()
.filter_map(|l| l.as_str())
.collect();
assert!(
lines.iter().any(|l| *l == "… znippy test start"),
"znippy test-start line missing; got {lines:?}"
);
assert!(
lines.iter().any(|l| *l == "✓ znippy test (1.2s)"),
"znippy test-end ok line missing; got {lines:?}"
);
assert!(
lines.iter().any(|l| *l == "… holger gate start"),
"holger gate-start line missing; got {lines:?}"
);
assert!(
live["events"].as_u64().unwrap() >= 3,
"all seeded boundaries surfaced"
);
let _ = std::fs::remove_dir_all(&warehouse_root);
}
#[test]
fn live_view_inject_exposes_run_header_and_lines() {
let warehouse_root =
std::env::temp_dir().join(format!("nornir-viz-live-inject-{}", 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_live_release_events_for_test(fixture_rows("run-c3-inject"));
let state = app.state_json();
assert_eq!(state["tab"].as_str(), Some("LiveRun"));
let live = &state["live"];
assert_eq!(live["hydrated_from_warehouse"], true);
assert_eq!(live["hydrated_run_id"].as_str(), Some("run-c3-inject"));
assert_eq!(live["run_id"].as_str(), Some("run-c3-inject"));
assert_eq!(live["workspace"].as_str(), Some("holger"));
let lines: Vec<&str> = live["lines"]
.as_array()
.unwrap()
.iter()
.filter_map(|l| l.as_str())
.collect();
assert!(
lines.iter().any(|l| *l == "✓ znippy test (1.2s)"),
"got {lines:?}"
);
}