diff --git a/src/astrbot_sdk/context.py b/src/astrbot_sdk/context.py index 16b88fb32..8ad93d92b 100644 --- a/src/astrbot_sdk/context.py +++ b/src/astrbot_sdk/context.py @@ -585,13 +585,13 @@ class Context: ) -> asyncio.Task[Any]: task_desc = str(desc) - async def _await_future(future: asyncio.Future[Any]) -> Any: + async def _wrap_future(future: asyncio.Future[Any]) -> Any: return await future if isinstance(task, asyncio.Task): background_task = task elif asyncio.isfuture(task): - background_task = asyncio.create_task(_await_future(task)) + background_task = asyncio.create_task(_wrap_future(task)) elif asyncio.iscoroutine(task): background_task = asyncio.create_task(task) else: @@ -609,14 +609,6 @@ class Context: return try: done_task.result() - except asyncio.CancelledError: - debug_logger = getattr(self.logger, "debug", None) - if callable(debug_logger): - debug_logger( - "SDK background task cancelled: plugin_id={} desc={}", - self.plugin_id, - task_desc, - ) except Exception: exception_logger = getattr(self.logger, "exception", None) if callable(exception_logger): diff --git a/tests_v4/conftest.py b/tests_v4/conftest.py new file mode 100644 index 000000000..574158965 --- /dev/null +++ b/tests_v4/conftest.py @@ -0,0 +1,13 @@ +from __future__ import annotations + +import sys +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +SRC = ROOT / "src" + +while str(SRC) in sys.path: + sys.path.remove(str(SRC)) + +sys.path.insert(0, str(SRC)) diff --git a/tests_v4/test_context_register_task.py b/tests_v4/test_context_register_task.py new file mode 100644 index 000000000..667b51fc5 --- /dev/null +++ b/tests_v4/test_context_register_task.py @@ -0,0 +1,97 @@ +from __future__ import annotations + +import asyncio + +import pytest + +from astrbot_sdk._testing_support import MockContext + + +class RecordingLogger: + def __init__(self) -> None: + self.debug_calls: list[tuple[str, str, str]] = [] + self.exception_calls: list[tuple[str, str, str]] = [] + + def debug(self, message: str, plugin_id: str, desc: str) -> None: + self.debug_calls.append((message, plugin_id, desc)) + + def exception(self, message: str, plugin_id: str, desc: str) -> None: + self.exception_calls.append((message, plugin_id, desc)) + + +@pytest.mark.asyncio +async def test_register_task_accepts_coroutine() -> None: + ctx = MockContext() + + async def background() -> str: + await asyncio.sleep(0) + return "done" + + task = await ctx.register_task(background(), "coroutine") + + assert isinstance(task, asyncio.Task) + assert await task == "done" + + +@pytest.mark.asyncio +async def test_register_task_wraps_future_inputs() -> None: + ctx = MockContext() + loop = asyncio.get_running_loop() + future: asyncio.Future[str] = loop.create_future() + + task = await ctx.register_task(future, "future") + future.set_result("done") + + assert isinstance(task, asyncio.Task) + assert task is not future + assert await task == "done" + + +@pytest.mark.asyncio +async def test_register_task_logs_cancel_once() -> None: + logger = RecordingLogger() + ctx = MockContext(logger=logger) + started = asyncio.Event() + + async def background() -> None: + started.set() + await asyncio.Future() + + task = await ctx.register_task(background(), "cancelled") + await started.wait() + task.cancel() + + with pytest.raises(asyncio.CancelledError): + await task + + assert logger.debug_calls == [ + ( + "SDK background task cancelled: plugin_id={} desc={}", + "test-plugin", + "cancelled", + ) + ] + assert logger.exception_calls == [] + + +@pytest.mark.asyncio +async def test_register_task_logs_failures() -> None: + logger = RecordingLogger() + ctx = MockContext(logger=logger) + + async def background() -> None: + raise RuntimeError("boom") + + task = await ctx.register_task(background(), "failing") + + with pytest.raises(RuntimeError, match="boom"): + await task + + assert logger.debug_calls == [] + assert logger.exception_calls == [ + ( + "SDK background task failed: plugin_id={} desc={}", + "test-plugin", + "failing", + ) + ]