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

nordisk/edda

crates/nornir-testmatrix/src/uiplane.rs

//! # uiplane — UI-PLANE reachability (LAW 9): the running app's navigation graph
//!
//! The mega-matrix tests components **in isolation** — it renders each surface
//! standalone and greens *"the component can render"*. But the user runs a
//! *running app* on which that surface lives on some **plane** (a tab / dialog /
//! view / app-mode), reachable only by **navigating** there. The classic failure:
//! the matrix greened "the Sverige showcase buttons render" while the *shipped*
//! app could not even **navigate** to the plane those buttons were on — they were
//! orphaned on an unreachable plane, or absent from the shipped wasm.
//!
//! This module models the UI as **planes + a navigation plan** and walks it on the
//! REAL app, so the matrix asserts the thing the user actually does: *reach* every
//! surface and *prove it ran* there.
//!
//! ```text
//!   UiPlane   = a tagged view-state (id + label + the interactive SURFACES on it)
//!   UiPlan    = a navigation graph: planes are nodes, TRANSITIONS are edges
//!               (a transition = a named action that moves plane A → B)
//!   PlaneDriver = the seam: drive the SAME plan against different backends
//!               (native robot-UI / kittest, or a deployed-wasm headless browser)
//!   RobotPlan::walk(plan, driver) = from the start plane, BFS the plan to REACH
//!               every plane, and on each assert every declared surface is
//!               PRESENT + RAN (read from the app's emitted trace / state_json).
//! ```
//!
//! ## The LAW 9 verdict
//! A declared surface that is **not reachable** via the plan, or is reachable but
//! **did not run**, is **RED** — and the row names the unreachable plane + the
//! missing transition. A declared plane that the start plane cannot reach (an
//! **orphan**) is RED. A surface declared on no plane (or on an unreachable plane)
//! is RED — that is the exact Sverige-buttons catch.
//!
//! ## Generic + UI-agnostic
//! No viz/facett/egui types leak here. A headless library with no UI simply has an
//! **empty plan** (zero planes, zero transitions) — `walk` on it is trivially green
//! (nothing to reach). nornir-viz, facett-demo, korp, etc. each supply their OWN
//! `UiPlan` + a `PlaneDriver` over their own app; this core walks any of them.
//!
//! ## Composes with — does NOT duplicate
//! - [`crate::functional`] — every (plane, surface) verdict is emitted as a
//!   `functional_status` row, so LAW 9 lands in the SAME matrix as everything else.
//! - [`crate::utfallsrum`] — a surface with an outcome space attaches its
//!   [`Outcome`](crate::utfallsrum::Outcome) and the walk records its sweep too.
//! - facett's `Navigable` (pan/zoom data struct) — orthogonal: `Navigable` is the
//!   *within-plane* spatial model; `UiPlan` is the *between-plane* navigation graph.
//!   A `PlaneDriver` impl uses `Navigable` to move within a plane if it needs to.
//! - `nornir-robotui` — the native `PlaneDriver` impl wraps its `RobotSession`
//!   clicks; this module only defines the seam, nornir wires the robot to it.

use std::collections::{BTreeMap, BTreeSet, VecDeque};

use serde::{Deserialize, Serialize};

use crate::model::{status, TestResultRow};

/// The aspect tag carried by UI-plane reachability [`TestResultRow`]s — distinct
/// from `"functional"` / `"utfallsrum"` so the matrix can group LAW 9 rows.
pub const UIPLANE_ASPECT: &str = "uiplane";

// ─── the declared model: planes, surfaces, transitions ──────────────────────

/// One **interactive surface** that lives on a [`UiPlane`] — a button, slider,
/// pane, control. The unit LAW 9 proves *reachable + ran*.
///
/// `id` is the stable id within its plane (matched against the driver's emitted
/// present/ran sets). `must_run` marks a surface that MUST have fired its emitter
/// once its plane is reached (a data-bearing pane, an auto-running view); a
/// passive surface (a static label) may be present-only. Default `must_run = true`
/// — pessimism (LAW 7): presume a surface should run unless declared passive.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct UiSurface {
    /// Stable id within the plane (matched against the driver's emit set).
    pub id: String,
    /// Human label for display (defaults to `id`).
    pub label: String,
    /// Must this surface have RAN (fired its emitter) once its plane is reached?
    /// `true` (default) = present-AND-ran required; `false` = present is enough.
    #[serde(default = "default_true")]
    pub must_run: bool,
}

fn default_true() -> bool {
    true
}

impl UiSurface {
    /// A surface that must be present AND ran once its plane is reached.
    pub fn new(id: impl Into<String>) -> Self {
        let id = id.into();
        Self { label: id.clone(), id, must_run: true }
    }

    /// A present-only surface (a static label / chrome) — present is enough, it
    /// need not fire an emitter.
    pub fn passive(id: impl Into<String>) -> Self {
        let mut s = Self::new(id);
        s.must_run = false;
        s
    }

    /// Set a display label distinct from the id.
    pub fn with_label(mut self, label: impl Into<String>) -> Self {
        self.label = label.into();
        self
    }
}

