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

nordisk/edda

crates/nornir-fleet/tests/fleet_harness.rs

//! End-to-end proof that the North-Star harness WIRES the FULL 5-app constellation:
//! load the checked-in `fleet.runa.ron` (nornir · dwarves · holger · korp · ordning),
//! lower it through the mock jera backend (no container host), and prove
//! **spins → drives → gates → tears-down** across all five members with ONE
//! constellation-wide `Gap == ∅` verdict.
//!
//! The honesty contract (the whole value — NO false green):
//!  * every declared surface driven both legs → `Gap == ∅` (green);
//!  * the CURRENT reality — only dwarves mounts a `CmdJsonServer`, the other four are
//!    probe-only — reds the gate, naming the un-driven apps' `@fat` surfaces (they are
//!    NOT silently counted as covered);
//!  * drop a scenario → its node goes missing → red (red-when-broken);
//!  * a spin failure reds that member's rows.

use std::collections::BTreeSet;
use std::io;

use nornir_fleet::{EguiWire, FleetHarness, Pipeline, ScenarioSource, ScenarioWire, Step};

use egui_kittest::Harness;
use nornir_robotui::cmd_json::{
    channel_over, duplex, CmdJsonServer, DuplexEnd, IoChannel, LineChannel,
};
use nornir_robotui::demo::{bump_draw, BumpApp};
use nornir_robotui::{Action, Assert, RobotSession, Scenario, Step as UiStep, Targets};
use serde_json::json;

/// The checked-in fleet descriptor — the real artifact, not a fixture.
const FLEET_RON: &str = include_str!("../fleet.runa.ron");

/// The five constellation apps and their CODE-SOURCED viz-tab counts (see the
/// descriptor header for the exact `Tab` enum each was read from). Kept here as the
/// oracle the coverage-count assertions join against.
const NORNIR_TABS: usize = 28;
const DWARVES_TABS: usize = 18;
const HOLGER_TABS: usize = 11;
const KORP_TABS: usize = 16;
const ORDNING_TABS: usize = 12;
const TOTAL_TABS: usize = NORNIR_TABS + DWARVES_TABS + HOLGER_TABS + KORP_TABS + ORDNING_TABS; // 85
const TOTAL_NODES: usize = TOTAL_TABS * 2; // × {fat, thin} = 170
/// The four apps that do NOT mount a CmdJsonServer yet — their `@fat` nodes are the
/// honest gap in the current-reality run.
const PROBE_ONLY_FAT: usize = NORNIR_TABS + HOLGER_TABS + KORP_TABS + ORDNING_TABS; // 67

/// The descriptor parses + validates as a runa pipeline (DAG cycle / needs check),
/// and declares ALL FIVE members + the constellation-wide gate.
#[test]
fn fleet_ron_parses_and_validates_the_five_members() {
    let pipeline = Pipeline::from_ron_str(FLEET_RON).expect("fleet.runa.ron is a valid pipeline");
    assert_eq!(pipeline.name, "nordisk-fleet-of-five");
    // build → provision → drive → gate.
    assert_eq!(pipeline.stages.len(), 4);

    // Exactly the five apps are spun, each with a non-empty declared surface.
    let mut spun: Vec<(String, usize)> = Vec::new();
    for stage in &pipeline.stages {
        for step in &stage.steps {
            if let Step::SpinFleetMember { name, surface, image, .. } = step {
                assert!(!image.is_empty(), "member `{name}` declares an image");
                spun.push((name.clone(), surface.len()));
            }
        }
    }
    spun.sort();
    assert_eq!(
        spun,
        vec![
            ("dwarves".to_string(), DWARVES_TABS),
            ("holger".to_string(), HOLGER_TABS),
            ("korp".to_string(), KORP_TABS),
            ("nornir".to_string(), NORNIR_TABS),
            ("ordning".to_string(), ORDNING_TABS),
        ],
        "all five constellation apps declared with their code-sourced tab counts"
    );

    // Each spun member is also driven (a RobotDriveMember step names it).
    let driven: BTreeSet<String> = pipeline
        .stages
        .iter()
        .flat_map(|s| &s.steps)
        .filter_map(|st| match st {
            Step::RobotDriveMember { member, .. } => Some(member.clone()),
            _ => None,
        })
        .collect();
    for (m, _) in &spun {
        assert!(driven.contains(m), "member `{m}` is robot-driven");
    }

    // One constellation-wide coverage gate.
    let gates: usize = pipeline
        .stages
        .iter()
        .flat_map(|s| &s.steps)
        .filter(|st| matches!(st, Step::CoverageGate { .. }))
        .count();
    assert_eq!(gates, 1, "exactly one aggregate CoverageGate");
}

