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

nordisk/dwarves

dwarves-gui/src/scenarios.rs

//! **`robot-remote`** — the per-tab robot **scenario library** for dwarves.
//!
//! The North-Star autotest fleet drives each app member WIRE-REMOTE over
//! [`cmd_json`](nornir_robotui::cmd_json). This module records a REAL
//! [`Scenario`](nornir_robotui::Scenario) per dwarves viz tab — SELECT the tab, perform
//! the meaningful interactions it supports, and ASSERT over the tab's OWN `state_json`
//! that the expected VALUES are there (LAW 6: correctness is read from emitted DATA,
//! never "it drew").
//!
//! ## Every tab is VALUE-asserted, not just "it drew" (LAW 6)
//! A `Drew` assertion proves a non-blank pane; it does NOT prove the pane carries the
//! right data. Since the shipped shell folds each pane's `state_json` under its slot
//! slug (`/warehouse`, `/builds`, `/ci-cd`, …), every scenario now reads REAL values
//! back from that fold and asserts them. The shell this library is driven against is
//! the SHIPPED shell over a dead endpoint (no server, no workspace), and a shipped shell
//! mounts no sample data — so what a server- or file-fed pane carries there is its NAMED
//! STATE (`/status/empty_state/cause == "unreachable"`, `/warehouse/skeleton_read ==
//! false`, `/coderoom/from_room == false`), an error string (`/forge/error` contains
//! "connect"), or the pane's own view/mode oracle after a real click. No step asserts a
//! count of sample rows: the steps that did (`crate_count 6`, `breaking_edges 3`,
//! `node_count 12` …) named fixtures no shipped constructor mounts, and were red. The set MUST equal the shipped
//! roster ([`crate::DWARVES_TABS`]) in order — the drive test asserts that, so an
//! added / removed / re-ordered tab goes RED here.
//!
//! ## REUSE (no second vocabulary, no second oracle)
//! * Built from the ONE robotui vocabulary ([`Action`](nornir_robotui::Action) /
//!   [`Assert`](nornir_robotui::Assert) / [`Step`](nornir_robotui::Step) /
//!   [`Scenario`](nornir_robotui::Scenario)) the fleet carries — the SAME `Step` wire
//!   form a [`CmdJsonClient`](nornir_robotui::cmd_json::CmdJsonClient) drives.
//! * Every assertion reads the tab's EXISTING `state_json` oracle (the shell's
//!   `/active_tab` + each pane's own folded sub-state under its slug), or the harness'es
//!   own [`Assert::Drew`](nornir_robotui::Assert::Drew) — no twinned per-tab oracle.

use nornir_robotui::{Action, Assert, Scenario, Step};
use serde_json::{json, Value};

/// One dwarves viz tab's recorded scenario plus its metadata.
#[derive(Clone, Debug)]
pub struct DwarvesTabScenario {
    /// The viz tab this scenario drives (the exact registered tab label — what the
    /// tab-bar `selectable_label` renders and what `ClickLabel` addresses).
    pub tab: &'static str,
    /// HONEST flag: `true` when the scenario performs NO state-changing interaction
    /// beyond selecting the tab (it still asserts real VALUES over `state_json`, but it
    /// clicks nothing inside the pane). `false` when it drives a real, state-changing
    /// interaction (a within-pane click whose result is asserted).
    pub probe_only: bool,
    /// The recorded steps (SELECT → assert-values [→ interact → assert-again]).
    pub scenario: Scenario,
}

/// Sugar: the SELECT step every scenario opens with — click the tab's label and assert
/// the shell's own `/active_tab` oracle moved to it.
fn select(tab: &str) -> Step {
    Step::new(format!("select the {tab} tab"))
        .action(Action::ClickLabel(tab.to_string()))
        .assert(Assert::State {
            pointer: "/active_tab".to_string(),
            expected: json!(tab),
        })
}

/// Sugar: prove the selected tab's pane actually drew (not a blank pane).
fn drew(tab: &str) -> Step {
    Step::new(format!("the {tab} pane rendered (not blank)")).assert(Assert::Drew)
}

/// RFC-6901-escape one pointer segment (a `/` inside a key — e.g. `ci-cd` has none, but
/// be safe — is escaped `~1`, a `~` is `~0`).
fn esc(seg: &str) -> String {
    seg.replace('~', "~0").replace('/', "~1")
}

/// Sugar: assert an exact VALUE at `/<slug>/<path…>` over the pane's folded `state_json`.
fn val(desc: &str, slug: &str, path: &str, expected: Value) -> Step {
    let ptr = std::iter::once(esc(slug))
        .chain(path.split('/').filter(|s| !s.is_empty()).map(esc))
        .fold(String::new(), |mut a, s| {
            a.push('/');
            a.push_str(&s);
            a
        });
    Step::new(desc.to_string()).assert(Assert::State {
        pointer: ptr,
        expected,
    })
}

