diff --git a/run_tests.py b/run_tests.py index ef914f0cd..3f779af57 100644 --- a/run_tests.py +++ b/run_tests.py @@ -9,6 +9,7 @@ Usage: python run_tests.py --cov # Run with coverage python run_tests.py -m "not slow" # Skip slow tests """ + from __future__ import annotations import subprocess @@ -30,11 +31,13 @@ def main() -> int: # Handle --cov flag if "--cov" in args: args.remove("--cov") - cmd.extend([ - "--cov=src-new/astrbot_sdk", - "--cov-report=term-missing", - "--cov-report=html:.htmlcov", - ]) + cmd.extend( + [ + "--cov=src-new/astrbot_sdk", + "--cov-report=term-missing", + "--cov-report=html:.htmlcov", + ] + ) # Default flags if no specific args if not args: diff --git a/src-new/astrbot_sdk/protocol/legacy_adapter.py b/src-new/astrbot_sdk/protocol/legacy_adapter.py index 7a99cfd36..63edafbb5 100644 --- a/src-new/astrbot_sdk/protocol/legacy_adapter.py +++ b/src-new/astrbot_sdk/protocol/legacy_adapter.py @@ -13,7 +13,6 @@ from .messages import ( InitializeMessage, InvokeMessage, PeerInfo, - ProtocolMessage, ResultMessage, ) diff --git a/src-new/astrbot_sdk/runtime/peer.py b/src-new/astrbot_sdk/runtime/peer.py index 5fe85357f..d9657f2e4 100644 --- a/src-new/astrbot_sdk/runtime/peer.py +++ b/src-new/astrbot_sdk/runtime/peer.py @@ -27,6 +27,13 @@ CancelHandler = Callable[[str], Awaitable[None]] class Peer: + """表示协议连接中的一个对等端。 + + `Peer` 封装一条双向传输通道上的消息收发、初始化握手、能力调用、 + 流式事件转发与取消处理。这里的 `peer` 指“通信对端/本端”这一网络 + 协议概念,而不是业务上的用户、群聊或会话对象。 + """ + def __init__( self, *, @@ -34,6 +41,13 @@ class Peer: peer_info: PeerInfo, protocol_version: str = "1.0", ) -> None: + """创建一个协议对等端实例。 + + Args: + transport: 底层传输实现,负责发送字符串消息并回调入站消息。 + peer_info: 当前端点对外声明的身份信息。 + protocol_version: 当前端点支持的协议版本,用于初始化握手校验。 + """ self.transport = transport self.peer_info = peer_info self.protocol_version = protocol_version @@ -55,19 +69,24 @@ class Peer: self._remote_initialized = asyncio.Event() def set_initialize_handler(self, handler: InitializeHandler) -> None: + """注册处理远端 `initialize` 请求的握手处理器。""" self._initialize_handler = handler def set_invoke_handler(self, handler: InvokeHandler) -> None: + """注册处理远端 `invoke` 请求的能力调用处理器。""" self._invoke_handler = handler def set_cancel_handler(self, handler: CancelHandler) -> None: + """注册处理远端 `cancel` 请求的取消回调。""" self._cancel_handler = handler async def start(self) -> None: + """启动传输层并将原始入站消息绑定到当前 `Peer`。""" self.transport.set_message_handler(self._handle_raw_message) await self.transport.start() async def stop(self) -> None: + """关闭 `Peer` 并清理所有挂起中的请求、流和入站任务。""" self._closed = True # 终止所有挂起的 RPC,避免调用方永久挂起 for future in list(self._pending_results.values()): @@ -88,9 +107,15 @@ class Peer: await self.transport.stop() async def wait_closed(self) -> None: + """等待底层传输彻底关闭。""" await self.transport.wait_closed() async def wait_until_remote_initialized(self, timeout: float | None = 30.0) -> None: + """等待远端完成初始化握手。 + + Args: + timeout: 等待秒数。传入 `None` 表示无限等待。 + """ if timeout is None: await self._remote_initialized.wait() return @@ -102,6 +127,15 @@ class Peer: *, metadata: dict[str, Any] | None = None, ) -> InitializeOutput: + """向远端发送初始化请求并缓存远端声明的能力信息。 + + Args: + handlers: 当前端点声明可接收的处理器列表。 + metadata: 附带给远端的握手元数据。 + + Returns: + 远端返回的初始化结果。 + """ self._ensure_usable() request_id = self._next_id() future: asyncio.Future[ResultMessage] = ( @@ -141,6 +175,14 @@ class Peer: stream: bool = False, request_id: str | None = None, ) -> dict[str, Any]: + """发起一次非流式能力调用并等待最终结果。 + + Args: + capability: 远端能力名。 + payload: 调用输入。 + stream: 必须为 `False`;流式场景应改用 `invoke_stream()`。 + request_id: 可选的请求 ID;未提供时自动生成。 + """ self._ensure_usable() if stream: raise ValueError("stream=True 请使用 invoke_stream()") @@ -171,6 +213,16 @@ class Peer: *, request_id: str | None = None, ) -> AsyncIterator[EventMessage]: + """发起一次流式能力调用并返回事件迭代器。 + + 调用方会收到 `delta` 事件,`started` 会被内部吞掉, + `completed` 用于结束迭代,`failed` 会转换为异常抛出。 + + Args: + capability: 远端能力名。 + payload: 调用输入。 + request_id: 可选的请求 ID;未提供时自动生成。 + """ self._ensure_usable() request_id = request_id or self._next_id() queue: asyncio.Queue[Any] = asyncio.Queue() @@ -209,17 +261,21 @@ class Peer: return iterator() async def cancel(self, request_id: str, reason: str = "user_cancelled") -> None: + """向远端发送取消请求,尝试中止指定 ID 的在途调用。""" await self._send(CancelMessage(id=request_id, reason=reason)) def _next_id(self) -> str: + """生成当前连接内递增的消息 ID。""" self._counter += 1 return f"msg_{self._counter:04d}" def _ensure_usable(self) -> None: + """确保连接仍处于可用状态,否则立即抛出协议错误。""" if self._unusable: raise AstrBotError.protocol_error("连接已进入不可用状态") async def _handle_raw_message(self, payload: str) -> None: + """解析原始消息并分发到对应的消息处理分支。""" message = parse_message(payload) if isinstance(message, ResultMessage): await self._handle_result(message) @@ -245,6 +301,7 @@ class Peer: return async def _handle_initialize(self, message: InitializeMessage) -> None: + """处理远端发起的初始化握手并返回握手结果。""" self.remote_peer = message.peer self.remote_handlers = message.handlers self.remote_metadata = message.metadata @@ -276,6 +333,7 @@ class Peer: self._remote_initialized.set() async def _handle_invoke(self, message: InvokeMessage) -> None: + """处理远端发起的能力调用,并按流式或非流式协议返回结果。""" active = self._inbound_tasks.get(message.id) token = active[1] if active is not None else CancelToken() try: @@ -320,6 +378,7 @@ class Peer: ) async def _handle_cancel(self, message: CancelMessage) -> None: + """处理远端取消请求并终止对应的入站任务。""" inbound = self._inbound_tasks.get(message.id) if inbound is None: return @@ -330,6 +389,7 @@ class Peer: task.cancel() async def _handle_result(self, message: ResultMessage) -> None: + """处理非流式结果消息并唤醒等待中的调用方。""" future = self._pending_results.pop(message.id, None) if future is None: queue = self._pending_streams.get(message.id) @@ -343,6 +403,7 @@ class Peer: future.set_result(message) async def _handle_event(self, message: EventMessage) -> None: + """处理流式事件消息并投递到对应请求的事件队列。""" queue = self._pending_streams.get(message.id) if queue is None: future = self._pending_results.get(message.id) @@ -356,6 +417,7 @@ class Peer: async def _send_error_result( self, message: InvokeMessage, error: AstrBotError ) -> None: + """根据调用模式,将错误编码为 `result` 或失败事件发回远端。""" if message.stream: await self._send( EventMessage( @@ -376,6 +438,7 @@ class Peer: async def _reject_initialize( self, message: InitializeMessage, error: AstrBotError ) -> None: + """拒绝一次初始化握手,并把连接标记为不可继续使用。""" await self._send( ResultMessage( id=message.id, @@ -389,8 +452,10 @@ class Peer: await self.stop() async def _send_cancelled_termination(self, message: InvokeMessage) -> None: + """把本端取消执行转换为标准化的取消错误响应。""" error = AstrBotError.cancelled() await self._send_error_result(message, error) async def _send(self, message) -> None: + """序列化协议消息并通过底层传输发送出去。""" await self.transport.send(message.model_dump_json(exclude_none=True)) diff --git a/src/astrbot_sdk/tests/test_supervisor.py b/src/astrbot_sdk/tests/test_supervisor.py index 27eab71ae..379080c2b 100644 --- a/src/astrbot_sdk/tests/test_supervisor.py +++ b/src/astrbot_sdk/tests/test_supervisor.py @@ -1,6 +1,5 @@ from __future__ import annotations -import asyncio import tempfile import unittest from pathlib import Path diff --git a/tests_v4/conftest.py b/tests_v4/conftest.py index e6f86ecc5..187c878e7 100644 --- a/tests_v4/conftest.py +++ b/tests_v4/conftest.py @@ -8,6 +8,7 @@ import pytest # 将 src-new 加入路径 - 这使得测试可以运行,但不算"已安装" import sys + SRC_NEW_PATH = str(Path(__file__).parent.parent / "src-new") sys.path.insert(0, SRC_NEW_PATH) @@ -16,6 +17,7 @@ sys.path.insert(0, SRC_NEW_PATH) # Async Configuration # ============================================================ + @pytest.fixture(scope="session") def event_loop() -> Generator[asyncio.AbstractEventLoop, None, None]: """Create an event loop for async tests.""" @@ -29,6 +31,7 @@ def event_loop() -> Generator[asyncio.AbstractEventLoop, None, None]: # Transport Fixtures # ============================================================ + class MemoryTransport: """In-memory transport for testing peer communication.""" @@ -74,6 +77,7 @@ def transport_pair() -> tuple[MemoryTransport, MemoryTransport]: # Mock/Fake Fixtures # ============================================================ + class FakeEnvManager: """Fake environment manager for testing.""" diff --git a/tests_v4/test_api_decorators.py b/tests_v4/test_api_decorators.py index 339b45e39..82fd82740 100644 --- a/tests_v4/test_api_decorators.py +++ b/tests_v4/test_api_decorators.py @@ -1,6 +1,7 @@ """ Unit tests for API decorators and Star class. """ + from __future__ import annotations import pytest @@ -167,6 +168,7 @@ class TestStarClass: def test_star_is_new_star_by_default(self): """Star subclasses should be recognized as new-style.""" + class MyPlugin(Star): pass @@ -174,6 +176,7 @@ class TestStarClass: def test_star_collects_handler_names_from_decorators(self): """Star should collect decorated method names in __handlers__.""" + class MyPlugin(Star): @on_command("hello") async def hello(self, event: MessageEvent, ctx: Context): diff --git a/tests_v4/test_api_event_filter.py b/tests_v4/test_api_event_filter.py index da38ad266..75fe34187 100644 --- a/tests_v4/test_api_event_filter.py +++ b/tests_v4/test_api_event_filter.py @@ -1,9 +1,9 @@ """ Tests for api/event/filter.py - Event filter decorators and utilities. """ + from __future__ import annotations -import pytest from astrbot_sdk.api.event.filter import ADMIN, command, filter, permission, regex from astrbot_sdk.decorators import get_handler_meta diff --git a/tests_v4/test_api_legacy_context.py b/tests_v4/test_api_legacy_context.py index f10eda757..99752b50f 100644 --- a/tests_v4/test_api_legacy_context.py +++ b/tests_v4/test_api_legacy_context.py @@ -1,9 +1,10 @@ """ Tests for _legacy_api.py - Legacy compatibility layer. """ + from __future__ import annotations -from unittest.mock import AsyncMock, MagicMock, patch +from unittest.mock import AsyncMock, MagicMock import pytest diff --git a/tests_v4/test_api_modules.py b/tests_v4/test_api_modules.py index 2158244f7..27d577513 100644 --- a/tests_v4/test_api_modules.py +++ b/tests_v4/test_api_modules.py @@ -1,9 +1,9 @@ """ Tests for API module exports and re-exports. """ + from __future__ import annotations -import pytest class TestApiStarModule: diff --git a/tests_v4/test_conftest_fixtures.py b/tests_v4/test_conftest_fixtures.py index 783c24a73..2c00ad51a 100644 --- a/tests_v4/test_conftest_fixtures.py +++ b/tests_v4/test_conftest_fixtures.py @@ -3,15 +3,19 @@ Pytest-based tests for transport and peer communication. These tests demonstrate the pytest fixtures defined in conftest.py. """ + from __future__ import annotations import pytest -from astrbot_sdk.context import CancelToken from astrbot_sdk.errors import AstrBotError from astrbot_sdk.protocol.descriptors import CapabilityDescriptor -from astrbot_sdk.protocol.messages import EventMessage, InitializeOutput, PeerInfo, ResultMessage -from astrbot_sdk.runtime.capability_router import CapabilityRouter, StreamExecution +from astrbot_sdk.protocol.messages import ( + EventMessage, + PeerInfo, + ResultMessage, +) +from astrbot_sdk.runtime.capability_router import CapabilityRouter from astrbot_sdk.runtime.peer import Peer @@ -98,7 +102,6 @@ class TestProtocolErrors: async def test_stream_true_receiving_result_is_error(self, transport_pair): """stream=true receiving result should raise protocol error.""" - import asyncio left, right = transport_pair @@ -113,7 +116,9 @@ class TestProtocolErrors: stream = await plugin.invoke_stream( "llm.stream_chat", {"prompt": "bad"}, request_id="stream-1" ) - await left.send(ResultMessage(id="stream-1", success=True, output={}).model_dump_json()) + await left.send( + ResultMessage(id="stream-1", success=True, output={}).model_dump_json() + ) with pytest.raises(AstrBotError) as exc_info: async for _ in stream: @@ -134,9 +139,7 @@ class TestCapabilityRouter: invalid_names = ["llm", "llm.chat.extra", "LLM.chat", "llm.Chat"] for name in invalid_names: with pytest.raises(ValueError) as exc_info: - router.register( - CapabilityDescriptor(name=name, description="invalid") - ) + router.register(CapabilityDescriptor(name=name, description="invalid")) assert name in str(exc_info.value) def test_reserved_namespaces_rejected_for_exposed(self): @@ -146,9 +149,7 @@ class TestCapabilityRouter: reserved_names = ["handler.demo", "system.health", "internal.trace"] for name in reserved_names: with pytest.raises(ValueError) as exc_info: - router.register( - CapabilityDescriptor(name=name, description="reserved") - ) + router.register(CapabilityDescriptor(name=name, description="reserved")) assert name in str(exc_info.value) def test_reserved_namespaces_allowed_for_hidden(self): diff --git a/tests_v4/test_context.py b/tests_v4/test_context.py index 6ebabd4a3..5b6c5f9a1 100644 --- a/tests_v4/test_context.py +++ b/tests_v4/test_context.py @@ -1,6 +1,7 @@ """ Unit tests for Context module. """ + from __future__ import annotations import asyncio diff --git a/tests_v4/test_decorators.py b/tests_v4/test_decorators.py index cceff5691..2edf7382d 100644 --- a/tests_v4/test_decorators.py +++ b/tests_v4/test_decorators.py @@ -1,9 +1,9 @@ """ Tests for decorators.py - Handler decorator infrastructure. """ + from __future__ import annotations -import pytest from astrbot_sdk.decorators import ( HANDLER_META_ATTR, @@ -57,6 +57,7 @@ class TestGetHandlerMeta: def test_returns_none_for_undecorated_function(self): """get_handler_meta should return None for undecorated functions.""" + async def plain_function(): pass @@ -212,6 +213,7 @@ class TestOnEventDecorator: event_types = ["message_received", "user_joined", "custom_event"] for event_type in event_types: + @on_event(event_type) async def handler(): pass diff --git a/tests_v4/test_entrypoints.py b/tests_v4/test_entrypoints.py index da0e7fd66..76d529f81 100644 --- a/tests_v4/test_entrypoints.py +++ b/tests_v4/test_entrypoints.py @@ -28,7 +28,7 @@ def _is_astrbot_sdk_installed_in_site_packages() -> bool: @pytest.mark.integration @pytest.mark.skipif( not _is_astrbot_sdk_installed_in_site_packages(), - reason="astrbot_sdk not installed in site-packages (run: pip install -e .)" + reason="astrbot_sdk not installed in site-packages (run: pip install -e .)", ) class EntryPointTest(unittest.TestCase): def test_import_package(self) -> None: diff --git a/tests_v4/test_events.py b/tests_v4/test_events.py index eb1f874f6..6872cc7f7 100644 --- a/tests_v4/test_events.py +++ b/tests_v4/test_events.py @@ -1,9 +1,9 @@ """ Unit tests for Events module. """ + from __future__ import annotations -import asyncio import pytest