Files
AstrBot/tests/unit/test_message_tools.py
NayukiMeko 22ba831a31 fix(message_tools): throw exception and block message sending when path does not exist (#8149)
* fix(message_tools): 路径不存在时抛出异常并阻止消息发送

- _resolve_path_from_sandbox 在所有解析路径均失败时改为抛出 FileNotFoundError,而非静默返回原始路径,避免将无效路径传递给下游组件
- 新增 component_type 关键字参数,使错误信息能明确指出是 image/record/video/file 哪类资源路径缺失
- 在 call 方法中捕获 FileNotFoundError 并提前返回错误字符串,确保路径无效时不会继续构建或发送任何消息组件
- 补充单元测试,验证缺失图片路径场景下 send_message 不会被调用

* Update tests/unit/test_message_tools.py

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

* fix(tools): propagate sandbox error instead of masking as FileNotFoundError

---------

Co-authored-by: Ruochen Pan <badbatch0x01@gmail.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: RC-CHN <1051989940@qq.com>
2026-05-13 11:50:06 +08:00

164 lines
5.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Tests for send_message_to_user session handling."""
from types import SimpleNamespace
from unittest.mock import AsyncMock
import pytest
from astrbot.core.tools.message_tools import SendMessageToUserTool
def _make_context(
current_session="feishu:GroupMessage:oc_xxx",
role="admin",
require_admin=True,
):
"""Build a minimal ContextWrapper for SendMessageToUserTool."""
cfg = {"provider_settings": {"computer_use_require_admin": require_admin}}
return SimpleNamespace(
context=SimpleNamespace(
event=SimpleNamespace(
unified_msg_origin=current_session,
role=role,
get_sender_id=lambda: "user-1",
),
context=SimpleNamespace(
get_config=lambda umo: cfg,
send_message=AsyncMock(),
),
)
)
@pytest.mark.asyncio
async def test_send_message_with_full_three_part_session():
"""LLM passes a complete three-part session string."""
tool = SendMessageToUserTool()
ctx = _make_context(current_session="feishu:GroupMessage:oc_aaa")
result = await tool.call(
ctx,
messages=[{"type": "plain", "text": "hello"}],
session="feishu:GroupMessage:oc_aaa",
)
assert "Message sent to session" in result
@pytest.mark.asyncio
async def test_send_message_with_partial_session_id_fallback():
"""LLM passes only session_id (no colons) — fallback to current_session's prefix."""
tool = SendMessageToUserTool()
ctx = _make_context(current_session="feishu:GroupMessage:oc_abc")
result = await tool.call(
ctx,
messages=[{"type": "plain", "text": "hello"}],
session="oc_abc",
)
assert "Message sent to session" in result
# Verify the target session was reconstructed with current_session's platform/msg_type
call_args = ctx.context.context.send_message.call_args
target_session = call_args[0][0]
assert target_session.platform_id == "feishu"
assert target_session.message_type.value == "GroupMessage"
assert target_session.session_id == "oc_abc"
@pytest.mark.asyncio
async def test_send_message_defaults_to_current_session():
"""LLM does not pass session — uses current_session directly."""
tool = SendMessageToUserTool()
ctx = _make_context(current_session="feishu:GroupMessage:oc_xxx")
result = await tool.call(
ctx,
messages=[{"type": "plain", "text": "hello"}],
)
assert "Message sent to session" in result
call_args = ctx.context.context.send_message.call_args
target_session = call_args[0][0]
assert str(target_session) == "feishu:GroupMessage:oc_xxx"
@pytest.mark.asyncio
async def test_send_message_partial_session_falls_back_to_current():
"""LLM passes session_id matching current_session's id — same session, just incomplete format."""
tool = SendMessageToUserTool()
ctx = _make_context(current_session="qq_official:GroupMessage:g123")
result = await tool.call(
ctx,
messages=[{"type": "plain", "text": "world"}],
session="g123",
)
assert "Message sent to session" in result
call_args = ctx.context.context.send_message.call_args
target_session = call_args[0][0]
assert target_session.platform_id == "qq_official"
assert target_session.message_type.value == "GroupMessage"
assert target_session.session_id == "g123"
@pytest.mark.asyncio
async def test_cron_context_current_session_is_target_session():
"""在 cron 场景中current_session 就是 cron 任务的目标 session。
cron 是主动唤醒,没有用户消息触发,因此没有"正在聊天的 session"
event.unified_msg_origin 来自 CronMessageEvent.session
而 CronMessageEvent.session 来自 cron job payload.session
即用户在 cron 配置中填写的目标会话。
"""
tool = SendMessageToUserTool()
# cron 任务的目标 session用户配置的完整三段式
cron_target_session = "feishu:GroupMessage:oc_cron_target"
ctx = _make_context(current_session=cron_target_session)
# LLM 在 cron 上下文中只传了 session_id 部分
result = await tool.call(
ctx,
messages=[{"type": "plain", "text": "cron message"}],
session="oc_cron_target",
)
assert "Message sent to session" in result
call_args = ctx.context.context.send_message.call_args
target_session = call_args[0][0]
# 补全后的 session 应与 cron 目标 session 完全一致
assert str(target_session) == cron_target_session
assert target_session.platform_id == "feishu"
assert target_session.message_type.value == "GroupMessage"
assert target_session.session_id == "oc_cron_target"
@pytest.mark.asyncio
async def test_send_message_empty_messages_returns_error():
"""Empty or missing messages returns error before session resolution."""
tool = SendMessageToUserTool()
ctx = _make_context()
result = await tool.call(ctx, messages=[], session="oc_xxx")
assert "error:" in result
assert "messages" in result.lower()
@pytest.mark.asyncio
async def test_send_message_missing_image_path_stops_before_send(tmp_path, monkeypatch):
"""Missing image paths fail before sending any message components."""
tool = SendMessageToUserTool()
ctx = _make_context()
missing_image_path = tmp_path / "missing.png"
async def mock_get_booter(*args, **kwargs):
del args, kwargs
raise RuntimeError("sandbox unavailable")
monkeypatch.setattr(
"astrbot.core.tools.message_tools.get_booter",
mock_get_booter,
)
result = await tool.call(
ctx,
messages=[
{"type": "plain", "text": "before image"},
{"type": "image", "path": str(missing_image_path)},
],
)
assert "error: failed to build messages[1] component: sandbox unavailable" in result
ctx.context.context.send_message.assert_not_called()