huskies: merge 527_story_remove_rate_limit_hard_block_bot_notifications_from_matrix_chat

This commit is contained in:
dave
2026-04-10 11:23:19 +00:00
parent 7e5b9839e8
commit fe405e81c6
@@ -141,30 +141,6 @@ const RATE_LIMIT_DEBOUNCE: Duration = Duration::from_secs(60);
/// into a single notification (only the final stage is announced).
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.
///
@@ -383,39 +359,13 @@ pub fn spawn_notification_listener(
ref agent_name,
reset_at,
}) => {
// Debounce: reuse the same key as RateLimitWarning so both
// types are rate-limited together for the same agent.
let debounce_key = format!("{story_id}:{agent_name}");
let now = Instant::now();
if let Some(&last) = rate_limit_last_notified.get(&debounce_key)
&& now.duration_since(last) < RATE_LIMIT_DEBOUNCE
{
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,
// Log server-side for debugging; do NOT post to Matrix.
// Hard-block auto-resume is normal operation — the status
// command already surfaces rate-limit state via emoji.
slog!(
"[bot] Rate-limit hard block for {story_id}/{agent_name}, \
auto-resume at {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) => {
// 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");
}
/// AC3: RateLimitHardBlock is always sent regardless of rate_limit_notifications.
/// RateLimitHardBlock is never posted to Matrix — it is logged server-side only.
#[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 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 (transport, calls) = MockTransport::new();
@@ -1094,7 +1037,7 @@ mod tests {
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
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.