feat(tests): refactor async coroutine handling with shared run_async function

This commit is contained in:
X-iZhang
2026-02-18 16:53:58 +00:00
parent 145b31dc2c
commit 2dd331be02
13 changed files with 37 additions and 91 deletions
+22
View File
@@ -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."""
+1 -7
View File
@@ -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):
+1 -6
View File
@@ -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
+1 -7
View File
@@ -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:
+1 -8
View File
@@ -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:
+1 -9
View File
@@ -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:
+1 -8
View File
@@ -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:
+1 -7
View File
@@ -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 ──
+1 -11
View File
@@ -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):
+1 -9
View File
@@ -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:
+1 -9
View File
@@ -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:
+4 -3
View File
@@ -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")
+1 -7
View File
@@ -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 ──────────────────────────────────────────────────