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

nordisk/edda

crates/step-board/tests/board_red.rs

//! **The producer registry, proven RED first.**
//!
//! Every assertion here pins a value the board can only produce by reading its input.
//! The order of the file is deliberate: the *partial*, *out-of-order* and *unmeasured*
//! cases come first, because a test that only ever drives a clean four-step run to
//! completion proves nothing about a bar drawn mid-run — and a bar that cannot show
//! anything but "complete" is the hollow green this seam exists to prevent.
//!
//! No GPU, no window: the board is pure data and `state_json` is pure JSON.

use step_board::{Board, OrderFault, StepDef, StepRun, board_state_json, shared};
use facett_jobview::{ProgressMeter, StepPhase};

/// dwarves' populate chain: clone the members, build the warehouse, scan the lenses.
fn populate_steps() -> Vec<StepDef> {
    vec![
        StepDef::new("clone", "clone the members"),
        StepDef::new("warehouse", "build the warehouse"),
        StepDef::new("lens", "scan the lenses"),
    ]
}

fn fresh() -> (Board, &'static str) {
    let b = Board::default();
    (b, "populate/nordisk")
}

// ── 1. THE ORDER FAULT — the reason `enter` returns a Result at all ────────────

/// **A step that overtakes an unfinished predecessor must be REFUSED.**
///
/// This is the load-bearing case: dwarves' clone and warehouse-build have different
/// owners, and nothing but this stops the second announcing itself while the first is
/// still cloning. A board that silently forward-filled `clone` to `Done` would publish a
/// race as tidy progress.
#[test]
fn entering_a_step_over_an_unfinished_predecessor_is_an_order_fault() {
    let (b, id) = fresh();
    b.begin(id, "nordisk", populate_steps(), "Re-sync clicked");
    b.enter(id, 0).expect("step 1 of a fresh run is in order");

    // The overtaking publish: warehouse-build announces itself while clone is ACTIVE.
    let err = b.enter(id, 1).expect_err("the order check MUST refuse this publish");

    match &err {
        OrderFault::PredecessorUnfinished { entered, blocker, entered_key, blocker_key, blocker_phase } => {
            assert_eq!(*entered, 1, "the step that overtook");
            assert_eq!(*blocker, 0, "the step it overtook");
            assert_eq!(entered_key, "warehouse");
            assert_eq!(blocker_key, "clone");
            assert_eq!(*blocker_phase, StepPhase::Active, "clone was still running — that is the fault");
        }
        other => panic!("wrong fault variant: {other:?}"),
    }
    assert_eq!(err.at(), 1, "the fault pins to the step that paints red");

    // The message names BOTH steps — "out of order" without saying which two is not
    // something anyone can act on at 03:00.
    let msg = err.message();
    assert!(msg.contains("warehouse"), "names the overtaking step: {msg}");
    assert!(msg.contains("clone"), "names the blocked step: {msg}");
    assert!(msg.contains("ACTIVE"), "says what the blocker was doing: {msg}");

    // **And the bar goes red WITHOUT the caller's cooperation.** A producer that drops
    // the Result must not get a plausible-looking bar.
    let run = b.get(id).expect("the run exists");
    assert_eq!(run.steps[1].phase, StepPhase::Failed, "the overtaking step paints RED");
    assert_eq!(
        run.steps[0].phase,
        StepPhase::Active,
        "and clone was NOT silently stamped Done — that forward-fill is the bug"
    );
    assert_eq!(run.error.as_deref(), Some(msg.as_str()), "the run carries the reason verbatim");
    assert_eq!(run.failed(), Some(1));
    assert!(run.caption().contains("FAILED"), "the caption says so: {}", run.caption());
    assert!(run.step_line(1).contains("OUT OF ORDER"), "the hover line says so: {}", run.step_line(1));
}

