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

nordisk/facett

facett-matrix/src/grid.rs

//! **Pure model** behind a per-repo × per-aspect **test matrix** — a verbatim
//! re-home of nornir's `src/viz/test_matrix_grid.rs`. No egui, no I/O: everything
//! the grid renders is derived here from a flat slice of
//! [`TestResultRow`]s, so it is inject-and-assert testable and folds straight
//! into a `state_json` (the "see what the user sees as data" LAW). The renderer
//! ([`crate::MatrixView`]) only paints what these functions compute.
//!
//! The shape:
//!   * **rows = repos**, taken from each repo's *latest run* (newest `ts_micros`).
//!   * **columns = aspects**, in [`ASPECT_ORDER`], filtered to those present (or
//!     an `expected` set unioned in, shown as not-run).
//!   * each **cell** = the aspect's status + numeric metric in that repo's latest
//!     run (plus the failing cases for drill-down), or "not-run" when absent.
//!   * a **health score** (0–100) per repo, and **sparkline series** per
//!     (repo, aspect) across recent runs so trends are visible.
//!
//! The row schema, status vocabulary, and run summariser come from
//! [`nornir_testmatrix`] (the shared pure model), so facett's grid and nornir's
//! grid derive from byte-identical inputs.

use nornir_testmatrix::{status, summarize_runs, RunSummary, TestResultRow};

/// Aspect columns in canonical display order.
pub const ASPECT_ORDER: &[&str] = &[
    "build",
    "unit",
    "doctest",
    "clippy",
    "fmt",
    "audit",
    "bench-smoke",
    "coverage",
    "feature-powerset",
    "msrv",
    "examples",
    "functional",
    // ROBOT-UI scenarios (accesskit-driven pane drives) as a first-class aspect column,
    // so a repo's robot-UI test suite shows its pass/fail alongside unit/bench-smoke.
    // Deliberately NOT in `ASPECT_DEFAULT`: a host that wants it always-visible unions it
    // into its own expected surface (dwarves' Testmatrix does), so the default not-run
    // surface stays the six core aspects.
    "robot-ui",
];

/// The **default expected** aspect columns — shown even before any run (as
/// NotRun), so the matrix is always visible for a workspace.
pub const ASPECT_DEFAULT: &[&str] = &["build", "unit", "doctest", "clippy", "fmt", "audit"];

/// How many recent runs (per repo) the trend sparklines span.
pub const SPARK_RUNS: usize = 12;

/// One cell of the matrix: a repo × aspect verdict in that repo's latest run.
#[derive(Debug, Clone, PartialEq)]
pub struct Cell {
    /// `pass` | `fail` | `stalled` | `skip` | `ignored`, or `""` = not run.
    pub status: String,
    /// The aspect's numeric metric (coverage %, warning count, …); 0.0 if N/A.
    pub metric: f64,
    /// How many cases rolled into this cell (1 for synthetic aspects; N for unit).
    pub count: usize,
    /// For the unit aspect: passed / failed sub-counts (else 0 / 0).
    pub passed: usize,
    pub failed: usize,
    /// Failing/stalled cases under this cell (for the drill-down panel).
    pub red_cases: Vec<RedCase>,
}

impl Cell {
    fn not_run() -> Self {
        Self { status: String::new(), metric: 0.0, count: 0, passed: 0, failed: 0, red_cases: Vec::new() }
    }
    /// Was this aspect run at all in the repo's latest run?
    pub fn ran(&self) -> bool {
        !self.status.is_empty()
    }
    pub fn is_red(&self) -> bool {
        status::is_red(&self.status)
    }
    pub fn is_green(&self) -> bool {
        status::is_green(&self.status)
    }

    /// The type-aware badge text for this cell, e.g. `"87%"`, `"⚠3"`, `"🛡0"`,
    /// `"142✓ 2✗"`. Empty when the cell didn't run or has nothing to show.
    pub fn badge(&self, aspect: &str) -> String {
        if !self.ran() {
            return String::new();
        }
        match aspect {
            "coverage" => format!("{:.0}%", self.metric),
            "clippy" => format!("⚠{}", self.metric as i64),
            "audit" => format!("🛡{}", self.metric as i64),
            "fmt" => {
                if self.metric > 0.0 {
                    format!("±{}", self.metric as i64)
                } else {
                    String::new()
                }
            }
            "unit" | "robot-ui" | "functional" => {
                if self.failed > 0 {
                    format!("{}✓ {}✗", self.passed, self.failed)
                } else {
                    format!("{}✓", self.passed)
                }
            }
            _ => String::new(),
        }
    }
}

