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

nordisk/nornir

src/autonom/facett_probe.rs

//! **The Elm-harness DRIVE seam** (`viz`-only) — turn nornir's six embedded facett
//! Elm components into six *driven, snapshot-asserted* surfaces.
//!
//! `nornir-testmatrix` cannot depend on facett (the pure crate stays facett-free),
//! so the harness DRIVING lives HERE, in the `nornir` binary crate, behind the same
//! `viz` feature that gates the embeds. This module constructs each embedded facett
//! component, drives it headless through a small representative `Msg` script via
//! [`facett_core::harness`] (no window, no GPU), and produces a
//! [`FacetRow`](nornir_testmatrix::discover::FacetRow) whose
//! [`driven`](nornir_testmatrix::discover::FacetRow::driven) flag is the **REAL drive
//! outcome** — not a static handler-name match. Those rows flow DOWN into
//! [`build_surface_and_covered`](crate::autonom::build_surface_and_covered), where
//! [`covered_facetts`](nornir_testmatrix::discover::covered_facetts) turns a clean
//! drive into a covered surface node — exactly mirroring how the live atom-walk's
//! `DiscoveredAtom`s become covered `ui_atom` nodes.
//!
//! It ALSO emits one [`functional_status`](nornir_testmatrix::functional_status) row
//! per component (green when it snapshots + draws clean), so a broken component
//! surfaces as a RED matrix row automatically.
//!
//! **Skip, never fail:** a component whose headless drive PANICS is recorded
//! `driven = false` with a red functional row (an honest uncovered surface) — the
//! panic is caught so one broken embed can never wedge the matrix. A build without
//! `viz` compiles none of this and the facett slot stays empty (the classic
//! missing-tool skip).

use egui::Color32;
use facett_core::harness;
use nornir_testmatrix::discover::FacetRow;

use facett_docview::{DocPage, DocView, Msg as DocMsg};
use facett_graph3d::{Graph3D, Layout3D, Msg as G3Msg};
use facett_graphview::MetroView;
use facett_graphview::metro::{Msg as MetroMsg, demo_map};
use facett_helix::{HelixView, Msg as HelixMsg};
use facett_jobview::{JobEntry, JobList, JobStatus, Msg as JobMsg};
use facett_warehousedeck::{DeckLayout, Msg as WhMsg, WarehouseDeck};

/// Drive one `Elm` + `Facet` component through `msgs` headless, then prove it drew.
///
/// 1. [`harness::snapshot_json`] applies every `Msg` through `update()` (the ONLY
///    mutation path) and serializes the resulting `Model` (FC-3). A non-object
///    snapshot is a dirty drive.
/// 2. [`harness::headless_render`] renders the post-drive state offscreen and counts
///    tessellated vertices — [`RenderReport::drew`](facett_core::harness::RenderReport::drew)
///    is the "it actually painted something" proof.
///
/// Returns `Ok((drew, detail, snapshot))` on a clean drive (the post-drive
/// `state_json` rides along for the field-level value-coverage enumerator, see
/// [`crate::autonom::ui_values`]); `Err(detail)` when the snapshot is not a
/// serializable object.
fn drive_elm<C>(
    comp: &mut C,
    msgs: Vec<C::Msg>,
) -> Result<(bool, String, serde_json::Value, String), String>
where
    C: facett_core::Elm + facett_core::Facet,
{
    let n_msgs = msgs.len();
    let state = harness::snapshot_json(comp, msgs);
    if !state.is_object() {
        return Err(format!("snapshot is not a JSON object: {state}"));
    }
    let keys = state.as_object().map(|o| o.len()).unwrap_or(0);
    let report = harness::headless_render(comp);
    // The STRUCTURAL error signal (decision (a)): read the post-drive
    // `Facet::severity()` directly instead of scanning rendered text. A pane that
    // folded a `Severity::Error` atom reports "error" here → the gate drops it as
    // uncovered even though it drove. The `error_atoms()` substring scan stays only
    // as the migration fallback for panes that have not adopted `severity()`.
    let sev = facett_core::Facet::severity(comp).as_str().to_string();
    Ok((
        report.drew(),
        format!(
            "msgs={n_msgs} state_keys={keys} verts={} drew={} severity={sev}",
            report.vertices,
            report.drew()
        ),
        state,
        sev,
    ))
}