/// **A producer that DISCARDS the `Result` still gets a red step.**
///
/// This is the half that protects us from ourselves, and it is the reason the check is
/// worth having rather than a lint. `enter` returning `Err` only helps a caller who looks;
/// a caller who writes `let _ = …` — or who uses `advance`, or who is three call frames
/// from the place that would handle it — gets nothing from a return value. So the refusal
/// is applied to the board itself, by construction: the overtaking step is `Failed`, the
/// run carries the reason, and the bar paints red whether or not anyone cooperated.
#[test]
fn a_producer_that_ignores_the_result_still_gets_a_red_bar() {
    let (b, id) = fresh();
    b.begin(id, "nordisk", populate_steps(), "Re-sync clicked");
    b.enter(id, 0).expect("step 1 of a fresh run is in order");

    // The careless producer. No `?`, no `expect`, no match — the value is thrown away.
    let _ = b.enter(id, 1);

    let run = b.get(id).expect("the run exists");
    assert_eq!(
        run.steps[1].phase,
        StepPhase::Failed,
        "the overtaking step must be RED even though nobody read the Result"
    );
    assert_eq!(
        run.steps[0].phase,
        StepPhase::Active,
        "and the predecessor was NOT forward-filled to Done — that inference is the bug"
    );
    assert_eq!(run.failed(), Some(1));
    assert!(
        run.error.as_deref().is_some_and(|e| e.contains("OUT OF ORDER")),
        "the run carries the reason: {:?}",
        run.error
    );

    // And it is visible in every surface a human or a robot reads, not just in the struct.
    assert!(run.caption().contains("FAILED"), "caption: {}", run.caption());
    assert!(run.hover_text().contains("OUT OF ORDER"), "hover: {}", run.hover_text());
    let j = run.state_json();
    assert_eq!(j["failed"], 1, "state_json pins the red step");
    assert_eq!(j["steps"][1]["phase"], "failed");
    assert!(
        j["error"].as_str().is_some_and(|e| e.contains("OUT OF ORDER")),
        "state_json carries the reason: {}",
        j["error"]
    );
}

/// **A step that goes BACKWARDS behind one already under way is also refused.**
#[test]
fn entering_a_step_behind_one_already_running_is_an_order_fault() {
    let (b, id) = fresh();
    b.begin(id, "nordisk", populate_steps(), "Re-sync clicked");
    b.complete_through(id, 0); // clone genuinely finished — SAID, not inferred
    b.enter(id, 1).expect("clone → warehouse is in order");

    let err = b.enter(id, 0).expect_err("the chain must not go backwards");
    match &err {
        OrderFault::Backwards { entered, ahead, entered_key, ahead_key } => {
            assert_eq!(*entered, 0);
            assert_eq!(*ahead, 1);
            assert_eq!(entered_key, "clone");
            assert_eq!(ahead_key, "warehouse");
        }
        other => panic!("wrong fault variant: {other:?}"),
    }
    assert!(err.message().contains("backwards"), "{}", err.message());
}

/// **The order check must not fire on an ORDERLY chain.** Without this the two tests
/// above pass for a board that refuses everything — a guard that always says red is as
/// blind as one that always says green.
#[test]
fn an_orderly_chain_raises_no_fault() {
    let (b, id) = fresh();
    b.begin(id, "nordisk", populate_steps(), "Re-sync clicked");
    b.enter(id, 0).expect("step 1");
    b.complete_through(id, 0);
    b.enter(id, 1).expect("step 2 after step 1 finished");
    b.complete_through(id, 1);
    b.enter(id, 2).expect("step 3 after step 2 finished");

    let run = b.get(id).unwrap();
    assert_eq!(run.active(), Some(2));
    assert_eq!(run.failed(), None, "an orderly chain has no red");
    assert_eq!(run.done(), 2);
    assert!(!run.is_complete(), "step 3 is still running");
}

// ── 2. MEASURED vs UNMEASURED — the fraction must be earned ───────────────────