/// GREEN path: with a wire that drives EVERY member for real (each tab's fat leg
/// answered by a live `CmdJsonServer`), all five apps' surfaces are covered both
/// legs → the constellation-wide gate is `Gap == ∅`.
#[test]
fn fleet_all_five_green_when_every_member_is_driven() {
    let pipeline = Pipeline::from_ron_str(FLEET_RON).unwrap();
    let outcome = FleetHarness::mock("nordisk")
        .with_egui_wire(AllDrivenWire)
        .run(&pipeline)
        .expect("pipeline lowers");

    // All five spun.
    let mut spun = outcome.spun.clone();
    spun.sort();
    assert_eq!(spun, vec!["dwarves", "holger", "korp", "nornir", "ordning"]);

    assert!(
        outcome.gate_green(),
        "gate must be GREEN when every member's whole surface is driven: {}",
        outcome.gate_summary()
    );
    let gate = outcome.gate.as_ref().unwrap();
    assert!(gate.gap.missing.is_empty(), "no missing surface");
    assert_eq!(gate.gap.total, TOTAL_NODES, "85 tabs × {{fat, thin}} = 170 nodes");
    assert_eq!(gate.gap.covered, TOTAL_NODES, "every node covered");

    let cov: Vec<_> = outcome.rows.iter().filter(|r| r.aspect == "coverage").collect();
    assert_eq!(cov.len(), 1);
    assert_eq!(cov[0].status, "pass");
}

/// THE CURRENT REALITY (no false green): only **dwarves** mounts a `CmdJsonServer`
/// (dwarves-gui `--robot-remote`), so it is driven for real; the other four are
/// PROBE-ONLY. Their `@fat` nodes read as not-yet-driven (uncovered), the gate reds
/// naming each `app:tab@fat`, and the probe rows are a DISTINCT `skip` (not a
/// swallowed pass) carrying the per-app mount TODO. dwarves is fully covered.
#[test]
fn current_reality_reds_gate_dwarves_driven_four_probe_only() {
    let pipeline = Pipeline::from_ron_str(FLEET_RON).unwrap();
    // Mirrors the production `MountRouter::new(dir, ["dwarves"])`: dwarves is really
    // driven over the wire, every other app answers probe-only (`open`→`Ok(None)`).
    let wire = MountedSetWire::new(["dwarves"]);
    let outcome = FleetHarness::mock("nordisk")
        .with_egui_wire(wire)
        .run(&pipeline)
        .expect("pipeline lowers");

    // NOT green — four apps aren't mounted.
    assert!(
        !outcome.gate_green(),
        "four un-mounted apps MUST red the gate (no false green): {}",
        outcome.gate_summary()
    );
    let gate = outcome.gate.as_ref().unwrap();

    // Exactly the four probe-only apps' `@fat` nodes are missing (their @thin legs +
    // ALL of dwarves stay covered).
    assert_eq!(
        gate.gap.missing.len(),
        PROBE_ONLY_FAT,
        "only the four un-mounted apps' fat legs are uncovered ({PROBE_ONLY_FAT})"
    );
    assert_eq!(gate.gap.covered, TOTAL_NODES - PROBE_ONLY_FAT);

    let missing: BTreeSet<String> = gate.gap.missing.iter().map(|n| n.key_str()).collect();
    // The gate names `app:tab@fat` for each un-driven app.
    for key in [
        "viz_tab:nornir:Nornir@fat",
        "viz_tab:holger:status@fat",
        "viz_tab:korp:demo@fat",
        "viz_tab:ordning:calendar@fat",
    ] {
        assert!(missing.contains(key), "the gap names the un-driven surface `{key}`");
    }
    // dwarves is genuinely driven — none of its nodes are missing, and no @thin is.
    assert!(
        !missing.iter().any(|k| k.starts_with("viz_tab:dwarves:")),
        "dwarves is really driven (mounted) — none of its surfaces are missing"
    );
    assert!(
        !missing.iter().any(|k| k.ends_with("@thin")),
        "every web (thin) leg stays covered; only the un-mounted fat legs are the gap"
    );

    // The probe-only fat legs are a DISTINCT `skip` naming the mount TODO — NOT a
    // pass (no false green) and NOT a hard `fail` (nothing is broken, just un-mounted).
    let skips: Vec<_> = outcome
        .rows
        .iter()
        .filter(|r| r.status == "skip")
        .collect();
    assert_eq!(skips.len(), PROBE_ONLY_FAT, "one probe-only skip per un-mounted fat leg");
    assert!(
        skips.iter().all(|r| r.message.contains("TODO(mount)")),
        "every probe-only row names the per-app mount follow-up"
    );
    assert!(
        skips.iter().any(|r| r.test_name.starts_with("korp::egui::") && r.message.contains("korp")),
        "a probe-only skip names korp's un-mounted egui leg"
    );

    // dwarves' egui rows genuinely PASSED over the wire (not skipped).
    let dwarves_egui: Vec<_> = outcome
        .rows
        .iter()
        .filter(|r| r.test_name.starts_with("dwarves::egui::"))
        .collect();
    assert_eq!(dwarves_egui.len(), DWARVES_TABS);
    assert!(
        dwarves_egui.iter().all(|r| r.status == "pass"),
        "dwarves is the mounted app — its egui leg is really driven and passes"
    );
}

