mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-22 00:32:50 +08:00
fix(agent): bind channel admin identity (#6294)
This commit is contained in:
425
tests/test_agent_channel_admin_identity.py
Normal file
425
tests/test_agent_channel_admin_identity.py
Normal file
@@ -0,0 +1,425 @@
|
||||
import json
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from app.helper.agent import matches_channel_admin
|
||||
from app.modules.discord import DiscordModule
|
||||
from app.modules.feishu.feishu import Feishu
|
||||
from app.modules.qqbot import QQBotModule
|
||||
from app.modules.slack import SlackModule
|
||||
from app.modules.synologychat import SynologyChatModule
|
||||
from app.modules.telegram import TelegramModule
|
||||
from app.modules.vocechat import VoceChatModule
|
||||
from app.modules.wechat import WechatModule
|
||||
from app.modules.wechatclawbot import WechatClawBotModule
|
||||
|
||||
|
||||
def _parse_module_message(module, *, config: dict, body, client=None, form=None):
|
||||
"""使用隔离的渠道配置调用消息解析器。"""
|
||||
client = client or SimpleNamespace()
|
||||
with patch.object(
|
||||
module,
|
||||
"get_config",
|
||||
return_value=SimpleNamespace(name="channel-test", config=config),
|
||||
), patch.object(module, "get_instance", return_value=client):
|
||||
return module.message_parser(
|
||||
source="channel-test",
|
||||
body=body,
|
||||
form=form or {},
|
||||
args={},
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config", "principal_ids", "expected"),
|
||||
[
|
||||
({"ADMINS": " user-1, 42 "}, ("user-1",), True),
|
||||
({"ADMINS": "user-1,42"}, ("user-2", 7), False),
|
||||
({"ADMINS": ""}, ("user-1",), False),
|
||||
({}, ("user-1",), False),
|
||||
(None, ("user-1",), False),
|
||||
],
|
||||
)
|
||||
def test_matches_channel_admin_uses_nonempty_stable_principal_set(
|
||||
config, principal_ids, expected
|
||||
):
|
||||
assert matches_channel_admin(config, "ADMINS", *principal_ids) is expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize("message_kind", ["message", "callback"])
|
||||
def test_telegram_uses_user_id_not_same_named_username(message_kind):
|
||||
module = TelegramModule()
|
||||
client = SimpleNamespace(bot_username=None, answer_callback_query=Mock())
|
||||
if message_kind == "message":
|
||||
payload = {
|
||||
"message_id": 10,
|
||||
"from": {"id": 10002, "username": "admin"},
|
||||
"chat": {"id": 10002},
|
||||
"text": "hello",
|
||||
}
|
||||
else:
|
||||
payload = {
|
||||
"callback_query": {
|
||||
"id": "callback-1",
|
||||
"from": {"id": 10002, "username": "admin"},
|
||||
"data": "choice:1",
|
||||
"message": {"message_id": 10, "chat": {"id": 10002}},
|
||||
}
|
||||
}
|
||||
|
||||
message = _parse_module_message(
|
||||
module,
|
||||
config={"TELEGRAM_ADMINS": "admin,10001"},
|
||||
body=json.dumps(payload),
|
||||
client=client,
|
||||
)
|
||||
|
||||
assert message.userid == 10002
|
||||
assert message.username == "admin"
|
||||
assert message.is_channel_admin is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize("message_kind", ["message", "callback"])
|
||||
def test_telegram_uses_stable_user_id_for_admin(message_kind):
|
||||
module = TelegramModule()
|
||||
client = SimpleNamespace(bot_username=None, answer_callback_query=Mock())
|
||||
if message_kind == "message":
|
||||
payload = {
|
||||
"message_id": 10,
|
||||
"from": {"id": 10001, "username": "renamed-user"},
|
||||
"chat": {"id": 10001},
|
||||
"text": "hello",
|
||||
}
|
||||
else:
|
||||
payload = {
|
||||
"callback_query": {
|
||||
"id": "callback-1",
|
||||
"from": {"id": 10001, "username": "renamed-user"},
|
||||
"data": "choice:1",
|
||||
"message": {"message_id": 10, "chat": {"id": 10001}},
|
||||
}
|
||||
}
|
||||
|
||||
message = _parse_module_message(
|
||||
module,
|
||||
config={"TELEGRAM_ADMINS": "10001"},
|
||||
body=json.dumps(payload),
|
||||
client=client,
|
||||
)
|
||||
|
||||
assert message.userid == 10001
|
||||
assert message.is_channel_admin is True
|
||||
|
||||
|
||||
def test_telegram_slash_does_not_accept_admin_display_username():
|
||||
"""Telegram 斜杠命令不得把可修改的 username 当作管理员 ID。"""
|
||||
module = TelegramModule()
|
||||
client = SimpleNamespace(bot_username=None, send_msg=Mock())
|
||||
message = _parse_module_message(
|
||||
module,
|
||||
config={"TELEGRAM_ADMINS": "admin"},
|
||||
body=json.dumps(
|
||||
{
|
||||
"message_id": 10,
|
||||
"from": {"id": 10002, "username": "admin"},
|
||||
"chat": {"id": 10002},
|
||||
"text": "/sites",
|
||||
}
|
||||
),
|
||||
client=client,
|
||||
)
|
||||
|
||||
assert message is None
|
||||
client.send_msg.assert_called_once()
|
||||
|
||||
|
||||
def test_telegram_empty_admin_list_keeps_legacy_slash_without_agent_admin():
|
||||
"""空名单保持传统命令可用,但不能生成 Agent 管理员身份。"""
|
||||
module = TelegramModule()
|
||||
client = SimpleNamespace(bot_username=None, send_msg=Mock())
|
||||
message = _parse_module_message(
|
||||
module,
|
||||
config={"TELEGRAM_ADMINS": ""},
|
||||
body=json.dumps(
|
||||
{
|
||||
"message_id": 10,
|
||||
"from": {"id": 10002, "username": "admin"},
|
||||
"chat": {"id": 10002},
|
||||
"text": "/sites",
|
||||
}
|
||||
),
|
||||
client=client,
|
||||
)
|
||||
|
||||
assert message.is_channel_admin is False
|
||||
client.send_msg.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"payload",
|
||||
[
|
||||
{"type": "message", "user": "UADMIN", "text": "hello"},
|
||||
{
|
||||
"type": "block_actions",
|
||||
"user": {"id": "UADMIN", "name": "renamed-user"},
|
||||
"actions": [{"value": "choice:1"}],
|
||||
"message": {"ts": "1710000000.000100"},
|
||||
"container": {"channel_id": "C01"},
|
||||
},
|
||||
],
|
||||
)
|
||||
def test_slack_message_and_callback_use_stable_user_id(payload):
|
||||
message = _parse_module_message(
|
||||
SlackModule(),
|
||||
config={"SLACK_ADMINS": "UADMIN"},
|
||||
body=json.dumps(payload),
|
||||
)
|
||||
|
||||
assert message.userid == "UADMIN"
|
||||
assert message.is_channel_admin is True
|
||||
|
||||
|
||||
def test_slack_slash_does_not_accept_admin_display_username():
|
||||
"""Slack 原生斜杠命令只接受稳定 user_id,不接受 user_name。"""
|
||||
client = SimpleNamespace(send_msg=Mock())
|
||||
message = _parse_module_message(
|
||||
SlackModule(),
|
||||
config={"SLACK_ADMINS": "admin"},
|
||||
body=json.dumps(
|
||||
{
|
||||
"command": "/sites",
|
||||
"user_id": "UUSER",
|
||||
"user_name": "admin",
|
||||
"channel_id": "C01",
|
||||
}
|
||||
),
|
||||
client=client,
|
||||
)
|
||||
|
||||
assert message is None
|
||||
client.send_msg.assert_called_once()
|
||||
|
||||
|
||||
def test_slack_empty_admin_list_keeps_legacy_slash_without_agent_admin():
|
||||
"""Slack 空名单不预拦截命令,但 Agent 管理员事实仍为否。"""
|
||||
client = SimpleNamespace(send_msg=Mock())
|
||||
message = _parse_module_message(
|
||||
SlackModule(),
|
||||
config={"SLACK_ADMINS": ""},
|
||||
body=json.dumps(
|
||||
{
|
||||
"command": "/sites",
|
||||
"user_id": "UUSER",
|
||||
"user_name": "admin",
|
||||
"channel_id": "C01",
|
||||
}
|
||||
),
|
||||
client=client,
|
||||
)
|
||||
|
||||
assert message.is_channel_admin is False
|
||||
client.send_msg.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"payload",
|
||||
[
|
||||
{
|
||||
"type": "message",
|
||||
"userid": "discord-admin-id",
|
||||
"username": "renamed-user",
|
||||
"text": "hello",
|
||||
},
|
||||
{
|
||||
"type": "interaction",
|
||||
"userid": "discord-admin-id",
|
||||
"username": "renamed-user",
|
||||
"callback_data": "choice:1",
|
||||
},
|
||||
],
|
||||
)
|
||||
def test_discord_message_and_callback_use_stable_user_id(payload):
|
||||
message = _parse_module_message(
|
||||
DiscordModule(),
|
||||
config={"DISCORD_ADMINS": "discord-admin-id"},
|
||||
body=json.dumps(payload),
|
||||
)
|
||||
|
||||
assert message.userid == "discord-admin-id"
|
||||
assert message.is_channel_admin is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("payload", "admins"),
|
||||
[
|
||||
(
|
||||
{
|
||||
"text": "hello",
|
||||
"sender": {
|
||||
"open_id": "ou_admin",
|
||||
"user_id": "u_other",
|
||||
"name": "renamed-user",
|
||||
},
|
||||
},
|
||||
"ou_admin",
|
||||
),
|
||||
(
|
||||
{
|
||||
"type": "cardAction",
|
||||
"callback_data": "choice:1",
|
||||
"sender": {
|
||||
"open_id": "ou_other",
|
||||
"user_id": "u_admin",
|
||||
"name": "renamed-user",
|
||||
},
|
||||
},
|
||||
"u_admin",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_feishu_message_and_card_callback_accept_open_id_or_user_id(payload, admins):
|
||||
with patch.object(Feishu, "_build_api_client", return_value=Mock()), patch.object(
|
||||
Feishu, "_start_ws_client"
|
||||
), patch("app.modules.feishu.feishu.UserOper") as user_oper:
|
||||
user_oper.return_value.get_name.return_value = None
|
||||
client = Feishu(
|
||||
FEISHU_APP_ID="app-id",
|
||||
FEISHU_APP_SECRET="app-secret",
|
||||
FEISHU_ADMINS=admins,
|
||||
name="feishu-test",
|
||||
)
|
||||
message = client.parse_message(payload)
|
||||
|
||||
assert message.userid == payload["sender"]["open_id"]
|
||||
assert message.is_channel_admin is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("user_id", "username", "expected"),
|
||||
[("wxid_admin", "renamed-user", True), ("wxid_user", "admin-name", False)],
|
||||
)
|
||||
def test_wechatclawbot_uses_channel_user_id_not_username(user_id, username, expected):
|
||||
message = _parse_module_message(
|
||||
WechatClawBotModule(),
|
||||
config={"WECHATCLAWBOT_ADMINS": "admin-name,wxid_admin"},
|
||||
body={
|
||||
"__channel__": "wechatclawbot",
|
||||
"userid": user_id,
|
||||
"username": username,
|
||||
"text": "hello",
|
||||
},
|
||||
)
|
||||
|
||||
assert message.userid == user_id
|
||||
assert message.is_channel_admin is expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("sender", "admins", "expected"),
|
||||
[("wechat-admin", "wechat-admin", True), ("wechat-user", "display-admin", False)],
|
||||
)
|
||||
def test_wechat_bot_uses_sender_userid(sender, admins, expected):
|
||||
message = _parse_module_message(
|
||||
WechatModule(),
|
||||
config={"WECHAT_MODE": "bot", "WECHAT_ADMINS": admins},
|
||||
body=json.dumps(
|
||||
{
|
||||
"body": {
|
||||
"from": {"userid": sender},
|
||||
"msgtype": "text",
|
||||
"text": {"content": "hello"},
|
||||
}
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
assert message.userid == sender
|
||||
assert message.is_channel_admin is expected
|
||||
|
||||
|
||||
def test_qq_c2c_uses_user_openid_for_admin():
|
||||
message = _parse_module_message(
|
||||
QQBotModule(),
|
||||
config={"QQBOT_ADMINS": "qq-admin"},
|
||||
body={
|
||||
"type": "C2C_MESSAGE_CREATE",
|
||||
"content": "hello",
|
||||
"author": {"user_openid": "qq-admin"},
|
||||
},
|
||||
)
|
||||
|
||||
assert message.userid == "qq-admin"
|
||||
assert message.is_channel_admin is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("admins", "expected"),
|
||||
[("member-admin", True), ("group:group-admin", False), ("group-admin", False)],
|
||||
)
|
||||
def test_qq_group_uses_only_member_openid_for_admin(admins, expected):
|
||||
message = _parse_module_message(
|
||||
QQBotModule(),
|
||||
config={"QQBOT_ADMINS": admins},
|
||||
body={
|
||||
"type": "GROUP_AT_MESSAGE_CREATE",
|
||||
"content": "hello",
|
||||
"author": {"member_openid": "member-admin"},
|
||||
"group_openid": "group-admin",
|
||||
},
|
||||
)
|
||||
|
||||
assert message.userid == "group:group-admin"
|
||||
assert message.username == "member-admin"
|
||||
assert message.is_channel_admin is expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("admins", "expected"),
|
||||
[("7", True), ("UID#7", True), ("GID#2", False)],
|
||||
)
|
||||
def test_vocechat_group_uses_only_sender_uid_for_admin(admins, expected):
|
||||
message = _parse_module_message(
|
||||
VoceChatModule(),
|
||||
config={"VOCECHAT_ADMINS": admins, "channel_id": "2"},
|
||||
body=json.dumps(
|
||||
{
|
||||
"detail": {
|
||||
"type": "normal",
|
||||
"content_type": "text/plain",
|
||||
"content": "hello",
|
||||
},
|
||||
"from_uid": 7,
|
||||
"target": {"gid": 2},
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
assert message.userid == "GID#2"
|
||||
assert message.username == "GID#2"
|
||||
assert message.is_channel_admin is expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("admins", "expected"),
|
||||
[("42", True), ("display-admin", False)],
|
||||
)
|
||||
def test_synology_chat_uses_numeric_user_id_not_username(admins, expected):
|
||||
client = SimpleNamespace(check_token=Mock(return_value=True))
|
||||
message = _parse_module_message(
|
||||
SynologyChatModule(),
|
||||
config={"SYNOLOGYCHAT_ADMINS": admins},
|
||||
body={},
|
||||
form={
|
||||
"token": "token",
|
||||
"text": "hello",
|
||||
"user_id": "42",
|
||||
"username": "display-admin",
|
||||
},
|
||||
client=client,
|
||||
)
|
||||
|
||||
assert message.userid == 42
|
||||
assert message.username == "display-admin"
|
||||
assert message.is_channel_admin is expected
|
||||
@@ -83,6 +83,75 @@ def test_explicit_ai_message_is_not_recorded_to_message_history():
|
||||
process_message.assert_called_once()
|
||||
|
||||
|
||||
def test_message_chain_passes_stable_channel_admin_principal_to_agent():
|
||||
"""消息链应将渠道适配器生成的管理员事实传给 Agent。"""
|
||||
chain = MessageChain()
|
||||
|
||||
with patch.object(settings, "AI_AGENT_ENABLE", True), patch(
|
||||
"app.chain.message.agent_manager.process_message",
|
||||
new_callable=AsyncMock,
|
||||
) as process_message, patch(
|
||||
"app.chain.message.asyncio.run_coroutine_threadsafe",
|
||||
side_effect=lambda coro, _loop: (coro.close(), Mock())[1],
|
||||
):
|
||||
chain.handle_message(
|
||||
channel=MessageChannel.Telegram,
|
||||
source="telegram-test",
|
||||
userid="10001",
|
||||
username="renamed-user",
|
||||
is_channel_admin=True,
|
||||
text="/ai 检查系统状态",
|
||||
)
|
||||
|
||||
assert process_message.call_args.kwargs["is_channel_admin"] is True
|
||||
|
||||
|
||||
def test_message_chain_does_not_trust_channel_display_username():
|
||||
"""消息链应保留适配器给出的明确非管理员结论。"""
|
||||
chain = MessageChain()
|
||||
|
||||
with patch.object(settings, "AI_AGENT_ENABLE", True), patch(
|
||||
"app.chain.message.agent_manager.process_message",
|
||||
new_callable=AsyncMock,
|
||||
) as process_message, patch(
|
||||
"app.chain.message.asyncio.run_coroutine_threadsafe",
|
||||
side_effect=lambda coro, _loop: (coro.close(), Mock())[1],
|
||||
):
|
||||
chain.handle_message(
|
||||
channel=MessageChannel.Telegram,
|
||||
source="telegram-test",
|
||||
userid="10002",
|
||||
username="admin",
|
||||
is_channel_admin=False,
|
||||
text="/ai 检查系统状态",
|
||||
)
|
||||
|
||||
assert process_message.call_args.kwargs["is_channel_admin"] is False
|
||||
|
||||
|
||||
def test_message_chain_uses_same_admin_contract_for_slack():
|
||||
"""管理员事实透传应复用于其他消息渠道,而不是 Telegram 特判。"""
|
||||
chain = MessageChain()
|
||||
|
||||
with patch.object(settings, "AI_AGENT_ENABLE", True), patch(
|
||||
"app.chain.message.agent_manager.process_message",
|
||||
new_callable=AsyncMock,
|
||||
) as process_message, patch(
|
||||
"app.chain.message.asyncio.run_coroutine_threadsafe",
|
||||
side_effect=lambda coro, _loop: (coro.close(), Mock())[1],
|
||||
):
|
||||
chain.handle_message(
|
||||
channel=MessageChannel.Slack,
|
||||
source="slack-test",
|
||||
userid="UADMIN",
|
||||
username="renamed-user",
|
||||
is_channel_admin=True,
|
||||
text="/ai 检查系统状态",
|
||||
)
|
||||
|
||||
assert process_message.call_args.kwargs["is_channel_admin"] is True
|
||||
|
||||
|
||||
def test_ask_user_choice_message_is_not_recorded_to_message_history():
|
||||
"""Agent 询问用户意图工具发送的按钮消息不登记到消息表。"""
|
||||
_clear_messages()
|
||||
@@ -200,6 +269,7 @@ def test_agent_choice_callback_is_not_recorded_to_message_history():
|
||||
source="telegram-test",
|
||||
userid="10001",
|
||||
username="tester",
|
||||
is_channel_admin=False,
|
||||
original_message_id=123,
|
||||
original_chat_id="456",
|
||||
)
|
||||
@@ -208,3 +278,4 @@ def test_agent_choice_callback_is_not_recorded_to_message_history():
|
||||
|
||||
record_user_message.assert_not_called()
|
||||
process_message.assert_called_once()
|
||||
assert process_message.call_args.kwargs["is_channel_admin"] is False
|
||||
|
||||
@@ -9,6 +9,7 @@ from app.agent.tools.impl.edit_file import EditFileTool
|
||||
from app.agent.tools.impl.list_directory import ListDirectoryTool
|
||||
from app.agent.tools.impl.query_downloaders import QueryDownloadersTool
|
||||
from app.agent.tools.impl.query_sites import QuerySitesTool
|
||||
from app.agent.tools.impl.query_system_settings import QuerySystemSettingsTool
|
||||
from app.agent.tools.impl.read_file import ReadFileTool
|
||||
from app.agent.tools.impl.write_file import WriteFileTool
|
||||
from app.agent.tools.manager import MoviePilotToolsManager
|
||||
@@ -357,3 +358,78 @@ def test_channel_agent_admin_user_id_does_not_bypass_user_lookup():
|
||||
)
|
||||
|
||||
assert context["is_admin"] is False
|
||||
|
||||
|
||||
def test_channel_agent_rejects_local_admin_username_without_trusted_principal():
|
||||
"""外部显示名与本地管理员同名时,不得获得 Agent 管理员权限。"""
|
||||
agent = MoviePilotAgent(
|
||||
session_id="session-1",
|
||||
user_id="10002",
|
||||
channel=MessageChannel.Telegram.value,
|
||||
source="telegram-main",
|
||||
username="admin",
|
||||
)
|
||||
agent.is_channel_admin = False
|
||||
|
||||
with patch("app.agent.UserOper") as user_oper:
|
||||
user_oper.return_value.async_get_by_name = AsyncMock(
|
||||
return_value=SimpleNamespace(is_superuser=True)
|
||||
)
|
||||
context = asyncio.run(
|
||||
agent._build_tool_context(should_dispatch_reply=True)
|
||||
)
|
||||
|
||||
assert context["is_admin"] is False
|
||||
user_oper.return_value.async_get_by_name.assert_not_awaited()
|
||||
|
||||
|
||||
def test_channel_agent_accepts_trusted_admin_principal_without_local_user():
|
||||
"""宿主确认的渠道管理员应直接获得 Agent 管理员权限。"""
|
||||
agent = MoviePilotAgent(
|
||||
session_id="session-1",
|
||||
user_id="10001",
|
||||
channel=MessageChannel.Telegram.value,
|
||||
source="telegram-main",
|
||||
username="renamed-user",
|
||||
)
|
||||
agent.is_channel_admin = True
|
||||
|
||||
with patch("app.agent.UserOper") as user_oper:
|
||||
context = asyncio.run(
|
||||
agent._build_tool_context(should_dispatch_reply=True)
|
||||
)
|
||||
|
||||
assert context["is_admin"] is True
|
||||
user_oper.return_value.async_get_by_name.assert_not_called()
|
||||
|
||||
|
||||
def test_tool_explicit_non_admin_context_does_not_fallback_to_channel_lookup():
|
||||
"""Agent 已判定为非管理员时,工具不得通过旧权限查询重新授权。"""
|
||||
tool = QuerySitesTool(session_id="session-1", user_id="10002")
|
||||
tool.set_message_attr(
|
||||
channel=MessageChannel.Telegram.value,
|
||||
source="telegram-main",
|
||||
username="admin",
|
||||
)
|
||||
tool.set_agent_context({"is_admin": False})
|
||||
|
||||
with patch.object(
|
||||
tool,
|
||||
"_has_channel_admin_permission",
|
||||
new=AsyncMock(return_value=True),
|
||||
) as has_channel_admin_permission:
|
||||
result = asyncio.run(tool.is_admin_user())
|
||||
|
||||
assert result is False
|
||||
has_channel_admin_permission.assert_not_awaited()
|
||||
|
||||
|
||||
def test_admin_tool_rejects_explicit_non_admin_without_channel_context():
|
||||
"""显式非管理员事实必须拒绝管理员工具,不能走无渠道兼容放行。"""
|
||||
tool = QuerySystemSettingsTool(session_id="session-1", user_id="10002")
|
||||
tool.set_agent_context({"is_admin": False})
|
||||
|
||||
result = asyncio.run(tool._check_permission())
|
||||
|
||||
assert result is not None
|
||||
assert "没有执行此工具的权限" in result
|
||||
|
||||
@@ -631,6 +631,7 @@ async def test_cached_agent_clears_channel_for_background_task() -> None:
|
||||
channel="Telegram",
|
||||
source="telegram-test",
|
||||
username="admin",
|
||||
is_channel_admin=True,
|
||||
original_chat_id="chat-123",
|
||||
)
|
||||
agent.process = AsyncMock(return_value="完成")
|
||||
@@ -652,9 +653,42 @@ async def test_cached_agent_clears_channel_for_background_task() -> None:
|
||||
assert result == "完成"
|
||||
assert agent.channel is None
|
||||
assert agent.source is None
|
||||
assert agent.is_channel_admin is None
|
||||
assert agent.original_chat_id is None
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_cached_agent_overwrites_channel_admin_with_explicit_false() -> None:
|
||||
"""复用会话 Agent 时,明确非管理员结论必须覆盖上一轮管理员身份。"""
|
||||
manager = AgentManager()
|
||||
agent = MoviePilotAgent(
|
||||
session_id="channel-admin-cached-session",
|
||||
user_id="user-1",
|
||||
channel="Telegram",
|
||||
source="telegram-test",
|
||||
username="admin",
|
||||
is_channel_admin=True,
|
||||
)
|
||||
agent.process = AsyncMock(return_value="完成")
|
||||
manager.active_agents[agent.session_id] = agent
|
||||
task = _MessageTask(
|
||||
session_id=agent.session_id,
|
||||
user_id="user-2",
|
||||
message="执行普通用户请求",
|
||||
channel="Telegram",
|
||||
source="telegram-test",
|
||||
username="admin",
|
||||
is_channel_admin=False,
|
||||
)
|
||||
|
||||
result = await manager._process_message_internal(task)
|
||||
|
||||
assert result == "完成"
|
||||
assert agent.user_id == "user-2"
|
||||
assert agent.username == "admin"
|
||||
assert agent.is_channel_admin is False
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_background_agent_final_message_is_broadcast() -> None:
|
||||
"""后台 Agent 的最终消息应清空渠道及渠道用户定位后广播。"""
|
||||
|
||||
@@ -75,7 +75,9 @@ class TestMessageProcessingStatus(unittest.TestCase):
|
||||
module = SlackModule()
|
||||
|
||||
with patch.object(
|
||||
module, "get_config", return_value=SimpleNamespace(name="slack-main")
|
||||
module,
|
||||
"get_config",
|
||||
return_value=SimpleNamespace(name="slack-main", config={}),
|
||||
):
|
||||
message = module.message_parser(
|
||||
source="slack-main",
|
||||
|
||||
Reference in New Issue
Block a user