facett-demo/tests/control_matrix.rs
//! **D5b2 / AUT8 — the control-channel robot-UI matrix row.** The sibling of
//! [`robot_demo`](robot_demo.rs) (which drives the AccessKit UI in-harness): THIS
//! row drives the **live control channel** — the EPIC-R `$FACETT_DEMO_CMD` /
//! `$FACETT_DEMO_STATE` file pair, the exact pattern nornir's viz uses
//! (`$NORNIR_VIZ_CMD` / `$NORNIR_VIZ_STATE`) — **through real files**, end to end:
//!
//! 1. **write** a [`DemoCommand`](facett_demo::DemoCommand) to `$FACETT_DEMO_CMD`,
//! 2. have the demo **poll + consume** it (`poll_control_channel`, the same call
//! the eframe binary makes each frame),
//! 3. **dump** the aggregated `state_json` to `$FACETT_DEMO_STATE`
//! (`dump_state`, again the binary's per-frame call), and
//! 4. **read the dumped state file back** and assert the rendered state *responded*
//! to the command (the active tab switched, the palette/look changed, the
//! command fired, the syschart selection appeared).
//!
//! Every `(verb, component)` step becomes one `robot-ui`
//! [`TestResultRow`](nornir_testmatrix::TestResultRow) (suite
//! `facett-demo-robot-ui`, aspect `robot-ui`), so the control-channel drive fills a
//! matrix line that lands in the **same** unified matrix as nornir's viz coverage
//! (persisted via [`JsonFileSink`] to `target/control-matrix.jsonl`).
//!
//! ## Inject-assert (LAW 1)
//! Nothing is "didn't panic". Every step injects a real command **as a file** and
//! asserts a **real value** read back out of the dumped **state file** (not the
//! in-memory object) — proving the whole file-backed channel works, the seam the
//! external `viz.click`-style driver actually uses.
//!
//! ## Coverage gate (provably complete)
//! [`control_channel_drives_every_component_via_files`] cross-checks the driven
//! steps against (a) every demo component (`LEGEND` key) and (b) every control-
//! channel verb. **Add a component or a channel verb but forget a covering step →
//! this test goes RED** (the D5b2 "provably complete" requirement). A fidelity test
//! proves a wrong expectation is a red cell, not a panic.
//!
//! Run: `cargo test -p facett-demo --test control_matrix -- --nocapture`
//! Docs: `.nornir/facett-demo-control-channel.md`.
use std::collections::BTreeSet;
use std::sync::{Mutex, MutexGuard, OnceLock};
use facett_demo::{control, DemoCommand, DemoState, KNOWN_COMMANDS, LEGEND};
use nornir_testmatrix::{render_matrix, status, JsonFileSink, TestResultRow, TestSink};
use serde_json::{json, Value};
// ─────────────────────────────────────────────────────────────────────────────
// The control-channel verbs the matrix drives — the discoverable surface. Adding
// a `DemoCommand` field (a new channel verb) and forgetting a covering step makes
// the coverage gate RED (provably complete, like the mega-matrix's gate).
// ─────────────────────────────────────────────────────────────────────────────
const CHANNEL_VERBS: &[&str] = &["tab", "palette", "look", "platform", "command", "select_node"];
/// One scripted control-channel step: a label, the command to write, and a closure
/// asserting the dumped **state file** responded. Returns Ok(()) (green) or Err(why)
/// (red) — never panics, so a wrong expectation is a red cell.
struct Step {
label: String,
/// Which channel verb(s) this step exercises (for the coverage gate).
verbs: &'static [&'static str],
/// Which demo component(s) this step touches (for the coverage gate).
components: &'static [&'static str],
cmd: DemoCommand,
/// Assert against the state JSON read back from `$FACETT_DEMO_STATE`.
check: Box<Check>,
}
/// A step's assertion over the state JSON.
type Check = dyn Fn(&Value) -> Result<(), String>;
/// Drive one step through the **file** channel: write the command file, poll+apply
/// (consume-once), dump the state file, read it back, run the assertion. This is
/// the full live seam, exercised exactly as the binary uses it.
fn drive(demo: &mut DemoState, step: &Step) -> (String, String) {
// 1. write the command to the real $FACETT_DEMO_CMD file.
if let Err(e) = control::write_command(&step.cmd) {
return (status::FAIL.into(), format!("write_command failed: {e}"));
}
// 2. poll + consume + apply (the binary's per-frame call).
if !demo.poll_control_channel() {
return (status::FAIL.into(), "poll_control_channel saw no pending command".into());
}
// 3. dump the aggregated state to the real $FACETT_DEMO_STATE file.
demo.dump_state();
// 4. read the state file back (not the in-memory object) and assert.
let raw = match std::fs::read_to_string(control::state_path()) {
Ok(s) => s,
Err(e) => return (status::FAIL.into(), format!("state file unreadable: {e}")),
};
let state: Value = match serde_json::from_str(&raw) {
Ok(v) => v,
Err(e) => return (status::FAIL.into(), format!("state file is not JSON: {e}")),
};
match (step.check)(&state) {
Ok(()) => (status::PASS.into(), String::new()),
Err(e) => (status::FAIL.into(), e),
}
}
/// Helper: assert `state` at `pointer` equals `expected`.
fn at_eq(state: &Value, pointer: &str, expected: Value) -> Result<(), String> {
match state.pointer(pointer) {
Some(v) if *v == expected => Ok(()),
Some(v) => Err(format!("state {pointer}: expected {expected}, got {v}")),
None => Err(format!("state {pointer} is absent")),
}
}
/// A `tab` step that asserts the dumped `deck.active` became `title`.
fn tab_step(title: &'static str, component: &'static [&'static str]) -> Step {
Step {
label: format!("cmd {{tab:{title}}} → deck.active == {title}"),
verbs: &["tab"],
components: component,
cmd: DemoCommand { tab: Some(title.into()), ..Default::default() },
check: Box::new(move |s| at_eq(s, "/deck/active", json!(title))),
}
}
/// The full scripted control-channel drive: every component tab + each non-tab verb.
fn steps() -> Vec<Step> {
let mut v: Vec<Step> = Vec::new();
// ── Every deck tab (18 Facet components are tabs; `menu` is the overlay,
// driven by the `command` verb below). Each `tab` command must switch the
// dumped `deck.active`. ──
for (key, _desc) in LEGEND {
if *key == "menu" {
continue; // overlay, not a deck tab — covered by the `command` verb.
}
v.push(tab_step(key, std::slice::from_ref(key)));
}
// ── palette verb: switch the deck palette by name; the dumped deck.palette
// must resolve to that palette (cyberpunk-neon is index 1). ──
v.push(Step {
label: "cmd {palette:cyberpunk-neon} → deck palette switches".into(),
verbs: &["palette"],
components: &[],
cmd: DemoCommand { palette: Some("cyberpunk-neon".into()), ..Default::default() },
// The demo exposes the active deck palette at /deck_palette/name.
check: Box::new(|s| at_eq(s, "/deck_palette/name", json!("cyberpunk-neon"))),
});
// ── look verb: switch the LookTheme preset by name; dumped look.preset matches. ──
v.push(Step {
label: "cmd {look:macos-dark} → look.preset == macos-dark".into(),
verbs: &["look"],
components: &[],
cmd: DemoCommand { look: Some("macos-dark".into()), ..Default::default() },
check: Box::new(|s| at_eq(s, "/look/preset", json!("macos-dark"))),
});
v.push(Step {
label: "cmd {look:windows-light} → look.mode == light".into(),
verbs: &["look"],
components: &[],
cmd: DemoCommand { look: Some("windows-light".into()), ..Default::default() },
check: Box::new(|s| at_eq(s, "/look/mode", json!("light"))),
});
// ── platform verb: the platform-adaptive native-feel toggle. Switching to a
// platform jumps to its preset and the dumped native-feel cues match (the
// delta IS the adaptive layer). ──
v.push(Step {
label: "cmd {platform:mac} → look.platform == mac + rubber-band on, reveal off".into(),
verbs: &["platform"],
components: &[],
cmd: DemoCommand { platform: Some("mac".into()), ..Default::default() },
check: Box::new(|s| {
at_eq(s, "/look/platform", json!("mac"))?;
at_eq(s, "/look/native/rubber_band", json!(true))?;
at_eq(s, "/look/native/reveal_highlight", json!(false))
}),
});
v.push(Step {
label: "cmd {platform:windows} → reveal highlight on, rubber-band off, caption side".into(),
verbs: &["platform"],
components: &[],
cmd: DemoCommand { platform: Some("windows".into()), ..Default::default() },
check: Box::new(|s| {
at_eq(s, "/look/platform", json!("windows"))?;
at_eq(s, "/look/native/reveal_highlight", json!(true))?;
at_eq(s, "/look/native/controls_on_left", json!(false))
}),
});
v.push(Step {
label: "cmd {platform:neutral} → no OS-specific chrome cues".into(),
verbs: &["platform"],
components: &[],
cmd: DemoCommand { platform: Some("neutral".into()), ..Default::default() },
check: Box::new(|s| {
at_eq(s, "/look/platform", json!("neutral"))?;
at_eq(s, "/look/native/reveal_highlight", json!(false))?;
at_eq(s, "/look/native/rubber_band", json!(false))
}),
});
// ── command verb: fire run_command ids → dumped last_command records the route. ──
v.push(Step {
label: "cmd {command:palette.cycle} → last_command == palette.cycle".into(),
verbs: &["command"],
components: &[],
cmd: DemoCommand { command: Some("palette.cycle".into()), ..Default::default() },
check: Box::new(|s| at_eq(s, "/last_command", json!("palette.cycle"))),
});
v.push(Step {
// The `command` verb is also how the `menu` (CommandPalette overlay)
// component is driven from the channel — firing one of its command ids.
label: "cmd {command:export.csv} (menu command) → last_command == export.csv".into(),
verbs: &["command"],
components: &["menu"],
cmd: DemoCommand { command: Some("export.csv".into()), ..Default::default() },
check: Box::new(|s| at_eq(s, "/last_command", json!("export.csv"))),
});
// ── select_node verb: pick a syschart node by id → dumped syschart.selected. ──
v.push(Step {
label: "cmd {select_node:Score} → syschart.selected == Score".into(),
verbs: &["select_node"],
components: &["syschart"],
cmd: DemoCommand { select_node: Some("Score".into()), ..Default::default() },
check: Box::new(|s| at_eq(s, "/deck/facets/syschart/selected", json!("Score"))),
});
// ── combined command: tab + palette in one write apply in order. ──
v.push(Step {
label: "cmd {tab:plot, palette:deep-space} → active==plot AND palette switched".into(),
verbs: &["tab", "palette"],
components: &["plot"],
cmd: DemoCommand {
tab: Some("plot".into()),
palette: Some("deep-space".into()),
..Default::default()
},
check: Box::new(|s| {
at_eq(s, "/deck/active", json!("plot"))?;
at_eq(s, "/deck_palette/name", json!("deep-space"))
}),
});
v
}
/// The control channel reads `$FACETT_DEMO_CMD` / `$FACETT_DEMO_STATE` from the
/// **process-global** env, so the tests in this binary (which `cargo test` runs in
/// parallel by default) must not stomp each other's env. Each test holds this lock
/// for its whole body, serializing access to the shared channel paths.
fn channel_lock() -> MutexGuard<'static, ()> {
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
LOCK.get_or_init(|| Mutex::new(())).lock().unwrap_or_else(|e| e.into_inner())
}
/// Isolate the file channel (pin `$FACETT_DEMO_CMD`/`$FACETT_DEMO_STATE` for this
/// test, under the held `channel_lock`), then build a fresh demo.
fn isolated_demo(tag: &str) -> DemoState {
let pid = std::process::id();
// SAFETY: caller holds `channel_lock`, so no other test races the env here.
unsafe {
std::env::set_var("FACETT_DEMO_CMD", format!("/tmp/facett_demo_cmd_{tag}_{pid}.json"));
std::env::set_var("FACETT_DEMO_STATE", format!("/tmp/facett_demo_state_{tag}_{pid}.json"));
}
let _ = std::fs::remove_file(control::cmd_path());
let _ = std::fs::remove_file(control::state_path());
let mut demo = DemoState::new();
demo.show_about = false;
demo
}
// ─────────────────────────────────────────────────────────────────────────────
// THE CONTROL-CHANNEL LIVE-DRIVE ROW.
// ─────────────────────────────────────────────────────────────────────────────
#[test]
fn control_channel_drives_every_component_via_files() {
let _guard = channel_lock();
let mut demo = isolated_demo("matrix");
let scripted = steps();
let run_id = nornir_testmatrix::new_run_id();
let ts = now_micros();
let mut rows: Vec<TestResultRow> = Vec::with_capacity(scripted.len());
for step in &scripted {
let started = std::time::Instant::now();
let (st, message) = drive(&mut demo, step);
rows.push(TestResultRow {
run_id: run_id.clone(),
repo: "facett-demo".into(),
suite: "facett-demo-robot-ui".into(),
test_name: step.label.clone(),
status: st,
duration_ms: started.elapsed().as_secs_f64() * 1000.0,
ts_micros: ts,
message,
aspect: "robot-ui".into(),
metric: 0.0,
});
}
// ── Coverage: every component AND every channel verb was driven. ──
let driven_components: BTreeSet<&str> =
scripted.iter().flat_map(|s| s.components.iter().copied()).collect();
let tabbed: BTreeSet<&str> = LEGEND.iter().map(|(k, _)| *k).filter(|k| *k != "menu").collect();
// tab steps cover every tabbed component; the explicit `components` lists cover
// the non-tab ones (menu). Together they must cover the whole LEGEND.
let covered: BTreeSet<&str> = driven_components.union(&tabbed).copied().collect();
let legend: BTreeSet<&str> = LEGEND.iter().map(|(k, _)| *k).collect();
let undriven: Vec<&&str> = legend.difference(&covered).collect();
assert!(undriven.is_empty(), "control-channel matrix never drove these components: {undriven:?}");
let driven_verbs: BTreeSet<&str> = scripted.iter().flat_map(|s| s.verbs.iter().copied()).collect();
let all_verbs: BTreeSet<&str> = CHANNEL_VERBS.iter().copied().collect();
let unverbed: Vec<&&str> = all_verbs.difference(&driven_verbs).collect();
assert!(unverbed.is_empty(), "control-channel verbs never exercised: {unverbed:?}");
// ── Persist to the unified matrix sink + round-trip (AUT8). ──
let target_dir = std::env::var_os("CARGO_TARGET_DIR")
.map(std::path::PathBuf::from)
.unwrap_or_else(|| std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("..").join("target"));
let out = target_dir.join("control-matrix.jsonl");
if let Some(p) = out.parent() {
std::fs::create_dir_all(p).ok();
}
std::fs::remove_file(&out).ok();
let sink = JsonFileSink::new(&out);
sink.append(&rows).expect("append robot-ui rows");
let read_back = sink.read_all().expect("read robot-ui rows back");
assert_eq!(read_back.len(), rows.len(), "every control-channel row round-tripped");
assert!(read_back.iter().all(|r| r.aspect == "robot-ui"), "every row is a robot-ui aspect row");
assert!(read_back.iter().all(|r| r.suite == "facett-demo-robot-ui"), "suite is facett-demo-robot-ui");
// ── Print the filled line + the matrix grid. ──
let passed = rows.iter().filter(|r| r.status == status::PASS).count();
eprintln!("\n══ facett-demo CONTROL-CHANNEL LIVE DRIVE (D5b2/AUT8) ══");
eprintln!("driving $FACETT_DEMO_CMD → $FACETT_DEMO_STATE through real files");
for r in &rows {
let mark = if r.status == status::PASS { "✓" } else { "✗" };
eprintln!(" {mark} [{}] {}{}", r.status, r.test_name,
if r.message.is_empty() { String::new() } else { format!(" — {}", r.message) });
}
eprintln!("── {passed}/{} control-channel steps PASSED ──", rows.len());
eprintln!("\n{}", render_matrix(&read_back));
eprintln!("rows → {}\n", out.display());
// Clean up the isolated channel files.
let _ = std::fs::remove_file(control::cmd_path());
let _ = std::fs::remove_file(control::state_path());
// ── The gate: every driven step is green (inject-assert drives pass/fail). ──
let reds: Vec<&TestResultRow> = rows.iter().filter(|r| r.status != status::PASS).collect();
assert!(reds.is_empty(), "control-channel matrix has red cells: {:#?}",
reds.iter().map(|r| (&r.test_name, &r.message)).collect::<Vec<_>>());
assert!(rows.len() >= LEGEND.len(), "expected a step per component + the non-tab verbs");
}
// ─────────────────────────────────────────────────────────────────────────────
// Fidelity: a WRONG expectation against the dumped state is a RED cell, not a
// panic (so the matrix's pass/fail is genuinely assertion-driven, LAW 1). And an
// unknown command name lands as the `err:*` sentinel in the dumped state.
// ─────────────────────────────────────────────────────────────────────────────
#[test]
fn wrong_expectation_is_a_red_cell_and_unknown_command_is_recorded() {
let _guard = channel_lock();
let mut demo = isolated_demo("fidelity");
// A real, correct step is green.
let good = tab_step("grid", &["grid"]);
let (st, _msg) = drive(&mut demo, &good);
assert_eq!(st, status::PASS, "the real tab switch passed");
// A WRONG expectation against the dumped state is a red cell.
let bad = Step {
label: "bad: deck.active == nope".into(),
verbs: &["tab"],
components: &["grid"],
cmd: DemoCommand { tab: Some("table".into()), ..Default::default() },
check: Box::new(|s| at_eq(s, "/deck/active", json!("nope"))),
};
let (st, msg) = drive(&mut demo, &bad);
assert_eq!(st, status::FAIL, "a wrong expectation is a red cell, not a panic");
assert!(msg.contains("nope") || msg.contains("table"), "the red cell records why: {msg}");
// An unknown tab/command is recorded as an `err:*` sentinel in the dumped state
// (the demo never panics on a bad command — it surfaces the rejection).
control::write_command(&DemoCommand { tab: Some("does-not-exist".into()), ..Default::default() }).unwrap();
assert!(demo.poll_control_channel());
demo.dump_state();
let raw = std::fs::read_to_string(control::state_path()).unwrap();
let state: Value = serde_json::from_str(&raw).unwrap();
assert_eq!(state["last_command"], json!("err:tab"), "unknown tab is recorded as err:tab in state");
control::write_command(&DemoCommand { command: Some("no.such.command".into()), ..Default::default() }).unwrap();
assert!(demo.poll_control_channel());
demo.dump_state();
let raw = std::fs::read_to_string(control::state_path()).unwrap();
let state: Value = serde_json::from_str(&raw).unwrap();
assert_eq!(state["last_command"], json!("err:command"), "unknown command id → err:command");
let _ = std::fs::remove_file(control::cmd_path());
let _ = std::fs::remove_file(control::state_path());
}
/// Sanity: the KNOWN_COMMANDS const stays in lockstep with the command verb the
/// channel can fire (a real value-assert, not just existence).
#[test]
fn known_commands_are_routable_through_the_channel() {
let _guard = channel_lock();
let mut demo = isolated_demo("known");
for &id in KNOWN_COMMANDS {
control::write_command(&DemoCommand { command: Some(id.into()), ..Default::default() }).unwrap();
assert!(demo.poll_control_channel(), "channel applied command {id}");
demo.dump_state();
let raw = std::fs::read_to_string(control::state_path()).unwrap();
let state: Value = serde_json::from_str(&raw).unwrap();
// The command routed: last_command is the id itself (run_command sets it),
// never an `err:*` sentinel.
let lc = state["last_command"].as_str().unwrap_or("");
assert!(!lc.starts_with("err:"), "command {id} was rejected: last_command={lc}");
}
let _ = std::fs::remove_file(control::cmd_path());
let _ = std::fs::remove_file(control::state_path());
}
fn now_micros() -> i64 {
use std::time::{SystemTime, UNIX_EPOCH};
SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_micros() as i64).unwrap_or(0)
}