/// RED-WHEN-BROKEN: drop ONE web scenario (nornir's `Chat`) → its
/// `viz_tab:nornir:Chat@thin` node goes uncovered → `Gap != ∅` → the gate reds,
/// naming the exact untested app:tab. (No wire installed → the legacy co-located mock
/// baseline is green, so the drop is the ONLY change.)
#[test]
fn fleet_gate_reds_when_a_scenario_is_dropped() {
    let mut pipeline = Pipeline::from_ron_str(FLEET_RON).unwrap();

    let mut dropped = false;
    for stage in &mut pipeline.stages {
        for step in &mut stage.steps {
            if let Step::RobotDriveMember { member, web_scenarios, .. } = step {
                if member == "nornir" {
                    web_scenarios.retain(|t| t != "Chat");
                    dropped = true;
                }
            }
        }
    }
    assert!(dropped, "test precondition: nornir's RobotDriveMember exists to mutate");

    let outcome = FleetHarness::mock("nordisk").run(&pipeline).expect("pipeline lowers");

    assert!(
        !outcome.gate_green(),
        "gate must RED when a scenario is dropped: {}",
        outcome.gate_summary()
    );
    let gate = outcome.gate.as_ref().unwrap();
    assert_eq!(gate.gap.missing.len(), 1, "exactly one surface node went uncovered");
    assert_eq!(
        gate.gap.missing[0].key_str(),
        "viz_tab:nornir:Chat@thin",
        "the gate names the exact untested app:tab@leg"
    );

    let cov = outcome.rows.iter().find(|r| r.aspect == "coverage").unwrap();
    assert_eq!(cov.status, "fail");
    assert_eq!(cov.metric, 1.0);
}

/// A spin failure propagates to a RED gate: if the whole fleet never reaches Ready,
/// every drive row fails, nothing is covered, and the gate reds — no false green from
/// a dead fleet (the existing red-when-broken behaviour, preserved for five members).
#[test]
fn spin_failure_propagates_to_a_red_gate() {
    use std::sync::Arc;

    let runner = jera::boot::FakeBootRunner::yielding(jera::boot::FakeProc::running());
    *runner.fail.lock().unwrap() = Some("no container socket".into());
    let harness = FleetHarness::with_runner(Arc::new(runner), "nordisk");

    let pipeline = Pipeline::from_ron_str(FLEET_RON).unwrap();
    let outcome = harness.run(&pipeline).expect("pipeline lowers even when spin fails");

    assert!(outcome.spun.is_empty(), "no member reached Ready");
    assert!(!outcome.gate_green(), "a dead fleet cannot be green: {}", outcome.gate_summary());
    let gate = outcome.gate.as_ref().unwrap();
    assert_eq!(gate.gap.covered, 0);
    assert_eq!(gate.gap.missing.len(), TOTAL_NODES, "every declared node is missing");
}