/// One **plane** of the running app: a tagged view-state with the interactive
/// surfaces that live on it.
///
/// `id` is the stable plane id (a tab name, a dialog id, `"workspace=nornir/tab=Bench"`).
/// Two planes are the same iff their `id` matches. Surfaces are keyed by id within
/// the plane.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct UiPlane {
    /// Stable plane id — the driver's `current_plane()` returns this when the app
    /// is on this plane.
    pub id: String,
    /// Human label for display (defaults to `id`).
    pub label: String,
    /// The interactive surfaces declared to live on this plane, keyed by id.
    pub surfaces: BTreeMap<String, UiSurface>,
}

impl UiPlane {
    /// A plane with no surfaces yet.
    pub fn new(id: impl Into<String>) -> Self {
        let id = id.into();
        Self { label: id.clone(), id, surfaces: BTreeMap::new() }
    }

    /// Set a display label distinct from the id.
    pub fn with_label(mut self, label: impl Into<String>) -> Self {
        self.label = label.into();
        self
    }

    /// Declare a surface on this plane (idempotent by surface id).
    pub fn surface(mut self, s: UiSurface) -> Self {
        self.surfaces.entry(s.id.clone()).or_insert(s);
        self
    }

    /// Declare many surfaces at once.
    pub fn surfaces<I: IntoIterator<Item = UiSurface>>(mut self, ss: I) -> Self {
        for s in ss {
            self = self.surface(s);
        }
        self
    }
}

/// A **transition**: a named action that moves the app from plane `from` to plane
/// `to`. The driver's [`PlaneDriver::apply`] interprets `action` (e.g. "click tab
/// Bench", "open dialog", "switch workspace knut") and the walk asserts the app
/// actually landed on `to` afterwards.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Transition {
    /// The plane this transition departs from.
    pub from: String,
    /// The plane this transition is declared to reach.
    pub to: String,
    /// The named action the driver applies (a click label, a command, …). The
    /// driver maps this string to a real action; the plan stays UI-agnostic.
    pub action: String,
}

impl Transition {
    pub fn new(
        from: impl Into<String>,
        to: impl Into<String>,
        action: impl Into<String>,
    ) -> Self {
        Self { from: from.into(), to: to.into(), action: action.into() }
    }
}

/// The **navigation graph**: planes (nodes) + transitions (edges) + the start
/// plane the app boots on. [`RobotPlan::walk`] BFS-walks this against a
/// [`PlaneDriver`] to reach every plane and prove every surface ran.
///
/// A headless library with no UI builds an empty plan (no planes, no transitions);
/// `walk` on it is trivially green (nothing to reach), so the primitive is truly
/// UI-agnostic.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct UiPlan {
    /// Every declared plane, keyed by id.
    pub planes: BTreeMap<String, UiPlane>,
    /// Every declared transition (plane→plane edges).
    pub transitions: Vec<Transition>,
    /// The plane the app boots on — the BFS root. Empty for an empty plan.
    pub start: String,
}

impl UiPlan {
    /// A plan whose start plane is `start` (the app's boot plane).
    pub fn new(start: impl Into<String>) -> Self {
        Self { start: start.into(), ..Default::default() }
    }

    /// An empty plan — a headless library with no UI. `walk` is trivially green.
    pub fn empty() -> Self {
        Self::default()
    }

    /// Declare a plane (idempotent by id).
    pub fn plane(mut self, p: UiPlane) -> Self {
        self.planes.entry(p.id.clone()).or_insert(p);
        self
    }

    /// Declare a transition edge.
    pub fn transition(mut self, t: Transition) -> Self {
        self.transitions.push(t);
        self
    }

    /// Convenience: declare a transition by parts.
    pub fn edge(
        self,
        from: impl Into<String>,
        to: impl Into<String>,
        action: impl Into<String>,
    ) -> Self {
        self.transition(Transition::new(from, to, action))
    }

    /// Is this an empty (no-UI) plan?
    pub fn is_empty(&self) -> bool {
        self.planes.is_empty()
    }

    /// Total declared surfaces across all planes.
    pub fn surface_count(&self) -> usize {
        self.planes.values().map(|p| p.surfaces.len()).sum()
    }

    /// The set of plane ids **reachable** from `start` over the transition edges
    /// (a pure BFS — no driver needed). The start plane is included iff it is a
    /// declared plane. Used by [`UiPlan::orphan_planes`] and by the walk's
    /// reachability check.
    pub fn reachable_planes(&self) -> BTreeSet<String> {
        let mut adj: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
        for t in &self.transitions {
            adj.entry(t.from.as_str()).or_default().push(t.to.as_str());
        }
        let mut reached: BTreeSet<String> = BTreeSet::new();
        if self.start.is_empty() {
            return reached;
        }
        let mut q: VecDeque<&str> = VecDeque::new();
        // The start is reachable only if it is actually a declared plane.
        if self.planes.contains_key(&self.start) {
            q.push_back(self.start.as_str());
        }
        while let Some(n) = q.pop_front() {
            if !reached.insert(n.to_string()) {
                continue;
            }
            if let Some(succ) = adj.get(n) {
                for &s in succ {
                    // Only traverse into declared planes.
                    if self.planes.contains_key(s) && !reached.contains(s) {
                        q.push_back(s);
                    }
                }
            }
        }
        reached
    }

