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

nordisk/nornir

tests/cli_ui_parity.rs

//! AUT9 — CLI ⟺ UI parity tests for the migrated twins.
//!
//! The principle (`.nornir/cli-command-contract.md` §3): a CLI command and its viz
//! tab are **two faces of the SAME data**. The parity is asserted **at the lib
//! level, not by scraping stdout** — both faces consume the SAME shared lib fn /
//! domain value, so their data is equal *by construction*. Each test here INJECTS
//! real domain values (no warehouse, no RPC), calls the CLI outcome-builder, and
//! asserts its `data` equals the canonical lib value the viz tab also reads.
//!
//! Why lib-level and not "drive the egui pane too": the CLI builders + the shared
//! source fns are public lib API (`nornir::bench`, `nornir::registry`,
//! `nornir::mimir`, `nornir::arch`, `nornir::warehouse::coverage`), so the test is
//! feature-robust (passes with or without `--features viz`). Where a viz pane reads
//! the same value (`BenchLive` ← `query_bench_runs` → `Vec<BenchRun>`,
//! `CoverageLive` ← `coverage::latest` → `Vec<CoverageRow>`, the Nornir roster ←
//! `RosterRow`, the dep pane ← `CrossRepoEdge`, the Arch board ← `arch::trace_from`)
//! the shared fn is named in the test doc — that shared-source guarantee IS the
//! parity (the `cli_outcome.data ≈ viz_state_json[slice]` comparison is the edge
//! check on top of it).
//!
//! LAW: `is_sannr()` (truth-verdict), inject real values + assert real output.

use serde_json::json;

use nornir::bench::{BenchResult, BenchRun, TestOutcome};

// ════════════════════════════════════════════════════════════════════════════
// 1. `bench history` ⟺ Bench tab — shared source: the `bench_runs` table
//    (`query_bench_runs` → `Vec<BenchRun>`). The CLI shaper is
//    `nornir::bench::history_outcome`; the viz `BenchLive` folds the SAME
//    `Vec<BenchRun>` (`fold_live` / `bench_status`) for its per-bench pass/fail.
//    Parity: feed BOTH faces the SAME injected runs and assert they agree on the
//    runs surfaced AND on each bench's pass/fail verdict.
// ════════════════════════════════════════════════════════════════════════════

fn bench_result(name: &str, metric: &str, v: f64) -> BenchResult {
    let mut metrics = serde_json::Map::new();
    metrics.insert(metric.to_string(), json!(v));
    BenchResult {
        name: name.to_string(),
        metrics,
        source: Default::default(),
    }
}

fn bench_run(
    date: &str,
    machine: &str,
    results: Vec<BenchResult>,
    tests: Vec<TestOutcome>,
) -> BenchRun {
    BenchRun {
        // this fixture is where the run is made, so it mints the id; it did
        // not look at the box
        id: BenchRun::new_id(),
        machine_facts: None,
        date: date.to_string(),
        timestamp: Some(format!("{date}T00:00:00Z")),
        version: "0.1.0".into(),
        machine: machine.into(),
        cores: 8,
        results,
        tests,
    }
}