/// Sugar: assert a VALUE at `/<slug>/<path…>` CONTAINS `needle` (for honest error /
/// diagnostic strings and arrays whose exact contents drift).
fn contains(desc: &str, slug: &str, path: &str, needle: &str) -> Step {
    let ptr = std::iter::once(esc(slug))
        .chain(path.split('/').filter(|s| !s.is_empty()).map(esc))
        .fold(String::new(), |mut a, s| {
            a.push('/');
            a.push_str(&s);
            a
        });
    Step::new(desc.to_string()).assert(Assert::StateContains {
        pointer: ptr,
        needle: needle.to_string(),
    })
}

/// A value-asserting probe: SELECT → DREW → the given value assertions. No within-pane
/// interaction (so `probe_only = true`), but never a bare "it drew" — every one carries
/// REAL values read back from the pane's own `state_json` (LAW 6).
fn value_probe(tab: &'static str, values: Vec<Step>) -> DwarvesTabScenario {
    let mut steps = vec![select(tab), drew(tab)];
    steps.extend(values);
    DwarvesTabScenario {
        tab,
        probe_only: true,
        scenario: Scenario::new(tab.to_string()).steps(steps),
    }
}

/// The recorded per-tab scenario library — one [`DwarvesTabScenario`] per registered
/// dwarves viz tab, in [`crate::DWARVES_TABS`] order. The drive test asserts this set
/// equals the shell's live `/tabs`, so a removed/renamed/added/re-ordered tab or a stale
/// oracle goes RED there.
pub fn dwarves_tab_scenario_library() -> Vec<DwarvesTabScenario> {
    vec![
        // ── Lenses — the MIMIR LENSES. A REAL within-pane drive (needs no server):
        // switch views and assert the pane's own `/lenses/view` oracle moves, plus the
        // invariants that it opens UN-scanned and that the find→fix affordance is on and
        // honestly marks auto-apply TODO. The scan itself is NOT asserted (it needs a
        // reachable nornir-server; a scenario that reddens when a service is down tests
        // the service, not the pane). ──
        DwarvesTabScenario {
            tab: "Lenses",
            probe_only: false,
            scenario: Scenario::new("Lenses".to_string()).steps([
                select("Lenses"),
                drew("Lenses"),
                contains("the Lenses pane folds its Mímir identity", "lenses", "kind", "mimir_lens"),
                val("the lenses open un-scanned, not falsely empty", "lenses", "scanned", json!(false)),
                val("the lenses open on the Generic-Cost (bloat) view", "lenses", "view", json!("bloat")),
                // find→fix: the actionable affordance is on by default, and the pane is
                // HONEST that auto-apply is not wired (a read-only suggestion).
                val("the find→fix affordance is on by default", "lenses", "show_fixes", json!(true)),
                val(
                    "…and auto-apply is honestly marked (read-only suggestion; partially wired)",
                    "lenses",
                    "fix_pattern/auto_apply",
                    json!("partial"),
                ),
                // The view toggle renders the FULL MIMIR_MENU labels (icon-prefixed), so
                // a click addresses that exact on-screen text; the `/lenses/view` oracle
                // is the short view id.
                Step::new("switch to the zombie-feature lens")
                    .action(Action::ClickLabel("🧟 Zombie Features".to_string()))
                    .assert(Assert::State { pointer: "/lenses/view".to_string(), expected: json!("zombie") }),
                Step::new("switch to the change-risk lens")
                    .action(Action::ClickLabel("🎯 Change Risk".to_string()))
                    .assert(Assert::State { pointer: "/lenses/view".to_string(), expected: json!("risk") }),
                Step::new("open the `where this belongs` view (the specified nornir RPCs)")
                    .action(Action::ClickLabel("① Where this belongs".to_string()))
                    .assert(Assert::State { pointer: "/lenses/view".to_string(), expected: json!("belongs") }),
                Step::new("return to the generic bloat tax")
                    .action(Action::ClickLabel("📦 Generic Cost".to_string()))
                    .assert(Assert::State { pointer: "/lenses/view".to_string(), expected: json!("bloat") }),
                // The relocated dupes query: Dupes stopped being a tab in `768983f`
                // and became this view. Driving it HERE is what keeps the retirement
                // honest — a sub-view nobody selects is a tab that was removed and
                // not replaced.
                Step::new("select the relocated Similar Code view (the old Dupes tab)")
                    .action(Action::ClickLabel("🔁 Similar Code".to_string()))
                    .assert(Assert::State { pointer: "/lenses/view".to_string(), expected: json!("similar_code") }),
                // …and back to the landing view, so this scenario leaves the pane as
                // it found it for whatever runs next.
                Step::new("return to the Generic-Cost landing view")
                    .action(Action::ClickLabel("📦 Generic Cost".to_string()))
                    .assert(Assert::State { pointer: "/lenses/view".to_string(), expected: json!("bloat") }),
]),
        },
        // ── Data view — the warehouse SQL / table deck. Opens un-scanned with the
        // default query staged; the honest error/empty fields (`error`, `sql_error`) are
        // null until a scan runs. ──
        value_probe(
            "Data view",
            vec![
                contains("the Data view folds its dataview identity", "data-view", "kind", "dataview"),
                val("it opens un-scanned (no warehouse read yet)", "data-view", "scanned", json!(false)),
                val("SQL is disabled until a query is run", "data-view", "sql_enabled", json!(false)),
                contains(
                    "the default query targets a table that is still written (git_heat_facts)",
                    "data-view",
                    "sql_input",
                    "git_heat_facts",
                ),
                val("no honest error until a scan is driven", "data-view", "sql_ok", json!(false)),
            ],
        ),
        // ── Dupes is NOT a tab any more, and this library must not claim one. ──
        // `768983f` moved it out of the top menu ("IGEN dublett, skall inte ligga i
        // top menyn") and into the Similar Code SUB-VIEW under Lenses, but its entry
        // stayed here — so the roster oracle in `robot_remote` compared a library
        // naming "Dupes" against a shell that no longer registers it, and the drive
        // test went RED on main for a tab that was deliberately retired.
        //
        // The drive is not deleted, it MOVED: the Lenses scenario above now selects
        // `🔁 Similar Code` and asserts `/lenses/view`. What did NOT come with it is
        // the old pane's tier-filter drive (`exact` / `all` against `/dupes/filter`)
        // — those addressed the retired pane's own oracle, and inventing their
        // replacement here without driving the sub-view would be recording a
        // scenario nobody watched pass.
        // ── Warehouse — the constellation navigator. Its graph is the code room's
        // (`Viz.CrateGraph`, asked through the forge); with no server it has NO graph:
        // not read, not one node, and no count (`null`, never a `0` nobody counted). The
        // room's real graph is driven in `tests/robot_warehouse_nav_room.rs`. ──
        value_probe(
            "Warehouse",
            vec![
                contains("the Warehouse folds its warehouse-nav identity", "warehouse", "kind", "warehouse-nav"),
                val("it lands on the constellation level", "warehouse", "level", json!("constellation")),
                val("no server ⇒ the skeleton was NOT read", "warehouse", "skeleton_read", json!(false)),
                val("…so not one node is drawn (nothing stands in for the room)", "warehouse", "nodes", json!([])),
                val("…and nothing is counted", "warehouse", "node_count", json!(null)),
            ],
        ),
        // ── Status — the pipeline-health badge wall, read from the forge
        // (`Dwarf.Show`). No server ⇒ the NAMED empty state: which feed, and why. ──
        // ── Tree — the front page: the forge's `Dwarves.Tree`. No server ⇒ the named
        // state of the ask (the forge did not answer), never an empty graph. ──
        value_probe(
            "Tree",
            vec![
                contains("the Tree pane folds its identity", "tree", "kind", "front_tree"),
                contains("…and asks the forge's own surface", "tree", "answer/surface", "Dwarves.Tree"),
            ],
        ),
        // ── Explorer — the first tab of ⚒ Build: the forge's project tree
        // (`dwarf.v1.Explorer`). No server ⇒ no tree, and it never asks nornir. ──
        value_probe(
            "Explorer",
            vec![
                contains("the Explorer folds its identity", "explorer", "kind", "explorer"),
                contains("…and asks the forge's Explorer service", "explorer", "via", "Explorer @"),
                val("…never nornir", "explorer", "reads_nornir", json!(false)),
            ],
        ),
        value_probe(
            "Status",
            vec![
                contains("the Status pane folds its identity", "status", "kind", "status"),
                val("no server ⇒ the wall is a NAMED empty state", "status", "empty_state/empty", json!(true)),
                val("…whose cause is the unreachable forge", "status", "empty_state/cause", json!("unreachable")),
                contains("…and which names the feed it has nothing from", "status", "empty_state/feed", "Dwarf.Show"),
            ],
        ),
        // ── Coverage — the COVERAGE INDEX. A REAL within-pane drive: switch views and
        // assert the pane's own `/coverage/view` oracle moves, plus the invariant that it
        // opens UN-scanned and SAYS SO in its status line. ──
        DwarvesTabScenario {
            tab: "Coverage",
            probe_only: false,
            scenario: Scenario::new("Coverage".to_string()).steps([
                select("Coverage"),
                drew("Coverage"),
                contains("the Coverage pane folds its identity", "coverage", "kind", "coverage"),
                val("the coverage index opens un-scanned, not falsely clean", "coverage", "scanned", json!(false)),
                val("…and it holds no index (no figure nobody computed)", "coverage", "index", json!(null)),
                Step::new("switch to the host-partitioned bench-integrity view")
                    .action(Action::ClickLabel("integrity".to_string()))
                    .assert(Assert::State { pointer: "/coverage/view".to_string(), expected: json!("integrity") }),
                Step::new("switch to the cannot-go-red detector")
                    .action(Action::ClickLabel("weak".to_string()))
                    .assert(Assert::State { pointer: "/coverage/view".to_string(), expected: json!("weak") }),
                Step::new("return to the per-repo coverage board")
                    .action(Action::ClickLabel("repos".to_string()))
                    .assert(Assert::State { pointer: "/coverage/view".to_string(), expected: json!("repos") }),
            ]),
        },
        // ── Binary — the binary half of the selected workspace's code room
        // (`Viz.Binary`, asked through the forge; Mímir v2 since 2026-10-15). With no
        // forge there is no room: no pane keys, no treemap — and it names the surface it
        // asks. (It used to read the four pre-v2 `nornir bin analyze` tables.) ──
        value_probe(
            "Binary",
            vec![
                contains("the Binary pane folds its binview identity", "binary", "kind", "binview"),
                val("nothing was read from a room", "binary", "from_room", json!(false)),
                contains("…and it names the room's surface it asks", "binary", "feed", "Viz.Binary"),
                val("…no treemap nobody measured", "binary", "crate_count", json!(null)),
            ],
        ),
        // ── Coderoom — the code room of the selected workspace (`Viz.CrateGraph`,
        // `Coderoom.SnapshotDiff`, asked through the forge). With no server there is no
        // room: no pane key at all (no `crate_count`), the room's totals are `null`.
        // (The values this step used to assert — 5 crates, 3 breaking edges — were
        // `facett_coderoom_view::StubCoderoomData`, which no shipped constructor mounts.)
        // The room's real graph is driven in `tests/robot_coderoom_room.rs`. ──
        value_probe(
            "Coderoom",
            vec![
                contains("the Coderoom folds its identity", "coderoom", "kind", "coderoom"),
                val("no server ⇒ nothing on screen came from a room", "coderoom", "from_room", json!(false)),
                val("…the room's node total is not a number", "coderoom", "room/nodes", json!(null)),
                val("…nor its edge total", "coderoom", "room/edges_total", json!(null)),
                val("…and the crate-graph answer is not a measurement", "coderoom", "room/graph/measured", json!(false)),
                val("it opens on the connections, the 3-D room not yet asked", "coderoom", "mode", json!("connections")),
            ],
        ),
        // ── Build Thing — the DATA-DRIVEN scene. It opens on step 1 with nothing
        // resolved: no recipe, no matrix, no pane. That empty is the honest opening
        // state, not a degraded one — the operator has not typed anything yet. The real
        // widget drive (type a name → click sök → click a row → click a column → an
        // armed BYGG) is `tests/robot_build_scene_ui.rs`, over a STAGED constellation,
        // because a tab tour must not depend on which checkouts a machine holds. ──
        //
        // The kernel leaf is RECIPE-GATED, and STRICTLY so (owner, 2026-08-26,
        // twice: first "varför skall kernel dyka upp jämt? det beror ju på receptet",
        // then — on finding that fix content-gated only — "dwarves-ui visar
        // kernel-panelen innan jag skrivit gunnar — dvs det är hårdkodad"). The leaf
        // must not EXIST until the resolved recipe declares a [[build.kernel]]: not
        // empty, not refusing — ABSENT, no region at all. In this tour's HERMETIC
        // shell (an empty constellation root) no recipe can ever resolve, so the
        // honest claim here is the strict gate itself: the shell's own /absent oracle
        // names the leaf, and its pane state stays null. The gated shapes over a
        // staged declaration (kernel-less recipe → still absent / kernel recipe → the
        // five-leaf tab with the configurator) are driven where a declaration exists
        // to gate on: `tests/robot_kernel_in_build.rs` and
        // `tests/robot_five_leaf_body.rs`.
        DwarvesTabScenario {
            tab: "Build Thing",
            probe_only: false,
            scenario: Scenario::new("Build Thing".to_string()).steps([
                select("Build Thing"),
                drew("Build Thing"),
                contains("the scene folds its dev-id", "build-thing", "component", "buildscene"),
                val("nothing is resolved before a name is typed", "build-thing", "recipe", json!(null)),
                val("…so the home matrix is empty", "build-thing", "rows", json!([])),
                val("…no row is picked", "build-thing", "row", json!(null)),
                val("…no column is picked", "build-thing", "target", json!(null)),
                val("…BYGG is not armed", "build-thing", "can_build", json!(false)),
                val("…and no pane is open below", "build-thing", "pane", json!(null)),
                // There is no console SELECTOR to default: a Console pane shows serial and
                // graphical together. With no pane open the list is empty, which is what
                // makes this assertion mean something.
                val("no pane is open, so no console is up", "build-thing", "consoles", json!([])),
                // ── the settings TREE and the `kärna` node: the recipe decides, and nothing is resolved ──
                val("no settings tree — no recipe is resolved, and the recipe decides", "build-thing", "tree", json!(null)),
                val("…so the `kärna` detail offers NOTHING (the kernel is a node of the tree, not a leaf)", "build-thing", "leaf", json!(null)),
                // ── the honest miss: this shell's constellation root is EMPTY, and
                // searching it must say so as DATA (the same claim
                // `robot_build_scene_empty_root.rs` drives over real widgets) ──
                Step::new("type a product name into the real artefakt field")
                    .action(Action::TypeInto { label: "artefakt".to_string(), text: "gunnar".to_string() })
                    .assert(Assert::State { pointer: "/build-thing/artifact".to_string(), expected: json!("gunnar") }),
                Step::new("search — the empty root refuses with the variable that fixes it")
                    .action(Action::ClickLabel("sök".to_string()))
                    .assert(Assert::StateContains { pointer: "/build-thing/error".to_string(), needle: "byggrum".to_string() }),
                val("…the miss resolves no recipe", "build-thing", "recipe", json!(null)),
                val("…so there is STILL no tree — a miss is not a recipe", "build-thing", "tree", json!(null)),
                val("…and the `kärna` detail still offers nothing", "build-thing", "leaf", json!(null)),
            ]),
        },
        // ── Builds — the holger build-matrix. SERVER-DEPENDENT: offline (dead endpoint)
        // it shows the HONEST red — `status: unreachable` + the transport error — as
        // DATA, not a bare red label. That caught red IS the assertion (LAW 6). ──
        value_probe(
            "Builds",
            vec![
                contains("the Builds pane folds its build-matrix identity", "builds", "kind", "holger_build_matrix"),
                val("offline, the matrix is honestly UNREACHABLE (a server-side dep)", "builds", "status", json!("unreachable")),
                val("…and reports it found no matrix rather than a fake-green board", "builds", "found", json!(false)),
                contains("…the error is a VALUE (the transport failure), not a bare red label", "builds", "error", "transport error"),
            ],
        ),
        // ── ⚙ Kernel: RETIRED 2026-08-26 (the owner). Its scenario is not deleted, it
        // MOVED — to the `build-thing-kernel` leaf, and then the leaf was RECIPE-GATED
        // the day after, so the no-selection question the tab was kept alive for is now
        // only asked of a recipe that declares a `[[build.kernel]]`. This tour's
        // hermetic shell resolves no recipe, so the gated states are driven in
        // `tests/robot_kernel_in_build.rs` (browse: hint / named refusal /
        // configurator, over a staged declaration); the cascade/refusal/camera-only
        // proofs still live in the dedicated `robot_kernel_scene.rs` binary, which
        // boxes the pane directly and never needed the tab. ──
        // ── CI/CD — the quick-setup wizard. Fat fixture: a valid 2-stage pipeline DAG. ──
        value_probe(
            "CI/CD",
            vec![
                contains("the CI/CD wizard folds its identity", "ci-cd", "kind", "cicd_wizard"),
                val("the generated pipeline validates", "ci-cd", "pipeline_valid", json!(true)),
                val("its DAG carries both stages (RAGNARÖK: not 0)", "ci-cd", "dag_nodes", json!(2)),
                val("…and the dependency edge between them", "ci-cd", "dag_edges", json!(1)),
                val("no run has been refused (the honest guardrail is clear)", "ci-cd", "run_refused", json!(false)),
            ],
        ),
        // ── Forge — the CI/CD forge run roster. SERVER-DEPENDENT: offline it shows the
        // HONEST red — `status: unavailable` + the connect error — as DATA (LAW 6). ──
        value_probe(
            "Forge",
            vec![
                contains("the Forge pane folds its identity", "forge", "kind", "forge"),
                val("offline, the forge is honestly UNAVAILABLE (a server-side dep)", "forge", "status", json!("unavailable")),
                val("…with no runs (not a fabricated roster)", "forge", "run_count", json!(0)),
                contains("…the error is a VALUE (the connect failure), not a bare red label", "forge", "error", "connect to dwarves-server"),
            ],
        ),
        // ── Pipeline — the forge's OWN authored `dwarves.runa.ron` on the reused
        // facett-pipeline viewer, with the brokkr build DAG behind the source toggle. A
        // REAL within-pane drive across the source classes (LAW 3). ──
        DwarvesTabScenario {
            tab: "Pipeline",
            probe_only: false,
            scenario: Scenario::new("Pipeline".to_string()).steps([
                select("Pipeline"),
                drew("Pipeline"),
                Step::new("Pipeline opens on the authored runa pipeline")
                    .assert(Assert::State { pointer: "/pipeline/source".to_string(), expected: json!("runa") }),
                Step::new("the rendered DAG is dwarves' OWN dwarves.runa.ron")
                    .assert(Assert::State { pointer: "/pipeline/pipeline_file".to_string(), expected: json!("dwarves.runa.ron") }),
                Step::new("it parsed + validated cleanly (no honest error line)")
                    .assert(Assert::State { pointer: "/pipeline/error".to_string(), expected: json!("") }),
                Step::new("one node per authored stage (lint·build·test·ui·doctor)")
                    .assert(Assert::State { pointer: "/pipeline/node_count".to_string(), expected: json!(5) }),
                Step::new("one edge per `needs` entry")
                    .assert(Assert::State { pointer: "/pipeline/edge_count".to_string(), expected: json!(5) }),
                Step::new("the repo's other committed pipelines are selectable")
                    .assert(Assert::StateContains { pointer: "/pipeline/available_pipelines".to_string(), needle: "release.runa.ron".to_string() }),
                Step::new("switch the source to the pipeline's runs")
                    .action(Action::ClickLabel("runs (forge)".to_string()))
                    .assert(Assert::State { pointer: "/pipeline/source".to_string(), expected: json!("runs") }),
                Step::new("…which are read from the forge, for this pipeline")
                    .assert(Assert::State { pointer: "/pipeline/status_source".to_string(), expected: json!("forge") })
                    .assert(Assert::State { pointer: "/pipeline/runs/pipeline".to_string(), expected: json!("dwarves-forge") }),
                Step::new("…no stage is drawn for a run nobody read")
                    .assert(Assert::State { pointer: "/pipeline/node_count".to_string(), expected: json!(0) }),
                Step::new("switch back to the runa pipeline")
                    .action(Action::ClickLabel("runa (*.runa.ron)".to_string()))
                    .assert(Assert::State { pointer: "/pipeline/pipeline_name".to_string(), expected: json!("dwarves-forge") }),
            ]),
        },
        // ── Release — the 3-plane release convergence UNDER the shooting-ramp band. A
        // REAL within-pane drive across the band sections, plus the SAFETY oracle that
        // this pane carries no publish affordance at all. ──
        DwarvesTabScenario {
            tab: "Release",
            probe_only: false,
            scenario: Scenario::new("Release".to_string()).steps([
                select("Release"),
                drew("Release"),
                contains("the Release pane folds its identity", "release", "release_target", ""),
                Step::new("the three convergence planes are rendered")
                    .assert(Assert::State { pointer: "/release/planes".to_string(), expected: json!(["cratesio", "sparring", "local"]) }),
                Step::new("the Release pane exposes NO publish affordance")
                    .assert(Assert::State { pointer: "/release/can_publish".to_string(), expected: json!(false) }),
                Step::new("no convergence file ⇒ the graph is a NAMED empty state, not a sample release")
                    .assert(Assert::State { pointer: "/release/convergence_empty/empty".to_string(), expected: json!(true) }),
                Step::new("…read from nowhere")
                    .assert(Assert::State { pointer: "/release/source".to_string(), expected: json!("empty") }),
                Step::new("the ramp band opens on the gate verdicts")
                    .assert(Assert::State { pointer: "/release/band".to_string(), expected: json!("gates") }),
                Step::new("Gate A reports a verdict (never a blank)")
                    .assert(Assert::StateContains { pointer: "/release/release_run/gate_a".to_string(), needle: "n".to_string() }),
                Step::new("open the ramp section")
                    .action(Action::ClickLabel("Ramp".to_string()))
                    .assert(Assert::State { pointer: "/release/band".to_string(), expected: json!("ramp") }),
                Step::new("open the bump-plan section")
                    .action(Action::ClickLabel("Bump plan".to_string()))
                    .assert(Assert::State { pointer: "/release/band".to_string(), expected: json!("plan") }),
                // (No `⟳ Rescan` step: the button was removed on 2026-08-25 — a scan is not
                // a user action — and this step kept clicking a label that no longer exists.)
                Step::new("return to the gate verdicts")
                    .action(Action::ClickLabel("Gates".to_string()))
                    .assert(Assert::State { pointer: "/release/band".to_string(), expected: json!("gates") }),
            ]),
        },
        // ── Mímir release — the release plan the FORGE reads from Mímir on its jera
        // queue (`dwarf.v1.MimirRelease.Plan`). With no forge there is no plan: the
        // pane names where it asks, draws no order, reads no checkout here and offers
        // nothing to publish. The plan itself is driven in `mimir_release`'s tests and
        // `tests/robot_mimir_release.rs` (a real forge lane over a mock nornir). ──
        value_probe(
            "Mímir release",
            vec![
                contains("the Mímir release pane folds its identity", "mimir-release", "kind", "mimir-release"),
                contains("…and names the forge verb it asks", "mimir-release", "via", "MimirRelease.Plan"),
                val("no forge ⇒ no plan (no order nobody computed)", "mimir-release", "plan", json!(null)),
                val("it reads no checkout on this machine", "mimir-release", "reads_local_checkouts", json!(false)),
                val("…and has NO publish affordance", "mimir-release", "can_publish", json!(false)),
            ],
        ),
        // ── Benches — the reused testmatrix grid + mimir bench trend, read from the
        // selected workspace's two files. With no workspace both halves are NAMED empty
        // states (the 6-bench / 12-green / 3-red board this step used to assert was a
        // fixture deleted on 2026-09-10). ──
        value_probe(
            "Benches",
            vec![
                contains("the Benches pane folds its identity", "benches", "component", "BenchesView"),
                val("no matrix file ⇒ the grid is empty, by name", "benches", "source_matrix", json!("empty")),
                val("…and says so", "benches", "matrix_empty/empty", json!(true)),
                val("no bench history ⇒ the trend is empty, by name", "benches", "source_benches", json!("empty")),
                val("…and says so", "benches", "bench_empty/empty", json!(true)),
            ],
        ),
        // ── Jera — dwarves' ONE jobs/zones surface, read from the server (`Viz.Jobs`).
        // No server ⇒ the read failed and the pane says so (the 7-job board this step
        // used to assert is a test fixture); plus the standing invariant: NO demo button. ──
        value_probe(
            "Jera",
            vec![
                contains("the Jera pane folds its identity", "jera", "kind", "jera"),
                val("no server ⇒ the jobs could not be READ, by name", "jera", "source", json!("unreadable")),
                contains("…with the reason as a value", "jera", "error", "unreachable"),
                val("the jobs surface carries NO demo button", "jera", "has_demo_button", json!(false)),
            ],
        ),
        // ── Deploy key — the SSH deploy-key lifecycle. Whether a key file exists is a
        // fact of the MACHINE the tour runs on (`no-key-found` on a box without one,
        // `present-never-used` on one with), so it is not asserted here; what holds on
        // every machine is that a tour probes nothing, and unprobed is never green. ──
        value_probe(
            "Deploy key",
            vec![
                val("nothing has been probed by a tour", "deploy-key", "remotes_probed", json!(0)),
                val("…so the state is NOT green (unknown ≠ working)", "deploy-key", "green", json!(false)),
            ],
        ),
        // ── Funnel — the embedded plan-DAG funnel with the server's funnel behind it.
        // No server ⇒ the embedded one, and its source line says so. (The 6-node plan
        // this step used to assert was a sample funnel seeded into every embedded pane;
        // the seed is test-only now, so a box's funnel holds what that box recorded.) ──
        value_probe(
            "Funnel",
            vec![
                contains("the Funnel pane folds its identity", "funnel", "kind", "funnel"),
                val("it runs embedded (no nornir-server needed)", "funnel", "mode", json!("embedded")),
                contains("…and names where its content is from", "funnel", "funnel_source", "embedded"),
            ],
        ),
        // ── Claude — the shared facett-claude conversation pane (offline stub). ──
        value_probe(
            "Claude",
            vec![
                contains("the Claude pane folds its identity", "claude", "kind", "claude"),
                val("it names the model it would talk to", "claude", "model", json!("claude-opus-4-8")),
                val("no turns yet (offline stub)", "claude", "turns", json!(0)),
                val("no honest error", "claude", "error", json!(null)),
            ],
        ),
        // ── Fleet — the shared FleetView coverage board over a fleet run
        // (`$DWARVES_FLEET_RUN`). No run ⇒ a NAMED empty state and NO gate verdict in
        // either colour (the 4-member / 17-green / 7-red board was a demo fixture). ──
        value_probe(
            "Fleet",
            vec![
                contains("the Fleet pane folds its identity", "fleet", "component", "FleetView"),
                val("no run ⇒ the board is empty, by name", "fleet", "source", json!("empty")),
                val("…and says so", "fleet", "fleet_empty/empty", json!(true)),
                val("…and a gate nobody ran has NO verdict", "fleet", "gate_green", json!(null)),
            ],
        ),
        // ── Saga — the tape-recorder journey view over the workspace's imported tape.
        // No tape ⇒ a NAMED empty state (the 2-session / 4-step tape was a sample). ──
        value_probe(
            "Saga",
            vec![
                contains("the Saga pane folds its identity", "saga", "component", "SagaView"),
                val("no tape ⇒ the view is empty, by name", "saga", "source", json!("empty")),
                val("…and says so", "saga", "tape_empty/empty", json!(true)),
            ],
        ),
        // ── Settings — workspace · backend · theme · vault. FEATURE-AWARE: a FAT build
        // (`dupes-fat`) carries the embedded engine, so both backend radios are on screen
        // and the fat⇆thin flip is a real drive; a THIN build has exactly one mode, drawn
        // as a STATED FACT (no radio), so the scenario asserts that fact instead of
        // clicking a control that does not exist. ──
        settings_scenario(),
        // ── Profile — the shared Identity + vault credential NAMES (never values). ──
        value_probe(
            "Profile",
            vec![
                contains("the Profile pane folds its identity", "profile", "kind", "profile"),
                val("the identity names dwarves", "profile", "identity/name", json!("dwarves")),
                val("no unlocked vault ⇒ nothing was read", "profile", "vault_available", json!(false)),
                val("…so no credential name is listed, and the pane says why", "profile", "credentials_empty/empty", json!(true)),
                val("nobody is signed in", "profile", "login/state", json!("signed_out")),
            ],
        ),
        // ── Manual — dwarves' embedded `.nornir/*.md` doc set in the shared manual pane. ──
        value_probe(
            "Manual",
            vec![
                val("the manual is for the dwarves app", "manual", "app", json!("dwarves")),
                val("the doc set is embedded in the binary", "manual", "embedded", json!(true)),
                contains("…and carries the idiot guides (RAGNARÖK: not empty)", "manual", "docs", "dwarves-for-idiots"),
            ],
        ),
    ]
}

