Files
MoviePilot/tests/test_slash_command_interactions.py
jxxghp 85a7f98a94 fix(feishu): 修复群聊@回复发私聊及无目标通知报错 (#6262)
- 回复类消息携带 original_chat_id 时优先发回原会话,避免群聊@机器人的回复错误发送到私聊窗口
- update_or_post_message 编辑失败回退发新消息时保留原消息/会话上下文
- 无显式目标且未配置默认目标时,回退向最近互动过的会话广播发送(与企业微信策略一致)
- 仍无可用目标时报出含配置指引的明确错误(FEISHU_OPEN_ID/FEISHU_CHAT_ID)

Closes #6262
2026-08-10 18:18:48 +08:00

268 lines
9.4 KiB
Python

import unittest
from types import SimpleNamespace
from unittest.mock import MagicMock, patch
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.chain.message import MessageChain
from app.chain.site import SiteChain, site_interaction_manager
from app.chain.skills import skills_interaction_manager
from app.chain.subscribe import SubscribeChain, subscribe_interaction_manager
from app.schemas.types import MessageChannel
class TestSlashCommandInteractions(unittest.TestCase):
def tearDown(self):
skills_interaction_manager.clear()
site_interaction_manager.clear()
subscribe_interaction_manager.clear()
def test_message_routes_text_reply_to_latest_sites_interaction(self):
chain = MessageChain()
skills_interaction_manager.create_or_replace(
user_id="10001",
channel=MessageChannel.Wechat,
source="wechat-test",
username="tester",
)
site_interaction_manager.create_or_replace(
user_id="10001",
command="/sites",
channel=MessageChannel.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.SkillsChain.handle_text_interaction"
) as handle_skills:
chain.handle_message(
channel=MessageChannel.Wechat,
source="wechat-test",
userid="10001",
username="tester",
text="禁用 1",
)
handle_site.assert_called_once()
handle_skills.assert_not_called()
def test_message_routes_text_reply_to_latest_subscribes_interaction(self):
chain = MessageChain()
site_interaction_manager.create_or_replace(
user_id="10001",
command="/sites",
channel=MessageChannel.Wechat,
source="wechat-test",
username="tester",
)
subscribe_interaction_manager.create_or_replace(
user_id="10001",
command="/subscribes",
channel=MessageChannel.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=MessageChannel.Wechat,
source="wechat-test",
userid="10001",
username="tester",
text="搜索 all",
)
handle_subscribes.assert_called_once()
handle_sites.assert_not_called()
def test_callback_routes_to_sites_chain(self):
chain = MessageChain()
request = site_interaction_manager.create_or_replace(
user_id="10001",
command="/sites",
channel=MessageChannel.Telegram,
source="telegram-test",
username="tester",
)
with patch(
"app.chain.message.SiteChain.handle_callback_interaction",
return_value=True,
) as handle_callback:
chain._handle_callback(
text=f"CALLBACK:sites:{request.request_id}:refresh",
channel=MessageChannel.Telegram,
source="telegram-test",
userid="10001",
username="tester",
)
handle_callback.assert_called_once()
def test_callback_routes_to_subscribes_chain(self):
chain = MessageChain()
request = subscribe_interaction_manager.create_or_replace(
user_id="10001",
command="/subscribes",
channel=MessageChannel.Telegram,
source="telegram-test",
username="tester",
)
with patch(
"app.chain.message.SubscribeChain.handle_callback_interaction",
return_value=True,
) as handle_callback:
chain._handle_callback(
text=f"CALLBACK:subscribes:{request.request_id}:refresh",
channel=MessageChannel.Telegram,
source="telegram-test",
userid="10001",
username="tester",
)
handle_callback.assert_called_once()
def test_sites_text_exit_skips_notification_history(self):
chain = SiteChain()
site_interaction_manager.create_or_replace(
user_id="10001",
command="/sites",
channel=MessageChannel.Telegram,
source="telegram-test",
username="tester",
)
with patch.object(chain, "post_message") as post_message:
handled = chain.handle_text_interaction(
channel=MessageChannel.Telegram,
source="telegram-test",
userid="10001",
username="tester",
text="退出",
)
self.assertTrue(handled)
notification = post_message.call_args.args[0]
self.assertEqual(notification.title, "站点交互已结束")
self.assertFalse(notification.save_history)
self.assertIsNone(site_interaction_manager.get_by_user("10001"))
def test_subscribes_text_exit_skips_notification_history(self):
chain = SubscribeChain()
subscribe_interaction_manager.create_or_replace(
user_id="10001",
command="/subscribes",
channel=MessageChannel.Telegram,
source="telegram-test",
username="tester",
)
with patch.object(chain, "post_message") as post_message:
handled = chain.handle_text_interaction(
channel=MessageChannel.Telegram,
source="telegram-test",
userid="10001",
username="tester",
text="退出",
)
self.assertTrue(handled)
notification = post_message.call_args.args[0]
self.assertEqual(notification.title, "订阅交互已结束")
self.assertFalse(notification.save_history)
self.assertIsNone(subscribe_interaction_manager.get_by_user("10001"))
def test_sites_renders_markdown_table_when_channel_supports_markdown(self):
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/",
)
]
with patch("app.chain.site.SiteOper.list", return_value=fake_sites), patch.object(
chain, "post_message"
) as post_message:
chain.remote_list(channel=MessageChannel.Web, userid="u1", source="web")
notification = post_message.call_args[0][0]
self.assertIn("| ID | 站点 | 状态 | Cookie | 渲染 | 域名 |", notification.text)
self.assertIn("| 1 | M-Team | 启用 | 已配置 | 是 | m-team.io |", notification.text)
def test_subscribes_renders_markdown_table_when_channel_supports_markdown(self):
chain = SubscribeChain()
fake_subscribes = [
SimpleNamespace(
id=12,
name="Example Show",
type="电视剧",
year="2024",
season=1,
total_episode=10,
lack_episode=3,
state="R",
)
]
with patch(
"app.chain.subscribe.SubscribeOper.list", return_value=fake_subscribes
), patch.object(chain, "post_message") as post_message:
chain.remote_list(channel=MessageChannel.Web, userid="u1", source="web")
notification = post_message.call_args[0][0]
self.assertIn("| ID | 名称 | 类型 | 年份 | 季/进度 | 状态 |", notification.text)
self.assertIn(
"| 12 | Example Show | 电视剧 | 2024 | 第1季 [7/10] | 订阅中 |",
notification.text,
)
class TestUpdateOrPostMessage(unittest.TestCase):
def test_fallback_post_keeps_original_message_context(self):
"""编辑失败回退发新消息时,必须保留原消息/会话上下文,供渠道回复到原会话。"""
from app.helper.interaction import update_or_post_message
chain = SimpleNamespace(
edit_message=MagicMock(return_value=False),
post_message=MagicMock(),
)
update_or_post_message(
chain=chain,
channel=MessageChannel.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]
self.assertEqual(notification.original_message_id, "om_origin")
self.assertEqual(notification.original_chat_id, "oc_group")