#[test]
fn bench_history_cli_and_bench_tab_share_bench_runs() {
    // INJECT the canonical source both faces read: a `Vec<BenchRun>` such as
    // `query_bench_runs` returns (fat) / the `Bench.History` RPC returns (thin).
    let runs = vec![
        bench_run(
            "2026-06-01",
            "ryzen",
            vec![bench_result("compress", "compress_mbs", 1200.0)],
            vec![TestOutcome {
                name: "compress".into(),
                passed: true,
                duration_ms: None,
                message: None,
            }],
        ),
        bench_run(
            "2026-06-03",
            "epyc",
            vec![bench_result("decompress", "decompress_mbs", 3400.0)],
            vec![TestOutcome {
                name: "decompress".into(),
                passed: false,
                duration_ms: None,
                message: Some("regressed".into()),
            }],
        ),
    ];

    // CLI face: the shared shaper.
    let cli = nornir::bench::history_outcome("znippy", runs.clone(), 0);
    assert!(cli.is_sannr(), "real runs are a true (sannr) CLI outcome");
    assert_eq!(cli.command, "bench history-show");

    // The CLI surfaces the SAME runs (newest first) it was fed — `data["runs"]`
    // round-trips back to the injected `BenchRun`s by construction.
    let cli_runs: Vec<BenchRun> = serde_json::from_value(cli.data["runs"].clone())
        .expect("data[runs] deserializes back to the canonical BenchRun");
    assert_eq!(cli_runs.len(), runs.len());
    // newest (2026-06-03 / epyc) first — the shaper's only transform.
    assert_eq!(cli_runs[0].machine, "epyc");
    assert_eq!(cli_runs[1].machine, "ryzen");

    // UI face (the Bench tab's verdict): a bench reads green/red from the SAME
    // `BenchRun.tests` the CLI carries. We assert the per-bench pass/fail derived
    // from the shared runs matches what `history_outcome` surfaced — same data,
    // two faces. (This mirrors `bench_live::bench_status`, the Bench tab's own
    // derivation, without depending on the private viz module.)
    let verdict = |bench: &str| -> Option<bool> {
        let leaf = bench.rsplit('.').next().unwrap_or(bench);
        for r in &runs {
            let names_it = r.results.iter().any(|x| x.name == bench || x.name == leaf);
            if !names_it && !r.tests.iter().any(|t| t.name == bench || t.name == leaf) {
                continue;
            }
            for t in &r.tests {
                if t.name == bench || t.name == leaf {
                    return Some(t.passed);
                }
            }
        }
        None
    };
    // The CLI's surfaced tests and the Bench-tab verdict agree, run for run.
    for r in &cli_runs {
        for t in &r.tests {
            assert_eq!(
                verdict(&t.name),
                Some(t.passed),
                "Bench tab verdict for `{}` must equal the run the CLI surfaced",
                t.name
            );
        }
    }
    assert_eq!(
        verdict("compress"),
        Some(true),
        "compress is green in both faces"
    );
    assert_eq!(
        verdict("decompress"),
        Some(false),
        "decompress is red in both faces"
    );
}

// ════════════════════════════════════════════════════════════════════════════
// 2. `workspace ls` ⟺ Nornir roster — shared projection: `RosterRow`.
//    BOTH CLI faces (fat `Registry::list`, thin `Workspaces.List`) map workspaces
//    into the SAME `RosterRow` and render via `registry::roster_outcome`; the
//    Nornir-tab roster renders that same projection. Parity: project an injected
//    workspace set into `RosterRow` and assert the CLI outcome echoes it verbatim.
// ════════════════════════════════════════════════════════════════════════════

#[test]
fn workspace_ls_cli_and_nornir_roster_share_rosterrow() {
    use nornir::registry::{RosterRow, roster_outcome};

    // INJECT the canonical roster projection both faces map into.
    let rows = vec![
        RosterRow {
            name: "nordisk".into(),
            mode: "monitored".into(),
            members: 8,
            last_synced: "2026-06-23".into(),
            populate: "green".into(),
            failing_member: String::new(),
            last_error: String::new(),
        },
        RosterRow {
            name: "holger".into(),
            mode: "local".into(),
            members: 1,
            last_synced: "never".into(),
            populate: "stale".into(),
            failing_member: String::new(),
            last_error: String::new(),
        },
    ];

    let cli = roster_outcome(&rows);
    assert!(
        cli.is_sannr(),
        "a populated roster is a true (sannr) CLI outcome"
    );
    assert_eq!(cli.command, "workspace ls");

    // The CLI `data` IS the shared `RosterRow` projection (serialized) — the exact
    // rows the Nornir-tab roster renders. Equal by construction.
    assert_eq!(
        cli.data,
        json!(rows),
        "CLI roster data == the shared RosterRow projection"
    );
    let arr = cli
        .data
        .as_array()
        .expect("roster data is an array of rows");
    assert_eq!(arr.len(), 2);
    assert_eq!(arr[0]["name"], json!("nordisk"));
    assert_eq!(arr[0]["mode"], json!("monitored"));
    assert_eq!(arr[0]["members"], json!(8));
    assert_eq!(arr[1]["last_synced"], json!("never"));
}