/// The Settings scenario, branched on whether THIS binary compiled in the embedded
/// engine ([`crate::settings::HAS_EMBEDDED_ENGINE`], gated by the `dupes-fat` feature).
/// A control that does not exist in this build is never clicked — the scenario asserts
/// the mode the build can actually enter (LAW 1: test the artifact that ships).
fn settings_scenario() -> DwarvesTabScenario {
    if crate::settings::HAS_EMBEDDED_ENGINE {
        // FAT build — both radios are real; drive the fat⇆thin flip and restore.
        DwarvesTabScenario {
            tab: "Settings",
            probe_only: false,
            scenario: Scenario::new("Settings".to_string()).steps([
                select("Settings"),
                drew("Settings"),
                contains(
                    "the Settings pane folds its identity",
                    "settings",
                    "kind",
                    "settings",
                ),
                val(
                    "a FAT build carries the embedded engine",
                    "settings",
                    "has_embedded_engine",
                    json!(true),
                ),
                val(
                    "Settings opens on the fat/embedded backend",
                    "settings",
                    "backend",
                    json!("embedded"),
                ),
                Step::new("switch the backend to Remote (thin)")
                    .action(Action::ClickLabel("Remote (thin · --server)".to_string()))
                    .assert(Assert::State {
                        pointer: "/settings/backend".to_string(),
                        expected: json!("remote"),
                    }),
                Step::new("switch the backend back to Embedded (fat)")
                    .action(Action::ClickLabel("Embedded (fat)".to_string()))
                    .assert(Assert::State {
                        pointer: "/settings/backend".to_string(),
                        expected: json!("embedded"),
                    }),
            ]),
        }
    } else {
        // THIN build — no embedded engine; the ONE mode is a stated fact, not a control.
        value_probe(
            "Settings",
            vec![
                contains(
                    "the Settings pane folds its identity",
                    "settings",
                    "kind",
                    "settings",
                ),
                val(
                    "a THIN build carries NO embedded engine",
                    "settings",
                    "has_embedded_engine",
                    json!(false),
                ),
                val(
                    "…so its only backend is remote (thin)",
                    "settings",
                    "backend",
                    json!("remote"),
                ),
                val(
                    "…and that is the ONLY choice offered (no impossible fat radio)",
                    "settings",
                    "backend_choices",
                    json!(["remote"]),
                ),
            ],
        )
    }
}

/// The recorded scenarios as pure wire DATA — one [`Scenario`] per drivable dwarves viz
/// tab, each `suite` carrying the tab name.
pub fn dwarves_tab_scenarios() -> Vec<Scenario> {
    dwarves_tab_scenario_library()
        .into_iter()
        .map(|t| t.scenario)
        .collect()
}