refactor(#2591): hivectl derives DagView label + roll-up state from nodes
hivectl/dag_progress.rs was reading the now-removed DagView.kind/state. Derive both from the node set via the shared DagView::rollup_state() + DagView::label() helpers (added to hive-sh4re). Timestamps are DateTime<Utc> now — elapsed calcs compare in unix seconds. Dropped the live step display (step left the wire). Test fixtures updated to the slim shape.
This commit is contained in:
parent
52dd9ede23
commit
33c3fb3b34
2 changed files with 71 additions and 60 deletions
|
|
@ -192,4 +192,32 @@ impl DagView {
|
|||
State::Done
|
||||
}
|
||||
}
|
||||
|
||||
/// A short human label for the DAG, derived from its node kinds — the
|
||||
/// shared replacement for the old `Template` wire string now that the kind
|
||||
/// isn't sent. Priority-ordered so the most distinctive node wins (an
|
||||
/// approval deploy reads as "deploy" even though it also rebuilds). Purely
|
||||
/// cosmetic (progress/queue display); consumers that need exactness inspect
|
||||
/// the node kinds directly.
|
||||
#[must_use]
|
||||
pub fn label(&self) -> &'static str {
|
||||
let has = |k: &str| self.nodes.iter().any(|n| n.kind == k);
|
||||
if has("approval_deploy") {
|
||||
"deploy"
|
||||
} else if has("meta_lock") {
|
||||
"meta-update"
|
||||
} else if has("create") || has("provision") {
|
||||
"spawn"
|
||||
} else if has("swap") || has("prebuild") || has("post_swap") {
|
||||
"rebuild"
|
||||
} else if has("write_perm_file") {
|
||||
"perm-change"
|
||||
} else if has("signal") || has("drain") {
|
||||
"graceful"
|
||||
} else if has("set_wanted") || has("stop_for_update") {
|
||||
"power"
|
||||
} else {
|
||||
"reconcile"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,8 +66,8 @@ async fn wait_for_dags_plain(socket: &Path, ids: Vec<u64>) -> Result<()> {
|
|||
// may still be running — keep watching so the operator
|
||||
// sees whether the agent came back.
|
||||
if d.nodes.iter().all(|n| n.state.is_terminal()) {
|
||||
if d.state == hive_sh4re::jobs::State::Failed {
|
||||
failed.push(format!("{} {}", d.kind.as_str(), dag_agents(d)));
|
||||
if d.rollup_state() == hive_sh4re::jobs::State::Failed {
|
||||
failed.push(format!("{} {}", d.label(), dag_agents(d)));
|
||||
}
|
||||
} else {
|
||||
all_terminal = false;
|
||||
|
|
@ -134,8 +134,8 @@ async fn wait_for_dags_animated(socket: &Path, ids: Vec<u64>) -> Result<()> {
|
|||
});
|
||||
hdr.set_message(format!(
|
||||
"{} {} {} · {}",
|
||||
state_glyph(d.state),
|
||||
d.kind.as_str(),
|
||||
state_glyph(d.rollup_state()),
|
||||
d.label(),
|
||||
dag_agents(d),
|
||||
fmt_dur(dag_elapsed(d, now)),
|
||||
));
|
||||
|
|
@ -163,8 +163,8 @@ async fn wait_for_dags_animated(socket: &Path, ids: Vec<u64>) -> Result<()> {
|
|||
}
|
||||
}
|
||||
if d.nodes.iter().all(|n| n.state.is_terminal()) {
|
||||
if d.state == hive_sh4re::jobs::State::Failed {
|
||||
failed.push(format!("{} {}", d.kind.as_str(), dag_agents(d)));
|
||||
if d.rollup_state() == hive_sh4re::jobs::State::Failed {
|
||||
failed.push(format!("{} {}", d.label(), dag_agents(d)));
|
||||
}
|
||||
} else {
|
||||
all_terminal = false;
|
||||
|
|
@ -224,18 +224,21 @@ fn now_unix() -> i64 {
|
|||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
/// Elapsed seconds for a DAG: `started_at` (falling back to `enqueued_at`)
|
||||
/// through `finished_at` or `now`.
|
||||
/// Elapsed seconds for a DAG: `started_at` (falling back to `created_at`)
|
||||
/// through `finished_at` or `now`. The wire carries these as RFC3339
|
||||
/// `DateTime<Utc>`; compare in unix seconds against `now`.
|
||||
fn dag_elapsed(d: &hive_sh4re::jobs::DagView, now: i64) -> i64 {
|
||||
let start = d.started_at.unwrap_or(d.enqueued_at);
|
||||
(d.finished_at.unwrap_or(now) - start).max(0)
|
||||
let start = d
|
||||
.started_at
|
||||
.map_or_else(|| d.created_at.timestamp(), |t| t.timestamp());
|
||||
(d.finished_at.map_or(now, |t| t.timestamp()) - start).max(0)
|
||||
}
|
||||
|
||||
/// Elapsed seconds for a node: `started_at` → `finished_at`/`now`, or 0
|
||||
/// when it hasn't started.
|
||||
fn node_elapsed(n: &hive_sh4re::jobs::NodeView, now: i64) -> i64 {
|
||||
match n.started_at {
|
||||
Some(start) => (n.finished_at.unwrap_or(now) - start).max(0),
|
||||
Some(start) => (n.finished_at.map_or(now, |t| t.timestamp()) - start.timestamp()).max(0),
|
||||
None => 0,
|
||||
}
|
||||
}
|
||||
|
|
@ -250,16 +253,11 @@ fn fmt_dur(secs: i64) -> String {
|
|||
}
|
||||
}
|
||||
|
||||
/// One animated node line: kind, live step, an `(after …)` marker for a
|
||||
/// fan-in node (>1 dep), its elapsed timer, and a truncated error tail.
|
||||
/// One animated node line: kind, an `(after …)` marker for a fan-in node
|
||||
/// (>1 dep), its elapsed timer, and a truncated error tail.
|
||||
fn node_line(d: &hive_sh4re::jobs::DagView, n: &hive_sh4re::jobs::NodeView, now: i64) -> String {
|
||||
use std::fmt::Write as _;
|
||||
let mut s = n.kind.clone();
|
||||
if n.state == hive_sh4re::jobs::State::Running
|
||||
&& let Some(step) = &n.step
|
||||
{
|
||||
let _ = write!(s, " ({step})");
|
||||
}
|
||||
if n.deps.len() > 1 {
|
||||
let after: Vec<&str> = n
|
||||
.deps
|
||||
|
|
@ -292,25 +290,21 @@ fn state_glyph(state: hive_sh4re::jobs::State) -> &'static str {
|
|||
}
|
||||
}
|
||||
|
||||
/// One progress line for a DAG: roll-up glyph, template, agent, then
|
||||
/// the node chain with the running node's live step label — the CLI
|
||||
/// twin of the dashboard's queue card. Used by the plain (non-TTY) path.
|
||||
/// One progress line for a DAG: roll-up glyph, derived label, agents, then
|
||||
/// the node chain — the CLI twin of the dashboard's queue card. Roll-up state
|
||||
/// + label are derived from the node set (the wire no longer carries them).
|
||||
/// Used by the plain (non-TTY) path.
|
||||
fn render_dag_line(d: &hive_sh4re::jobs::DagView) -> String {
|
||||
use std::fmt::Write as _;
|
||||
let mut out = format!(
|
||||
"{} {} {:<12}",
|
||||
state_glyph(d.state),
|
||||
d.kind.as_str(),
|
||||
state_glyph(d.rollup_state()),
|
||||
d.label(),
|
||||
dag_agents(d)
|
||||
);
|
||||
for (i, n) in d.nodes.iter().enumerate() {
|
||||
let sep = if i == 0 { " " } else { " → " };
|
||||
let _ = write!(out, "{sep}{} {}", state_glyph(n.state), n.kind);
|
||||
if n.state == hive_sh4re::jobs::State::Running
|
||||
&& let Some(step) = &n.step
|
||||
{
|
||||
let _ = write!(out, " ({step})");
|
||||
}
|
||||
}
|
||||
if let Some(err) = d.nodes.iter().find_map(|n| n.error.as_deref()) {
|
||||
let short: String = err.chars().take(120).collect();
|
||||
|
|
@ -321,76 +315,65 @@ fn render_dag_line(d: &hive_sh4re::jobs::DagView) -> String {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use hive_sh4re::jobs::{DagView, NodeView, Source, State, Template};
|
||||
use hive_sh4re::jobs::{DagView, NodeView, Source, State};
|
||||
use hive_sh4re::wire_time::from_secs;
|
||||
|
||||
use super::render_dag_line;
|
||||
|
||||
fn node(id: u64, agent: &str, kind: &str, state: State, step: Option<&str>) -> NodeView {
|
||||
fn node(id: u64, agent: &str, kind: &str, state: State) -> NodeView {
|
||||
NodeView {
|
||||
id,
|
||||
agent: agent.to_owned(),
|
||||
kind: kind.to_owned(),
|
||||
deps: if id == 0 { vec![] } else { vec![id - 1] },
|
||||
state,
|
||||
step: step.map(str::to_owned),
|
||||
build_log_id: None,
|
||||
started_at: None,
|
||||
finished_at: None,
|
||||
error: None,
|
||||
approval_id: None,
|
||||
inputs: vec![],
|
||||
has_log: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_dag_line_shows_chain_and_running_step() {
|
||||
fn render_dag_line_shows_chain_with_derived_label_and_state() {
|
||||
// Roll-up state (Running) + label ("rebuild") are derived from the node
|
||||
// set — the wire no longer carries them. (`Done` nodes are included
|
||||
// here to exercise glyph rendering; production filters them off.)
|
||||
let dag = DagView {
|
||||
id: 7,
|
||||
kind: Template::Rebuild,
|
||||
state: State::Running,
|
||||
source: Source::Manual,
|
||||
reason: "manual".to_owned(),
|
||||
enqueued_at: 0,
|
||||
started_at: Some(1),
|
||||
created_at: from_secs(0),
|
||||
started_at: Some(from_secs(1)),
|
||||
finished_at: None,
|
||||
inputs: vec![],
|
||||
approval_id: None,
|
||||
nodes: vec![
|
||||
node(0, "alice", "prebuild", State::Done, None),
|
||||
node(1, "alice", "stop_for_update", State::Done, None),
|
||||
node(
|
||||
2,
|
||||
"alice",
|
||||
"swap",
|
||||
State::Running,
|
||||
Some("nixos-container update"),
|
||||
),
|
||||
node(3, "alice", "reconcile", State::Queued, None),
|
||||
node(0, "alice", "prebuild", State::Done),
|
||||
node(1, "alice", "stop_for_update", State::Done),
|
||||
node(2, "alice", "swap", State::Running),
|
||||
node(3, "alice", "reconcile", State::Queued),
|
||||
],
|
||||
};
|
||||
let line = render_dag_line(&dag);
|
||||
assert!(line.starts_with("▶ rebuild alice"), "{line}");
|
||||
assert!(
|
||||
line.contains(
|
||||
"✔ prebuild → ✔ stop_for_update → ▶ swap (nixos-container update) → ⏸ reconcile"
|
||||
),
|
||||
line.contains("✔ prebuild → ✔ stop_for_update → ▶ swap → ⏸ reconcile"),
|
||||
"{line}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_dag_line_surfaces_first_node_error() {
|
||||
let mut failed = node(0, "bob", "prebuild", State::Failed, None);
|
||||
let mut failed = node(0, "bob", "prebuild", State::Failed);
|
||||
failed.error = Some("nix build exploded".to_owned());
|
||||
let dag = DagView {
|
||||
id: 8,
|
||||
kind: Template::Rebuild,
|
||||
state: State::Failed,
|
||||
source: Source::Manual,
|
||||
reason: "manual".to_owned(),
|
||||
enqueued_at: 0,
|
||||
started_at: Some(1),
|
||||
finished_at: Some(2),
|
||||
inputs: vec![],
|
||||
approval_id: None,
|
||||
created_at: from_secs(0),
|
||||
started_at: Some(from_secs(1)),
|
||||
finished_at: Some(from_secs(2)),
|
||||
nodes: vec![failed],
|
||||
};
|
||||
let line = render_dag_line(&dag);
|
||||
|
|
|
|||
Loading…
Reference in a new issue