// ════════════════════════════════════════════════════════════════════════════
// 3. `mimir deps-of` ⟺ dep pane — shared source: the dep graph
//    (`WorkspaceGraph.edges` / `CrossRepoEdge`). `mimir::deps_of` computes the
//    Value the CLI surfaces; the viz dep pane reads the SAME `CrossRepoEdge`s
//    (`DepGraphSnapshot.edges`). The contract: `mimir::outcome(cmd, v).data == v`
//    — the CLI surfaces exactly the graph value the viz dep pane reads.
// ════════════════════════════════════════════════════════════════════════════

#[test]
fn mimir_deps_of_cli_surfaces_the_same_graph_value_the_dep_pane_reads() {
    use std::collections::{BTreeMap, BTreeSet};

    use nornir::mimir::{deps_of, outcome};
    use nornir::warehouse::dep_graph::{CrossRepoEdge, WorkspaceGraph};

    // INJECT a dep graph straight from edges (no cargo_metadata): nornir → skade → znippy.
    let edge = |from: &str, to: &str, via: &[&str]| CrossRepoEdge {
        from: from.into(),
        to: to.into(),
        via: via
            .iter()
            .map(|s| s.to_string())
            .collect::<BTreeSet<String>>(),
        kind: Default::default(),
    };
    let edges = vec![
        edge("nornir", "skade", &["skade-katalog"]),
        edge("skade", "znippy", &["znippy-iceberg"]),
    ];
    let g = WorkspaceGraph::from_query_parts(BTreeMap::new(), edges);

    // The shared graph Value (direct + transitive) the dep pane also consumes.
    let v_direct = deps_of(&g, "nornir", false).expect("deps_of direct");
    let v_trans = deps_of(&g, "nornir", true).expect("deps_of transitive");

    // mimir is JSON-native: the CLI outcome's `data` IS the graph Value, unchanged.
    let o_direct = outcome("mimir deps-of", v_direct.clone());
    let o_trans = outcome("mimir deps-of", v_trans.clone());
    assert!(
        o_direct.is_sannr(),
        "a real dep result is a true (sannr) CLI outcome"
    );
    assert_eq!(o_direct.command, "mimir deps-of");
    // THE parity assertion the contract names: data == the shared graph value.
    assert_eq!(
        o_direct.data, v_direct,
        "CLI surfaces the same graph the dep pane reads"
    );
    assert_eq!(o_trans.data, v_trans);

    // Sanity on the injected shape: nornir directly depends on skade (via the crate).
    assert_eq!(o_direct.data["dependencies"][0]["repo"], json!("skade"));
    assert_eq!(
        o_direct.data["dependencies"][0]["via"],
        json!(["skade-katalog"])
    );
    // Transitively it reaches znippy too.
    let trans: Vec<String> =
        serde_json::from_value(o_trans.data["dependencies"].clone()).expect("transitive list");
    assert!(trans.contains(&"skade".to_string()));
    assert!(
        trans.contains(&"znippy".to_string()),
        "transitive closure reaches znippy"
    );
}

// ════════════════════════════════════════════════════════════════════════════
// 4. `coverage show` ⟺ coverage pane — shared source: `coverage::latest` →
//    (`Vec<CoverageRow>`, `Vec<CoverageFnRow>`). The CLI `coverage --show --json`
//    shapes those rows (`coverage_rows_json`); the viz `CoverageLive` reads the
//    SAME rows (`inject_for_test`/`load`) and renders `line_pct` / `region_pct` /
//    per-crate / boundary-fn slices. Parity: inject identical rows and assert the
//    CLI projection equals the value derived from those same rows.
// ════════════════════════════════════════════════════════════════════════════

