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

nordisk/dwarves

dwarves-gui/tests/robot_funnel_dag_3d.rs

//! **robot_funnel_dag_3d** — the RED-when-broken proof that the funnel pane renders its
//! Plan-DAG as a **3D graph**, with a camera-only rotate and a clickable drill.
//!
//! The funnel is nornir's persistent Plan-DAG (ideas → plans → nodes, with dependency
//! + decomposition edges). Beside the `facett-plandag` card lane and the flat
//! `facett-graphview` node-link graph, the pane now offers a **3D layered graph**: the
//! ranks are stacked as planes in space and projected through the world graph's
//! orthographic 3D camera (a CPU port of `facett_graphview::world_still`'s projection,
//! which is `gpu`-gated). A robot mounts the embedded pane over a real seeded funnel
//! (ideas → a write→fix→test plan chain + a triaged error) and drives it entirely
//! through the `Facet` state/update seam.
//!
//! The invariants proven here:
//!  1. switching to the 3D view renders the whole DAG in 3D (node + edge counts, lane
//!     histogram, `graph3d_on`) — nothing is capped or dropped;
//!  2. clicking a node (the headless `graph3d_select` twin of a pointer click) drills to
//!     the card detail — the SAME `facett-plandag` view's selection lands on that node;
//!  3. **3D rotating spins the CAMERA, not the LAYOUT** — over 24 repaint frames the
//!     projection azimuth sweeps while `graph3d_build_calls()` stays EXACTLY 0 (the
//!     cached layout is re-projected, never rebuilt); turning rotation off freezes it.
//!
//! Own test binary so the process-wide `graph3d_build_calls()` counter reflects only
//! this test (each integration file is a separate process — the same isolation the
//! world graph's `robot_world_3d_rotate` uses). Rotation calls `ctx.request_repaint()`
//! every frame, so the spin frames use `run_steps(N)` (bounded), never `run()`.
//!
//! **No crate-level `#![cfg(feature = ...)]`** (the silenced-tests trap): the scenario
//! runs unconditionally; only the test-matrix emit is feature-gated.

#![cfg(not(target_arch = "wasm32"))]

use std::cell::RefCell;
use std::rc::Rc;

use egui::vec2;
use egui_kittest::kittest::Queryable;
use egui_kittest::Harness;
use facett_core::Facet;
use nornir_funnel_embed::{graph3d_build_calls, FunnelPanel};

