Files
MoviePilot/tests/test_slash_command_interactions.py
DDSRem 2de976f6b3 Merge pull request #7050 from DDSRem-Dev/codex-comment-migration
docs: 统一全项目中文注释和结构化契约说明
2026-10-17 19:26:29 +08:00

330 lines
10 KiB
Python

"""
斜杠命令交互路由测试。
"""
# 可选运行时依赖必须在导入消息 Chain 前完成隔离。
# ruff: noqa: E402
from types import SimpleNamespace
from unittest.mock import MagicMock, patch
import pytest
from app.testing.bootstrap import ensure_optional_stub
ensure_optional_stub("qbittorrentapi", TorrentFilesList=list)
ensure_optional_stub("transmission_rpc", File=object)
ensure_optional_stub("psutil")
ensure_optional_stub("aioshutil")
ensure_optional_stub("pyquery", PyQuery=object)
from app.application.messaging.interaction import InteractionContext
from app.application.messaging.site import site_interaction_manager
from app.application.messaging.skill import skill_interaction_manager
from app.application.messaging.subscribe import subscribe_interaction_manager
from app.chain.message import MessageChain
from app.chain.site import SiteChain
from app.chain.subscribe.facade import SubscribeChain
from app.schemas.types import NotificationChannel
@pytest.fixture
def _clear_slash_interactions():
"""
隔离测试之间共享的会话与交互状态,并在结束时清理。
"""
try:
yield
finally:
skill_interaction_manager.clear()
site_interaction_manager.clear()
subscribe_interaction_manager.clear()
@pytest.mark.usefixtures("_clear_slash_interactions")
def test_message_routes_text_reply_to_latest_sites_interaction() -> None:
"""
文本回复优先路由到最新的站点交互会话。
"""
chain = MessageChain()
skill_interaction_manager.create_or_replace(
user_id="10001",
channel=NotificationChannel.Wechat,
source="wechat-test",
username="tester",
)
site_interaction_manager.create_or_replace(
user_id="10001",
command="/sites",
channel=NotificationChannel.Wechat,
source="wechat-test",
username="tester",
)
with (
patch.object(chain, "_record_user_message"),
patch(
"app.chain.message.SiteChain.handle_text_interaction",
return_value=True,
) as handle_site,
patch("app.chain.message.SkillInteractionHandler.handle_text_interaction") as handle_skills,
):
chain.handle_message(
channel=NotificationChannel.Wechat,
source="wechat-test",
userid="10001",
username="tester",
text="禁用 1",
)
handle_site.assert_called_once()
handle_skills.assert_not_called()
@pytest.mark.usefixtures("_clear_slash_interactions")
def test_message_routes_text_reply_to_latest_subscribes_interaction() -> None:
"""
文本回复优先路由到最新的订阅交互会话。
"""
chain = MessageChain()
site_interaction_manager.create_or_replace(
user_id="10001",
command="/sites",
channel=NotificationChannel.Wechat,
source="wechat-test",
username="tester",
)
subscribe_interaction_manager.create_or_replace(
user_id="10001",
command="/subscribes",
channel=NotificationChannel.Wechat,
source="wechat-test",
username="tester",
)
with (
patch.object(chain, "_record_user_message"),
patch(
"app.chain.message.SubscribeChain.handle_text_interaction",
return_value=True,
) as handle_subscribes,
patch("app.chain.message.SiteChain.handle_text_interaction") as handle_sites,
):
chain.handle_message(
channel=NotificationChannel.Wechat,
source="wechat-test",
userid="10001",
username="tester",
text="搜索 all",
)
handle_subscribes.assert_called_once()
handle_sites.assert_not_called()
@pytest.mark.usefixtures("_clear_slash_interactions")
def test_callback_routes_to_sites_chain() -> None:
"""
站点按钮回调路由至站点交互处理器。
"""
chain = MessageChain()
request = site_interaction_manager.create_or_replace(
user_id="10001",
command="/sites",
channel=NotificationChannel.Telegram,
source="telegram-test",
username="tester",
)
with patch(
"app.chain.message.SiteChain.handle_callback_interaction",
return_value=True,
) as handle_callback:
chain._handle_callback(
callback_data=f"sites:{request.request_id}:refresh",
context=InteractionContext(
channel=NotificationChannel.Telegram,
source="telegram-test",
user_id="10001",
username="tester",
),
)
handle_callback.assert_called_once()
@pytest.mark.usefixtures("_clear_slash_interactions")
def test_callback_routes_to_subscribes_chain() -> None:
"""
订阅按钮回调路由至订阅交互处理器。
"""
chain = MessageChain()
request = subscribe_interaction_manager.create_or_replace(
user_id="10001",
command="/subscribes",
channel=NotificationChannel.Telegram,
source="telegram-test",
username="tester",
)
with patch(
"app.chain.message.SubscribeChain.handle_callback_interaction",
return_value=True,
) as handle_callback:
chain._handle_callback(
callback_data=f"subscribes:{request.request_id}:refresh",
context=InteractionContext(
channel=NotificationChannel.Telegram,
source="telegram-test",
user_id="10001",
username="tester",
),
)
handle_callback.assert_called_once()
@pytest.mark.usefixtures("_clear_slash_interactions")
def test_sites_text_exit_skips_notification_history() -> None:
"""
退出站点交互的提示不写入通知历史。
"""
chain = SiteChain()
site_interaction_manager.create_or_replace(
user_id="10001",
command="/sites",
channel=NotificationChannel.Telegram,
source="telegram-test",
username="tester",
)
with patch.object(chain, "post_message") as post_message:
handled = chain.handle_text_interaction(
channel=NotificationChannel.Telegram,
source="telegram-test",
userid="10001",
username="tester",
text="退出",
)
assert handled
notification = post_message.call_args.args[0]
assert notification.title == "站点交互已结束"
assert not notification.save_history
assert site_interaction_manager.get_by_user("10001") is None
@pytest.mark.usefixtures("_clear_slash_interactions")
def test_subscribes_text_exit_skips_notification_history() -> None:
"""
退出订阅交互的提示不写入通知历史。
"""
chain = SubscribeChain()
subscribe_interaction_manager.create_or_replace(
user_id="10001",
command="/subscribes",
channel=NotificationChannel.Telegram,
source="telegram-test",
username="tester",
)
with patch.object(chain, "post_message") as post_message:
handled = chain.handle_text_interaction(
channel=NotificationChannel.Telegram,
source="telegram-test",
userid="10001",
username="tester",
text="退出",
)
assert handled
notification = post_message.call_args.args[0]
assert notification.title == "订阅交互已结束"
assert not notification.save_history
assert subscribe_interaction_manager.get_by_user("10001") is None
@pytest.mark.usefixtures("_clear_slash_interactions")
def test_sites_renders_markdown_table_when_channel_supports_markdown() -> None:
"""
渠道支持 Markdown 时用表格展示站点信息。
"""
chain = SiteChain()
fake_sites = [
SimpleNamespace(
id=1,
name="M-Team",
is_active=True,
cookie="cookie=value",
render=1,
domain="m-team.io",
url="https://m-team.io/",
)
]
chain.site_repository = SimpleNamespace(list=lambda: fake_sites)
with patch.object(chain, "post_message") as post_message:
chain.remote_list(channel=NotificationChannel.Web, userid="u1", source="web")
notification = post_message.call_args[0][0]
assert "| ID | 站点 | 状态 | Cookie | 渲染 | 域名 |" in notification.text
assert "| 1 | M-Team | 启用 | 已配置 | 是 | m-team.io |" in notification.text
@pytest.mark.usefixtures("_clear_slash_interactions")
def test_subscribes_renders_markdown_table_when_channel_supports_markdown() -> None:
"""
渠道支持 Markdown 时用表格展示订阅信息。
"""
chain = SubscribeChain()
fake_subscribes = [
SimpleNamespace(
id=12,
name="Example Show",
type="电视剧",
year="2024",
season=1,
total_episode=10,
lack_episode=3,
state="R",
)
]
subscribe_port = SimpleNamespace(list=lambda: fake_subscribes)
chain.subscription_repository = subscribe_port
with patch.object(chain, "post_message") as post_message:
chain.remote_list(channel=NotificationChannel.Web, userid="u1", source="web")
notification = post_message.call_args[0][0]
assert "| ID | 名称 | 类型 | 年份 | 季/进度 | 状态 |" in notification.text
assert "| 12 | Example Show | 电视剧 | 2024 | 第1季 [7/10] | 订阅中 |" in notification.text
def test_fallback_post_keeps_original_message_context() -> None:
"""
编辑失败回退发新消息时,必须保留原消息/会话上下文,供渠道回复到原会话。
"""
from app.application.messaging.interaction import update_or_post_message
chain = SimpleNamespace(
edit_message=MagicMock(return_value=False),
post_message=MagicMock(),
)
update_or_post_message(
chain=chain,
channel=NotificationChannel.Feishu,
source="feishu-main",
userid="ou_user",
username="tester",
title="标题",
text="正文",
original_message_id="om_origin",
original_chat_id="oc_group",
)
chain.post_message.assert_called_once()
notification = chain.post_message.call_args[0][0]
assert notification.original_message_id == "om_origin"
assert notification.original_chat_id == "oc_group"