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

nordisk/facett

facett-brokkr/examples/brokkr_watch.rs

//! # WATCH the brokkr pipeline BUILDER drive itself on a real window
//!
//! The **visible sibling** of the headless robot scenario in
//! `facett-brokkr/tests/robot.rs::scenario_builds_a_pipeline_by_click_and_fills_the_matrix`:
//! the SAME scripted build — Build mode → add a stage → add a step → set its
//! `needs` → flip to the DAG view → flip to the raw TOML — run through
//! `nornir-robotui`'s live WATCH backend (feature `watch`) so a human WATCHES the
//! robot steer a cursor across the builder, click each control, and see the
//! pipeline take shape: stages and steps appear, the DAG preview grows an edge,
//! and the emitted `brokkr.toml` fills in.
//!
//! One `Observable` + one `draw(ui, app, targets)` closure + one `Scenario` — the
//! 10-line adoption pattern from
//! `../../nornir/crates/nornir-robotui/.nornir/robotui-watch-mode.md`. The headless
//! matrix path in `tests/robot.rs` is untouched; this is purely additive.
//!
//! ## Run it
//!
//! ```sh
//! DISPLAY=:0 cargo run -p facett-brokkr --example brokkr_watch
//! # slower / hold the window open at the end:
//! DISPLAY=:0 ROBOTUI_WATCH_DELAY_MS=1600 ROBOTUI_WATCH_HOLD=1 \
//!     cargo run -p facett-brokkr --example brokkr_watch
//! ```

use facett_brokkr::{ContainerRun, Mode, Msg, PipelineBuilder, RunKind};
use nornir_robotui::{
    Action, Assert, Observable, RunMode, Scenario, Step, Targets, WatchConfig, run_scenario_mode,
};
use serde_json::{Value, json};

/// The live builder wrapped as an [`Observable`] — `state_json` forwards the
/// builder's own introspection blob, so the SAME JSON pointers the headless robot
/// scenario reads (`/mode`, `/stage_count`, `/step_count`, `/toml`) resolve here.
struct WatchBrokkr(PipelineBuilder);

impl Observable for WatchBrokkr {
    fn state_json(&self) -> Value {
        self.0.state_json()
    }
}

/// ONE closure, BOTH modes. The top control row records a compact button per
/// scenario step (the WATCH driver resolves an `Action`'s label to a recorded rect
/// and steers the cursor there); each button drives the builder through its public
/// API — the exact edits the headless scenario clicks. Below the row the LIVE
/// builder surface is drawn (`ui`), so the growing stage/step list, the DAG
/// preview and the raw TOML are all on screen to watch react.
fn draw(ui: &mut egui::Ui, app: &mut WatchBrokkr, t: &mut Targets) {
    ui.horizontal_wrapped(|ui| {
        if t.record("Build mode", ui.button("Build mode")).clicked() {
            app.0.set_mode(Mode::Build);
        }
        if t.record("Add stage 'build'", ui.button("Add stage 'build'"))
            .clicked()
        {
            app.0.set_name("nornir-ci");
            app.0.add_stage("build");
        }
        if t.record("Add step 'compile'", ui.button("Add step 'compile'"))
            .clicked()
        {
            app.0
                .add_step(0, "compile", RunKind::Variant("server".into()));
        }
        if t.record(
            "Add stage 'test' + step",
            ui.button("Add stage 'test' + step"),
        )
        .clicked()
        {
            app.0.add_stage("test");
            app.0.add_step(
                1,
                "unit",
                RunKind::Container(ContainerRun {
                    name: "rust:1.85".into(),
                    cmd: vec!["cargo".into(), "test".into()],
                }),
            );
        }
        if t.record("Set needs = compile", ui.button("Set needs = compile"))
            .clicked()
        {
            app.0.set_step_needs(1, 0, ["compile"]);
        }
        if t.record("DAG view", ui.button("DAG view")).clicked() {
            app.0.set_mode(Mode::Graph);
        }
        if t.record("Disconnect", ui.button("Disconnect")).clicked() {
            app.0.update(Msg::Disconnect {
                from: "compile".into(),
                to: "unit".into(),
            });
        }
        if t.record("Connect", ui.button("Connect")).clicked() {
            app.0.update(Msg::Connect {
                from: "compile".into(),
                to: "unit".into(),
            });
        }
        if t.record("Move compile", ui.button("Move compile"))
            .clicked()
        {
            app.0.update(Msg::MoveNode {
                node: "compile".into(),
                position: [40.0, 80.0],
            });
        }
        if t.record("Undo edit", ui.button("Undo edit")).clicked() {
            app.0.update(Msg::Undo);
        }
        if t.record("Redo edit", ui.button("Redo edit")).clicked() {
            app.0.update(Msg::Redo);
        }
        if t.record("TOML view", ui.button("TOML view")).clicked() {
            app.0.set_mode(Mode::Text);
        }
    });
    ui.separator();
    // The LIVE builder — the surface the robot's control row drives.
    app.0.ui(ui);
}

