diff --git a/astrbot/_internal/protocols/lsp/client.py b/astrbot/_internal/protocols/lsp/client.py index 74d7a7180..18d0dc265 100644 --- a/astrbot/_internal/protocols/lsp/client.py +++ b/astrbot/_internal/protocols/lsp/client.py @@ -7,6 +7,7 @@ that provide language intelligence features (completions, diagnostics, etc.). from __future__ import annotations +import asyncio import json from typing import Any @@ -35,8 +36,7 @@ class AstrbotLspClient(BaseAstrbotLspClient): self._pending_requests: dict[int, Any] = {} self._request_id = 0 self._server_command: list[str] | None = None - # anyio TaskGroup handle for background readers - self._task_group: Any | None = None + self._reader_task: asyncio.Task[None] | None = None @property def connected(self) -> bool: @@ -77,11 +77,8 @@ class AstrbotLspClient(BaseAstrbotLspClient): self._server_command = command self._connected = True - # Start reading responses in background using anyio TaskGroup - # Create and enter a TaskGroup so the reader runs until we close it at shutdown. - self._task_group = anyio.create_task_group() - await self._task_group.__aenter__() - self._task_group.start_soon(self._read_responses) + # Start reading responses in the background. + self._reader_task = asyncio.create_task(self._read_responses()) # Send initialize request await self.send_request( @@ -217,13 +214,13 @@ class AstrbotLspClient(BaseAstrbotLspClient): """Shutdown the LSP client.""" self._connected = False - if self._task_group: + if self._reader_task: + self._reader_task.cancel() try: - # Exit the TaskGroup, which cancels background tasks started within it - await self._task_group.__aexit__(None, None, None) - except anyio.get_cancelled_exc_class(): + await self._reader_task + except asyncio.CancelledError: pass - self._task_group = None + self._reader_task = None if self._server_process: try: diff --git a/tests/integration/test_lsp_integration.py b/tests/integration/test_lsp_integration.py index 17055acf7..8716227da 100644 --- a/tests/integration/test_lsp_integration.py +++ b/tests/integration/test_lsp_integration.py @@ -6,11 +6,17 @@ Tests the LSP client against a real LSP server fixture. from __future__ import annotations -import os +import sys +from pathlib import Path + +import anyio import pytest from astrbot._internal.protocols.lsp.client import AstrbotLspClient +TEST_DIR = Path(__file__).resolve().parent +SERVER_PATH = TEST_DIR / "fixtures" / "echo_lsp_server.py" + @pytest.mark.anyio async def test_lsp_client_initialization(): @@ -25,11 +31,8 @@ async def test_lsp_client_connect_to_echo_server(): """Test LSP client can connect to echo LSP server.""" client = AstrbotLspClient() - test_dir = os.path.dirname(os.path.abspath(__file__)) - server_path = os.path.join(test_dir, "fixtures", "echo_lsp_server.py") - await client.connect_to_server( - command=["python", server_path], + command=[sys.executable, str(SERVER_PATH)], workspace_uri="file:///tmp", ) @@ -44,11 +47,8 @@ async def test_lsp_client_send_request(): """Test LSP client can send a request and receive response.""" client = AstrbotLspClient() - test_dir = os.path.dirname(os.path.abspath(__file__)) - server_path = os.path.join(test_dir, "fixtures", "echo_lsp_server.py") - await client.connect_to_server( - command=["python", server_path], + command=[sys.executable, str(SERVER_PATH)], workspace_uri="file:///tmp", ) @@ -68,11 +68,8 @@ async def test_lsp_client_send_notification(): """Test LSP client can send a notification (no response).""" client = AstrbotLspClient() - test_dir = os.path.dirname(os.path.abspath(__file__)) - server_path = os.path.join(test_dir, "fixtures", "echo_lsp_server.py") - await client.connect_to_server( - command=["python", server_path], + command=[sys.executable, str(SERVER_PATH)], workspace_uri="file:///tmp", ) @@ -99,4 +96,22 @@ async def test_lsp_client_send_notification_not_connected(): """Test LSP client raises RuntimeError when sending notification while not connected.""" client = AstrbotLspClient() with pytest.raises(RuntimeError, match="LSP client not connected"): - await client.send_notification("test", {}) \ No newline at end of file + await client.send_notification("test", {}) + + +@pytest.mark.anyio +async def test_lsp_client_connect_does_not_corrupt_anyio_cancel_scope(): + """Test connect/shutdown can run inside fail_after scopes without scope corruption.""" + client = AstrbotLspClient() + + with anyio.fail_after(5): + await client.connect_to_server( + command=[sys.executable, str(SERVER_PATH)], + workspace_uri="file:///tmp", + ) + + try: + assert client.connected + finally: + with anyio.fail_after(5): + await client.shutdown()