Files
EvoScientist/tests/conftest.py
T
Xi Zhang c407d2e20f Fix/async subagent model switch (#217)
* feat(middleware): add ConfigurableModelMiddleware for dynamic model resolution

- Introduced ConfigurableModelMiddleware to resolve chat models from RunnableConfig.configurable on each call.
- Updated middleware initialization to include ConfigurableModelMiddleware.
- Enhanced context editing middleware tests to verify presence of ConfigurableModelMiddleware.
- Implemented tests for ConfigurableModelMiddleware to ensure correct model overriding and caching behavior.
- Added tests for deepagents model-passthrough patch to verify configuration injection in async tasks.

* feat(async-subagent): update middleware handling to prevent deadlocks in async sub-agents

* style: Refactor code formatting for improved readability in patches and test files

* refactor: streamline middleware construction and improve async handling in ConfigurableModelMiddleware

* fix: remove unused request parameter from _read_model_override function

* refactor: improve async handling in _ClientProxy and enhance logging in ConfigurableModelMiddleware
test: add behavior test to ensure AskUserMiddleware is excluded in async subagent mode
2026-05-08 22:52:24 +01:00

140 lines
4.4 KiB
Python

"""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(name="run_async")
def run_async_fixture():
"""Pytest fixture that exposes run_async as a callable for test functions."""
return run_async
@pytest.fixture
def sample_tool_call():
"""A minimal tool call dict."""
return {"id": "tc_001", "name": "execute", "args": {"command": "ls -la"}}
@pytest.fixture
def sample_tool_result():
"""A minimal tool result dict."""
return {"name": "execute", "content": "[OK] file1.py file2.py", "success": True}
@pytest.fixture
def sample_events():
"""A sequence of stream event dicts covering common types."""
return [
{"type": "thinking", "content": "Let me think..."},
{"type": "text", "content": "Here is the answer."},
{
"type": "tool_call",
"id": "tc_001",
"name": "execute",
"args": {"command": "ls"},
},
{
"type": "tool_result",
"name": "execute",
"content": "[OK] done",
"success": True,
},
{
"type": "subagent_start",
"name": "research-agent",
"description": "Find papers",
},
{
"type": "subagent_tool_call",
"subagent": "research-agent",
"name": "tavily_search",
"args": {"query": "test"},
"id": "tc_sa_001",
},
{
"type": "subagent_tool_result",
"subagent": "research-agent",
"name": "tavily_search",
"content": "Results...",
"success": True,
},
{"type": "subagent_end", "name": "research-agent"},
{"type": "done", "response": "Here is the answer."},
]
@pytest.fixture
def tmp_workspace(tmp_path):
"""Provide a temporary workspace directory path."""
ws = tmp_path / "workspace"
ws.mkdir()
return str(ws)
# Capture deepagents tool factories at conftest load time — BEFORE any test
# imports EvoScientist, which can trigger ``_patch_deepagents_model_passthrough``
# during agent construction. Once captured here, the ``restore_model_passthrough_patch``
# fixture has a stable "truly unpatched" baseline to reset to between tests, even
# if upstream code paths apply the patch as a side effect.
try:
from deepagents.middleware import async_subagents as _ds_async_subagents
_DEEPAGENTS_ORIGINAL_BUILD_START = _ds_async_subagents._build_start_tool
_DEEPAGENTS_ORIGINAL_BUILD_UPDATE = _ds_async_subagents._build_update_tool
except Exception:
_ds_async_subagents = None
_DEEPAGENTS_ORIGINAL_BUILD_START = None
_DEEPAGENTS_ORIGINAL_BUILD_UPDATE = None
@pytest.fixture
def restore_model_passthrough_patch():
"""Reset deepagents internals + ``_model_passthrough_patched`` to unpatched.
The model-passthrough patch wraps ``deepagents.middleware.async_subagents``
module-level functions in place. The originals are captured at conftest
load time (above) so this fixture can always start each test from a
known-unpatched state regardless of what other tests / agent fixtures
did to the module before.
"""
from EvoScientist.llm import patches as patches_mod
if _ds_async_subagents is None:
# deepagents not importable — fixture is a no-op (the patch fn itself
# returns early in that case).
yield
return
def _reset() -> None:
_ds_async_subagents._build_start_tool = _DEEPAGENTS_ORIGINAL_BUILD_START
_ds_async_subagents._build_update_tool = _DEEPAGENTS_ORIGINAL_BUILD_UPDATE
patches_mod._model_passthrough_patched = False
_reset()
try:
yield
finally:
_reset()