huskies: merge 1073
This commit is contained in:
@@ -38,6 +38,14 @@ pub(crate) fn tool_create_bug(args: &Value, ctx: &AppContext) -> Result<String,
|
|||||||
depends_on.as_deref(),
|
depends_on.as_deref(),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
let _ = ctx
|
||||||
|
.watcher_tx
|
||||||
|
.send(crate::io::watcher::WatcherEvent::NewItemCreated {
|
||||||
|
item_id: bug_id.clone(),
|
||||||
|
item_type: "bug".to_string(),
|
||||||
|
name: req.name.as_ref().to_string(),
|
||||||
|
});
|
||||||
|
|
||||||
Ok(format!("Created bug: {bug_id}"))
|
Ok(format!("Created bug: {bug_id}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,14 @@ pub(crate) fn tool_create_refactor(args: &Value, ctx: &AppContext) -> Result<Str
|
|||||||
depends_on.as_deref(),
|
depends_on.as_deref(),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
let _ = ctx
|
||||||
|
.watcher_tx
|
||||||
|
.send(crate::io::watcher::WatcherEvent::NewItemCreated {
|
||||||
|
item_id: refactor_id.clone(),
|
||||||
|
item_type: "refactor".to_string(),
|
||||||
|
name: req.name.as_ref().to_string(),
|
||||||
|
});
|
||||||
|
|
||||||
Ok(format!("Created refactor: {refactor_id}"))
|
Ok(format!("Created refactor: {refactor_id}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,14 @@ pub(crate) fn tool_create_spike(args: &Value, ctx: &AppContext) -> Result<String
|
|||||||
depends_on.as_deref(),
|
depends_on.as_deref(),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
let _ = ctx
|
||||||
|
.watcher_tx
|
||||||
|
.send(crate::io::watcher::WatcherEvent::NewItemCreated {
|
||||||
|
item_id: spike_id.clone(),
|
||||||
|
item_type: "spike".to_string(),
|
||||||
|
name: req.name.as_ref().to_string(),
|
||||||
|
});
|
||||||
|
|
||||||
Ok(format!("Created spike: {spike_id}"))
|
Ok(format!("Created spike: {spike_id}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,14 @@ pub(crate) fn tool_create_story(args: &Value, ctx: &AppContext) -> Result<String
|
|||||||
false,
|
false,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
let _ = ctx
|
||||||
|
.watcher_tx
|
||||||
|
.send(crate::io::watcher::WatcherEvent::NewItemCreated {
|
||||||
|
item_id: story_id.clone(),
|
||||||
|
item_type: "story".to_string(),
|
||||||
|
name: req.name.as_ref().to_string(),
|
||||||
|
});
|
||||||
|
|
||||||
// Bug 503: warn at creation time if any depends_on points at an already-archived story.
|
// Bug 503: warn at creation time if any depends_on points at an already-archived story.
|
||||||
let archived_deps: Vec<u32> = depends_on_ids
|
let archived_deps: Vec<u32> = depends_on_ids
|
||||||
.as_deref()
|
.as_deref()
|
||||||
|
|||||||
@@ -90,4 +90,14 @@ pub enum WatcherEvent {
|
|||||||
/// `true` if acceptance gates passed; `false` if they failed.
|
/// `true` if acceptance gates passed; `false` if they failed.
|
||||||
success: bool,
|
success: bool,
|
||||||
},
|
},
|
||||||
|
/// A new work item was successfully created and added to the backlog.
|
||||||
|
/// Triggers a creation notification to configured chat rooms.
|
||||||
|
NewItemCreated {
|
||||||
|
/// Work item ID (e.g. `"1075_refactor_split_stage_enum"`).
|
||||||
|
item_id: String,
|
||||||
|
/// Human-readable item type (`"story"`, `"bug"`, `"refactor"`, `"spike"`).
|
||||||
|
item_type: String,
|
||||||
|
/// Human-readable item name.
|
||||||
|
name: String,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ pub enum EventAction {
|
|||||||
/// `true` if acceptance gates passed.
|
/// `true` if acceptance gates passed.
|
||||||
success: bool,
|
success: bool,
|
||||||
},
|
},
|
||||||
|
/// Post a new-item-created notification.
|
||||||
|
NewItemCreated,
|
||||||
/// Log server-side only; do not post to chat (e.g. hard rate-limit blocks).
|
/// Log server-side only; do not post to chat (e.g. hard rate-limit blocks).
|
||||||
LogOnly,
|
LogOnly,
|
||||||
/// Reload the project configuration.
|
/// Reload the project configuration.
|
||||||
@@ -51,6 +53,7 @@ pub fn classify(event: &WatcherEvent) -> EventAction {
|
|||||||
WatcherEvent::AgentCompleted { success, .. } => {
|
WatcherEvent::AgentCompleted { success, .. } => {
|
||||||
EventAction::AgentCompleted { success: *success }
|
EventAction::AgentCompleted { success: *success }
|
||||||
}
|
}
|
||||||
|
WatcherEvent::NewItemCreated { .. } => EventAction::NewItemCreated,
|
||||||
_ => EventAction::Skip,
|
_ => EventAction::Skip,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -178,4 +181,14 @@ mod tests {
|
|||||||
EventAction::AgentCompleted { success: false }
|
EventAction::AgentCompleted { success: false }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn new_item_created_is_classified_correctly() {
|
||||||
|
let event = WatcherEvent::NewItemCreated {
|
||||||
|
item_id: "1075_refactor_split_stage".to_string(),
|
||||||
|
item_type: "refactor".to_string(),
|
||||||
|
name: "Split Stage enum".to_string(),
|
||||||
|
};
|
||||||
|
assert_eq!(classify(&event), EventAction::NewItemCreated);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -220,6 +220,26 @@ pub fn format_agent_completed_notification(
|
|||||||
(plain, html)
|
(plain, html)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Format a new-work-item creation notification.
|
||||||
|
///
|
||||||
|
/// Returns `(plain_text, html)` suitable for `ChatTransport::send_message`.
|
||||||
|
pub fn format_new_item_notification(
|
||||||
|
item_id: &str,
|
||||||
|
item_type: &str,
|
||||||
|
name: &str,
|
||||||
|
) -> (String, String) {
|
||||||
|
let number = extract_item_number(item_id).unwrap_or(item_id);
|
||||||
|
let emoji = match item_type {
|
||||||
|
"bug" => "\u{1f41b}", // 🐛
|
||||||
|
"refactor" => "\u{1f4dd}", // 📝
|
||||||
|
"spike" => "\u{1f52c}", // 🔬
|
||||||
|
_ => "\u{1f4d6}", // 📖 (story and unknown)
|
||||||
|
};
|
||||||
|
let plain = format!("{emoji} New {item_type} #{number} \u{2014} {name}");
|
||||||
|
let html = format!("{emoji} New {item_type} <strong>#{number}</strong> \u{2014} {name}");
|
||||||
|
(plain, html)
|
||||||
|
}
|
||||||
|
|
||||||
/// Extract the first non-empty line from a merge failure reason, truncated to `max_len` chars.
|
/// Extract the first non-empty line from a merge failure reason, truncated to `max_len` chars.
|
||||||
///
|
///
|
||||||
/// Used to produce a compact snippet for chat notifications.
|
/// Used to produce a compact snippet for chat notifications.
|
||||||
@@ -599,6 +619,67 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── format_new_item_notification ──────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn format_new_item_notification_story() {
|
||||||
|
let (plain, html) =
|
||||||
|
format_new_item_notification("42_story_my_feature", "story", "My Feature");
|
||||||
|
assert_eq!(plain, "\u{1f4d6} New story #42 \u{2014} My Feature");
|
||||||
|
assert_eq!(
|
||||||
|
html,
|
||||||
|
"\u{1f4d6} New story <strong>#42</strong> \u{2014} My Feature"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn format_new_item_notification_bug() {
|
||||||
|
let (plain, html) =
|
||||||
|
format_new_item_notification("99_bug_login_crash", "bug", "Login Crash");
|
||||||
|
assert_eq!(plain, "\u{1f41b} New bug #99 \u{2014} Login Crash");
|
||||||
|
assert_eq!(
|
||||||
|
html,
|
||||||
|
"\u{1f41b} New bug <strong>#99</strong> \u{2014} Login Crash"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn format_new_item_notification_refactor() {
|
||||||
|
let (plain, html) = format_new_item_notification(
|
||||||
|
"1075_refactor_split_stage",
|
||||||
|
"refactor",
|
||||||
|
"Split Stage enum into Pipeline + Status",
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
plain,
|
||||||
|
"\u{1f4dd} New refactor #1075 \u{2014} Split Stage enum into Pipeline + Status"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
html,
|
||||||
|
"\u{1f4dd} New refactor <strong>#1075</strong> \u{2014} Split Stage enum into Pipeline + Status"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn format_new_item_notification_spike() {
|
||||||
|
let (plain, html) =
|
||||||
|
format_new_item_notification("7_spike_encoder_comparison", "spike", "Compare Encoders");
|
||||||
|
assert_eq!(plain, "\u{1f52c} New spike #7 \u{2014} Compare Encoders");
|
||||||
|
assert_eq!(
|
||||||
|
html,
|
||||||
|
"\u{1f52c} New spike <strong>#7</strong> \u{2014} Compare Encoders"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn format_new_item_notification_non_numeric_id_uses_full_id() {
|
||||||
|
let (plain, _html) = format_new_item_notification("abc_story_thing", "story", "Some Story");
|
||||||
|
assert_eq!(
|
||||||
|
plain,
|
||||||
|
"\u{1f4d6} New story #abc_story_thing \u{2014} Some Story"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn format_agent_completed_notification_empty_name_falls_back_to_number() {
|
fn format_agent_completed_notification_empty_name_falls_back_to_number() {
|
||||||
let (plain, _html) =
|
let (plain, _html) =
|
||||||
|
|||||||
@@ -15,8 +15,9 @@ use super::super::events::classify;
|
|||||||
use super::super::filter::{AGENT_EVENT_DEBOUNCE, should_send_rate_limit};
|
use super::super::filter::{AGENT_EVENT_DEBOUNCE, should_send_rate_limit};
|
||||||
use super::super::format::{
|
use super::super::format::{
|
||||||
format_agent_completed_notification, format_agent_started_notification,
|
format_agent_completed_notification, format_agent_started_notification,
|
||||||
format_blocked_notification, format_error_notification, format_oauth_account_swapped,
|
format_blocked_notification, format_error_notification, format_new_item_notification,
|
||||||
format_oauth_accounts_exhausted, format_rate_limit_notification, merge_failure_snippet,
|
format_oauth_account_swapped, format_oauth_accounts_exhausted, format_rate_limit_notification,
|
||||||
|
merge_failure_snippet,
|
||||||
};
|
};
|
||||||
use super::super::route::rooms_for_notification;
|
use super::super::route::rooms_for_notification;
|
||||||
use super::{find_story_name_any_stage, read_story_name};
|
use super::{find_story_name_any_stage, read_story_name};
|
||||||
@@ -276,6 +277,26 @@ pub fn spawn_notification_listener(
|
|||||||
pending_agent_events.insert(key, (plain, html));
|
pending_agent_events.insert(key, (plain, html));
|
||||||
agent_flush_deadline = Some(tokio::time::Instant::now() + AGENT_EVENT_DEBOUNCE);
|
agent_flush_deadline = Some(tokio::time::Instant::now() + AGENT_EVENT_DEBOUNCE);
|
||||||
}
|
}
|
||||||
|
EventAction::NewItemCreated => {
|
||||||
|
if !config.status_push_enabled {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let WatcherEvent::NewItemCreated {
|
||||||
|
ref item_id,
|
||||||
|
ref item_type,
|
||||||
|
ref name,
|
||||||
|
} = event
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let (plain, html) = format_new_item_notification(item_id, item_type, name);
|
||||||
|
slog!("[bot] Sending new-item notification: {plain}");
|
||||||
|
for room_id in &rooms_for_notification(&get_room_ids) {
|
||||||
|
if let Err(e) = transport.send_message(room_id, &plain, &html).await {
|
||||||
|
slog!("[bot] Failed to send new-item notification to {room_id}: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
EventAction::LogOnly => {
|
EventAction::LogOnly => {
|
||||||
// Hard-block: log server-side for debugging; do NOT post to chat.
|
// Hard-block: log server-side for debugging; do NOT post to chat.
|
||||||
// Hard-block auto-resume is normal operation — the status command
|
// Hard-block auto-resume is normal operation — the status command
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ pub fn watcher_event_to_response(e: WatcherEvent) -> Option<WsResponse> {
|
|||||||
// Agent lifecycle events are forwarded to chat transports only; no WebSocket message.
|
// Agent lifecycle events are forwarded to chat transports only; no WebSocket message.
|
||||||
WatcherEvent::AgentStarted { .. } => None,
|
WatcherEvent::AgentStarted { .. } => None,
|
||||||
WatcherEvent::AgentCompleted { .. } => None,
|
WatcherEvent::AgentCompleted { .. } => None,
|
||||||
|
// Creation notifications are forwarded to chat transports only; no WebSocket message.
|
||||||
|
WatcherEvent::NewItemCreated { .. } => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user