mirror of
https://github.com/snowy-yang/qqbotsdk.git
synced 2026-10-21 10:08:50 +00:00
feat!: api 拆包、心跳迁入传输层、限流重试与后台 handler
This commit is contained in:
1 parent
750ed81d96
commit
3237558128
30 files changed
+1145
-714
No files matched your search
+96
-11
@@ -1,28 +1,27 @@
|
||||
import asyncio
|
||||
|
||||
import pytest
|
||||
|
||||
from qqbotsdk.config import Config
|
||||
from qqbotsdk.emitter import EventEmitter
|
||||
from qqbotsdk.model import Opcode, payload_of
|
||||
from qqbotsdk.payloads import GroupAtMessage
|
||||
from qqbotsdk.queue import EventQueue
|
||||
from qqbotsdk.session import Session
|
||||
from qqbotsdk.token import AccessToken
|
||||
from qqbotsdk.ws_protocol import WebsocketProtocol
|
||||
|
||||
|
||||
def test_sequence_only_moves_forward():
|
||||
s = Session()
|
||||
assert s.sequence_id is None
|
||||
assert s.seq == 0
|
||||
s.update_sequence(5)
|
||||
s.update_sequence(3) # 回退被忽略
|
||||
assert s.sequence_id == 5
|
||||
assert s.seq == 5
|
||||
s.update_sequence(7)
|
||||
assert s.sequence_id == 7
|
||||
assert s.seq == 7
|
||||
|
||||
|
||||
def test_seq_defaults_to_zero():
|
||||
s = Session()
|
||||
assert s.seq == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handler_exception_is_isolated():
|
||||
queue = EventQueue()
|
||||
@@ -96,7 +95,93 @@ async def test_unregistered_annotation_falls_back_to_raw_dict():
|
||||
async def handler(dep: Session): # Session 未登记进 services
|
||||
received.append(dep)
|
||||
|
||||
await emitter.emit(
|
||||
{"op": Opcode.DISPATCH, "t": "READY", "d": {"session_id": "s1"}}
|
||||
)
|
||||
await emitter.emit({"op": Opcode.DISPATCH, "t": "READY", "d": {"session_id": "s1"}})
|
||||
assert received == [{"session_id": "s1"}]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_scalar_d_falls_back_to_raw_value():
|
||||
"""回归:op=9 INVALID_SESSION 的 d 是布尔值,DI 回退必须传原始 d。
|
||||
此前实现把 d dict 化成 {}(恒 falsy),resumable=true 也被当成不可续传。
|
||||
走真实协议层 handler 验证路由 → 注入 → 会话状态的全链路。"""
|
||||
config = Config(
|
||||
app_id="a",
|
||||
app_secret="s",
|
||||
base_url="https://x",
|
||||
timeout=None, # type: ignore[arg-type]
|
||||
connecter="websocket",
|
||||
intents_file="no-such.toml",
|
||||
webhook_host="0.0.0.0",
|
||||
webhook_port=8080,
|
||||
webhook_path="/x",
|
||||
)
|
||||
emitter = EventEmitter()
|
||||
session = Session()
|
||||
session.session_id = "S1"
|
||||
emitter.register_protocol(
|
||||
WebsocketProtocol(config, session, AccessToken(config, None)) # type: ignore[arg-type]
|
||||
)
|
||||
seen: list[bool] = []
|
||||
|
||||
@emitter.on(Opcode.INVALID_SESSION)
|
||||
async def spy(resumable: bool):
|
||||
seen.append(resumable)
|
||||
|
||||
await emitter.emit({"op": Opcode.INVALID_SESSION, "d": True})
|
||||
assert seen == [True]
|
||||
assert session.session_id == "S1" # 可续传,会话保留
|
||||
|
||||
await emitter.emit({"op": Opcode.INVALID_SESSION, "d": False})
|
||||
assert seen == [True, False]
|
||||
assert session.session_id is None # 不可续传,会话清空
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_background_handler_does_not_block_dispatch():
|
||||
"""background=True:慢 handler 进后台任务,不阻塞同一事件的后续 handler。"""
|
||||
emitter = EventEmitter()
|
||||
slow_done = asyncio.Event()
|
||||
quick_done = asyncio.Event()
|
||||
|
||||
@emitter.on("GROUP_AT_MESSAGE_CREATE", background=True)
|
||||
async def slow(msg: GroupAtMessage):
|
||||
await asyncio.sleep(0.05)
|
||||
slow_done.set()
|
||||
|
||||
@emitter.on("GROUP_AT_MESSAGE_CREATE")
|
||||
async def quick(msg: GroupAtMessage):
|
||||
quick_done.set()
|
||||
|
||||
await emitter.emit(
|
||||
{
|
||||
"op": Opcode.DISPATCH,
|
||||
"t": "GROUP_AT_MESSAGE_CREATE",
|
||||
"d": {"content": "hi"},
|
||||
}
|
||||
)
|
||||
# emit 返回时快 handler 已跑完,慢 handler 仍在后台
|
||||
assert quick_done.is_set()
|
||||
assert not slow_done.is_set()
|
||||
await asyncio.wait_for(slow_done.wait(), timeout=2)
|
||||
await asyncio.sleep(0) # 让已完成任务的 done_callback 清理 _bg_tasks
|
||||
await emitter.close()
|
||||
assert not emitter._bg_tasks # 收尾后在飞任务清空
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_close_cancels_inflight_background_tasks():
|
||||
emitter = EventEmitter()
|
||||
cancelled = asyncio.Event()
|
||||
|
||||
@emitter.on("READY", background=True)
|
||||
async def forever():
|
||||
try:
|
||||
await asyncio.sleep(60)
|
||||
except asyncio.CancelledError:
|
||||
cancelled.set()
|
||||
raise
|
||||
|
||||
await emitter.emit({"op": Opcode.DISPATCH, "t": "READY", "d": {}})
|
||||
await asyncio.sleep(0) # 让后台任务实际启动(未启动的任务被 cancel 不会执行函数体)
|
||||
await emitter.close()
|
||||
assert cancelled.is_set()
|
||||
Reference in new issue
Block a user