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");
+ }
}