story-kit: merge 262_story_bot_error_notifications_for_story_failures
This commit is contained in:
@@ -1788,6 +1788,13 @@ fn tool_report_merge_failure(args: &Value, ctx: &AppContext) -> Result<String, S
|
|||||||
slog!("[mergemaster] Merge failure reported for '{story_id}': {reason}");
|
slog!("[mergemaster] Merge failure reported for '{story_id}': {reason}");
|
||||||
ctx.agents.set_merge_failure_reported(story_id);
|
ctx.agents.set_merge_failure_reported(story_id);
|
||||||
|
|
||||||
|
// Broadcast the failure so the Matrix notification listener can post an
|
||||||
|
// error message to configured rooms without coupling this tool to the bot.
|
||||||
|
let _ = ctx.watcher_tx.send(crate::io::watcher::WatcherEvent::MergeFailure {
|
||||||
|
story_id: story_id.to_string(),
|
||||||
|
reason: reason.to_string(),
|
||||||
|
});
|
||||||
|
|
||||||
// Persist the failure reason to the story file's front matter so it
|
// Persist the failure reason to the story file's front matter so it
|
||||||
// survives server restarts and is visible in the web UI.
|
// survives server restarts and is visible in the web UI.
|
||||||
if let Ok(project_root) = ctx.state.get_project_root() {
|
if let Ok(project_root) = ctx.state.get_project_root() {
|
||||||
|
|||||||
@@ -150,6 +150,9 @@ impl From<WatcherEvent> for Option<WsResponse> {
|
|||||||
}),
|
}),
|
||||||
WatcherEvent::ConfigChanged => Some(WsResponse::AgentConfigChanged),
|
WatcherEvent::ConfigChanged => Some(WsResponse::AgentConfigChanged),
|
||||||
WatcherEvent::AgentStateChanged => Some(WsResponse::AgentStateChanged),
|
WatcherEvent::AgentStateChanged => Some(WsResponse::AgentStateChanged),
|
||||||
|
// MergeFailure is handled by the Matrix notification listener only;
|
||||||
|
// no WebSocket message is needed for the frontend.
|
||||||
|
WatcherEvent::MergeFailure { .. } => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,14 @@ pub enum WatcherEvent {
|
|||||||
/// Triggers a pipeline state refresh so the frontend can update agent
|
/// Triggers a pipeline state refresh so the frontend can update agent
|
||||||
/// assignments without waiting for a filesystem event.
|
/// assignments without waiting for a filesystem event.
|
||||||
AgentStateChanged,
|
AgentStateChanged,
|
||||||
|
/// A story encountered a failure (e.g. merge failure).
|
||||||
|
/// Triggers an error notification to configured Matrix rooms.
|
||||||
|
MergeFailure {
|
||||||
|
/// Work item ID (e.g. `"42_story_my_feature"`).
|
||||||
|
story_id: String,
|
||||||
|
/// Human-readable description of the failure.
|
||||||
|
reason: String,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return `true` if `path` is the root-level `.story_kit/project.toml`, i.e.
|
/// Return `true` if `path` is the root-level `.story_kit/project.toml`, i.e.
|
||||||
|
|||||||
@@ -81,6 +81,24 @@ pub fn format_stage_notification(
|
|||||||
(plain, html)
|
(plain, html)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Format an error notification message for a story failure.
|
||||||
|
///
|
||||||
|
/// Returns `(plain_text, html)` suitable for `RoomMessageEventContent::text_html`.
|
||||||
|
pub fn format_error_notification(
|
||||||
|
item_id: &str,
|
||||||
|
story_name: Option<&str>,
|
||||||
|
reason: &str,
|
||||||
|
) -> (String, String) {
|
||||||
|
let number = extract_story_number(item_id).unwrap_or(item_id);
|
||||||
|
let name = story_name.unwrap_or(item_id);
|
||||||
|
|
||||||
|
let plain = format!("\u{274c} #{number} {name} \u{2014} {reason}");
|
||||||
|
let html = format!(
|
||||||
|
"\u{274c} <strong>#{number}</strong> <em>{name}</em> \u{2014} {reason}"
|
||||||
|
);
|
||||||
|
(plain, html)
|
||||||
|
}
|
||||||
|
|
||||||
/// Spawn a background task that listens for watcher events and posts
|
/// Spawn a background task that listens for watcher events and posts
|
||||||
/// stage-transition notifications to all configured Matrix rooms.
|
/// stage-transition notifications to all configured Matrix rooms.
|
||||||
pub fn spawn_notification_listener(
|
pub fn spawn_notification_listener(
|
||||||
@@ -126,6 +144,32 @@ pub fn spawn_notification_listener(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Ok(WatcherEvent::MergeFailure {
|
||||||
|
ref story_id,
|
||||||
|
ref reason,
|
||||||
|
}) => {
|
||||||
|
let story_name =
|
||||||
|
read_story_name(&project_root, "4_merge", story_id);
|
||||||
|
let (plain, html) = format_error_notification(
|
||||||
|
story_id,
|
||||||
|
story_name.as_deref(),
|
||||||
|
reason,
|
||||||
|
);
|
||||||
|
|
||||||
|
slog!("[matrix-bot] Sending error notification: {plain}");
|
||||||
|
|
||||||
|
for room_id in &room_ids {
|
||||||
|
if let Some(room) = client.get_room(room_id) {
|
||||||
|
let content =
|
||||||
|
RoomMessageEventContent::text_html(plain.clone(), html.clone());
|
||||||
|
if let Err(e) = room.send(content).await {
|
||||||
|
slog!(
|
||||||
|
"[matrix-bot] Failed to send error notification to {room_id}: {e}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Ok(_) => {} // Ignore non-work-item events
|
Ok(_) => {} // Ignore non-work-item events
|
||||||
Err(broadcast::error::RecvError::Lagged(n)) => {
|
Err(broadcast::error::RecvError::Lagged(n)) => {
|
||||||
slog!(
|
slog!(
|
||||||
@@ -246,6 +290,42 @@ mod tests {
|
|||||||
assert_eq!(name, None);
|
assert_eq!(name, None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── format_error_notification ────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn format_error_notification_with_story_name() {
|
||||||
|
let (plain, html) =
|
||||||
|
format_error_notification("262_story_bot_errors", Some("Bot error notifications"), "merge conflict in src/main.rs");
|
||||||
|
assert_eq!(
|
||||||
|
plain,
|
||||||
|
"\u{274c} #262 Bot error notifications \u{2014} merge conflict in src/main.rs"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
html,
|
||||||
|
"\u{274c} <strong>#262</strong> <em>Bot error notifications</em> \u{2014} merge conflict in src/main.rs"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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");
|
||||||
|
assert_eq!(
|
||||||
|
plain,
|
||||||
|
"\u{274c} #abc_story_thing Some Story \u{2014} clippy errors"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ── format_stage_notification ───────────────────────────────────────────
|
// ── format_stage_notification ───────────────────────────────────────────
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user