#[test]
fn funnel_plan_dag_renders_in_3d_rotates_camera_only_and_drills() {
    // The embedded funnel seeds a real nordisk Plan-DAG (offline, no server): idea
    // i-001 decomposed into n-001 write → n-002 fix → n-003 test, plus error i-002 →
    // n-004 triage — a few ideas → plan → nodes with edges.
    let panel = Rc::new(RefCell::new(FunnelPanel::new_embedded_seeded("nordisk")));
    let p = panel.clone();
    let mut h = Harness::builder()
        .with_size(vec2(1400.0, 1000.0))
        .build_ui(move |ui| {
            Facet::ui(&mut *p.borrow_mut(), ui);
        });

    // Pause the live auto-poll so the frame accounting is clean (the embedded DAG is
    // static, so a poll would not rebuild anyway — this just removes repaint noise).
    panel.borrow_mut().update_json(r#"{"auto_poll":false}"#);
    // Switch to the 3D layered render. Rotation is ON BY DEFAULT (the spinning
    // showpiece), which keeps repaint requests coming — turn it off explicitly so
    // run() settles; section (3) below turns it back on and measures the spin.
    panel.borrow_mut().update_json(r#"{"dag_view":"graph3d"}"#);
    panel
        .borrow_mut()
        .update_json(r#"{"graph3d_rotate":false}"#);
    h.run();

    // ── (1) The whole DAG renders in 3D ──────────────────────────────────────────
    let s = Facet::state_json(&*panel.borrow());
    assert_eq!(s["dag_view"], "graph3d", "the 3D view is active: {s}");
    assert_eq!(s["graph3d_on"], true, "state reports 3D on: {s}");
    let g = &s["graph3d"];
    let nodes = g["nodes"].as_u64().unwrap();
    let edges = g["edges"].as_u64().unwrap();
    assert!(
        nodes >= 6,
        "every funnel node lowered into 3D (>=6, got {nodes}): {s}"
    );
    assert!(
        edges >= 4,
        "every DAG edge lowered into 3D (>=4, got {edges}): {s}"
    );
    assert!(
        g["node_cap"].is_null(),
        "no MAX_NODES ceiling in the 3D render: {s}"
    );
    // Coloured by lane/state (idea / plan / node / done / failed) — as DATA.
    let lanes = &g["lanes"];
    assert!(
        lanes["idea"].as_u64().unwrap() >= 1,
        "idea lane present: {s}"
    );
    assert!(
        lanes["done"].as_u64().unwrap() >= 1,
        "a done node present: {s}"
    );
    let distinct = ["idea", "plan", "node", "done", "failed"]
        .iter()
        .filter(|k| lanes[**k].as_u64().unwrap_or(0) > 0)
        .count();
    assert!(
        distinct >= 3,
        "the DAG spans several lanes (got {distinct}): {s}"
    );

    // The "3D rotating" checkbox is really rendered in the pane (wired into ui()).
    assert!(
        h.query_by_label("3D rotating").is_some(),
        "the '3D rotating' checkbox renders in the 3D view controls"
    );
    // The idea WORDS are attached in 3D: labels default ON and the painted texts
    // carry the seeded idea's own words (asserted as TEXT, not a count).
    assert_eq!(g["labels"], true, "labels default ON in the 3D funnel: {s}");
    let words: Vec<&str> = g["label_texts"]
        .as_array()
        .unwrap()
        .iter()
        .filter_map(|v| v.as_str())
        .collect();
    assert!(
        words.iter().any(|w| w.contains("release engine")),
        "the 3D labels carry the idea's words: {words:?}"
    );
    assert!(
        h.query_by_label("labels").is_some(),
        "the 'labels' checkbox renders in the 3D view controls"
    );

    // ── (2) Clicking a node drills to the card detail (same plandag selection) ────
    // The 3D node id is the raw funnel card id (`n-002`); the drill maps it to the card
    // view's `node:<id>` resolved key.
    panel
        .borrow_mut()
        .update_json(r#"{"graph3d_select":"n-002"}"#);
    h.run();
    let s = Facet::state_json(&*panel.borrow());
    assert_eq!(
        s["graph3d"]["selected"], "n-002",
        "the 3D node is selected: {s}"
    );
    assert!(
        s["graph3d"]["selected_title"].is_string(),
        "the drilled node carries its title: {s}"
    );
    // The drill reuses the EXISTING card detail: the same `facett-plandag` view now has
    // that node selected, so switching to Cards shows its detail line.
    assert_eq!(
        s["plandag"]["selected"], "node:n-002",
        "the drill lands on the card view's selection: {s}"
    );

    // ── (3) 3D rotating spins the CAMERA, not the LAYOUT ──────────────────────────
    panel.borrow_mut().update_json(r#"{"graph3d_rotate":true}"#);
    let s = Facet::state_json(&*panel.borrow());
    assert_eq!(
        s["graph3d"]["rotate"], true,
        "the rotate toggle reads on: {s}"
    );
    let angle0 = s["graph3d"]["rotate_angle"]
        .as_f64()
        .expect("rotate_angle reported");
    let base_builds = graph3d_build_calls();

    const FRAMES: usize = 24;
    h.run_steps(FRAMES); // EXACTLY N frames (run() would spin forever while rotating)

    let builds_delta = graph3d_build_calls() - base_builds;
    let s = Facet::state_json(&*panel.borrow());
    let angle1 = s["graph3d"]["rotate_angle"].as_f64().unwrap();
    assert_eq!(
        builds_delta, 0,
        "rotation is projection-only: {FRAMES} spin frames rebuilt the layout {builds_delta} times (must be 0)"
    );
    let moved = (angle1 - angle0).abs();
    assert!(
        moved > 1e-3,
        "the camera azimuth advanced over {FRAMES} frames (Δ={moved:.4} rad)"
    );

    // Turning rotation OFF freezes the angle — no drift, still 0 rebuilds.
    panel
        .borrow_mut()
        .update_json(r#"{"graph3d_rotate":false}"#);
    let steady = graph3d_build_calls();
    h.run_steps(6);
    let s = Facet::state_json(&*panel.borrow());
    let angle2 = s["graph3d"]["rotate_angle"].as_f64().unwrap();
    assert_eq!(
        graph3d_build_calls() - steady,
        0,
        "a static 3D view does not rebuild either"
    );
    assert!(
        (angle2 - angle1).abs() < 1e-4,
        "the azimuth is frozen once rotation is off"
    );

    let ok = nodes >= 6
        && edges >= 4
        && distinct >= 3
        && s["plandag"]["selected"] == "node:n-002"
        && builds_delta == 0
        && moved > 1e-3
        && (angle2 - angle1).abs() < 1e-4;

    // A SHIPPED surface (the funnel pane's 3D Plan-DAG render) owes the coverage board a
    // row — reuse edda's testmatrix rather than hand-roll a harness.
    #[cfg(feature = "testmatrix")]
    nornir_testmatrix::functional_status(
        "nornir-funnel-embed",
        "funnel_plan_dag_renders_in_3d_rotates_camera_only_and_drills",
        ok,
        "the funnel pane renders its Plan-DAG as a 3D layered graph (ranks stacked in space, \
         projected through the world graph's orthographic 3D camera, coloured by lane/state): \
         switching to the 3D view lowers every node + edge (no cap, multi-lane), a node click \
         drills to the SAME facett-plandag card selection, and over 24 repaint frames the '3D \
         rotating' azimuth sweeps (Δ>1e-3 rad) while graph3d_build_calls() stays EXACTLY 0 — \
         proving the layout is reused from cache, never rebuilt per frame; rotation off freezes it",
    );
    assert!(ok);
}