    /// Declared planes that the start plane **cannot reach** — ORPHANS. A
    /// non-empty result is a LAW 9 RED: a plane (and every surface on it) the
    /// running app can never navigate to. Sorted.
    pub fn orphan_planes(&self) -> Vec<String> {
        let reachable = self.reachable_planes();
        let mut orphans: Vec<String> = self
            .planes
            .keys()
            .filter(|id| !reachable.contains(*id))
            .cloned()
            .collect();
        orphans.sort();
        orphans
    }

    /// Transitions whose `from` or `to` names a plane that is not declared — a
    /// dangling edge (the plan references a plane it never defined). Sorted by the
    /// `(from,to)` pair. A dangling edge is a plan-authoring bug, surfaced so it is
    /// never silent.
    pub fn dangling_transitions(&self) -> Vec<&Transition> {
        let mut out: Vec<&Transition> = self
            .transitions
            .iter()
            .filter(|t| !self.planes.contains_key(&t.from) || !self.planes.contains_key(&t.to))
            .collect();
        out.sort_by(|a, b| (a.from.as_str(), a.to.as_str()).cmp(&(b.from.as_str(), b.to.as_str())));
        out
    }

    /// A BFS **plan**: an ordered list of `(plane, Option<transition-action>)`
    /// steps that reaches every reachable plane from start, each annotated with the
    /// transition action that gets there (`None` for the start plane). This is the
    /// driving script [`RobotPlan::walk`] executes. Planes are visited once; the
    /// path to each is the first BFS path found (the SHORTEST in edge count).
    pub fn bfs_route(&self) -> Vec<RouteStep> {
        let mut adj: BTreeMap<&str, Vec<&Transition>> = BTreeMap::new();
        for t in &self.transitions {
            if self.planes.contains_key(&t.from) && self.planes.contains_key(&t.to) {
                adj.entry(t.from.as_str()).or_default().push(t);
            }
        }
        let mut route = Vec::new();
        if self.start.is_empty() || !self.planes.contains_key(&self.start) {
            return route;
        }
        let mut seen: BTreeSet<&str> = BTreeSet::new();
        let mut q: VecDeque<(&str, Option<&Transition>)> = VecDeque::new();
        q.push_back((self.start.as_str(), None));
        while let Some((plane, via)) = q.pop_front() {
            if !seen.insert(plane) {
                continue;
            }
            route.push(RouteStep {
                plane: plane.to_string(),
                via_action: via.map(|t| t.action.clone()),
                from: via.map(|t| t.from.clone()),
            });
            if let Some(succ) = adj.get(plane) {
                for &t in succ {
                    if !seen.contains(t.to.as_str()) {
                        q.push_back((t.to.as_str(), Some(t)));
                    }
                }
            }
        }
        route
    }
}

/// One step of a [`UiPlan::bfs_route`]: the plane to be on, and the transition
/// action that reaches it from a previously-visited plane (`None` = the start).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RouteStep {
    /// The plane this step lands on.
    pub plane: String,
    /// The transition action that reaches it (`None` for the start plane). When
    /// `Some`, the driver applies this action to move to `plane`.
    pub via_action: Option<String>,
    /// The plane the transition departs FROM (`None` for the start).
    pub from: Option<String>,
}

// ─── the driver seam ────────────────────────────────────────────────────────

/// The seam that lets the SAME [`UiPlan`] run against different backends — a
/// native robot-UI (egui_kittest / `nornir-robotui`) **or** a deployed-wasm
/// headless-browser driver that reads `window.__facett_state()` (LAW 1: drive the
/// SHIPPED artifact). The walk only ever talks to this trait, so a plan written
/// once runs against both the native build and the shipped wasm.
///
/// ## Contract
/// - [`current_plane`](PlaneDriver::current_plane): the app's current plane id —
///   read from the app's emitted state (`state_json["tab"]` / `window.__facett_state().plane`).
/// - [`apply`](PlaneDriver::apply): perform a transition's named action (click the
///   tab, switch the workspace, open the dialog) and settle (run frames / await).
///   Returns `Ok(())` if the action was performed; the WALK (not the driver)
///   verifies the resulting plane matches the transition's `to`.
/// - [`surfaces_present`](PlaneDriver::surfaces_present): the set of surface ids
///   the app reports PRESENT on the current plane (read from its trace / state).
/// - [`surfaces_ran`](PlaneDriver::surfaces_ran): the set of surface ids that have
///   RAN (fired their emitter) on the current plane.
///
/// A driver reads PRESENT/RAN from the app's *emitted data*, never from pixels
/// (LAW 6). The native impl reads `state_json`; the wasm impl reads the JS hook.
pub trait PlaneDriver {
    /// The plane id the app is currently on (from its emitted state).
    fn current_plane(&mut self) -> String;

    /// Apply a transition's named `action` (move the app). The walk verifies the
    /// landing plane; this only performs the action. An error means the action
    /// itself could not be performed (e.g. the control wasn't found) — the walk
    /// records that as a RED unreachable verdict naming the missing transition.
    fn apply(&mut self, action: &str) -> Result<(), String>;

    /// The surface ids the app reports PRESENT on the current plane.
    fn surfaces_present(&mut self) -> BTreeSet<String>;

