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