dwarves-gui/src/tabbed.rs
//! **TabbedView** — a tiny tab shell that turns dwarves-gui into a small TABBED app
//! (**Forge | Funnel | Claude**) without touching any pane.
//!
//! It is itself a [`Facet`] (so the `facett-wasm-demo` harness mounts it exactly like
//! any other panel), holding one `Box<dyn Panel>` per tab. The **Forge** tab is the
//! existing [`ForgeView`](crate::panel::ForgeView); the **Funnel** tab is the LIVE
//! `nornir-funnel-embed` pane (backend (2)); the **Claude** tab is the shared
//! `facett-claude::ClaudePane` (direct-Claude chat) hosted by [`ClaudeTab`] over the
//! shared `nornir-claude` edda leaf's transport (offline stub by default; live HTTP
//! behind the `claude-http` feature) — no dwarves-local Claude client. Additive: no pane is modified, and the demo's
//! `__dwarves_state` hook still returns valid JSON — folded per tab
//! (`{ "active_tab", "tabs", "forge": …, "funnel": …, "claude": … }`).
//!
//! **The tab INTERNALS adopted the scene too.** dwarves' SHELL was already the
//! constellation's reference — a draggable master/detail root, a drawer rail, foldable
//! panes — but every tab BODY was still ONE `Slot` holding one imperative egui flow, so
//! minimise / maximise / drag operated on a whole tab and could do NOTHING inside it: a
//! readout that grew came straight out of the list or canvas it shared the flow with,
//! with no divider to correct it. A pane that implements [`SplitPanel`] is mounted as
//! [`TabPanel::Split`] and its body becomes `Split(axis, ratio, DRAGGABLE)` over
//! `<slug>-main` | `<slug>-inspector` — two chromed, foldable scene panes with a divider
//! between them. See [`body_split`] for the roster and for WHY most tabs are not on it.
//!
//! **And a THIRD shape, for a body that is genuinely more than two things.** A pane that
//! implements [`LeafPanel`] is mounted as [`TabPanel::Leaves`] and its body becomes the
//! whole [`Pane`] subtree its own module declares ([`body_leaves`]) — N named, chromed,
//! foldable leaves over N-1 draggable dividers. ⚒ Build Thing is the one body on it:
//! `build_thing_scene::body()` had declared its leaves over their dividers since it was
//! written and had **no callers**, because the two-way shape above was the ceiling. The
//! opt-in reasoning is unchanged — most tabs stay whole, and a thin wrapper over a shared
//! facett widget still cannot decompose at all.
//!
//! ⚠ **facett patches wanted**, in priority order:
//! 1. ~~`TabView::show` neither wraps nor scrolls~~ — **FIXED UPSTREAM, and this note was
//! a month out of date.** `facett-app/src/tabview.rs:87` puts the header row in an
//! `egui::ScrollArea::horizontal`, landed in facett `cdb3cb63` on **2026-07-22**; its
//! comment names this very host ("dwarves lost its 18th tab exactly this way"). This
//! paragraph was written on **2026-08-21**, a month AFTER the fix, by describing the
//! defect from memory instead of opening the file — so a solved problem stayed on a
//! wanted-list, and the reader below was told to route around something that works.
//! facett is a PATH dep here, so dwarves has always built against the fixed code.
//! Kept rather than deleted, because a wanted-list that silently loses entries cannot
//! be audited: what belongs in the record is that it was wanted, and is done.
//! 2. Most dwarves tabs are thin wrappers whose body is `self.inner.ui(ui)` over a
//! SHARED facett widget (`facett_bench_view` · `facett_fleet_view` · `facett_saga_view`
//! · `facett_binview` · `facett_coderoom` · `facett_matrix` · `facett_releaseview` …).
//! Their master/detail lives INSIDE facett, so they cannot decompose here. Giving those
//! widgets a `ui_main` / `ui_inspector` pair (the [`SplitPanel`] shape) would let every
//! host decompose them.
//! 3. `scene::Pane::header_title` is private, so a host cannot audit the AccessKit labels
//! its own scene publishes — and chrome is default-ON, which makes every pane TITLE an
//! addressable node where a duplicate makes BOTH unreachable.
use facett_app::scene::{
self, Action, Axis, Layout, MenuItem, NavState, Pane, Route, Scene, SceneHost, Target,
};
use facett_core::{Facet, Panel};
use serde_json::{json, Value};
use crate::warehouse_select::WarehouseSelector;
// ── the declarative shell, as DATA (facett-app `scene`) ────────────────────────
// The whole tabbed shell is a [`Scene`]: a menubar + a foldable pane tree + routes.
// `TabbedView` is a thin [`SceneHost`] that fills each `Slot` by driving the SAME
// existing `Box<dyn Panel>` (reuse, not rewrite). The three stable pane ids:
/// The root master/detail split pane (rail | workspace). `pub` so the
/// `tests/robot_scene.rs` integration suite can address the shell split by id.
pub const ROOT: &str = "shell";
/// The left rail pane (warehouse selector + About) — a foldable DRAWER. `pub` for the
/// robot suite (drawer / fold assertions address it by id).
pub const RAIL: &str = "rail";
/// The workspace pane — a `Tabs` layout over the existing tab panels. `pub` for the
/// robot suite (tab-switch assertions address it by id).
pub const BODY: &str = "body";
/// The stable SLOT ID for a tab label — the scene pane name, the `state_json` fold key
/// and the deep-link (`app#pane`) path segment, all in one.
///
/// Lowercase, with every non-alphanumeric run collapsed to a single `-` (trimmed at the
/// ends). Plain one-word labels are unchanged (`"Warehouse"` → `"warehouse"`), but a
/// label carrying punctuation can never leak it into an addressable id: `"CI/CD"` →
/// `"ci-cd"`, not `"ci/cd"`. A `/` in a pane name is a latent trap — it is a path
/// separator in the deep-link form — so it is normalised HERE, in the one place every id
/// is derived, rather than by renaming the user-visible label.
pub fn slug(label: &str) -> String {
let mut out = String::with_capacity(label.len());
for ch in label.chars() {
// A Nordic letter keeps its base letter (`Mímir` → `mimir`), never a `-` hole.
let ch = match ch {
'á' | 'Á' | 'å' | 'Å' | 'ä' | 'Ä' => 'a',
'é' | 'É' => 'e',
'í' | 'Í' => 'i',
'ó' | 'Ó' | 'ö' | 'Ö' | 'ø' | 'Ø' => 'o',
'ú' | 'Ú' => 'u',
other => other,
};
if ch.is_ascii_alphanumeric() {
out.push(ch.to_ascii_lowercase());
} else if !out.ends_with('-') {
out.push('-');
}
}
out.trim_matches('-').to_string()
}
/// A stable header glyph per known tab (decorates the foldable tab header).
fn tab_icon(label: &str) -> &'static str {
match slug(label).as_str() {
"warehouse" => "🗄",
"forge" => "🔨",
"pipeline" => "🪈",
"builds" => "🧱",
"build-thing" => "⚒",
"funnel" => "🫙",
"claude" => "✳",
"jera" => "🗂",
"data-view" => "🏢",
"tree" => "🌳",
"status" => "🚦",
"ci-cd" => "⚙",
"release" => "🚀",
"mimir-release" => "🌳",
"benches" => "📊",
"fleet" => "🛰",
"binary" => "🧬",
"profile-an-app" => "🔬",
"coderoom" => "📚",
"saga" => "🎞",
"settings" => "🎛",
"profile" => "👤",
"manual" => "📖",
_ => "▪",
}
}
// ── the tab INTERNALS: a body that decomposes into a master/detail pair ────────
/// **A tab body that DECOMPOSES into a master/detail pair of scene panes.**
///
/// dwarves' shell was already the constellation's reference: a draggable master/detail
/// root, a drawer rail, foldable panes. But every tab BODY was still ONE `Slot` holding
/// one imperative egui flow, so minimise / maximise / drag operated on a whole tab and
/// could do NOTHING inside it — a readout that grew (a badge SVG, a poll banner, a step
/// log) simply pushed the list or canvas it shared the flow with off the bottom, with no
/// divider to correct it.
///
/// A pane that implements this is mounted as [`TabPanel::Split`] and gets a
/// `Split(axis, ratio, DRAGGABLE)` body over two named, chromed, foldable scene panes:
/// `<slug>-main` (the list / table / canvas) and `<slug>-inspector` (the detail beside
/// it). It is a DECOMPOSITION, not a rewrite: `Facet::ui` keeps drawing both halves in
/// order, so the same pane still mounts whole in the unit tests that box it alone.
pub trait SplitPanel: Panel {
/// The MASTER half — the list / table / canvas.
fn ui_main(&mut self, ui: &mut egui::Ui);
/// The INSPECTOR half — controls, readouts, detail. Returns its own observable state
/// (folded into the scene's `render.slots` under `<slug>-inspector`).
fn ui_inspector(&mut self, ui: &mut egui::Ui) -> Value;
}
/// **A tab body that decomposes into N NAMED LEAVES over a DECLARED pane tree.**
///
/// [`SplitPanel`] can express exactly one shape — a two-way `<slug>-main` |
/// `<slug>-inspector` pair — and that ceiling is the whole reason
/// `build_thing_scene::body()`, which declares THREE leaves over TWO dividers (five over four until the kernel became a node of the build graph and the matrix and settings became the graph and the menu), sat in
/// the tree with **no callers**: the shell could not host it, so the module read as the
/// running layout while describing a shape nothing rendered. This is the
/// generalisation. The tab hands the shell a whole [`Pane`] subtree ([`body_leaves`])
/// and the shell fills each of its leaves BY NAME.
///
/// It keeps every property [`body_split`]'s docs give as the reason most tabs are NOT
/// decomposed — a third shape must not quietly widen the roster:
///
/// * **a decomposition, not a rewrite** — `Facet::ui` still draws every leaf in reading
/// order, so the same pane mounts whole in the unit tests that box it alone, and the
/// `__dwarves_state()["<slug>"]` object keeps its exact shape;
/// * **opt-in per tab** — a body with no internal structure stays one honest `Slot`.
/// Forcing leaves on it buys dividers nobody can use and chrome bars that only steal
/// rows, which is worse than leaving it whole;
/// * **not available to a thin wrapper over a SHARED facett widget** — that widget's
/// master/detail lives inside facett and is not dwarves' to carve, exactly as for
/// [`SplitPanel`]. Nothing here changes that; it changes only what a tab whose body
/// dwarves DOES own is allowed to say about itself.
pub trait LeafPanel: Panel {
/// Draw ONE leaf, addressed by the pane name the declared tree gives it, and return
/// that leaf's own observable state (folded into the scene's `render.slots` under
/// the same name).
///
/// The name is one of [`body_leaf_names`] for this tab's slug — the SAME constants
/// the declared tree is built from, so a leaf cannot be drawn under a name nothing
/// declares. An implementation that is handed a name it does not know must SAY SO in
/// the pane rather than draw nothing: a blank leaf is indistinguishable from a leaf
/// whose content is empty, which is the defect this whole tab learned twice.
fn ui_leaf(&mut self, leaf: &str, ui: &mut egui::Ui) -> Value;
/// **Is this leaf's SUBJECT absent right now** — so the leaf must not EXIST in the
/// tab this frame: no region, no title, no fold chrome?
///
/// The declared tree ([`body_leaves`]) is the tab's full CAPABILITY; this is the
/// per-frame fact of which declared leaves have a subject. The shell asks every
/// frame, before layout, and drives the scene's runtime presence with the answer
/// (`NavState::set_absent`) — so presence is always a consequence of the panel's own
/// domain facts and never a stale flag. The ⚒ case that forced it: the kernel
/// configurator leaf was HARDMOUNTED, so an operator saw a kernel pane frame before
/// any recipe was resolved — content-gating the leaf (an empty or refusing pane)
/// still left the frame on screen, and the owner's rule is strict: *the kernel leaf
/// must not exist until the resolved recipe declares a `[[build.kernel]]`*.
///
/// Defaulted to `false` — "every declared leaf always has a subject" is the honest
/// answer for a body whose leaves are unconditional, and there is no second story
/// the default could hide (a leaf wrongly present is on screen to see; a leaf
/// wrongly absent fails the robot that addresses it).
fn leaf_absent(&self, leaf: &str) -> bool {
let _ = leaf;
false
}
}
/// One mounted tab: a pane that is honestly WHOLE, one that decomposes into a
/// master/detail pair, or one that decomposes into N named leaves.
pub enum TabPanel {
/// One pane, one slot — the honest shape for a tab with no master/detail, and the
/// only shape available for a tab that is a thin wrapper around a SHARED facett
/// widget (the widget's own internals are not dwarves' to split).
Whole(Box<dyn Panel>),
/// A body that decomposes into `<slug>-main` | `<slug>-inspector`.
Split(Box<dyn SplitPanel>),
/// A body that decomposes into the N named leaves [`body_leaves`] declares for it.
Leaves(Box<dyn LeafPanel>),
}
impl TabPanel {
/// The pane as a plain [`Panel`] (title / state / update are identical either way).
pub fn panel(&self) -> &dyn Panel {
match self {
TabPanel::Whole(p) => &**p,
TabPanel::Split(p) => &**p,
TabPanel::Leaves(p) => &**p,
}
}
/// The pane as a mutable [`Panel`].
pub fn panel_mut(&mut self) -> &mut dyn Panel {
match self {
TabPanel::Whole(p) => &mut **p,
TabPanel::Split(p) => &mut **p,
TabPanel::Leaves(p) => &mut **p,
}
}
/// The decomposable pane, when this tab is one.
fn split_mut(&mut self) -> Option<&mut dyn SplitPanel> {
match self {
TabPanel::Split(p) => Some(&mut **p),
TabPanel::Whole(_) | TabPanel::Leaves(_) => None,
}
}
/// The N-leaf pane, when this tab is one.
fn leaves_mut(&mut self) -> Option<&mut dyn LeafPanel> {
match self {
TabPanel::Leaves(p) => Some(&mut **p),
TabPanel::Whole(_) | TabPanel::Split(_) => None,
}
}
/// The N-leaf pane, immutably — the presence question ([`LeafPanel::leaf_absent`])
/// is a read of the panel's domain facts, so the frame-path ask never needs `&mut`.
fn leaves(&self) -> Option<&dyn LeafPanel> {
match self {
TabPanel::Leaves(p) => Some(&**p),
TabPanel::Whole(_) | TabPanel::Split(_) => None,
}
}
}
impl From<Box<dyn Panel>> for TabPanel {
fn from(p: Box<dyn Panel>) -> Self {
TabPanel::Whole(p)
}
}
impl From<Box<dyn SplitPanel>> for TabPanel {
fn from(p: Box<dyn SplitPanel>) -> Self {
TabPanel::Split(p)
}
}
impl From<Box<dyn LeafPanel>> for TabPanel {
fn from(p: Box<dyn LeafPanel>) -> Self {
TabPanel::Leaves(p)
}
}
/// **The master/detail decomposition of ONE tab body** — the axis it divides along, the
/// master's seed share, and the two halves' titles + fold icons.
pub struct BodySplit {
/// Which way the body divides.
pub axis: Axis,
/// The MASTER's seed share (what the divider then moves).
pub ratio: f32,
/// `(title, fold icon)` of the master half.
pub main: (&'static str, &'static str),
/// `(title, fold icon)` of the inspector half.
pub inspector: (&'static str, &'static str),
}
/// The decomposition for a tab slug, or **`None` for a body that is honestly one pane**.
///
/// Forcing a split on a body with no master/detail is worse than leaving it whole: it
/// buys a divider nobody can use and a chrome bar that only steals rows. Most dwarves
/// tabs are deliberately absent here for a second, structural reason: they are thin
/// wrappers whose body is `self.inner.ui(ui)` over a SHARED facett widget
/// (`facett_bench_view` · `facett_fleet_view` · `facett_saga_view` · `facett_binview` ·
/// `facett_coderoom` · `facett_matrix` …). Their master/detail lives INSIDE facett, so
/// dwarves cannot decompose them without the shared widget exposing its own halves —
/// see the facett patch note in this module's docs.
///
/// **Every slug listed here MUST be mounted as [`TabPanel::Split`]** — asserted in
/// [`TabbedView::new`], so a declared split with no `SplitPanel` behind it is a loud
/// failure rather than two panes reading "no widget for slot".
pub fn body_split(slug: &str) -> Option<BodySplit> {
match slug {
// The health BOARD is the master; the expanded badge SVG is the detail. The SVG
// used to be appended under the board in the same flow, so opening one pushed
// the board up and (with enough pipelines) off the top of the pane.
"status" => Some(BodySplit {
axis: Axis::Horizontal,
ratio: 0.52,
main: ("Status · board", "🚦"),
inspector: ("Status · badge", "🏷"),
}),
// The stage DAG is the master; the source selector + stage detail (which runa
// file, how many stages, where the per-stage light came from, the reload) is the
// inspector. That header WRAPPED — a long file name or a parse error added rows
// that came straight out of the graph.
"pipeline" => Some(BodySplit {
axis: Axis::Horizontal,
ratio: 0.70,
main: ("Pipeline · DAG", "🪈"),
inspector: ("Pipeline · source", "🔎"),
}),
// The three-graph convergence viewer is the master; the shooting-ramp band
// (Gates · Ramp · Bump plan) is the detail. The band's detail GROWS — a ramp line
// per member repo, a bump line per crate — and it sat ABOVE the graph, so every
// line it grew came straight out of the graph's height.
"release" => Some(BodySplit {
axis: Axis::Horizontal,
ratio: 0.62,
main: ("Release · convergence", "🚀"),
inspector: ("Release · ramp", "🎯"),
}),
// The jobs/zones tree is the master; the live-poll banner (refresh · live/paused ·
// job count · source) is its control strip. The banner sat ABOVE the tree and
// every row it grew came straight out of the tree's height.
"jera" => Some(BodySplit {
axis: Axis::Horizontal,
ratio: 0.76,
main: ("Jera · jobs", "🗂"),
inspector: ("Jera · feed", "📡"),
}),
_ => None,
}
}
/// **Every tab slug whose body decomposes into N NAMED LEAVES** — the roster
/// [`body_leaves`] answers for, and the one the slot host prefix-matches against so
/// dispatching a leaf costs no allocation on the frame path.
///
/// One entry today, and the list is here rather than inlined so adding a second N-leaf
/// body is one line in one place. **Every slug listed here MUST be mounted as
/// [`TabPanel::Leaves`]** — asserted in [`TabbedView::new`], the same way a declared
/// `body_split` must have a real [`SplitPanel`] behind it.
pub const LEAF_BODIES: [&str; 1] = [crate::build_thing_scene::TAB];
/// The declared pane subtree for a tab whose body is N NAMED LEAVES, or `None` for a
/// body that is one pane or a master/detail pair.
///
/// The tree is the tab's OWN module's — `build_thing_scene::body()` for ⚒ — not a shape
/// spelled here: the leaf names, the axes, the seed ratios and the phone fallback are
/// decisions about that tab's content, and a second copy of them in the shell is a
/// second thing that can disagree with the first. The returned pane's `name` IS the tab
/// slug, so it lands in the workspace `Tabs` as the tab itself.
#[must_use]
pub fn body_leaves(slug: &str) -> Option<Pane> {
match slug {
crate::build_thing_scene::TAB => Some(crate::build_thing_scene::body()),
_ => None,
}
}
/// The leaf pane names of an N-leaf body, in reading order — walked off the DECLARED
/// tree rather than listed, so a leaf added to the design appears here with no edit.
#[must_use]
pub fn body_leaf_names(slug: &str) -> Vec<String> {
fn walk(p: &Pane, out: &mut Vec<String>) {
match &p.content {
scene::Content::Children(kids) => {
for k in kids {
walk(k, out);
}
}
_ => out.push(p.name.clone()),
}
}
let mut out = Vec::new();
if let Some(p) = body_leaves(slug) {
walk(&p, &mut out);
}
out
}
/// Which N-leaf body a slot id belongs to, if any — `build-thing-kernel` → `build-thing`.
///
/// A prefix test rather than a roster walk because this runs on the FRAME path, once per
/// leaf per frame: building every declared subtree to compare names would allocate five
/// pane trees a frame to answer a question about a string. Slugs are unique and every
/// leaf is `<slug>-<something>`, so the prefix is decisive. A `<slug>-…` id that is not a
/// declared leaf still reaches the owning panel, which names it — see [`LeafPanel`].
fn leaf_owner(id: &str) -> Option<&'static str> {
LEAF_BODIES
.iter()
.copied()
.find(|s| id.len() > s.len() && id.starts_with(s) && id.as_bytes()[s.len()] == b'-')
}
/// The pane name of a decomposed body's MASTER half.
pub fn main_pane(slug: &str) -> String {
format!("{slug}-main")
}
/// The pane name of a decomposed body's INSPECTOR half.
pub fn inspector_pane(slug: &str) -> String {
format!("{slug}-inspector")
}
/// The number of tabs that can carry a real keyboard accelerator: `Ctrl+1`…`Ctrl+9`.
///
/// **`Ctrl+0` is deliberately NOT offered** — egui reserves it (with `Ctrl++` /
/// `Ctrl+-`) for zoom reset, so a tenth entry advertising it would be a menu row whose
/// key resets the zoom instead of switching the tab. Nine real keys beat ten hints.
pub const GO_ACCELERATORS: usize = 9;
/// The keyboard accelerator advertised — and genuinely BOUND, in [`TabbedView`]'s draw —
/// for the Go-menu entry of the tab at strip index `i`: `Ctrl+1`…`Ctrl+9`, then **`None`**.
///
/// The old formula was `Ctrl+{i + 1}` for every tab, so the shell advertised `Ctrl+13`,
/// `Ctrl+17`, `Ctrl+20` … — keys that exist on no keyboard — and, worse, NOTHING was
/// bound at all: `facett_menu::Command::shortcut` is a display HINT and no key handler
/// ever read it. Twenty entries each promising an accelerator; twenty that did nothing.
///
/// Past the digit row the honest answer is "no accelerator" (the Go menu still lists
/// every tab, one click away, at any window width).
fn go_accelerator(i: usize) -> Option<String> {
(i < GO_ACCELERATORS).then(|| format!("Ctrl+{}", i + 1))
}
/// The digit key accelerator index `i` maps to (`0..=8` → `Num1`…`Num9`).
fn go_key(i: usize) -> Option<egui::Key> {
Some(match i {
0 => egui::Key::Num1,
1 => egui::Key::Num2,
2 => egui::Key::Num3,
3 => egui::Key::Num4,
4 => egui::Key::Num5,
5 => egui::Key::Num6,
6 => egui::Key::Num7,
7 => egui::Key::Num8,
8 => egui::Key::Num9,
_ => return None,
})
}
/// The first tab's pane id (route target) — falls back to the workspace pane.
fn first_tab_pane(labels: &[String]) -> String {
labels
.first()
.map(|l| slug(l))
.unwrap_or_else(|| BODY.to_string())
}
/// Build dwarves' tabbed shell as a declarative [`Scene`] from the tab labels — the
/// **Describe** step of the facett-scene adoption recipe. Each tab becomes a FOLDABLE
/// `Slot` leaf (id = the lowercased label, the SAME key the shell folds each pane's
/// state under). The root is a DRAGGABLE horizontal master/detail split of a foldable
/// **drawer** rail (the warehouse selector + About) over a `Tabs` workspace, collapsing
/// to `Tabs` on a phone-class width. A `View`/`Go`/`Help` menubar (shortcut hints) and a
/// few declared `Router` routes complete it.
///
/// ```text
/// shell ─ Split(H, 0.20, draggable) [phone → Tabs under 760pt]
/// ├─ rail (Foldable 🗄, drawer@760) slot:rail — warehouse selector + About; phone drawer
/// └─ body ─ Tabs(active 0) [the workspace]
/// ├─ warehouse (Foldable 🗄) slot:warehouse
/// ├─ forge (Foldable 🔨) slot:forge
/// └─ …one foldable Slot per existing tab panel…
/// ```
/// `pub` so the `tests/robot_scene.rs` integration suite can build the exact shipped
/// scene value and drive it through a real `egui_kittest` render (drag / drawer / fold).
pub fn dwarves_scene(tab_labels: &[String]) -> Scene {
let tabs: Vec<Pane> = tab_labels
.iter()
.map(|label| {
let id = slug(label);
let icon = tab_icon(label);
// A body that decomposes into N NAMED LEAVES brings its OWN declared
// subtree (axes, seed ratios, per-leaf titles + fold icons, phone
// fallback) — the shell only re-stamps the tab's roster label and glyph
// on the root, so the ONE place a tab label lives stays `DWARVES_TABS`.
if let Some(p) = body_leaves(&id) {
debug_assert_eq!(
p.name, id,
"an N-leaf body's root pane IS the tab slug — it is what the \
workspace `Tabs` layout and every deep link address"
);
return p.title(label.clone()).foldable_icon(true, icon);
}
match body_split(&id) {
// A body that DECOMPOSES: a draggable master/detail split of two named,
// chromed, foldable halves. The pane keeps the tab's own id + label, so
// it is still the `TabView` header and the handle every existing fold /
// deep-link / robot cell addresses.
Some(b) => Pane::split(
id.clone(),
b.axis,
b.ratio,
vec![
Pane::slot(main_pane(&id), main_pane(&id))
.title(b.main.0)
.foldable_icon(true, b.main.1),
Pane::slot(inspector_pane(&id), inspector_pane(&id))
.title(b.inspector.0)
.foldable_icon(true, b.inspector.1),
],
)
.title(label.clone())
.foldable_icon(true, icon),
None => Pane::slot(id.clone(), id)
.title(label.clone())
.foldable_icon(true, icon),
}
})
.collect();
let body = Pane::tabs(BODY, 0, tabs).title("Workspace");
// The left RAIL: the warehouse selector + About — a foldable pane that also acts as a
// DRAWER (auto-folds crossing into phone-class, restores above it).
// NB the title is "Warehouses", PLURAL, and deliberately not "Warehouse": the shell
// also mounts a *tab* called Warehouse, and both now draw a chrome title bar carrying
// their label. Two nodes reading `🗄 Warehouse` make BOTH un-addressable to a robot
// (and ambiguous to a screen reader). The plural is also the truer name — the rail
// lists every registered warehouse, one of them selected.
let rail = Pane::slot(RAIL, "rail")
.title("Warehouses")
.foldable_icon(true, "🗄")
.drawer(760.0);
// The shell root: a DRAGGABLE horizontal master/detail split; phone-class → Tabs.
let root = Pane::split(ROOT, Axis::Horizontal, 0.20, vec![rail, body])
.title("dwarves")
.phone(Layout::Tabs { active: 1 }, 760.0);
// The menubar: View (fold toggles) · Go (switch tab) · Help (About) — shortcut hints.
let view_items: Vec<MenuItem> = tab_labels
.iter()
.map(|label| {
let id = slug(label);
MenuItem::new(format!("view.{id}"), format!("Toggle {label}"))
.action(Action::Toggle(id))
})
.collect();
// **Go — the width-INDEPENDENT way to every tab.** Originally written as a
// workaround for a `TabView` strip that neither wrapped nor scrolled; that was fixed
// upstream in facett `cdb3cb63` (2026-07-22, `tabview.rs:87`) and the strip has
// scrolled ever since — see the module docs, where the claim outlived the defect by a
// month.
//
// The menu STAYS, and not out of inertia: a scrolling strip keeps every tab
// reachable but not every tab VISIBLE, so with 24 tabs finding one still means
// dragging a strip and reading as you go. This lists all of them at once, sorted, one
// click away, with a real accelerator for the first ten. That is a better affordance
// than the strip, not a substitute for a broken one — which is why it should not be
// removed now that its original justification is gone.
let go_items: Vec<MenuItem> = tab_labels
.iter()
.enumerate()
.map(|(i, label)| {
let id = slug(label);
let item = MenuItem::new(format!("go.{id}"), label.clone())
.action(Action::Command(format!("go:{id}")));
match go_accelerator(i) {
Some(keys) => item.shortcut(keys),
// Past the digit row there is no accelerator to give. Better a menu entry
// with none than one that lies (see `go_accelerator`).
None => item,
}
})
.collect();
// **The Mímir menu — every code-graph / quality operation in ONE group.**
// The owner: "alla operationer för att hitta dubbletter och liknande skall ligga
// under Mimir-meny, nu ligger de utspridda." The dedup (SimilarCode), dead-code
// (Zombie), risk, weak-test, bench-gap, generic-cost, the new purity (Villa) and
// the Brokkr quality lenses were reachable only by opening the Lenses tab and
// hunting its in-pane selector. This menu gathers them under one name: each item
// fires `mimir:<view>`, which jumps to the Lenses tab and selects that lens — the
// single source is `mimir_lens::MIMIR_MENU`, shared with the in-pane selector so
// the two surfaces cannot drift.
let mimir_items: Vec<MenuItem> = crate::mimir_lens::MIMIR_MENU
.iter()
.map(|(view, label)| {
MenuItem::new(format!("mimir.{view}"), (*label).to_string())
.action(Action::Command(format!("mimir:{view}")))
})
.collect();
Scene::new("dwarves", root)
.menu(MenuItem::new("view", "View").items(view_items))
.menu(MenuItem::new("go", "Go").items(go_items))
.menu(MenuItem::new("mimir", "Mímir").items(mimir_items))
.menu(MenuItem::new("help", "Help").items(vec![
MenuItem::new("help.about", "About dwarves")
.shortcut("F1")
.action(Action::Command("help.about".into())),
]))
// Declared routes over the shared Router (button / shortcut / event triggers) —
// robot-fireable via `NavState::fire`, observable in `state_json`.
.route(Route::on_button(
"focus-warehouse",
Target::Focus(first_tab_pane(tab_labels)),
))
.route(Route::on_shortcut(
"Ctrl+Home",
Target::Show(first_tab_pane(tab_labels)),
))
.route(Route::on_event("toggle-rail", Target::Toggle(RAIL.into())))
}
/// The egui temp-data key a pane's tab request rides on (see [`request_tab`]).
pub const GOTO_TAB: &str = "dwarves.goto_tab";
/// **Ask the shell to open the tab `slug`** (its [`DWARVES_TABS`](crate::DWARVES_TABS)
/// key) — how a pane links to another tab's full view. The shell switches after the
/// frame; an unknown slug does nothing. A link that carries a selection uses
/// [`request_tab_with`].
pub fn request_tab(ctx: &egui::Context, slug: &str) {
request_tab_with(ctx, slug, Value::Null);
}
/// **Open the tab `slug` ON a selection**: after the switch the shell hands `select`
/// to that tab's own `update_json` — the same door a robot or a user selection takes
/// there (`{"artifact":"tunnr","resolve":true}` on ⚒ Build Thing, `{"run_id":…}` on
/// Forge, …). `Null` = just open it.
pub fn request_tab_with(ctx: &egui::Context, slug: &str, select: Value) {
let ask = (
slug.to_string(),
if select.is_null() {
String::new()
} else {
select.to_string()
},
);
ctx.data_mut(|d| d.insert_temp(egui::Id::new(GOTO_TAB), ask));
}
/// A minimal tabbed host over N `Panel`s. Renders a selectable-label tab bar, then
/// the active pane. State + input route to the active pane (or the whole shell).
///
/// Optionally carries a **warehouse selector** (nornir parity: many registered, ONE
/// selected — [`with_warehouse_selector`](Self::with_warehouse_selector)) rendered in
/// a header above the tabs; changing the selection re-binds EVERY tab (broadcasts
/// `{"set_workspace":…}`), so the funnel + warehouse-bound panes fetch the SELECTED
/// warehouse. And an **About popup** ([`with_about`](Self::with_about)) — the shared
/// `facett-about` card behind an ℹ button.
pub struct TabbedView {
title: String,
tabs: Vec<(String, TabPanel)>,
/// The warehouse selector (rail widget); `None` = no selector (bare shell/tests).
selector: Option<WarehouseSelector>,
/// The About identity (data for the shared `facett-about` card); `None` = no About.
about_id: Option<facett_about::Identity>,
/// Whether the About popup is open.
about_open: bool,
/// The decoded About card (logo texture), built lazily on the first frame (needs
/// an egui `Context`), so `new` stays host-agnostic + headless-constructible.
about_card: Option<facett_about::AboutCard>,
/// The declarative shell (menubar + foldable pane tree + routes) — the DATA the
/// renderer walks. Built from the tab labels in [`new`](Self::new).
scene: Scene,
/// The scene's runtime nav state (per-pane fold / active-tab / split-ratio + Router).
nav: NavState,
/// Per N-leaf tab: `(slug, its declared leaf names)` — cached at mount because the
/// presence sync runs every frame, and walking [`body_leaves`]'s freshly built pane
/// tree per frame would allocate a tree to answer a question about a string (the
/// same reasoning as [`leaf_owner`]'s prefix test).
leaf_roster: Vec<(String, Vec<String>)>,
/// The last `scene::render`'s observable facts (device class, slot states, pane rects),
/// folded into [`state_json`](Facet::state_json) — the **Observe** step.
last_render: Value,
/// The menu AREAS ([`with_areas`](Self::with_areas)): the rail lists them and the
/// tab strip shows only the active area's tabs. Empty = one flat strip.
areas: Vec<Area>,
/// Per area, the tab index it was last on — clicking an area returns there.
area_last: Vec<usize>,
}
/// One menu AREA: its rail label and the slugs of the tabs it holds.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Area {
pub label: String,
pub tabs: Vec<String>,
}
/// The area holding tab `slug`, if any.
fn area_of(areas: &[Area], slug: &str) -> Option<usize> {
areas.iter().position(|a| a.tabs.iter().any(|t| t == slug))
}
impl TabbedView {
/// Build the shell from `(label, panel)` pairs; the first tab is active. No
/// selector / About until [`with_warehouse_selector`](Self::with_warehouse_selector)
/// / [`with_about`](Self::with_about) is chained. The declarative [`Scene`] is derived
/// from the tab labels (one foldable `Slot` per panel) + its [`NavState`] seeded.
pub fn new<P: Into<TabPanel>>(
title: impl Into<String>,
tabs: impl IntoIterator<Item = (String, P)>,
) -> Self {
let tabs: Vec<(String, TabPanel)> = tabs.into_iter().map(|(n, p)| (n, p.into())).collect();
let labels: Vec<String> = tabs.iter().map(|(n, _)| n.clone()).collect();
let scene = dwarves_scene(&labels);
// Fail loudly in debug if the addressability invariant is ever broken.
debug_assert!(
scene.validate().is_ok(),
"scene names must be unique: {:?}",
scene.validate()
);
// **A declared split MUST have a `SplitPanel` behind it.** Otherwise the scene
// grows two panes whose slots no host can fill and the tab silently renders "no
// widget for slot" twice — a decomposition that looks declared and draws nothing.
for (label, panel) in &tabs {
let id = slug(label);
assert!(
body_split(&id).is_none() || matches!(panel, TabPanel::Split(_)),
"tab `{label}` declares a master/detail body_split but is mounted as a \
whole Panel — mount it as `Box<dyn SplitPanel>`"
);
// …and the same for the THIRD shape. `build_thing_scene::body()` spent its
// whole life with no caller because nothing asserted this: the tree was
// declared, the tab was mounted `Whole`, and the two facts never met.
assert!(
body_leaves(&id).is_none() || matches!(panel, TabPanel::Leaves(_)),
"tab `{label}` declares an N-leaf body but is not mounted as one — mount \
it as `Box<dyn LeafPanel>`"
);
// ONE SHAPE PER BODY. A tab claiming both would grow `<slug>-main` /
// `<slug>-inspector` panes beside its leaves, and the two would fight over
// the same tab slug.
assert!(
body_leaves(&id).is_none() || body_split(&id).is_none(),
"tab `{label}` declares BOTH a master/detail body_split and an N-leaf \
body — a body has exactly one shape"
);
assert!(
!LEAF_BODIES.contains(&id.as_str()) || body_leaves(&id).is_some(),
"tab `{label}` is listed in LEAF_BODIES but `body_leaves` gives it no tree"
);
}
let nav = NavState::new(&scene);
let leaf_roster: Vec<(String, Vec<String>)> = tabs
.iter()
.filter_map(|(label, panel)| {
let id = slug(label);
(matches!(panel, TabPanel::Leaves(_)) && body_leaves(&id).is_some())
.then(|| (id.clone(), body_leaf_names(&id)))
})
.collect();
Self {
title: title.into(),
tabs,
selector: None,
about_id: None,
about_open: false,
about_card: None,
scene,
nav,
leaf_roster,
last_render: Value::Null,
areas: Vec::new(),
area_last: Vec::new(),
}
}
/// Group the tabs into AREAS: `(rail label, tab slugs)`. The rail lists the areas;
/// the strip shows the active area's tabs; the active area is always the one
/// holding the active tab, so every way of reaching a tab (Go menu, Ctrl+N,
/// `set_tab`) brings its area along. Every tab must be in exactly one area.
pub fn with_areas(mut self, areas: impl IntoIterator<Item = (String, Vec<String>)>) -> Self {
let areas: Vec<Area> = areas
.into_iter()
.map(|(label, tabs)| Area { label, tabs })
.collect();
for (n, _) in &self.tabs {
let id = slug(n);
let holders = areas.iter().filter(|a| a.tabs.contains(&id)).count();
assert_eq!(
holders, 1,
"tab `{n}` must sit in exactly one area, not {holders}"
);
}
let known: Vec<String> = self.tabs.iter().map(|(n, _)| slug(n)).collect();
for a in &areas {
assert!(!a.tabs.is_empty(), "area `{}` holds no tab", a.label);
for t in &a.tabs {
assert!(
known.contains(t),
"area `{}` names `{t}`, which is no tab",
a.label
);
}
}
self.area_last = areas
.iter()
.map(|a| known.iter().position(|k| *k == a.tabs[0]).unwrap_or(0))
.collect();
self.areas = areas;
self
}
/// The active area's label (`""` without areas).
pub fn active_area(&self) -> &str {
let active = self
.tabs
.get(self.active_index())
.map(|(n, _)| slug(n))
.unwrap_or_default();
area_of(&self.areas, &active)
.map(|i| self.areas[i].label.as_str())
.unwrap_or("")
}
/// Switch to `area` (its label, or the label without its icon, any case): back to
/// the tab it was last on. `false` if there is no such area.
pub fn select_area(&mut self, area: &str) -> bool {
let want = area.trim().to_lowercase();
let Some(i) = self.areas.iter().position(|a| {
let l = a.label.to_lowercase();
l == want || l.split_once(' ').is_some_and(|(_, name)| name == want)
}) else {
return false;
};
self.nav.set_active_tab(BODY, self.area_last[i]);
true
}
/// The active tab index (the `body` Tabs pane's live active child, clamped).
fn active_index(&self) -> usize {
self.nav
.active_tab(BODY)
.min(self.tabs.len().saturating_sub(1))
}
/// The scene, as canonical (byte-stable) RON — the on-disk authoring form.
/// (Robot/test-facing surface; unreferenced by the non-test binary.)
#[allow(dead_code)]
pub fn to_ron(&self) -> String {
self.scene.to_ron()
}
/// Every FOLDABLE pane name (the rail + each content tab). Delegates to facett-app's
/// own [`Scene::foldable_panes`] — this used to re-walk the tree here, which is the
/// twin L5 forbids: the shared crate is the one place that knows what "foldable"
/// means.
pub fn foldable_panes(&self) -> Vec<String> {
self.scene.foldable_panes()
}
/// Every DRAWER pane name (auto-folds into a drawer on phone-class width).
pub fn drawer_panes(&self) -> Vec<String> {
self.scene
.pane_names()
.into_iter()
.filter(|n| {
self.scene
.pane(n)
.map(|p| p.responsive.drawer)
.unwrap_or(false)
})
.collect()
}
/// Every DRAGGABLE (resizable) split pane name — delegated to facett-app's own
/// [`Scene::draggable_splits`] (L5).
pub fn draggable_splits(&self) -> Vec<String> {
self.scene.draggable_splits()
}
/// Every pane that draws a **window-chrome title bar** (fold · minimise · maximise).
/// facett-app puts chrome on every pane but the scene root by default, so this is the
/// whole shell minus `shell` — the affordance roster a robot drives.
pub fn chrome_panes(&self) -> Vec<String> {
self.scene.chrome_panes()
}
/// Fold / unfold a pane by name (the robot-addressable toggle). Returns the new state.
/// (Also reachable over the JSON seam via `{"toggle_fold":"<pane>"}`.)
pub fn toggle_fold(&mut self, pane: &str) -> bool {
self.nav.toggle_fold(pane)
}
/// Whether `pane` is currently folded.
#[allow(dead_code)]
pub fn is_folded(&self, pane: &str) -> bool {
self.nav.is_folded(pane)
}
/// **Minimise / un-minimise** a pane — collapse it to its chrome title bar and hand
/// its space back to its siblings. The same toggle the bar's `−` control runs.
/// Returns the new state. (JSON seam: `{"toggle_minimize":"<pane>"}`.)
pub fn toggle_minimize(&mut self, pane: &str) -> bool {
self.nav.toggle_minimize(pane)
}
/// **Maximise / un-maximise** a pane — give it the whole shell area, shrinking every
/// non-ancestor sibling to zero. At most one pane holds it. Returns the new state.
/// (JSON seam: `{"toggle_maximize":"<pane>"}`.)
pub fn toggle_maximize(&mut self, pane: &str) -> bool {
self.nav.toggle_maximize(pane)
}
/// **Restore** a pane: drop its minimise AND its maximise. The split ratios were never
/// overwritten, so the layout returns exactly as it was.
/// (JSON seam: `{"restore":"<pane>"}`.)
pub fn restore_pane(&mut self, pane: &str) -> bool {
self.nav.restore(pane)
}
/// **Reset the layout** — clear every minimise and the maximise in one call.
/// (JSON seam: `{"restore_all":true}`.)
pub fn restore_layout(&mut self) -> bool {
self.nav.restore_all()
}
/// Attach a warehouse selector. If it already has a selection, EVERY tab is bound
/// to it immediately (the funnel fetches that warehouse from the first frame).
pub fn with_warehouse_selector(mut self, selector: WarehouseSelector) -> Self {
if selector.has_selection() {
let ws = selector.selected().to_string();
self.broadcast_set_workspace(&ws);
}
self.selector = Some(selector);
self
}
/// Attach the About popup identity (the shared `facett-about` card renders it).
pub fn with_about(mut self, id: facett_about::Identity) -> Self {
self.about_id = Some(id);
self
}
/// Attach the About popup AND open it on the first frame (task #31 — dwarves
/// About-at-startup). Same shared `facett-about` card as [`with_about`], just shown
/// immediately (the user dismisses it with ✕ / Close, or re-opens via the ℹ button).
pub fn with_about_at_startup(mut self, id: facett_about::Identity) -> Self {
self.about_id = Some(id);
self.about_open = true;
self
}
/// The active tab label (for tests / introspection) — the `body` Tabs pane's live
/// active child, read from [`NavState`].
pub fn active_tab(&self) -> &str {
self.tabs
.get(self.active_index())
.map(|(n, _)| n.as_str())
.unwrap_or("")
}
/// Each body-tab header's last-drawn on-screen rect `[x, y, w, h]`, keyed by the tab's
/// VISIBLE label (e.g. `"Warehouse"`, `"CI/CD"`) — the geometry a **visible-window**
/// robot travels to so it can CLICK a tab by name. Read from facett-app's scene render
/// (`tab_rects`, keyed by pane slug) and mapped back to the label via [`slug`]. Empty
/// until the shell has drawn at least once (`last_render` is seeded on the first frame).
pub fn tab_label_rects(&self) -> std::collections::BTreeMap<String, [f32; 4]> {
let mut out = std::collections::BTreeMap::new();
let Some(rects) = self
.last_render
.get("tab_rects")
.and_then(|v| v.as_object())
else {
return out;
};
for (label, _) in &self.tabs {
if let Some(arr) = rects.get(&slug(label)).and_then(|v| v.as_array()) {
if arr.len() == 4 {
let f = |i: usize| arr[i].as_f64().unwrap_or(0.0) as f32;
out.insert(label.clone(), [f(0), f(1), f(2), f(3)]);
}
}
}
out
}
/// The selected warehouse (`""` = none / no selector) — the active-warehouse the
/// funnel + release bind to.
pub fn selected_warehouse(&self) -> &str {
self.selector.as_ref().map(|s| s.selected()).unwrap_or("")
}
/// **Re-bind every tab to `workspace`** — the broadcast nornir's `switch_workspace`
/// does (it calls `set_workspace` on every tab). Each pane's `update_json` picks up
/// `{"set_workspace":…}` (the funnel re-fetches; panes that don't understand it
/// ignore it — never a panic).
fn broadcast_set_workspace(&mut self, workspace: &str) {
let msg = serde_json::json!({ "set_workspace": workspace }).to_string();
for (_, panel) in self.tabs.iter_mut() {
panel.panel_mut().update_json(&msg);
}
}
}
impl Facet for TabbedView {
fn title(&self) -> &str {
&self.title
}
fn ui(&mut self, ui: &mut egui::Ui) {
// ── RENDER the declarative scene through facett-app's own primitives ──────────
// Disjoint field borrows so the host holds the slot widgets (tabs / selector /
// About) while `render` reads the scene + mutates the nav state.
let Self {
title: _,
tabs,
selector,
about_id,
about_open,
about_card,
scene,
nav,
leaf_roster,
last_render,
areas,
area_last,
} = self;
// ── AREAS: the strip shows the active tab's area, nothing else ───────────────
// An absent tab has no header and no body (facett-app's Tabs layout), and the
// active index is never rewritten, so leaving an area and coming back is exact.
let active_area = if areas.is_empty() || tabs.is_empty() {
None
} else {
let active = nav.active_tab(BODY).min(tabs.len() - 1);
let area = area_of(areas, &slug(&tabs[active].0));
if let Some(a) = area {
area_last[a] = active;
for (n, _) in tabs.iter() {
let id = slug(n);
nav.set_absent(&id, !areas[a].tabs.contains(&id));
}
}
area
};
let mut picked_area: Option<usize> = None;
// ── PRESENCE, before layout: a leaf whose subject is absent must not EXIST ────
// Asked of the panel every frame (its own domain facts — for ⚒, whether the
// resolved recipe declares a `[[build.kernel]]`), and asserted into the scene's
// runtime presence, so the region — title, fold chrome, divider — never renders
// for it. This is the strict form of the kernel-leaf gate: content-gating left a
// hardmounted pane frame on screen before any recipe was resolved.
for (id, leaves) in leaf_roster.iter() {
if let Some(lp) = tabs
.iter()
.find(|(n, _)| &slug(n) == id)
.and_then(|(_, p)| p.leaves())
{
for leaf in leaves {
nav.set_absent(leaf, lp.leaf_absent(leaf));
}
}
}
let mut rebind: Option<String> = None;
let mut pending: Option<String> = None;
{
let mut host = Host {
tabs: &mut *tabs,
selector: &mut *selector,
about_id: &*about_id,
about_open: &mut *about_open,
about_card: &mut *about_card,
rebind: &mut rebind,
pending: &mut pending,
areas: &*areas,
active_area,
picked_area: &mut picked_area,
};
*last_render = scene::render(&*scene, &mut *nav, ui, &mut host);
}
// An area clicked on the rail: back to the tab that area was last on.
if let Some(a) = picked_area {
nav.set_active_tab(BODY, area_last[a]);
}
// **The advertised accelerators, actually BOUND.** `Ctrl+1`…`Ctrl+9` select the
// first nine tabs without touching the strip — the keyboard path that works no
// matter how far past the pane's right edge the headers laid out.
//
// …but NOT while a text widget owns the keyboard. `consume_key` takes the event
// away from every widget below it, so an unconditional loop here steals
// `Ctrl+1`…`Ctrl+9` out from under a focused `TextEdit` (stinky `kopiera-3`) —
// and the shell just gained a lot more of them, because every log/console block
// is now a real `CopyableBlock` text widget the operator can click into. A shell
// accelerator that fires while someone is typing is the shell reaching past the
// pane that has focus.
//
// `egui_wants_keyboard_input()` is egui's own answer to "does a text widget have
// focus" — the same predicate its built-in shortcuts defer to.
let n = tabs.len();
if !ui.ctx().egui_wants_keyboard_input() {
for i in 0..n.min(GO_ACCELERATORS) {
let Some(key) = go_key(i) else { continue };
if ui.input_mut(|inp| inp.consume_key(egui::Modifiers::COMMAND, key)) {
nav.set_active_tab(BODY, i);
}
}
}
// A pane asked to open another tab ([`request_tab`]: the 🧭 Explorer's links to
// the full views) — the same switch a `go:<tab>` menu command makes.
if let Some((t, select)) = ui
.ctx()
.data_mut(|d| d.remove_temp::<(String, String)>(egui::Id::new(GOTO_TAB)))
{
if let Some(i) = tabs.iter().position(|(n, _)| slug(n) == t) {
nav.set_active_tab(BODY, i);
if !select.is_empty() {
tabs[i].1.panel_mut().update_json(&select);
}
}
}
// A `go:<tab>` menu command switches the visible tab (the Router doesn't drive the
// Tabs active index — the host does, after the render).
if let Some(cmd) = pending.as_deref() {
if let Some(t) = cmd.strip_prefix("go:") {
if let Some(i) = tabs.iter().position(|(n, _)| slug(n) == t) {
nav.set_active_tab(BODY, i);
}
}
// A `mimir:<view>` menu command jumps to the Lenses tab AND selects that lens
// — the Mímir menu is a single door onto every code-graph/quality operation.
if let Some(view) = cmd.strip_prefix("mimir:") {
if let Some(i) = tabs.iter().position(|(n, _)| slug(n) == "lenses") {
nav.set_active_tab(BODY, i);
let msg = json!({ "view": view }).to_string();
tabs[i].1.panel_mut().update_json(&msg);
}
}
}
// A warehouse selection changed in the rail → re-bind EVERY tab to it (the same
// broadcast nornir's `switch_workspace` does).
if let Some(ws) = rebind {
let msg = json!({ "set_workspace": ws }).to_string();
for (_, panel) in tabs.iter_mut() {
panel.panel_mut().update_json(&msg);
}
}
}
fn state_json(&self) -> serde_json::Value {
let mut obj = serde_json::Map::new();
obj.insert("active_tab".into(), serde_json::json!(self.active_tab()));
if !self.areas.is_empty() {
obj.insert("active_area".into(), serde_json::json!(self.active_area()));
obj.insert(
"areas".into(),
serde_json::json!(self
.areas
.iter()
.map(|a| json!({ "label": a.label, "tabs": a.tabs }))
.collect::<Vec<_>>()),
);
}
obj.insert(
"tabs".into(),
serde_json::json!(self.tabs.iter().map(|(n, _)| n.clone()).collect::<Vec<_>>()),
);
// ── the declarative shell as observable data (the Observe step) ──────────────
obj.insert("scene".into(), serde_json::json!(self.scene.name));
obj.insert("panes".into(), serde_json::json!(self.scene.pane_names()));
obj.insert("foldable".into(), serde_json::json!(self.foldable_panes()));
obj.insert("drawers".into(), serde_json::json!(self.drawer_panes()));
obj.insert(
"draggable_splits".into(),
serde_json::json!(self.draggable_splits()),
);
// Every pane that draws a window title bar, plus facett-app's own adoption audit
// (the one blob naming every affordance this shell ships — asserting against IT
// is what makes "the chrome silently disappeared" a red test, not a shrug).
obj.insert("chrome".into(), serde_json::json!(self.chrome_panes()));
obj.insert("affordances".into(), self.scene.affordances());
obj.insert("folded".into(), serde_json::json!(self.nav.folded));
// The live WINDOW state: which panes are collapsed to their title bar, which one
// (at most) holds the whole shell, and the last chrome control that fired.
obj.insert("minimized".into(), serde_json::json!(self.nav.minimized));
obj.insert("maximized".into(), serde_json::json!(self.nav.maximized));
// The panes whose SUBJECT is absent this frame — they do not exist on screen
// (no region, no chrome, no divider), however much the declared tree names
// them. For ⚒ this is the kernel leaf whenever the resolved recipe declares no
// `[[build.kernel]]` — including the opening state, where nothing is resolved.
// Computed LIVE from the panels (the same ask the per-frame sync makes), not
// read back from `nav`, so the oracle is honest before the first render too.
let absent: Vec<&String> = self
.leaf_roster
.iter()
.filter_map(|(id, leaves)| {
let lp = self.tabs.iter().find(|(n, _)| &slug(n) == id)?.1.leaves()?;
Some(leaves.iter().filter(move |l| lp.leaf_absent(l)))
})
.flatten()
.collect();
obj.insert("absent".into(), serde_json::json!(absent));
obj.insert(
"last_chrome".into(),
serde_json::json!(self.nav.last_chrome),
);
obj.insert(
"split_ratio".into(),
serde_json::json!(self.nav.split_ratio),
);
// **The tab INTERNALS, as data.** Which tab bodies DECOMPOSED into a draggable
// master/detail split of `<slug>-main` | `<slug>-inspector`, which decomposed
// into N NAMED LEAVES, and which are honestly ONE pane. A robot reads this
// instead of hard-coding the decomposition, so a body split (or leaved, or
// un-split) later is swept without editing the suite.
//
// The three rosters PARTITION the strip — every tab is in exactly one, which is
// what `TabbedView::new`'s one-shape-per-body assertion buys.
let (mut split_bodies, mut leaf_bodies, mut single_bodies) =
(Vec::new(), Vec::new(), Vec::new());
let mut leaves = serde_json::Map::new();
for (n, _) in &self.tabs {
let id = slug(n);
if body_split(&id).is_some() {
split_bodies.push(id);
} else if body_leaves(&id).is_some() {
leaves.insert(id.clone(), serde_json::json!(body_leaf_names(&id)));
leaf_bodies.push(id);
} else {
single_bodies.push(id);
}
}
obj.insert("split_bodies".into(), serde_json::json!(split_bodies));
obj.insert("leaf_bodies".into(), serde_json::json!(leaf_bodies));
// …and, per leaved body, its leaf roster in reading order — the names a driver
// addresses, published rather than copied into every suite that walks them.
obj.insert("body_leaves".into(), Value::Object(leaves));
obj.insert(
"single_pane_bodies".into(),
serde_json::json!(single_bodies),
);
obj.insert("menus".into(), serde_json::json!(self.scene.menubar.len()));
obj.insert("routes".into(), serde_json::json!(self.scene.routes.len()));
obj.insert("render".into(), self.last_render.clone());
// The SELECTED warehouse (nornir parity) — top-level so a reader of
// `__dwarves_state()` finds the active warehouse the funnel + release bind to.
if let Some(sel) = &self.selector {
obj.insert(
"selected_warehouse".into(),
serde_json::json!(sel.selected()),
);
obj.insert("warehouses".into(), serde_json::json!(sel.warehouses()));
obj.insert("warehouse_select".into(), sel.state_json());
}
// The About popup oracle (shared facett-about identity + open flag).
if let Some(id) = &self.about_id {
obj.insert(
"about".into(),
facett_about::state_json(id, self.about_open),
);
}
// Fold each pane's own state under its (lower-cased) label — additive, so a
// reader of `__dwarves_state()["forge"]` finds the untouched Forge shape.
for (name, panel) in &self.tabs {
obj.insert(slug(name), panel.panel().state_json());
}
serde_json::Value::Object(obj)
}
fn update_json(&mut self, msg_json: &str) {
// `{"set_tab":"Funnel"}` switches tabs (case-insensitive, drives the scene's
// `body` Tabs active index) — its area comes along;
// `{"set_area":"Build"}` switches to an area, on the tab it was last on;
// `{"toggle_fold":"forge"}` folds/unfolds a pane by name (the scene fold seam);
// `{"toggle_minimize":"rail"}` / `{"toggle_maximize":"body"}` / `{"restore":"rail"}`
// / `{"restore_all":true}` drive the per-pane WINDOW CHROME headlessly — the
// exact state the title bar's controls mutate;
// `{"select_warehouse":"…"}` selects the active warehouse + re-binds every tab;
// `{"register_warehouse":"…"}` adds a warehouse to the registry;
// `{"about": true|false}` toggles the About popup;
// anything else is forwarded to the active pane's own JSON seam.
if let Ok(v) = serde_json::from_str::<serde_json::Value>(msg_json) {
if let Some(want) = v.get("set_area").and_then(|t| t.as_str()) {
if self.select_area(want) {
return;
}
}
if let Some(want) = v.get("set_tab").and_then(|t| t.as_str()) {
if let Some(i) = self
.tabs
.iter()
.position(|(n, _)| slug(n) == want || n.eq_ignore_ascii_case(want))
{
self.nav.set_active_tab(BODY, i);
return;
}
}
// ONE WRITER PER VERB (L5). These five used to call `self.nav.*` DIRECTLY,
// in parallel with the identically-named `pub fn` wrappers below — so the
// shell carried two independent paths to the same state, the wrappers had
// ZERO callers (five `#[allow(dead_code)]` claims that were true), and any
// behaviour added to a wrapper would silently NOT reach the seam a robot
// drives. The seam now goes THROUGH the wrapper, which is the only code that
// touches `self.nav` for these verbs, and the `#[allow(dead_code)]` came off
// with the last caller that made it necessary.
//
// What holds it is `the_json_seam_and_the_shell_methods_are_one_writer` in
// this file's tests, NOT the compiler: `dead_code` is only a warning here (no
// `deny`, nothing in .cargo/config.toml — checked 2026-08-22), so reverting
// one of these five lines to `self.nav.*` would compile and warn about
// exactly nothing. The test is the thing that can go red.
if let Some(pane) = v.get("toggle_fold").and_then(|t| t.as_str()) {
self.toggle_fold(pane);
return;
}
// ── window chrome, over the same JSON seam the fold already uses ──────────
if let Some(pane) = v.get("toggle_minimize").and_then(|t| t.as_str()) {
self.toggle_minimize(pane);
return;
}
if let Some(pane) = v.get("toggle_maximize").and_then(|t| t.as_str()) {
self.toggle_maximize(pane);
return;
}
if let Some(pane) = v.get("restore").and_then(|t| t.as_str()) {
self.restore_pane(pane);
return;
}
if v.get("restore_all")
.and_then(|t| t.as_bool())
.unwrap_or(false)
{
self.restore_layout();
return;
}
// Warehouse selection (nornir's switch_workspace, mirrored): select + broadcast.
if let Some(ws) = v.get("select_warehouse").and_then(|t| t.as_str()) {
let changed = self
.selector
.as_mut()
.map(|s| s.select(ws))
.unwrap_or(false);
if changed {
let selected = self.selected_warehouse().to_string();
self.broadcast_set_workspace(&selected);
}
return;
}
if let Some(ws) = v.get("register_warehouse").and_then(|t| t.as_str()) {
if let Some(s) = self.selector.as_mut() {
s.register(ws);
}
return;
}
if let Some(open) = v.get("about").and_then(|t| t.as_bool()) {
self.about_open = open;
return;
}
}
let active = self.active_index();
if let Some((_, panel)) = self.tabs.get_mut(active) {
panel.panel_mut().update_json(msg_json);
}
}
}
// ── the slot host: fills each `Content::Slot` by DRIVING an existing widget ─────
//
// `TabbedView`'s slots map to the SAME `Box<dyn Panel>` tabs (+ the rail's warehouse
// selector / About) — the host draws each and returns its `state_json`, folded into the
// scene's observable state. Nothing is forked: the scene DRIVES the existing components.
struct Host<'a> {
tabs: &'a mut Vec<(String, TabPanel)>,
selector: &'a mut Option<WarehouseSelector>,
about_id: &'a Option<facett_about::Identity>,
about_open: &'a mut bool,
about_card: &'a mut Option<facett_about::AboutCard>,
/// A warehouse selection made in the rail this frame (broadcast after the render).
rebind: &'a mut Option<String>,
/// A menu command fired this frame (applied after the render — e.g. `go:<tab>`).
pending: &'a mut Option<String>,
/// The menu areas the rail lists, and which one holds the active tab.
areas: &'a [Area],
active_area: Option<usize>,
/// An area clicked on the rail this frame (applied after the render).
picked_area: &'a mut Option<usize>,
}
/// Which half of a decomposed body a `<slug>-<half>` slot id addresses.
enum Half {
Main,
Inspector,
}
impl SceneHost for Host<'_> {
fn slot(&mut self, id: &str, ui: &mut egui::Ui) -> Value {
if id == "rail" {
// The rail: the menu AREAS, then the warehouse selector (nornir parity) +
// the About affordance.
let mut st = serde_json::Map::new();
if !self.areas.is_empty() {
for (i, a) in self.areas.iter().enumerate() {
let on = self.active_area == Some(i);
let label = egui::RichText::new(&a.label).size(15.0).strong();
if ui
.selectable_label(on, label)
.on_hover_text(format!("{} tab(s)", a.tabs.len()))
.clicked()
{
*self.picked_area = Some(i);
}
}
ui.separator();
st.insert(
"areas".into(),
json!(self
.areas
.iter()
.map(|a| a.label.clone())
.collect::<Vec<_>>()),
);
}
if let Some(sel) = self.selector.as_mut() {
if let Some(new_ws) = sel.ui(ui) {
*self.rebind = Some(new_ws);
}
st.insert("warehouse_select".into(), sel.state_json());
}
if let Some(idm) = self.about_id {
ui.separator();
if ui
.button("ℹ About")
.on_hover_text("about dwarves")
.clicked()
{
*self.about_open = true;
}
// Lazily decode the logo once (needs the egui Context — only here at render).
if self.about_card.is_none() {
*self.about_card = Some(facett_about::AboutCard::new(ui.ctx(), idm));
}
if let Some(card) = self.about_card.as_ref() {
card.show(ui.ctx(), self.about_open, idm);
}
st.insert(
"about".into(),
facett_about::state_json(idm, *self.about_open),
);
}
if st.is_empty() {
Value::Null
} else {
Value::Object(st)
}
} else if let Some((_, panel)) = self.tabs.iter_mut().find(|(n, _)| slug(n) == id) {
// A content tab: draw the EXISTING panel, return its untouched state.
panel.panel_mut().ui(ui);
panel.panel().state_json()
} else if let Some(owner) = leaf_owner(id) {
// ONE LEAF of a body that decomposes into N named leaves. The panel draws
// that leaf and returns its own observable state under the leaf's name; the
// tab's whole `state_json` is still folded under the tab slug by
// `TabbedView::state_json`, so every existing oracle reads the same object.
match self.tabs.iter_mut().find(|(n, _)| slug(n) == owner) {
Some((_, p)) => match p.leaves_mut() {
Some(lp) => lp.ui_leaf(id, ui),
None => {
ui.weak(format!("no leaf widget for slot: {id}"));
Value::Null
}
},
None => {
ui.weak(format!("no widget for slot: {id}"));
Value::Null
}
}
} else if let Some((base, half)) = id
.strip_suffix("-main")
.map(|b| (b, Half::Main))
.or_else(|| id.strip_suffix("-inspector").map(|b| (b, Half::Inspector)))
{
// One HALF of a decomposed body. The master returns the pane's own untouched
// `state_json` (so `__dwarves_state()["<slug>"]` keeps its exact shape and
// every existing oracle reads the same object); the inspector returns its own.
match self.tabs.iter_mut().find(|(n, _)| slug(n) == base) {
Some((_, p)) => match half {
Half::Main => match p.split_mut() {
Some(sp) => {
sp.ui_main(ui);
p.panel().state_json()
}
None => {
ui.weak(format!("no split widget for slot: {id}"));
Value::Null
}
},
Half::Inspector => match p.split_mut() {
Some(sp) => sp.ui_inspector(ui),
None => {
ui.weak(format!("no split widget for slot: {id}"));
Value::Null
}
},
},
None => {
ui.weak(format!("no widget for slot: {id}"));
Value::Null
}
}
} else {
ui.weak(format!("no widget for slot: {id}"));
Value::Null
}
}
fn command(&mut self, id: &str) {
match id {
"help.about" => *self.about_open = true,
_ if id.starts_with("wh:") => {
let name = &id[3..];
if let Some(s) = self.selector.as_mut() {
if s.select(name) {
*self.rebind = Some(s.selected().to_string());
}
}
}
// Everything else (notably `go:<tab>`) is applied by the shell after render.
_ => *self.pending = Some(id.to_string()),
}
}
}
// ── the Claude tab (host wrapper over facett-claude::ClaudePane) ──────────────
//
// `ClaudePane` is a pure facett-Elm leaf: its `Send` emits a `ClaudeEffect::Send`
// (data) that a HOST must run through a `ClaudeTransport`. `ClaudeTab` is that host
// — it owns the pane + an injected transport and pumps the effect loop, so the pane
// is fully drivable (Send actually produces a reply) as a plain `Box<dyn Panel>`.
//
// L5 (REUSE): the transport is NOT hand-rolled here. dwarves CONSUMES the shared
// `nornir-claude` edda leaf — the ONE Anthropic Messages-API client (offline stub +
// live HTTP transport + credential resolution). [`EddaBackend`] is the ~3-line newtype
// adapter documented in edda `.nornir/guide.md` §10: it maps `facett_claude::WireMsg`
// → `nornir_claude::WireMsg` and forwards to the leaf's byte-identical `send_wire`.
// (Previously dwarves carried a bespoke `OfflineClaudeTransport` twin — retired.)
use std::sync::Arc;
/// The offline stub's canned reply — carries the `offline demo` label the robot
/// scenario asserts on, and names the shared leaf it now rides.
/// (`allow(dead_code)`: unreferenced by the non-test binary when `claude-http` picks
/// the live path in `main`; still used by the default build + every robot test.)
#[allow(dead_code)]
const OFFLINE_DEMO_REPLY: &str =
"(offline demo — no live Claude wired) — via the shared nornir-claude leaf";
/// Adapter bridging dwarves' facett `ClaudeTransport` seam onto the shared
/// `nornir-claude` leaf transport. The pane speaks `facett_claude`; the leaf owns the
/// real send (offline stub or live HTTP + credential resolution) — this newtype is the
/// only glue (the guide §10 pattern). Both `WireMsg`s are structurally identical, so it
/// is a field-copy `.map` + a forward to the leaf's `send_wire`.
struct EddaBackend(Arc<dyn nornir_claude::ClaudeTransport>);
impl facett_claude::ClaudeTransport for EddaBackend {
fn send(&self, model: &str, messages: &[facett_claude::WireMsg]) -> Result<String, String> {
let wire: Vec<nornir_claude::WireMsg> = messages
.iter()
.map(|w| nornir_claude::WireMsg {
role: w.role.clone(),
content: w.content.clone(),
})
.collect();
self.0.send_wire(model, &wire)
}
}
/// Hosts a [`facett_claude::ClaudePane`] with an injected transport, running the
/// pane's `Send`-effect loop so it drives end-to-end (view→update→deliver→reply).
pub struct ClaudeTab {
pane: facett_claude::ClaudePane,
transport: Box<dyn facett_claude::ClaudeTransport>,
}
impl ClaudeTab {
/// Host the pane over any transport.
pub fn new(transport: Box<dyn facett_claude::ClaudeTransport>) -> Self {
Self {
pane: facett_claude::ClaudePane::new("Claude"),
transport,
}
}
/// The demo default: the shared leaf's OFFLINE stub transport
/// (`nornir_claude::OfflineTransport`) via the [`EddaBackend`] adapter — no live
/// key, no network, a clean `offline demo` labelled reply. Consumes the edda leaf
/// (L5), NOT a dwarves-local twin.
// `allow(dead_code)`: the default binary + every robot test use this; only the
// `claude-http` binary (which wires `live()` in `main`) leaves it unreferenced.
#[allow(dead_code)]
pub fn offline() -> Self {
Self::new(Box::new(EddaBackend(Arc::new(
nornir_claude::OfflineTransport::replying(OFFLINE_DEMO_REPLY),
))))
}
/// The LIVE tab: the shared leaf's HTTP transport (`nornir_claude::HttpTransport`)
/// via the same [`EddaBackend`] adapter. The leaf resolves the Anthropic credential
/// first-hit-wins at send time (`ANTHROPIC_API_KEY` x-api-key → `ANTHROPIC_AUTH_TOKEN`
/// Bearer → per-session token; never hardcoded) and POSTs `/v1/messages` — see
/// `.nornir/claude-token-path.md`. Behind dwarves-gui's `claude-http` feature,
/// which maps to the leaf's `http` feature. Native-only.
#[cfg(feature = "claude-http")]
pub fn live() -> Self {
Self::new(Box::new(EddaBackend(Arc::new(
nornir_claude::HttpTransport,
))))
}
/// Apply Elm messages and run any emitted `Send` effect through the transport,
/// feeding the reply back — the whole host loop the `impl_facet_via_elm!` macro
/// deliberately leaves to the host (it drops effects).
fn pump(&mut self, msgs: Vec<facett_claude::ClaudeMsg>) {
use facett_core::Elm;
for m in msgs {
for effect in self.pane.update(m) {
let back = facett_claude::ClaudePane::deliver(&effect, &*self.transport);
let _ = self.pane.update(back);
}
}
}
}
impl Facet for ClaudeTab {
fn title(&self) -> &str {
facett_core::Elm::title(&self.pane)
}
fn ui(&mut self, ui: &mut egui::Ui) {
let msgs = facett_core::Elm::view(&self.pane, ui);
self.pump(msgs);
}
fn state_json(&self) -> serde_json::Value {
Panel::state_json(&self.pane)
}
fn update_json(&mut self, msg_json: &str) {
// Parse a ClaudeMsg and pump it (so `"send"` runs the transport) — malformed
// input is ignored, never a panic.
if let Ok(msg) = serde_json::from_str::<facett_claude::ClaudeMsg>(msg_json) {
self.pump(vec![msg]);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::panel::ForgeView;
/// ROBOT SCENARIO (task #6 + #28 + #31) — the **Release graph pane** mounted in the
/// shell + the **About-at-startup** modal, driven over the remote seam. Asserts:
/// (1) the Release tab folds the reused `facett-releaseview` pane's state — the THREE
/// planes (LOCAL · SPARRING · CRATES_IO) + the red/amber/green convergence buckets of
/// the nordisk fixture; (2) the About popup is OPEN on the first frame (task #31) and
/// its shared `facett-about` oracle == dwarves' Identity (wordmark · embedded forge
/// logo). Emits a `nornir-testmatrix` cell (reuse edda).
#[test]
fn robot_release_pane_and_about_at_startup() {
// The SHIPPED Release tab (`ReleaseTab`, which wraps the reused
// `facett-releaseview` pane), not the bare `dwarves_embed::release_panel` this
// used to mount: the shell mounts the wrapper, the wrapper is the thing whose
// body DECOMPOSES, and a test that mounts something else under the same label is
// testing a pane the app does not ship. With no convergence file it falls back to
// the same nordisk fixture, so the assertions below are unchanged.
let release: Box<dyn SplitPanel> = Box::new(crate::release::ReleaseTab::new(String::new()));
let mut app = TabbedView::new(
"dwarves — Release",
vec![("Release".to_string(), TabPanel::Split(release))],
)
.with_about_at_startup(crate::about::identity());
// (task #31) About is OPEN at startup; the oracle carries dwarves' identity.
let st = Facet::state_json(&app);
let about_ok = st["about"]["open"] == true
&& st["about"]["wordmark"] == "dwarves"
&& st["about"]["has_logo"] == true;
// (task #6 + #28) The Release tab folds the 3-plane convergence pane.
Facet::update_json(&mut app, r#"{"set_tab":"Release"}"#);
let st = Facet::state_json(&app);
let pane = &st["release"];
let planes = pane["planes"].as_array().unwrap();
// ROWS OR A NAMED EMPTY — never neither, never both, and it must hold in BOTH
// worlds. This used to assert `node_count == 4` with two `needs_release`, one
// `bump_drift` and one `converged`: the nordisk fixture, deleted 2026-09-10.
//
// The obvious repair — assert the empty state instead — would be the same
// mistake pointing the other way. This test mounts the SHIPPED tab against the
// real constellation root and the real convergence path, so on a box that has
// actually run a release the file EXISTS and the pane correctly has rows. An
// assertion of emptiness is green only on a machine that never released, and
// goes red on the forge, which is a fabricated red.
//
// So the invariant is the disjunction, and it is stronger than either half: the
// pane either shows a convergence (rows, and `source` names which run produced
// it) or says by name that there is none (zero nodes, and the empty block naming
// the file it wanted). What it may never do is draw nodes it cannot source, or
// go blank without saying why.
let node_count = pane["node_count"].as_u64().unwrap_or(0);
let empty_block = &pane["convergence_empty"];
let has_rows = node_count > 0;
let named_empty = node_count == 0
&& empty_block["empty"] == true
&& !empty_block["feed"].as_str().unwrap_or("").is_empty()
&& pane["source"] == "empty"
&& pane["convergence_path"]
.as_str()
.is_some_and(|p| !p.is_empty());
// A pane showing rows must name their provenance, and it must NOT also be
// carrying an empty state — "both" is as broken as "neither".
let rows_are_sourced = !has_rows
|| (["workspace-run", "release-run"].contains(&pane["source"].as_str().unwrap_or(""))
&& empty_block.is_null());
let pane_ok = pane["component"] == "ReleaseView"
&& planes.len() == 3
&& planes.iter().any(|p| p == "local")
&& planes.iter().any(|p| p == "sparring")
&& planes.iter().any(|p| p == "cratesio")
&& (has_rows ^ named_empty)
&& rows_are_sourced;
// About dismisses over the seam (the ℹ button re-opens it).
Facet::update_json(&mut app, r#"{"about": false}"#);
let closed_ok = Facet::state_json(&app)["about"]["open"] == false;
let drivable = about_ok && pane_ok && closed_ok;
assert!(
drivable,
"the Release pane + About-at-startup drive over the remote seam: {st}"
);
#[cfg(feature = "testmatrix")]
nornir_testmatrix::functional_status(
"dwarves-gui",
"release_pane_and_about_at_startup",
drivable,
"Release graph pane (reused facett-releaseview 3-plane convergence: LOCAL·SPARRING·CRATES_IO, red/amber/green buckets) mounted in the shell + About modal OPEN at startup (shared facett-about oracle == dwarves Identity) — driven headless over state_json/update_json",
);
}
/// ROBOT SCENARIO — the **Warehouse Navigator** landing page + a drill-down step,
/// driven purely over the remote seam (`state_json`/`update_json`) through the
/// tabbed HOST, exactly as the shipped demo mounts it (the FIRST/landing tab).
/// Asserts: (1) the landing view IS the L0 constellation (one super-node per repo,
/// cross-repo edges, no hull, colour-by-repo palette present); (2) a drill-down
/// `{"drill":"repo:facett"}` forwarded through the shell descends to the repo's L1
/// view — its crates appear inside exactly ONE grouping hull. No pixels, self-
/// driving in code (the north star). Emits a `nornir-testmatrix` cell (reuse edda).
#[test]
fn robot_warehouse_landing_and_drilldown_matrix() {
let warehouse: Box<dyn Panel> = nornir_warehouse_nav::panel();
let forge: Box<dyn Panel> = Box::new(ForgeView::from_fixture());
let mut app = TabbedView::new(
"dwarves — Warehouse | Forge",
vec![("Warehouse".into(), warehouse), ("Forge".into(), forge)],
);
// The shell opens on the Warehouse (landing) tab — its state is folded under
// the lower-cased label. The landing view is the whole-warehouse constellation.
assert_eq!(
app.active_tab(),
"Warehouse",
"the landing tab is the Warehouse"
);
let landing = &Facet::state_json(&app)["warehouse"];
let landing_ok = landing["kind"] == "warehouse-nav"
&& landing["level"] == "constellation"
&& landing["repo_count"].as_u64().unwrap() >= 10
&& landing["crate_count"] == 0
&& landing["hull_count"] == 0
&& landing["edge_count"].as_u64().unwrap() >= 10
// colour-by-repo: a stable per-repo hex palette is exposed.
&& landing["palette"]["dwarves"].as_str().unwrap_or("").starts_with('#')
&& landing["palette"]["nornir"] != landing["palette"]["edda"];
// Drill down: forward `{"drill":…}` through the shell to the ACTIVE (Warehouse)
// pane — the SAME channel a chip click drives. Descend into the facett repo.
Facet::update_json(&mut app, r#"{"drill":"repo:facett"}"#);
let repo = Facet::state_json(&app);
let repo = &repo["warehouse"];
let drill_ok = repo["level"] == "repo"
&& repo["focus_repo"] == "facett"
&& repo["hull_count"] == 1 // the repo grouping hull
&& repo["crate_count"].as_u64().unwrap() >= 5 // facett's crates appear
&& repo["hulls"][0]["repo"] == "facett";
// Climb back to the landing constellation over the same seam.
Facet::update_json(&mut app, r#"{"up":true}"#);
let back_ok = Facet::state_json(&app)["warehouse"]["level"] == "constellation";
let drivable = landing_ok && drill_ok && back_ok;
assert!(
drivable,
"the Warehouse landing + drill-down drive over the remote seam: {}",
Facet::state_json(&app)
);
#[cfg(feature = "testmatrix")]
nornir_testmatrix::functional_status(
"dwarves-gui",
"warehouse_landing_and_drilldown_drivable",
drivable,
"Warehouse Navigator landing (L0 constellation, colour-by-repo) + click-to-drill-down (repo→L1 hull) driven headless over state_json/update_json through the tabbed host",
);
}
/// A headless tab-switch: the shell opens on Forge, switches to Funnel via the
/// JSON seam, and both panes' states are folded under their labels. The Funnel
/// pane points at a dead port, so it renders the clean `unavailable` state — the
/// switch itself never panics.
#[test]
fn tab_switch_routes_state_and_input() {
let forge: Box<dyn Panel> = Box::new(ForgeView::from_fixture());
let funnel: Box<dyn Panel> = nornir_funnel_embed::panel_with("http://127.0.0.1:0", "", "");
let mut app = TabbedView::new(
"dwarves — Forge | Funnel",
vec![("Forge".into(), forge), ("Funnel".into(), funnel)],
);
// Opens on Forge.
assert_eq!(app.active_tab(), "Forge");
let s = Facet::state_json(&app);
assert_eq!(s["active_tab"], "Forge");
assert_eq!(s["tabs"], serde_json::json!(["Forge", "Funnel"]));
// Both panes' states are present, folded under their labels (additive).
assert_eq!(
s["forge"]["view"], "roster",
"the untouched Forge shape survives"
);
assert_eq!(s["funnel"]["kind"], "funnel");
// Empty workspace ⇒ the DISTINCT "no_warehouse" state (nothing selected to
// fetch) — NOT the old "offline" lie, and no network touched.
assert_eq!(
s["funnel"]["status"], "no_warehouse",
"no warehouse selected → clean state, no panic"
);
// Switch to Funnel through the same JSON seam the robot harness uses.
Facet::update_json(&mut app, r#"{"set_tab":"Funnel"}"#);
assert_eq!(app.active_tab(), "Funnel");
assert_eq!(Facet::state_json(&app)["active_tab"], "Funnel");
// Switch back (case-insensitive), then a non-tab message forwards to the pane.
Facet::update_json(&mut app, r#"{"set_tab":"forge"}"#);
assert_eq!(app.active_tab(), "Forge");
Facet::update_json(&mut app, r#"{"set_view":"dag"}"#);
assert_eq!(
Facet::state_json(&app)["forge"]["view"],
"dag",
"input routes to the active pane"
);
}
/// ROBOT SCENARIO — drive the **Pipeline | Funnel** panes purely over the
/// remote seam (`state_json` out / `update_json` in), the same channel a
/// `nornir-robotui` scenario and the thin gRPC client use — NO pixels, self-
/// driving in code (the north-star: tests exercise the remote control, not a
/// human/AI). Emits a `nornir-testmatrix` cell (reuse edda) so coverage is
/// ledgered. The visible `watch`-on-DISPLAY:0 leg (egui_kittest render) is the
/// flagged follow-up; this headless leg is the matrix-side of wire-once.
#[test]
fn robot_pipeline_funnel_panes_matrix() {
let forge: Box<dyn Panel> = Box::new(ForgeView::from_fixture());
let funnel: Box<dyn Panel> = nornir_funnel_embed::panel_with("http://127.0.0.1:0", "", "");
let mut app = TabbedView::new(
"dwarves — Pipeline | Funnel",
vec![("Forge".into(), forge), ("Funnel".into(), funnel)],
);
// Drive: open on Pipeline(Forge) → switch to Funnel → back — asserting each
// pane answers over the seam (the remote-controllable contract).
let pipeline_ok = Facet::state_json(&app)["forge"]["view"] == "roster";
Facet::update_json(&mut app, r#"{"set_tab":"Funnel"}"#);
let funnel_ok =
app.active_tab() == "Funnel" && Facet::state_json(&app)["funnel"]["kind"] == "funnel";
Facet::update_json(&mut app, r#"{"set_tab":"Forge"}"#);
let back_ok = app.active_tab() == "Forge";
let drivable = pipeline_ok && funnel_ok && back_ok;
assert!(drivable, "both dwarves panes drive over the remote seam");
#[cfg(feature = "testmatrix")]
nornir_testmatrix::functional_status(
"dwarves-gui",
"pipeline_funnel_panes_drivable",
drivable,
"Pipeline|Funnel panes driven headless over state_json/update_json",
);
}
/// ROBOT SCENARIO — drive the **Funnel WRITE leg** through the shell: switch to
/// Funnel, "type" an idea into the submit strip, submit it, and assert the pane
/// answers over the seam (offline server → clean `error` outcome, draft preserved,
/// no panic). Same channel `nornir-robotui` and the thin gRPC client use — no
/// pixels, self-driving in code. Emits a `nornir-testmatrix` cell (reuse edda).
#[test]
fn robot_funnel_submit_leg_matrix() {
let forge: Box<dyn Panel> = Box::new(ForgeView::from_fixture());
let funnel: Box<dyn Panel> = nornir_funnel_embed::panel_with("http://127.0.0.1:0", "", "");
let mut app = TabbedView::new(
"dwarves — Pipeline | Funnel",
vec![("Forge".into(), forge), ("Funnel".into(), funnel)],
);
// Switch to Funnel, type an idea, submit — all through the host's JSON seam.
Facet::update_json(&mut app, r#"{"set_tab":"Funnel"}"#);
Facet::update_json(
&mut app,
r#"{"draft":"grow the plan-DAG","intake_kind":"idea"}"#,
);
let typed_ok = Facet::state_json(&app)["funnel"]["draft"] == "grow the plan-DAG";
Facet::update_json(&mut app, r#"{"submit":true}"#);
let st = Facet::state_json(&app);
// Offline stub server → clean error outcome, draft preserved, pane still live.
let submit_ok = st["funnel"]["submit_status"] == "error"
&& st["funnel"]["draft"] == "grow the plan-DAG"
&& st["funnel"]["kind"] == "funnel";
let drivable = typed_ok && submit_ok;
assert!(
drivable,
"the funnel submit strip drives over the remote seam, offline-safe"
);
#[cfg(feature = "testmatrix")]
nornir_testmatrix::functional_status(
"dwarves-gui",
"funnel_submit_leg_drivable",
drivable,
"Funnel WRITE leg (type idea → submit) driven headless over state_json/update_json; offline-safe",
);
}
/// ROBOT SCENARIO — the **Funnel growth-history trail** ("addera historik") over
/// the remote seam. The funnel pane's `state_json` now carries a `history` trail
/// (the DAG's node-count over successive polls) + `history_len`/`node_growth` — the
/// new UI surface under the live DAG. Points at a dead port (no server needed): the
/// trail is present-as-data and EMPTY (nothing polled), and the `{"clear_history"}`
/// control drives over the seam without panicking. Emits a `nornir-testmatrix` cell.
#[test]
fn robot_funnel_history_trail_over_the_seam() {
let funnel: Box<dyn Panel> = nornir_funnel_embed::panel_with("http://127.0.0.1:0", "", "");
let mut app = TabbedView::new("dwarves — Funnel", vec![("Funnel".into(), funnel)]);
Facet::update_json(&mut app, r#"{"set_tab":"Funnel"}"#);
let st = Facet::state_json(&app);
let pane = &st["funnel"];
// The history surface is present as data: an (empty, offline) trail + scalars.
let shape_ok = pane["history"].is_array()
&& pane["history"].as_array().unwrap().is_empty()
&& pane["history_len"] == 0
&& pane["history_peak_nodes"] == 0
&& pane["node_growth"] == 0;
// The clear-history control drives over the same seam a button click uses.
Facet::update_json(&mut app, r#"{"clear_history":true}"#);
let cleared_ok = Facet::state_json(&app)["funnel"]["history_len"] == 0;
let drivable = shape_ok && cleared_ok;
assert!(
drivable,
"the funnel growth-history trail drives over the remote seam: {st}"
);
#[cfg(feature = "testmatrix")]
nornir_testmatrix::functional_status(
"dwarves-gui",
"funnel_history_trail_drivable",
drivable,
"Funnel growth-history trail (addera historik) exposed + cleared headless over state_json/update_json",
);
}
/// ROBOT SCENARIO — the **fat-mode default** (the funnel-embedded fix). The shipped
/// funnel pane, with **NO remote nornir configured**, must default to EMBEDDED:
/// render its in-process Plan-DAG and report an embedded-mode state over the seam —
/// NEVER the old "no remote nornir"/unreachable error — and grow the DAG offline when
/// an idea is submitted. RED if the pane regresses to thin/remote-by-default. Emits a
/// `nornir-testmatrix` cell (reuse edda).
#[test]
fn robot_funnel_embedded_default_no_server() {
let funnel: Box<dyn Panel> = nornir_funnel_embed::panel_embedded_seeded("nordisk");
let mut app = TabbedView::new("dwarves — Funnel", vec![("Funnel".into(), funnel)]);
Facet::update_json(&mut app, r#"{"set_tab":"Funnel"}"#);
let st = Facet::state_json(&app);
let pane = &st["funnel"];
// Fat/embedded default: real DAG, embedded status, NO remote-failure error.
let embedded_ok = pane["kind"] == "funnel"
&& pane["mode"] == "embedded"
&& pane["node_count"].as_u64().unwrap_or(0) > 0
&& pane["status"]
.as_str()
.map_or(false, |s| s.starts_with("embedded"))
&& pane["error"].is_null();
assert!(embedded_ok, "funnel defaults to fat/embedded with no server (does NOT error 'no remote nornir'): {st}");
// The WRITE leg grows the in-process DAG offline (no server).
let before = Facet::state_json(&app)["funnel"]["node_count"]
.as_u64()
.unwrap_or(0);
Facet::update_json(
&mut app,
r#"{"submit_text":"a local offline idea","submit_kind":"idea"}"#,
);
let st2 = Facet::state_json(&app);
let grew = st2["funnel"]["submit_status"] == "ok"
&& st2["funnel"]["node_count"].as_u64().unwrap_or(0) > before;
assert!(
grew,
"the embedded funnel accepts a submit + grows the DAG with no server: {st2}"
);
let drivable = embedded_ok && grew;
#[cfg(feature = "testmatrix")]
nornir_testmatrix::functional_status(
"dwarves-gui",
"funnel_embedded_default_no_server",
drivable,
"Funnel pane defaults to FAT/embedded (no remote nornir): renders its in-process Plan-DAG + grows it on submit over state_json/update_json — never the 'no remote nornir'/unreachable error",
);
let _ = drivable;
}
/// ROBOT SCENARIO — the **Funnel RUN HISTORY + bottleneck** layer over the remote
/// seam. The funnel pane's `state_json` now also carries `runs`/`run_count`/
/// `selected_run` (the past-run picker) and `bottleneck`/`bottleneck_title`/
/// `critical_path` (the constraint hint consumed from facett-plandag). Points at a
/// dead port (no server): the run history is present-as-data and EMPTY, no run is
/// selected, and there's no bottleneck (empty view) — and the `{"select_run":null}`
/// + `{"clear_runs":true}` controls drive over the seam without panicking. (The rich
/// select-a-run-and-see-its-bottleneck path — which needs a built plan-DAG model — is
/// covered by `nornir-funnel-embed`'s unit + egui_kittest render tests.) Emits a
/// `nornir-testmatrix` cell.
#[test]
fn robot_funnel_run_history_and_bottleneck_over_the_seam() {
let funnel: Box<dyn Panel> = nornir_funnel_embed::panel_with("http://127.0.0.1:0", "", "");
let mut app = TabbedView::new("dwarves — Funnel", vec![("Funnel".into(), funnel)]);
Facet::update_json(&mut app, r#"{"set_tab":"Funnel"}"#);
let st = Facet::state_json(&app);
let pane = &st["funnel"];
// The run-history + bottleneck surface is present as data (empty, offline).
let shape_ok = pane["runs"].is_array()
&& pane["runs"].as_array().unwrap().is_empty()
&& pane["run_count"] == 0
&& pane["selected_run"].is_null()
&& pane["bottleneck"].is_null()
&& pane["bottleneck_title"].is_null();
// The run-picker controls drive over the same seam a click uses (no panic).
Facet::update_json(&mut app, r#"{"select_run": null}"#);
Facet::update_json(&mut app, r#"{"clear_runs": true}"#);
let after = Facet::state_json(&app);
let controls_ok =
after["funnel"]["run_count"] == 0 && after["funnel"]["selected_run"].is_null();
let drivable = shape_ok && controls_ok;
assert!(
drivable,
"the funnel run-history + bottleneck surface drives over the remote seam: {st}"
);
#[cfg(feature = "testmatrix")]
nornir_testmatrix::functional_status(
"dwarves-gui",
"funnel_run_history_bottleneck_drivable",
drivable,
"Funnel run-history picker + bottleneck hint (addera historik, next layer) exposed + driven headless over state_json/update_json",
);
}
/// ROBOT SCENARIO — the **Pipeline pane** (the build-pipeline DAG on the REUSED
/// `facett-pipeline` viewer) wired into the host alongside Forge, driven over the
/// remote seam: switch to it and assert its folded `state_json` shows the mapped
/// build DAG (5 nodes, all Done from a real live RunReport) — the `ForgePipelineAdapter`
/// wired end-to-end into the UI. Emits a `nornir-testmatrix` cell (reuse edda).
#[test]
fn robot_pipeline_dag_pane_wired_into_the_host() {
let forge: Box<dyn Panel> = Box::new(ForgeView::from_fixture());
// Pipeline is a `SplitPanel` (DAG │ source). The vec is built as `TabPanel`
// explicitly: a `vec![]` holding both a `Box<dyn Panel>` and a
// `Box<dyn SplitPanel>` upcasts the latter to unify the element type, silently
// mounting it whole.
let pipeline: Box<dyn SplitPanel> = Box::new(crate::pipeline::pipeline_view());
let mut app = TabbedView::new(
"dwarves — Forge | Pipeline",
vec![
("Forge".to_string(), TabPanel::Whole(forge)),
("Pipeline".to_string(), TabPanel::Split(pipeline)),
],
);
// Switch to the Pipeline tab over the same JSON seam the robot harness uses.
Facet::update_json(&mut app, r#"{"set_tab":"Pipeline"}"#);
assert_eq!(app.active_tab(), "Pipeline");
// The pipeline pane's state is folded under its label. The tab OPENS on the runa
// source — the forge's own authored `dwarves.runa.ron` (5 stages). The runs leg
// is one `{"pipeline_source":…}` message away and reads the forge. Both legs are
// asserted through the host, so the wiring proof covers the source toggle too.
let st = Facet::state_json(&app);
let p = &st["pipeline"];
let runa_wired = p["source"] == "runa"
&& p["pipeline_name"] == "dwarves-forge"
&& p["node_count"] == 5
&& p["edge_count"] == 5;
assert!(
runa_wired,
"the runa pipeline DAG is wired into the host and renders over the seam: {p}"
);
// Flip to the runs leg through the host's own broadcast seam: it reads the forge
// (asynchronously); it draws no stage of its own and never a green step nobody ran.
Facet::update_json(&mut app, r#"{"pipeline_source":"runs"}"#);
let st = Facet::state_json(&app);
let p = &st["pipeline"];
let forge_wired = p["source"] == "runs"
&& p["node_count"] == 0
&& p["status_source"] == "forge"
&& p["runs"]["pipeline"] == "dwarves-forge"
&& p["stage_status_counts"]["done"] == 0;
assert!(
forge_wired,
"the runs leg asks the forge for this pipeline's runs: {p}"
);
let wired = runa_wired && forge_wired;
#[cfg(feature = "testmatrix")]
nornir_testmatrix::functional_status(
"dwarves-gui",
"pipeline_dag_pane_wired",
wired,
"the Pipeline tab wired into the tabbed host renders the authored runa DAG (RunaPipelineAdapter, 5 stages / 5 `needs` edges) by default; after a {\"pipeline_source\":\"runs\"} drive the runs leg asks the forge for the pipeline's runs and draws no stage of its own — over state_json/update_json",
);
}
/// ROBOT SCENARIO — the **Forge (live)** pane (`dwarves-embed`, backend (2)) wired
/// into the tabbed host (the unified **Forge** tab) and driven over the remote seam.
/// This is the **offline-safety leg**: pointed at a dead port, it must render the
/// clean `unavailable` state (no dwarves-server needed) and its **run-picker** seam
/// must stay drivable (pin a run over `{"run_id": …}`, clear back to newest). The
/// **live-data proof** — that a connected forge shows REAL rows and the pane is RED
/// when empty — lives in `dwarves-embed`'s `forge_shows_live_rows_red_when_empty`
/// (it needs the wire report types). Emits a `nornir-testmatrix` cell (reuse edda).
#[test]
fn robot_forge_live_pane_run_picker_matrix() {
let forge_live: Box<dyn Panel> = dwarves_embed::panel_with("http://127.0.0.1:0", "", None);
let mut app = TabbedView::new("dwarves — Forge", vec![("Forge".into(), forge_live)]);
// The live pane is folded under its lower-cased label; offline ⇒ clean state,
// and the run-picker surface exists (an array of runs + a null pin = newest).
Facet::update_json(&mut app, r#"{"set_tab":"Forge"}"#);
let st = Facet::state_json(&app);
let pane = &st["forge"];
let offline_ok = pane["kind"] == "forge"
&& pane["status"] == "unavailable"
&& pane["runs"].is_array()
&& pane["run_count"] == 0
&& pane["selected_run_id"].is_null();
// Drive the run-picker over the SAME seam a ComboBox click drives: pin a run,
// then clear back to newest. Offline-safe (the pin is recorded pre-refetch).
Facet::update_json(&mut app, r#"{"run_id":"run-42"}"#);
let pinned_ok = Facet::state_json(&app)["forge"]["selected_run_id"] == "run-42";
Facet::update_json(&mut app, r#"{"run_id":""}"#);
let cleared_ok = Facet::state_json(&app)["forge"]["selected_run_id"].is_null();
let drivable = offline_ok && pinned_ok && cleared_ok;
assert!(
drivable,
"the Forge pane + run-picker drive over the remote seam, offline-safe: {st}"
);
#[cfg(feature = "testmatrix")]
nornir_testmatrix::functional_status(
"dwarves-gui",
"forge_live_run_picker_drivable",
drivable,
"Forge(live) pane (dwarves-embed) + run-picker (pin run / clear to newest) driven headless over state_json/update_json; offline-safe",
);
}
/// ROBOT SCENARIO — the **Status board** pane (the UI surface of the `dwarves
/// badge` verb) wired into the tabbed host and driven over the remote seam.
/// Switch to it, assert the health board folds each pipeline's LATEST-run status
/// (green count + amber `running` bench + red `failing` release, all from the
/// shared `brokkr::badge`), then SELECT a pipeline over the `{"select":…}` seam and
/// assert its wire-identical SVG expands. Closes the L4 robot-coverage gap on the
/// badge surface. Emits a `nornir-testmatrix` cell (reuse edda).
#[test]
fn robot_status_board_pane_matrix() {
// Mounted as a `SplitPanel` — its body DECOMPOSES, and `TabbedView::new` asserts a
// declared `body_split` has a real `SplitPanel` behind it.
let status: Box<dyn SplitPanel> = Box::new(crate::status::test_board());
let mut app = TabbedView::new(
"dwarves — Status",
vec![("Status".to_string(), TabPanel::Split(status))],
);
Facet::update_json(&mut app, r#"{"set_tab":"Status"}"#);
assert_eq!(app.active_tab(), "Status");
let st = Facet::state_json(&app);
let pane = &st["status"];
let badges = pane["badges"].as_array().unwrap();
let status_of =
|name: &str| badges.iter().find(|b| b["pipeline"] == name).unwrap()["status"].clone();
// The board here is `status::test_board()` — rows this TEST injects, so its
// content is known and asserting it is legitimate. What changed is the two lines
// that used to sit here: `source == "fixture"` and `from_fixture == true`, the
// honesty markers on a board the SHIPPED tab drew. That board is gone (the tab
// reads `dwarf.v1/Dwarf.Show` now), and so are the markers — there is no fixture
// left to mark. The feed is named instead, which is the fact worth pinning: even
// an injected board reports which feed the shipped pane reads.
let board_ok = pane["kind"] == "status"
&& pane["green_count"] == 3
&& pane["feed"] == "dwarf.v1/Dwarf.Show"
&& pane.get("from_fixture").is_none() // the marker died with the fixture
&& status_of("bench") == "running" // amber, not the old catch-all red
&& status_of("release") == "failing"
&& status_of("history") == "failing" // the NEWER run wins
&& status_of("unknown-pipe") == "unknown"; // no run ⇒ grey, never 404
// Drive the select seam (a row click): expand `build`'s SVG, then clear.
Facet::update_json(&mut app, r#"{"select":"build"}"#);
let sel = Facet::state_json(&app);
let svg = sel["status"]["selected_svg"].as_str().unwrap_or("");
let select_ok = sel["status"]["selected"] == "build"
&& svg.starts_with("<svg")
&& svg.contains("passing")
&& svg.contains("#4c1");
Facet::update_json(&mut app, r#"{"select":""}"#);
let clear_ok = Facet::state_json(&app)["status"]["selected"].is_null();
let drivable = board_ok && select_ok && clear_ok;
assert!(
drivable,
"the Status board drives over the remote seam: {st}"
);
#[cfg(feature = "testmatrix")]
nornir_testmatrix::functional_status(
"dwarves-gui",
"status_board_drivable",
drivable,
"Status board (dwarves::brokkr::badge health wall) driven headless over state_json/update_json: latest-status board + select-expands-SVG",
);
}
/// ROBOT SCENARIO — the **3rd pane (Claude)** driven purely over the remote seam
/// (`state_json`/`update_json`), with the OFFLINE stub transport doing the reply
/// IN CODE (no key, no network, no AI in the loop). Proves the 3-pane shell
/// **Forge | Funnel | Claude** and that Send→reply round-trips through the host
/// transport loop. Emits a `nornir-testmatrix` cell (reuse edda).
#[test]
fn robot_claude_pane_drives_over_the_seam() {
let forge: Box<dyn Panel> = Box::new(ForgeView::from_fixture());
let funnel: Box<dyn Panel> = nornir_funnel_embed::panel_with("http://127.0.0.1:0", "", "");
let claude: Box<dyn Panel> = Box::new(ClaudeTab::offline());
let mut app = TabbedView::new(
"dwarves — Forge | Funnel | Claude",
vec![
("Forge".into(), forge),
("Funnel".into(), funnel),
("Claude".into(), claude),
],
);
// Three panes present.
assert_eq!(
Facet::state_json(&app)["tabs"],
serde_json::json!(["Forge", "Funnel", "Claude"]),
"the 3-pane shell"
);
// Drive Claude over the seam: switch → type a prompt → send → assert the
// reply landed (the host transport loop ran the offline transport in code).
Facet::update_json(&mut app, r#"{"set_tab":"Claude"}"#);
assert_eq!(app.active_tab(), "Claude");
Facet::update_json(&mut app, r#"{"prompt_changed":"hi claude"}"#);
Facet::update_json(&mut app, "\"send\"");
let st = Facet::state_json(&app);
let claude_ok = st["claude"]["kind"] == "claude"
&& st["claude"]["turns"] == 2
&& st["claude"]["sending"] == false
&& st["claude"]["last_assistant"]
.as_str()
.unwrap_or("")
.contains("offline demo");
assert!(
claude_ok,
"Claude pane drives send→reply over the remote seam (offline transport)"
);
#[cfg(feature = "testmatrix")]
nornir_testmatrix::functional_status(
"dwarves-gui",
"claude_pane_drivable",
claude_ok,
"Claude tab (3rd pane) driven headless over state_json/update_json with the offline transport",
);
}
/// L5 DEDUP GUARD — the `ClaudeTab` now drives the SHARED `nornir-claude` edda leaf
/// transport, not a dwarves-local twin. Host the pane over the leaf's
/// `OfflineTransport::echoing()` through the [`EddaBackend`] adapter, keep an `Arc`
/// handle to the leaf, drive `set_tab → prompt → send` over the JSON seam, and
/// assert: (1) the reply is the LEAF's deterministic `[offline · <model>] <prompt>`
/// echo (only the leaf produces that shape), and (2) the leaf RECORDED the exact wire
/// request the pane sent (`requests()`), proving the pane→adapter→leaf path is live.
/// RED-when-broken: revert to a dwarves-local transport ⇒ the leaf sees no request
/// and the reply loses the `[offline · …]` shape. Emits a `nornir-testmatrix` cell.
#[test]
fn claude_tab_drives_the_shared_nornir_claude_leaf() {
let leaf = Arc::new(nornir_claude::OfflineTransport::echoing());
let backend: Box<dyn facett_claude::ClaudeTransport> = Box::new(EddaBackend(leaf.clone()));
let claude: Box<dyn Panel> = Box::new(ClaudeTab::new(backend));
let mut app = TabbedView::new("dwarves — Claude", vec![("Claude".into(), claude)]);
// Drive over the SAME seam the robot harness / thin client use.
Facet::update_json(&mut app, r#"{"set_tab":"Claude"}"#);
Facet::update_json(&mut app, r#"{"prompt_changed":"ping the leaf"}"#);
Facet::update_json(&mut app, "\"send\"");
let st = Facet::state_json(&app);
// (1) the reply is the LEAF's echo (its exact `[offline · model] last` shape).
let reply = st["claude"]["last_assistant"].as_str().unwrap_or("");
let reply_from_leaf = reply.starts_with("[offline · ") && reply.ends_with("ping the leaf");
// (2) the leaf recorded the wire request the pane sent through the adapter.
let reqs = leaf.requests();
let leaf_saw_request = reqs
.last()
.and_then(|r| r.messages.last())
.map(|m| m.role == "user" && m.content == "ping the leaf")
.unwrap_or(false);
let drives_leaf = reply_from_leaf && leaf_saw_request && st["claude"]["turns"] == 2;
assert!(
drives_leaf,
"ClaudeTab drives the shared nornir-claude leaf transport: state={st}; reqs={reqs:?}"
);
#[cfg(feature = "testmatrix")]
nornir_testmatrix::functional_status(
"dwarves-gui",
"claude_tab_drives_nornir_claude_leaf",
drives_leaf,
"ClaudeTab consumes the shared nornir-claude leaf transport (EddaBackend adapter): pane send → leaf recorded the wire request → leaf echo returned",
);
}
/// ROBOT SCENARIO — the **warehouse SELECTOR (nornir parity) + funnel-binds +
/// release-binds + About popup**, all driven over the remote seam. This is the
/// selected-warehouse spine:
/// 1. with NO warehouse selected, the funnel is in the DISTINCT `no_warehouse`
/// state (RED-when-broken: the old code said "unavailable/offline" — a lie when
/// the server is up);
/// 2. `{"select_warehouse":"nordisk"}` lands `selected_warehouse` in `state_json`
/// (nornir's `workspace_picker.selected`, mirrored) AND re-binds the funnel — its
/// `workspace` is now `nordisk` and, against a dead port, its state is the
/// DISTINCT `unreachable` (server truly down), proving the selection propagated
/// and the funnel distinguishes no-selection from server-down;
/// 3. RELEASE binds to the selection — `release_target` == the selected warehouse;
/// 4. the About popup opens over the seam and the shared `facett-about` card oracle
/// reports the dwarves wordmark + the embedded forge logo.
/// Emits a `nornir-testmatrix` cell (reuse edda).
#[test]
fn robot_warehouse_selector_binds_funnel_release_and_about() {
use crate::warehouse_select::WarehouseSelector;
// A dead port + empty token: the funnel can only report distinct offline-safe
// states, never load — exactly what we want to assert on.
let funnel: Box<dyn Panel> = nornir_funnel_embed::panel_with("http://127.0.0.1:0", "", "");
let selector = WarehouseSelector::new(vec!["nordisk".into(), "skade".into()], "");
let mut app = TabbedView::new("dwarves — Funnel", vec![("Funnel".into(), funnel)])
.with_warehouse_selector(selector)
.with_about(crate::about::identity());
// (1) No selection → funnel is `no_warehouse` (NOT "offline"), release has no target.
let st = Facet::state_json(&app);
let none_ok = st["selected_warehouse"] == ""
&& st["warehouse_select"]["release_target"] == ""
&& st["funnel"]["status"] == "no_warehouse";
// (2) Select a warehouse over the seam → selected_warehouse lands + funnel re-binds.
Facet::update_json(&mut app, r#"{"select_warehouse":"nordisk"}"#);
let st = Facet::state_json(&app);
let selected_ok = st["selected_warehouse"] == "nordisk"
&& st["warehouse_select"]["selected_warehouse"] == "nordisk"
&& st["warehouses"] == serde_json::json!(["nordisk", "skade"]);
// The funnel bound to the selection AND now reports the DISTINCT server-down
// state (dead port) — proving no-selection vs server-down are told apart.
let funnel_bound =
st["funnel"]["workspace"] == "nordisk" && st["funnel"]["status"] == "unreachable";
// (3) RELEASE binds to the selected warehouse.
let release_bound = st["warehouse_select"]["release_target"] == "nordisk";
// (4) About popup opens over the seam; the shared card oracle carries the logo.
Facet::update_json(&mut app, r#"{"about": true}"#);
let st = Facet::state_json(&app);
let about_open_ok = st["about"]["open"] == true
&& st["about"]["wordmark"] == "dwarves"
&& st["about"]["has_logo"] == true;
Facet::update_json(&mut app, r#"{"about": false}"#);
let about_closed_ok = Facet::state_json(&app)["about"]["open"] == false;
let drivable = none_ok
&& selected_ok
&& funnel_bound
&& release_bound
&& about_open_ok
&& about_closed_ok;
assert!(
drivable,
"the selected-warehouse spine drives over the remote seam (selector→funnel/release bind + distinct funnel errors + About): {}",
Facet::state_json(&app)
);
#[cfg(feature = "testmatrix")]
nornir_testmatrix::functional_status(
"dwarves-gui",
"warehouse_selector_binds_funnel_release_about",
drivable,
"warehouse SELECTOR (nornir parity: many registered, ONE selected → selected_warehouse in state_json) re-binds the funnel to the selected warehouse (no_warehouse vs unreachable told apart, never the 'offline' mislabel) + release binds to the selection (release_target) + About popup (shared facett-about card w/ embedded forge logo) — all driven headless over state_json/update_json",
);
}
}
// ── the declarative SCENE shell tests (L4) ─────────────────────────────────────
//
// Mirrors facett-demo's `scene_demo` tests: the shell is VALID + RON byte-stable, a
// route fires over the Router, and — driven headless over the state_json/update_json
// seam and through a real kittest render — a fold is observable + OMITS content, and a
// pointer drag on the master/detail divider re-apportions the LIVE split ratio.
#[cfg(test)]
mod scene_shell_tests {
use super::*;
use crate::panel::ForgeView;
use crate::status::test_board;
use facett_app::scene::Trigger;
use serde_json::json as j;
/// The SHIPPED tab labels — read from the crate's `DWARVES_TABS` roster, never a
/// copied literal list, so these in-crate scene tests describe the same shell
/// `build_native_app` mounts (the twin's hardcoded 9-name list is gone).
fn labels() -> Vec<String> {
crate::tab_labels()
}
/// The declared shell is valid, its content panes are foldable, the rail is a drawer,
/// and the root master/detail split is draggable.
#[test]
fn dwarves_scene_is_valid_foldable_drawer_and_draggable() {
let scene = dwarves_scene(&labels());
scene.validate().expect("unique pane + menu names");
assert!(
matches!(
scene.pane(ROOT).map(|p| &p.layout),
Some(Layout::Split {
resizable: true,
..
})
),
"the root master/detail split is draggable"
);
// facett-app's own roster (L5) — not a third re-walk of the tree in a test.
let foldable = scene.foldable_panes();
for want in [RAIL, "warehouse", "forge", "status"] {
assert!(foldable.contains(&want.to_string()), "`{want}` is foldable");
}
assert!(
scene.pane(RAIL).unwrap().responsive.drawer,
"the rail is a drawer"
);
assert_eq!(
scene.pane(RAIL).unwrap().responsive.collapse_below,
Some(760.0)
);
assert_eq!(scene.menubar.len(), 4, "View / Go / Mímir / Help");
// The Mímir menu gathers every code-graph / quality operation under one group.
let mimir = scene
.menubar
.iter()
.find(|m| m.name == "mimir")
.expect("a Mímir menu");
assert_eq!(
mimir.items.len(),
crate::mimir_lens::MIMIR_MENU.len(),
"one item per lens op"
);
assert!(
mimir.items.iter().any(|i| i.name == "mimir.villa"),
"the new Villa (purity) lens is under the Mímir menu"
);
assert!(
mimir.items.iter().any(|i| i.name == "mimir.similar_code"),
"dedup (SimilarCode) is under the Mímir menu — no longer scattered"
);
assert_eq!(scene.routes.len(), 3, "the declared routes");
// ── WINDOW CHROME: every pane but the root wears a title bar with fold ·
// minimise · maximise. The root is bare (there is nothing for it to minimise
// into, and maximising it is the identity). RED-when-broken if a pane is ever
// silently opted out of chrome — a shell whose panes cannot be minimised or
// maximised is exactly the shell this repo shipped before.
let chrome = scene.chrome_panes();
assert!(
!chrome.contains(&ROOT.to_string()),
"the scene root is bare: {chrome:?}"
);
assert_eq!(
chrome.len(),
scene.pane_names().len() - 1,
"chrome on every pane but the root: {chrome:?}"
);
for want in [RAIL, BODY, "warehouse", "forge"] {
let spec = scene.chrome_of(want);
assert!(spec.bar, "`{want}` draws a chrome title bar");
assert!(spec.minimize, "`{want}` draws a minimise control");
assert!(spec.maximize, "`{want}` draws a maximise control");
}
// The fold toggle rides INSIDE the chrome bar for a foldable pane (L5 — the bar
// hosts the fold rather than a second header beside it), so the rail has one and
// the non-foldable workspace does not.
assert!(
scene.chrome_of(RAIL).fold,
"the foldable rail's bar hosts the fold toggle"
);
assert!(
!scene.chrome_of(BODY).fold,
"the non-foldable workspace has no fold toggle"
);
}
/// RED-when-broken: the shell's RON is a byte-stable fixed point (parse ∘ serialize),
/// and the builder and RON yield the SAME `Scene`.
#[test]
fn dwarves_scene_ron_round_trips_byte_stable() {
let scene = dwarves_scene(&labels());
let ron1 = scene.to_ron();
let reparsed = Scene::from_ron(&ron1).expect("the shell parses back from its own RON");
assert_eq!(ron1, reparsed.to_ron(), "the shell's RON is byte-stable");
assert_eq!(scene, reparsed, "RON and the builder yield the SAME Scene");
}
/// A declared route fires by trigger over the shared Router (observable in NavState).
#[test]
fn a_declared_route_fires_over_the_router() {
let scene = dwarves_scene(&labels());
let mut nav = NavState::new(&scene);
assert!(!nav.is_folded(RAIL), "the rail starts open");
assert!(
nav.fire(&scene, &Trigger::Event("toggle-rail".into())),
"the declared event route matched"
);
assert!(nav.is_folded(RAIL), "firing the route folded the rail");
assert!(
nav.fire(&scene, &Trigger::Button("focus-warehouse".into())),
"the button route matched"
);
assert_eq!(
nav.router.current(),
"tree",
"the button route focused the first tab pane (the 🌳 Tree, the landing view)"
);
}
/// ROBOT SCENARIO — the migrated declarative shell driven purely over the remote seam
/// (`state_json`/`update_json`): the shell is DATA (draggable split + drawer rail +
/// foldable tabs), a pane folds observably, and a tab switch drives the scene's `body`
/// Tabs active index while the EXISTING panels stay reused + folded under their labels.
#[test]
fn robot_scene_fold_and_tab_switch_over_the_seam() {
let forge: Box<dyn Panel> = Box::new(ForgeView::from_fixture());
// Status is mounted as a `SplitPanel`: its body DECOMPOSES into `status-main`
// (the health board) │ `status-inspector` (the expanded badge SVG). The vec is
// built as `TabPanel` explicitly — a `vec![]` holding both a `Box<dyn Panel>` and
// a `Box<dyn SplitPanel>` UPCASTS the latter to `Box<dyn Panel>` to unify the
// element type, silently mounting it whole (which `TabbedView::new` then
// catches, loudly — that is what this shape is avoiding).
let status: Box<dyn SplitPanel> = Box::new(test_board());
let mut app = TabbedView::new(
"dwarves — Forge | Status",
vec![
("Forge".to_string(), TabPanel::Whole(forge)),
("Status".to_string(), TabPanel::Split(status)),
],
);
let st = Facet::state_json(&app);
// The shell's own divider comes first; every one after it is a decomposed tab
// BODY's internal divider (here: Status board │ badge).
let shape_ok = st["scene"] == "dwarves"
&& st["draggable_splits"] == j!([ROOT, "status"])
&& st["split_bodies"] == j!(["status"])
&& st["single_pane_bodies"] == j!(["forge"])
&& st["drawers"] == j!([RAIL])
&& st["foldable"]
.as_array()
.unwrap()
.iter()
.any(|n| n == "forge")
&& st["active_tab"] == "Forge";
// Fold a content pane over the seam → observable in `folded`, then unfold.
Facet::update_json(&mut app, r#"{"toggle_fold":"forge"}"#);
let folded_ok = Facet::state_json(&app)["folded"]["forge"] == true;
Facet::update_json(&mut app, r#"{"toggle_fold":"forge"}"#);
let unfolded_ok = Facet::state_json(&app)["folded"]["forge"] == false;
// Switch tabs over the seam (drives the scene's `body` Tabs active index).
Facet::update_json(&mut app, r#"{"set_tab":"Status"}"#);
let sw = Facet::state_json(&app);
let switch_ok = app.active_tab() == "Status"
&& sw["active_tab"] == "Status"
&& sw["status"]["kind"] == "status"; // the reused panel still answers, folded under its label
let drivable = shape_ok && folded_ok && unfolded_ok && switch_ok;
assert!(
drivable,
"the declarative scene shell drives over the remote seam: {}",
Facet::state_json(&app)
);
#[cfg(feature = "testmatrix")]
nornir_testmatrix::functional_status(
"dwarves-gui",
"scene_shell_fold_and_tab_switch_drivable",
drivable,
"the tabbed shell migrated onto facett-app scene (draggable master/detail split + drawer rail + foldable tabs) driven headless over state_json/update_json: fold a pane, switch a tab, existing panels reused + folded under their labels",
);
}
/// A tiny host that draws a queryable label per slot (so the AccessKit tree carries a
/// node we can assert present/absent), and returns a small state_json.
struct TestHost;
impl SceneHost for TestHost {
fn slot(&mut self, id: &str, ui: &mut egui::Ui) -> Value {
ui.label(format!("SLOT:{id}"));
j!({ "slot": id })
}
}
/// RED-when-broken (kittest render): folding a content pane OMITS its slot content from
/// the AccessKit tree; only the fold header (title) remains.
#[test]
fn robot_render_fold_omits_content_from_the_tree() {
use egui_kittest::kittest::Queryable;
use egui_kittest::Harness;
let scene = dwarves_scene(&labels());
// The active tab (`lenses`, selected — a single-slot body) is drawn: its slot
// content is laid out.
let lenses = labels()
.iter()
.position(|l| slug(l) == "lenses")
.expect("a Lenses tab");
let mut nav_open = NavState::new(&scene);
nav_open.set_active_tab(BODY, lenses);
let sc = scene.clone();
let mut h_open = Harness::builder()
.with_size(egui::vec2(1200.0, 800.0))
.build_ui(move |ui| {
scene::render(&sc, &mut nav_open, ui, &mut TestHost);
});
h_open.run();
assert!(
h_open.query_by_label("SLOT:lenses").is_some(),
"open: the active tab's slot content is laid out"
);
// Fold it, re-render at the SAME width: the slot content is GONE from the tree.
let mut nav_folded = NavState::new(&scene);
nav_folded.set_active_tab(BODY, lenses);
nav_folded.set_fold("lenses", true);
let sc2 = scene.clone();
let mut h_folded = Harness::builder()
.with_size(egui::vec2(1200.0, 800.0))
.build_ui(move |ui| {
scene::render(&sc2, &mut nav_folded, ui, &mut TestHost);
});
h_folded.run();
let omitted = h_folded.query_by_label("SLOT:lenses").is_none();
assert!(
omitted,
"folded: the slot content is OMITTED from the AccessKit tree"
);
// The pane's CHROME TITLE BAR remains laid out — icon + title — so the folded
// pane is still visible and still addressable. (The bar now HOSTS the fold
// toggle, so this is the chrome label `▪ Lenses`, not the old `CollapsingHeader`'s
// `▸ ▪ Lenses`.) `lenses` has no bespoke glyph, so its icon is the `▪` fallback.
assert!(
h_folded.query_by_label("▪ Lenses").is_some(),
"folded: the pane's chrome title bar remains"
);
// …and the bar's fold control is showing its UNFOLD affordance, so the body can
// be got back. A fold with no way back is a pane the operator has lost.
assert!(
h_folded.query_by_label("unfold lenses").is_some(),
"folded: the chrome bar offers the unfold control"
);
#[cfg(feature = "testmatrix")]
nornir_testmatrix::functional_status(
"dwarves-gui",
"scene_fold_omits_content",
omitted,
"folding a content pane omits its slot content from the AccessKit tree (kittest render), the fold header retained",
);
}
/// RED-when-broken (kittest render): a REAL pointer drag on the master/detail divider
/// handle re-apportions the LIVE split ratio, observable in state_json.
#[test]
fn robot_render_drag_reapportions_the_master_detail_ratio() {
use egui_kittest::Harness;
use std::cell::RefCell;
use std::rc::Rc;
let scene = dwarves_scene(&labels());
let nav = Rc::new(RefCell::new(NavState::new(&scene)));
assert!(
(nav.borrow().split_ratios(ROOT).unwrap()[0] - 0.20).abs() < 1e-5,
"the builder seeds the 0.20 rail fraction"
);
let state = Rc::new(RefCell::new(Value::Null));
let (s, n, sc) = (state.clone(), nav.clone(), scene.clone());
let mut h = Harness::builder()
.with_size(egui::vec2(1200.0, 800.0))
.build_ui(move |ui| {
*s.borrow_mut() = scene::render(&sc, &mut n.borrow_mut(), ui, &mut TestHost);
});
h.run();
// Grab the reported divider handle rect and drag its centre 200pt to the RIGHT.
let sep = state.borrow()["panes"][ROOT]["separators"][0].clone();
let (sx, sy, sw, sh) = (
sep[0].as_f64().expect("a divider handle rect is reported") as f32,
sep[1].as_f64().unwrap() as f32,
sep[2].as_f64().unwrap() as f32,
sep[3].as_f64().unwrap() as f32,
);
let from = egui::pos2(sx + sw / 2.0, sy + sh / 2.0);
let to = egui::pos2(from.x + 200.0, from.y);
h.event(egui::Event::PointerMoved(from));
h.drag_at(from);
h.event(egui::Event::PointerMoved(to));
h.run();
h.drop_at(to);
h.run();
let r0 = nav.borrow().split_ratios(ROOT).unwrap()[0];
let dragged = r0 > 0.25;
assert!(
dragged,
"dragging the divider right grew the rail fraction (ratio {r0} > 0.25)"
);
assert!(
state.borrow()["nav"]["split_ratio"][ROOT][0]
.as_f64()
.unwrap()
> 0.25,
"the live ratio is observable in state_json"
);
#[cfg(feature = "testmatrix")]
nornir_testmatrix::functional_status(
"dwarves-gui",
"scene_split_drag_reapportions_ratio",
dragged,
"a real pointer drag on the master/detail divider re-apportions the live split ratio (kittest render), observable in state_json",
);
}
/// **ONE WRITER: the JSON seam and the shell's own methods must not diverge.**
///
/// The five window-chrome verbs used to exist TWICE. `update_json` called
/// `self.nav.*` directly, while the identically-named `pub fn` wrappers sat
/// `#[allow(dead_code)]` with zero callers anywhere in dwarves, facett or korp — so
/// behaviour added to a wrapper would silently never reach the seam a robot drives,
/// and behaviour added to the seam would never reach a caller of the method. The
/// seam now routes THROUGH the wrappers, which are the only code that touches
/// `self.nav` for these verbs.
///
/// This test is what KEEPS that true. `dead_code` is a warning in this crate, not
/// an error, so nothing about the compile stops the split from coming back — only
/// an assertion can.
///
/// Asserts APPLIED state — the whole `state_json`, which carries `folded`,
/// `minimized`, `maximized` and `last_chrome` — never a return value. And it
/// carries a NON-VACUITY floor per verb: the verb must actually CHANGE the shell,
/// so two identically-inert paths cannot pass by both doing nothing (the failure
/// this whole family of guards keeps dying of).
#[test]
fn the_json_seam_and_the_shell_methods_are_one_writer() {
fn shell() -> TabbedView {
let forge: Box<dyn Panel> = Box::new(ForgeView::from_fixture());
TabbedView::new("dwarves \u{2014} Forge", vec![("Forge".into(), forge)])
}
let pane = shell()
.foldable_panes()
.first()
.cloned()
.expect("the shell ships at least one foldable pane");
// (seam JSON, the method the seam must go through, does it need a prior
// minimise to have anything to act on). `restore`/`restore_all` are no-ops on a
// pristine shell, so without the prep their non-vacuity floor would (correctly)
// fail — the prep is what gives them state to undo.
type Drive = Box<dyn Fn(&mut TabbedView)>;
let verbs: Vec<(String, Drive, bool)> = vec![
(
format!(r#"{{"toggle_fold":"{pane}"}}"#),
{
let p = pane.clone();
Box::new(move |v: &mut TabbedView| {
v.toggle_fold(&p);
})
},
false,
),
(
format!(r#"{{"toggle_minimize":"{pane}"}}"#),
{
let p = pane.clone();
Box::new(move |v: &mut TabbedView| {
v.toggle_minimize(&p);
})
},
false,
),
(
format!(r#"{{"toggle_maximize":"{pane}"}}"#),
{
let p = pane.clone();
Box::new(move |v: &mut TabbedView| {
v.toggle_maximize(&p);
})
},
false,
),
(
format!(r#"{{"restore":"{pane}"}}"#),
{
let p = pane.clone();
Box::new(move |v: &mut TabbedView| {
v.restore_pane(&p);
})
},
true,
),
(
r#"{"restore_all":true}"#.to_string(),
Box::new(|v: &mut TabbedView| {
v.restore_layout();
}),
true,
),
];
let mut proven = 0usize;
for (seam, drive, needs_prep) in &verbs {
let mut by_seam = shell();
let mut by_method = shell();
if *needs_prep {
by_seam.toggle_minimize(&pane);
by_method.toggle_minimize(&pane);
}
let before = Facet::state_json(&by_seam);
assert_eq!(
before,
Facet::state_json(&by_method),
"the two shells must start identical, or the comparison below proves nothing"
);
Facet::update_json(&mut by_seam, seam);
drive(&mut by_method);
let after_seam = Facet::state_json(&by_seam);
let after_method = Facet::state_json(&by_method);
assert_eq!(
after_seam, after_method,
"the seam `{seam}` and the method it must route through applied DIFFERENT state \u{2014} they have split again"
);
assert_ne!(
after_seam, before,
"non-vacuity: `{seam}` changed nothing, so this comparison would pass for two dead paths"
);
proven += 1;
}
assert_eq!(proven, 5, "all five window-chrome verbs must be covered");
#[cfg(feature = "testmatrix")]
nornir_testmatrix::functional_status(
"dwarves-gui",
"json_seam_and_shell_methods_are_one_writer",
proven == 5,
"the 5 window-chrome verbs (toggle_fold/minimize/maximize, restore, restore_all) apply identical state via the JSON seam and via the shell's pub methods, each with a non-vacuity floor",
);
}
}