/// **A measured step reads `Determinate` at its REAL fraction; an unmeasured one reads
/// `Indeterminate` and publishes NO `fraction` key.**
///
/// A zero in that slot reads as a measurement — "0% done" — when the truth is "nobody
/// counted". The two must be distinguishable in the JSON, not only in the paint.
#[test]
fn only_a_counted_step_is_determinate_and_an_uncounted_one_publishes_no_fraction() {
    let (b, id) = fresh();
    b.begin(id, "nordisk", populate_steps(), "Re-sync clicked");
    b.enter(id, 0).expect("step 1");
    // 12 of 19 members cloned — a real count off the members' sync states.
    b.set_count(id, 0, 12, 19);

    let run = b.get(id).unwrap();

    // ── the MEASURED step ──
    let clone = &run.steps[0];
    assert_eq!(clone.count, Some((12, 19)));
    match clone.meter() {
        ProgressMeter::Determinate(f) => {
            // NOT the identity value, and not a constant: 12/19 and nothing else.
            let want = 12.0f32 / 19.0f32;
            assert!((f - want).abs() < 1e-6, "fraction is the measured 12/19, got {f}");
            assert!(f > 0.6 && f < 0.64, "a genuine partial fill, not 0.0 or 1.0: {f}");
        }
        other => panic!("a counted step must be Determinate, got {other:?}"),
    }
    assert!(clone.line.contains("12/19"), "the hover line carries the count: {}", clone.line);

    // ── the UNMEASURED steps ──
    assert_eq!(run.steps[1].count, None, "nobody counted the warehouse build");
    assert_eq!(run.steps[1].meter(), ProgressMeter::None, "pending + uncounted draws no bar");

    // Drive step 2 active so the *active-and-uncounted* case is covered too.
    b.complete_through(id, 0);
    b.enter(id, 1).expect("step 2");
    let run = b.get(id).unwrap();
    assert_eq!(
        run.steps[1].meter(),
        ProgressMeter::Indeterminate,
        "active with no counter is Indeterminate — a POSITIVE statement, never Determinate(0.0)"
    );

    // ── and the published JSON agrees, key for key ──
    let j = run.state_json();
    let steps = j["steps"].as_array().expect("steps array");

    let m0 = &steps[0]["meter"];
    assert_eq!(m0["kind"], "determinate");
    assert_eq!(m0["current"], 12);
    assert_eq!(m0["total"], 19);
    assert!(m0["fraction"].as_f64().unwrap() > 0.6);

    let m1 = &steps[1]["meter"];
    assert_eq!(m1["kind"], "indeterminate");
    assert!(
        m1.get("fraction").is_none(),
        "an unmeasured step must publish NO fraction key — a zero there reads as a \
         measurement. got: {m1}"
    );
    assert!(m1.get("current").is_none(), "and no current: {m1}");
}

/// **A restart must not carry the previous run's numbers.** A bar showing 12/19 from
/// work that is no longer happening is worse than one showing nothing.
#[test]
fn begin_clears_the_previous_runs_measurements() {
    let (b, id) = fresh();
    b.begin(id, "nordisk", populate_steps(), "first");
    b.enter(id, 0).unwrap();
    b.set_count(id, 0, 12, 19);
    assert_eq!(b.get(id).unwrap().steps[0].count, Some((12, 19)));

    let generation = b.begin(id, "nordisk", populate_steps(), "Re-sync clicked again");
    let run = b.get(id).unwrap();
    assert_eq!(generation, 2, "the run number makes the restart legible");
    assert_eq!(run.generation, 2);
    assert_eq!(run.steps[0].count, None, "the new run has measured NOTHING yet");
    assert_eq!(run.steps[0].phase, StepPhase::Pending);
    assert_eq!(run.error, None, "and the old failure is gone");
    assert!(run.reason_line().contains("Re-sync clicked again"), "{}", run.reason_line());
}

// ── 3. THE PARTIAL RUN — the bar must be able to say something other than done ─

