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

nordisk/nornir

tests/cli_autopilot.rs

//! AUT9 — the autopilot CLI check (the CLI twin of the viz atom-walk).
//!
//! `nornir commands` self-describes the CLI as a uniform [`CommandOutcome`] whose
//! `data` is the recursive leaf-command list (the autopilot's surface source — the
//! CLI twin of MCP `tools/list`). From that one source we assert:
//!   1. the outcome is SANE (the `CommandOutcome` sane-data contract), and
//!   2. EVERY enumerated leaf is REAL — it parses (`--help` short-circuits required
//!      args), so a dropped/mis-wired command fails here mechanically, no hand list.
//!
//! This is the reachability floor; per-command sane-DATA checks land as each handler
//! migrates to `CommandOutcome` (see `.nornir/cli-command-contract.md`).

use nornir::cli_outcome::CommandOutcome;
use std::process::Command;
use std::sync::OnceLock;

/// A dedicated, empty HOME for the CLI invocations so they are hermetic: they read
/// their own fresh `~/.nornir` (empty index / no funnel) instead of the caller's
/// real one. Without this, `index stats` opens the *real* `~/.nornir` index redb —
/// which fails with "Database already open" whenever a `nornir-server` (or any other
/// nornir process) is running against that same home. The read-only commands we run
/// all emit a well-formed `CommandOutcome` against an empty home (`index stats` ->
/// ok:true empty counts, `funnel show` -> the ok:false empty=RED path), so an
/// isolated home preserves exactly what these tests assert while removing the shared
/// -lock flake.
fn test_home() -> &'static std::path::Path {
    static HOME: OnceLock<tempfile::TempDir> = OnceLock::new();
    HOME.get_or_init(|| tempfile::tempdir().expect("create isolated test HOME"))
        .path()
}

fn nornir() -> Command {
    let mut c = Command::new(env!("CARGO_BIN_EXE_nornir"));
    c.env("HOME", test_home());
    c
}

fn commands_outcome() -> CommandOutcome {
    let out = nornir()
        .args(["commands", "--json"])
        .output()
        .expect("run `nornir commands --json`");
    assert!(
        out.status.success(),
        "`nornir commands --json` failed: {}",
        String::from_utf8_lossy(&out.stderr)
    );
    serde_json::from_slice(&out.stdout).unwrap_or_else(|e| {
        panic!(
            "stdout is not a CommandOutcome: {e}\n{}",
            String::from_utf8_lossy(&out.stdout)
        )
    })
}

#[test]
fn commands_self_description_is_a_sane_command_outcome() {
    let o = commands_outcome();
    assert_eq!(o.command, "commands");
    assert!(
        o.is_sannr(),
        "the `commands` outcome must pass the sannr check (truth-verdict): {o:?}"
    );

    let leaves: Vec<String> = o
        .data
        .as_array()
        .expect("data is the leaf-command array")
        .iter()
        .map(|v| v.as_str().expect("each leaf is a string").to_string())
        .collect();

    // recursion happened: far more leaves than the ~25 top-level roots.
    assert!(
        leaves.len() > 25,
        "expected recursive leaves (>25), got {}",
        leaves.len()
    );
    // confirmed nested leaves are present (self + parity with cli_parity.rs).
    for leaf in ["commands", "mimir deps-of", "diagram timeline"] {
        assert!(
            leaves.contains(&leaf.to_string()),
            "missing leaf `{leaf}` in {leaves:?}"
        );
    }
}

#[test]
fn every_enumerated_command_is_real() {
    let leaves: Vec<String> = commands_outcome()
        .data
        .as_array()
        .unwrap()
        .iter()
        .map(|v| v.as_str().unwrap().to_string())
        .collect();

    // Each leaf must parse — `--help` exits 0 even past required args. A command the
    // autopilot enumerates but that no longer wires up fails here (no silent rot).
    let mut broken = Vec::new();
    for leaf in &leaves {
        let mut args: Vec<&str> = leaf.split(' ').collect();
        args.push("--help");
        let st = nornir().args(&args).output().expect("run leaf --help");
        if !st.status.success() {
            broken.push(format!(
                "{leaf} ({})",
                String::from_utf8_lossy(&st.stderr).trim()
            ));
        }
    }
    assert!(
        broken.is_empty(),
        "enumerated commands that don't parse: {broken:#?}"
    );
}

