| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230 |
- from __future__ import annotations
- import asyncio
- from types import SimpleNamespace
- from unittest.mock import AsyncMock
- import pytest
- class FakeMessage:
- def __init__(self, chat_id: int, message_id: int, *, text: str = "") -> None:
- self.chat = SimpleNamespace(
- id=chat_id,
- type=SimpleNamespace(value="supergroup" if chat_id < 0 else "private"),
- )
- self.id = message_id
- self.text = text
- self.caption = None
- self.delete_calls: list[bool] = []
- async def delete(self, *, revoke: bool = True) -> None:
- self.delete_calls.append(revoke)
- class FakeClient:
- def __init__(self) -> None:
- self.next_message_id = 100
- async def send_message(
- self,
- chat_id: int,
- _text: str,
- *,
- reply_to_message_id: int | None = None,
- ) -> FakeMessage:
- self.next_message_id += 1
- return FakeMessage(chat_id, self.next_message_id)
- @pytest.fixture
- def cleanup(app_modules):
- module = app_modules.load("wbb.services.message_cleanup")
- module._scheduled_messages.clear()
- module._cleanup_tasks.clear()
- module._active_interactions.clear()
- yield module
- module._scheduled_messages.clear()
- module._cleanup_tasks.clear()
- module._active_interactions.clear()
- async def test_group_cleanup_revokes_once_for_everyone(cleanup, monkeypatch):
- real_sleep = asyncio.sleep
- async def immediate_sleep(_delay: int) -> None:
- return None
- monkeypatch.setattr(cleanup.asyncio, "sleep", immediate_sleep)
- message = FakeMessage(-1001, 10, text="/points")
- cleanup.schedule_group_cleanup(message)
- cleanup.schedule_group_cleanup(message)
- await real_sleep(0)
- await real_sleep(0)
- assert message.delete_calls == [True]
- assert (-1001, 10) not in cleanup._scheduled_messages
- async def test_private_messages_are_not_scheduled(cleanup):
- message = FakeMessage(1001, 11, text="/points")
- cleanup.schedule_group_cleanup(message)
- assert not cleanup.is_group_cleanup_scheduled(message)
- assert message.delete_calls == []
- async def test_cleanup_task_is_referenced_until_deletion_finishes(
- cleanup, monkeypatch
- ):
- real_sleep = asyncio.sleep
- release = asyncio.Event()
- async def blocked_sleep(_delay: int) -> None:
- await release.wait()
- monkeypatch.setattr(cleanup.asyncio, "sleep", blocked_sleep)
- message = FakeMessage(-1001, 17, text="签到")
- cleanup.schedule_group_cleanup(message)
- await real_sleep(0)
- assert len(cleanup._cleanup_tasks) == 1
- release.set()
- await real_sleep(0)
- await real_sleep(0)
- assert message.delete_calls == [True]
- assert cleanup._cleanup_tasks == set()
- async def test_command_context_tracks_only_responses_in_the_same_group(
- cleanup, monkeypatch
- ):
- scheduled: list[FakeMessage] = []
- monkeypatch.setattr(
- cleanup,
- "schedule_group_cleanup",
- lambda *messages, **_kwargs: scheduled.extend(messages),
- )
- client = FakeClient()
- cleanup.install_group_interaction_tracking(client)
- command = FakeMessage(-1001, 12, text="/points")
- assert cleanup.begin_group_message_interaction(command)
- group_response = await client.send_message(
- -1001, "积分余额", reply_to_message_id=command.id
- )
- await client.send_message(-1001, "抽奖主消息")
- await client.send_message(-1002, "其他群通知", reply_to_message_id=command.id)
- await client.send_message(1001, "私聊通知", reply_to_message_id=command.id)
- assert scheduled == [command, group_response]
- async def test_plain_message_clears_previous_command_context(cleanup, monkeypatch):
- scheduled: list[FakeMessage] = []
- monkeypatch.setattr(
- cleanup,
- "schedule_group_cleanup",
- lambda *messages, **_kwargs: scheduled.extend(messages),
- )
- client = FakeClient()
- cleanup.install_group_interaction_tracking(client)
- command = FakeMessage(-1001, 13, text="/points")
- plain_message = FakeMessage(-1001, 14, text="大家好")
- assert cleanup.begin_group_message_interaction(command)
- assert not cleanup.begin_group_message_interaction(plain_message)
- await client.send_message(
- -1001, "普通自动回复", reply_to_message_id=plain_message.id
- )
- assert scheduled == [command]
- async def test_checkin_button_is_treated_as_group_interaction(cleanup, monkeypatch):
- scheduled: list[FakeMessage] = []
- monkeypatch.setattr(
- cleanup,
- "schedule_group_cleanup",
- lambda *messages, **_kwargs: scheduled.extend(messages),
- )
- message = FakeMessage(-1001, 15, text="签到")
- assert cleanup.begin_group_message_interaction(message, menu_texts={"签到"})
- assert scheduled == [message]
- async def test_callback_only_continues_an_already_ephemeral_menu(
- cleanup, monkeypatch
- ):
- scheduled: list[FakeMessage] = []
- monkeypatch.setattr(
- cleanup,
- "schedule_group_cleanup",
- lambda *messages, **_kwargs: scheduled.extend(messages),
- )
- client = FakeClient()
- cleanup.install_group_interaction_tracking(client)
- persistent_menu = FakeMessage(-1001, 16)
- query = SimpleNamespace(message=persistent_menu)
- assert not cleanup.begin_group_callback_interaction(query)
- await client.send_message(
- -1001, "不应自动删除", reply_to_message_id=persistent_menu.id
- )
- cleanup._scheduled_messages.add((-1001, 16))
- assert cleanup.begin_group_callback_interaction(query)
- callback_response = await client.send_message(
- -1001, "临时菜单回复", reply_to_message_id=persistent_menu.id
- )
- assert scheduled == [callback_response]
- async def test_group_reply_explicitly_schedules_request_and_response(
- cleanup, monkeypatch
- ):
- scheduled: list[FakeMessage] = []
- monkeypatch.setattr(
- cleanup,
- "schedule_group_cleanup",
- lambda *messages, **_kwargs: scheduled.extend(messages),
- )
- request = FakeMessage(-1001, 18, text="/start")
- response = FakeMessage(-1001, 19, text="请私聊我查看详细功能。")
- request.reply = AsyncMock(return_value=response)
- result = await cleanup.reply_group_interaction(
- request,
- "请私聊我查看详细功能。",
- reply_markup=object(),
- )
- assert result is response
- request.reply.assert_awaited_once()
- assert scheduled == [request, response]
- async def test_chinese_history_command_tracks_reply_with_configured_ttl(
- cleanup, monkeypatch
- ):
- scheduled: list[tuple[FakeMessage, int]] = []
- monkeypatch.setattr(
- cleanup,
- "schedule_group_cleanup",
- lambda *messages, delay_seconds=10: scheduled.extend(
- (message, delay_seconds) for message in messages
- ),
- )
- client = FakeClient()
- cleanup.install_group_interaction_tracking(client)
- command = FakeMessage(-1001, 20, text="流水")
- assert cleanup.begin_group_message_interaction(command, delay_seconds=12)
- response = await client.send_message(
- -1001, "最近积分流水", reply_to_message_id=command.id
- )
- assert scheduled == [(command, 12), (response, 12)]
|