Files
wehub-resource-sync e4dcfc49aa
Tests / Import Check (Python 3.13) (push) Has been cancelled
Tests / Import Check (Python 3.14) (push) Has been cancelled
Tests / Python Tests (Python 3.11) (push) Has been cancelled
Tests / Python Tests (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.14) (push) Has been cancelled
Tests / Test Summary (push) Has been cancelled
Tests / Lint and Format (push) Has been cancelled
Tests / Web Node Tests (push) Has been cancelled
Tests / Import Check (Python 3.11) (push) Has been cancelled
Tests / Import Check (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.13) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:00:43 +08:00

217 lines
6.9 KiB
Python

"""Tests for ChatOrchestrator routing and lifecycle."""
from __future__ import annotations
import asyncio
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from deeptutor.core.capability_protocol import BaseCapability, CapabilityManifest
from deeptutor.core.context import UnifiedContext
from deeptutor.core.stream import StreamEvent, StreamEventType
from deeptutor.core.stream_bus import StreamBus
from deeptutor.runtime.orchestrator import ChatOrchestrator
@pytest.fixture(autouse=True)
def _patch_event_bus():
"""Prevent EventBus background processor from running during tests."""
mock_bus = MagicMock()
mock_bus.publish = AsyncMock()
with patch("deeptutor.runtime.orchestrator.get_event_bus", return_value=mock_bus):
yield
from deeptutor.events.event_bus import EventBus
EventBus.reset()
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
class _EchoCapability(BaseCapability):
"""Minimal capability that echoes the user message."""
manifest = CapabilityManifest(
name="echo",
description="Echoes back user message.",
stages=["responding"],
)
async def run(self, context: UnifiedContext, stream: StreamBus) -> None:
await stream.content(context.user_message, source=self.name)
class _FailingCapability(BaseCapability):
"""Capability that raises."""
manifest = CapabilityManifest(name="fail", description="Always fails.")
async def run(self, context: UnifiedContext, stream: StreamBus) -> None:
raise RuntimeError("intentional failure")
def _make_orchestrator(
capabilities: dict[str, BaseCapability] | None = None,
) -> ChatOrchestrator:
"""Build an orchestrator with fake registries."""
cap_reg = MagicMock()
cap_map = capabilities or {}
cap_reg.get = lambda name: cap_map.get(name)
cap_reg.list_capabilities = lambda: list(cap_map.keys())
tool_reg = MagicMock()
tool_reg.list_tools = MagicMock(return_value=[])
tool_reg.build_openai_schemas = MagicMock(return_value=[])
orch = ChatOrchestrator.__new__(ChatOrchestrator)
orch._cap_registry = cap_reg
orch._tool_registry = tool_reg
return orch
# ---------------------------------------------------------------------------
# Routing
# ---------------------------------------------------------------------------
class TestOrchestratorRouting:
@pytest.mark.asyncio
async def test_routes_to_active_capability(self) -> None:
echo = _EchoCapability()
orch = _make_orchestrator({"echo": echo})
ctx = UnifiedContext(
user_message="ping",
active_capability="echo",
)
events: list[StreamEvent] = []
async for event in orch.handle(ctx):
events.append(event)
types = [e.type for e in events]
assert StreamEventType.SESSION in types
assert StreamEventType.CONTENT in types
assert StreamEventType.DONE in types
content_events = [e for e in events if e.type == StreamEventType.CONTENT]
assert content_events[0].content == "ping"
@pytest.mark.asyncio
async def test_defaults_to_chat_capability(self) -> None:
chat_cap = _EchoCapability()
chat_cap.manifest = CapabilityManifest(
name="chat", description="Default chat.", stages=["responding"]
)
orch = _make_orchestrator({"chat": chat_cap})
ctx = UnifiedContext(user_message="hello")
events: list[StreamEvent] = []
async for event in orch.handle(ctx):
events.append(event)
content_events = [e for e in events if e.type == StreamEventType.CONTENT]
assert len(content_events) == 1
assert content_events[0].content == "hello"
@pytest.mark.asyncio
async def test_unknown_capability_yields_error(self) -> None:
orch = _make_orchestrator({})
ctx = UnifiedContext(
user_message="hi",
active_capability="nonexistent",
)
events: list[StreamEvent] = []
async for event in orch.handle(ctx):
events.append(event)
error_events = [e for e in events if e.type == StreamEventType.ERROR]
assert len(error_events) == 1
assert "Unknown capability" in error_events[0].content
# ---------------------------------------------------------------------------
# Error handling
# ---------------------------------------------------------------------------
class TestOrchestratorErrorHandling:
@pytest.mark.asyncio
async def test_capability_exception_yields_error_event(self) -> None:
fail_cap = _FailingCapability()
orch = _make_orchestrator({"fail": fail_cap})
ctx = UnifiedContext(
user_message="boom",
active_capability="fail",
)
events: list[StreamEvent] = []
async for event in orch.handle(ctx):
events.append(event)
error_events = [e for e in events if e.type == StreamEventType.ERROR]
assert len(error_events) == 1
assert "intentional failure" in error_events[0].content
done_events = [e for e in events if e.type == StreamEventType.DONE]
assert len(done_events) == 1
# ---------------------------------------------------------------------------
# Session ID management
# ---------------------------------------------------------------------------
class TestOrchestratorSessionId:
@pytest.mark.asyncio
async def test_assigns_session_id_if_missing(self) -> None:
echo = _EchoCapability()
orch = _make_orchestrator({"echo": echo})
ctx = UnifiedContext(user_message="test", active_capability="echo")
assert ctx.session_id == ""
async for _ in orch.handle(ctx):
pass
assert ctx.session_id != ""
@pytest.mark.asyncio
async def test_preserves_existing_session_id(self) -> None:
echo = _EchoCapability()
orch = _make_orchestrator({"echo": echo})
ctx = UnifiedContext(
session_id="my-session",
user_message="test",
active_capability="echo",
)
async for _ in orch.handle(ctx):
pass
assert ctx.session_id == "my-session"
# ---------------------------------------------------------------------------
# List helpers
# ---------------------------------------------------------------------------
class TestOrchestratorHelpers:
def test_list_tools(self) -> None:
orch = _make_orchestrator()
assert orch.list_tools() == []
def test_list_capabilities(self) -> None:
echo = _EchoCapability()
orch = _make_orchestrator({"echo": echo})
assert orch.list_capabilities() == ["echo"]
def test_get_tool_schemas(self) -> None:
orch = _make_orchestrator()
schemas = orch.get_tool_schemas()
assert isinstance(schemas, list)