diff --git a/tests/conftest.py b/tests/conftest.py index 7cce355..ff9d15a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,9 +1,31 @@ """Shared fixtures for EvoScientist tests.""" +import asyncio + import pytest +def run_async(coro): + """Run an async coroutine safely, cancelling pending tasks before closing. + + This prevents 'Event loop is closed' errors from asyncio.Queue cleanup + when tasks are still waiting on Queue.get() at teardown time. + """ + loop = asyncio.new_event_loop() + try: + return loop.run_until_complete(coro) + finally: + # Cancel all pending tasks so Queue getters don't raise on close + pending = asyncio.all_tasks(loop) + for task in pending: + task.cancel() + if pending: + loop.run_until_complete(asyncio.gather(*pending, return_exceptions=True)) + loop.run_until_complete(loop.shutdown_asyncgens()) + loop.close() + + @pytest.fixture def sample_tool_call(): """A minimal tool call dict.""" diff --git a/tests/test_bus_integration.py b/tests/test_bus_integration.py index d118c93..6427fca 100644 --- a/tests/test_bus_integration.py +++ b/tests/test_bus_integration.py @@ -13,13 +13,7 @@ from EvoScientist.channels.channel_manager import ChannelManager from EvoScientist.channels.base import Channel, OutgoingMessage -def _run(coro): - """Run an async coroutine safely, creating a fresh event loop.""" - loop = asyncio.new_event_loop() - try: - return loop.run_until_complete(coro) - finally: - loop.close() +from tests.conftest import run_async as _run def _drain_queue(q): diff --git a/tests/test_channel_comprehensive.py b/tests/test_channel_comprehensive.py index 678a2d0..6b4949c 100644 --- a/tests/test_channel_comprehensive.py +++ b/tests/test_channel_comprehensive.py @@ -46,12 +46,7 @@ from EvoScientist.channels.formatter import convert_markdown # Helpers # ═══════════════════════════════════════════════════════════════════ -def _run(coro): - loop = asyncio.new_event_loop() - try: - return loop.run_until_complete(coro) - finally: - loop.close() +from tests.conftest import run_async as _run @dataclass diff --git a/tests/test_channel_manager.py b/tests/test_channel_manager.py index 9cd8656..0b5740c 100644 --- a/tests/test_channel_manager.py +++ b/tests/test_channel_manager.py @@ -9,13 +9,7 @@ from EvoScientist.channels.channel_manager import ChannelManager from EvoScientist.channels.base import Channel, OutboundMessage -def _run(coro): - """Run an async coroutine safely, creating a fresh event loop.""" - loop = asyncio.new_event_loop() - try: - return loop.run_until_complete(coro) - finally: - loop.close() +from tests.conftest import run_async as _run class _FakeConfig: diff --git a/tests/test_dingtalk_channel.py b/tests/test_dingtalk_channel.py index 3f40f32..3aae8fb 100644 --- a/tests/test_dingtalk_channel.py +++ b/tests/test_dingtalk_channel.py @@ -1,6 +1,5 @@ """Tests for DingTalk channel implementation.""" -import asyncio import json from unittest.mock import AsyncMock, MagicMock @@ -10,13 +9,7 @@ from EvoScientist.channels.dingtalk.channel import DingTalkChannel, DingTalkConf from EvoScientist.channels.base import ChannelError, OutboundMessage -def _run(coro): - """Run an async coroutine safely, creating a fresh event loop.""" - loop = asyncio.new_event_loop() - try: - return loop.run_until_complete(coro) - finally: - loop.close() +from tests.conftest import run_async as _run class TestDingTalkConfig: diff --git a/tests/test_discord_channel.py b/tests/test_discord_channel.py index 6d5152d..dd4cdd2 100644 --- a/tests/test_discord_channel.py +++ b/tests/test_discord_channel.py @@ -1,20 +1,12 @@ """Tests for Discord channel implementation.""" -import asyncio - import pytest from EvoScientist.channels.discord.channel import DiscordChannel, DiscordConfig from EvoScientist.channels.base import ChannelError -def _run(coro): - """Run an async coroutine safely, creating a fresh event loop.""" - loop = asyncio.new_event_loop() - try: - return loop.run_until_complete(coro) - finally: - loop.close() +from tests.conftest import run_async as _run class TestDiscordConfig: diff --git a/tests/test_feishu_channel.py b/tests/test_feishu_channel.py index dad38f1..ef53333 100644 --- a/tests/test_feishu_channel.py +++ b/tests/test_feishu_channel.py @@ -1,6 +1,5 @@ """Tests for Feishu channel implementation.""" -import asyncio import json from unittest.mock import AsyncMock, MagicMock @@ -16,13 +15,7 @@ from EvoScientist.channels.feishu.channel import ( from EvoScientist.channels.base import ChannelError, OutboundMessage -def _run(coro): - """Run an async coroutine safely, creating a fresh event loop.""" - loop = asyncio.new_event_loop() - try: - return loop.run_until_complete(coro) - finally: - loop.close() +from tests.conftest import run_async as _run class TestFeishuConfig: diff --git a/tests/test_message_bus.py b/tests/test_message_bus.py index 700423d..99eefa8 100644 --- a/tests/test_message_bus.py +++ b/tests/test_message_bus.py @@ -7,13 +7,7 @@ from EvoScientist.channels.bus.events import InboundMessage, OutboundMessage from EvoScientist.channels.bus.message_bus import MessageBus -def _run(coro): - """Run an async coroutine safely, creating a fresh event loop.""" - loop = asyncio.new_event_loop() - try: - return loop.run_until_complete(coro) - finally: - loop.close() +from tests.conftest import run_async as _run # ── Event tests ── diff --git a/tests/test_sessions.py b/tests/test_sessions.py index 7533fc3..467a8d7 100644 --- a/tests/test_sessions.py +++ b/tests/test_sessions.py @@ -1,6 +1,5 @@ """Tests for EvoScientist.sessions — thread CRUD, ID generation, helpers.""" -import asyncio import json import os import tempfile @@ -21,16 +20,7 @@ from EvoScientist.sessions import ( ) -def _run(coro): - """Run an async coroutine synchronously (resilient to closed loops).""" - try: - loop = asyncio.get_event_loop() - if loop.is_closed(): - raise RuntimeError("closed") - except RuntimeError: - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - return loop.run_until_complete(coro) +from tests.conftest import run_async as _run class TestGenerateThreadId(unittest.TestCase): diff --git a/tests/test_slack_channel.py b/tests/test_slack_channel.py index f33bb95..4f1b694 100644 --- a/tests/test_slack_channel.py +++ b/tests/test_slack_channel.py @@ -1,20 +1,12 @@ """Tests for Slack channel implementation.""" -import asyncio - import pytest from EvoScientist.channels.slack.channel import SlackChannel, SlackConfig from EvoScientist.channels.base import ChannelError -def _run(coro): - """Run an async coroutine safely, creating a fresh event loop.""" - loop = asyncio.new_event_loop() - try: - return loop.run_until_complete(coro) - finally: - loop.close() +from tests.conftest import run_async as _run class TestSlackConfig: diff --git a/tests/test_telegram_channel.py b/tests/test_telegram_channel.py index e50f3a5..dfa6b1e 100644 --- a/tests/test_telegram_channel.py +++ b/tests/test_telegram_channel.py @@ -1,20 +1,12 @@ """Tests for Telegram channel implementation.""" -import asyncio - import pytest from EvoScientist.channels.telegram.channel import TelegramChannel, TelegramConfig from EvoScientist.channels.base import ChannelError -def _run(coro): - """Run an async coroutine safely, creating a fresh event loop.""" - loop = asyncio.new_event_loop() - try: - return loop.run_until_complete(coro) - finally: - loop.close() +from tests.conftest import run_async as _run class TestTelegramConfig: diff --git a/tests/test_tool_error_handler.py b/tests/test_tool_error_handler.py index c90b0bb..85e6a56 100644 --- a/tests/test_tool_error_handler.py +++ b/tests/test_tool_error_handler.py @@ -1,6 +1,5 @@ """Tests for ToolErrorHandlerMiddleware.""" -import asyncio from unittest.mock import MagicMock import pytest @@ -150,8 +149,10 @@ class TestWrapToolCallAsync: def setup_method(self): self.mw = ToolErrorHandlerMiddleware() - def _run(self, coro): - return asyncio.get_event_loop().run_until_complete(coro) + @staticmethod + def _run(coro): + from tests.conftest import run_async + return run_async(coro) def test_success_passes_through(self): expected = ToolMessage(content="ok", tool_call_id="tc_001", name="t") diff --git a/tests/test_wechat_channel.py b/tests/test_wechat_channel.py index e0ae457..1cae0b3 100644 --- a/tests/test_wechat_channel.py +++ b/tests/test_wechat_channel.py @@ -22,13 +22,7 @@ from EvoScientist.channels.wechat.crypto import ( from EvoScientist.channels.base import ChannelError -def _run(coro): - """Run an async coroutine safely, creating a fresh event loop.""" - loop = asyncio.new_event_loop() - try: - return loop.run_until_complete(coro) - finally: - loop.close() +from tests.conftest import run_async as _run # ── Config tests ──────────────────────────────────────────────────