// ─────────────────────────────────────────────────────────────────────────────
// The egui leg is WIRE-REMOTE — proven over an in-memory duplex (no container, no
// OCI): a `RobotDriveMember` step drives a REMOTE egui app through a `CmdJsonClient`,
// and a failing scenario step surfaces as a FAILED TestResultRow.
// ─────────────────────────────────────────────────────────────────────────────

/// A `CmdJsonServer` over a `RobotSession` driving the demo Bump app — the "host
/// process". In production a container's egui app builds the same session over ITS
/// kittest harness and calls `serve()` on a socket; here it answers a duplex.
fn bump_host() -> CmdJsonServer<RobotSession<'static, BumpApp>> {
    let harness = Harness::new_ui_state(
        |ui, app: &mut BumpApp| {
            let mut t = Targets::default();
            bump_draw(ui, app, &mut t);
        },
        BumpApp::default(),
    );
    CmdJsonServer::new(RobotSession::new(harness, "fleet-egui-host"))
}

/// A single-threaded `LineChannel` that, on each read, first lets the co-located
/// host serve the request the driver just wrote — so a blocking `CmdJsonClient`
/// roundtrip resolves on ONE thread (ROOT LAW #0: no thread::spawn / no rayon). The
/// bytes still cross a real duplex + full (de)serialize; only the scheduling is
/// collapsed. This stands in for the host's own I/O task headlessly.
struct Coupled {
    driver: IoChannel<io::BufReader<DuplexEnd>, DuplexEnd>,
    host: CmdJsonServer<RobotSession<'static, BumpApp>>,
    host_chan: IoChannel<io::BufReader<DuplexEnd>, DuplexEnd>,
}

impl LineChannel for Coupled {
    fn write_line(&mut self, line: &str) -> io::Result<()> {
        self.driver.write_line(line)
    }
    fn read_line(&mut self) -> io::Result<Option<String>> {
        // Serve the request the driver just wrote, then read the reply.
        self.host.serve_one(&mut self.host_chan)?;
        self.driver.read_line()
    }
}

/// A fresh coupled driver end over a Bump `CmdJsonServer` — the reusable "a live host
/// answers this member over the wire" primitive both driving test wires build.
fn coupled_host() -> Box<dyn LineChannel> {
    let (driver_end, host_end) = duplex();
    Box::new(Coupled {
        driver: channel_over(driver_end),
        host: bump_host(),
        host_chan: channel_over(host_end),
    })
}

/// A test `EguiWire` source: every `open` stands up a FRESH duplex + a
/// `CmdJsonServer` over a Bump `RobotSession`, and returns a `Coupled` driver end.
/// The per-tab scenario is real robot steps — a "good" tab bumps the remote counter
/// and asserts it moved; a "bad" tab asserts a value the remote app never reaches
/// (the red-when-broken step).
struct DuplexBumpWire;

impl EguiWire for DuplexBumpWire {
    fn open(&mut self, _member: &str, _ports: &[u16]) -> io::Result<Option<Box<dyn LineChannel>>> {
        Ok(Some(coupled_host()))
    }

    fn scenario(&self, _member: &str, tab: &str) -> Vec<UiStep> {
        match tab {
            "ok_tab" => vec![
                UiStep::new("remote bump → clicks == 1")
                    .action(Action::ClickButton("Bump".into()))
                    .assert(Assert::State { pointer: "/clicks".into(), expected: json!(1) }),
                UiStep::new("remote bump again → clicks == 2")
                    .action(Action::ClickButton("Bump".into()))
                    .assert(Assert::State { pointer: "/clicks".into(), expected: json!(2) }),
            ],
            _ => vec![
                UiStep::new("broken expectation → clicks == 999")
                    .action(Action::ClickButton("Bump".into()))
                    .assert(Assert::State { pointer: "/clicks".into(), expected: json!(999) }),
            ],
        }
    }
}

