huskies: merge 617_story_split_gateway_into_service_and_transport
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
//! Gateway aggregation — pure functions for cross-project pipeline status.
|
||||
//!
|
||||
//! Formats aggregated pipeline data into compact text suitable for chat
|
||||
//! transports (Matrix, Slack). Uses `service::pipeline::aggregate_pipeline_counts`
|
||||
//! for per-project parsing.
|
||||
|
||||
use serde_json::Value;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
/// Format an aggregated status map as a compact, one-line-per-project string
|
||||
/// suitable for Matrix/Slack messages.
|
||||
///
|
||||
/// Healthy projects: `🟢 **name** — B:5 C:2 Q:1 M:0 D:12`
|
||||
/// Blocked items appended on the same line: `| blocked: 42 [story]`
|
||||
/// Unreachable projects: `🔴 **name** — UNREACHABLE`
|
||||
pub fn format_aggregate_status_compact(statuses: &BTreeMap<String, Value>) -> String {
|
||||
let mut lines: Vec<String> = Vec::new();
|
||||
for (name, status) in statuses {
|
||||
if let Some(err) = status.get("error").and_then(|e| e.as_str()) {
|
||||
lines.push(format!("\u{1F534} **{name}** — UNREACHABLE: {err}"));
|
||||
} else {
|
||||
let counts = status.get("counts");
|
||||
let b = counts
|
||||
.and_then(|c| c.get("backlog"))
|
||||
.and_then(|n| n.as_u64())
|
||||
.unwrap_or(0);
|
||||
let c = counts
|
||||
.and_then(|c| c.get("current"))
|
||||
.and_then(|n| n.as_u64())
|
||||
.unwrap_or(0);
|
||||
let q = counts
|
||||
.and_then(|c| c.get("qa"))
|
||||
.and_then(|n| n.as_u64())
|
||||
.unwrap_or(0);
|
||||
let m = counts
|
||||
.and_then(|c| c.get("merge"))
|
||||
.and_then(|n| n.as_u64())
|
||||
.unwrap_or(0);
|
||||
let d = counts
|
||||
.and_then(|c| c.get("done"))
|
||||
.and_then(|n| n.as_u64())
|
||||
.unwrap_or(0);
|
||||
|
||||
let blocked_arr = status
|
||||
.get("blocked")
|
||||
.and_then(|a| a.as_array())
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
|
||||
let indicator = if blocked_arr.is_empty() {
|
||||
"\u{1F7E2}" // 🟢
|
||||
} else {
|
||||
"\u{1F7E0}" // 🟠
|
||||
};
|
||||
|
||||
let mut line = format!("{indicator} **{name}** — B:{b} C:{c} Q:{q} M:{m} D:{d}");
|
||||
|
||||
if !blocked_arr.is_empty() {
|
||||
let ids: Vec<String> = blocked_arr
|
||||
.iter()
|
||||
.filter_map(|item| item.get("story_id").and_then(|s| s.as_str()))
|
||||
.map(|s| s.to_string())
|
||||
.collect();
|
||||
line.push_str(&format!(" | blocked: {}", ids.join(", ")));
|
||||
}
|
||||
|
||||
lines.push(line);
|
||||
}
|
||||
}
|
||||
if lines.is_empty() {
|
||||
return "No projects registered.".to_string();
|
||||
}
|
||||
format!("**All Projects**\n\n{}", lines.join("\n\n"))
|
||||
}
|
||||
|
||||
// ── Tests ────────────────────────────────────────────────────────────────────
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn format_healthy_project() {
|
||||
let mut statuses = BTreeMap::new();
|
||||
statuses.insert(
|
||||
"huskies".to_string(),
|
||||
json!({
|
||||
"counts": { "backlog": 5, "current": 2, "qa": 1, "merge": 0, "done": 12 },
|
||||
"blocked": []
|
||||
}),
|
||||
);
|
||||
let output = format_aggregate_status_compact(&statuses);
|
||||
assert!(output.contains("huskies"));
|
||||
assert!(output.contains("B:5"));
|
||||
assert!(output.contains("C:2"));
|
||||
assert!(output.contains("Q:1"));
|
||||
assert!(output.contains("D:12"));
|
||||
assert!(!output.contains("blocked:"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_unreachable_project() {
|
||||
let mut statuses = BTreeMap::new();
|
||||
statuses.insert(
|
||||
"broken".to_string(),
|
||||
json!({ "error": "connection refused" }),
|
||||
);
|
||||
let output = format_aggregate_status_compact(&statuses);
|
||||
assert!(output.contains("broken"));
|
||||
assert!(output.contains("UNREACHABLE"));
|
||||
assert!(output.contains("connection refused"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_blocked_items_shown() {
|
||||
let mut statuses = BTreeMap::new();
|
||||
statuses.insert(
|
||||
"myproj".to_string(),
|
||||
json!({
|
||||
"counts": { "backlog": 0, "current": 1, "qa": 0, "merge": 0, "done": 0 },
|
||||
"blocked": [{ "story_id": "42_story_x", "name": "X", "stage": "current", "reason": "blocked" }]
|
||||
}),
|
||||
);
|
||||
let output = format_aggregate_status_compact(&statuses);
|
||||
assert!(output.contains("blocked:"));
|
||||
assert!(output.contains("42_story_x"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_empty_projects() {
|
||||
let statuses = BTreeMap::new();
|
||||
let output = format_aggregate_status_compact(&statuses);
|
||||
assert_eq!(output, "No projects registered.");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user