/// One failing case under a cell, surfaced in the drill-down.
#[derive(Debug, Clone, PartialEq)]
pub struct RedCase {
    pub test_name: String,
    pub suite: String,
    pub status: String,
    pub message: String,
}

/// One matrix row: a repo, its latest-run cells (one per aspect column), its
/// health score, and its run identity.
#[derive(Debug, Clone)]
pub struct RepoRow {
    pub repo: String,
    pub run_id: String,
    pub ts_micros: i64,
    /// Latest-run verdict per aspect column (parallel to [`Matrix::aspects`]).
    pub cells: Vec<Cell>,
    /// 0–100 derived health (green-ratio + coverage − warnings/advisories).
    pub health: f64,
    /// Per-aspect sparkline series across recent runs (parallel to `aspects`).
    pub sparks: Vec<Vec<f64>>,
}

/// The whole grid + the summary strip totals.
#[derive(Debug, Clone)]
pub struct Matrix {
    /// Aspect column labels actually present in the data, in [`ASPECT_ORDER`].
    pub aspects: Vec<String>,
    /// One row per repo, sorted by repo name.
    pub rows: Vec<RepoRow>,
    /// Summary strip: total cells that are green / red / skip / not-run.
    pub total_green: usize,
    pub total_red: usize,
    pub total_skip: usize,
    pub total_blank: usize,
}

impl Matrix {
    /// Build the matrix from a flat slice of rows — axes derived purely from the
    /// data. `build_with_expected` with no expectations.
    pub fn build(rows: &[TestResultRow]) -> Self {
        Self::build_with_expected(&[], &[], rows)
    }

    /// Build the matrix over an EXPECTED surface, so cells are visible — and
    /// honestly marked **not-run** — even before anything has run. The data's own
    /// repos/aspects are unioned in so nothing is hidden.
    pub fn build_with_expected(
        expected_repos: &[String],
        expected_aspects: &[&str],
        rows: &[TestResultRow],
    ) -> Self {
        use std::collections::{BTreeMap, BTreeSet, HashSet};

        let mut by_repo: BTreeMap<String, Vec<&TestResultRow>> = BTreeMap::new();
        for r in rows {
            by_repo.entry(r.repo.clone()).or_default().push(r);
        }

        let mut want: HashSet<&str> = expected_aspects.iter().copied().collect();
        for r in rows {
            want.insert(r.aspect.as_str());
        }
        let aspects: Vec<String> =
            ASPECT_ORDER.iter().filter(|a| want.contains(**a)).map(|a| a.to_string()).collect();

        let mut repos: BTreeSet<String> = expected_repos.iter().cloned().collect();
        for repo in by_repo.keys() {
            repos.insert(repo.clone());
        }

        let mut out_rows = Vec::new();
        let (mut tg, mut tr, mut ts, mut tb) = (0usize, 0usize, 0usize, 0usize);

        for repo in &repos {
            let repo_rows = by_repo.get(repo);
            let summaries = repo_rows
                .map(|rr| summarize_runs(&rr.iter().map(|r| (*r).clone()).collect::<Vec<_>>()))
                .unwrap_or_default();
            let latest = summaries.first();
            let latest_id = latest.map(|s| s.run_id.clone()).unwrap_or_default();
            let ts_micros = latest.map(|s| s.ts_micros).unwrap_or(0);

            let mut cells = Vec::with_capacity(aspects.len());
            for aspect in &aspects {
                let cell = match repo_rows {
                    Some(rr) if !latest_id.is_empty() => build_cell(rr, &latest_id, aspect),
                    _ => Cell::not_run(),
                };
                match () {
                    _ if !cell.ran() => tb += 1,
                    _ if cell.is_red() => tr += 1,
                    _ if cell.is_green() => tg += 1,
                    _ => ts += 1,
                }
                cells.push(cell);
            }

            let health = health_score(&aspects, &cells);
            let sparks = match repo_rows {
                Some(rr) => aspects.iter().map(|a| spark_series(rr, &summaries, a)).collect(),
                None => aspects.iter().map(|_| Vec::new()).collect(),
            };

            out_rows.push(RepoRow { repo: repo.clone(), run_id: latest_id, ts_micros, cells, health, sparks });
        }

        Matrix { aspects, rows: out_rows, total_green: tg, total_red: tr, total_skip: ts, total_blank: tb }
    }

