refactor(runtime): centralize async bridges under an owned runtime (#376)
* feat(runtime): add application-scoped async runtime * refactor(cli): use owned runtime for session stats * refactor(onboard): use the owned async runtime * docs(runtime): record async bridge ownership * refactor(middleware): keep sync fallback synchronous * refactor(mcp): load tools on an owned runtime * refactor(cli): share owned runtime across entry points * refactor(channels): make inbound sync bridge explicit * refactor(stream): run Rich streaming on owned runtime * chore(runtime): remove nest-asyncio dependency * refactor(asyncio): require active loops in async code * docs(runtime): document final event loop ownership * fix(stream): cancel stalled owned streams * fix(cli): recover cleanly from stream cancellation * fix(runtime): drain executor work before shutdown * fix(runtime): terminate cancelled shell process trees * fix(models): let fallback bypass selector failures * fix(cli): reset interrupt handling between turns * docs: rm implementation spec * fix(serve): cancel active turns during shutdown * fix(runtime): protect settlement from waiter cancellation * fix(backends): reject empty shell commands * fix(runtime): terminate descendants after shell exit * fix(mcp): keep standalone discovery off channel loop * fix(cli): own and settle interactive prompt cancellation * fix(serve): keep channel sends off runtime loop * fix(stream): scope cancel context to iterator steps * refactor(serve): require the owned async runtime * fix(channels): keep interactive sends off runtime loop * fix(selector): surface fallback without log spam * test(runtime): normalize Windows shell marker * fix(cli): serialize interactive session turns * fix(shell): bound output drain after termination * fix(ui): do not retry owned runtime failures * fix(shell): allow signal-safe registry reentry * fix(shell): avoid terminating reused process ids * fix(channels): preserve streaming send order * fix(cli): report runtime shutdown timeouts cleanly * fix(mcp): guide async callers to async loader * docs(runtime): clarify reserved async bridge APIs * fix(runtime): bound code interpreter cleanup * test(shell): use active Python for drain regression --------- Co-authored-by: Xi Zhang <106144707+X-iZhang@users.noreply.github.com>
This commit is contained in:
@@ -16,8 +16,10 @@ from langchain_core.messages import AIMessage, HumanMessage
|
||||
from EvoScientist.middleware.events import NoOpSink
|
||||
from EvoScientist.middleware.model_fallback import (
|
||||
_guard_and_fallback,
|
||||
_guard_and_fallback_sync,
|
||||
_is_non_fallbackable,
|
||||
_try_fallbacks,
|
||||
_try_fallbacks_sync,
|
||||
add_fallback,
|
||||
clear_fallbacks,
|
||||
)
|
||||
@@ -410,6 +412,49 @@ class TestGuardAndFallback:
|
||||
invoke.assert_awaited_once()
|
||||
|
||||
|
||||
class TestSynchronousFallback:
|
||||
"""The sync middleware path must not create or nest an event loop."""
|
||||
|
||||
def test_first_fallback_succeeds_without_async_bridge(self):
|
||||
add_fallback("fb-model", "fb-provider")
|
||||
req = _fake_request()
|
||||
invoke = MagicMock(return_value=AI_RESPONSE)
|
||||
|
||||
with patch("EvoScientist.llm.models.get_chat_model") as mock_gcm:
|
||||
mock_gcm.return_value = MagicMock()
|
||||
result = _try_fallbacks_sync(req, invoke, Exception("503 boom"), _SINK)
|
||||
|
||||
assert result is AI_RESPONSE
|
||||
invoke.assert_called_once()
|
||||
|
||||
def test_guard_rejects_non_fallbackable_error_before_handler(self):
|
||||
add_fallback("fb", "prov")
|
||||
req = _fake_request()
|
||||
invoke = MagicMock()
|
||||
|
||||
with pytest.raises(ContextOverflowError):
|
||||
_guard_and_fallback_sync(
|
||||
ContextOverflowError("overflow"), req, invoke, _SINK
|
||||
)
|
||||
|
||||
invoke.assert_not_called()
|
||||
|
||||
def test_middleware_sync_entrypoint_uses_native_traversal(self):
|
||||
from EvoScientist.middleware.model_fallback import ModelFallbackMiddleware
|
||||
|
||||
add_fallback("fb", "prov")
|
||||
req = _fake_request()
|
||||
response = AI_RESPONSE
|
||||
handler = MagicMock(side_effect=[Exception("503 primary"), response])
|
||||
|
||||
with patch("EvoScientist.llm.models.get_chat_model") as mock_gcm:
|
||||
mock_gcm.return_value = MagicMock()
|
||||
result = ModelFallbackMiddleware().wrap_model_call(req, handler)
|
||||
|
||||
assert result is response
|
||||
assert handler.call_count == 2
|
||||
|
||||
|
||||
# ═════════════════════════════════════════════════════════════════
|
||||
# 4. UI emit callback
|
||||
# ═════════════════════════════════════════════════════════════════
|
||||
|
||||
Reference in New Issue
Block a user