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");
}