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

nordisk/facett

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