fix(agents): make librarian failures structured and user-safe
- run_librarian / run_librarian_stream raise AgentError instead of
returning/yielding error text as normal output; detail stays in logs
- delegate_to_* wrappers now put a curated butler-toned sentence in
DelegationResult.output on failure and never expose str(e), so
streaming's error branch is reachable and honest
- _execute_single_delegation propagates success; direct delegation only
records delegate_to_* as called when the expert actually succeeded
- librarian tools return user-safe messages instead of
'Error searching: {e}' strings that leaked internal URLs into
synthesis; coordination stream errors are curated as well
- ruff cleanups (TYPE_CHECKING forward refs, B904, unused locals) in
the touched files to keep them lint-clean
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
"""
|
||||
Tests for structured failure behavior of The Librarian entry points.
|
||||
|
||||
run_librarian and run_librarian_stream must raise AgentError on failure
|
||||
instead of returning/yielding error text as if it were research output,
|
||||
and the raised error must not leak exception detail (internal URLs etc.).
|
||||
"""
|
||||
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from src.agents.librarian.agent import run_librarian, run_librarian_stream
|
||||
from src.agents.protocol import AgentError
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestRunLibrarianFailures:
|
||||
"""run_librarian raises structured errors instead of returning text."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_librarian_raises_agent_error(self):
|
||||
"""Failures raise AgentError rather than returning error prose."""
|
||||
mock_agent = MagicMock()
|
||||
mock_agent.run = AsyncMock(
|
||||
side_effect=RuntimeError("Connection refused to http://internal:8089")
|
||||
)
|
||||
|
||||
with patch(
|
||||
"src.agents.librarian.agent.get_librarian_agent",
|
||||
return_value=mock_agent,
|
||||
):
|
||||
with pytest.raises(AgentError) as exc_info:
|
||||
await run_librarian(task="Find Docker docs")
|
||||
|
||||
assert exc_info.value.agent_name == "librarian"
|
||||
# Exception detail stays in logs only
|
||||
assert "internal" not in str(exc_info.value)
|
||||
assert "Connection refused" not in str(exc_info.value)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_librarian_stream_raises_agent_error(self):
|
||||
"""Streaming failures raise instead of yielding error text."""
|
||||
mock_agent = MagicMock()
|
||||
mock_agent.run_stream = MagicMock(
|
||||
side_effect=RuntimeError("Connection refused to http://internal:8089")
|
||||
)
|
||||
|
||||
with patch(
|
||||
"src.agents.librarian.agent.get_librarian_agent",
|
||||
return_value=mock_agent,
|
||||
):
|
||||
collected: list[str] = []
|
||||
with pytest.raises(AgentError) as exc_info:
|
||||
async for delta in run_librarian_stream(task="Find Docker docs"):
|
||||
collected.append(delta)
|
||||
|
||||
assert exc_info.value.agent_name == "librarian"
|
||||
assert collected == [], "no error text may be yielded as output"
|
||||
assert "internal" not in str(exc_info.value)
|
||||
Reference in New Issue
Block a user