crates/dwarves-embed/src/holgerbuild.rs
//! **The holger build-MATRIX pane** — the CI forge cockpit's "which of holger's
//! deployment executables are wired to build" (separation-spec item **B2**).
//!
//! holger declares one product list; dwarves builds the products a holger checkout builds
//! (`holger`, `holger-downstream`, `holger-cli`, each one crate and one row). dwarves
//! ORCHESTRATES that build; the **`ResolveHolgerBuild`** RPC resolves the plan against holger's
//! declared build-space (`.nornir/build-variants.toml`) WITHOUT compiling holger — each
//! tier mapped onto its build target (bin · package · materialized `cargo` argv) plus
//! which resolve and whether the plan is complete. This pane renders that MATRIX so a
//! forge operator sees the build landscape (crates/tiers × targets × status) in the
//! dwarves UI.
//!
//! The reusable `<guest>-embed` adapter shape (like [`crate::ForgePanel`] /
//! [`crate::JobsFleetPanel`]): a **thin tonic gRPC client** fetches the reply from a
//! running `dwarves-server` over the [`ResolveHolgerBuild`](crate::pb) RPC, a **pure**
//! mapping projects the wire tiers onto rows, and the matrix renders on the SHARED
//! [`facett_table::Table`] (L5 — do NOT twin a table renderer; the pane just formats each
//! tier to strings). NO `holger`/`nornir` dep (that would be a dependency cycle — nornir
//! already depends on dwarves) and NO dwarves `server` feature.
//!
//! ## Honest states — never a silent blank
//! Every non-render outcome is a DISTINCT, named state (folded into `state_json` for the
//! status line + robot assertions):
//! - **`loaded`** — the reply carried tiers; the matrix renders (with per-tier resolved
//! status + the plan's `complete` verdict).
//! - **`no_build_space`** — connected, but holger declares no build-space (`found=false`)
//! — an honest empty, distinct from offline.
//! - **`empty`** — connected, `found=true`, yet zero tiers (a degenerate reply).
//! - **`unreachable`** — could not connect to `dwarves-server`.
//! - **`auth_failed`** — the server rejected the bearer (Unauthenticated/PermissionDenied).
//! - **`not_served`** — the server does not expose `ResolveHolgerBuild` (Unimplemented).
//! - **`error`** — any other RPC failure (the message surfaced for the tooltip).
//!
//! Auto-polls on an interval like the sibling panes, and drives headless over the
//! `state_json`/`update_json` seam (the channel a `nornir-robotui` scenario + a thin gRPC
//! client use) — so the pane is robot-tested without pixels.
use std::time::Duration;
use facett_core::{Facet, Panel};
use facett_table::Table;
use crate::pb;
/// Default seconds between live auto-poll re-resolves of the build matrix. A plan/wiring
/// check is cheap on the server, but it changes rarely — a calmer interval than Forge.
pub const DEFAULT_BUILD_POLL_INTERVAL_S: f64 = 5.0;
/// The pane's default holger checkout when the request leaves `holger_root` empty: the
/// SERVER resolves it (`$DWARVES_HOLGER_REPO`, else `../holger`). The pane sends empty
/// unless a host pins one, so the resolution stays server-side (it has the checkout).
pub const HOLGER_ROOT_ENV: &str = "DWARVES_HOLGER_REPO";
// ── the wire → domain projection (pure, unit-testable) ────────────────────────
/// One resolved (or unwired) holger deployment tier — the domain projection of a wire
/// [`pb::HolgerTier`]. A plain serde row so `state_json` carries the whole matrix for a
/// robot to assert, and so [`tier_cells`] formats it to the shared table's string cells.
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
pub struct TierRow {
/// The row/variant name (`holger`|`holger-downstream`|`holger-cli`).
pub tier: String,
/// The pipeline step id (`build-<tier>`).
pub step: String,
/// True iff the tier names a build target holger's build-space declares.
pub resolved: bool,
/// The tier's executable name (empty when unwired).
pub bin: String,
/// The tier's cargo package (empty when unwired).
pub package: String,
/// The materialized `cargo build …` argv (empty when unwired).
pub command_line: String,
}
/// **Pure** map the wire tiers onto the matrix rows (order preserved). Unit-testable with
/// stub rows (no server).
pub fn tiers_from_wire(rows: &[pb::HolgerTier]) -> Vec<TierRow> {
rows.iter()
.map(|t| TierRow {
tier: t.tier.clone(),
step: t.step.clone(),
resolved: t.resolved,
bin: t.bin.clone(),
package: t.package.clone(),
command_line: t.command_line.clone(),
})
.collect()
}
/// The build-matrix table's column headers (stable order — the `state_json` shape a deck
/// / render-matrix reads).
pub const MATRIX_COLUMNS: [&str; 5] = ["tier", "status", "bin", "package", "cargo"];
/// Format one tier as the shared table's string cells (aligned with [`MATRIX_COLUMNS`]).
/// An unwired tier reads `unwired` with a `—` placeholder for the empty target fields, so
/// a dropped/renamed executable is VISIBLE in the grid, never a blank row.
fn tier_cells(t: &TierRow) -> Vec<String> {
let status = if t.resolved { "resolved" } else { "unwired" };
let dash = |s: &str| {
if s.is_empty() {
"—".to_string()
} else {
s.to_string()
}
};
vec![
t.tier.clone(),
status.to_string(),
dash(&t.bin),
dash(&t.package),
dash(&t.command_line),
]
}
/// Build the SHARED [`Table`] from the matrix rows: the header columns + one string-cell
/// row per tier, keyed on the tier name (a stable row id — a re-sort keeps identity). The
/// REUSED renderer (L5); the pane owns only the string formatting.
fn table_from_tiers(tiers: &[TierRow]) -> Table {
let mut t = Table::new(
"holger build matrix",
MATRIX_COLUMNS.iter().map(|c| c.to_string()).collect(),
);
for tier in tiers {
t.push_row(tier_cells(tier));
}
t.with_row_ids(tiers.iter().map(|r| r.tier.clone()).collect())
}
// ── thin gRPC fetch (with the honest-state error classification) ──────────────
/// A build-matrix fetch failure, classified into the pane's HONEST states. The pane maps
/// each variant onto a distinct status (never a generic "offline"): a dead server is
/// `Unreachable`, a rejected bearer is `AuthFailed`, a server that does not expose the RPC
/// is `NotServed`, anything else is `Rpc`.
#[derive(Clone, Debug)]
pub enum BuildErr {
/// Could not connect to `dwarves-server` (dead port / bad url / timeout).
Unreachable(String),
/// The server rejected the bearer (gRPC Unauthenticated / PermissionDenied).
AuthFailed(String),
/// The server does not expose `ResolveHolgerBuild` (gRPC Unimplemented).
NotServed(String),
/// Any other RPC failure (the message is surfaced for the tooltip).
Rpc(String),
}
impl BuildErr {
/// The state key this failure maps to (mirrors [`Conn::key`]).
fn key(&self) -> &'static str {
match self {
BuildErr::Unreachable(_) => "unreachable",
BuildErr::AuthFailed(_) => "auth_failed",
BuildErr::NotServed(_) => "not_served",
BuildErr::Rpc(_) => "error",
}
}
/// The human message (for the hover tooltip + tests).
pub fn message(&self) -> &str {
match self {
BuildErr::Unreachable(m)
| BuildErr::AuthFailed(m)
| BuildErr::NotServed(m)
| BuildErr::Rpc(m) => m,
}
}
/// Classify a tonic RPC `Status` into the honest error kinds.
fn from_status(s: &tonic::Status) -> Self {
use tonic::Code;
let msg = format!("{}: {}", s.code(), s.message());
match s.code() {
Code::Unauthenticated | Code::PermissionDenied => BuildErr::AuthFailed(msg),
Code::Unimplemented => BuildErr::NotServed(msg),
_ => BuildErr::Rpc(msg),
}
}
}
/// Normalize an endpoint: prefix `http://` if the scheme is missing.
fn norm_endpoint(endpoint: &str) -> String {
if endpoint.starts_with("http") {
endpoint.to_string()
} else {
format!("http://{endpoint}")
}
}
/// Resolve holger's 3-tier build matrix from `dwarves-server` at `endpoint` over the thin
/// `ResolveHolgerBuild` gRPC (Bearer `token`, empty ⇒ no real auth). `holger_root` is
/// resolved on the SERVER's filesystem (empty ⇒ the server's default). Runs on a private
/// current-thread runtime so it is safe from the synchronous egui loop; a 3s connect
/// timeout keeps a dead server from wedging the UI thread. Failures are classified into
/// the honest [`BuildErr`] states — never a panic, never a silent blank.
pub fn fetch_holger_build(
endpoint: &str,
token: &str,
holger_root: &str,
) -> Result<pb::HolgerBuildReply, BuildErr> {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(|e| BuildErr::Unreachable(format!("build tokio runtime: {e}")))?;
let endpoint = norm_endpoint(endpoint);
let token = token.to_string();
let holger_root = holger_root.to_string();
rt.block_on(async move {
// The ONE shared client bearer-builder (Law #1) — no per-site
// `format!("Bearer …").parse()` twin. `None` ⇒ open (empty token).
let bearer = nornir_service::client::bearer(&token)
.map_err(|e| BuildErr::AuthFailed(format!("parse bearer token: {e}")))?;
let channel = tonic::transport::Channel::from_shared(endpoint.clone())
.map_err(|e| BuildErr::Unreachable(format!("invalid server url `{endpoint}`: {e}")))?
.connect_timeout(Duration::from_secs(3))
.timeout(crate::READ_TIMEOUT)
.connect()
.await
.map_err(|e| {
BuildErr::Unreachable(format!("connect to dwarves-server at {endpoint}: {e}"))
})?;
let mut client = pb::dwarf_client::DwarfClient::with_interceptor(
channel,
move |mut req: tonic::Request<()>| {
if let Some(b) = &bearer {
req.metadata_mut().insert("authorization", b.clone());
}
Ok(req)
},
);
let reply = client
.resolve_holger_build(pb::HolgerBuildRequest { holger_root })
.await
.map_err(|s| BuildErr::from_status(&s))?
.into_inner();
Ok(reply)
})
}
// ── the pane ──────────────────────────────────────────────────────────────────
/// The outcome of the last resolve — drives the status line + `state_json`. Every variant
/// is a DISTINCT honest state; there is no silent-blank fallthrough.
#[derive(Clone, Debug, PartialEq, Eq)]
enum Conn {
/// Never resolved yet.
Idle,
/// The reply carried tiers; the matrix renders.
Loaded,
/// Connected, but holger declares no build-space (`found=false`) — an honest empty.
NoBuildSpace,
/// Connected, `found=true`, yet zero tiers (a degenerate reply).
Empty,
/// Could not connect.
Unreachable,
/// The bearer was rejected.
AuthFailed,
/// The server does not expose the RPC.
NotServed,
/// Any other RPC failure.
Error,
}
impl Conn {
fn key(&self) -> &'static str {
match self {
Conn::Idle => "idle",
Conn::Loaded => "loaded",
Conn::NoBuildSpace => "no_build_space",
Conn::Empty => "empty",
Conn::Unreachable => "unreachable",
Conn::AuthFailed => "auth_failed",
Conn::NotServed => "not_served",
Conn::Error => "error",
}
}
/// True for the states that render nothing (so `ui` shows the honest empty message).
fn is_error_like(&self) -> bool {
!matches!(self, Conn::Loaded)
}
}
/// The holger build-matrix cockpit pane. A [`Facet`] (hence a [`Panel`] via the blanket
/// impl) a host mounts in a tab.
pub struct HolgerBuildPanel {
endpoint: String,
token: String,
/// The holger checkout the SERVER resolves against (empty ⇒ the server's default).
holger_root: String,
/// The rendered matrix rows (the domain projection surfaced in `state_json`).
tiers: Vec<TierRow>,
/// The SHARED table the matrix renders on (rebuilt on each successful resolve).
table: Table,
conn: Conn,
/// The plan's completeness verdict + counts from the last successful resolve.
complete: bool,
resolved: u32,
missing: u32,
found: bool,
/// The last error's display text (folded into `state_json` for the tooltip + tests).
error: Option<String>,
auto_poll: bool,
poll_interval_s: f64,
last_poll_s: f64,
}
impl HolgerBuildPanel {
/// Build the pane for a `dwarves-server` `endpoint` and eagerly do the first resolve.
/// `holger_root` pins the server-side checkout (empty ⇒ the server's default). Offline
/// / no build-space ⇒ the honest states, never a panic.
pub fn new(
endpoint: impl Into<String>,
token: impl Into<String>,
holger_root: impl Into<String>,
) -> Self {
let mut p = Self {
endpoint: endpoint.into(),
token: token.into(),
holger_root: holger_root.into(),
tiers: Vec::new(),
table: table_from_tiers(&[]),
conn: Conn::Idle,
complete: false,
resolved: 0,
missing: 0,
found: false,
error: None,
auto_poll: true,
poll_interval_s: DEFAULT_BUILD_POLL_INTERVAL_S,
last_poll_s: 0.0,
};
p.refresh();
p
}
/// True when auto-poll is on AND at least one interval has elapsed. Pure →
/// unit-testable without a clock.
pub fn poll_due(&self, now_s: f64) -> bool {
self.auto_poll && (now_s - self.last_poll_s) >= self.poll_interval_s
}
/// Turn live auto-poll on/off (the ● live / ⏸ paused toggle).
pub fn set_auto_poll(&mut self, on: bool) {
self.auto_poll = on;
}
/// (Re-)resolve the build matrix and apply the outcome.
pub fn refresh(&mut self) {
let outcome = fetch_holger_build(&self.endpoint, &self.token, &self.holger_root);
self.apply_fetch(outcome);
}
/// Fold a [`fetch_holger_build`] outcome into the pane's rendered state. Split from
/// [`refresh`] so the transitions drive headless in tests (the same `state_json` seam a
/// robot reads) without a live server. Each outcome lands in a DISTINCT honest state.
pub fn apply_fetch(&mut self, outcome: Result<pb::HolgerBuildReply, BuildErr>) {
match outcome {
Ok(reply) => {
self.tiers = tiers_from_wire(&reply.tiers);
self.table = table_from_tiers(&self.tiers);
self.complete = reply.complete;
self.resolved = reply.resolved;
self.missing = reply.missing;
self.found = reply.found;
self.error = None;
self.conn = if !reply.found {
// holger declares no build-space — an honest empty, not offline.
Conn::NoBuildSpace
} else if self.tiers.is_empty() {
// found but zero tiers (degenerate) — still honest, not a blank.
Conn::Empty
} else {
Conn::Loaded
};
}
Err(e) => {
// A failed resolve keeps the last-good matrix out of the way (cleared) and
// lands in the classified honest state — the tooltip carries the detail.
self.tiers = Vec::new();
self.table = table_from_tiers(&[]);
self.complete = false;
self.resolved = 0;
self.missing = 0;
self.found = false;
self.error = Some(e.message().to_string());
self.conn = match e {
BuildErr::Unreachable(_) => Conn::Unreachable,
BuildErr::AuthFailed(_) => Conn::AuthFailed,
BuildErr::NotServed(_) => Conn::NotServed,
BuildErr::Rpc(_) => Conn::Error,
};
debug_assert_eq!(self.conn.key(), e.key());
}
}
}
/// The honest one-line status message the header shows for the current state.
fn status_line(&self) -> String {
match self.conn {
Conn::Idle => "resolving…".to_string(),
Conn::Loaded => {
let verdict = if self.complete {
"complete"
} else {
"INCOMPLETE"
};
// PLAIN NUMBERS (no thousands separators) — a nordisk law.
format!(
"{} tiers · {} resolved / {} unwired · plan {verdict}",
self.tiers.len(),
self.resolved,
self.missing
)
}
Conn::NoBuildSpace => format!(
"● connected to {} · holger declares no build-space",
self.endpoint
),
Conn::Empty => "connected · the build-space declares no tiers".to_string(),
Conn::Unreachable => {
format!(
"build matrix unavailable — cannot reach dwarves-server at {}",
self.endpoint
)
}
Conn::AuthFailed => {
"auth failed — the dwarves-server rejected the bearer token".to_string()
}
Conn::NotServed => {
"not served — this dwarves-server does not expose ResolveHolgerBuild".to_string()
}
Conn::Error => "resolve failed — see the error detail".to_string(),
}
}
/// The status colour for the current state (green live · amber degraded · red down).
fn status_color(&self) -> egui::Color32 {
match self.conn {
Conn::Loaded if self.complete => egui::Color32::from_rgb(120, 190, 120),
Conn::Loaded => egui::Color32::from_rgb(224, 160, 60), // rendered but incomplete plan
Conn::NoBuildSpace | Conn::Empty => egui::Color32::from_rgb(150, 150, 150),
Conn::Idle => egui::Color32::from_rgb(150, 150, 150),
Conn::Unreachable | Conn::AuthFailed | Conn::NotServed | Conn::Error => {
egui::Color32::from_rgb(224, 120, 90)
}
}
}
}
impl Facet for HolgerBuildPanel {
fn title(&self) -> &str {
"Builds"
}
fn ui(&mut self, ui: &mut egui::Ui) {
// LIVE auto-poll: re-resolve on the interval so a build-space edit shows up.
let now_s = ui.input(|i| i.time);
if self.poll_due(now_s) {
self.refresh();
self.last_poll_s = now_s;
}
if self.auto_poll {
ui.ctx()
.request_repaint_after(Duration::from_secs_f32(self.poll_interval_s as f32));
}
let mut do_refresh = false;
ui.horizontal(|ui| {
if ui
.button("⟳ Refresh")
.on_hover_text(format!(
"re-resolve holger's build matrix from {}",
self.endpoint
))
.clicked()
{
do_refresh = true;
}
let (glyph, hint) = if self.auto_poll {
("● live", "auto-polling — click to pause")
} else {
("⏸ paused", "auto-poll paused — click to resume live")
};
if ui.button(glyph).on_hover_text(hint).clicked() {
self.auto_poll = !self.auto_poll;
self.last_poll_s = now_s;
}
ui.separator();
let resp = ui.colored_label(self.status_color(), self.status_line());
if let Some(e) = &self.error {
resp.on_hover_text(e);
}
});
if do_refresh {
self.refresh();
self.last_poll_s = now_s;
}
ui.separator();
// Render the shared matrix table when loaded; else the honest empty message.
if self.conn.is_error_like() {
ui.centered_and_justified(|ui| {
ui.weak(self.status_line());
});
} else {
Facet::ui(&mut self.table, ui);
}
}
fn state_json(&self) -> serde_json::Value {
serde_json::json!({
"kind": "holger_build_matrix",
"endpoint": self.endpoint,
"holger_root": self.holger_root,
"status": self.conn.key(),
// The matrix as data — the whole 3-tier resolution a robot asserts.
"tiers": self.tiers,
"tier_count": self.tiers.len(),
"resolved": self.resolved,
"missing": self.missing,
"complete": self.complete,
"found": self.found,
"error": self.error,
// Poll state.
"auto_poll": self.auto_poll,
"poll_interval_s": self.poll_interval_s,
// The shared table's own state (columns + row count) — the reused renderer.
"table": Facet::state_json(&self.table),
})
}
fn update_json(&mut self, msg_json: &str) {
// The pane's own control surface (drives it headless from a robot test):
// {"refresh": true} / {"poll": true} → re-resolve the build matrix
// {"auto_poll": bool} → toggle live polling
// {"holger_root": "…"} → pin the server-side checkout + re-resolve
// Anything else is forwarded to the inner table (its sort/select JSON seam).
if let Ok(v) = serde_json::from_str::<serde_json::Value>(msg_json) {
if let Some(on) = v.get("auto_poll").and_then(|r| r.as_bool()) {
self.auto_poll = on;
return;
}
if let Some(root) = v.get("holger_root").and_then(|r| r.as_str()) {
self.holger_root = root.to_string();
self.refresh();
return;
}
if v.get("refresh").and_then(|r| r.as_bool()) == Some(true)
|| v.get("poll").and_then(|r| r.as_bool()) == Some(true)
{
self.refresh();
return;
}
}
Facet::update_json(&mut self.table, msg_json);
}
}
// ── the seam ──────────────────────────────────────────────────────────────────
/// **The holger build-matrix pane as a `Box<dyn Panel>`.** `endpoint` is the
/// `dwarves-server` url (e.g. `http://127.0.0.1:7900`). The bearer comes from the
/// dwarves client chain ([`crate::ClientCredential`]: `$DWARVES_SERVER_TOKEN`, then
/// `~/.dwarves/token`); the server-side holger checkout is pinned from
/// `$DWARVES_HOLGER_REPO` (empty ⇒ the server resolves its own default). The first resolve
/// happens eagerly; offline / no build-space ⇒ an honest state, never a panic.
pub fn holger_build_panel(endpoint: impl Into<String>) -> Box<dyn Panel> {
let token = crate::ClientCredential::resolve().token;
let holger_root = std::env::var(HOLGER_ROOT_ENV).unwrap_or_default();
Box::new(HolgerBuildPanel::new(endpoint.into(), token, holger_root))
}
/// Like [`holger_build_panel`] but with the token + holger root passed explicitly (for
/// hosts that manage their own credentials rather than reading the environment).
pub fn holger_build_panel_with(
endpoint: impl Into<String>,
token: impl Into<String>,
holger_root: impl Into<String>,
) -> Box<dyn Panel> {
Box::new(HolgerBuildPanel::new(
endpoint.into(),
token.into(),
holger_root.into(),
))
}
#[cfg(test)]
mod tests {
use super::*;
/// A full reply over holger's product rows (`holger`, `holger-downstream`,
/// `holger-cli`), all resolved.
fn full_reply() -> pb::HolgerBuildReply {
let tier = |name: &str| pb::HolgerTier {
tier: name.into(),
step: format!("build-{name}"),
resolved: true,
bin: name.into(),
package: name.into(),
command_line: format!("cargo build -p {name} --bin {name} --release"),
};
pb::HolgerBuildReply {
tiers: vec![tier("holger"), tier("holger-downstream"), tier("holger-cli")],
resolved: 3,
missing: 0,
complete: true,
found: true,
}
}
/// PURE mapping: the wire tiers project onto the matrix rows (order preserved), and an
/// unwired tier keeps its empty target fields. RED-when-broken: a dropped field mapping
/// loses the bin/package/status.
#[test]
fn wire_tiers_map_to_matrix_rows() {
let rows = tiers_from_wire(&full_reply().tiers);
assert_eq!(rows.len(), 3, "one row per tier");
assert_eq!(rows[0].tier, "holger");
assert_eq!(rows[0].bin, "holger");
assert_eq!(rows[0].package, "holger");
assert!(rows[0].resolved);
assert!(
rows[2].command_line.starts_with("cargo build"),
"the cargo argv crosses"
);
assert!(tiers_from_wire(&[]).is_empty(), "no tiers → empty matrix");
// The shared table gets the columns + one row per tier, keyed on the tier name.
let t = table_from_tiers(&rows);
let st = Facet::state_json(&t);
assert_eq!(st["rows"], 3, "the reused table holds one row per tier");
assert_eq!(st["columns"][1], "status", "the status column is present");
}
/// **The headline live-data oracle — RED-when-empty.** A full 3-tier reply driven
/// through the real `apply_fetch` path makes the pane render LIVE rows: `loaded`, the
/// matrix has 3 tiers, the plan is complete, and the reused table holds 3 rows. RED
/// when the pane is empty/disconnected (the paired negative leg: an unreachable resolve
/// lands in `unreachable` with zero tiers — the state the oracle rejects). Emits a
/// testmatrix cell (reuse facett-core → edda).
#[test]
fn robot_build_matrix_shows_live_rows_red_when_empty() {
let mut p = HolgerBuildPanel::new("http://127.0.0.1:0", "", "");
p.apply_fetch(Ok(full_reply()));
let st = Facet::state_json(&p);
let shows_live_rows = st["status"] == "loaded"
&& st["tier_count"] == 3
&& st["resolved"] == 3
&& st["missing"] == 0
&& st["complete"] == true
&& st["found"] == true
&& st["table"]["rows"] == 3
&& st["tiers"][0]["tier"] == "holger"
&& st["tiers"][2]["bin"] == "holger-cli";
assert!(
shows_live_rows,
"the Builds pane must show LIVE tier rows, not a blank pane: {st}"
);
// Negative leg — pin the oracle: an unreachable resolve is the zero-tier state the
// live oracle rejects, so the test can only pass when real data is present.
let mut down = HolgerBuildPanel::new("http://127.0.0.1:0", "", "");
down.apply_fetch(Err(BuildErr::Unreachable("connect refused".into())));
let dst = Facet::state_json(&down);
let empty_is_red =
dst["status"] == "unreachable" && dst["tier_count"] == 0 && dst["table"]["rows"] == 0;
assert!(
empty_is_red,
"an unreachable resolve is the zero-tier state the oracle rejects: {dst}"
);
facett_core::testmatrix::emit(
"dwarves-embed",
"holger_build_matrix_shows_live_rows_red_when_empty",
shows_live_rows && empty_is_red,
"the holger build-matrix pane driven through apply_fetch shows LIVE 3-tier rows (loaded + tier_count/resolved/table.rows, plan complete) on the reused facett-table; RED when the pane is empty/disconnected (unreachable, zero tiers)",
);
}
/// RED-when-broken: a build-space missing the `holger-cli` row resolves to an INCOMPLETE
/// plan — the pane still renders (loaded) but surfaces `complete=false` + the unwired
/// tier as an `unwired` row, never a false-green. The guard against a silently-dropped
/// deployment target.
#[test]
fn a_dropped_tier_renders_incomplete_and_unwired() {
let reply = pb::HolgerBuildReply {
tiers: vec![
pb::HolgerTier {
tier: "holger".into(),
step: "build-holger".into(),
resolved: true,
bin: "holger".into(),
package: "holger".into(),
command_line: "cargo build …".into(),
},
pb::HolgerTier {
tier: "holger-downstream".into(),
step: "build-holger-downstream".into(),
resolved: true,
bin: "holger-downstream".into(),
package: "holger-downstream".into(),
command_line: "cargo build …".into(),
},
pb::HolgerTier {
tier: "holger-cli".into(),
step: "build-holger-cli".into(),
resolved: false,
bin: String::new(),
package: String::new(),
command_line: String::new(),
},
],
resolved: 2,
missing: 1,
complete: false,
found: true,
};
let mut p = HolgerBuildPanel::new("http://127.0.0.1:0", "", "");
p.apply_fetch(Ok(reply));
let st = Facet::state_json(&p);
assert_eq!(
st["status"], "loaded",
"an incomplete plan still renders, not a blank"
);
assert_eq!(
st["complete"], false,
"a dropped tier ⇒ complete=false (no false green)"
);
assert_eq!(st["missing"], 1);
let full = st["tiers"]
.as_array()
.unwrap()
.iter()
.find(|t| t["tier"] == "holger-cli")
.unwrap();
assert_eq!(full["resolved"], false, "the `holger-cli` row is unwired");
assert_eq!(full["bin"], "", "an unwired tier has an empty bin");
// The cell formatting makes the unwired tier VISIBLE (a `—` placeholder, never blank).
let cells = tier_cells(
&tiers_from_wire(&[pb::HolgerTier {
tier: "holger-cli".into(),
step: "build-holger-cli".into(),
resolved: false,
..Default::default()
}])[0],
);
assert_eq!(cells[1], "unwired");
assert_eq!(cells[2], "—", "empty bin renders as the — placeholder");
}
/// Every failure lands in its DISTINCT honest state (never a silent blank / a generic
/// "offline"): connect ⇒ `unreachable`, Unauthenticated ⇒ `auth_failed`, Unimplemented
/// ⇒ `not_served`, other RPC ⇒ `error`; and `found=false` ⇒ `no_build_space`. The
/// error text is preserved for the tooltip in each case.
#[test]
fn honest_states_are_all_distinct() {
let drive = |outcome: Result<pb::HolgerBuildReply, BuildErr>| {
let mut p = HolgerBuildPanel::new("http://127.0.0.1:0", "", "");
p.apply_fetch(outcome);
Facet::state_json(&p)
};
// The four failure legs — classified from the tonic Code.
let unreachable = drive(Err(BuildErr::from_status(&tonic::Status::unavailable(
"no route",
))));
assert_eq!(
unreachable["status"], "error",
"generic Unavailable ⇒ error"
);
let auth = drive(Err(BuildErr::from_status(&tonic::Status::unauthenticated(
"bad bearer",
))));
assert_eq!(auth["status"], "auth_failed");
assert!(
auth["error"].as_str().unwrap().contains("bad bearer"),
"the auth error is surfaced"
);
let denied = drive(Err(BuildErr::from_status(
&tonic::Status::permission_denied("nope"),
)));
assert_eq!(
denied["status"], "auth_failed",
"PermissionDenied ⇒ auth_failed too"
);
let not_served = drive(Err(BuildErr::from_status(&tonic::Status::unimplemented(
"no such method",
))));
assert_eq!(not_served["status"], "not_served");
let conn = drive(Err(BuildErr::Unreachable("connect refused".into())));
assert_eq!(conn["status"], "unreachable");
// found=false ⇒ the honest empty (no_build_space), NOT loaded and NOT offline.
let empty = drive(Ok(pb::HolgerBuildReply {
found: false,
..Default::default()
}));
assert_eq!(empty["status"], "no_build_space");
assert_eq!(empty["tier_count"], 0);
assert!(empty["error"].is_null(), "an honest empty is not an error");
// found=true but zero tiers ⇒ the degenerate `empty` (still not a blank).
let degenerate = drive(Ok(pb::HolgerBuildReply {
found: true,
tiers: vec![],
..Default::default()
}));
assert_eq!(degenerate["status"], "empty");
facett_core::testmatrix::emit(
"dwarves-embed",
"holger_build_matrix_honest_states",
unreachable["status"] == "error"
&& auth["status"] == "auth_failed"
&& not_served["status"] == "not_served"
&& conn["status"] == "unreachable"
&& empty["status"] == "no_build_space"
&& degenerate["status"] == "empty",
"the holger build-matrix pane surfaces every non-render outcome as a DISTINCT honest state: unreachable / auth_failed / not_served / error / no_build_space / empty — never a silent blank",
);
}
/// ROBOT SCENARIO — the pane drives purely over the `state_json`/`update_json` seam
/// (the channel a `nornir-robotui` scenario + a thin gRPC client use): feed a live
/// reply, assert the matrix, toggle auto-poll + pin a holger root over the seam, then a
/// down leg. RED-when-broken: an unreachable resolve blanks the matrix.
#[test]
fn robot_pane_drives_over_the_seam() {
let mut p = HolgerBuildPanel::new("http://127.0.0.1:0", "", "");
p.apply_fetch(Ok(full_reply()));
let st = Facet::state_json(&p);
let loaded = st["status"] == "loaded" && st["tier_count"] == 3 && st["table"]["rows"] == 3;
assert!(loaded, "live reply reflected on the seam: {st}");
// Toggle auto-poll off over the seam.
Facet::update_json(&mut p, r#"{"auto_poll":false}"#);
let poll_off = Facet::state_json(&p)["auto_poll"] == serde_json::json!(false);
// Pin a server-side holger root over the seam (re-resolves against the dead port ⇒
// unreachable, blanking the matrix — the RED-when-broken leg).
Facet::update_json(&mut p, r#"{"holger_root":"/some/holger"}"#);
let after = Facet::state_json(&p);
let pinned = after["holger_root"] == "/some/holger";
let red = after["status"] == "unreachable" && after["tier_count"] == 0;
let drivable = loaded && poll_off && pinned && red;
assert!(drivable, "the Builds pane drives live-reply + auto-poll + holger-root pin + down over the seam: {after}");
facett_core::testmatrix::emit(
"dwarves-embed",
"holger_build_matrix_pane_drivable",
drivable,
"the holger build-matrix pane drives over state_json/update_json: a live 3-tier reply renders on the reused table, auto-poll toggles, the server-side holger_root pins + re-resolves; a dead endpoint blanks to unreachable (RED-when-broken)",
);
}
#[test]
fn poll_due_respects_interval_and_toggle() {
let p = HolgerBuildPanel::new("http://127.0.0.1:0", "", "");
assert!(p.auto_poll);
assert!(!p.poll_due(0.5), "not due before one interval");
assert!(
p.poll_due(DEFAULT_BUILD_POLL_INTERVAL_S),
"due at one interval"
);
let mut p = p;
p.set_auto_poll(false);
assert!(!p.poll_due(99.0), "never due while paused");
}
/// An offline endpoint yields the clean `unreachable` state on construction, never a
/// panic (the read leg's non-panic discipline).
#[test]
fn offline_server_is_a_clean_unreachable_state() {
let p = HolgerBuildPanel::new("http://127.0.0.1:0", "", "");
let st = Facet::state_json(&p);
assert_eq!(st["status"], "unreachable");
assert_eq!(st["tier_count"], 0);
assert!(
st["error"].is_string(),
"the error is captured for the tooltip"
);
}
}