/// Run one component `probe` under a panic guard, record its functional-status row,
/// and push the resulting [`FacetRow`]. `driven` is `true` iff the drive returned a
/// clean snapshot (no panic, serializable `Model`); a panic or dirty snapshot is a
/// RED functional row + an uncovered `driven = false` surface (skip, never fail).
fn record(
    rows: &mut Vec<FacetRow>,
    snaps: &mut Vec<(String, serde_json::Value)>,
    component: &str,
    caps: &[&str],
    probe: impl FnOnce() -> Result<(bool, String, serde_json::Value, String), String>,
) {
    let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(probe));
    // `severity` is the STRUCTURAL error signal (decision (a)): the pane's own
    // `Facet::severity()` on a clean drive, or `"error"` when the drive was dirty /
    // panicked (a non-serializable snapshot or a panic IS a red pane). A driven pane
    // reporting `"error"` is dropped by `covered_facetts` → the HARD gate fails on it.
    let (driven, detail, severity) = match outcome {
        Ok(Ok((drew, d, snap, sev))) => {
            // A clean drive → keep the live `state_json` for the value enumerator.
            snaps.push((component.to_string(), snap));
            (
                true,
                format!("clean headless drive ({d}, drew_proof={drew})"),
                sev,
            )
        }
        Ok(Err(e)) => (
            false,
            format!("dirty headless drive: {e}"),
            "error".to_string(),
        ),
        Err(_) => (
            false,
            "panicked during headless drive".to_string(),
            "error".to_string(),
        ),
    };
    nornir_testmatrix::functional_status(component, "headless_drive", driven, &detail);
    rows.push(FacetRow {
        component: component.to_string(),
        in_registry: true,
        // These embedded facett components are stateless VIEWS (no `local()`/`remote()`
        // fat/thin ctor split), so each is a single `Mode::NA` surface node.
        has_local_ctor: false,
        has_remote_ctor: false,
        caps: caps.iter().map(|s| s.to_string()).collect(),
        driven,
        severity,
    });
}

/// **Drive all six embedded facett Elm components headless** and return their
/// [`FacetRow`]s with the live `driven` verdict + one `functional_status` row each.
///
/// This is the payoff of the Elm migration: every embedded facett component is a
/// pure `Model`/`Msg`/`update`/`view` value the harness drives with no device, so the
/// test-matrix asserts each component's `update()` end-to-end — not just the
/// monolithic viz render.
pub fn probe_facett_components() -> Vec<FacetRow> {
    probe_facett_components_inner().0
}

/// The live post-drive `state_json` snapshot for each embedded facett component
/// that drove clean — `(component, state_json)`. This is the EMIT source the
/// field-level value-coverage enumerator ([`crate::autonom::ui_values`]) walks: a
/// component that panicked / drove dirty contributes no snapshot (skip, never
/// fail). Empty (skip) in a non-`viz` build.
pub fn probe_facett_snapshots() -> Vec<(String, serde_json::Value)> {
    probe_facett_components_inner().1
}

