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

nordisk/facett

facett-jobview/tests/robot_step_bar.rs

//! **Robot-UI drive of the collector step bar** — a REAL egui render of a real
//! `JobList`, observed through `Facet::state_json`.
//!
//! This is the headless path on purpose. Robot-UI requirement 10b (measured 2026-08-02,
//! `robot-ui-for-utvecklare.md`): the `--robot-watch` backend can put a window on screen
//! but cannot reliably manipulate widgets on oden, so a state assertion must come from
//! the headless `egui_kittest` drive. Pixels are a separate, weaker claim.
//!
//! What it proves that the pure tests cannot: the bar goes through **tessellation** with
//! its steps intact, and the string a robot reads out of `state_json` after a real frame
//! is the string the producer wrote. A `StepMeter` that decoded correctly but painted
//! nothing would pass the pure tests and fail here.

use facett_jobview::{JobEntry, JobList, JobStatus, StepCell, StepMeter, StepPhase};

/// Two collectors, mid-run — the posture `KORP_ROBOT_COLLECTOR_STEPS=midrun` drives korp
/// into, rebuilt here as plain data so facett stays free of any korp dependency.
fn two_collectors() -> Vec<JobEntry> {
    let sweden = StepMeter {
        generation: 1,
        caption: "run #1 · step 3 of 4 · relation clip + geometry assembly".into(),
        hover: "run #1 started because: not provisioned yet\n\
                a  convert pass 1 (node coord store)  DONE\n\
                b  convert pass 2 (nodes/ways/relations)  DONE\n\
                c  relation clip + geometry assembly  ACTIVE\n\
                d  tile pyramid  PENDING"
            .into(),
        steps: vec![
            StepCell::new("a", "convert pass 1 (node coord store)", StepPhase::Done),
            StepCell::new("b", "convert pass 2 (nodes/ways/relations)", StepPhase::Done),
            StepCell::new("c", "relation clip + geometry assembly", StepPhase::Active),
            StepCell::new("d", "tile pyramid", StepPhase::Pending),
        ],
    };
    let norway = StepMeter {
        generation: 1,
        caption: "run #1 · step 1 of 4 · convert pass 1 (node coord store)".into(),
        hover: "run #1 started because: the previous cut failed — retrying".into(),
        steps: vec![
            StepCell::new("a", "convert pass 1 (node coord store)", StepPhase::Active),
            StepCell::new("b", "convert pass 2 (nodes/ways/relations)", StepPhase::Pending),
            StepCell::new("c", "relation clip + geometry assembly", StepPhase::Pending),
            StepCell::new("d", "tile pyramid", StepPhase::Pending),
        ],
    };
    vec![
        {
            let mut e = JobEntry::new("map-regions/sweden", "collector", JobStatus::Running);
            e.target = "Sweden".into();
            e.with_steps(&sweden)
        },
        {
            let mut e = JobEntry::new("map-regions/norway", "collector", JobStatus::Running);
            e.target = "Norway".into();
            e.with_steps(&norway)
        },
        // A plain ledger row with no steps — the two kinds must coexist in one list.
        JobEntry::new("clip-2026", "clip", JobStatus::Done),
    ]
}

