//! **`robot-remote`** — the per-tab robot **scenario library** for holger.
//!
//! The North-Star autotest fleet drives each app member WIRE-REMOTE over
//! [`cmd_json`](nornir_robotui::cmd_json). Until now the fleet's `RobotDriveMember`
//! had no per-tab script for holger, so it fell back to a bare liveness probe
//! (ping + read_state). This module closes that gap — mirroring dwarves'
//! `dwarves-demo/src/scenarios.rs`: it records a REAL
//! [`Scenario`](nornir_robotui::Scenario) per holger operator tab — SELECT the tab,
//! perform the meaningful interactions it supports, and ASSERT over the tab's OWN
//! `state_json` that the expected state changed.
//!
//! ## REUSE (no second vocabulary, no second oracle)
//! * The scenarios are built from the ONE robotui vocabulary
//! ([`Action`](nornir_robotui::Action) / [`Assert`](nornir_robotui::Assert) /
//! [`Step`](nornir_robotui::Step) / [`Scenario`](nornir_robotui::Scenario)) the
//! fleet already carries — the SAME `Step` wire form a
//! [`CmdJsonClient`](nornir_robotui::cmd_json::CmdJsonClient) drives. Nothing new
//! is invented.
//! * Every assertion reads the tab's EXISTING `state_json` oracle (the shell's
//! `/tab` active-tab id + each pane's own folded sub-state under its own key), or
//! the harness'es own [`Assert::Drew`](nornir_robotui::Assert::Drew) — no twinned
//! per-tab oracle. holger's active-tab value is the tab's stable `slot_id` (a
//! lower-cased id), while the tab-bar button a robot CLICKS renders the tab's
//! display `label()`, so each scenario carries both (SELECT clicks the label and
//! asserts the `slot_id` moved).
//!
//! ## Consumable as DATA
//! [`holger_tab_scenarios`] returns a `Vec<Scenario>` (serde-serializable to the
//! SAME `Step` wire form the fleet uses under the `wire` feature), so the fleet's
//! `RobotDriveMember` can LOAD holger's scenarios by member name
//! (`<dir>/holger.scenarios.json`) and drive them over the socket
//! [`serve_robot_remote`](crate::robot_remote::serve_robot_remote) mounts.
//! [`holger_tab_scenario_library`] carries the same data plus per-tab metadata (the
//! tab id + label + an HONEST `probe_only` flag for tabs that can't yet be
//! meaningfully driven headless beyond a select + render + identity assertion).
use nornir_robotui::{Action, Assert, Scenario, Step};
use serde_json::json;
/// One holger operator tab's recorded scenario plus its metadata.
///
/// `scenario.suite` carries the tab's `slot_id`, so a consumer that only wants the
/// wire data can take [`Self::scenario`] and still know which tab it drives.
#[derive(Clone, Debug)]
pub struct HolgerTabScenario {
/// The stable scene-slot id of the tab this scenario drives — the value holger's
/// `state_json` reports at `/tab` (and the entry in the shell's `/tabs` roster).
pub tab: &'static str,
/// The tab-bar button's display label — what the TabView renders and what
/// [`Action::ClickLabel`](nornir_robotui::Action::ClickLabel) addresses (e.g.
/// the `signin` tab is clicked as `"Sign in"`).
pub label: &'static str,
/// HONEST flag: `true` when this tab has no safe headless interaction beyond
/// selecting it + asserting it rendered its identity (a select + render probe,
/// not a fabricated interaction). `false` when the scenario drives a real,
/// state-changing interaction the tab supports today. (Within-pane graph/grid
/// nodes become addressable once facett node-addressing lands — a later upgrade;
/// `probe_only` is correct for those now.)
pub probe_only: bool,
/// The recorded steps (SELECT → interact → assert-over-`state_json`).
pub scenario: Scenario,
}
/// Sugar: the SELECT step every scenario opens with — click the tab's display label
/// and assert the shell's own `/tab` oracle moved to the tab's `slot_id`. This is the
/// tab's identity over the shell oracle (reused, not twinned) AND a real state change
/// (a different tab was active). The same locate+click the existing wire-remote mount
/// test uses.
fn select(label: &str, slot: &str) -> Step {
Step::new(format!("select the {label} tab"))
.action(Action::ClickLabel(label.to_string()))
.assert(Assert::State {
pointer: "/tab".to_string(),
expected: json!(slot),
})
}
/// Sugar: an assertion-only step proving the selected tab's pane actually drew (not a
/// blank pane) — the harness'es own `Drew` oracle, reused, no per-tab twin.
fn drew(label: &str) -> Step {
Step::new(format!("the {label} pane rendered (not blank)")).assert(Assert::Drew)
}
/// Sugar: an assertion-only step proving the tab folded its OWN identity into
/// `state_json` — reuses the pane's existing sub-oracle at `pointer` (RFC-6901),
/// asserting a stable field-name `needle` is present in its serialized shape.
fn identity(label: &str, pointer: &str, needle: &str) -> Step {
Step::new(format!("the {label} pane folded its own state_json identity")).assert(
Assert::StateContains {
pointer: pointer.to_string(),
needle: needle.to_string(),
},
)
}
/// The recorded per-tab scenario library — one [`HolgerTabScenario`] per registered
/// holger operator tab, in tab-bar order (the shell's `TAB_ORDER`). The holger-side
/// drive test drives every scenario here against the LIVE mounted shell, so a
/// removed/renamed tab or a stale oracle goes RED there.
pub fn holger_tab_scenario_library() -> Vec<HolgerTabScenario> {
// A tab whose only safe headless drive today is select + render + identity.
let probe = |slot: &'static str, label: &'static str, pointer: &str, needle: &str| {
HolgerTabScenario {
tab: slot,
label,
probe_only: true,
scenario: Scenario::new(slot.to_string()).steps([
select(label, slot),
drew(label),
identity(label, pointer, needle),
]),
}
};
vec![
// ── Status — the server-health grid (status/version/uptime). Auto-loaded at
// startup, so a ⟳ refresh is idempotent → probe-only. ──
probe("status", "Status", "/status", "uptime_seconds"),
// ── Repos — the repository picker. Row selection collides with the facett
// Table cell (not label-addressable today), and ⟳ refresh is idempotent →
// probe-only. ──
probe("repos", "Repos", "/repos", "selected"),
// ── Browse — the artifact listing. Auto-loads on select → probe-only. ──
probe("browse", "Browse", "/browse", "next_page_token"),
// ── Archive — the znippy file tree + stats. Auto-loads on select → probe-only. ──
probe("archive", "Archive", "/archive", "archive_path"),
// ── Artifact — the fetch pane. A meaningful fetch needs a typed query set
// through the host seam, not a label click → probe-only. ──
probe("artifact", "Artifact", "/artifact", "content_type"),
// ── Graph — the neon 3D repo graph (built on select). A within-pane NODE
// click needs facett node-addressing (a later upgrade) → probe-only. ──
probe("graph", "Graph", "/graph", "palette"),
// ── Helix — the time-helix DAG (wound on select). Within-pane node
// addressing pending → probe-only. ──
probe("helix", "Helix", "/helix", "coil_count"),
// ── Lineage — the live cache-fill facet. A REAL within-pane drive: toggle the
// render mode between the rotating 3D fill and the time-helix companion by
// their plain-text labels and watch the pane's own `/lineage_mode` oracle
// flip, then restore the shipped default (no residue). ──
HolgerTabScenario {
tab: "lineage",
label: "Lineage",
probe_only: false,
scenario: Scenario::new("lineage".to_string()).steps([
select("Lineage", "lineage"),
drew("Lineage"),
identity("Lineage", "/lineage", "total_events"),
// The tab opens on the rotating 3D fill — assert the starting oracle.
Step::new("Lineage opens on the rotating 3D fill").assert(Assert::State {
pointer: "/lineage_mode".to_string(),
expected: json!("graph3d"),
}),
// Click the plain-text `🌀 time helix` toggle → the render mode flips.
Step::new("toggle the time-helix companion → mode == helix")
.action(Action::ClickLabel("🌀 time helix".to_string()))
.assert(Assert::State {
pointer: "/lineage_mode".to_string(),
expected: json!("helix"),
}),
// Click `◉ 3D fill` back → restore the shipped default (no residue).
Step::new("back to the 3D fill → mode == graph3d (no residue)")
.action(Action::ClickLabel("◉ 3D fill".to_string()))
.assert(Assert::State {
pointer: "/lineage_mode".to_string(),
expected: json!("graph3d"),
}),
]),
},
// ── Tools — the operator tool menu. A REAL within-pane drive: pick the
// Móðguðr security function by its plain-text menu label and watch the pane's
// own `/tool` oracle change, then restore the shipped default (holger-ds). ──
HolgerTabScenario {
tab: "tools",
label: "Tools",
probe_only: false,
scenario: Scenario::new("tools".to_string()).steps([
select("Tools", "tools"),
drew("Tools"),
identity("Tools", "/tools_pane", "pane"),
// The tab opens on the holger-ds function — assert the starting oracle.
Step::new("Tools opens on the holger-ds function").assert(Assert::State {
pointer: "/tool".to_string(),
expected: json!("holger-ds"),
}),
// Click the `3 · security · Móðguðr` menu label → the function changes.
Step::new("select the security · Móðguðr function → tool == security")
.action(Action::ClickLabel("3 · security · Móðguðr".to_string()))
.assert(Assert::State {
pointer: "/tool".to_string(),
expected: json!("security"),
}),
// Click `1 · holger-ds` back → restore the shipped default (no residue).
Step::new("select the holger-ds function back → tool == holger-ds (no residue)")
.action(Action::ClickLabel("1 · holger-ds".to_string()))
.assert(Assert::State {
pointer: "/tool".to_string(),
expected: json!("holger-ds"),
}),
]),
},
// ── Upload — the write gate (reflects the selected repo's writability). The
// gate is driven by repo selection (a Table row, not label-addressable today)
// → probe-only. ──
probe("upload", "Upload", "/upload", "writable"),
// ── Sign in — the shared device-code login pane. A real sign-in needs an IdP
// + network (the device grant), unsafe headless → probe-only. Clicked as its
// display label "Sign in". ──
probe("signin", "Sign in", "/login", "state"),
// ── Settings — the ⚙ look & feel pane, the ONE home of the unified facett
// preset picker (it absorbed the retired root "Look" menu). A REAL within-pane
// drive: click a preset by its plain name label and watch the shell's OWN
// `/look` oracle switch, then restore the shipped `dark` default (no residue).
// Without this entry the shell grew a twelfth tab the recorded library never
// drove — the Settings pane asserted NOTHING over the wire lane. ──
HolgerTabScenario {
tab: "settings",
label: "Settings",
probe_only: false,
scenario: Scenario::new("settings".to_string()).steps([
select("Settings", "settings"),
drew("Settings"),
// The shell ships on the `dark` preset — assert the starting oracle.
Step::new("Settings opens on the shipped dark look").assert(Assert::State {
pointer: "/look".to_string(),
expected: json!("dark"),
}),
// Click the `light` preset label → the whole shell re-themes.
Step::new("pick the light preset → look == light")
.action(Action::ClickLabel("light".to_string()))
.assert(Assert::State {
pointer: "/look".to_string(),
expected: json!("light"),
}),
// Click `dark` back → restore the shipped default (no residue).
Step::new("back to the dark preset → look == dark (no residue)")
.action(Action::ClickLabel("dark".to_string()))
.assert(Assert::State {
pointer: "/look".to_string(),
expected: json!("dark"),
}),
]),
},
]
}
/// The recorded scenarios as pure wire DATA — one [`Scenario`] per holger operator
/// tab, each `suite` carrying the tab's `slot_id`. Serde-serializable to the SAME
/// `Step` wire form the fleet's [`CmdJsonClient`](nornir_robotui::cmd_json::CmdJsonClient)
/// drives, so the fleet's `RobotDriveMember` can LOAD holger's per-tab scenarios by
/// member name and drive them over the mounted socket.
pub fn holger_tab_scenarios() -> Vec<Scenario> {
holger_tab_scenario_library()
.into_iter()
.map(|t| t.scenario)
.collect()
}