/// Drives EVERY member for real: `open` returns a live coupled host, and the default
/// (empty) scenario makes the fat leg run a liveness+`read_state` probe that passes
/// for any tab (the remote app renders an observable state object). Used by the
/// all-green constellation test.
struct AllDrivenWire;

impl EguiWire for AllDrivenWire {
    fn open(&mut self, _member: &str, _ports: &[u16]) -> io::Result<Option<Box<dyn LineChannel>>> {
        Ok(Some(coupled_host()))
    }
}

/// The member-aware wire mirroring production `MountRouter`: members in `mounted` are
/// driven for real over a live coupled host; every other member is PROBE-ONLY
/// (`open`→`Ok(None)`), so its fat leg reads as honestly not-yet-driven.
struct MountedSetWire {
    mounted: BTreeSet<String>,
}

impl MountedSetWire {
    fn new<'a>(mounted: impl IntoIterator<Item = &'a str>) -> Self {
        Self { mounted: mounted.into_iter().map(str::to_string).collect() }
    }
}

impl EguiWire for MountedSetWire {
    fn open(&mut self, member: &str, _ports: &[u16]) -> io::Result<Option<Box<dyn LineChannel>>> {
        if self.mounted.contains(member) {
            Ok(Some(coupled_host()))
        } else {
            Ok(None) // no mount yet → probe-only (honest not-yet-driven)
        }
    }
}

/// A focused 2-tab descriptor: one tab whose egui scenario drives the remote app
/// clean, one whose egui scenario fails. Web stays co-located (Ready == pass), so
/// exactly the failed tab's `@fat` node goes uncovered.
const WIRE_RON: &str = r#"
Pipeline(
    name: "egui-wire-proof",
    triggers: [ Manual ],
    stages: [
        Stage(name: "provision", steps: [
            SpinFleetMember(
                name: "nornir",
                image: "localhost/nornir-server:wire-test",
                ports: [7777],
                env: [],
                surface: ["ok_tab", "bad_tab"],
            ),
        ]),
        Stage(name: "drive", needs: ["provision"], steps: [
            RobotDriveMember(
                member: "nornir",
                egui_scenarios: ["ok_tab", "bad_tab"],
                web_scenarios: ["ok_tab", "bad_tab"],
            ),
        ]),
        Stage(name: "gate", needs: ["drive"], steps: [
            CoverageGate(workspace: "nornir", allow: []),
        ]),
    ],
)
"#;

/// The red-when-broken proof for the RobotDriveMember → CmdJsonClient path: with the
/// duplex-backed `EguiWire` source installed, the fat leg drives each egui tab
/// REMOTELY. The clean tab moves the remote `state_json` and passes; the broken tab
/// fails over the wire and surfaces a FAILED TestResultRow — which reds the gate's
/// `@fat` node. Break the (de)serialize, the dispatch, or the drive and this goes
/// red instead of falsely green.
#[test]
fn egui_leg_drives_remote_over_the_wire() {
    let pipeline = Pipeline::from_ron_str(WIRE_RON).expect("wire descriptor parses");
    let outcome = FleetHarness::mock("nornir")
        .with_egui_wire(DuplexBumpWire)
        .run(&pipeline)
        .expect("pipeline lowers");

    assert_eq!(outcome.spun, vec!["nornir".to_string()], "the member reached Ready");

    let ok_row = outcome
        .rows
        .iter()
        .find(|r| r.test_name == "nornir::egui::ok_tab")
        .expect("an egui row exists for ok_tab");
    assert_eq!(ok_row.status, "pass", "the clean remote drive passed: {}", ok_row.message);

    let bad_row = outcome
        .rows
        .iter()
        .find(|r| r.test_name == "nornir::egui::bad_tab")
        .expect("an egui row exists for bad_tab");
    assert_eq!(bad_row.status, "fail", "the broken remote step must RED the row");
    assert!(
        bad_row.message.contains("999"),
        "the wired-back assertion failure reason travels to the row: {}",
        bad_row.message
    );

    assert!(!outcome.gate_green(), "a failed remote drive reds the gate: {}", outcome.gate_summary());
    let gate = outcome.gate.as_ref().unwrap();
    assert_eq!(gate.gap.missing.len(), 1, "exactly the failed egui tab is uncovered");
    assert_eq!(
        gate.gap.missing[0].key_str(),
        "viz_tab:nornir:bad_tab@fat",
        "the uncovered node names app:tab@leg — the egui (fat) leg of the tab whose drive failed"
    );
}