    /// The introspection blob a host folds into a `state_json` (LAW 6): aspects,
    /// per-repo cells with their badges + health, and the summary totals — so a
    /// driver reads the rendered grid without a screenshot.
    pub fn state_json(&self) -> serde_json::Value {
        serde_json::json!({
            "aspects": self.aspects,
            "repo_count": self.rows.len(),
            "total_green": self.total_green,
            "total_red": self.total_red,
            "total_skip": self.total_skip,
            "total_blank": self.total_blank,
            "rows": self.rows.iter().map(|r| serde_json::json!({
                "repo": r.repo,
                "run_id": r.run_id,
                "health": r.health,
                "cells": r.cells.iter().zip(self.aspects.iter()).map(|(c, a)| serde_json::json!({
                    "aspect": a,
                    "status": c.status,
                    "ran": c.ran(),
                    "green": c.is_green(),
                    "red": c.is_red(),
                    "metric": c.metric,
                    "badge": c.badge(a),
                    "red_cases": c.red_cases.iter().map(|rc| serde_json::json!({
                        "test": rc.test_name,
                        "suite": rc.suite,
                        "status": rc.status,
                        "message": rc.message,
                    })).collect::<Vec<_>>(),
                })).collect::<Vec<_>>(),
            })).collect::<Vec<_>>(),
        })
    }
}

/// Aspects whose rows are **one named case each** (many cases per cell) and so roll up
/// into a pass/fail tally + red-case list, exactly like `unit`. `robot-ui` (accesskit
/// pane-drive scenarios) and `functional` (functional-status checks) are multi-case the
/// same way a unit suite is; the synthetic single-value aspects (coverage/clippy/…) are
/// not.
fn is_multicase(aspect: &str) -> bool {
    matches!(aspect, "unit" | "robot-ui" | "functional")
}

/// Roll the latest run's rows for one aspect into a [`Cell`]. `listed` rows are
/// discovery-only (a test that EXISTS but has not run yet) → they never make a
/// cell "ran"; a real `pass`/`fail`/`ignored` row overrides the seed.
fn build_cell(repo_rows: &[&TestResultRow], run_id: &str, aspect: &str) -> Cell {
    let hits: Vec<&&TestResultRow> = repo_rows
        .iter()
        .filter(|r| r.run_id == run_id && r.aspect == aspect)
        .filter(|r| !status::is_listed(&r.status))
        .collect();
    if hits.is_empty() {
        return Cell::not_run();
    }

    if is_multicase(aspect) {
        let mut passed = 0;
        let mut failed = 0;
        let mut red_cases = Vec::new();
        for r in &hits {
            if status::is_green(&r.status) {
                passed += 1;
            } else if status::is_red(&r.status) {
                failed += 1;
            }
            if status::is_red(&r.status) {
                red_cases.push(RedCase {
                    test_name: r.test_name.clone(),
                    suite: r.suite.clone(),
                    status: r.status.clone(),
                    message: r.message.clone(),
                });
            }
        }
        let cell_status = if failed > 0 { status::FAIL } else { status::PASS };
        return Cell {
            status: cell_status.to_string(),
            metric: failed as f64,
            count: hits.len(),
            passed,
            failed,
            red_cases,
        };
    }

    let r = hits[0];
    let red_cases = if status::is_red(&r.status) {
        vec![RedCase {
            test_name: r.test_name.clone(),
            suite: r.suite.clone(),
            status: r.status.clone(),
            message: r.message.clone(),
        }]
    } else {
        Vec::new()
    };
    Cell {
        status: r.status.clone(),
        metric: r.metric,
        count: hits.len(),
        passed: usize::from(status::is_green(&r.status)),
        failed: usize::from(status::is_red(&r.status)),
        red_cases,
    }
}

