huskies: merge 964
This commit is contained in:
@@ -30,12 +30,16 @@ pub fn stage_display_name(stage: &Stage) -> &'static str {
|
||||
/// Returns `(plain_text, html)` suitable for `ChatTransport::send_message`.
|
||||
pub fn format_stage_notification(
|
||||
item_id: &str,
|
||||
story_name: Option<&str>,
|
||||
story_name: &str,
|
||||
from_stage: &Stage,
|
||||
to_stage: &Stage,
|
||||
) -> (String, String) {
|
||||
let number = extract_item_number(item_id).unwrap_or(item_id);
|
||||
let effective_name = story_name.filter(|n| !n.is_empty());
|
||||
let effective_name = if story_name.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(story_name)
|
||||
};
|
||||
let name_plain = effective_name.map(|n| format!("{n} ")).unwrap_or_default();
|
||||
let name_html = effective_name
|
||||
.map(|n| format!("<em>{n}</em> "))
|
||||
@@ -61,11 +65,15 @@ pub fn format_stage_notification(
|
||||
/// Returns `(plain_text, html)` suitable for `ChatTransport::send_message`.
|
||||
pub fn format_error_notification(
|
||||
item_id: &str,
|
||||
story_name: Option<&str>,
|
||||
story_name: &str,
|
||||
reason: &str,
|
||||
) -> (String, String) {
|
||||
let number = extract_item_number(item_id).unwrap_or(item_id);
|
||||
let effective_name = story_name.filter(|n| !n.is_empty());
|
||||
let effective_name = if story_name.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(story_name)
|
||||
};
|
||||
let name_plain = effective_name.map(|n| format!("{n} ")).unwrap_or_default();
|
||||
let name_html = effective_name
|
||||
.map(|n| format!("<em>{n}</em> "))
|
||||
@@ -81,11 +89,15 @@ pub fn format_error_notification(
|
||||
/// Returns `(plain_text, html)` suitable for `ChatTransport::send_message`.
|
||||
pub fn format_blocked_notification(
|
||||
item_id: &str,
|
||||
story_name: Option<&str>,
|
||||
story_name: &str,
|
||||
reason: &str,
|
||||
) -> (String, String) {
|
||||
let number = extract_item_number(item_id).unwrap_or(item_id);
|
||||
let effective_name = story_name.filter(|n| !n.is_empty());
|
||||
let effective_name = if story_name.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(story_name)
|
||||
};
|
||||
let name_plain = effective_name.map(|n| format!("{n} ")).unwrap_or_default();
|
||||
let name_html = effective_name
|
||||
.map(|n| format!("<em>{n}</em> "))
|
||||
@@ -102,11 +114,15 @@ pub fn format_blocked_notification(
|
||||
/// Returns `(plain_text, html)` suitable for `ChatTransport::send_message`.
|
||||
pub fn format_rate_limit_notification(
|
||||
item_id: &str,
|
||||
story_name: Option<&str>,
|
||||
story_name: &str,
|
||||
agent_name: &str,
|
||||
) -> (String, String) {
|
||||
let number = extract_item_number(item_id).unwrap_or(item_id);
|
||||
let effective_name = story_name.filter(|n| !n.is_empty());
|
||||
let effective_name = if story_name.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(story_name)
|
||||
};
|
||||
let name_plain = effective_name.map(|n| format!("{n} ")).unwrap_or_default();
|
||||
let name_html = effective_name
|
||||
.map(|n| format!("<em>{n}</em> "))
|
||||
@@ -149,11 +165,15 @@ pub fn format_oauth_accounts_exhausted(earliest_reset_msg: &str) -> (String, Str
|
||||
/// Returns `(plain_text, html)` suitable for `ChatTransport::send_message`.
|
||||
pub fn format_agent_started_notification(
|
||||
item_id: &str,
|
||||
story_name: Option<&str>,
|
||||
story_name: &str,
|
||||
agent_name: &str,
|
||||
) -> (String, String) {
|
||||
let number = extract_item_number(item_id).unwrap_or(item_id);
|
||||
let effective_name = story_name.filter(|n| !n.is_empty());
|
||||
let effective_name = if story_name.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(story_name)
|
||||
};
|
||||
let name_plain = effective_name.map(|n| format!("{n} ")).unwrap_or_default();
|
||||
let name_html = effective_name
|
||||
.map(|n| format!("<em>{n}</em> "))
|
||||
@@ -171,12 +191,16 @@ pub fn format_agent_started_notification(
|
||||
/// Returns `(plain_text, html)` suitable for `ChatTransport::send_message`.
|
||||
pub fn format_agent_completed_notification(
|
||||
item_id: &str,
|
||||
story_name: Option<&str>,
|
||||
story_name: &str,
|
||||
agent_name: &str,
|
||||
success: bool,
|
||||
) -> (String, String) {
|
||||
let number = extract_item_number(item_id).unwrap_or(item_id);
|
||||
let effective_name = story_name.filter(|n| !n.is_empty());
|
||||
let effective_name = if story_name.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(story_name)
|
||||
};
|
||||
let name_plain = effective_name.map(|n| format!("{n} ")).unwrap_or_default();
|
||||
let name_html = effective_name
|
||||
.map(|n| format!("<em>{n}</em> "))
|
||||
@@ -243,7 +267,7 @@ mod tests {
|
||||
fn format_notification_done_stage_includes_party_emoji() {
|
||||
let (plain, html) = format_stage_notification(
|
||||
"353_story_done",
|
||||
Some("Done Story"),
|
||||
"Done Story",
|
||||
&merge_stage(),
|
||||
&done_stage(),
|
||||
);
|
||||
@@ -261,7 +285,7 @@ mod tests {
|
||||
fn format_notification_non_done_stage_has_no_emoji() {
|
||||
let (plain, _html) = format_stage_notification(
|
||||
"42_story_thing",
|
||||
Some("Some Story"),
|
||||
"Some Story",
|
||||
&Stage::Backlog,
|
||||
&Stage::Coding,
|
||||
);
|
||||
@@ -272,7 +296,7 @@ mod tests {
|
||||
fn format_notification_with_story_name() {
|
||||
let (plain, html) = format_stage_notification(
|
||||
"261_story_bot_notifications",
|
||||
Some("Bot notifications"),
|
||||
"Bot notifications",
|
||||
&Stage::Upcoming,
|
||||
&Stage::Coding,
|
||||
);
|
||||
@@ -289,19 +313,15 @@ mod tests {
|
||||
#[test]
|
||||
fn format_stage_notification_without_story_name_falls_back_to_number() {
|
||||
let (plain, html) =
|
||||
format_stage_notification("42_bug_fix_thing", None, &Stage::Coding, &Stage::Qa);
|
||||
format_stage_notification("42_bug_fix_thing", "", &Stage::Coding, &Stage::Qa);
|
||||
assert_eq!(plain, "#42 \u{2014} Current \u{2192} QA");
|
||||
assert_eq!(html, "<strong>#42</strong> \u{2014} Current \u{2192} QA");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_notification_non_numeric_id_uses_full_id() {
|
||||
let (plain, _html) = format_stage_notification(
|
||||
"abc_story_thing",
|
||||
Some("Some Story"),
|
||||
&Stage::Qa,
|
||||
&merge_stage(),
|
||||
);
|
||||
let (plain, _html) =
|
||||
format_stage_notification("abc_story_thing", "Some Story", &Stage::Qa, &merge_stage());
|
||||
assert_eq!(
|
||||
plain,
|
||||
"#abc_story_thing Some Story \u{2014} QA \u{2192} Merge"
|
||||
@@ -312,14 +332,14 @@ mod tests {
|
||||
fn format_stage_notification_long_name_is_preserved() {
|
||||
let long_name = "A".repeat(300);
|
||||
let (plain, _html) =
|
||||
format_stage_notification("1_story_long", Some(&long_name), &Stage::Coding, &Stage::Qa);
|
||||
format_stage_notification("1_story_long", &long_name, &Stage::Coding, &Stage::Qa);
|
||||
assert!(plain.contains(&long_name));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_stage_notification_empty_story_name_falls_back_to_number() {
|
||||
let (plain, html) =
|
||||
format_stage_notification("42_story_empty", Some(""), &Stage::Coding, &Stage::Qa);
|
||||
format_stage_notification("42_story_empty", "", &Stage::Coding, &Stage::Qa);
|
||||
assert_eq!(plain, "#42 \u{2014} Current \u{2192} QA");
|
||||
assert_eq!(html, "<strong>#42</strong> \u{2014} Current \u{2192} QA");
|
||||
}
|
||||
@@ -328,7 +348,7 @@ mod tests {
|
||||
fn format_stage_notification_unicode_name() {
|
||||
let (plain, html) = format_stage_notification(
|
||||
"7_story_i18n",
|
||||
Some("Ünïcödé Ñämé 🎉"),
|
||||
"Ünïcödé Ñämé 🎉",
|
||||
&Stage::Qa,
|
||||
&merge_stage(),
|
||||
);
|
||||
@@ -342,7 +362,7 @@ mod tests {
|
||||
fn format_error_notification_with_story_name() {
|
||||
let (plain, html) = format_error_notification(
|
||||
"262_story_bot_errors",
|
||||
Some("Bot error notifications"),
|
||||
"Bot error notifications",
|
||||
"merge conflict in src/main.rs",
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -357,7 +377,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
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", "", "tests failed");
|
||||
assert_eq!(plain, "\u{274c} #42 \u{2014} tests failed");
|
||||
assert_eq!(html, "\u{274c} <strong>#42</strong> \u{2014} tests failed");
|
||||
}
|
||||
@@ -365,7 +385,7 @@ mod tests {
|
||||
#[test]
|
||||
fn format_error_notification_non_numeric_id_uses_full_id() {
|
||||
let (plain, _html) =
|
||||
format_error_notification("abc_story_thing", Some("Some Story"), "clippy errors");
|
||||
format_error_notification("abc_story_thing", "Some Story", "clippy errors");
|
||||
assert_eq!(
|
||||
plain,
|
||||
"\u{274c} #abc_story_thing Some Story \u{2014} clippy errors"
|
||||
@@ -375,21 +395,19 @@ mod tests {
|
||||
#[test]
|
||||
fn format_error_notification_long_reason_preserved() {
|
||||
let long_reason = "x".repeat(500);
|
||||
let (plain, _html) = format_error_notification("1_story_foo", None, &long_reason);
|
||||
let (plain, _html) = format_error_notification("1_story_foo", "", &long_reason);
|
||||
assert!(plain.contains(&long_reason));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_error_notification_unicode_reason() {
|
||||
let (plain, _html) =
|
||||
format_error_notification("5_story_foo", Some("Foo"), "错误:合并冲突");
|
||||
let (plain, _html) = format_error_notification("5_story_foo", "Foo", "错误:合并冲突");
|
||||
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");
|
||||
let (plain, _html) = format_error_notification("42_bug_fix_thing", "", "tests failed");
|
||||
assert_eq!(plain, "\u{274c} #42 \u{2014} tests failed");
|
||||
}
|
||||
|
||||
@@ -399,7 +417,7 @@ mod tests {
|
||||
fn format_blocked_notification_with_story_name() {
|
||||
let (plain, html) = format_blocked_notification(
|
||||
"425_story_blocking_reason",
|
||||
Some("Blocking Reason Story"),
|
||||
"Blocking Reason Story",
|
||||
"Retry limit exceeded (3/3) at coder stage",
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -414,7 +432,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
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", "", "empty diff");
|
||||
assert_eq!(plain, "\u{1f6ab} #42 \u{2014} BLOCKED: empty diff");
|
||||
assert_eq!(
|
||||
html,
|
||||
@@ -424,13 +442,13 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn format_blocked_notification_empty_story_name_falls_back_to_number() {
|
||||
let (plain, _html) = format_blocked_notification("42_story_thing", Some(""), "empty diff");
|
||||
let (plain, _html) = format_blocked_notification("42_story_thing", "", "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"), "理由:空の差分");
|
||||
let (plain, _html) = format_blocked_notification("3_story_x", "X", "理由:空の差分");
|
||||
assert!(plain.contains("BLOCKED: 理由:空の差分"));
|
||||
}
|
||||
|
||||
@@ -439,7 +457,7 @@ mod tests {
|
||||
#[test]
|
||||
fn format_rate_limit_notification_includes_agent_and_story() {
|
||||
let (plain, html) =
|
||||
format_rate_limit_notification("365_story_my_feature", Some("My Feature"), "coder-2");
|
||||
format_rate_limit_notification("365_story_my_feature", "My Feature", "coder-2");
|
||||
assert_eq!(
|
||||
plain,
|
||||
"\u{26a0}\u{fe0f} #365 My Feature \u{2014} coder-2 hit an API rate limit"
|
||||
@@ -452,7 +470,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
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", "", "coder-1");
|
||||
assert_eq!(
|
||||
plain,
|
||||
"\u{26a0}\u{fe0f} #42 \u{2014} coder-1 hit an API rate limit"
|
||||
@@ -465,7 +483,7 @@ mod tests {
|
||||
|
||||
#[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");
|
||||
let (plain, _html) = format_rate_limit_notification("42_story_thing", "", "coder-1");
|
||||
assert_eq!(
|
||||
plain,
|
||||
"\u{26a0}\u{fe0f} #42 \u{2014} coder-1 hit an API rate limit"
|
||||
@@ -474,7 +492,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn format_rate_limit_notification_unicode_agent_name() {
|
||||
let (plain, _html) = format_rate_limit_notification("9_story_foo", Some("Foo"), "агент-1");
|
||||
let (plain, _html) = format_rate_limit_notification("9_story_foo", "Foo", "агент-1");
|
||||
assert!(plain.contains("агент-1"));
|
||||
assert!(plain.contains("hit an API rate limit"));
|
||||
}
|
||||
@@ -484,7 +502,7 @@ mod tests {
|
||||
#[test]
|
||||
fn format_agent_started_notification_with_story_name() {
|
||||
let (plain, html) =
|
||||
format_agent_started_notification("42_story_foo", Some("My Feature"), "coder-1");
|
||||
format_agent_started_notification("42_story_foo", "My Feature", "coder-1");
|
||||
assert_eq!(plain, "\u{1F916} #42 My Feature \u{2014} coder-1 started");
|
||||
assert_eq!(
|
||||
html,
|
||||
@@ -494,7 +512,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn format_agent_started_notification_falls_back_to_number() {
|
||||
let (plain, html) = format_agent_started_notification("42_story_foo", None, "coder-1");
|
||||
let (plain, html) = format_agent_started_notification("42_story_foo", "", "coder-1");
|
||||
assert_eq!(plain, "\u{1F916} #42 \u{2014} coder-1 started");
|
||||
assert_eq!(
|
||||
html,
|
||||
@@ -504,7 +522,7 @@ mod tests {
|
||||
|
||||
#[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");
|
||||
let (plain, _html) = format_agent_started_notification("42_story_foo", "", "coder-1");
|
||||
assert_eq!(plain, "\u{1F916} #42 \u{2014} coder-1 started");
|
||||
}
|
||||
|
||||
@@ -512,12 +530,8 @@ mod tests {
|
||||
|
||||
#[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,
|
||||
);
|
||||
let (plain, html) =
|
||||
format_agent_completed_notification("42_story_foo", "My Feature", "coder-1", true);
|
||||
assert_eq!(plain, "\u{2705} #42 My Feature \u{2014} coder-1 completed");
|
||||
assert_eq!(
|
||||
html,
|
||||
@@ -527,19 +541,15 @@ mod tests {
|
||||
|
||||
#[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,
|
||||
);
|
||||
let (plain, _html) =
|
||||
format_agent_completed_notification("42_story_foo", "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);
|
||||
format_agent_completed_notification("42_story_foo", "", "coder-1", true);
|
||||
assert_eq!(plain, "\u{2705} #42 \u{2014} coder-1 completed");
|
||||
assert_eq!(
|
||||
html,
|
||||
@@ -550,7 +560,7 @@ mod tests {
|
||||
#[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);
|
||||
format_agent_completed_notification("42_story_foo", "", "coder-1", false);
|
||||
assert_eq!(plain, "\u{274C} #42 \u{2014} coder-1 failed");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,8 +52,7 @@ pub fn spawn_notification_listener(
|
||||
// Rapid successive transitions for the same item are coalesced: the
|
||||
// original `from_stage` is kept while `to_stage` is updated to the
|
||||
// latest destination, so only one notification fires for the final stage.
|
||||
let mut pending_transitions: HashMap<String, (Stage, Stage, Option<String>)> =
|
||||
HashMap::new();
|
||||
let mut pending_transitions: HashMap<String, (Stage, Stage, String)> = HashMap::new();
|
||||
let mut flush_deadline: Option<tokio::time::Instant> = None;
|
||||
|
||||
// Pending agent-status notifications, keyed by "{story_id}:{event_kind}".
|
||||
@@ -88,7 +87,7 @@ pub fn spawn_notification_listener(
|
||||
{
|
||||
let (plain, html) = format_stage_notification(
|
||||
&item_id,
|
||||
story_name.as_deref(),
|
||||
&story_name,
|
||||
&from_stage,
|
||||
&to_stage,
|
||||
);
|
||||
@@ -139,7 +138,7 @@ pub fn spawn_notification_listener(
|
||||
{
|
||||
let (plain, html) = format_stage_notification(
|
||||
&item_id,
|
||||
story_name.as_deref(),
|
||||
&story_name,
|
||||
&from_stage,
|
||||
&to_stage,
|
||||
);
|
||||
@@ -191,8 +190,14 @@ pub fn spawn_notification_listener(
|
||||
|
||||
// Look up the story name in the expected stage directory; fall
|
||||
// back to a full search so stale events still show the name.
|
||||
let story_name = read_story_name(&project_root, stage, item_id)
|
||||
.or_else(|| find_story_name_any_stage(&project_root, item_id));
|
||||
let story_name = {
|
||||
let n = read_story_name(&project_root, stage, item_id);
|
||||
if n.is_empty() {
|
||||
find_story_name_any_stage(&project_root, item_id)
|
||||
} else {
|
||||
n
|
||||
}
|
||||
};
|
||||
|
||||
// Buffer the transition. If this item_id is already pending (rapid
|
||||
// succession), update the destination stage to the latest while
|
||||
@@ -201,7 +206,7 @@ pub fn spawn_notification_listener(
|
||||
.entry(item_id.clone())
|
||||
.and_modify(|e| {
|
||||
e.1 = to_typed.clone();
|
||||
if story_name.is_some() {
|
||||
if !story_name.is_empty() {
|
||||
e.2 = story_name.clone();
|
||||
}
|
||||
})
|
||||
@@ -225,8 +230,7 @@ pub fn spawn_notification_listener(
|
||||
// AC3: include only the first non-empty line of the failure,
|
||||
// truncated to ~120 chars.
|
||||
let snippet = merge_failure_snippet(reason, 120);
|
||||
let (plain, html) =
|
||||
format_error_notification(story_id, story_name.as_deref(), &snippet);
|
||||
let (plain, html) = format_error_notification(story_id, &story_name, &snippet);
|
||||
slog!("[bot] Sending error notification: {plain}");
|
||||
for room_id in &rooms_for_notification(&get_room_ids) {
|
||||
if let Err(e) = transport.send_message(room_id, &plain, &html).await {
|
||||
@@ -267,7 +271,7 @@ pub fn spawn_notification_listener(
|
||||
rate_limit_last_notified.insert(debounce_key, now);
|
||||
let story_name = find_story_name_any_stage(&project_root, story_id);
|
||||
let (plain, html) =
|
||||
format_rate_limit_notification(story_id, story_name.as_deref(), agent_name);
|
||||
format_rate_limit_notification(story_id, &story_name, agent_name);
|
||||
slog!("[bot] Sending rate-limit notification: {plain}");
|
||||
for room_id in &rooms_for_notification(&get_room_ids) {
|
||||
if let Err(e) = transport.send_message(room_id, &plain, &html).await {
|
||||
@@ -290,8 +294,7 @@ pub fn spawn_notification_listener(
|
||||
continue;
|
||||
};
|
||||
let story_name = find_story_name_any_stage(&project_root, story_id);
|
||||
let (plain, html) =
|
||||
format_blocked_notification(story_id, story_name.as_deref(), reason);
|
||||
let (plain, html) = format_blocked_notification(story_id, &story_name, reason);
|
||||
slog!("[bot] Sending blocked notification: {plain}");
|
||||
for room_id in &rooms_for_notification(&get_room_ids) {
|
||||
if let Err(e) = transport.send_message(room_id, &plain, &html).await {
|
||||
@@ -350,11 +353,8 @@ pub fn spawn_notification_listener(
|
||||
continue;
|
||||
};
|
||||
let story_name = find_story_name_any_stage(&project_root, story_id);
|
||||
let (plain, html) = format_agent_started_notification(
|
||||
story_id,
|
||||
story_name.as_deref(),
|
||||
agent_name,
|
||||
);
|
||||
let (plain, html) =
|
||||
format_agent_started_notification(story_id, &story_name, agent_name);
|
||||
// Buffer with 5s debounce; later arrivals overwrite earlier ones.
|
||||
let key = format!("{story_id}:started");
|
||||
pending_agent_events.insert(key, (plain, html));
|
||||
@@ -375,7 +375,7 @@ pub fn spawn_notification_listener(
|
||||
let story_name = find_story_name_any_stage(&project_root, story_id);
|
||||
let (plain, html) = format_agent_completed_notification(
|
||||
story_id,
|
||||
story_name.as_deref(),
|
||||
&story_name,
|
||||
agent_name,
|
||||
success,
|
||||
);
|
||||
|
||||
@@ -16,16 +16,20 @@ mod tests_notifications;
|
||||
#[cfg(test)]
|
||||
mod tests_stage;
|
||||
|
||||
/// Read the story name from the typed CRDT register (story 929).
|
||||
/// Read the story name from the typed CRDT register.
|
||||
///
|
||||
/// Returns `None` if the item is not in the CRDT or has no name set.
|
||||
pub fn read_story_name(_project_root: &Path, _stage: &str, item_id: &str) -> Option<String> {
|
||||
crate::crdt_state::read_item(item_id).map(|v| v.name().to_string())
|
||||
/// Returns the name as a `String`, or an empty string if the item is not in
|
||||
/// the CRDT or has no name set. Callers that display the name unconditionally
|
||||
/// should pass this directly to the format_* notification helpers.
|
||||
pub fn read_story_name(_project_root: &Path, _stage: &str, item_id: &str) -> String {
|
||||
crate::crdt_state::read_item(item_id)
|
||||
.map(|v| v.name().to_string())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Look up a story name from the CRDT content store regardless of stage.
|
||||
///
|
||||
/// Used for events (like rate-limit warnings) that arrive without a known stage.
|
||||
fn find_story_name_any_stage(project_root: &Path, item_id: &str) -> Option<String> {
|
||||
fn find_story_name_any_stage(project_root: &Path, item_id: &str) -> String {
|
||||
read_story_name(project_root, "", item_id)
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ fn read_story_name_reads_from_front_matter() {
|
||||
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let name = read_story_name(tmp.path(), "2_current", "9942_story_my_feature");
|
||||
assert_eq!(name.as_deref(), Some("My Cool Feature"));
|
||||
assert_eq!(name, "My Cool Feature");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -124,7 +124,7 @@ fn read_story_name_returns_none_for_missing_file() {
|
||||
crate::db::ensure_content_store();
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let name = read_story_name(tmp.path(), "2_current", "99_story_missing_notif_test");
|
||||
assert_eq!(name, None);
|
||||
assert!(name.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -139,7 +139,7 @@ fn read_story_name_returns_none_for_missing_name_field() {
|
||||
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let name = read_story_name(tmp.path(), "2_current", "9943_story_no_name");
|
||||
assert_eq!(name, None);
|
||||
assert!(name.is_empty());
|
||||
}
|
||||
|
||||
// ── Bug 549: synthetic events with from_stage=None must not notify ───────────
|
||||
|
||||
Reference in New Issue
Block a user