/// The clean side, asserted directly on the surface: with the wire source, the GOOD
/// egui tab covers its `@fat` node exactly like the co-located leg used to — so an
/// all-passing wire drive keeps `Gap == ∅` semantics intact.
#[test]
fn wire_driven_egui_tab_still_contributes_its_coverage_node() {
    let ron = r#"
Pipeline(
    name: "egui-wire-green",
    triggers: [ Manual ],
    stages: [
        Stage(name: "provision", steps: [
            SpinFleetMember(name: "nornir", image: "localhost/x:t", ports: [7777], env: [], surface: ["ok_tab"]),
        ]),
        Stage(name: "drive", needs: ["provision"], steps: [
            RobotDriveMember(member: "nornir", egui_scenarios: ["ok_tab"], web_scenarios: ["ok_tab"]),
        ]),
        Stage(name: "gate", needs: ["drive"], steps: [
            CoverageGate(workspace: "nornir", allow: []),
        ]),
    ],
)
"#;
    let pipeline = Pipeline::from_ron_str(ron).unwrap();
    let outcome = FleetHarness::mock("nornir")
        .with_egui_wire(DuplexBumpWire)
        .run(&pipeline)
        .expect("pipeline lowers");

    assert!(
        outcome.gate_green(),
        "an all-passing WIRE drive covers @fat + @thin → Gap == ∅: {}",
        outcome.gate_summary()
    );
    let gate = outcome.gate.as_ref().unwrap();
    assert_eq!(gate.gap.total, 2, "1 tab × {{fat, thin}}");
    assert_eq!(gate.gap.covered, 2, "the wire-driven fat node is covered");
}

// ─────────────────────────────────────────────────────────────────────────────
// The per-member SCENARIO SOURCE drives an app's REAL recorded steps (the wire DATA
// it emits — dwarves-gui `scenarios --emit`) instead of the bare liveness probe.
// This proves the emit→load→drive round-trip end-to-end, single-threaded, no binary
// build-dep: build scenarios in Rust → serde JSON to a `<dir>/<member>.scenarios.json`
// file → `ScenarioSource::from_dir` decodes them → `ScenarioWire` feeds them into the
// fat leg → each step folds into the member×surface grid, and a deliberately-failing
// step reds that node.
// ─────────────────────────────────────────────────────────────────────────────

/// A 1-member descriptor whose egui + web legs both drive two tabs (`ok_tab` clean,
/// `bad_tab` broken). Member `dwarves` — the first app to emit a scenario library.
const SCENARIO_SOURCE_RON: &str = r#"
Pipeline(
    name: "scenario-source-proof",
    triggers: [ Manual ],
    stages: [
        Stage(name: "provision", steps: [
            SpinFleetMember(
                name: "dwarves",
                image: "localhost/dwarves-gui:scenario-test",
                ports: [7788],
                env: [],
                surface: ["ok_tab", "bad_tab"],
            ),
        ]),
        Stage(name: "drive", needs: ["provision"], steps: [
            RobotDriveMember(
                member: "dwarves",
                egui_scenarios: ["ok_tab", "bad_tab"],
                web_scenarios: ["ok_tab", "bad_tab"],
            ),
        ]),
        Stage(name: "gate", needs: ["drive"], steps: [
            CoverageGate(workspace: "dwarves", allow: []),
        ]),
    ],
)
"#;