/// **The drive.** Render for real, then read the bar back out of `state_json`.
#[test]
fn the_step_bar_survives_a_real_render_and_reads_back_partial() {
    let mut view = JobList::new("Jobs");
    view.set_jobs(two_collectors());

    let probe = facett_core::testmatrix::probe_and_emit(
        "facett-jobview/step-bar",
        &mut view,
        &[r#"{"select":"map-regions/sweden"}"#],
    );

    // The rows really tessellated — a bar that decoded but painted nothing fails here.
    assert!(probe.drew(), "the job tree with its step bars must tessellate to vertices");
    assert!(probe.cardinality() > 0);
    assert!(probe.responded(), "the JSON drive seam is live");

    let jobs = probe.final_state["jobs"].as_array().expect("jobs array");
    assert_eq!(jobs.len(), 3, "two collectors + one ledger row");

    // ── The RED-side assertion: after a real frame, the bar still says THREE of four.
    let se = jobs.iter().find(|j| j["id"] == "map-regions/sweden").expect("sweden row");
    assert_eq!(se["steps"]["index"], 3, "step 3 of 4, through a real render");
    assert_eq!(se["steps"]["total"], 4);
    assert_eq!(se["steps"]["done"], 2);
    assert_eq!(se["steps"]["complete"], false, "NOT full");
    assert_eq!(
        se["steps"]["caption"], "run #1 · step 3 of 4 · relation clip + geometry assembly",
        "the string a robot reads is the string the producer wrote"
    );
    // The four steps are individually addressable, each with its own phase and its own
    // hover line — this is "hovra så ser man vilket steg som är grönt", as data.
    let cells = se["steps"]["steps"].as_array().unwrap();
    assert_eq!(cells.len(), 4);
    assert_eq!(cells[0]["phase"], "done");
    assert_eq!(cells[1]["phase"], "done");
    assert_eq!(cells[2]["phase"], "active");
    assert_eq!(cells[3]["phase"], "pending");
    assert_eq!(cells[2]["line"], "c  relation clip + geometry assembly  ACTIVE");
    assert!(
        se["steps"]["hover"].as_str().unwrap().contains("not provisioned yet"),
        "the hover carries why this run started"
    );

    // ── Two collectors, two DIFFERENT bars. One bar per collector is the whole ask.
    let no = jobs.iter().find(|j| j["id"] == "map-regions/norway").expect("norway row");
    assert_eq!(no["steps"]["index"], 1, "Norway is on step 1");
    assert_ne!(no["steps"]["caption"], se["steps"]["caption"], "the bars say different things");

    // ── A row with no steps publishes null, not an empty bar.
    let clip = jobs.iter().find(|j| j["id"] == "clip-2026").expect("ledger row");
    assert!(clip["steps"].is_null());
}

/// **A restart, through a real render.** Same list, re-set with the collector begun
/// again — the run number must be what distinguishes it from lost ground, on screen.
#[test]
fn a_restart_renders_as_a_new_run_not_a_regression() {
    let mut view = JobList::new("Jobs");
    view.set_jobs(two_collectors());
    let before = facett_core::Facet::state_json(&view);
    let b = &before["jobs"].as_array().unwrap().iter()
        .find(|j| j["id"] == "map-regions/sweden").unwrap()["steps"];
    assert_eq!(b["index"], 3);
    assert_eq!(b["generation"], 1);

    // The collector found a newer planet file overnight.
    let restarted = StepMeter {
        generation: 2,
        caption: "run #2 · step 1 of 4 · convert pass 1 (node coord store)".into(),
        hover: "run #2 started because: a newer planet file appeared".into(),
        steps: vec![
            StepCell::new("a", "convert pass 1 (node coord store)", StepPhase::Active),
            StepCell::new("b", "convert pass 2 (nodes/ways/relations)", StepPhase::Pending),
            StepCell::new("c", "relation clip + geometry assembly", StepPhase::Pending),
            StepCell::new("d", "tile pyramid", StepPhase::Pending),
        ],
    };
    let mut e = JobEntry::new("map-regions/sweden", "collector", JobStatus::Running);
    e.target = "Sweden".into();
    view.set_jobs(vec![e.with_steps(&restarted)]);

    let probe = facett_core::testmatrix::probe_and_emit(
        "facett-jobview/step-bar-restart",
        &mut view,
        &[r#"{"select":"map-regions/sweden"}"#],
    );
    assert!(probe.drew(), "the restarted bar renders");
    let a = &probe.final_state["jobs"].as_array().unwrap()[0]["steps"];

    // The bar DID go backwards on screen …
    assert_eq!(a["index"], 1);
    assert!(a["index"].as_u64() < b["index"].as_u64());
    // … and the run number rendered alongside it is what says that is a new run.
    assert_eq!(a["generation"], 2);
    assert!(
        a["caption"].as_str().unwrap().starts_with("run #2"),
        "the run number leads the sentence a human reads: {}",
        a["caption"]
    );
    assert!(
        a["hover"].as_str().unwrap().contains("a newer planet file appeared"),
        "and the reason is one hover away, so a reset bar does not read as a fault"
    );
}