    /// The surface ids that have RAN (fired their emitter) on the current plane.
    fn surfaces_ran(&mut self) -> BTreeSet<String>;
}

// ─── the walk + its verdict ─────────────────────────────────────────────────

/// The per-(plane, surface) outcome of a [`RobotPlan::walk`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SurfaceVerdict {
    /// Reached the plane, surface present AND ran (or present-only if passive).
    ReachableAndRan,
    /// Reached the plane, but the surface was PRESENT and never RAN (its emitter
    /// never fired). RED — a button that exists but whose handler never fires.
    ReachableNotRan,
    /// Reached the plane, but the declared surface was ABSENT. RED — the shipped
    /// app is on the plane but the surface isn't there (the wasm-parity catch).
    ReachableAbsent,
    /// The plane itself was UNREACHABLE (orphan / a transition failed to land), so
    /// the surface could never be reached. RED — the Sverige-buttons catch.
    Unreachable,
}

impl SurfaceVerdict {
    /// Is this verdict a RED (failing) one? Everything but [`ReachableAndRan`].
    pub fn is_red(self) -> bool {
        !matches!(self, SurfaceVerdict::ReachableAndRan)
    }

    pub fn label(self) -> &'static str {
        match self {
            SurfaceVerdict::ReachableAndRan => "reachable_and_ran",
            SurfaceVerdict::ReachableNotRan => "reachable_not_ran",
            SurfaceVerdict::ReachableAbsent => "reachable_absent",
            SurfaceVerdict::Unreachable => "unreachable",
        }
    }
}

/// One row of a walk report: a (plane, surface) and its [`SurfaceVerdict`], plus
/// the transition action that reached the plane (so a RED names the missing
/// transition). Converts to a [`TestResultRow`] / `functional_status` emit.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WalkRow {
    pub plane: String,
    pub surface: String,
    pub verdict: SurfaceVerdict,
    /// The transition action that reaches this surface's plane (`None` for the
    /// start plane / for an unreachable plane that had no working path).
    pub via_action: Option<String>,
    /// Whether the surface required running (`must_run`).
    pub must_run: bool,
}

impl WalkRow {
    /// The `functional_status`-style check id: `"<plane>/<surface>"`.
    pub fn check(&self) -> String {
        format!("{}/{}", self.plane, self.surface)
    }

    /// A human detail explaining the verdict (and, on RED, what's missing).
    pub fn detail(&self) -> String {
        match self.verdict {
            SurfaceVerdict::ReachableAndRan => format!(
                "reached plane '{}'{} · surface present{}",
                self.plane,
                self.via_action
                    .as_deref()
                    .map(|a| format!(" via '{a}'"))
                    .unwrap_or_else(|| " (start)".into()),
                if self.must_run { " AND ran" } else { " (passive)" },
            ),
            SurfaceVerdict::ReachableNotRan => format!(
                "LAW 9 RED: plane '{}' reached but surface '{}' PRESENT and never RAN \
                 (its emitter never fired — present ≠ ran)",
                self.plane, self.surface,
            ),
            SurfaceVerdict::ReachableAbsent => format!(
                "LAW 9 RED: plane '{}' reached but declared surface '{}' is ABSENT \
                 (shipped surface missing — the wasm-parity / orphan-surface catch)",
                self.plane, self.surface,
            ),
            SurfaceVerdict::Unreachable => format!(
                "LAW 9 RED: surface '{}' UNREACHABLE — plane '{}' is an orphan or no \
                 transition lands on it (name + add the missing transition)",
                self.surface, self.plane,
            ),
        }
    }

    /// Convert to a matrix [`TestResultRow`] (aspect [`UIPLANE_ASPECT`]). `suite`
    /// = `component`, `test_name` = `"<plane>/<surface>"`, pass iff
    /// [`SurfaceVerdict::ReachableAndRan`].
    pub fn to_row(&self, component: &str) -> TestResultRow {
        let ok = !self.verdict.is_red();
        TestResultRow {
            run_id: String::new(),
            repo: String::new(),
            suite: component.to_string(),
            test_name: self.check(),
            status: if ok { status::PASS } else { status::FAIL }.to_string(),
            duration_ms: 0.0,
            ts_micros: 0,
            message: self.detail(),
            aspect: UIPLANE_ASPECT.to_string(),
            metric: if ok { 0.0 } else { 1.0 },
        }
    }
}

/// The whole result of a [`RobotPlan::walk`]: every (plane, surface) verdict, the
/// orphan planes, the dangling transitions, and the reached-plane set. The
/// LAW 9 verdict ([`WalkReport::is_green`]) is *green ⟺ every declared plane was
/// reached AND every declared surface was reachable-and-ran*.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct WalkReport {
    /// One row per (plane, surface) declared in the plan.
    pub rows: Vec<WalkRow>,
    /// Declared planes the walk never reached (orphans + transitions that failed
    /// to land). Sorted. Non-empty ⇒ RED.
    pub unreachable_planes: Vec<String>,
    /// Plan-authoring dangling edges (referenced an undeclared plane). Sorted.
    pub dangling_edges: Vec<Transition>,
    /// Planes actually reached during the walk (drove-and-landed). Sorted.
    pub reached_planes: Vec<String>,
    /// Total declared planes in the plan.
    pub total_planes: usize,
}

