tests/funnel_3d.rs
//! Inject-assert proof for the **rotating 3-D funnel view** (sphere/helix/force).
//!
//! LAW #1: inject real input + assert real output. LAW #6: the view exposes what
//! it renders as `state_json()["funnel"]["graph3d"]`.
//!
//! Flow: build a LOCAL `UrdrThreadsApp` on a tempdir warehouse, run the funnel
//! demo (the `🌱 Run demo` button's path) to inject a real plan DAG, then drive
//! the funnel tab's 3-D toggle + layout-cycle test hooks exactly as the `🌐 3D`
//! and `⟳` buttons do, and assert the 3-D graph reflects the injected plan
//! (node/edge counts) and that the layout cycles sphere → helix → force → sphere.
#![cfg(feature = "viz")]
use nornir::viz::UrdrThreadsApp;
use serde_json::Value;
#[test]
fn demo_then_3d_view_reflects_plan_and_cycles_layouts() {
// Free the catalog's exclusive lock for this single-process test (the viz
// durable action-log sink would otherwise hold it; see src/viz/app.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 mut app = UrdrThreadsApp::with_repos(warehouse, "viz-3d-test".to_string(), ws, vec![]);
// --- inject a real plan DAG via the demo (size 4) ---
let size = 4;
app.funnel_tab_for_test()
.run_demo_for_test(size)
.expect("run demo");
// The 2-D state reports the plan shape we expect to mirror in 3-D.
let s = app.state_json();
let plan = &s["funnel"]["plans"][0];
let plan_nodes = plan["nodes"].as_u64().expect("plan nodes") as usize;
let plan_edges = plan["edges"].as_u64().expect("plan edges") as usize;
assert_eq!(plan_nodes, size, "one node per fake repo");
assert!(plan_edges > 0, "a layered repo DAG must have dep edges");
// Before toggling, the view is 2-D.
assert_eq!(
s["funnel"]["view3d"].as_bool(),
Some(false),
"starts in 2-D"
);
// --- toggle the rotating 3-D view (the 🌐 3D button) ---
let (g_nodes, g_edges) = app.funnel_tab_for_test().set_3d_for_test(true);
assert_eq!(g_nodes, plan_nodes, "3-D cloud has one node per plan node");
assert_eq!(g_edges, plan_edges, "3-D cloud has one edge per plan dep");
let s = app.state_json();
let g3 = &s["funnel"]["graph3d"];
assert_eq!(s["funnel"]["view3d"].as_bool(), Some(true), "now in 3-D");
assert_eq!(
g3["nodes"].as_u64(),
Some(plan_nodes as u64),
"graph3d node count = plan"
);
assert_eq!(
g3["edges"].as_u64(),
Some(plan_edges as u64),
"graph3d edge count = plan deps"
);
assert_eq!(
g3["layout"].as_str(),
Some("sphere"),
"first layout is sphere"
);
assert_eq!(g3["spin"].as_bool(), Some(true), "auto-spins by default");
// yaw is a finite number (the spin angle the user sees).
assert!(matches!(&g3["yaw"], Value::Number(_)), "yaw is numeric");
// --- cycle the layout (the ⟳ button): sphere → helix → force → sphere ---
assert_eq!(
app.funnel_tab_for_test().cycle_3d_layout_for_test(),
"helix"
);
assert_eq!(
app.state_json()["funnel"]["graph3d"]["layout"].as_str(),
Some("helix")
);
assert_eq!(
app.funnel_tab_for_test().cycle_3d_layout_for_test(),
"force"
);
assert_eq!(
app.state_json()["funnel"]["graph3d"]["layout"].as_str(),
Some("force")
);
assert_eq!(
app.funnel_tab_for_test().cycle_3d_layout_for_test(),
"sphere",
"wraps back"
);
// --- toggle back to 2-D ---
app.funnel_tab_for_test().set_3d_for_test(false);
assert_eq!(
app.state_json()["funnel"]["view3d"].as_bool(),
Some(false),
"toggles back to the 2-D snarl DAG"
);
}