/// Drive all six embedded components once, returning BOTH their [`FacetRow`]s (the
/// `driven` verdicts) and the live `state_json` snapshots of the clean drives.
fn probe_facett_components_inner() -> (Vec<FacetRow>, Vec<(String, serde_json::Value)>) {
    let mut rows = Vec::with_capacity(6);
    let mut snaps: Vec<(String, serde_json::Value)> = Vec::with_capacity(6);

    // 🧬 jobview — a hierarchical task tree: push a parent + child, fold, select.
    record(
        &mut rows,
        &mut snaps,
        "facett-jobview",
        &["interactive", "hierarchical"],
        || {
            let mut v = JobList::new("facett-jobview");
            let parent = JobEntry::new("p1", "build", JobStatus::Done);
            let child = JobEntry {
                parent_id: Some("p1".to_string()),
                ..JobEntry::new("c1", "compile", JobStatus::Running)
            };
            drive_elm(
                &mut v,
                vec![
                    JobMsg::SetJobs(vec![parent, child]),
                    JobMsg::ToggleFold("p1".to_string()),
                    JobMsg::Select("c1".to_string()),
                ],
            )
        },
    );

    // 📖 docview — a vector-SVG manual: one page, zoom in twice, toggle the TOC, jump.
    record(
        &mut rows,
        &mut snaps,
        "facett-docview",
        &["interactive", "reads_docs"],
        || {
            let svg = br##"<svg xmlns="http://www.w3.org/2000/svg" width="120" height="160"><rect width="120" height="160" fill="#ffffff"/><rect x="10" y="10" width="100" height="20" fill="#3355aa"/></svg>"##;
            let pages: Vec<DocPage> = DocPage::from_svg_bytes(svg).into_iter().collect();
            let toc = if pages.is_empty() {
                Vec::new()
            } else {
                vec![("Intro".to_string(), 0usize)]
            };
            let mut v = DocView::new("facett-docview", pages, toc);
            drive_elm(
                &mut v,
                vec![
                    DocMsg::ZoomIn,
                    DocMsg::ZoomIn,
                    DocMsg::ToggleSidebar,
                    DocMsg::JumpTo(0),
                ],
            )
        },
    );

    // 🚇 graphview — the Metro transit board: load the demo map, drill a line, clear.
    record(
        &mut rows,
        &mut snaps,
        "facett-graphview",
        &["interactive", "graph"],
        || {
            let mut v = MetroView::new("facett-graphview");
            v.set_map(demo_map());
            drive_elm(
                &mut v,
                vec![
                    MetroMsg::SelectLine("bench_run→Bench.Submit".to_string()),
                    MetroMsg::ClearSelection,
                ],
            )
        },
    );

    // 🌐 graph3d — the 3-D node cloud: orbit, tick the clock, select, relayout, fit.
    record(
        &mut rows,
        &mut snaps,
        "facett-graph3d",
        &["interactive", "graph", "3d"],
        || {
            let nodes = vec![
                ("alpha".to_string(), Color32::RED),
                ("beta".to_string(), Color32::GREEN),
                ("gamma".to_string(), Color32::LIGHT_BLUE),
            ];
            let edges = vec![(0usize, 1usize), (1, 2)];
            let mut v = Graph3D::new(nodes, edges, Layout3D::Sphere);
            drive_elm(
                &mut v,
                vec![
                    G3Msg::SetYaw(0.5),
                    G3Msg::Tick(0.1),
                    G3Msg::SelectNode(1),
                    G3Msg::SetLayout(Layout3D::Helix),
                    G3Msg::Fit,
                ],
            )
        },
    );

    // 🏢 warehousedeck — the FacetDeck wall: bump columns, focus a tab, page, return.
    record(
        &mut rows,
        &mut snaps,
        "facett-warehousedeck",
        &["interactive", "deck", "reads_warehouse"],
        || {
            let mut v = WarehouseDeck::new("facett-warehousedeck").with_cols(2);
            drive_elm(
                &mut v,
                vec![
                    WhMsg::IncCols,
                    WhMsg::SetLayout(DeckLayout::Focus),
                    WhMsg::FocusNext,
                    WhMsg::BackToWall,
                ],
            )
        },
    );

    // 🧬 helix — the TIME-HELIX DAG: advance the clock, seek, traverse, level, spin.
    record(
        &mut rows,
        &mut snaps,
        "facett-helix",
        &["interactive", "dag", "time"],
        || {
            let mut v = HelixView::demo();
            drive_elm(
                &mut v,
                vec![
                    HelixMsg::Tick(0.2),
                    HelixMsg::Seek(0.5),
                    HelixMsg::ToggleTraverse(true),
                    HelixMsg::SetLevel(1),
                    HelixMsg::SetYaw(0.3),
                ],
            )
        },
    );

    (rows, snaps)
}

#[cfg(test)]
mod tests {
    use super::*;

    /// All six embedded components drive clean (green) through the harness — the
    /// first assertion that each INDIVIDUAL component's `update()` runs headless,
    /// not just the whole-app kittest render.
    #[test]
    fn all_six_embedded_components_drive_green() {
        let rows = probe_facett_components();
        assert_eq!(rows.len(), 6, "one FacetRow per embedded component");
        for r in &rows {
            assert!(
                r.in_registry,
                "{} must be a registered surface",
                r.component
            );
            assert!(
                r.driven,
                "{} must drive clean through the harness (its update() + render)",
                r.component
            );
        }
        // The driven rows become covered facett surface nodes.
        let covered = nornir_testmatrix::discover::covered_facetts(&rows);
        assert_eq!(
            covered.len(),
            6,
            "each driven NA component → one covered key"
        );
        assert!(covered.contains("facett_component:facett-helix@na"));
    }
}