huskies: merge 1193 story overview chat command: active work across all connected sleds
This commit is contained in:
@@ -73,6 +73,58 @@ pub fn format_aggregate_status_compact(statuses: &BTreeMap<String, Value>) -> St
|
||||
format!("**All Projects**\n\n{}", lines.join("\n\n"))
|
||||
}
|
||||
|
||||
/// Format per-project active pipeline items as a compact cross-sled overview
|
||||
/// for the `overview` gateway chat command (story 1193).
|
||||
///
|
||||
/// Only items in the In Progress (`current`), QA (`qa`), and Merge (`merge`)
|
||||
/// stages are listed — one line per item, grouped under a `**project**`
|
||||
/// heading. Projects with no items in those stages get a one-line "idle"
|
||||
/// note. Projects whose status carries an `error` key (fetch failure or no
|
||||
/// URL configured) are marked unreachable rather than being dropped from the
|
||||
/// output, so a disconnected sled is never silently omitted.
|
||||
pub fn format_overview_compact(items_by_project: &BTreeMap<String, Value>) -> String {
|
||||
if items_by_project.is_empty() {
|
||||
return "No projects registered.".to_string();
|
||||
}
|
||||
|
||||
let mut sections: Vec<String> = Vec::new();
|
||||
for (name, status) in items_by_project {
|
||||
if let Some(err) = status.get("error").and_then(|e| e.as_str()) {
|
||||
sections.push(format!("**{name}** — unreachable: {err}"));
|
||||
continue;
|
||||
}
|
||||
|
||||
let active = status
|
||||
.get("active")
|
||||
.and_then(|a| a.as_array())
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
|
||||
let lines: Vec<String> = active
|
||||
.iter()
|
||||
.filter_map(|item| {
|
||||
let label = match item.get("stage").and_then(|s| s.as_str()).unwrap_or("") {
|
||||
"current" => "In Progress",
|
||||
"qa" => "QA",
|
||||
"merge" => "Merge",
|
||||
_ => return None,
|
||||
};
|
||||
let story_id = item.get("story_id").and_then(|s| s.as_str()).unwrap_or("?");
|
||||
let story_name = item.get("name").and_then(|s| s.as_str()).unwrap_or("");
|
||||
Some(format!(" • {story_id} — {story_name} ({label})"))
|
||||
})
|
||||
.collect();
|
||||
|
||||
if lines.is_empty() {
|
||||
sections.push(format!("**{name}** — idle"));
|
||||
} else {
|
||||
sections.push(format!("**{name}**\n{}", lines.join("\n")));
|
||||
}
|
||||
}
|
||||
|
||||
format!("**Overview: Active Work**\n\n{}", sections.join("\n\n"))
|
||||
}
|
||||
|
||||
// ── Tests ────────────────────────────────────────────────────────────────────
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -133,4 +185,86 @@ mod tests {
|
||||
let output = format_aggregate_status_compact(&statuses);
|
||||
assert_eq!(output, "No projects registered.");
|
||||
}
|
||||
|
||||
// ── format_overview_compact ────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn overview_lists_items_grouped_by_project_with_stage_labels() {
|
||||
let mut items = BTreeMap::new();
|
||||
items.insert(
|
||||
"huskies".to_string(),
|
||||
json!({
|
||||
"active": [
|
||||
{ "story_id": "42_story_a", "name": "A", "stage": "current" },
|
||||
{ "story_id": "43_story_b", "name": "B", "stage": "qa" },
|
||||
{ "story_id": "44_story_c", "name": "C", "stage": "merge" },
|
||||
{ "story_id": "45_story_d", "name": "D", "stage": "done" },
|
||||
]
|
||||
}),
|
||||
);
|
||||
let output = format_overview_compact(&items);
|
||||
assert!(output.contains("**huskies**"));
|
||||
assert!(output.contains("42_story_a — A (In Progress)"));
|
||||
assert!(output.contains("43_story_b — B (QA)"));
|
||||
assert!(output.contains("44_story_c — C (Merge)"));
|
||||
assert!(
|
||||
!output.contains("45_story_d"),
|
||||
"done-stage items must not appear in the overview: {output}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overview_idle_project_gets_idle_note() {
|
||||
let mut items = BTreeMap::new();
|
||||
items.insert("quiet-proj".to_string(), json!({ "active": [] }));
|
||||
let output = format_overview_compact(&items);
|
||||
assert!(output.contains("**quiet-proj** — idle"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overview_unreachable_project_is_marked_not_omitted() {
|
||||
let mut items = BTreeMap::new();
|
||||
items.insert(
|
||||
"down-proj".to_string(),
|
||||
json!({ "error": "connection refused" }),
|
||||
);
|
||||
let output = format_overview_compact(&items);
|
||||
assert!(output.contains("down-proj"));
|
||||
assert!(output.contains("unreachable"));
|
||||
assert!(output.contains("connection refused"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overview_no_url_project_marked_unreachable() {
|
||||
let mut items = BTreeMap::new();
|
||||
items.insert(
|
||||
"no-url-proj".to_string(),
|
||||
json!({ "error": "no URL configured" }),
|
||||
);
|
||||
let output = format_overview_compact(&items);
|
||||
assert!(output.contains("no-url-proj"));
|
||||
assert!(output.contains("unreachable: no URL configured"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overview_empty_projects_map() {
|
||||
let items = BTreeMap::new();
|
||||
let output = format_overview_compact(&items);
|
||||
assert_eq!(output, "No projects registered.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overview_multiple_projects_one_section_each() {
|
||||
let mut items = BTreeMap::new();
|
||||
items.insert(
|
||||
"alpha".to_string(),
|
||||
json!({ "active": [{ "story_id": "1_x", "name": "X", "stage": "current" }] }),
|
||||
);
|
||||
items.insert("beta".to_string(), json!({ "active": [] }));
|
||||
let output = format_overview_compact(&items);
|
||||
assert!(output.contains("**alpha**"));
|
||||
assert!(output.contains("**beta** — idle"));
|
||||
// alpha's section should come before beta's (BTreeMap sorted order).
|
||||
assert!(output.find("alpha").unwrap() < output.find("beta").unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,11 +19,12 @@ pub mod polling;
|
||||
/// Pure signed release-manifest verification (signature, sha256, rollback) — no I/O.
|
||||
pub mod release_manifest;
|
||||
|
||||
pub use aggregation::format_aggregate_status_compact;
|
||||
pub use aggregation::{format_aggregate_status_compact, format_overview_compact};
|
||||
pub use config::{GatewayConfig, ProjectEntry};
|
||||
pub use identity::{IdentityCheck, check_identity};
|
||||
pub use io::{
|
||||
fetch_all_project_pipeline_statuses, probe_identity, spawn_gateway_broadcaster_forwarder,
|
||||
fetch_all_project_pipeline_items, fetch_all_project_pipeline_statuses, probe_identity,
|
||||
spawn_gateway_broadcaster_forwarder,
|
||||
};
|
||||
|
||||
use crate::http::context::PermissionForward;
|
||||
|
||||
Reference in New Issue
Block a user