/// The self-driving WATCH scenario — the visible twin of the headless build:
/// stage → step → second stage/step → needs → DAG → TOML, asserting the observable
/// state after each driven step.
fn scenario() -> Scenario {
    Scenario::new("robot-brokkr-watch")
        .step(Step::new("open in Build mode").assert(Assert::State {
            pointer: "/mode".into(),
            expected: json!("Build"),
        }))
        .step(
            Step::new("add the 'build' stage")
                .action(Action::ClickButton("Add stage 'build'".into()))
                .assert(Assert::State {
                    pointer: "/stage_count".into(),
                    expected: json!(1),
                }),
        )
        .step(
            Step::new("add the 'compile' step")
                .action(Action::ClickButton("Add step 'compile'".into()))
                .assert(Assert::State {
                    pointer: "/step_count".into(),
                    expected: json!(1),
                }),
        )
        .step(
            Step::new("add a 'test' stage with a 'unit' step")
                .action(Action::ClickButton("Add stage 'test' + step".into()))
                .assert(Assert::State {
                    pointer: "/step_count".into(),
                    expected: json!(2),
                }),
        )
        .step(
            Step::new("wire unit needs compile — the DAG grows an edge")
                .action(Action::ClickButton("Set needs = compile".into()))
                .assert(Assert::StateContains {
                    pointer: "/toml".into(),
                    needle: "needs = [\"compile\"]".into(),
                }),
        )
        .step(
            Step::new("flip to the DAG preview")
                .action(Action::ClickButton("DAG view".into()))
                .assert(Assert::State {
                    pointer: "/mode".into(),
                    expected: json!("Graph"),
                }),
        )
        .step(
            Step::new("disconnect the dependency")
                .action(Action::ClickButton("Disconnect".into()))
                .assert(Assert::State {
                    pointer: "/valid".into(),
                    expected: json!(true),
                }),
        )
        .step(
            Step::new("connect through the graph reducer")
                .action(Action::ClickButton("Connect".into()))
                .assert(Assert::StateContains {
                    pointer: "/toml".into(),
                    needle: "needs = [\"compile\"]".into(),
                }),
        )
        .step(
            Step::new("move compile in session layout")
                .action(Action::ClickButton("Move compile".into()))
                .assert(Assert::State {
                    pointer: "/layout_overrides/compile".into(),
                    expected: json!([40.0, 80.0]),
                }),
        )
        .step(
            Step::new("undo the move")
                .action(Action::ClickButton("Undo edit".into()))
                .assert(Assert::State {
                    pointer: "/layout_overrides".into(),
                    expected: json!({}),
                }),
        )
        .step(
            Step::new("redo the move")
                .action(Action::ClickButton("Redo edit".into()))
                .assert(Assert::State {
                    pointer: "/layout_overrides/compile".into(),
                    expected: json!([40.0, 80.0]),
                }),
        )
        .step(
            Step::new("flip to the raw brokkr.toml")
                .action(Action::ClickButton("TOML view".into()))
                .assert(Assert::State {
                    pointer: "/mode".into(),
                    expected: json!("Text"),
                }),
        )
}

fn main() -> anyhow::Result<()> {
    let cfg = WatchConfig::from_env().with_title("facett — brokkr pipeline builder (watch)");
    let _ = RunMode::Headless; // (documents the other backend the headless test uses)
    run_scenario_mode(
        WatchBrokkr(PipelineBuilder::new("ci")),
        draw,
        &scenario(),
        "facett",
        RunMode::Watch,
        cfg,
    )?;
    Ok(())
}