/// AUT9 step 4 — the generic sannr smoke test.
///
/// Beyond "every leaf parses", this RUNS a curated set of read-only commands with
/// `--json` and asserts each emits a WELL-FORMED [`CommandOutcome`] (parses, with
/// `command`/`ok`/`data` present and `command` == the leaf we asked for). This is
/// the CLI twin of the viz `state_json` check: it proves migrated handlers actually
/// route through `CommandOutcome::print` and that the empty=RED path renders as a
/// proper outcome (`ok:false` + a clean human reason) rather than a crash or garbage.
///
/// ## SAFETY LIST (read this before adding to it)
/// `SAFE_READ_COMMANDS` is an explicit *safety* allow list — it is NOT a completeness
/// list. Completeness comes from the autopilot gate + `every_enumerated_command_is_real`
/// above, which already cover EVERY leaf. This list exists only to pick the subset that
/// is safe to actually execute with no arguments and no populated warehouse:
///   * READ-only, no required positional args, supports the global `--json`, and
///   * emits a `CommandOutcome` even when the warehouse/funnel is empty (empty ⇒ a
///     well-formed `ok:false` outcome, NOT a panic and NOT free-form text).
/// We DELIBERATELY never run side-effecting / arg-hungry commands here — `serve`, `viz`,
/// anything `*run*`, `publish`, `stage`, `pack`, `install`, `key`, `register`, `fetch`,
/// `populate`, `update-db`, etc. Those mutate state, hit the network, or block; their
/// reachability is still proven by the `--help` parse check, not by execution.
const SAFE_READ_COMMANDS: &[&str] = &[
    // The reference AUT9 migration — self-describing, no config, always has real data.
    "commands",
    // Reads the forbidden-path table from policy alone; no warehouse fixture needed.
    "guard status",
    // Per-corpus index counts; well-formed CommandOutcome whether or not the index is built.
    "index stats",
    // Funnel DAG read. With no plan it returns the empty=RED path: ok:false + a clean
    // human reason ("funnel is empty …"), exactly the well-formed empty outcome we assert.
    "funnel show",
];

#[test]
fn safe_read_commands_emit_conformant_outcomes() {
    for &leaf in SAFE_READ_COMMANDS {
        let mut args: Vec<&str> = leaf.split(' ').collect();
        args.push("--json");
        let out = nornir().args(&args).output().expect("run safe leaf --json");
        assert!(
            out.status.success(),
            "`nornir {leaf} --json` exited non-zero: {}",
            String::from_utf8_lossy(&out.stderr)
        );

        // stdout must be a CommandOutcome (the CLI's state_json) — parses with the
        // contract fields present. A handler that printed ad-hoc text fails here.
        let o: CommandOutcome = serde_json::from_slice(&out.stdout).unwrap_or_else(|e| {
            panic!(
                "`nornir {leaf} --json` stdout is not a CommandOutcome: {e}\n{}",
                String::from_utf8_lossy(&out.stdout)
            )
        });

        // The outcome self-identifies as the leaf we asked for (no cross-wired arm).
        assert_eq!(
            o.command, leaf,
            "outcome.command mismatch for `{leaf}`: {o:?}"
        );
        // `data` is always present on the wire (serde): null is a legitimate empty value.
        let parsed: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
        for key in ["command", "ok", "data"] {
            assert!(
                parsed.get(key).is_some(),
                "`{leaf}` outcome missing `{key}`: {parsed}"
            );
        }

        if leaf == "commands" {
            // The reference migration must pass the FULL sannr check (truth-verdict):
            // ok + non-empty data + no error atom. This is the one we run as a real
            // autopilot assertion, not merely "well-formed".
            assert!(o.is_sannr(), "`commands` outcome must be sannr: {o:?}");
        } else {
            // Data commands have no fixture here, so we assert WELL-FORMEDNESS, not
            // is_sannr(): the contract holds (parses + command set) and, crucially, the
            // empty/no-data path renders as a proper outcome — a failed outcome carries a
            // clean human reason (no panic atom leaked), an ok outcome carries real data.
            assert!(
                !o.command.is_empty(),
                "`{leaf}` outcome has empty command: {o:?}"
            );
            if !o.ok {
                assert!(
                    !o.human.trim().is_empty(),
                    "`{leaf}` failed outcome must explain itself (empty=RED with a reason): {o:?}"
                );
            } else {
                assert!(
                    !matches!(o.data, serde_json::Value::Null),
                    "`{leaf}` claims ok but carries null data: {o:?}"
                );
            }
        }
    }
}