/// A 0–100 health score for a repo from its latest-run cells: green-ratio (60) +
/// coverage (25) − a penalty for clippy warnings + audit advisories.
pub fn health_score(aspects: &[String], cells: &[Cell]) -> f64 {
    let mut green = 0.0;
    let mut considered = 0.0;
    let mut coverage: Option<f64> = None;
    let mut penalty = 0.0;
    for (a, c) in aspects.iter().zip(cells) {
        if !c.ran() {
            continue;
        }
        if c.is_green() || c.is_red() || c.status == status::SKIP {
            considered += 1.0;
            if c.is_green() {
                green += 1.0;
            }
        }
        match a.as_str() {
            "coverage" if c.ran() => coverage = Some(c.metric),
            "clippy" => penalty += c.metric.min(8.0) * 4.0,
            "audit" => penalty += c.metric.min(8.0) * 6.0,
            _ => {}
        }
    }
    if considered == 0.0 {
        return 0.0;
    }
    let green_ratio = green / considered;
    let score = match coverage {
        Some(cov) => green_ratio * 60.0 + (cov / 100.0).clamp(0.0, 1.0) * 25.0 + 15.0,
        None => green_ratio * 85.0 + 15.0,
    };
    (score - penalty).clamp(0.0, 100.0)
}

/// A per-(repo, aspect) sparkline: one normalized point per recent run (oldest →
/// newest, last [`SPARK_RUNS`]). Aspect-aware: coverage → the %; clippy/audit/fmt
/// → the count **inverted** (fewer = up); else 100 green / 0 red / 50 neutral.
pub fn spark_series(repo_rows: &[&TestResultRow], summaries: &[RunSummary], aspect: &str) -> Vec<f64> {
    let mut runs: Vec<&RunSummary> = summaries.iter().take(SPARK_RUNS).collect();
    runs.reverse();
    let mut series = Vec::with_capacity(runs.len());
    for run in runs {
        let hits: Vec<&&TestResultRow> =
            repo_rows.iter().filter(|r| r.run_id == run.run_id && r.aspect == aspect).collect();
        if hits.is_empty() {
            continue;
        }
        let v = match aspect {
            "coverage" => hits.iter().map(|r| r.metric).fold(0.0, f64::max),
            "clippy" | "audit" | "fmt" => {
                let count = hits.iter().map(|r| r.metric).fold(0.0, f64::max);
                (1.0 - (count / 10.0).clamp(0.0, 1.0)) * 100.0
            }
            "unit" => {
                let total = hits.len() as f64;
                let pass = hits.iter().filter(|r| status::is_green(&r.status)).count() as f64;
                if total == 0.0 {
                    50.0
                } else {
                    pass / total * 100.0
                }
            }
            _ => {
                let r = hits[0];
                if status::is_green(&r.status) {
                    100.0
                } else if status::is_red(&r.status) {
                    0.0
                } else {
                    50.0
                }
            }
        };
        series.push(v);
    }
    series
}

#[cfg(test)]
mod tests {
    use super::*;

    fn row(run: &str, repo: &str, aspect: &str, name: &str, st: &str, metric: f64, ts: i64) -> TestResultRow {
        TestResultRow {
            run_id: run.into(),
            repo: repo.into(),
            suite: repo.into(),
            test_name: name.into(),
            status: st.into(),
            duration_ms: 1.0,
            ts_micros: ts,
            message: if status::is_red(st) { format!("{name} boom") } else { String::new() },
            aspect: aspect.into(),
            metric,
        }
    }

    #[test]
    fn matrix_rows_are_repos_cols_are_aspects() {
        let rows = vec![
            row("r1", "alpha", "build", "build", status::PASS, 0.0, 100),
            row("r1", "alpha", "clippy", "clippy", status::FAIL, 3.0, 100),
            row("r1", "beta", "build", "build", status::PASS, 0.0, 100),
            row("r1", "beta", "coverage", "coverage", status::PASS, 87.0, 100),
        ];
        let m = Matrix::build(&rows);
        assert_eq!(m.rows.iter().map(|r| r.repo.as_str()).collect::<Vec<_>>(), vec!["alpha", "beta"]);
        assert_eq!(m.aspects, vec!["build", "clippy", "coverage"]);
    }

    #[test]
    fn cells_carry_status_and_metric_badges() {
        let rows = vec![
            row("r1", "alpha", "clippy", "clippy", status::FAIL, 3.0, 100),
            row("r1", "alpha", "coverage", "coverage", status::PASS, 87.0, 100),
            row("r1", "alpha", "audit", "audit", status::PASS, 0.0, 100),
        ];
        let m = Matrix::build(&rows);
        let a = &m.rows[0];
        let at = |x: &str| m.aspects.iter().position(|y| y == x).unwrap();
        assert_eq!(a.cells[at("clippy")].badge("clippy"), "⚠3");
        assert_eq!(a.cells[at("coverage")].badge("coverage"), "87%");
        assert_eq!(a.cells[at("audit")].badge("audit"), "🛡0");
    }

