Files
xiaonuan/tests/test_onebot.py
T
snowy-yang ea83b5df4b OneBot 11 适配器:反向 WS 接入 QQ(NapCat/Lagrange)
- app/onebot.py: /onebot/v11/ws 反向 WS;私聊全回、群聊仅回应 @她 并自动 @ 回去
- CQ 码字符串与消息段数组双格式解析;会话 id ob-<self_id>-p/g-<目标> 与 TUI 隔离
- action echo 配对调用(send_private_msg / send_group_msg),retcode==0 才落历史
- remember_later 的推送(事件确认/关怀/连续天数/身世)自动转成 QQ 消息
- 主动消息路由:push_proactive 优先 TUI、ob- 会话走 QQ,均不在线保持 pending
- 主干重构:chat_turn() 供 TUI 与 OneBot 共用;助手消息由传输层确认送达后落库
- 可选 OB_ACCESS_TOKEN 令牌校验;8 个新测试(回复/CQ剥离/群@/通知转换/主动推送/令牌)
2026-09-29 20:27:13 +08:00

141 lines
5.7 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.
"""OneBot 11 适配器测试(TestClient 模拟 OneBot 实现的反向 WS)"""
import json
import time
import pytest
from starlette.websockets import WebSocketDisconnect
from app import onebot
@pytest.fixture(autouse=True)
def clean_conn():
onebot.connections.clear()
yield
onebot.connections.clear()
def _msg_event(user_id=123, message="你好呀", message_type="private",
group_id=None, self_id=10000):
ev = {"time": 0, "self_id": self_id, "post_type": "message",
"message_type": message_type,
"sub_type": "friend" if message_type == "private" else "normal",
"message_id": 1, "user_id": user_id, "message": message,
"raw_message": str(message),
"sender": {"user_id": user_id, "nickname": "tester"}}
if group_id:
ev["group_id"] = group_id
return ev
def _drain_action(ws):
"""收一个 action 并替 OneBot 实现回一个成功 echo,返回该 action"""
action = json.loads(ws.receive_text())
ws.send_text(json.dumps({"status": "ok", "retcode": 0, "data": {},
"echo": action["echo"]}))
return action
def test_private_reply(client, route_sig, fdb):
route_sig({})
with client.websocket_connect("/onebot/v11/ws") as ws:
ws.send_text(json.dumps(_msg_event(message="你好呀")))
action = _drain_action(ws)
assert action["action"] == "send_private_msg"
assert action["params"]["user_id"] == 123
assert "回声模式" in action["params"]["message"]
assert "你好呀" in action["params"]["message"]
# 会话映射注册成功
assert "ob-10000-p123" in onebot.connections[10000].targets
time.sleep(0.3)
# 对话进了历史(user + assistant)
assert len(fdb.history) == 2
def test_cq_code_stripped(client, route_sig, fdb):
route_sig({})
with client.websocket_connect("/onebot/v11/ws") as ws:
ws.send_text(json.dumps(
_msg_event(message="[CQ:face,id=1]你叫什么名字")))
action = _drain_action(ws)
assert "你叫什么名字" in action["params"]["message"]
def test_group_requires_at(client, route_sig, fdb):
route_sig({})
with client.websocket_connect("/onebot/v11/ws") as ws:
# 群里没 @ 她 -> 不回应
ws.send_text(json.dumps(_msg_event(
message_type="group", group_id=777, message="大家好啊")))
# 紧跟一条私聊 -> 第一条收到的 action 应该是私聊回复
ws.send_text(json.dumps(_msg_event(message="私聊一下")))
action = _drain_action(ws)
assert action["action"] == "send_private_msg"
assert fdb.history[0]["session"] == "ob-10000-p123"
def test_group_at_replies_with_at(client, route_sig, fdb):
route_sig({})
with client.websocket_connect("/onebot/v11/ws") as ws:
ws.send_text(json.dumps(_msg_event(
message_type="group", group_id=777,
message=[{"type": "at", "data": {"qq": 10000}},
{"type": "text", "data": {"text": " 在吗"}}])))
action = _drain_action(ws)
assert action["action"] == "send_group_msg"
assert action["params"]["group_id"] == 777
assert action["params"]["message"].startswith("[CQ:at,qq=123]")
assert "回声模式" in action["params"]["message"]
# 群会话独立于私聊
sessions = {h["session"] for h in fdb.history}
assert "ob-10000-g777" in sessions
def test_notify_event_becomes_qq_message(client, route_sig, fdb):
from datetime import datetime, timedelta
route_sig({"events": [{"kind": "alarm",
"fire_at": datetime.now() + timedelta(hours=14),
"text": "起床", "repeat": "daily"}]})
with client.websocket_connect("/onebot/v11/ws") as ws:
ws.send_text(json.dumps(_msg_event(message="每天早上7点叫我起床")))
a1 = _drain_action(ws) # 回复
assert "回声模式" in a1["params"]["message"]
a2 = _drain_action(ws) # 事件确认 -> QQ 消息
assert a2["action"] == "send_private_msg"
assert "我记下了" in a2["params"]["message"]
assert "每天都算" in a2["params"]["message"]
assert len(fdb.events) == 1
def test_streak_pushed_proactively_via_onebot(client, route_sig, fdb):
route_sig({})
from datetime import datetime, timedelta
import asyncio
yesterday = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d")
# 用 FakeDB 直接种下"昨天来过、已连续 4 天" -> 这次该庆祝第 5 天
asyncio.run(fdb.set_meta("ob-10000-p123", "last_active", yesterday))
asyncio.run(fdb.set_meta("ob-10000-p123", "streak", "4"))
with client.websocket_connect("/onebot/v11/ws") as ws:
ws.send_text(json.dumps(_msg_event(message="我来了")))
actions = [_drain_action(ws), _drain_action(ws)]
texts = [a["params"]["message"] for a in actions]
assert any("连续陪伴的第 5 天" in t for t in texts), texts
assert any("回声模式" in t for t in texts), texts
def test_access_token_enforced(client, route_sig, monkeypatch):
monkeypatch.setattr(onebot, "ACCESS_TOKEN", "secret")
with pytest.raises(WebSocketDisconnect):
with client.websocket_connect("/onebot/v11/ws") as ws:
ws.receive_text()
with client.websocket_connect("/onebot/v11/ws?access_token=secret") as ws:
ws.send_text(json.dumps(_msg_event(message="在吗")))
action = _drain_action(ws)
assert "回声模式" in action["params"]["message"]
def test_disconnected_target_returns_false(client, route_sig):
import asyncio
route_sig({})
assert asyncio.run(onebot.send_to_session("ob-x-p1", "hi")) is False