thermograph/backend/tests/notifications/test_discord_bot.py
Emi Griffith a4be7066e5 Subtree-merge thermograph-backend (origin/main) into backend/
git-subtree-dir: backend
git-subtree-mainline: 6723fc0326
git-subtree-split: 83c2e05b96
2026-07-22 22:01:11 -07:00

125 lines
4.3 KiB
Python

"""Discord gateway bot: the message-handling logic that decides when to reply and
with what. No live gateway — we feed MESSAGE_CREATE payloads straight to the pure
handler and stub the shared /grade lookup so these stay data-independent."""
import asyncio
import pytest
from notifications import discord
from notifications import discord_bot as bot
from notifications import discord_interactions as di
BOT_ID = "999"
@pytest.fixture(autouse=True)
def _stub_grade(monkeypatch):
"""Reply payload for /grade, mirroring the real shape (embeds + an ephemeral
flag the gateway path must drop)."""
monkeypatch.setattr(
di, "_grade_message",
lambda query: {"embeds": [{"title": f"grade:{query}"}], "flags": 64})
def _msg(content="", *, author_id="1", bot_author=False, mentions=(), guild=True):
m = {
"content": content,
"id": "42",
"channel_id": "77",
"author": {"id": author_id, "bot": bot_author},
"mentions": [{"id": mid} for mid in mentions],
}
if guild:
m["guild_id"] = "5"
return m
# --- when the bot stays silent ----------------------------------------------
def test_ignores_other_bots():
assert bot._response_for_message(
_msg("<@999> Phoenix", author_id="8", bot_author=True, mentions=[BOT_ID]), BOT_ID) is None
def test_ignores_its_own_messages():
assert bot._response_for_message(
_msg("hello", author_id=BOT_ID, mentions=[BOT_ID]), BOT_ID) is None
def test_ignores_guild_message_without_a_mention():
assert bot._response_for_message(_msg("just chatting", mentions=[]), BOT_ID) is None
# --- when the bot replies ----------------------------------------------------
def test_mention_with_city_returns_grade_without_ephemeral_flag():
out = bot._response_for_message(_msg("<@999> Phoenix", mentions=[BOT_ID]), BOT_ID)
assert out == {"embeds": [{"title": "grade:Phoenix"}]} # flags dropped
assert "flags" not in out
def test_legacy_nickname_mention_is_stripped():
out = bot._response_for_message(_msg("<@!999> Tokyo", mentions=[BOT_ID]), BOT_ID)
assert out["embeds"][0]["title"] == "grade:Tokyo"
def test_dm_needs_no_mention():
out = bot._response_for_message(_msg("Berlin", guild=False, mentions=[]), BOT_ID)
assert out["embeds"][0]["title"] == "grade:Berlin"
def test_mention_with_no_query_gives_help():
out = bot._response_for_message(_msg("<@999>", mentions=[BOT_ID]), BOT_ID)
assert "content" in out and "mention me" in out["content"].lower()
# --- helpers -----------------------------------------------------------------
def test_strip_mentions_handles_both_forms():
assert bot._strip_mentions("<@999> <@!999> Sao Paulo", BOT_ID) == "Sao Paulo"
def test_is_dm_is_true_without_guild_id():
assert bot._is_dm(_msg("x", guild=False)) is True
assert bot._is_dm(_msg("x", guild=True)) is False
# --- _dispatch: same reply, off the event loop -------------------------------
def test_dispatch_delivers_the_same_reply_via_a_thread(monkeypatch):
"""_dispatch now runs _response_for_message through asyncio.to_thread (the
event-loop fix) -- confirm that still produces the exact same reply and gets
it to _send_reply, i.e. the offload didn't change behaviour."""
sent = {}
async def _fake_send_reply(channel_id, message_id, data):
sent["channel_id"] = channel_id
sent["message_id"] = message_id
sent["data"] = data
monkeypatch.setattr(bot, "_send_reply", _fake_send_reply)
sess = bot._Session()
sess.user_id = BOT_ID
msg = {"t": "MESSAGE_CREATE", "d": _msg("<@999> Phoenix", mentions=[BOT_ID])}
asyncio.run(bot._dispatch(msg, sess))
assert sent["channel_id"] == "77"
assert sent["message_id"] == "42"
assert sent["data"] == {"embeds": [{"title": "grade:Phoenix"}]}
# --- enabled() gating --------------------------------------------------------
def test_enabled_is_off_by_default(monkeypatch):
monkeypatch.delenv("THERMOGRAPH_DISCORD_BOT", raising=False)
monkeypatch.setattr(discord, "BOT_TOKEN", "a-token")
assert bot.enabled() is False
def test_enabled_needs_both_flag_and_token(monkeypatch):
monkeypatch.setenv("THERMOGRAPH_DISCORD_BOT", "1")
monkeypatch.setattr(discord, "BOT_TOKEN", "")
assert bot.enabled() is False
monkeypatch.setattr(discord, "BOT_TOKEN", "a-token")
assert bot.enabled() is True