Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/nornir

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:?}"
    );
}