facett-kanban/tests/matrix.rs
//! **The functional matrix cell for facett-kanban** — the pane emitted NO
//! testmatrix row of either aspect before this file (see
//! `.nornir/docs/test-matrix-inventory-history.md`, "~10 panes emit no cell").
//!
//! ONE call: the shared generic-`Facet` probe in `facett_core::harness`
//! headless-renders the board, drives the JSON robot seam and
//! `facett_core::testmatrix::probe_and_emit` turns the result into the
//! `headless_render` + `responds_to_input` rows. No bespoke harness, no twinned
//! sink (L5).
use facett_kanban::Board;
#[test]
fn kanban_probe_emits_render_and_input_cells() {
let mut view = Board::local();
let script = [
r#"{"select":"SUPP-1"}"#, // headless card click
r#"{"move":{"card":"SUPP-1","to":"done"}}"#, // headless drag-and-drop
r#""clear_selection""#,
];
let probe = facett_core::testmatrix::probe_and_emit("facett-kanban", &mut view, &script);
assert!(probe.drew(), "the board must tessellate to vertices");
assert!(probe.cardinality() > 0, "state_json reports the columns/cards");
assert!(probe.responded(), "update_json must drive state — a dead seam is a regression");
// EXACT transitions. Before the move `active` holds 3 cards over its WIP of 2
// (breach); after it holds 2 and the breach clears — the oracle reads the
// WIP contract, not merely "something changed".
let col = |s: &serde_json::Value, id: &str| -> serde_json::Value {
s["columns"].as_array().unwrap().iter().find(|c| c["id"] == id).unwrap().clone()
};
assert_eq!(col(&probe.initial_state, "active")["count"], 3);
assert_eq!(col(&probe.initial_state, "active")["wip_breach"], true);
assert_eq!(probe.steps[0].state["selected"], "SUPP-1");
assert_eq!(col(&probe.steps[1].state, "active")["count"], 2, "the card left the column");
assert_eq!(col(&probe.steps[1].state, "active")["wip_breach"], false, "the breach cleared");
assert_eq!(col(&probe.steps[1].state, "done")["count"], 1);
assert!(probe.steps[2].state["selected"].is_null());
// The headless move queued the SAME payload a real drop does.
let moves = view.take_moves();
assert_eq!(moves.len(), 1);
assert_eq!(moves[0].card, "SUPP-1");
assert_eq!(moves[0].from, "active");
assert_eq!(moves[0].to, "done");
}
/// RED-when-broken: the headless move seam must reject an unknown card, an
/// unknown target column, and a no-op move to the card's current column — none of
/// which may queue a `Move` the host would then apply as a bogus transition.
#[test]
fn move_card_rejects_unknown_and_no_op_targets() {
let mut b = Board::local();
assert!(!b.move_card("NOPE", "done"), "unknown card");
assert!(!b.move_card("SUPP-1", "nowhere"), "unknown column");
assert!(!b.move_card("SUPP-1", "active"), "already there — not a transition");
assert!(b.take_moves().is_empty(), "no rejected move may reach the host");
assert!(!b.select_id("NOPE"));
assert_eq!(b.selected(), None);
}