1
0

test_mentions.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. """Tests des mentions hors du salon dédié : une rafale ne lance pas un appel par message.
  2. Lancer depuis la racine du projet : python -m unittest"""
  3. import asyncio
  4. import contextlib
  5. import unittest
  6. from unittest import mock
  7. from bot.discord_bot import ChatBot
  8. CHANNEL_ID = 20
  9. def fake_bot() -> mock.Mock:
  10. bot = mock.Mock()
  11. bot._mentions = {}
  12. bot._waiters = {}
  13. bot.available.return_value = True
  14. lock = asyncio.Lock()
  15. bot.history.lock.return_value = lock
  16. bot.answer_mentions = lambda channel, late=None: ChatBot.answer_mentions(bot, channel, late)
  17. bot.answer_mention = lambda mentions, late=None: ChatBot.answer_mention(bot, mentions, late)
  18. bot.mention_part = lambda message: ChatBot.mention_part(bot, message)
  19. bot.settings.context_message_limit = 20
  20. bot.settings.pdf_max_pages = 100
  21. bot.presence_note.return_value = None
  22. bot.timestamp = lambda message: "03/11/2026 18:00"
  23. bot.read_reply = mock.AsyncMock(return_value=(None, []))
  24. bot.member_notes = mock.AsyncMock(return_value="")
  25. bot.claude.ask = mock.AsyncMock(return_value=mock.Mock(text="Coucou"))
  26. bot.send_answer = mock.AsyncMock()
  27. bot.safe_reply = mock.AsyncMock()
  28. return bot
  29. def fake_mention(message_id: int, text: str, channel: mock.Mock) -> mock.Mock:
  30. message = mock.Mock(id=message_id, clean_content=f"@Rika {text}", attachments=[], stickers=[])
  31. message.channel = channel
  32. message.guild.me.display_name = "Rika"
  33. message.author.display_name = message.author.name = f"membre{message_id}"
  34. return message
  35. def fake_channel(history: list) -> mock.Mock:
  36. channel = mock.Mock(id=CHANNEL_ID)
  37. channel.name = "général"
  38. async def read(limit, before):
  39. for msg in reversed([m for m in history if before is None or m.id < before.id][-limit:]):
  40. yield msg
  41. channel.history = read
  42. return channel
  43. @mock.patch("bot.discord_bot.typing", lambda channel: contextlib.nullcontext())
  44. @mock.patch("bot.discord_bot.emoji_names", lambda guild: [])
  45. class MentionTest(unittest.IsolatedAsyncioTestCase):
  46. async def test_burst_grouped(self):
  47. # 10 mentions en rafale : la première part seule, les 9 autres attendent puis partent ensemble.
  48. bot = fake_bot()
  49. calls, release = [], asyncio.Event()
  50. async def answer(mentions, late=None):
  51. calls.append([m.id for m in mentions])
  52. await release.wait()
  53. bot.answer_mention = answer
  54. channel = mock.Mock(id=CHANNEL_ID)
  55. tasks = [asyncio.create_task(ChatBot.handle_mention(bot, mock.Mock(id=1, channel=channel)))]
  56. await asyncio.sleep(0)
  57. tasks += [asyncio.create_task(ChatBot.handle_mention(bot, mock.Mock(id=i, channel=channel)))
  58. for i in range(2, 11)]
  59. await asyncio.sleep(0)
  60. release.set()
  61. await asyncio.gather(*tasks)
  62. self.assertEqual(calls, [[1], list(range(2, 11))])
  63. self.assertEqual(bot._mentions, {})
  64. async def test_one_call_for_grouped_mentions(self):
  65. bot = fake_bot()
  66. history = []
  67. channel = fake_channel(history)
  68. history += [mock.Mock(id=1, clean_content="salut tout le monde", attachments=[], stickers=[]),
  69. fake_mention(2, "ça va ?", channel),
  70. mock.Mock(id=3, clean_content="il dort", attachments=[], stickers=[]),
  71. fake_mention(4, "réveille-toi", channel)]
  72. for msg in history:
  73. msg.author.display_name = msg.author.name = f"membre{msg.id}"
  74. bot._mentions[CHANNEL_ID] = [history[3], history[1]]
  75. await ChatBot.answer_mentions(bot, channel)
  76. bot.claude.ask.assert_awaited_once()
  77. parts = bot.claude.ask.await_args.args[1]
  78. self.assertEqual(len(parts), 2) # une partie par mention, dans l'ordre
  79. context, second = parts[0][0], parts[1][0]
  80. self.assertIn("salut tout le monde", context)
  81. self.assertIn("il dort", context)
  82. self.assertNotIn("membre2 : ", context) # la mention n'est pas recopiée dans le contexte
  83. self.assertTrue(context.endswith("membre2 te mentionne et écrit : ça va ?"))
  84. self.assertEqual(second, "03/11/2026 18:00, membre4 te mentionne et écrit : réveille-toi")
  85. bot.member_notes.assert_awaited_once_with([history[1], history[3]])
  86. bot.send_answer.assert_awaited_once_with(history[3], "Coucou", reply=False)
  87. async def test_grouped_answer_sees_previous_answer(self):
  88. # La réponse à la 1re mention est postée après les mentions regroupées : elle doit être dans le contexte.
  89. bot = fake_bot()
  90. history = []
  91. channel = fake_channel(history)
  92. history += [fake_mention(1, "tu fais quoi ce soir ?", channel), fake_mention(2, "?", channel),
  93. fake_mention(3, "réponds !", channel),
  94. mock.Mock(id=4, clean_content="Je regarde un film !", attachments=[], stickers=[])]
  95. history[3].author.display_name = history[3].author.name = "Rika"
  96. await ChatBot.answer_mention(bot, history[1:3])
  97. self.assertIn("Rika : Je regarde un film !", bot.claude.ask.await_args.args[1][0][0])
  98. async def test_late_context_stops_at_last_mention(self):
  99. # En retard, la discussion qui a suivi les mentions ne remplace pas celle qui les entoure.
  100. bot = fake_bot()
  101. history = []
  102. channel = fake_channel(history)
  103. history += [mock.Mock(id=1, clean_content="avant", attachments=[], stickers=[]),
  104. fake_mention(2, "tu es là ?", channel),
  105. mock.Mock(id=3, clean_content="après", attachments=[], stickers=[])]
  106. for msg in history:
  107. msg.author.display_name = msg.author.name = f"membre{msg.id}"
  108. bot.presence.late_note.return_value = "(en retard)"
  109. await ChatBot.answer_mention(bot, [history[1]], late=mock.Mock())
  110. context = bot.claude.ask.await_args.args[1][0][0]
  111. self.assertIn("avant", context)
  112. self.assertNotIn("après", context)
  113. async def test_refused_attachment_drops_only_that_mention(self):
  114. bot = fake_bot()
  115. channel = fake_channel([])
  116. first, second = fake_mention(1, "regarde", channel), fake_mention(2, "et ça ?", channel)
  117. bot.mention_part = mock.AsyncMock(side_effect=[None, ("texte", [])])
  118. await ChatBot.answer_mention(bot, [first, second])
  119. self.assertEqual(bot.claude.ask.await_args.args[1], [("texte", [])])
  120. bot.member_notes.assert_awaited_once_with([second])
  121. async def test_all_refused_no_call(self):
  122. bot = fake_bot()
  123. channel = fake_channel([])
  124. bot.mention_part = mock.AsyncMock(return_value=None)
  125. await ChatBot.answer_mention(bot, [fake_mention(1, "regarde", channel)])
  126. bot.claude.ask.assert_not_awaited()
  127. bot.send_answer.assert_not_awaited()
  128. if __name__ == "__main__":
  129. unittest.main()