huskies: merge 527_story_remove_rate_limit_hard_block_bot_notifications_from_matrix_chat
This commit is contained in:
@@ -141,30 +141,6 @@ const RATE_LIMIT_DEBOUNCE: Duration = Duration::from_secs(60);
|
|||||||
/// into a single notification (only the final stage is announced).
|
/// into a single notification (only the final stage is announced).
|
||||||
const STAGE_TRANSITION_DEBOUNCE: Duration = Duration::from_millis(200);
|
const STAGE_TRANSITION_DEBOUNCE: Duration = Duration::from_millis(200);
|
||||||
|
|
||||||
/// Format a rate limit hard block notification message with scheduled resume time.
|
|
||||||
///
|
|
||||||
/// Returns `(plain_text, html)` suitable for `ChatTransport::send_message`.
|
|
||||||
pub fn format_rate_limit_hard_block_notification(
|
|
||||||
item_id: &str,
|
|
||||||
story_name: Option<&str>,
|
|
||||||
agent_name: &str,
|
|
||||||
resume_at: chrono::DateTime<chrono::Utc>,
|
|
||||||
) -> (String, String) {
|
|
||||||
let number = extract_story_number(item_id).unwrap_or(item_id);
|
|
||||||
let name = story_name.unwrap_or(item_id);
|
|
||||||
let local_time = resume_at.with_timezone(&chrono::Local);
|
|
||||||
let resume_str = local_time.format("%Y-%m-%d %H:%M").to_string();
|
|
||||||
|
|
||||||
let plain = format!(
|
|
||||||
"\u{1f6d1} #{number} {name} \u{2014} {agent_name} hit a hard rate limit; \
|
|
||||||
will auto-resume at {resume_str}"
|
|
||||||
);
|
|
||||||
let html = format!(
|
|
||||||
"\u{1f6d1} <strong>#{number}</strong> <em>{name}</em> \u{2014} \
|
|
||||||
{agent_name} hit a hard rate limit; will auto-resume at {resume_str}"
|
|
||||||
);
|
|
||||||
(plain, html)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Format a rate limit warning notification message.
|
/// Format a rate limit warning notification message.
|
||||||
///
|
///
|
||||||
@@ -383,39 +359,13 @@ pub fn spawn_notification_listener(
|
|||||||
ref agent_name,
|
ref agent_name,
|
||||||
reset_at,
|
reset_at,
|
||||||
}) => {
|
}) => {
|
||||||
// Debounce: reuse the same key as RateLimitWarning so both
|
// Log server-side for debugging; do NOT post to Matrix.
|
||||||
// types are rate-limited together for the same agent.
|
// Hard-block auto-resume is normal operation — the status
|
||||||
let debounce_key = format!("{story_id}:{agent_name}");
|
// command already surfaces rate-limit state via emoji.
|
||||||
let now = Instant::now();
|
slog!(
|
||||||
if let Some(&last) = rate_limit_last_notified.get(&debounce_key)
|
"[bot] Rate-limit hard block for {story_id}/{agent_name}, \
|
||||||
&& now.duration_since(last) < RATE_LIMIT_DEBOUNCE
|
auto-resume at {reset_at}"
|
||||||
{
|
|
||||||
slog!(
|
|
||||||
"[bot] Rate-limit hard-block notification debounced for \
|
|
||||||
{story_id}:{agent_name}"
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
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_hard_block_notification(
|
|
||||||
story_id,
|
|
||||||
story_name.as_deref(),
|
|
||||||
agent_name,
|
|
||||||
reset_at,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
slog!("[bot] Sending rate-limit hard-block notification: {plain}");
|
|
||||||
|
|
||||||
for room_id in &get_room_ids() {
|
|
||||||
if let Err(e) = transport.send_message(room_id, &plain, &html).await {
|
|
||||||
slog!(
|
|
||||||
"[bot] Failed to send rate-limit hard-block notification \
|
|
||||||
to {room_id}: {e}"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Ok(WatcherEvent::ConfigChanged) => {
|
Ok(WatcherEvent::ConfigChanged) => {
|
||||||
// Hot-reload: pick up any changes to rate_limit_notifications.
|
// Hot-reload: pick up any changes to rate_limit_notifications.
|
||||||
@@ -1062,17 +1012,10 @@ mod tests {
|
|||||||
assert_eq!(calls.len(), 0, "RateLimitWarning should be suppressed when rate_limit_notifications = false");
|
assert_eq!(calls.len(), 0, "RateLimitWarning should be suppressed when rate_limit_notifications = false");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// AC3: RateLimitHardBlock is always sent regardless of rate_limit_notifications.
|
/// RateLimitHardBlock is never posted to Matrix — it is logged server-side only.
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn rate_limit_hard_block_always_sent_when_config_false() {
|
async fn rate_limit_hard_block_never_sent_to_matrix() {
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
let sk_dir = tmp.path().join(".huskies");
|
|
||||||
std::fs::create_dir_all(&sk_dir).unwrap();
|
|
||||||
std::fs::write(
|
|
||||||
sk_dir.join("project.toml"),
|
|
||||||
"rate_limit_notifications = false\n",
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let (watcher_tx, watcher_rx) = broadcast::channel::<WatcherEvent>(16);
|
let (watcher_tx, watcher_rx) = broadcast::channel::<WatcherEvent>(16);
|
||||||
let (transport, calls) = MockTransport::new();
|
let (transport, calls) = MockTransport::new();
|
||||||
@@ -1094,7 +1037,7 @@ mod tests {
|
|||||||
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
||||||
|
|
||||||
let calls = calls.lock().unwrap();
|
let calls = calls.lock().unwrap();
|
||||||
assert_eq!(calls.len(), 1, "RateLimitHardBlock should always be sent");
|
assert_eq!(calls.len(), 0, "RateLimitHardBlock must not post to Matrix");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// AC3: StoryBlocked is always sent regardless of rate_limit_notifications.
|
/// AC3: StoryBlocked is always sent regardless of rate_limit_notifications.
|
||||||
|
|||||||
Reference in New Issue
Block a user