#[test]
fn a_partial_run_reads_partial_everywhere() {
    let (b, id) = fresh();
    b.begin(id, "nordisk", populate_steps(), "Re-sync clicked");
    b.complete_through(id, 0);
    b.enter(id, 1).unwrap();

    let run = b.get(id).unwrap();
    assert_eq!(run.total(), 3);
    assert_eq!(run.done(), 1, "one of three — NOT three");
    assert_eq!(run.human_index(), 2, "the human reads 'step 2 of 3'");
    assert!(!run.is_complete());
    assert!(run.in_progress());

    let cap = run.caption();
    assert_eq!(cap, "run #1 · step 2 of 3 · build the warehouse");
    assert!(!cap.contains("3 of 3"), "the caption must not claim a finished run: {cap}");

    // The pane and state_json read the SAME strings — one writer, two consumers.
    let meter = run.to_meter();
    assert_eq!(meter.caption_text(), cap, "the widget draws the producer's sentence verbatim");
    assert_eq!(meter.hover_text(), run.hover_text());
    assert_eq!(run.state_json()["caption"], cap);
}

/// **A run that has not started says so** — never "0 of 3 complete", never a full bar.
#[test]
fn an_unstarted_run_is_not_a_complete_one() {
    let run = StepRun::new("populate/x", "x", populate_steps());
    assert_eq!(run.generation, 0);
    assert_eq!(run.caption(), "not started");
    assert!(!run.is_complete());
    assert_eq!(run.reason_line(), "", "no reason is invented");

    // And an EMPTY run is not complete either — the zero-step hollow green.
    let empty = StepRun::new("populate/empty", "empty", vec![]);
    assert!(!empty.is_complete(), "a zero-step bar must never read complete");
}

// ── 4. FAILURE AND NOTES ──────────────────────────────────────────────────────

#[test]
fn a_failure_keeps_finished_steps_finished_and_carries_the_error() {
    let (b, id) = fresh();
    b.begin(id, "nordisk", populate_steps(), "Re-sync clicked");
    b.complete_through(id, 0);
    b.enter(id, 1).unwrap();
    b.fail(id, None, "warehouse build: dep_graph_edges write failed");

    let run = b.get(id).unwrap();
    assert_eq!(run.steps[0].phase, StepPhase::Done, "clone really did finish — it stays green");
    assert_eq!(run.steps[1].phase, StepPhase::Failed);
    assert_eq!(run.steps[2].phase, StepPhase::Pending);
    assert!(run.step_line(1).contains("dep_graph_edges"), "{}", run.step_line(1));
    assert!(run.caption().contains("FAILED"), "{}", run.caption());
    assert_eq!(run.state_json()["error"], "warehouse build: dep_graph_edges write failed");
}

#[test]
fn a_note_explains_a_run_that_is_underway_before_step_one() {
    let (b, id) = fresh();
    b.begin(id, "nordisk", populate_steps(), "Re-sync clicked");
    b.note(id, "waiting on the server to open the workspace");

    let run = b.get(id).unwrap();
    assert_eq!(run.active(), None, "no step is active yet");
    assert_eq!(
        run.caption(),
        "run #1 · 0 of 3 steps complete · waiting on the server to open the workspace",
        "a bar saying only '0 of 3' tells a watcher nothing about whether it is advancing"
    );
}

// ── 5. THE BOARD ITSELF ───────────────────────────────────────────────────────

