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

nordisk/nornir

tests/funnel_viz.rs

//! E1 — funnel **viz** demo/nuke/confirm-guard test (inject-and-assert).
//!
//! Builds a LOCAL `UrdrThreadsApp` on a tempdir warehouse and drives the Funnel
//! tab's demo controls through the SAME code paths the buttons invoke:
//!
//!   * `🌱 Run demo` → `state_json()["funnel"]["plans"]` reflects the injected
//!     DAG (plan present, nodes/edges > 0, a root is Ready),
//!   * confirm-guard: ONE click arms `confirm_pending` and the repos are STILL
//!     on disk (nothing deleted yet),
//!   * the confirming second click → disk repos gone, but the funnel plan
//!     PERSISTS in `state_json` (nuke is disk-only; the event log is kept).
//!
//! The warehouse is shaped `<tmp>/ws/.nornir/warehouse` so the demo's git-root
//! resolution lands the fake repos at `<tmp>/git/…` — inside the tempdir, so
//! the test can verify their creation/removal precisely.
#![cfg(feature = "viz")]

use std::path::PathBuf;

use nornir::viz::UrdrThreadsApp;
use serde_json::Value;

fn fake_repos_present(git_root: &PathBuf, size: usize) -> bool {
    if size == 1 {
        git_root
            .join("funnel_fake")
            .join("funnel1_fake_project")
            .exists()
    } else {
        (1..=size).all(|i| {
            git_root
                .join(format!("funnel{size}_fake_projects"))
                .join(format!("funnel_fake_{i}"))
                .exists()
        })
    }
}

#[test]
fn viz_demo_then_nuke_keeps_funnel_plans() {
    // The viz durable action-log sink would hold the catalog's exclusive redb
    // lock for the app's lifetime, so the demo's writable `Store::open` hits
    // "Database already open". Free it (the documented escape in src/viz/app.rs);
    // the file/stderr action-log channels stay live. (See the same fix in
    // tests/funnel_planning.rs and tests/funnel_3d.rs.)
    // TODO: Audit that the environment access only happens in single-threaded code.
    unsafe { std::env::set_var("NORNIR_VIZ_NO_DURABLE_ACTIONLOG", "1") };

    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 git_root = tmp.path().join("git");

    let mut app = UrdrThreadsApp::with_repos(
        warehouse.clone(),
        "viz-demo-test".to_string(),
        ws.clone(),
        vec![],
    );

    // --- 🌱 Run demo (size 3) ---
    let size = 3;
    app.funnel_tab_for_test()
        .run_demo_for_test(size)
        .expect("run demo");

    assert!(
        fake_repos_present(&git_root, size),
        "demo must create {size} fake repos under {}",
        git_root.display()
    );

    let s = app.state_json();
    let funnel = &s["funnel"];
    let plans = funnel["plans"]
        .as_array()
        .expect("funnel.plans is an array");
    assert_eq!(plans.len(), 1, "exactly one demo plan");
    let plan = &plans[0];
    let nodes = plan["nodes"].as_u64().unwrap();
    let edges = plan["edges"].as_u64().unwrap();
    let ready = plan["ready_count"].as_u64().unwrap();
    assert_eq!(nodes, size as u64, "one node per fake repo");
    assert!(edges > 0, "a layered repo DAG must have dep edges");
    assert!(ready >= 1, "at least one root node is Ready");
    assert_eq!(
        funnel["demo"]["available"].as_bool(),
        Some(true),
        "demo controls available in local mode"
    );

    // --- confirm-guard: first click arms, repos still present ---
    app.funnel_tab_for_test().arm_nuke_for_test();
    assert_eq!(
        app.state_json()["funnel"]["demo"]["confirm_pending"].as_bool(),
        Some(true),
        "one click arms the confirm guard"
    );
    assert!(
        fake_repos_present(&git_root, size),
        "arming the guard must NOT delete anything"
    );

    // --- confirming second click: repos gone, plans persist ---
    let removed = app
        .funnel_tab_for_test()
        .confirm_nuke_for_test()
        .expect("nuke");
    assert_eq!(removed.len(), size, "nuke removed all {size} fake repos");
    assert!(
        !fake_repos_present(&git_root, size),
        "fake repos must be gone after confirmed nuke"
    );
    assert_eq!(
        app.state_json()["funnel"]["demo"]["confirm_pending"].as_bool(),
        Some(false),
        "confirm guard resets after nuke"
    );

    // Reload the funnel from the warehouse: the plan PERSISTS (disk-only nuke).
    app.funnel_tab_for_test().reload_for_test();
    let s2: Value = app.state_json();
    let plans2 = s2["funnel"]["plans"].as_array().expect("plans array");
    assert_eq!(
        plans2.len(),
        1,
        "funnel plan must survive a disk nuke (warehouse data kept)"
    );
}