    #[test]
    fn unit_cell_rolls_up_pass_fail_counts_and_badge() {
        let rows = vec![
            row("r1", "alpha", "unit", "a::t1", status::PASS, 0.0, 100),
            row("r1", "alpha", "unit", "a::t2", status::PASS, 0.0, 100),
            row("r1", "alpha", "unit", "a::t3", status::FAIL, 0.0, 100),
        ];
        let m = Matrix::build(&rows);
        let unit = &m.rows[0].cells[0];
        assert_eq!(unit.passed, 2);
        assert_eq!(unit.failed, 1);
        assert_eq!(unit.status, status::FAIL);
        assert_eq!(unit.badge("unit"), "2✓ 1✗");
        assert_eq!(unit.red_cases.len(), 1);
        assert_eq!(unit.red_cases[0].test_name, "a::t3");
    }

    #[test]
    fn latest_run_wins_per_repo() {
        let rows = vec![
            row("old", "alpha", "clippy", "clippy", status::FAIL, 5.0, 100),
            row("new", "alpha", "clippy", "clippy", status::PASS, 0.0, 200),
        ];
        let m = Matrix::build(&rows);
        assert_eq!(m.rows[0].run_id, "new");
        assert_eq!(m.rows[0].cells[0].status, status::PASS);
    }

    #[test]
    fn health_high_for_green_high_coverage_low_warnings() {
        let aspects: Vec<String> = vec!["build".into(), "coverage".into(), "clippy".into()];
        let cells = vec![
            Cell { status: status::PASS.into(), metric: 0.0, count: 1, passed: 1, failed: 0, red_cases: vec![] },
            Cell { status: status::PASS.into(), metric: 95.0, count: 1, passed: 1, failed: 0, red_cases: vec![] },
            Cell { status: status::PASS.into(), metric: 0.0, count: 1, passed: 1, failed: 0, red_cases: vec![] },
        ];
        assert!(health_score(&aspects, &cells) > 90.0);
    }

    #[test]
    fn health_drops_for_failures_and_warnings() {
        let aspects: Vec<String> = vec!["build".into(), "coverage".into(), "clippy".into()];
        let cells = vec![
            Cell { status: status::FAIL.into(), metric: 1.0, count: 1, passed: 0, failed: 1, red_cases: vec![] },
            Cell { status: status::PASS.into(), metric: 40.0, count: 1, passed: 1, failed: 0, red_cases: vec![] },
            Cell { status: status::FAIL.into(), metric: 6.0, count: 1, passed: 0, failed: 1, red_cases: vec![] },
        ];
        assert!(health_score(&aspects, &cells) < 50.0);
    }

    #[test]
    fn sparkline_tracks_coverage_over_runs() {
        let rows = vec![
            row("run1", "alpha", "coverage", "coverage", status::PASS, 70.0, 100),
            row("run2", "alpha", "coverage", "coverage", status::PASS, 80.0, 200),
            row("run3", "alpha", "coverage", "coverage", status::PASS, 90.0, 300),
        ];
        let m = Matrix::build(&rows);
        let cov_idx = m.aspects.iter().position(|x| x == "coverage").unwrap();
        assert_eq!(&m.rows[0].sparks[cov_idx], &vec![70.0, 80.0, 90.0]);
    }

    #[test]
    fn expected_surface_shows_not_run_before_any_result() {
        let expected = vec!["znippy".to_string(), "nornir".to_string()];
        let m = Matrix::build_with_expected(&expected, ASPECT_DEFAULT, &[]);
        assert_eq!(m.rows.iter().map(|r| r.repo.as_str()).collect::<Vec<_>>(), vec!["nornir", "znippy"]);
        assert_eq!(m.aspects, vec!["build", "unit", "doctest", "clippy", "fmt", "audit"]);
        assert!(m.rows.iter().all(|r| r.cells.iter().all(|c| !c.ran())));
        assert_eq!(m.total_blank, 2 * ASPECT_DEFAULT.len());
        assert_eq!(m.total_green + m.total_red + m.total_skip, 0);
    }

