diff --git a/server/src/gateway/tests.rs b/server/src/gateway/tests.rs index 99fecf11..f2372707 100644 --- a/server/src/gateway/tests.rs +++ b/server/src/gateway/tests.rs @@ -195,7 +195,7 @@ async fn gateway_notification_poller_continues_when_one_project_unreachable() { ); let has_good = messages .iter() - .any(|m| m.contains("[good-project]") && m.contains("10_story_ok")); + .any(|m| m.contains("[good-project]") && m.contains("#10")); assert!( has_good, "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}" ); assert!( - plain.contains("7_story_x"), - "Expected story ID; got: {plain}" + plain.contains("#7"), + "Expected story number #7; got: {plain}" ); } diff --git a/server/src/service/notifications/format.rs b/server/src/service/notifications/format.rs index bd6a5bbc..c423dc46 100644 --- a/server/src/service/notifications/format.rs +++ b/server/src/service/notifications/format.rs @@ -33,12 +33,16 @@ pub fn format_stage_notification( to_stage: &str, ) -> (String, String) { 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!("{n} ")) + .unwrap_or_default(); 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!( - "{prefix}#{number} {name} \u{2014} {from_stage} \u{2192} {to_stage}" + "{prefix}#{number} {name_html}\u{2014} {from_stage} \u{2192} {to_stage}" ); (plain, html) } @@ -52,10 +56,14 @@ pub fn format_error_notification( reason: &str, ) -> (String, String) { 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!("{n} ")) + .unwrap_or_default(); - let plain = format!("\u{274c} #{number} {name} \u{2014} {reason}"); - let html = format!("\u{274c} #{number} {name} \u{2014} {reason}"); + let plain = format!("\u{274c} #{number} {name_plain}\u{2014} {reason}"); + let html = format!("\u{274c} #{number} {name_html}\u{2014} {reason}"); (plain, html) } @@ -68,11 +76,15 @@ pub fn format_blocked_notification( reason: &str, ) -> (String, String) { 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!("{n} ")) + .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 = - format!("\u{1f6ab} #{number} {name} \u{2014} BLOCKED: {reason}"); + format!("\u{1f6ab} #{number} {name_html}\u{2014} BLOCKED: {reason}"); (plain, html) } @@ -85,12 +97,17 @@ pub fn format_rate_limit_notification( agent_name: &str, ) -> (String, String) { 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!("{n} ")) + .unwrap_or_default(); - let plain = - format!("\u{26a0}\u{fe0f} #{number} {name} \u{2014} {agent_name} hit an API rate limit"); + let plain = format!( + "\u{26a0}\u{fe0f} #{number} {name_plain}\u{2014} {agent_name} hit an API rate limit" + ); let html = format!( - "\u{26a0}\u{fe0f} #{number} {name} \u{2014} \ + "\u{26a0}\u{fe0f} #{number} {name_html}\u{2014} \ {agent_name} hit an API rate limit" ); (plain, html) @@ -127,11 +144,15 @@ pub fn format_agent_started_notification( agent_name: &str, ) -> (String, String) { let number = extract_item_number(item_id).unwrap_or(item_id); - let name = story_name.unwrap_or(item_id); - let plain = format!("\u{1F916} #{number} {name} \u{2014} {agent_name} started"); - let html = format!( - "\u{1F916} #{number} {name} \u{2014} {agent_name} started" - ); + 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!("{n} ")) + .unwrap_or_default(); + + let plain = format!("\u{1F916} #{number} {name_plain}\u{2014} {agent_name} started"); + let html = + format!("\u{1F916} #{number} {name_html}\u{2014} {agent_name} started"); (plain, html) } @@ -146,16 +167,20 @@ pub fn format_agent_completed_notification( success: bool, ) -> (String, String) { 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!("{n} ")) + .unwrap_or_default(); + let (emoji, result) = if success { ("\u{2705}", "completed") // ✅ } else { ("\u{274C}", "failed") // ❌ }; - let plain = format!("{emoji} #{number} {name} \u{2014} {agent_name} {result}"); - let html = format!( - "{emoji} #{number} {name} \u{2014} {agent_name} {result}" - ); + let plain = format!("{emoji} #{number} {name_plain}\u{2014} {agent_name} {result}"); + let html = + format!("{emoji} #{number} {name_html}\u{2014} {agent_name} {result}"); (plain, html) } @@ -241,9 +266,10 @@ mod tests { } #[test] - fn format_notification_without_story_name_falls_back_to_item_id() { - 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"); + fn format_stage_notification_without_story_name_falls_back_to_number() { + let (plain, html) = format_stage_notification("42_bug_fix_thing", None, "Current", "QA"); + assert_eq!(plain, "#42 \u{2014} Current \u{2192} QA"); + assert_eq!(html, "#42 \u{2014} Current \u{2192} QA"); } #[test] @@ -265,12 +291,10 @@ mod tests { } #[test] - fn format_stage_notification_empty_story_name_falls_back_to_id() { - // 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"); - // The name slot is empty but the structure is still correct. - assert!(plain.contains("#42")); - assert!(plain.contains("Current \u{2192} QA")); + fn format_stage_notification_empty_story_name_falls_back_to_number() { + let (plain, html) = format_stage_notification("42_story_empty", Some(""), "Current", "QA"); + assert_eq!(plain, "#42 \u{2014} Current \u{2192} QA"); + assert_eq!(html, "#42 \u{2014} Current \u{2192} QA"); } #[test] @@ -301,9 +325,10 @@ mod tests { } #[test] - fn format_error_notification_without_story_name_falls_back_to_item_id() { - 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"); + fn format_error_notification_without_story_name_falls_back_to_number() { + let (plain, html) = format_error_notification("42_bug_fix_thing", None, "tests failed"); + assert_eq!(plain, "\u{274c} #42 \u{2014} tests failed"); + assert_eq!(html, "\u{274c} #42 \u{2014} tests failed"); } #[test] @@ -330,6 +355,13 @@ mod tests { 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 ─────────────────────────────────────────── #[test] @@ -350,14 +382,21 @@ mod tests { } #[test] - fn format_blocked_notification_falls_back_to_item_id() { - let (plain, _html) = format_blocked_notification("42_story_thing", None, "empty diff"); + fn format_blocked_notification_falls_back_to_number() { + 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!( - plain, - "\u{1f6ab} #42 42_story_thing \u{2014} BLOCKED: empty diff" + html, + "\u{1f6ab} #42 \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] fn format_blocked_notification_unicode_reason() { let (plain, _html) = format_blocked_notification("3_story_x", Some("X"), "理由:空の差分"); @@ -381,11 +420,24 @@ mod tests { } #[test] - fn format_rate_limit_notification_falls_back_to_item_id() { - let (plain, _html) = format_rate_limit_notification("42_story_thing", None, "coder-1"); + fn format_rate_limit_notification_falls_back_to_number() { + let (plain, html) = format_rate_limit_notification("42_story_thing", None, "coder-1"); assert_eq!( 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} #42 \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("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} #42 My Feature \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} #42 \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} #42 My Feature \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} #42 \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"); + } }