/// RED-WHEN-BROKEN: the fleet LOADS a member's recorded scenarios (round-tripped
/// through the SAME serde-JSON wire form dwarves-gui `scenarios --emit` prints, via
/// a real `<dir>/<member>.scenarios.json` file), drives them over the wire, and folds
/// every step into the grid — a multi-step `ok_tab` scenario passes only if BOTH its
/// steps drove in order (the remote counter reaches 2), while a `bad_tab` scenario
/// whose step asserts an impossible value FAILS over the wire and reds its `@fat` node.
/// Break the emit format, the loader, or the step drive and this goes red, not green.
#[test]
fn scenario_source_drives_recorded_steps_and_a_bad_step_reds_the_node() {
    // ── author two scenarios in Rust, then EMIT them as the wire JSON the fleet loads.
    // `ok_tab`: TWO ordered bumps — the second (`/clicks == 2`) only holds if the first
    // step actually drove, so a pass proves both steps folded. `bad_tab`: one step that
    // asserts a value the remote app never reaches (the deliberately-failing step).
    let ok = Scenario::new("ok_tab").steps([
        UiStep::new("remote bump → clicks == 1")
            .action(Action::ClickButton("Bump".into()))
            .assert(Assert::State { pointer: "/clicks".into(), expected: json!(1) }),
        UiStep::new("remote bump again → clicks == 2")
            .action(Action::ClickButton("Bump".into()))
            .assert(Assert::State { pointer: "/clicks".into(), expected: json!(2) }),
    ]);
    let bad = Scenario::new("bad_tab").steps([
        UiStep::new("broken expectation → clicks == 999")
            .action(Action::ClickButton("Bump".into()))
            .assert(Assert::State { pointer: "/clicks".into(), expected: json!(999) }),
    ]);
    let emitted = serde_json::to_string_pretty(&vec![ok, bad]).expect("scenarios serialize to wire JSON");

    // Write the emitted JSON to `<dir>/dwarves.scenarios.json` and load it by member
    // name — the EXACT `$FLEET_SCENARIOS_DIR/<member>.scenarios.json` mechanism.
    let dir = std::env::temp_dir().join(format!("fleet-scenario-source-{}", std::process::id()));
    std::fs::create_dir_all(&dir).unwrap();
    std::fs::write(dir.join("dwarves.scenarios.json"), &emitted).unwrap();

    let source = ScenarioSource::from_dir(&dir, ["dwarves", "korp"]).expect("scenarios load from dir");
    assert!(source.has("dwarves"), "dwarves' emitted scenarios loaded by member name");
    assert!(!source.has("korp"), "a member with no scenarios file keeps the probe fallback");
    assert_eq!(source.steps_for("dwarves", "ok_tab").len(), 2, "the ok_tab scenario's 2 steps loaded");
    assert!(source.steps_for("dwarves", "no_such_tab").is_empty(), "an unknown tab → empty (probe fallback)");

    // Wrap a live-host opener (a fresh Bump `CmdJsonServer` per tab) with the source:
    // `open()` delegates to the inner wire; `scenario()` serves the loaded steps.
    let wire = ScenarioWire::new(AllDrivenWire, source);

    let pipeline = Pipeline::from_ron_str(SCENARIO_SOURCE_RON).expect("descriptor parses");
    let outcome = FleetHarness::mock("dwarves")
        .with_egui_wire(wire)
        .run(&pipeline)
        .expect("pipeline lowers");

    assert_eq!(outcome.spun, vec!["dwarves".to_string()], "the member reached Ready");

    // The clean multi-step scenario PASSED — both bumps folded (counter reached 2).
    let ok_row = outcome
        .rows
        .iter()
        .find(|r| r.test_name == "dwarves::egui::ok_tab")
        .expect("an egui row exists for ok_tab");
    assert_eq!(ok_row.status, "pass", "the loaded multi-step scenario drove clean: {}", ok_row.message);

    // The broken step FAILED over the wire and its reason folded into the grid row.
    let bad_row = outcome
        .rows
        .iter()
        .find(|r| r.test_name == "dwarves::egui::bad_tab")
        .expect("an egui row exists for bad_tab");
    assert_eq!(bad_row.status, "fail", "the deliberately-failing loaded step must RED the row");
    assert!(
        bad_row.message.contains("999"),
        "the failed step's reason folds into the grid row: {}",
        bad_row.message
    );

    // The failed step reds exactly `bad_tab@fat`; `ok_tab@fat` + both thin legs cover.
    assert!(!outcome.gate_green(), "a failed recorded step reds the gate: {}", outcome.gate_summary());
    let gate = outcome.gate.as_ref().unwrap();
    assert_eq!(gate.gap.missing.len(), 1, "exactly the failed tab's fat node is uncovered");
    assert_eq!(
        gate.gap.missing[0].key_str(),
        "viz_tab:dwarves:bad_tab@fat",
        "the uncovered node names the fat leg of the tab whose recorded step failed"
    );
    assert_eq!(gate.gap.covered, 3, "ok_tab@fat + ok_tab@thin + bad_tab@thin cover; only bad_tab@fat reds");

    let _ = std::fs::remove_file(dir.join("dwarves.scenarios.json"));
    let _ = std::fs::remove_dir(&dir);
}