    /// ROBOT-UI is a first-class aspect column: a repo carrying `robot-ui` result rows
    /// gets a `robot-ui` column whose cell reflects the scenario verdict (green when the
    /// pane-drives passed, red when one failed). This is how a driver reads "robot-UI
    /// tests + their status" out of the matrix.
    #[test]
    fn robot_ui_is_an_aspect_column_with_status() {
        assert!(ASPECT_ORDER.contains(&"robot-ui"), "robot-ui is a known aspect");
        let rows = vec![
            row("r1", "dwarves", "robot-ui", "benches_tab_drives", status::PASS, 0.0, 100),
            row("r1", "dwarves", "robot-ui", "forge_tab_drives", status::FAIL, 0.0, 100),
            row("r1", "dwarves", "unit", "u::t1", status::PASS, 0.0, 100),
        ];
        let m = Matrix::build(&rows);
        // A `robot-ui` column exists, after the core aspects (ASPECT_ORDER position).
        assert!(m.aspects.iter().any(|a| a == "robot-ui"), "robot-ui column present: {:?}", m.aspects);
        let ru = m.aspects.iter().position(|a| a == "robot-ui").unwrap();
        let cell = &m.rows[0].cells[ru];
        // One scenario failed → the aggregate robot-ui cell is red and NAMES the failure.
        assert!(cell.ran());
        assert!(cell.is_red(), "a failed robot-ui scenario reds the cell");
        assert!(cell.red_cases.iter().any(|c| c.test_name == "forge_tab_drives"));
        let j = m.state_json();
        let dwarves = j["rows"].as_array().unwrap().iter().find(|r| r["repo"] == "dwarves").unwrap();
        let ru_cell = dwarves["cells"].as_array().unwrap().iter().find(|c| c["aspect"] == "robot-ui").unwrap();
        assert_eq!(ru_cell["red"], true, "robot-ui status surfaces in state_json");
    }

    #[test]
    fn listed_only_cell_is_not_run() {
        let rows = vec![
            row("r1", "alpha", "unit", "a::t1", status::LISTED, 0.0, 100),
            row("r1", "alpha", "unit", "a::t2", status::LISTED, 0.0, 100),
        ];
        let m = Matrix::build(&rows);
        let unit = &m.rows[0].cells[m.aspects.iter().position(|x| x == "unit").unwrap()];
        assert!(!unit.ran());
        assert_eq!(m.total_blank, m.aspects.len());
    }

    /// inject-assert (LAW 6): the matrix's `state_json` exposes the rendered grid
    /// — aspects, per-cell verdict/badge, summary totals — a driver asserts on.
    #[test]
    fn state_json_exposes_grid_cells_and_totals() {
        let rows = vec![
            row("r1", "alpha", "unit", "a::t1", status::PASS, 0.0, 100),
            row("r1", "alpha", "unit", "a::t2", status::FAIL, 0.0, 100),
            row("r1", "alpha", "coverage", "coverage", status::PASS, 91.0, 100),
            row("r1", "beta", "build", "build", status::PASS, 0.0, 100),
        ];
        let m = Matrix::build(&rows);
        let j = m.state_json();
        assert_eq!(j["repo_count"], 2);
        assert!(j["aspects"].as_array().unwrap().iter().any(|a| a == "coverage"));
        // alpha row: a failing unit cell + a green coverage cell with a "91%" badge.
        let alpha = j["rows"].as_array().unwrap().iter().find(|r| r["repo"] == "alpha").unwrap();
        let cov = alpha["cells"].as_array().unwrap().iter().find(|c| c["aspect"] == "coverage").unwrap();
        assert_eq!(cov["green"], true);
        assert_eq!(cov["badge"], "91%");
        let unit = alpha["cells"].as_array().unwrap().iter().find(|c| c["aspect"] == "unit").unwrap();
        assert_eq!(unit["red"], true);
        assert_eq!(unit["badge"], "1✓ 1✗");
        assert_eq!(unit["red_cases"].as_array().unwrap().len(), 1);
        // totals: one red (alpha unit), greens for alpha coverage + beta build.
        assert!(j["total_red"].as_u64().unwrap() >= 1);
        assert!(j["total_green"].as_u64().unwrap() >= 2);
    }
}