#[test]
fn the_board_rolls_up_many_jobs_and_a_stray_publish_invents_none() {
    let b = Board::default();
    b.begin("a", "A", populate_steps(), "r");
    b.enter("a", 0).unwrap();
    b.begin("z", "Z", populate_steps(), "r");
    b.finish("z");

    // A publish for a job that never began must NOT create a row.
    b.enter("ghost", 0).expect("a stray publish is a no-op, not a fault");
    b.set_count("ghost", 0, 1, 2);
    b.fail("ghost", None, "boom");
    assert!(b.get("ghost").is_none(), "a stray publish must never invent a run");

    // Out-of-range index on a real run is likewise a no-op, not a panic.
    b.enter("a", 99).expect("out of range is a no-op");

    let j = board_state_json(&b, "jobs");
    assert_eq!(j["jobs"], 2, "the ghost is not counted");
    assert_eq!(j["in_progress"], 1);
    assert_eq!(j["complete"], 1);
    assert_eq!(j["failed"], 0);
    assert_eq!(j["runs"][0]["id"], "a", "id order");
    assert_eq!(j["runs"][1]["id"], "z");

    b.forget("a");
    assert_eq!(board_state_json(&b, "jobs")["jobs"], 1);

    // The count key is the HOST's word, and it really is the parameter — not a rename
    // that happens to look right for one caller.
    assert_eq!(board_state_json(&b, "collectors")["collectors"], 1);
    assert!(
        board_state_json(&b, "collectors").get("jobs").is_none(),
        "one key, named by the host — not both"
    );
}

// ── 6. THE SERIAL PRODUCER, AND THE SEED ──────────────────────────────────────

/// **`advance` walks the chain and cannot raise a fault** — it only ever leaves a
/// finished step for the one immediately after it. A serial producer never has to think
/// about indices, and never has to think about the order check either.
#[test]
fn advance_walks_the_chain_and_cannot_break_the_order() {
    let (b, id) = fresh();
    b.begin(id, "nordisk", populate_steps(), "Re-sync clicked");
    b.enter(id, 0).expect("step 1");

    for expect in [1usize, 2] {
        assert_eq!(b.advance(id), Some(expect), "advance enters exactly the next step");
        let run = b.get(id).unwrap();
        assert_eq!(run.active(), Some(expect));
        assert_eq!(run.done(), expect, "every earlier step really is finished");
        assert!(run.failed().is_none(), "a walked chain never faults");
    }
    assert_eq!(b.advance(id), None, "past the last step there is nothing to enter");

    let run = b.get(id).unwrap();
    assert_eq!(run.done(), 3);
    assert!(run.is_complete());
    assert!((run.fraction() - 1.0).abs() < 1e-6);
    // And advancing a job that has no run, or no active step, invents nothing.
    assert_eq!(b.advance("ghost"), None);
    assert_eq!(b.advance(id), None, "a complete run has no active step to leave");
}

/// **The seed records a verdict the caller already has, and ONLY that verdict.** The
/// all-absent case is the one that catches a seed wired to "mark everything done".
#[test]
fn the_seed_carries_the_verdict_and_only_the_verdict() {
    let (b, id) = fresh();
    b.begin(id, "nordisk", populate_steps(), "server start — resumed from what is on disk");

    let already = ["clone", "warehouse"];
    let n = b.seed_done(id, |k| already.contains(&k));
    assert_eq!(n, 2, "two steps seeded — the seed says how many, so silence is visible");

    let run = b.get(id).unwrap();
    assert_eq!(run.done(), 2, "two of three — NOT three");
    assert!(!run.is_complete(), "the unseeded step is still work to do");
    assert_eq!(run.phases(), vec![StepPhase::Done, StepPhase::Done, StepPhase::Pending]);
    assert_eq!(run.human_index(), 2);
    assert!((run.fraction() - 2.0 / 3.0).abs() < 1e-6, "{}", run.fraction());
    assert_eq!(run.caption(), "run #1 · 2 of 3 steps complete · idle");

    // A verdict that says nothing seeds NOTHING — the bar starts at zero, not full.
    let (b2, id2) = fresh();
    b2.begin(id2, "nordisk", populate_steps(), "server start");
    assert_eq!(b2.seed_done(id2, |_| false), 0);
    let r2 = b2.get(id2).unwrap();
    assert_eq!(r2.done(), 0, "an empty verdict seeds a bar at ZERO");
    assert!(!r2.is_complete());
    assert!((r2.fraction() - 0.0).abs() < 1e-6);
}