impl WalkReport {
    /// LAW 9 verdict: green ⟺ no unreachable plane, no dangling edge, and every
    /// (plane, surface) row is [`SurfaceVerdict::ReachableAndRan`]. An empty plan
    /// (no planes) is trivially green — a headless library has nothing to reach.
    pub fn is_green(&self) -> bool {
        self.unreachable_planes.is_empty()
            && self.dangling_edges.is_empty()
            && self.rows.iter().all(|r| !r.verdict.is_red())
    }

    /// The RED rows (the failing (plane, surface) verdicts).
    pub fn red_rows(&self) -> Vec<&WalkRow> {
        self.rows.iter().filter(|r| r.verdict.is_red()).collect()
    }

    /// A one-line summary for the CLI / viz.
    pub fn summary(&self) -> String {
        let red = self.red_rows().len();
        format!(
            "{}/{} planes reached · {} surfaces · {} RED · {} orphan-planes · {} dangling — {}",
            self.reached_planes.len(),
            self.total_planes,
            self.rows.len(),
            red,
            self.unreachable_planes.len(),
            self.dangling_edges.len(),
            if self.is_green() { "GREEN" } else { "RED (LAW 9)" },
        )
    }

    /// Convert every walk row to a matrix [`TestResultRow`] (aspect
    /// [`UIPLANE_ASPECT`]), `suite = component`.
    pub fn to_rows(&self, component: &str) -> Vec<TestResultRow> {
        self.rows.iter().map(|r| r.to_row(component)).collect()
    }

    /// Emit every walk row to the process-global functional buffer (feature
    /// `testmatrix` ON) so `nornir test` / the matrix drains LAW 9 verdicts like
    /// any other functional row. One emit per (plane, surface). A no-op in release.
    #[cfg(feature = "testmatrix")]
    pub fn emit(&self, component: &str) {
        for r in &self.rows {
            crate::functional::functional_status(
                component,
                &r.check(),
                !r.verdict.is_red(),
                &r.detail(),
            );
        }
    }

    /// No-op emit in release (feature OFF).
    #[cfg(not(feature = "testmatrix"))]
    #[inline]
    pub fn emit(&self, _component: &str) {}
}

/// The walker: BFS the [`UiPlan`] against a [`PlaneDriver`], reaching every plane
/// and proving every surface present + ran. Stateless — a free function dressed as
/// a type for a tidy call site (`RobotPlan::walk(&plan, &mut driver)`).
pub struct RobotPlan;

impl RobotPlan {
    /// Walk `plan` with `driver`: from the start plane, BFS to reach every plane,
    /// and on each reached plane assert every declared surface is PRESENT + RAN.
    ///
    /// ## Algorithm
    /// 1. Compute the BFS route ([`UiPlan::bfs_route`]) + the orphan set
    ///    ([`UiPlan::orphan_planes`]) + dangling edges.
    /// 2. For each route step: if it has a transition action, [`apply`] it and
    ///    verify the driver's [`current_plane`] now equals the target — a mismatch
    ///    (or an `apply` error) marks the plane (and below it) UNREACHABLE.
    /// 3. On a reached plane, read [`surfaces_present`] + [`surfaces_ran`] and emit
    ///    one [`WalkRow`] per declared surface:
    ///    `present && (ran || !must_run)` → ReachableAndRan;
    ///    present but not ran (and must_run) → ReachableNotRan;
    ///    absent → ReachableAbsent.
    /// 4. Every surface on an unreachable/orphan plane → Unreachable.
    ///
    /// The result is one [`WalkRow`] for EVERY declared surface in the plan, so the
    /// report is provably complete (LAW 9 reachability completeness): no declared
    /// surface is silently skipped.
    pub fn walk(plan: &UiPlan, driver: &mut dyn PlaneDriver) -> WalkReport {
        let mut report = WalkReport {
            total_planes: plan.planes.len(),
            ..Default::default()
        };
        report.dangling_edges = plan.dangling_transitions().into_iter().cloned().collect();

        let route = plan.bfs_route();
        // The action that reached each plane (for naming the missing transition on
        // a RED row), keyed by plane id.
        let mut via_of: BTreeMap<String, Option<String>> = BTreeMap::new();
        let mut reached: BTreeSet<String> = BTreeSet::new();

        for step in &route {
            via_of.insert(step.plane.clone(), step.via_action.clone());
            // Drive the transition (if any) and verify we landed on the target.
            let landed = match &step.via_action {
                None => {
                    // Start plane: verify the app actually boots here.
                    driver.current_plane() == step.plane
                }
                Some(action) => match driver.apply(action) {
                    Ok(()) => driver.current_plane() == step.plane,
                    Err(_) => false,
                },
            };
            if landed {
                reached.insert(step.plane.clone());
                Self::record_plane(plan, &step.plane, &step.via_action, driver, &mut report);
            }
            // If not landed, the plane stays unreached; its surfaces are recorded
            // as Unreachable in the final sweep below.
        }

        // Reachability completeness: EVERY declared plane must have been reached.
        // Any plane not in `reached` (orphan, or a transition that failed to land)
        // → record all its surfaces Unreachable + add to unreachable_planes.
        for (plane_id, plane) in &plan.planes {
            if !reached.contains(plane_id) {
                report.unreachable_planes.push(plane_id.clone());
                let via = via_of.get(plane_id).cloned().flatten();
                for s in plane.surfaces.values() {
                    report.rows.push(WalkRow {
                        plane: plane_id.clone(),
                        surface: s.id.clone(),
                        verdict: SurfaceVerdict::Unreachable,
                        via_action: via.clone(),
                        must_run: s.must_run,
                    });
                }
            }
        }

        report.unreachable_planes.sort();
        report.unreachable_planes.dedup();
        report.reached_planes = reached.into_iter().collect();
        report.reached_planes.sort();
        // Stable row order: (plane, surface).
        report.rows.sort_by(|a, b| {
            (a.plane.as_str(), a.surface.as_str()).cmp(&(b.plane.as_str(), b.surface.as_str()))
        });
        report
    }