#[test]
fn coverage_show_cli_and_coverage_pane_share_coverage_rows() {
    use nornir::warehouse::coverage::{CoverageFnRow, CoverageRow};

    // INJECT the latest-run rows both faces read (as `coverage::latest` returns).
    let rows = vec![
        CoverageRow {
            run_id: "r1".into(),
            ts_micros: 1,
            repo: "nornir".into(),
            scope: "overall".into(),
            krate: String::new(),
            file: String::new(),
            lines: 1000,
            lines_covered: 800,
            regions: 500,
            regions_covered: 350,
        },
        CoverageRow {
            run_id: "r1".into(),
            ts_micros: 1,
            repo: "nornir".into(),
            scope: "crate:nornir".into(),
            krate: "nornir".into(),
            file: String::new(),
            lines: 600,
            lines_covered: 540,
            regions: 300,
            regions_covered: 240,
        },
    ];
    let fns = vec![
        CoverageFnRow {
            run_id: "r1".into(),
            ts_micros: 1,
            repo: "nornir".into(),
            name: "Bench.History".into(),
            file: "src/bin/nornir-server.rs".into(),
            boundary: "grpc".into(),
            lines: 10,
            lines_covered: 10,
            regions: 4,
            regions_covered: 4,
            line: 0,
            end_line: 0,
            commit: String::new(),
            carried_from: String::new(),
            changed: None,
        },
        CoverageFnRow {
            run_id: "r1".into(),
            ts_micros: 1,
            repo: "nornir".into(),
            name: "TestTab::state_json".into(),
            file: "src/viz/test_tab.rs".into(),
            boundary: "ui".into(),
            lines: 8,
            lines_covered: 0,
            regions: 3,
            regions_covered: 0,
            line: 0,
            end_line: 0,
            commit: String::new(),
            carried_from: String::new(),
            changed: None,
        },
    ];

    // CLI face: the `coverage --show --json` projection (the SAME shape
    // `src/bin/nornir.rs::coverage_rows_json` builds from these rows). Reproduced
    // here from the shared `CoverageRow`/`CoverageFnRow` lib API (the binary's fn
    // is private; the DATA SOURCE — these rows — is the shared canonical value).
    let overall = rows.iter().find(|r| r.scope == "overall");
    let cli_data = json!({
        "line_pct": overall.map(|o| o.line_pct()),
        "region_pct": overall.map(|o| o.region_pct()),
        "crates": rows.iter().filter(|r| r.scope.starts_with("crate:")).map(|c| json!({
            "krate": c.krate, "line_pct": c.line_pct(), "region_pct": c.region_pct(),
        })).collect::<Vec<_>>(),
        "boundary_fns": fns.iter().filter(|r| r.is_boundary()).map(|f| json!({
            "name": f.name, "boundary": f.boundary, "exercised": f.exercised(),
        })).collect::<Vec<_>>(),
    });

    // UI face (the coverage pane's verdict): derived from the SAME rows, the way
    // `CoverageLive::overall_pcts`/`crate_rows`/`boundary_fns` do.
    let ui_overall = rows
        .iter()
        .find(|r| r.scope == "overall")
        .expect("overall row");
    let ui_line = ui_overall.line_pct();
    let ui_region = ui_overall.region_pct();
    let ui_crate = rows
        .iter()
        .find(|r| r.scope == "crate:nornir")
        .expect("crate row");
    let ui_boundary: Vec<_> = fns.iter().filter(|r| r.is_boundary()).collect();

    // Same data, two faces: overall line%/region% agree.
    assert_eq!(
        cli_data["line_pct"],
        json!(ui_line),
        "overall line% agrees across faces"
    );
    assert_eq!(
        cli_data["region_pct"],
        json!(ui_region),
        "overall region% agrees across faces"
    );
    // 800/1000 = 80%.
    assert_eq!(ui_line, 80.0);

    // Per-crate slice agrees.
    assert_eq!(cli_data["crates"][0]["krate"], json!(ui_crate.krate));
    assert_eq!(
        cli_data["crates"][0]["line_pct"],
        json!(ui_crate.line_pct())
    );
    assert_eq!(
        cli_data["crates"][0]["region_pct"],
        json!(ui_crate.region_pct())
    );

    // Boundary-fn slice agrees: both faces surface the grpc + ui boundary fns and
    // the SAME exercised verdict (grpc exercised, the ui state_json fn NOT).
    assert_eq!(
        cli_data["boundary_fns"].as_array().unwrap().len(),
        ui_boundary.len()
    );
    assert_eq!(
        ui_boundary.len(),
        2,
        "both boundary fns surface (grpc + ui)"
    );
    let bench_hist = ui_boundary
        .iter()
        .find(|r| r.name == "Bench.History")
        .unwrap();
    let test_tab = ui_boundary
        .iter()
        .find(|r| r.name == "TestTab::state_json")
        .unwrap();
    assert!(
        bench_hist.exercised(),
        "grpc boundary fn is exercised in both faces"
    );
    assert!(
        !test_tab.exercised(),
        "untested ui emitter is RED in both faces"
    );
    // And the CLI projection carries the same verdicts.
    let cli_bh = cli_data["boundary_fns"]
        .as_array()
        .unwrap()
        .iter()
        .find(|f| f["name"] == json!("Bench.History"))
        .unwrap();
    assert_eq!(cli_bh["exercised"], json!(true));
}