/// **The Jobs row carries the producer's own sentences, and its `kind` is the host's.**
#[test]
fn the_job_row_re_authors_nothing_and_names_three_outcomes() {
    use facett_jobview::JobStatus;
    use step_board::{job_entries, to_job_entry};

    let (b, id) = fresh();
    b.begin(id, "nordisk", populate_steps(), "Re-sync clicked");
    b.complete_through(id, 0);
    b.enter(id, 1).unwrap();
    b.set_count(id, 1, 3, 7);

    let run = b.get(id).unwrap();
    let e = to_job_entry(&run, "populate");
    assert_eq!(e.kind, "populate", "the host's own word, not a hardcoded one");
    assert_eq!(e.status, JobStatus::Running, "an active step is a running row");
    assert_eq!(e.target, "nordisk");

    let m = e.steps_meter().expect("the row carries a step meter");
    assert_eq!(m.caption, run.caption(), "verbatim, not re-derived");
    assert_eq!(m.caption_text(), run.caption());
    assert_eq!(m.hover, run.hover_text());
    assert_eq!(m.generation, 1);
    assert_eq!(m.human_index(), 2);
    for i in 0..3 {
        assert_eq!(m.steps[i].line, run.step_line(i), "step {i}'s line is the producer's");
    }
    // The measured count survives the trip into the row — the Determinate half.
    assert_eq!(m.steps[1].count, Some((3, 7)));
    assert!(matches!(m.steps[1].meter(), ProgressMeter::Determinate(_)));

    // Three DISTINCT outcomes, not one.
    b.fail(id, None, "boom");
    assert_eq!(to_job_entry(&b.get(id).unwrap(), "populate").status, JobStatus::Failed);
    b.finish(id);
    let done = to_job_entry(&b.get(id).unwrap(), "populate");
    assert_eq!(done.status, JobStatus::Done);
    assert!(done.steps_meter().unwrap().is_complete());

    assert_eq!(job_entries(&b, "populate").len(), 1, "one bar per run on the board");
}

/// **The run-level compat keys are the meter's own numbers**, not a second count that
/// could drift from it.
#[test]
fn the_run_level_step_numbers_agree_with_the_meters() {
    let (b, id) = fresh();
    b.begin(id, "nordisk", populate_steps(), "Re-sync clicked");
    b.complete_through(id, 0);
    b.enter(id, 1).unwrap();

    let run = b.get(id).unwrap();
    let j = run.state_json();
    assert_eq!(j["step_index"], j["index"], "one number, two spellings");
    assert_eq!(j["step_total"], j["total"]);
    assert_eq!(j["step_index"], 2);
    assert_eq!(j["step_total"], 3);
    // The run-level fraction is FINISHED STEPS ONLY — 1 of 3, never blended with the
    // active step's own fill.
    assert!((j["fraction"].as_f64().unwrap() - 1.0 / 3.0).abs() < 1e-6, "{}", j["fraction"]);
    b.set_count(id, 1, 6, 7);
    let j = b.get(id).unwrap().state_json();
    assert!(
        (j["fraction"].as_f64().unwrap() - 1.0 / 3.0).abs() < 1e-6,
        "a step's own measurement must NOT move the run-level fraction: {}",
        j["fraction"]
    );
}

/// Two handles on the same namespace see the same board — a producer thread and a pane
/// must not each get their own.
#[test]
fn shared_namespaces_are_one_board_and_distinct_ones_are_not() {
    shared("t-same").reset();
    shared("t-other").reset();

    shared("t-same").begin("j", "J", populate_steps(), "r");
    assert!(shared("t-same").get("j").is_some(), "the second handle sees the first's write");
    assert!(shared("t-other").get("j").is_none(), "a different namespace is a different board");
}