fix: simplify register_task completion handling (#27)

* fix: simplify register_task completion handling

Remove duplicated cancellation logging in Context.register_task while keeping Future inputs compatible with asyncio.create_task semantics. Add regression coverage for coroutine, Future, cancellation, and failure paths.

* fix: prioritize local src in tests_v4

Ensure tests_v4 always imports the working tree package by moving src to sys.path[0] even when another checkout or installed copy is already present.
This commit is contained in:
letr
2026-03-19 00:22:35 +08:00
committed by GitHub
parent e76a588818
commit c7d47add23
3 changed files with 112 additions and 10 deletions

View File

@@ -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):

13
tests_v4/conftest.py Normal file
View File

@@ -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))

View File

@@ -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",
)
]