// ════════════════════════════════════════════════════════════════════════════
// 5. `arch trace` ⟺ Arch tab — shared source: `arch::trace_from(graph, entry)`,
//    the forward-BFS reachable-set fn. `arch::trace_outcome` shapes its result for
//    the CLI; the Arch board's own `trace_from` (`state_json["traced_downstream"]`)
//    runs the SAME forward-BFS over `graph.edges`. Parity: on an injected graph,
//    the CLI's reachable node-label set == the board's reachable set.
// ════════════════════════════════════════════════════════════════════════════

#[test]
fn arch_trace_cli_data_equals_the_arch_board_reachable_set() {
    use std::collections::BTreeSet;

    use nornir::arch::{
        ArchEdge, ArchEdgeKind, ArchGraph, ArchNode, NodeKind, trace_from, trace_outcome,
    };

    let node = |id: &str, label: &str, kind: NodeKind| ArchNode {
        id: id.into(),
        label: label.into(),
        kind,
    };
    let edge = |from: &str, to: &str, kind: ArchEdgeKind| ArchEdge {
        from: from.into(),
        to: to.into(),
        kind,
    };

    // INJECT a coarsened board: a CLI command reaches a gRPC verb, which reads a
    // table; a second, DISCONNECTED component must NOT be reachable.
    let graph = ArchGraph {
        nodes: vec![
            node("cli:bench-history", "bench history", NodeKind::Cli),
            node("grpc:Bench.History", "Bench.History", NodeKind::Grpc),
            node("table:bench_runs", "bench_runs", NodeKind::Table),
            node("component:OtherView", "OtherView", NodeKind::Component),
        ],
        edges: vec![
            edge(
                "cli:bench-history",
                "grpc:Bench.History",
                ArchEdgeKind::Calls,
            ),
            edge(
                "grpc:Bench.History",
                "table:bench_runs",
                ArchEdgeKind::Reads,
            ),
        ],
    };

    // The shared reachable-set fn — what the Arch board ALSO runs (its private
    // `trace_from` is the same forward-BFS). Entrypoint "bench history" matches
    // exactly the one CLI node, so the board's single-seed BFS coincides.
    let sub = trace_from(&graph, "bench history");
    let board_reachable: BTreeSet<String> = sub.nodes.iter().map(|n| n.label.clone()).collect();

    // CLI face: `arch trace` shapes the SAME reachable sub-board.
    let cli = trace_outcome("nornir", "bench history", &sub);
    assert!(
        cli.is_sannr(),
        "a non-empty trace is a true (sannr) CLI outcome"
    );
    assert_eq!(cli.command, "arch trace");

    // THE parity assertion: the CLI's reachable node set == the board's reachable
    // set (the `state_json["traced_downstream"]` labels), by sharing `trace_from`.
    let cli_reachable: BTreeSet<String> = cli.data["nodes"]
        .as_array()
        .expect("nodes array")
        .iter()
        .map(|n| n["label"].as_str().unwrap().to_string())
        .collect();
    assert_eq!(
        cli_reachable, board_reachable,
        "CLI trace data == Arch board reachable set"
    );

    // The reachable set is the BFS closure; the disconnected component is excluded.
    let expected: BTreeSet<String> = ["bench history", "Bench.History", "bench_runs"]
        .iter()
        .map(|s| s.to_string())
        .collect();
    assert_eq!(cli_reachable, expected);
    assert!(
        !cli_reachable.contains("OtherView"),
        "the disconnected node is NOT reachable in either face"
    );
    assert_eq!(cli.data["node_count"], json!(3));
    assert_eq!(cli.data["edge_count"], json!(2));
}