// ── the Unix-socket wire source — a REAL local socket, no container ───────────
// `UnixEguiWire` is the first-real-app transport (dwarves-gui's `--robot-remote
// <socket>` mount answers it). This proves `open()` genuinely connects an
// `IoChannel` to a LIVE `CmdJsonServer` over a real `AF_UNIX` socket — single
// threaded (ROOT LAW #0): connect into the listener backlog, accept, then pump the
// host's `serve_one` in lockstep between the client's write and read.
#[cfg(unix)]
#[test]
fn unix_egui_wire_opens_a_live_socket_to_a_cmdjson_server() {
    use nornir_fleet::UnixEguiWire;
    use nornir_robotui::cmd_json::{CmdRequest, CmdResponse};
    use std::io::BufReader;
    use std::os::unix::net::UnixListener;

    let base = std::env::temp_dir().join(format!("fleet-unix-egui-{}", std::process::id()));
    std::fs::create_dir_all(&base).unwrap();
    let mut wire = UnixEguiWire::in_dir(&base);
    let path = wire.socket_path("dwarves");
    assert!(
        path.file_name().and_then(|n| n.to_str()) == Some("dwarves.sock"),
        "a member routes to <dir>/<member>.sock: {path:?}"
    );
    let _ = std::fs::remove_file(&path);

    let listener = UnixListener::bind(&path).unwrap();
    let mut chan = wire.open("dwarves", &[]).unwrap().expect("unix wire opens a channel");
    let (server_stream, _) = listener.accept().unwrap();
    let mut server_chan =
        IoChannel::new(BufReader::new(server_stream.try_clone().unwrap()), server_stream);
    let mut server = bump_host();

    chan.write_line(&serde_json::to_string(&CmdRequest::Ping).unwrap()).unwrap();
    assert!(server.serve_one(&mut server_chan).unwrap(), "host served over the socket");
    let line = chan.read_line().unwrap().expect("a reply line off the socket");
    assert!(
        matches!(serde_json::from_str::<CmdResponse>(&line).unwrap(), CmdResponse::Pong),
        "UnixEguiWire.open() connected to a live CmdJsonServer over a real AF_UNIX socket"
    );

    let _ = std::fs::remove_file(&path);
    let _ = std::fs::remove_dir(&base);
}

/// The production `MountRouter` routes HONESTLY over a REAL socket: a mounted member
/// (`dwarves`) dials its live `CmdJsonServer` (`open`→`Ok(Some)`), while an un-mounted
/// member (`korp`) is probe-only (`open`→`Ok(None)`) — never a false co-located
/// channel. This is the exact seam the fleet-of-five run installs.
#[cfg(unix)]
#[test]
fn mount_router_drives_mounted_probes_unmounted() {
    use nornir_fleet::MountRouter;
    use std::os::unix::net::UnixListener;

    let base = std::env::temp_dir().join(format!("fleet-mount-router-{}", std::process::id()));
    std::fs::create_dir_all(&base).unwrap();
    let mut router = MountRouter::new(&base, ["dwarves"]);
    assert!(router.is_mounted("dwarves"));
    assert!(!router.is_mounted("korp"));

    // The un-mounted app is probe-only — no socket dialed, `Ok(None)`.
    assert!(
        router.open("korp", &[]).unwrap().is_none(),
        "an un-mounted app is probe-only (Ok(None)), never a false channel"
    );

    // The mounted app dials its LIVE socket and gets a real channel.
    let path = base.join("dwarves.sock");
    let _ = std::fs::remove_file(&path);
    let _listener = UnixListener::bind(&path).unwrap();
    let opened = router.open("dwarves", &[]).unwrap();
    assert!(opened.is_some(), "the mounted app dials its live CmdJsonServer socket");

    let _ = std::fs::remove_file(&path);
    let _ = std::fs::remove_dir(&base);
}