mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-06 07:56:52 +08:00
feat: add plugin-scoped text input sessions (#6068)
This commit is contained in:
@@ -1,13 +1,15 @@
|
||||
from datetime import datetime, timedelta
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from app.chain.message import MediaInteractionChain, MessageChain
|
||||
from app.core.event import EventManager
|
||||
from app.core.context import Context, MediaInfo, TorrentInfo
|
||||
from app.core.meta import MetaBase
|
||||
from app.helper.interaction import media_interaction_manager
|
||||
from app.schemas import TransferDirectoryConf
|
||||
from app.schemas.types import MediaType, MessageChannel
|
||||
from app.helper.interaction import media_interaction_manager, plugin_input_interaction_manager
|
||||
from app.schemas import CommingMessage, TransferDirectoryConf
|
||||
from app.schemas.types import EventType, MediaType, MessageChannel
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
@@ -15,6 +17,7 @@ def clear_media_interactions():
|
||||
"""清理媒体交互状态,避免用例之间共享内存会话。"""
|
||||
yield
|
||||
media_interaction_manager.clear()
|
||||
plugin_input_interaction_manager.clear()
|
||||
|
||||
|
||||
def _build_meta(name: str) -> MetaBase:
|
||||
@@ -157,6 +160,795 @@ def test_message_routes_text_reply_to_media_interaction_before_ai():
|
||||
handle_ai.assert_not_called()
|
||||
|
||||
|
||||
def test_plugin_input_session_captures_plain_text_before_media_interaction():
|
||||
"""插件输入会话存在时,普通文本应派发给插件而不是媒体交互。"""
|
||||
chain = MessageChain()
|
||||
request = plugin_input_interaction_manager.create_or_replace(
|
||||
user_id="10001",
|
||||
plugin_id="demo_plugin",
|
||||
channel=MessageChannel.Wechat,
|
||||
source="wechat-test",
|
||||
username="tester",
|
||||
prompt_id="prompt-1",
|
||||
payload={"step": "name"},
|
||||
)
|
||||
media_interaction_manager.create_or_replace(
|
||||
user_id="10001",
|
||||
channel=MessageChannel.Wechat,
|
||||
source="wechat-test",
|
||||
username="tester",
|
||||
action="Search",
|
||||
keyword="星际穿越",
|
||||
title="星际穿越",
|
||||
meta=_build_meta("星际穿越"),
|
||||
items=[MediaInfo(title="星际穿越", year="2014")],
|
||||
)
|
||||
|
||||
with patch.object(chain, "_record_user_message"), patch(
|
||||
"app.chain.message.MediaInteractionChain.handle_text_interaction",
|
||||
return_value=True,
|
||||
) as handle_media, patch.object(chain.eventmanager, "send_event") as send_event:
|
||||
chain.handle_message(
|
||||
channel=MessageChannel.Wechat,
|
||||
source="wechat-test",
|
||||
userid="10001",
|
||||
username="tester",
|
||||
text="用户输入内容",
|
||||
)
|
||||
|
||||
handle_media.assert_not_called()
|
||||
send_event.assert_called_once_with(
|
||||
EventType.MessageAction,
|
||||
{
|
||||
"plugin_id": "demo_plugin",
|
||||
"__mp_target_plugin_id": "demo_plugin",
|
||||
"text": f"plugin_input|{request.request_id}",
|
||||
"input_text": "用户输入内容",
|
||||
"userid": "10001",
|
||||
"channel": MessageChannel.Wechat,
|
||||
"source": "wechat-test",
|
||||
"username": "tester",
|
||||
"chat_id": None,
|
||||
"prompt_id": "prompt-1",
|
||||
"input_session_id": request.request_id,
|
||||
"payload": {"step": "name"},
|
||||
},
|
||||
)
|
||||
assert plugin_input_interaction_manager.get_by_user("10001", MessageChannel.Wechat) is None
|
||||
|
||||
|
||||
def test_plugin_input_session_does_not_record_sensitive_text_history():
|
||||
"""插件输入命中时不应先写入普通用户消息历史。"""
|
||||
chain = MessageChain()
|
||||
plugin_input_interaction_manager.create_or_replace(
|
||||
user_id="10001",
|
||||
plugin_id="demo_plugin",
|
||||
channel=MessageChannel.Telegram,
|
||||
source="telegram-test",
|
||||
username="tester",
|
||||
)
|
||||
|
||||
with patch.object(chain, "_record_user_message") as record_message, patch.object(
|
||||
chain.eventmanager, "send_event"
|
||||
):
|
||||
chain.handle_message(
|
||||
channel=MessageChannel.Telegram,
|
||||
source="telegram-test",
|
||||
userid="10001",
|
||||
username="tester",
|
||||
text="secret-value",
|
||||
)
|
||||
|
||||
record_message.assert_not_called()
|
||||
|
||||
|
||||
def test_plugin_input_session_captures_slash_like_text_before_commands():
|
||||
"""插件输入会话中的 /path 文本不应被当成 slash 命令。"""
|
||||
chain = MessageChain()
|
||||
request = plugin_input_interaction_manager.create_or_replace(
|
||||
user_id="10001",
|
||||
plugin_id="demo_plugin",
|
||||
channel=MessageChannel.Telegram,
|
||||
source="telegram-test",
|
||||
username="tester",
|
||||
prompt_id="path",
|
||||
)
|
||||
|
||||
with patch.object(chain, "_record_user_message"), patch.object(
|
||||
chain.eventmanager, "send_event"
|
||||
) as send_event:
|
||||
chain.handle_message(
|
||||
channel=MessageChannel.Telegram,
|
||||
source="telegram-test",
|
||||
userid="10001",
|
||||
username="tester",
|
||||
text="/downloads/tv",
|
||||
)
|
||||
|
||||
send_event.assert_called_once()
|
||||
event_type, payload = send_event.call_args.args
|
||||
assert event_type == EventType.MessageAction
|
||||
assert payload["text"] == f"plugin_input|{request.request_id}"
|
||||
assert payload["input_text"] == "/downloads/tv"
|
||||
assert payload["__mp_target_plugin_id"] == "demo_plugin"
|
||||
|
||||
|
||||
def test_plugin_input_session_cancel_notifies_plugin_and_clears():
|
||||
"""取消词应清理插件输入会话并通知目标插件。"""
|
||||
chain = MessageChain()
|
||||
plugin_input_interaction_manager.create_or_replace(
|
||||
user_id="10001",
|
||||
plugin_id="demo_plugin",
|
||||
channel=MessageChannel.Telegram,
|
||||
source="telegram-test",
|
||||
username="tester",
|
||||
)
|
||||
|
||||
with patch.object(chain, "_record_user_message"), patch.object(
|
||||
chain.eventmanager, "send_event"
|
||||
) as send_event, patch.object(chain, "post_message") as post_message:
|
||||
chain.handle_message(
|
||||
channel=MessageChannel.Telegram,
|
||||
source="telegram-test",
|
||||
userid="10001",
|
||||
username="tester",
|
||||
text="取消",
|
||||
)
|
||||
|
||||
send_event.assert_called_once()
|
||||
event_type, payload = send_event.call_args.args
|
||||
assert event_type == EventType.MessageAction
|
||||
assert payload["cancelled"] is True
|
||||
assert payload["__mp_target_plugin_id"] == "demo_plugin"
|
||||
post_message.assert_called_once()
|
||||
assert plugin_input_interaction_manager.get_by_user("10001", MessageChannel.Telegram) is None
|
||||
|
||||
|
||||
def test_plugin_input_cancel_does_not_block_next_command():
|
||||
"""取消输入后下一条 slash 命令应按正常命令路由处理。"""
|
||||
chain = MessageChain()
|
||||
plugin_input_interaction_manager.create_or_replace(
|
||||
user_id="10001",
|
||||
plugin_id="demo_plugin",
|
||||
channel=MessageChannel.Telegram,
|
||||
source="telegram-test",
|
||||
username="tester",
|
||||
)
|
||||
|
||||
with patch.object(chain, "_record_user_message"), patch.object(
|
||||
chain.eventmanager, "send_event"
|
||||
) as send_event, patch.object(chain, "post_message"):
|
||||
chain.handle_message(
|
||||
channel=MessageChannel.Telegram,
|
||||
source="telegram-test",
|
||||
userid="10001",
|
||||
username="tester",
|
||||
text="取消",
|
||||
)
|
||||
chain.handle_message(
|
||||
channel=MessageChannel.Telegram,
|
||||
source="telegram-test",
|
||||
userid="10001",
|
||||
username="tester",
|
||||
text="/tvh",
|
||||
)
|
||||
|
||||
assert send_event.call_args_list[0].args[0] == EventType.MessageAction
|
||||
assert send_event.call_args_list[1].args[0] == EventType.CommandExcute
|
||||
assert send_event.call_args_list[1].args[1]["cmd"] == "/tvh"
|
||||
|
||||
|
||||
def test_plugin_input_session_ignores_non_text_messages():
|
||||
"""图片/文件等非纯文本消息不应消费待输入会话。"""
|
||||
chain = MessageChain()
|
||||
request = plugin_input_interaction_manager.create_or_replace(
|
||||
user_id="10001",
|
||||
plugin_id="demo_plugin",
|
||||
channel=MessageChannel.Telegram,
|
||||
source="telegram-test",
|
||||
username="tester",
|
||||
)
|
||||
image = CommingMessage.MessageImage(ref="https://example.invalid/image.jpg")
|
||||
|
||||
with patch.object(chain, "_record_user_message"), patch.object(
|
||||
chain.eventmanager, "send_event"
|
||||
) as send_event:
|
||||
chain.handle_message(
|
||||
channel=MessageChannel.Telegram,
|
||||
source="telegram-test",
|
||||
userid="10001",
|
||||
username="tester",
|
||||
text="图片说明",
|
||||
images=[image],
|
||||
)
|
||||
|
||||
assert plugin_input_interaction_manager.get_by_user(
|
||||
"10001", MessageChannel.Telegram, "telegram-test"
|
||||
) == request
|
||||
assert not any(
|
||||
call.args and call.args[0] == EventType.MessageAction
|
||||
for call in send_event.call_args_list
|
||||
)
|
||||
|
||||
|
||||
def test_plugin_input_session_ignores_none_text_messages():
|
||||
"""文本为空时不应因 CALLBACK 检查崩溃或消费待输入会话。"""
|
||||
chain = MessageChain()
|
||||
request = plugin_input_interaction_manager.create_or_replace(
|
||||
user_id="10001",
|
||||
plugin_id="demo_plugin",
|
||||
channel=MessageChannel.Telegram,
|
||||
source="telegram-test",
|
||||
username="tester",
|
||||
)
|
||||
image = CommingMessage.MessageImage(ref="https://example.invalid/image.jpg")
|
||||
|
||||
handled = chain._handle_plugin_input_interaction(
|
||||
channel=MessageChannel.Telegram,
|
||||
source="telegram-test",
|
||||
userid="10001",
|
||||
username="tester",
|
||||
text=None,
|
||||
images=[image],
|
||||
)
|
||||
|
||||
assert handled is False
|
||||
assert plugin_input_interaction_manager.get_by_user(
|
||||
"10001", MessageChannel.Telegram, "telegram-test"
|
||||
) == request
|
||||
|
||||
|
||||
def test_plugin_input_session_is_bound_to_user_and_channel():
|
||||
"""同一用户不同渠道的插件输入会话互不匹配。"""
|
||||
request = plugin_input_interaction_manager.create_or_replace(
|
||||
user_id="10001",
|
||||
plugin_id="demo_plugin",
|
||||
channel=MessageChannel.Telegram,
|
||||
source="telegram-test",
|
||||
username="tester",
|
||||
)
|
||||
|
||||
assert plugin_input_interaction_manager.get_by_user("10001", MessageChannel.Wechat) is None
|
||||
assert plugin_input_interaction_manager.get_by_user(
|
||||
"10001", MessageChannel.Telegram, "telegram-test"
|
||||
) == request
|
||||
assert plugin_input_interaction_manager.get_by_user("10002", MessageChannel.Telegram) is None
|
||||
|
||||
|
||||
def test_plugin_input_session_does_not_capture_other_channel_text():
|
||||
"""生产消费路径也必须保持用户+渠道绑定。"""
|
||||
chain = MessageChain()
|
||||
request = plugin_input_interaction_manager.create_or_replace(
|
||||
user_id="10001",
|
||||
plugin_id="demo_plugin",
|
||||
channel=MessageChannel.Telegram,
|
||||
source="telegram-test",
|
||||
username="tester",
|
||||
)
|
||||
|
||||
with patch.object(chain, "_record_user_message"), patch.object(
|
||||
chain.eventmanager, "send_event"
|
||||
) as send_event:
|
||||
chain.handle_message(
|
||||
channel=MessageChannel.Wechat,
|
||||
source="wechat-test",
|
||||
userid="10001",
|
||||
username="tester",
|
||||
text="普通搜索",
|
||||
)
|
||||
|
||||
assert plugin_input_interaction_manager.get_by_user(
|
||||
"10001", MessageChannel.Telegram, "telegram-test"
|
||||
) == request
|
||||
assert not any(
|
||||
call.args and call.args[0] == EventType.MessageAction
|
||||
for call in send_event.call_args_list
|
||||
)
|
||||
|
||||
|
||||
def test_plugin_input_session_does_not_capture_other_source_text():
|
||||
"""同一渠道不同来源的插件输入会话互不匹配。"""
|
||||
chain = MessageChain()
|
||||
request = plugin_input_interaction_manager.create_or_replace(
|
||||
user_id="10001",
|
||||
plugin_id="demo_plugin",
|
||||
channel=MessageChannel.Telegram,
|
||||
source="telegram-bot-a",
|
||||
username="tester",
|
||||
)
|
||||
|
||||
with patch.object(chain, "_record_user_message"), patch.object(
|
||||
chain.eventmanager, "send_event"
|
||||
) as send_event:
|
||||
chain.handle_message(
|
||||
channel=MessageChannel.Telegram,
|
||||
source="telegram-bot-b",
|
||||
userid="10001",
|
||||
username="tester",
|
||||
text="普通搜索",
|
||||
)
|
||||
|
||||
assert plugin_input_interaction_manager.get_by_user(
|
||||
"10001", MessageChannel.Telegram, "telegram-bot-a"
|
||||
) == request
|
||||
assert not any(
|
||||
call.args and call.args[0] == EventType.MessageAction
|
||||
for call in send_event.call_args_list
|
||||
)
|
||||
|
||||
|
||||
def test_plugin_input_session_does_not_capture_other_chat_text():
|
||||
"""同一用户同一 bot 的不同 chat 不应串扰插件输入会话。"""
|
||||
chain = MessageChain()
|
||||
request = plugin_input_interaction_manager.create_or_replace(
|
||||
user_id="10001",
|
||||
plugin_id="demo_plugin",
|
||||
channel=MessageChannel.Telegram,
|
||||
source="telegram-test",
|
||||
username="tester",
|
||||
chat_id="chat-a",
|
||||
)
|
||||
|
||||
with patch.object(chain, "_record_user_message"), patch.object(
|
||||
chain.eventmanager, "send_event"
|
||||
) as send_event:
|
||||
chain.handle_message(
|
||||
channel=MessageChannel.Telegram,
|
||||
source="telegram-test",
|
||||
userid="10001",
|
||||
username="tester",
|
||||
text="普通搜索",
|
||||
original_chat_id="chat-b",
|
||||
)
|
||||
|
||||
assert plugin_input_interaction_manager.get_by_user(
|
||||
"10001", MessageChannel.Telegram, "telegram-test", "chat-a"
|
||||
) == request
|
||||
assert not any(
|
||||
call.args and call.args[0] == EventType.MessageAction
|
||||
for call in send_event.call_args_list
|
||||
)
|
||||
|
||||
with patch.object(chain, "_record_user_message"), patch.object(
|
||||
chain.eventmanager, "send_event"
|
||||
) as send_event:
|
||||
chain.handle_message(
|
||||
channel=MessageChannel.Telegram,
|
||||
source="telegram-test",
|
||||
userid="10001",
|
||||
username="tester",
|
||||
text="真正输入",
|
||||
original_chat_id="chat-a",
|
||||
)
|
||||
|
||||
send_event.assert_called_once()
|
||||
event_type, payload = send_event.call_args.args
|
||||
assert event_type == EventType.MessageAction
|
||||
assert payload["input_text"] == "真正输入"
|
||||
assert payload["chat_id"] == "chat-a"
|
||||
|
||||
|
||||
def test_plugin_input_chatless_session_keeps_legacy_chat_fallback():
|
||||
"""旧插件未绑定 chat_id 时,同 source 消息仍可兼容消费。"""
|
||||
chain = MessageChain()
|
||||
request = plugin_input_interaction_manager.create_or_replace(
|
||||
user_id="10001",
|
||||
plugin_id="demo_plugin",
|
||||
channel=MessageChannel.Telegram,
|
||||
source="telegram-test",
|
||||
username="tester",
|
||||
)
|
||||
|
||||
with patch.object(chain, "_record_user_message"), patch.object(
|
||||
chain.eventmanager, "send_event"
|
||||
) as send_event:
|
||||
chain.handle_message(
|
||||
channel=MessageChannel.Telegram,
|
||||
source="telegram-test",
|
||||
userid="10001",
|
||||
username="tester",
|
||||
text="兼容输入",
|
||||
original_chat_id="chat-a",
|
||||
)
|
||||
|
||||
send_event.assert_called_once()
|
||||
event_type, payload = send_event.call_args.args
|
||||
assert event_type == EventType.MessageAction
|
||||
assert payload["input_session_id"] == request.request_id
|
||||
assert payload["input_text"] == "兼容输入"
|
||||
|
||||
|
||||
def test_plugin_input_wildcard_session_does_not_match_missing_source_with_chat():
|
||||
"""chat fallback 不应把完全 wildcard 会话扩大到有渠道但缺来源的消息。"""
|
||||
request = plugin_input_interaction_manager.create_or_replace(
|
||||
user_id="10001",
|
||||
plugin_id="demo_plugin",
|
||||
channel=None,
|
||||
source=None,
|
||||
username="tester",
|
||||
)
|
||||
|
||||
assert plugin_input_interaction_manager.consume_by_user(
|
||||
"10001", MessageChannel.Telegram, None, "chat-a"
|
||||
) == (None, None)
|
||||
assert plugin_input_interaction_manager.get_by_user("10001", None, None) == request
|
||||
|
||||
|
||||
def test_plugin_input_chat_bound_session_does_not_match_missing_chat():
|
||||
"""绑定 chat_id 的会话不应被缺少 chat_id 的消息消费。"""
|
||||
request = plugin_input_interaction_manager.create_or_replace(
|
||||
user_id="10001",
|
||||
plugin_id="demo_plugin",
|
||||
channel=MessageChannel.Telegram,
|
||||
source="telegram-test",
|
||||
username="tester",
|
||||
chat_id="chat-a",
|
||||
)
|
||||
|
||||
assert plugin_input_interaction_manager.consume_by_user(
|
||||
"10001", MessageChannel.Telegram, "telegram-test"
|
||||
) == (None, None)
|
||||
assert plugin_input_interaction_manager.get_by_user(
|
||||
"10001", MessageChannel.Telegram, "telegram-test", "chat-a"
|
||||
) == request
|
||||
|
||||
|
||||
def test_plugin_input_core_path_preserves_original_chat_id():
|
||||
"""直接进入核心路由时也应保留 chat_id 绑定,避免防御路径漏掉插件输入。"""
|
||||
chain = MessageChain()
|
||||
request = plugin_input_interaction_manager.create_or_replace(
|
||||
user_id="10001",
|
||||
plugin_id="demo_plugin",
|
||||
channel=MessageChannel.Telegram,
|
||||
source="telegram-test",
|
||||
username="tester",
|
||||
chat_id="chat-a",
|
||||
)
|
||||
|
||||
with patch.object(chain.eventmanager, "send_event") as send_event:
|
||||
chain._handle_message_core(
|
||||
channel=MessageChannel.Telegram,
|
||||
source="telegram-test",
|
||||
userid="10001",
|
||||
username="tester",
|
||||
text="核心路径输入",
|
||||
original_chat_id="chat-a",
|
||||
)
|
||||
|
||||
send_event.assert_called_once()
|
||||
event_type, payload = send_event.call_args.args
|
||||
assert event_type == EventType.MessageAction
|
||||
assert payload["input_session_id"] == request.request_id
|
||||
assert payload["input_text"] == "核心路径输入"
|
||||
assert payload["chat_id"] == "chat-a"
|
||||
|
||||
|
||||
def test_plugin_input_session_does_not_capture_missing_source_text():
|
||||
"""来源缺失的入站消息不应捕获绑定到具体来源的输入会话。"""
|
||||
chain = MessageChain()
|
||||
request = plugin_input_interaction_manager.create_or_replace(
|
||||
user_id="10001",
|
||||
plugin_id="demo_plugin",
|
||||
channel=MessageChannel.Telegram,
|
||||
source="telegram-bot-a",
|
||||
username="tester",
|
||||
)
|
||||
|
||||
with patch.object(chain, "_record_user_message"), patch.object(
|
||||
chain.eventmanager, "send_event"
|
||||
) as send_event:
|
||||
chain.handle_message(
|
||||
channel=MessageChannel.Telegram,
|
||||
source=None,
|
||||
userid="10001",
|
||||
username="tester",
|
||||
text="普通搜索",
|
||||
)
|
||||
|
||||
assert plugin_input_interaction_manager.get_by_user(
|
||||
"10001", MessageChannel.Telegram, "telegram-bot-a"
|
||||
) == request
|
||||
assert not any(
|
||||
call.args and call.args[0] == EventType.MessageAction
|
||||
for call in send_event.call_args_list
|
||||
)
|
||||
|
||||
|
||||
def test_plugin_input_session_does_not_capture_callback_payload():
|
||||
"""待输入会话存在时,按钮回调仍应优先进入回调链而不是投递给插件。"""
|
||||
chain = MessageChain()
|
||||
request = plugin_input_interaction_manager.create_or_replace(
|
||||
user_id="10001",
|
||||
plugin_id="demo_plugin",
|
||||
channel=MessageChannel.Telegram,
|
||||
source="telegram-test",
|
||||
username="tester",
|
||||
chat_id="chat-a",
|
||||
)
|
||||
|
||||
with patch.object(chain, "_record_user_message") as record_message, patch.object(
|
||||
chain.eventmanager, "send_event"
|
||||
) as send_event, patch.object(chain, "_handle_callback", return_value=True) as handle_callback:
|
||||
chain.handle_message(
|
||||
channel=MessageChannel.Telegram,
|
||||
source="telegram-test",
|
||||
userid="10001",
|
||||
username="tester",
|
||||
text="CALLBACK:media:req:page-next",
|
||||
original_message_id="msg-1",
|
||||
original_chat_id="chat-a",
|
||||
)
|
||||
|
||||
record_message.assert_not_called()
|
||||
handle_callback.assert_called_once()
|
||||
assert plugin_input_interaction_manager.get_by_user(
|
||||
"10001", MessageChannel.Telegram, "telegram-test", "chat-a"
|
||||
) == request
|
||||
assert not any(
|
||||
call.args and call.args[0] == EventType.MessageAction
|
||||
for call in send_event.call_args_list
|
||||
)
|
||||
|
||||
|
||||
def test_plugin_input_session_expires_after_timeout():
|
||||
"""插件输入会话超过 TTL 后不再匹配。"""
|
||||
request = plugin_input_interaction_manager.create_or_replace(
|
||||
user_id="10001",
|
||||
plugin_id="demo_plugin",
|
||||
channel=MessageChannel.Wechat,
|
||||
source="wechat-test",
|
||||
username="tester",
|
||||
timeout_seconds=120,
|
||||
)
|
||||
request.created_at = datetime.now() - timedelta(seconds=121)
|
||||
|
||||
assert plugin_input_interaction_manager.get_by_user("10001", MessageChannel.Wechat) is None
|
||||
|
||||
|
||||
def test_plugin_input_session_expired_text_notifies_plugin_and_continues_routing():
|
||||
"""过期插件输入遇到命令时应提示超时,并让命令继续正常路由。"""
|
||||
chain = MessageChain()
|
||||
request = plugin_input_interaction_manager.create_or_replace(
|
||||
user_id="10001",
|
||||
plugin_id="demo_plugin",
|
||||
channel=MessageChannel.Telegram,
|
||||
source="telegram-test",
|
||||
username="tester",
|
||||
timeout_seconds=120,
|
||||
)
|
||||
request.created_at = datetime.now() - timedelta(seconds=121)
|
||||
|
||||
with patch.object(chain, "_record_user_message") as record_message, patch.object(
|
||||
chain.eventmanager, "send_event"
|
||||
) as send_event, patch.object(chain, "post_message") as post_message:
|
||||
chain.handle_message(
|
||||
channel=MessageChannel.Telegram,
|
||||
source="telegram-test",
|
||||
userid="10001",
|
||||
username="tester",
|
||||
text="/tvh",
|
||||
)
|
||||
|
||||
record_message.assert_called_once()
|
||||
post_message.assert_called_once()
|
||||
assert send_event.call_count == 2
|
||||
event_type, payload = send_event.call_args_list[0].args
|
||||
assert event_type == EventType.MessageAction
|
||||
assert payload["expired"] is True
|
||||
assert payload["input_session_id"] == request.request_id
|
||||
command_type, command_payload = send_event.call_args_list[1].args
|
||||
assert command_type == EventType.CommandExcute
|
||||
assert command_payload["cmd"] == "/tvh"
|
||||
|
||||
|
||||
def test_plugin_input_session_expired_sensitive_text_is_not_recorded_or_routed():
|
||||
"""过期插件输入中的普通文本仍可能是敏感信息,应提示后吞掉。"""
|
||||
chain = MessageChain()
|
||||
request = plugin_input_interaction_manager.create_or_replace(
|
||||
user_id="10001",
|
||||
plugin_id="demo_plugin",
|
||||
channel=MessageChannel.Telegram,
|
||||
source="telegram-test",
|
||||
username="tester",
|
||||
timeout_seconds=120,
|
||||
)
|
||||
request.created_at = datetime.now() - timedelta(seconds=121)
|
||||
|
||||
with patch.object(chain, "_record_user_message") as record_message, patch.object(
|
||||
chain.eventmanager, "send_event"
|
||||
) as send_event, patch.object(chain, "post_message") as post_message:
|
||||
chain.handle_message(
|
||||
channel=MessageChannel.Telegram,
|
||||
source="telegram-test",
|
||||
userid="10001",
|
||||
username="tester",
|
||||
text="late-secret",
|
||||
)
|
||||
|
||||
record_message.assert_not_called()
|
||||
post_message.assert_called_once()
|
||||
send_event.assert_called_once()
|
||||
event_type, payload = send_event.call_args.args
|
||||
assert event_type == EventType.MessageAction
|
||||
assert payload["expired"] is True
|
||||
assert payload["input_session_id"] == request.request_id
|
||||
|
||||
|
||||
def test_plugin_input_expired_text_after_cleanup_is_not_recorded_or_routed():
|
||||
"""其他用户触发过期清理后,迟到的普通文本仍应按过期插件输入吞掉。"""
|
||||
chain = MessageChain()
|
||||
request = plugin_input_interaction_manager.create_or_replace(
|
||||
user_id="10001",
|
||||
plugin_id="demo_plugin",
|
||||
channel=MessageChannel.Telegram,
|
||||
source="telegram-test",
|
||||
username="tester",
|
||||
timeout_seconds=120,
|
||||
)
|
||||
request.created_at = datetime.now() - timedelta(seconds=121)
|
||||
|
||||
plugin_input_interaction_manager.create_or_replace(
|
||||
user_id="10002",
|
||||
plugin_id="demo_plugin",
|
||||
channel=MessageChannel.Telegram,
|
||||
source="telegram-test",
|
||||
username="other",
|
||||
timeout_seconds=120,
|
||||
)
|
||||
assert request.request_id not in plugin_input_interaction_manager._by_id
|
||||
|
||||
with patch.object(chain, "_record_user_message") as record_message, patch.object(
|
||||
chain.eventmanager, "send_event"
|
||||
) as send_event, patch.object(chain, "post_message") as post_message:
|
||||
chain.handle_message(
|
||||
channel=MessageChannel.Telegram,
|
||||
source="telegram-test",
|
||||
userid="10001",
|
||||
username="tester",
|
||||
text="late-secret",
|
||||
)
|
||||
|
||||
record_message.assert_not_called()
|
||||
post_message.assert_called_once()
|
||||
send_event.assert_called_once()
|
||||
event_type, payload = send_event.call_args.args
|
||||
assert event_type == EventType.MessageAction
|
||||
assert payload["expired"] is True
|
||||
assert payload["input_session_id"] == request.request_id
|
||||
|
||||
|
||||
def test_plugin_input_session_with_no_channel_matches_specific_channel():
|
||||
"""插件拿不到渠道时创建的会话应能被该用户下一条具体渠道消息消费。"""
|
||||
chain = MessageChain()
|
||||
request = plugin_input_interaction_manager.create_or_replace(
|
||||
user_id="10001",
|
||||
plugin_id="demo_plugin",
|
||||
channel=None,
|
||||
source="telegram-test",
|
||||
username="tester",
|
||||
)
|
||||
|
||||
with patch.object(chain, "_record_user_message"), patch.object(
|
||||
chain.eventmanager, "send_event"
|
||||
) as send_event:
|
||||
chain.handle_message(
|
||||
channel=MessageChannel.Telegram,
|
||||
source="telegram-test",
|
||||
userid="10001",
|
||||
username="tester",
|
||||
text="keyword",
|
||||
)
|
||||
|
||||
send_event.assert_called_once()
|
||||
event_type, payload = send_event.call_args.args
|
||||
assert event_type == EventType.MessageAction
|
||||
assert payload["input_session_id"] == request.request_id
|
||||
assert payload["input_text"] == "keyword"
|
||||
|
||||
|
||||
def test_plugin_input_session_with_no_channel_and_no_source_does_not_match_specific_message():
|
||||
"""完全缺少渠道来源的会话不应跨渠道捕获具体消息。"""
|
||||
chain = MessageChain()
|
||||
request = plugin_input_interaction_manager.create_or_replace(
|
||||
user_id="10001",
|
||||
plugin_id="demo_plugin",
|
||||
channel=None,
|
||||
source=None,
|
||||
username="tester",
|
||||
)
|
||||
|
||||
with patch.object(chain, "_record_user_message"), patch.object(
|
||||
chain.eventmanager, "send_event"
|
||||
) as send_event:
|
||||
chain.handle_message(
|
||||
channel=MessageChannel.Telegram,
|
||||
source="telegram-test",
|
||||
userid="10001",
|
||||
username="tester",
|
||||
text="keyword",
|
||||
)
|
||||
|
||||
assert plugin_input_interaction_manager.get_by_user("10001", None, None) == request
|
||||
assert not any(
|
||||
call.args and call.args[0] == EventType.MessageAction
|
||||
for call in send_event.call_args_list
|
||||
)
|
||||
|
||||
|
||||
def test_plugin_input_specific_session_replaces_overlapping_no_channel_session():
|
||||
"""同用户创建具体渠道会话时,应替换重叠的无渠道会话,避免下一条消息被连环接管。"""
|
||||
old_request = plugin_input_interaction_manager.create_or_replace(
|
||||
user_id="10001",
|
||||
plugin_id="old_plugin",
|
||||
channel=None,
|
||||
source="telegram-test",
|
||||
username="tester",
|
||||
)
|
||||
new_request = plugin_input_interaction_manager.create_or_replace(
|
||||
user_id="10001",
|
||||
plugin_id="demo_plugin",
|
||||
channel=MessageChannel.Telegram,
|
||||
source="telegram-test",
|
||||
username="tester",
|
||||
)
|
||||
|
||||
assert plugin_input_interaction_manager.pop_by_user(
|
||||
"10001", MessageChannel.Telegram, "telegram-test"
|
||||
) == new_request
|
||||
assert plugin_input_interaction_manager.pop_by_user(
|
||||
"10001", MessageChannel.Telegram, "telegram-test"
|
||||
) is None
|
||||
|
||||
|
||||
def test_plugin_input_session_pop_by_user_consumes_once():
|
||||
"""原子消费应保证同一个会话只返回一次。"""
|
||||
request = plugin_input_interaction_manager.create_or_replace(
|
||||
user_id="10001",
|
||||
plugin_id="demo_plugin",
|
||||
channel=MessageChannel.Telegram,
|
||||
source="telegram-test",
|
||||
username="tester",
|
||||
)
|
||||
|
||||
assert plugin_input_interaction_manager.pop_by_user(
|
||||
"10001", MessageChannel.Telegram, "telegram-test"
|
||||
) == request
|
||||
assert plugin_input_interaction_manager.pop_by_user(
|
||||
"10001", MessageChannel.Telegram, "telegram-test"
|
||||
) is None
|
||||
|
||||
|
||||
def test_target_plugin_filter_only_allows_target_plugin_handler():
|
||||
"""带目标插件的输入事件不应投递给其他插件或模块级处理器。"""
|
||||
|
||||
def demo_plugin_handler(_event):
|
||||
return None
|
||||
|
||||
def other_plugin_handler(_event):
|
||||
return None
|
||||
|
||||
demo_plugin_handler.__qualname__ = "demo_plugin.handle"
|
||||
other_plugin_handler.__qualname__ = "other_plugin.handle"
|
||||
|
||||
def module_handler(_event):
|
||||
return None
|
||||
|
||||
should_dispatch = EventManager._EventManager__should_dispatch_to_target_plugin
|
||||
handler_id = EventManager._EventManager__get_handler_identifier(demo_plugin_handler)
|
||||
|
||||
assert should_dispatch(
|
||||
demo_plugin_handler, "tests.plugins.demo_plugin.handle", "demo_plugin"
|
||||
) is True
|
||||
assert should_dispatch(demo_plugin_handler, handler_id, "demo_plugin") is True
|
||||
assert should_dispatch(
|
||||
demo_plugin_handler, "tests.plugins.other_plugin.handle", "demo_plugin"
|
||||
) is False
|
||||
assert should_dispatch(
|
||||
other_plugin_handler, "tests.plugins.other_plugin.handle", "demo_plugin"
|
||||
) is False
|
||||
assert should_dispatch(module_handler, "tests.module_handler", "demo_plugin") is False
|
||||
|
||||
|
||||
def test_noai_prefix_starts_traditional_search_when_global_ai_enabled():
|
||||
"""全局 AI 开启时,/noai 前缀应让本条消息进入传统搜索交互。"""
|
||||
chain = MessageChain()
|
||||
|
||||
Reference in New Issue
Block a user