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