huskies: merge 940

This commit is contained in:
dave
2026-05-12 23:05:50 +00:00
parent b8ec3e2025
commit 0f0cf59329
2 changed files with 172 additions and 45 deletions
+3 -3
View File
@@ -195,7 +195,7 @@ async fn gateway_notification_poller_continues_when_one_project_unreachable() {
); );
let has_good = messages let has_good = messages
.iter() .iter()
.any(|m| m.contains("[good-project]") && m.contains("10_story_ok")); .any(|m| m.contains("[good-project]") && m.contains("#10"));
assert!( assert!(
has_good, has_good,
"Expected a notification from [good-project]; got: {messages:?}" "Expected a notification from [good-project]; got: {messages:?}"
@@ -792,8 +792,8 @@ async fn broadcaster_forwarder_forwards_events_with_project_tag() {
"Expected [my-project] prefix; got: {plain}" "Expected [my-project] prefix; got: {plain}"
); );
assert!( assert!(
plain.contains("7_story_x"), plain.contains("#7"),
"Expected story ID; got: {plain}" "Expected story number #7; got: {plain}"
); );
} }
+169 -42
View File
@@ -33,12 +33,16 @@ pub fn format_stage_notification(
to_stage: &str, to_stage: &str,
) -> (String, String) { ) -> (String, String) {
let number = extract_item_number(item_id).unwrap_or(item_id); let number = extract_item_number(item_id).unwrap_or(item_id);
let name = story_name.unwrap_or(item_id); let effective_name = story_name.filter(|n| !n.is_empty());
let name_plain = effective_name.map(|n| format!("{n} ")).unwrap_or_default();
let name_html = effective_name
.map(|n| format!("<em>{n}</em> "))
.unwrap_or_default();
let prefix = if to_stage == "Done" { "\u{1f389} " } else { "" }; let prefix = if to_stage == "Done" { "\u{1f389} " } else { "" };
let plain = format!("{prefix}#{number} {name} \u{2014} {from_stage} \u{2192} {to_stage}"); let plain = format!("{prefix}#{number} {name_plain}\u{2014} {from_stage} \u{2192} {to_stage}");
let html = format!( let html = format!(
"{prefix}<strong>#{number}</strong> <em>{name}</em> \u{2014} {from_stage} \u{2192} {to_stage}" "{prefix}<strong>#{number}</strong> {name_html}\u{2014} {from_stage} \u{2192} {to_stage}"
); );
(plain, html) (plain, html)
} }
@@ -52,10 +56,14 @@ pub fn format_error_notification(
reason: &str, reason: &str,
) -> (String, String) { ) -> (String, String) {
let number = extract_item_number(item_id).unwrap_or(item_id); let number = extract_item_number(item_id).unwrap_or(item_id);
let name = story_name.unwrap_or(item_id); let effective_name = story_name.filter(|n| !n.is_empty());
let name_plain = effective_name.map(|n| format!("{n} ")).unwrap_or_default();
let name_html = effective_name
.map(|n| format!("<em>{n}</em> "))
.unwrap_or_default();
let plain = format!("\u{274c} #{number} {name} \u{2014} {reason}"); let plain = format!("\u{274c} #{number} {name_plain}\u{2014} {reason}");
let html = format!("\u{274c} <strong>#{number}</strong> <em>{name}</em> \u{2014} {reason}"); let html = format!("\u{274c} <strong>#{number}</strong> {name_html}\u{2014} {reason}");
(plain, html) (plain, html)
} }
@@ -68,11 +76,15 @@ pub fn format_blocked_notification(
reason: &str, reason: &str,
) -> (String, String) { ) -> (String, String) {
let number = extract_item_number(item_id).unwrap_or(item_id); let number = extract_item_number(item_id).unwrap_or(item_id);
let name = story_name.unwrap_or(item_id); let effective_name = story_name.filter(|n| !n.is_empty());
let name_plain = effective_name.map(|n| format!("{n} ")).unwrap_or_default();
let name_html = effective_name
.map(|n| format!("<em>{n}</em> "))
.unwrap_or_default();
let plain = format!("\u{1f6ab} #{number} {name} \u{2014} BLOCKED: {reason}"); let plain = format!("\u{1f6ab} #{number} {name_plain}\u{2014} BLOCKED: {reason}");
let html = let html =
format!("\u{1f6ab} <strong>#{number}</strong> <em>{name}</em> \u{2014} BLOCKED: {reason}"); format!("\u{1f6ab} <strong>#{number}</strong> {name_html}\u{2014} BLOCKED: {reason}");
(plain, html) (plain, html)
} }
@@ -85,12 +97,17 @@ pub fn format_rate_limit_notification(
agent_name: &str, agent_name: &str,
) -> (String, String) { ) -> (String, String) {
let number = extract_item_number(item_id).unwrap_or(item_id); let number = extract_item_number(item_id).unwrap_or(item_id);
let name = story_name.unwrap_or(item_id); let effective_name = story_name.filter(|n| !n.is_empty());
let name_plain = effective_name.map(|n| format!("{n} ")).unwrap_or_default();
let name_html = effective_name
.map(|n| format!("<em>{n}</em> "))
.unwrap_or_default();
let plain = let plain = format!(
format!("\u{26a0}\u{fe0f} #{number} {name} \u{2014} {agent_name} hit an API rate limit"); "\u{26a0}\u{fe0f} #{number} {name_plain}\u{2014} {agent_name} hit an API rate limit"
);
let html = format!( let html = format!(
"\u{26a0}\u{fe0f} <strong>#{number}</strong> <em>{name}</em> \u{2014} \ "\u{26a0}\u{fe0f} <strong>#{number}</strong> {name_html}\u{2014} \
{agent_name} hit an API rate limit" {agent_name} hit an API rate limit"
); );
(plain, html) (plain, html)
@@ -127,11 +144,15 @@ pub fn format_agent_started_notification(
agent_name: &str, agent_name: &str,
) -> (String, String) { ) -> (String, String) {
let number = extract_item_number(item_id).unwrap_or(item_id); let number = extract_item_number(item_id).unwrap_or(item_id);
let name = story_name.unwrap_or(item_id); let effective_name = story_name.filter(|n| !n.is_empty());
let plain = format!("\u{1F916} #{number} {name} \u{2014} {agent_name} started"); let name_plain = effective_name.map(|n| format!("{n} ")).unwrap_or_default();
let html = format!( let name_html = effective_name
"\u{1F916} <strong>#{number}</strong> <em>{name}</em> \u{2014} {agent_name} started" .map(|n| format!("<em>{n}</em> "))
); .unwrap_or_default();
let plain = format!("\u{1F916} #{number} {name_plain}\u{2014} {agent_name} started");
let html =
format!("\u{1F916} <strong>#{number}</strong> {name_html}\u{2014} {agent_name} started");
(plain, html) (plain, html)
} }
@@ -146,16 +167,20 @@ pub fn format_agent_completed_notification(
success: bool, success: bool,
) -> (String, String) { ) -> (String, String) {
let number = extract_item_number(item_id).unwrap_or(item_id); let number = extract_item_number(item_id).unwrap_or(item_id);
let name = story_name.unwrap_or(item_id); let effective_name = story_name.filter(|n| !n.is_empty());
let name_plain = effective_name.map(|n| format!("{n} ")).unwrap_or_default();
let name_html = effective_name
.map(|n| format!("<em>{n}</em> "))
.unwrap_or_default();
let (emoji, result) = if success { let (emoji, result) = if success {
("\u{2705}", "completed") // ✅ ("\u{2705}", "completed") // ✅
} else { } else {
("\u{274C}", "failed") // ❌ ("\u{274C}", "failed") // ❌
}; };
let plain = format!("{emoji} #{number} {name} \u{2014} {agent_name} {result}"); let plain = format!("{emoji} #{number} {name_plain}\u{2014} {agent_name} {result}");
let html = format!( let html =
"{emoji} <strong>#{number}</strong> <em>{name}</em> \u{2014} {agent_name} {result}" format!("{emoji} <strong>#{number}</strong> {name_html}\u{2014} {agent_name} {result}");
);
(plain, html) (plain, html)
} }
@@ -241,9 +266,10 @@ mod tests {
} }
#[test] #[test]
fn format_notification_without_story_name_falls_back_to_item_id() { fn format_stage_notification_without_story_name_falls_back_to_number() {
let (plain, _html) = format_stage_notification("42_bug_fix_thing", None, "Current", "QA"); let (plain, html) = format_stage_notification("42_bug_fix_thing", None, "Current", "QA");
assert_eq!(plain, "#42 42_bug_fix_thing \u{2014} Current \u{2192} QA"); assert_eq!(plain, "#42 \u{2014} Current \u{2192} QA");
assert_eq!(html, "<strong>#42</strong> \u{2014} Current \u{2192} QA");
} }
#[test] #[test]
@@ -265,12 +291,10 @@ mod tests {
} }
#[test] #[test]
fn format_stage_notification_empty_story_name_falls_back_to_id() { fn format_stage_notification_empty_story_name_falls_back_to_number() {
// Some("") is a valid Some but empty — treat as missing? Currently we use it as-is. let (plain, html) = format_stage_notification("42_story_empty", Some(""), "Current", "QA");
let (plain, _html) = format_stage_notification("42_story_empty", Some(""), "Current", "QA"); assert_eq!(plain, "#42 \u{2014} Current \u{2192} QA");
// The name slot is empty but the structure is still correct. assert_eq!(html, "<strong>#42</strong> \u{2014} Current \u{2192} QA");
assert!(plain.contains("#42"));
assert!(plain.contains("Current \u{2192} QA"));
} }
#[test] #[test]
@@ -301,9 +325,10 @@ mod tests {
} }
#[test] #[test]
fn format_error_notification_without_story_name_falls_back_to_item_id() { fn format_error_notification_without_story_name_falls_back_to_number() {
let (plain, _html) = format_error_notification("42_bug_fix_thing", None, "tests failed"); let (plain, html) = format_error_notification("42_bug_fix_thing", None, "tests failed");
assert_eq!(plain, "\u{274c} #42 42_bug_fix_thing \u{2014} tests failed"); assert_eq!(plain, "\u{274c} #42 \u{2014} tests failed");
assert_eq!(html, "\u{274c} <strong>#42</strong> \u{2014} tests failed");
} }
#[test] #[test]
@@ -330,6 +355,13 @@ mod tests {
assert!(plain.contains("错误:合并冲突")); assert!(plain.contains("错误:合并冲突"));
} }
#[test]
fn format_error_notification_empty_story_name_falls_back_to_number() {
let (plain, _html) =
format_error_notification("42_bug_fix_thing", Some(""), "tests failed");
assert_eq!(plain, "\u{274c} #42 \u{2014} tests failed");
}
// ── format_blocked_notification ─────────────────────────────────────────── // ── format_blocked_notification ───────────────────────────────────────────
#[test] #[test]
@@ -350,14 +382,21 @@ mod tests {
} }
#[test] #[test]
fn format_blocked_notification_falls_back_to_item_id() { fn format_blocked_notification_falls_back_to_number() {
let (plain, _html) = format_blocked_notification("42_story_thing", None, "empty diff"); let (plain, html) = format_blocked_notification("42_story_thing", None, "empty diff");
assert_eq!(plain, "\u{1f6ab} #42 \u{2014} BLOCKED: empty diff");
assert_eq!( assert_eq!(
plain, html,
"\u{1f6ab} #42 42_story_thing \u{2014} BLOCKED: empty diff" "\u{1f6ab} <strong>#42</strong> \u{2014} BLOCKED: empty diff"
); );
} }
#[test]
fn format_blocked_notification_empty_story_name_falls_back_to_number() {
let (plain, _html) = format_blocked_notification("42_story_thing", Some(""), "empty diff");
assert_eq!(plain, "\u{1f6ab} #42 \u{2014} BLOCKED: empty diff");
}
#[test] #[test]
fn format_blocked_notification_unicode_reason() { fn format_blocked_notification_unicode_reason() {
let (plain, _html) = format_blocked_notification("3_story_x", Some("X"), "理由:空の差分"); let (plain, _html) = format_blocked_notification("3_story_x", Some("X"), "理由:空の差分");
@@ -381,11 +420,24 @@ mod tests {
} }
#[test] #[test]
fn format_rate_limit_notification_falls_back_to_item_id() { fn format_rate_limit_notification_falls_back_to_number() {
let (plain, _html) = format_rate_limit_notification("42_story_thing", None, "coder-1"); let (plain, html) = format_rate_limit_notification("42_story_thing", None, "coder-1");
assert_eq!( assert_eq!(
plain, plain,
"\u{26a0}\u{fe0f} #42 42_story_thing \u{2014} coder-1 hit an API rate limit" "\u{26a0}\u{fe0f} #42 \u{2014} coder-1 hit an API rate limit"
);
assert_eq!(
html,
"\u{26a0}\u{fe0f} <strong>#42</strong> \u{2014} coder-1 hit an API rate limit"
);
}
#[test]
fn format_rate_limit_notification_empty_story_name_falls_back_to_number() {
let (plain, _html) = format_rate_limit_notification("42_story_thing", Some(""), "coder-1");
assert_eq!(
plain,
"\u{26a0}\u{fe0f} #42 \u{2014} coder-1 hit an API rate limit"
); );
} }
@@ -395,4 +447,79 @@ mod tests {
assert!(plain.contains("агент-1")); assert!(plain.contains("агент-1"));
assert!(plain.contains("hit an API rate limit")); assert!(plain.contains("hit an API rate limit"));
} }
// ── format_agent_started_notification ─────────────────────────────────────
#[test]
fn format_agent_started_notification_with_story_name() {
let (plain, html) =
format_agent_started_notification("42_story_foo", Some("My Feature"), "coder-1");
assert_eq!(plain, "\u{1F916} #42 My Feature \u{2014} coder-1 started");
assert_eq!(
html,
"\u{1F916} <strong>#42</strong> <em>My Feature</em> \u{2014} coder-1 started"
);
}
#[test]
fn format_agent_started_notification_falls_back_to_number() {
let (plain, html) = format_agent_started_notification("42_story_foo", None, "coder-1");
assert_eq!(plain, "\u{1F916} #42 \u{2014} coder-1 started");
assert_eq!(
html,
"\u{1F916} <strong>#42</strong> \u{2014} coder-1 started"
);
}
#[test]
fn format_agent_started_notification_empty_name_falls_back_to_number() {
let (plain, _html) = format_agent_started_notification("42_story_foo", Some(""), "coder-1");
assert_eq!(plain, "\u{1F916} #42 \u{2014} coder-1 started");
}
// ── format_agent_completed_notification ───────────────────────────────────
#[test]
fn format_agent_completed_notification_success_with_story_name() {
let (plain, html) = format_agent_completed_notification(
"42_story_foo",
Some("My Feature"),
"coder-1",
true,
);
assert_eq!(plain, "\u{2705} #42 My Feature \u{2014} coder-1 completed");
assert_eq!(
html,
"\u{2705} <strong>#42</strong> <em>My Feature</em> \u{2014} coder-1 completed"
);
}
#[test]
fn format_agent_completed_notification_failure_with_story_name() {
let (plain, _html) = format_agent_completed_notification(
"42_story_foo",
Some("My Feature"),
"coder-1",
false,
);
assert_eq!(plain, "\u{274C} #42 My Feature \u{2014} coder-1 failed");
}
#[test]
fn format_agent_completed_notification_falls_back_to_number() {
let (plain, html) =
format_agent_completed_notification("42_story_foo", None, "coder-1", true);
assert_eq!(plain, "\u{2705} #42 \u{2014} coder-1 completed");
assert_eq!(
html,
"\u{2705} <strong>#42</strong> \u{2014} coder-1 completed"
);
}
#[test]
fn format_agent_completed_notification_empty_name_falls_back_to_number() {
let (plain, _html) =
format_agent_completed_notification("42_story_foo", Some(""), "coder-1", false);
assert_eq!(plain, "\u{274C} #42 \u{2014} coder-1 failed");
}
} }