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()
}