    /// Record one reached plane's surfaces (present/ran sweep).
    fn record_plane(
        plan: &UiPlan,
        plane_id: &str,
        via_action: &Option<String>,
        driver: &mut dyn PlaneDriver,
        report: &mut WalkReport,
    ) {
        let plane = match plan.planes.get(plane_id) {
            Some(p) => p,
            None => return,
        };
        let present = driver.surfaces_present();
        let ran = driver.surfaces_ran();
        for s in plane.surfaces.values() {
            let verdict = if !present.contains(&s.id) {
                SurfaceVerdict::ReachableAbsent
            } else if s.must_run && !ran.contains(&s.id) {
                SurfaceVerdict::ReachableNotRan
            } else {
                SurfaceVerdict::ReachableAndRan
            };
            report.rows.push(WalkRow {
                plane: plane_id.to_string(),
                surface: s.id.clone(),
                verdict,
                via_action: via_action.clone(),
                must_run: s.must_run,
            });
        }
    }
}

// ─── a tiny in-memory driver: the test double + the reference shape ─────────

/// A pure, in-memory [`PlaneDriver`] for unit-testing a [`UiPlan`] without any UI
/// — and the reference shape a real driver mimics. You hand it a transition table
/// (`action → target plane`) and, per plane, the present + ran surface sets the
/// "app" would report. The walk drives it exactly as it would a real app.
///
/// This is also how the harness's OWN tests prove the LAW 9 verdicts (orphan →
/// RED, absent → RED, present-not-ran → RED, present+ran → GREEN) without standing
/// up egui.
#[derive(Debug, Clone, Default)]
pub struct MockDriver {
    /// The current plane id.
    pub current: String,
    /// `action → plane it lands on`. An action not in here fails to land (apply
    /// returns Err) — modelling a missing/broken transition.
    pub transitions: BTreeMap<String, String>,
    /// Per plane: the surface ids the app reports PRESENT.
    pub present: BTreeMap<String, BTreeSet<String>>,
    /// Per plane: the surface ids the app reports RAN.
    pub ran: BTreeMap<String, BTreeSet<String>>,
}

impl MockDriver {
    /// A driver booted on `start`.
    pub fn new(start: impl Into<String>) -> Self {
        Self { current: start.into(), ..Default::default() }
    }

    /// Register a transition `action → target plane`.
    pub fn with_transition(mut self, action: impl Into<String>, to: impl Into<String>) -> Self {
        self.transitions.insert(action.into(), to.into());
        self
    }

    /// Declare, for `plane`, the present + ran surface id sets the app reports.
    pub fn with_plane_state<I, J, S, T>(mut self, plane: &str, present: I, ran: J) -> Self
    where
        I: IntoIterator<Item = S>,
        J: IntoIterator<Item = T>,
        S: Into<String>,
        T: Into<String>,
    {
        self.present
            .insert(plane.to_string(), present.into_iter().map(Into::into).collect());
        self.ran
            .insert(plane.to_string(), ran.into_iter().map(Into::into).collect());
        self
    }
}

impl PlaneDriver for MockDriver {
    fn current_plane(&mut self) -> String {
        self.current.clone()
    }

    fn apply(&mut self, action: &str) -> Result<(), String> {
        match self.transitions.get(action) {
            Some(to) => {
                self.current = to.clone();
                Ok(())
            }
            None => Err(format!("MockDriver: no transition for action '{action}'")),
        }
    }

    fn surfaces_present(&mut self) -> BTreeSet<String> {
        self.present.get(&self.current).cloned().unwrap_or_default()
    }

