Files
AstrBot/tests/unit/test_aiocqhttp_reply.py
Weilong Liao 0d8e8682db refactor(core): migrate backend backbone from Quart to FastAPI and introduce more OpenAPI (#8688)
* refactor: migrate to fastapi

* structure refactor

* fix: pyright fix

* refactor: improve error handling and public messages in plugin services

* feat(api): refactor API client integration and enhance request handling

- Updated API client configuration to use a dedicated HTTP client.
- Introduced utility functions for generating options, queries, and form data for API requests.
- Refactored multiple API methods to utilize the new utility functions for improved consistency and readability.
- Renamed types for clarity and updated import statements accordingly.

feat(docs): add script to update OpenAPI JSON from YAML spec

- Created a Python script to convert OpenAPI YAML specification to JSON format.
- The script supports customizable input and output paths.
- Ensured the script handles directory creation for output paths and validates the YAML structure.

* fix

* feat(auth): implement rate limiting for v1 login endpoint and enhance request handling

* Refactor dashboard API routers to use legacy_router for backward compatibility

- Changed all instances of dashboard_router to legacy_router across multiple API modules including platform, plugins, providers, sessions, skills, stats, subagents, t2i, tools, updates, and asgi_runtime.
- Updated route definitions to ensure existing endpoints remain functional under the new router structure.
- Introduced support for Quart request context in asgi_runtime to enhance compatibility with existing Quart-based plugins.
- Added a test case to validate the functionality of the new Quart request context handling in plugin extensions.

* chore: remove cli test

* fix: update dashboard tests for fastapi migration

* chore: satisfy ruff checks

* fix: update openapi api key scopes

* fix: sync config scope chip selection

* fix: restore quart dependency

* docs: clarify quart plugin api compatibility

* docs: update openapi scope documentation

* fix: use singular skill openapi scope

* fix: hide update service exception details

* fix: address fastapi review comments

* fix: address dashboard review findings

* docs: revert unrelated package deployment changes

* docs: update agent api generation guidance

* feat: add plugin page web api helpers

* docs: add plugin page bridge demo

* fix: type plugin upload files

* fix: stabilize plugin page uploads

* fix: type plugin web request proxy

* docs: remove plugin page docs example

* fix: authenticate plugin page SSE bridge
2026-06-14 15:03:26 +08:00

199 lines
6.8 KiB
Python
Raw Permalink 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.
"""测试 aiocqhttp 平台中私聊环境下引用回复Reply的消息发送行为。
Bug 背景在私聊中引用上文消息时OneBot 协议端返回
ActionFailed status='failed', retcode=100, wording='message not found'
根因Reply.toDict() 继承了 BaseMessageComponent.toDict()
会将所有非 None 的默认字段chain, sender_id, qq, seq 等)序列化到
OneBot 协议的 message 数组中。OneBot V11 标准只期望
{"type": "reply", "data": {"id": "..."}},多余的字段可能导致
协议端napcat/Lagrange查找消息时失败。
"""
from unittest.mock import AsyncMock
import pytest
import astrbot.core.message.components as Comp
from astrbot.core.message.message_event_result import MessageChain
from astrbot.core.pipeline.respond.stage import (
RespondStage, # noqa: F401 — 预加载避免循环导入
)
from astrbot.core.platform.sources.aiocqhttp.aiocqhttp_message_event import (
AiocqhttpMessageEvent,
)
# ============================================================
# Reply.toDict() 输出格式测试
# ============================================================
def test_reply_to_dict_contains_only_id_in_data():
"""Reply.toDict() 应当只输出 id 字段,不含 chain、sender_id 等多余字段。
当前实际行为:继承了 BaseMessageComponent.toDict(),会将所有
非 None 的默认值chain: [], sender_id: 0, qq: 0, seq: 0 等)
一起序列化,违反了 OneBot V11 的 reply 段格式约定。
"""
reply = Comp.Reply(id="123456")
result = reply.toDict()
assert result["type"] == "reply"
assert "id" in result["data"]
# 这些字段不应出现在 OneBot 协议的 reply segment 中
unexpectedFields = ["chain", "sender_id", "qq", "seq", "text"]
for field in unexpectedFields:
if field in result["data"]:
pytest.fail(
f"Reply.toDict() 的 data 中不应包含 '{field}' 字段,"
f"但实际输出了 {field}={result['data'][field]!r}"
f"完整输出: {result}"
)
def test_reply_to_dict_outputs_only_id():
"""Reply.toDict() 应当只输出 id 字段,不含任何多余字段。"""
reply = Comp.Reply(id="123456")
result = reply.toDict()
assert result["type"] == "reply"
assert set(result["data"].keys()) == {"id"}, (
f"Reply.toDict() data 中包含多余字段: {set(result['data'].keys()) - {'id'}}"
)
assert result["data"]["id"] == "123456"
# ============================================================
# _parse_onebot_json 输出测试
# ============================================================
@pytest.mark.asyncio
async def test_parse_onebot_json_reply_produces_extra_fields():
"""_parse_onebot_json 处理 Reply 时会输出多余字段。
这验证了 bug 的链路:从 Reply 组件 → _parse_onebot_json →
OneBot 协议 payload多余字段一直传递到 send_private_msg。
"""
chain = MessageChain([Comp.Reply(id="123456"), Comp.Plain("你好")])
data = await AiocqhttpMessageEvent._parse_onebot_json(chain)
assert len(data) == 2
replySegment = data[0]
assert replySegment["type"] == "reply"
# 检查 reply 段的 data 中是否有多余字段
extraFields = [k for k in replySegment["data"] if k != "id"]
if extraFields:
pytest.fail(
f"_parse_onebot_json 输出的 reply 段包含了多余的 data 字段: "
f"{extraFields}。这些字段可能被 OneBot 协议端误解析,"
f"导致 message not found 错误。\n"
f"完整 reply 段: {replySegment}"
)
# ============================================================
# 私聊发送路径测试
# ============================================================
@pytest.mark.asyncio
async def test_send_private_msg_with_reply_includes_extra_fields():
"""验证私聊发送带 Reply 的消息时,实际传给 bot.send_private_msg 的
payload 包含多余字段。
这是导致私聊下 'message not found' 的直接原因:
OneBot 协议端收到的 reply 段数据不符合标准格式。
"""
bot = AsyncMock()
chain = MessageChain([Comp.Reply(id="123456"), Comp.Plain("你好")])
await AiocqhttpMessageEvent.send_message(
bot=bot,
message_chain=chain,
event=None,
is_group=False, # 私聊
session_id="987654",
)
# 验证调用了 send_private_msg而非 send_group_msg
bot.send_private_msg.assert_awaited_once()
callArgs = bot.send_private_msg.call_args
assert callArgs.kwargs["user_id"] == 987654
messages = callArgs.kwargs["message"]
assert len(messages) >= 1
replySegment = messages[0]
assert replySegment["type"] == "reply"
# 检查 payload 中的多余字段
extraFields = [k for k in replySegment["data"] if k != "id"]
if extraFields:
pytest.fail(
f"send_private_msg 的 message[0] reply 段包含了多余的 data 字段: "
f"{extraFields}\n"
f"这是导致私聊引用回复报 'message not found' 的根因。\n"
f"完整 payload: {messages}"
)
@pytest.mark.asyncio
async def test_send_group_msg_with_reply_also_includes_extra_fields():
"""对比:群聊发送带 Reply 的消息同样包含多余字段。
如果群聊引用回复正常而私聊失败,可能的原因是不同协议端
对多余字段的容忍度不同(例如 napcat 在 send_group_msg 中
忽略了多余字段,但在 send_private_msg 中严格校验)。
"""
bot = AsyncMock()
chain = MessageChain([Comp.Reply(id="123456"), Comp.Plain("你好")])
await AiocqhttpMessageEvent.send_message(
bot=bot,
message_chain=chain,
event=None,
is_group=True,
session_id="123456",
)
bot.send_group_msg.assert_awaited_once()
callArgs = bot.send_group_msg.call_args
messages = callArgs.kwargs["message"]
replySegment = messages[0]
assert replySegment["type"] == "reply"
extraFields = [k for k in replySegment["data"] if k != "id"]
if extraFields:
pytest.fail(
f"send_group_msg 的 reply 段也包含多余字段: {extraFields}\n"
f"完整 payload: {messages}"
)
# ============================================================
# Reply 组件只传 id 时的正确 OneBot 格式测试
# ============================================================
def test_reply_to_dict_matches_onebot_v11_format():
"""OneBot V11 标准 reply 段格式:
{"type": "reply", "data": {"id": "..."}}
"""
expected = {
"type": "reply",
"data": {"id": "123456"},
}
reply = Comp.Reply(id="123456")
actual = reply.toDict()
assert actual == expected, (
f"Reply.toDict() 输出不符合 OneBot V11 标准。\n期望: {expected}\n实际: {actual}"
)