mirror of
https://github.com/Sea-Haven-Industries/afterhours-shift-manager.git
synced 2026-09-30 11:23:12 +00:00
180 lines
6.2 KiB
Python
180 lines
6.2 KiB
Python
"""Tests for the activity bump that keeps the schedule post at the bottom of
|
|
the channel (slack-bot handle_channel_message)."""
|
|
|
|
import time
|
|
|
|
from freezegun import freeze_time
|
|
|
|
CHANNEL = "C_TEST"
|
|
MON = "2026-06-08 12:00:00"
|
|
|
|
|
|
class _SlackError(Exception):
|
|
"""Stand-in for slack_sdk.errors.SlackApiError with a `.response` dict."""
|
|
|
|
def __init__(self, code):
|
|
super().__init__(code)
|
|
self.response = {"error": code}
|
|
|
|
|
|
def _msg(channel=CHANNEL, user="U_ALICE", ts="100.500", **extra):
|
|
return {"channel": channel, "user": user, "ts": ts, **extra}
|
|
|
|
|
|
@freeze_time(MON)
|
|
def test_bumps_on_new_message(slackbot_app, schedule, seed, client):
|
|
seed.schedule_post(CHANNEL, "111.111")
|
|
client.chat_postMessage.return_value = {"ts": "222.222"}
|
|
|
|
slackbot_app.handle_channel_message(_msg(), client, schedule, CHANNEL)
|
|
|
|
client.chat_delete.assert_called_once()
|
|
assert client.chat_delete.call_args.kwargs["ts"] == "111.111"
|
|
client.chat_postMessage.assert_called_once()
|
|
# The stored ts now points at the freshly reposted message + debounce stamp.
|
|
post = schedule.get_schedule_post(CHANNEL)
|
|
assert post["message_ts"] == "222.222"
|
|
assert post.get("last_bump_ts") is not None
|
|
|
|
|
|
@freeze_time(MON)
|
|
def test_debounce_skips_within_window(slackbot_app, schedule, client):
|
|
schedule.save_schedule_post(
|
|
CHANNEL, "111.111", "2026-06-08", last_bump_ts=time.time()
|
|
)
|
|
|
|
slackbot_app.handle_channel_message(_msg(), client, schedule, CHANNEL)
|
|
|
|
client.chat_delete.assert_not_called()
|
|
client.chat_postMessage.assert_not_called()
|
|
# Stored post is untouched.
|
|
assert schedule.get_schedule_post(CHANNEL)["message_ts"] == "111.111"
|
|
|
|
|
|
@freeze_time(MON)
|
|
def test_bumps_after_debounce_window(slackbot_app, schedule, client):
|
|
old = time.time() - (slackbot_app.SCHEDULE_BUMP_DEBOUNCE_SECONDS + 60)
|
|
schedule.save_schedule_post(CHANNEL, "111.111", "2026-06-08", last_bump_ts=old)
|
|
client.chat_postMessage.return_value = {"ts": "222.222"}
|
|
|
|
slackbot_app.handle_channel_message(_msg(), client, schedule, CHANNEL)
|
|
|
|
client.chat_postMessage.assert_called_once()
|
|
assert schedule.get_schedule_post(CHANNEL)["message_ts"] == "222.222"
|
|
|
|
|
|
@freeze_time(MON)
|
|
def test_skips_when_no_stored_post(slackbot_app, schedule, client):
|
|
slackbot_app.handle_channel_message(_msg(), client, schedule, CHANNEL)
|
|
|
|
client.chat_delete.assert_not_called()
|
|
client.chat_postMessage.assert_not_called()
|
|
|
|
|
|
@freeze_time(MON)
|
|
def test_skips_bots_own_message(slackbot_app, schedule, seed, client):
|
|
seed.schedule_post(CHANNEL, "111.111")
|
|
|
|
slackbot_app.handle_channel_message(_msg(bot_id="B123"), client, schedule, CHANNEL)
|
|
|
|
client.chat_delete.assert_not_called()
|
|
client.chat_postMessage.assert_not_called()
|
|
|
|
|
|
@freeze_time(MON)
|
|
def test_skips_message_edit_and_delete_subtypes(slackbot_app, schedule, seed, client):
|
|
seed.schedule_post(CHANNEL, "111.111")
|
|
|
|
slackbot_app.handle_channel_message(
|
|
_msg(subtype="message_changed"), client, schedule, CHANNEL
|
|
)
|
|
|
|
client.chat_delete.assert_not_called()
|
|
client.chat_postMessage.assert_not_called()
|
|
|
|
|
|
@freeze_time(MON)
|
|
def test_skips_other_channel(slackbot_app, schedule, seed, client):
|
|
seed.schedule_post(CHANNEL, "111.111")
|
|
|
|
slackbot_app.handle_channel_message(
|
|
_msg(channel="C_OTHER"), client, schedule, CHANNEL
|
|
)
|
|
|
|
client.chat_delete.assert_not_called()
|
|
client.chat_postMessage.assert_not_called()
|
|
|
|
|
|
@freeze_time(MON)
|
|
def test_retried_event_is_a_noop(slackbot_app, schedule, seed, client):
|
|
# A Slack retry (X-Slack-Retry-Num set) must never trigger a second bump.
|
|
seed.schedule_post(CHANNEL, "111.111")
|
|
|
|
slackbot_app.handle_channel_message(
|
|
_msg(), client, schedule, CHANNEL, retry_num="1"
|
|
)
|
|
|
|
client.chat_delete.assert_not_called()
|
|
client.chat_postMessage.assert_not_called()
|
|
# The record is left untouched (no optimistic debounce stamp either).
|
|
post = schedule.get_schedule_post(CHANNEL)
|
|
assert post["message_ts"] == "111.111"
|
|
assert post.get("last_bump_ts") is None
|
|
|
|
|
|
@freeze_time(MON)
|
|
def test_thread_reply_is_a_noop(slackbot_app, schedule, seed, client):
|
|
# A threaded reply doesn't push the schedule down the main timeline.
|
|
seed.schedule_post(CHANNEL, "111.111")
|
|
|
|
slackbot_app.handle_channel_message(
|
|
_msg(thread_ts="050.000"), client, schedule, CHANNEL
|
|
)
|
|
|
|
client.chat_delete.assert_not_called()
|
|
client.chat_postMessage.assert_not_called()
|
|
assert schedule.get_schedule_post(CHANNEL)["message_ts"] == "111.111"
|
|
|
|
|
|
@freeze_time(MON)
|
|
def test_stamps_debounce_before_repost(slackbot_app, schedule, seed, client):
|
|
# The debounce window is stamped optimistically before the delete/repost so
|
|
# a retry mid-bump is suppressed even if this run dies before the final save.
|
|
seed.schedule_post(CHANNEL, "111.111")
|
|
captured = {}
|
|
|
|
def _delete(**kwargs):
|
|
# Mid-bump snapshot: the record already carries last_bump_ts.
|
|
captured["post"] = schedule.get_schedule_post(CHANNEL)
|
|
|
|
client.chat_delete.side_effect = _delete
|
|
client.chat_postMessage.return_value = {"ts": "222.222"}
|
|
|
|
slackbot_app.handle_channel_message(_msg(), client, schedule, CHANNEL)
|
|
|
|
assert captured["post"].get("last_bump_ts") is not None
|
|
|
|
|
|
@freeze_time(MON)
|
|
def test_skips_when_bot_not_in_channel(slackbot_app, schedule, seed, client):
|
|
seed.schedule_post(CHANNEL, "111.111")
|
|
client.chat_delete.side_effect = _SlackError("not_in_channel")
|
|
|
|
slackbot_app.handle_channel_message(_msg(), client, schedule, CHANNEL)
|
|
|
|
# No repost, and the stored post is left intact.
|
|
client.chat_postMessage.assert_not_called()
|
|
assert schedule.get_schedule_post(CHANNEL)["message_ts"] == "111.111"
|
|
|
|
|
|
@freeze_time(MON)
|
|
def test_reposts_when_old_message_already_gone(slackbot_app, schedule, seed, client):
|
|
# A stale ts (deleted manually) → delete fails non-fatally; still reposts.
|
|
seed.schedule_post(CHANNEL, "111.111")
|
|
client.chat_delete.side_effect = _SlackError("message_not_found")
|
|
client.chat_postMessage.return_value = {"ts": "222.222"}
|
|
|
|
slackbot_app.handle_channel_message(_msg(), client, schedule, CHANNEL)
|
|
|
|
client.chat_postMessage.assert_called_once()
|
|
assert schedule.get_schedule_post(CHANNEL)["message_ts"] == "222.222"
|