    fn surfaces_ran(&mut self) -> BTreeSet<String> {
        self.ran.get(&self.current).cloned().unwrap_or_default()
    }
}

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

    /// A 2-plane plan: Home --click Bench--> Bench. Bench declares a data surface.
    fn home_bench_plan() -> UiPlan {
        UiPlan::new("Home")
            .plane(UiPlane::new("Home").surface(UiSurface::passive("title")))
            .plane(UiPlane::new("Bench").surface(UiSurface::new("bench_chart")))
            .edge("Home", "Bench", "click:Bench")
    }

    #[test]
    fn reachable_planes_is_bfs_from_start() {
        // A --x--> B --y--> C, plus an orphan D with no inbound edge.
        let plan = UiPlan::new("A")
            .plane(UiPlane::new("A"))
            .plane(UiPlane::new("B"))
            .plane(UiPlane::new("C"))
            .plane(UiPlane::new("D"))
            .edge("A", "B", "x")
            .edge("B", "C", "y");
        let reach = plan.reachable_planes();
        assert!(reach.contains("A") && reach.contains("B") && reach.contains("C"));
        assert!(!reach.contains("D"), "D has no inbound edge → orphan");
        assert_eq!(plan.orphan_planes(), vec!["D".to_string()], "D named as the orphan");
    }

    #[test]
    fn dangling_transition_references_undeclared_plane() {
        let plan = UiPlan::new("A")
            .plane(UiPlane::new("A"))
            .edge("A", "GHOST", "x"); // GHOST never declared
        let dangling = plan.dangling_transitions();
        assert_eq!(dangling.len(), 1);
        assert_eq!(dangling[0].to, "GHOST");
    }

    #[test]
    fn bfs_route_reaches_every_reachable_plane_with_actions() {
        let plan = home_bench_plan();
        let route = plan.bfs_route();
        assert_eq!(route.len(), 2, "Home + Bench");
        assert_eq!(route[0].plane, "Home");
        assert!(route[0].via_action.is_none(), "start has no via");
        assert_eq!(route[1].plane, "Bench");
        assert_eq!(route[1].via_action.as_deref(), Some("click:Bench"));
        assert_eq!(route[1].from.as_deref(), Some("Home"));
    }

    // ── the four LAW 9 verdicts, sensitivity-proven ──────────────────────────

    #[test]
    fn walk_green_when_every_surface_present_and_ran() {
        let plan = home_bench_plan();
        let mut driver = MockDriver::new("Home")
            .with_transition("click:Bench", "Bench")
            .with_plane_state("Home", ["title"], [] as [&str; 0]) // passive: present, need not run
            .with_plane_state("Bench", ["bench_chart"], ["bench_chart"]); // present + ran
        let report = RobotPlan::walk(&plan, &mut driver);
        assert!(report.is_green(), "all surfaces reachable+ran → GREEN: {}", report.summary());
        assert_eq!(report.rows.len(), 2, "title + bench_chart");
        assert!(report.unreachable_planes.is_empty());
        assert_eq!(report.reached_planes, vec!["Bench".to_string(), "Home".to_string()]);
        let chart = report.rows.iter().find(|r| r.surface == "bench_chart").unwrap();
        assert_eq!(chart.verdict, SurfaceVerdict::ReachableAndRan);
    }

    #[test]
    fn walk_red_when_surface_present_but_never_ran() {
        // SENSITIVITY: the SAME plan goes RED when the data surface didn't run.
        let plan = home_bench_plan();
        let mut driver = MockDriver::new("Home")
            .with_transition("click:Bench", "Bench")
            .with_plane_state("Home", ["title"], [] as [&str; 0])
            .with_plane_state("Bench", ["bench_chart"], [] as [&str; 0]); // present, NOT ran
        let report = RobotPlan::walk(&plan, &mut driver);
        assert!(!report.is_green(), "present-but-not-ran → RED");
        let chart = report.rows.iter().find(|r| r.surface == "bench_chart").unwrap();
        assert_eq!(chart.verdict, SurfaceVerdict::ReachableNotRan);
        assert!(chart.detail().contains("never RAN"), "names the failure");
    }

    #[test]
    fn walk_red_when_declared_surface_absent_on_reached_plane() {
        // The wasm-parity catch: on the plane but the surface isn't there.
        let plan = home_bench_plan();
        let mut driver = MockDriver::new("Home")
            .with_transition("click:Bench", "Bench")
            .with_plane_state("Home", ["title"], [] as [&str; 0])
            .with_plane_state("Bench", [] as [&str; 0], [] as [&str; 0]); // ABSENT
        let report = RobotPlan::walk(&plan, &mut driver);
        assert!(!report.is_green(), "absent surface → RED");
        let chart = report.rows.iter().find(|r| r.surface == "bench_chart").unwrap();
        assert_eq!(chart.verdict, SurfaceVerdict::ReachableAbsent);
        assert!(chart.detail().contains("ABSENT"));
    }

    #[test]
    fn walk_red_when_plane_is_orphan_unreachable() {
        // The Sverige-buttons catch: a surface declared on a plane no transition
        // reaches. Add an orphan "Sverige" plane with a button, NO edge to it.
        let plan = home_bench_plan()
            .plane(UiPlane::new("Sverige").surface(UiSurface::new("sverige_button")));
        let mut driver = MockDriver::new("Home")
            .with_transition("click:Bench", "Bench")
            .with_plane_state("Home", ["title"], [] as [&str; 0])
            .with_plane_state("Bench", ["bench_chart"], ["bench_chart"]);
        let report = RobotPlan::walk(&plan, &mut driver);
        assert!(!report.is_green(), "orphan plane → RED");
        assert_eq!(report.unreachable_planes, vec!["Sverige".to_string()], "names the orphan plane");
        let btn = report.rows.iter().find(|r| r.surface == "sverige_button").unwrap();
        assert_eq!(btn.verdict, SurfaceVerdict::Unreachable);
        assert!(btn.detail().contains("UNREACHABLE"), "names the missing transition need");
        // The OTHER surfaces are still green — the orphan didn't poison them.
        let chart = report.rows.iter().find(|r| r.surface == "bench_chart").unwrap();
        assert_eq!(chart.verdict, SurfaceVerdict::ReachableAndRan);
    }

    #[test]
    fn walk_red_when_transition_fails_to_land() {
        // The transition exists in the plan but the DRIVER can't perform it (the
        // control is missing in the shipped app) → the plane is unreachable.
        let plan = home_bench_plan();
        let mut driver = MockDriver::new("Home")
            // NO "click:Bench" transition registered → apply() errors.
            .with_plane_state("Home", ["title"], [] as [&str; 0])
            .with_plane_state("Bench", ["bench_chart"], ["bench_chart"]);
        let report = RobotPlan::walk(&plan, &mut driver);
        assert!(!report.is_green(), "broken transition → RED");
        assert!(report.unreachable_planes.contains(&"Bench".to_string()));
        let chart = report.rows.iter().find(|r| r.surface == "bench_chart").unwrap();
        assert_eq!(chart.verdict, SurfaceVerdict::Unreachable);
    }

    #[test]
    fn walk_red_when_app_boots_on_wrong_start_plane() {
        // The app claims to start on Home but actually boots elsewhere → even the
        // start plane is unreached.
        let plan = home_bench_plan();
        let mut driver = MockDriver::new("SomewhereElse")
            .with_transition("click:Bench", "Bench")
            .with_plane_state("Bench", ["bench_chart"], ["bench_chart"]);
        let report = RobotPlan::walk(&plan, &mut driver);
        assert!(!report.is_green());
        assert!(
            report.unreachable_planes.contains(&"Home".to_string()),
            "start plane unreached when the app boots elsewhere"
        );
    }

    #[test]
    fn empty_plan_is_trivially_green() {
        // A headless library with no UI: nothing to reach, nothing to run.
        let plan = UiPlan::empty();
        let mut driver = MockDriver::new("");
        let report = RobotPlan::walk(&plan, &mut driver);
        assert!(report.is_green(), "no planes → trivially green");
        assert_eq!(report.rows.len(), 0);
        assert_eq!(report.total_planes, 0);
    }

    #[test]
    fn report_is_complete_one_row_per_declared_surface() {
        // Reachability completeness: EVERY declared surface gets exactly one row,
        // reachable or not — no surface is silently skipped.
        let plan = home_bench_plan()
            .plane(UiPlane::new("Orphan").surface(UiSurface::new("lost")));
        let mut driver = MockDriver::new("Home")
            .with_transition("click:Bench", "Bench")
            .with_plane_state("Home", ["title"], [] as [&str; 0])
            .with_plane_state("Bench", ["bench_chart"], ["bench_chart"]);
        let report = RobotPlan::walk(&plan, &mut driver);
        // title + bench_chart + lost = 3 declared surfaces → 3 rows.
        assert_eq!(report.rows.len(), plan.surface_count());
        assert_eq!(report.rows.len(), 3);
    }

    #[test]
    fn walk_rows_convert_to_matrix_rows_with_uiplane_aspect() {
        let plan = home_bench_plan();
        let mut driver = MockDriver::new("Home")
            .with_transition("click:Bench", "Bench")
            .with_plane_state("Home", ["title"], [] as [&str; 0])
            .with_plane_state("Bench", ["bench_chart"], [] as [&str; 0]); // not ran → red row
        let report = RobotPlan::walk(&plan, &mut driver);
        let rows = report.to_rows("viz");
        assert_eq!(rows.len(), 2);
        let chart = rows.iter().find(|r| r.test_name == "Bench/bench_chart").unwrap();
        assert_eq!(chart.aspect, UIPLANE_ASPECT);
        assert_eq!(chart.status, status::FAIL, "not-ran surface → RED matrix row");
        assert_eq!(chart.suite, "viz");
        assert_eq!(chart.metric, 1.0);
        let title = rows.iter().find(|r| r.test_name == "Home/title").unwrap();
        assert_eq!(title.status, status::PASS, "passive present surface → green");
    }

    // ── feature ON: emit drains the LAW 9 rows into the functional buffer ─────
    #[cfg(feature = "testmatrix")]
    #[test]
    fn emit_drains_uiplane_rows_into_functional_buffer() {
        let _guard = crate::functional::test_lock();
        let _ = crate::functional::drain_functional_rows();
        let plan = home_bench_plan();
        let mut driver = MockDriver::new("Home")
            .with_transition("click:Bench", "Bench")
            .with_plane_state("Home", ["title"], [] as [&str; 0])
            .with_plane_state("Bench", ["bench_chart"], ["bench_chart"]);
        let report = RobotPlan::walk(&plan, &mut driver);
        report.emit("viz-uiplane");
        let rows = crate::functional::drain_functional_rows();
        let mine: Vec<_> = rows.iter().filter(|r| r.suite == "viz-uiplane").collect();
        assert_eq!(mine.len(), 2, "title + bench_chart emitted");
        assert!(mine.iter().all(|r| r.status == status::PASS), "all green this run");
        let chart = mine.iter().find(|r| r.test_name == "Bench/bench_chart").unwrap();
        assert!(chart.message.contains("present AND ran"));
    }
}