Files
huskies/server/src/service/notifications/events.rs
T

142 lines
4.9 KiB
Rust
Raw Normal View History

//! Event-to-notification mapping.
//!
//! Pure functions that classify [`WatcherEvent`] variants into notification
//! actions, deciding which events produce user-visible messages and which
//! are suppressed or logged server-side only.
use crate::io::watcher::WatcherEvent;
/// The notification action to take in response to a [`WatcherEvent`].
#[derive(Debug, PartialEq)]
pub enum EventAction {
/// Post a stage-transition notification; the event carries a known source stage.
StageTransition,
/// Post a merge-failure error notification.
MergeFailure,
/// Post a rate-limit warning (subject to config/debounce suppression).
RateLimitWarning,
/// Post a story-blocked notification.
StoryBlocked,
/// Post an OAuth account-swap notification naming the new account.
OAuthAccountSwapped,
/// Post an OAuth accounts-exhausted notification with the earliest reset time.
OAuthAccountsExhausted,
/// Log server-side only; do not post to chat (e.g. hard rate-limit blocks).
LogOnly,
/// Reload the project configuration.
ReloadConfig,
/// Skip silently (synthetic events, unknown variants).
Skip,
}
/// Classify a [`WatcherEvent`] into the action the notification listener should take.
pub fn classify(event: &WatcherEvent) -> EventAction {
match event {
WatcherEvent::WorkItem { from_stage, .. } => {
if from_stage.is_some() {
EventAction::StageTransition
} else {
// Synthetic events (creation, reassign) have no from_stage.
// Posting a notification for these would produce incorrect messages.
EventAction::Skip
}
}
WatcherEvent::MergeFailure { .. } => EventAction::MergeFailure,
WatcherEvent::RateLimitWarning { .. } => EventAction::RateLimitWarning,
WatcherEvent::StoryBlocked { .. } => EventAction::StoryBlocked,
WatcherEvent::RateLimitHardBlock { .. } => EventAction::LogOnly,
WatcherEvent::ConfigChanged => EventAction::ReloadConfig,
WatcherEvent::OAuthAccountSwapped { .. } => EventAction::OAuthAccountSwapped,
WatcherEvent::OAuthAccountsExhausted { .. } => EventAction::OAuthAccountsExhausted,
_ => EventAction::Skip,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn work_item(from_stage: Option<&str>) -> WatcherEvent {
WatcherEvent::WorkItem {
stage: "3_qa".to_string(),
item_id: "1_story_foo".to_string(),
action: "qa".to_string(),
commit_msg: String::new(),
from_stage: from_stage.map(str::to_string),
}
}
#[test]
fn work_item_with_from_stage_is_stage_transition() {
let event = work_item(Some("2_current"));
assert_eq!(classify(&event), EventAction::StageTransition);
}
#[test]
fn work_item_without_from_stage_is_skip() {
let event = work_item(None);
assert_eq!(classify(&event), EventAction::Skip);
}
#[test]
fn merge_failure_is_classified_correctly() {
let event = WatcherEvent::MergeFailure {
story_id: "1_story_foo".to_string(),
reason: "conflict".to_string(),
};
assert_eq!(classify(&event), EventAction::MergeFailure);
}
#[test]
fn rate_limit_warning_is_classified_correctly() {
let event = WatcherEvent::RateLimitWarning {
story_id: "1_story_foo".to_string(),
agent_name: "coder-1".to_string(),
};
assert_eq!(classify(&event), EventAction::RateLimitWarning);
}
#[test]
fn story_blocked_is_classified_correctly() {
let event = WatcherEvent::StoryBlocked {
story_id: "1_story_foo".to_string(),
reason: "empty diff".to_string(),
};
assert_eq!(classify(&event), EventAction::StoryBlocked);
}
#[test]
fn rate_limit_hard_block_is_log_only() {
let event = WatcherEvent::RateLimitHardBlock {
story_id: "1_story_foo".to_string(),
agent_name: "coder-1".to_string(),
reset_at: chrono::Utc::now(),
};
assert_eq!(classify(&event), EventAction::LogOnly);
}
#[test]
fn config_changed_triggers_reload() {
assert_eq!(
classify(&WatcherEvent::ConfigChanged),
EventAction::ReloadConfig
);
}
#[test]
fn oauth_account_swapped_is_classified_correctly() {
let event = WatcherEvent::OAuthAccountSwapped {
new_email: "new@example.com".to_string(),
};
assert_eq!(classify(&event), EventAction::OAuthAccountSwapped);
}
#[test]
fn oauth_accounts_exhausted_is_classified_correctly() {
let event = WatcherEvent::OAuthAccountsExhausted {
earliest_reset_msg: "All accounts rate-limited; earliest reset in 2h".to_string(),
};
assert_eq!(classify(&event), EventAction::OAuthAccountsExhausted);
}
}