"""pytest 异步测试:启动 OneBotServer,用内置的模拟 OneBot 客户端验证命令回复。""" import asyncio import json import pytest import pytest_asyncio from aiohttp import ClientSession import bot as bot_mod HOST = "127.0.0.1" PORT = 18765 class FakeOneBotClient: """模拟 OneBot 实现端:连入反向 WS,回执 API 调用并收集发送的消息。""" def __init__(self, session: ClientSession): self.session = session self.ws = None self.replies: list[str] = [] async def connect(self): self.ws = await self.session.ws_connect(f"ws://{HOST}:{PORT}/ws") self._reader = asyncio.create_task(self._read()) async def _read(self): async for msg in self.ws: data = json.loads(msg.data) if data.get("action"): if data["action"] == "get_login_info": result = {"user_id": 88888, "nickname": "bot"} else: result = None if data["action"].startswith("send_"): self.replies.append(data["params"].get("message", "")) await self.ws.send_str(json.dumps( {"status": "ok", "retcode": 0, "data": result, "echo": data.get("echo")})) async def send_message(self, text: str): await self.ws.send_str(json.dumps({ "post_type": "message", "message_type": "private", "user_id": 10000, "message": text, })) async def send_group_message(self, message, group_id: int = 999): """message 可为字符串或 CQ 段数组。""" await self.ws.send_str(json.dumps({ "post_type": "message", "message_type": "group", "group_id": group_id, "user_id": 10000, "message": message, })) async def close(self): self._reader.cancel() await self.ws.close() @pytest_asyncio.fixture async def client(): bot = bot_mod.Bot({ "server": {"host": HOST, "port": PORT, "access_token": ""}, "alert": {"groups": [], "friends": []}, }) await bot.ob.start(bot.on_event) async with ClientSession() as session: fake = FakeOneBotClient(session) await fake.connect() yield fake await fake.close() await bot.ob.stop() async def wait_reply(fake: FakeOneBotClient, timeout: float = 30) -> str: """等待并返回下一条机器人发出的消息。""" for _ in range(int(timeout * 10)): if fake.replies: return fake.replies.pop(0) await asyncio.sleep(0.1) raise TimeoutError("机器人未在超时时间内回复") @pytest.mark.asyncio @pytest.mark.parametrize("cmd,expect", [ ("状态", "服务器状态"), ("top", "TOP10"), ("温度", "温度"), ("网络", "网络速率"), ("帮助", "可用命令"), ("/help", "可用命令"), ("!状态", "服务器状态"), ]) async def test_commands(client, cmd, expect): await client.send_message(cmd) reply = await wait_reply(client) assert expect in reply @pytest.mark.asyncio async def test_status_cpu_display(client): await client.send_message("状态") reply = await wait_reply(client) # Linux 风格总占用 + 核心 assert "核心" in reply and "/" in reply async def _expect_no_reply(client, send, timeout: float = 3.0): """发送后确认机器人在超时内没有回复。""" before = len(client.replies) await send() for _ in range(int(timeout * 10)): if len(client.replies) > before: raise AssertionError(f"不应回复但收到了: {client.replies[-1]}") await asyncio.sleep(0.1) @pytest.mark.asyncio async def test_private_non_command_ignored(client): # 私聊发普通消息/无文本消息不触发 await _expect_no_reply(client, lambda: client.send_message("今天天气怎么样")) await _expect_no_reply( client, lambda: client.send_message( [{"type": "image", "data": {"file": "abc.jpg"}}])) @pytest.mark.asyncio async def test_group_requires_at_me(client): # 群聊不 @ 不响应 await _expect_no_reply(client, lambda: client.send_group_message("/status")) # @ 别人不响应 await _expect_no_reply(client, lambda: client.send_group_message( [{"type": "at", "data": {"qq": "66666"}}, {"type": "text", "data": {"text": " /status"}}])) # @ 本人才响应 await client.send_group_message( [{"type": "at", "data": {"qq": "88888"}}, {"type": "text", "data": {"text": " /status"}}]) reply = await wait_reply(client) assert "服务器状态" in reply