Independent FastAPI backend for the MaskanX agentic growth platform. Includes the agent runtime, MCP client integrations (Meta Ads, LinkedIn, HubSpot, Tavily, Exa, xAI, Citedy, image generation), PostgreSQL storage for chats and cron jobs, provider and secret management, and the CLI. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
189 lines
7.4 KiB
Python
189 lines
7.4 KiB
Python
import asyncio
|
|
import inspect
|
|
from unittest.mock import patch, MagicMock
|
|
|
|
import pytest
|
|
from adclaw.agents.tools.delegation import make_delegate_tool, DelegationContext
|
|
from adclaw.agents.persona_manager import PersonaManager
|
|
from adclaw.config.config import PersonaConfig
|
|
|
|
|
|
def _tool_text(response) -> str:
|
|
parts = []
|
|
for block in response.content:
|
|
if isinstance(block, dict):
|
|
parts.append(block.get("text", ""))
|
|
else:
|
|
parts.append(getattr(block, "text", ""))
|
|
return "".join(parts)
|
|
|
|
|
|
def test_delegation_executor_text_from_content_normalizes_none():
|
|
from adclaw.agents.tools.delegation_executor import _text_from_content
|
|
|
|
assert _text_from_content(None) == ""
|
|
|
|
|
|
class TestDelegation:
|
|
def test_make_delegate_tool_returns_callable(self):
|
|
personas = [PersonaConfig(id="researcher", name="Researcher", soul_md="Find facts.")]
|
|
mgr = PersonaManager(working_dir="/tmp/test", personas=personas)
|
|
tool_fn = make_delegate_tool(mgr)
|
|
assert callable(tool_fn)
|
|
if hasattr(inspect, "markcoroutinefunction"):
|
|
assert inspect.iscoroutinefunction(tool_fn)
|
|
|
|
def test_delegate_to_unknown_agent_sync_compatibility(self):
|
|
personas = [PersonaConfig(id="researcher", name="Researcher", soul_md="Find facts.")]
|
|
mgr = PersonaManager(working_dir="/tmp/test", personas=personas)
|
|
tool_fn = make_delegate_tool(mgr)
|
|
result = tool_fn(agent_id="nonexistent", task="do something")
|
|
assert isinstance(result, str)
|
|
assert "not found" in result.lower()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delegate_to_unknown_agent(self):
|
|
personas = [PersonaConfig(id="researcher", name="Researcher", soul_md="Find facts.")]
|
|
mgr = PersonaManager(working_dir="/tmp/test", personas=personas)
|
|
tool_fn = make_delegate_tool(mgr)
|
|
result = _tool_text(
|
|
await tool_fn(agent_id="nonexistent", task="do something"),
|
|
)
|
|
assert "not found" in result.lower()
|
|
|
|
def test_delegation_depth_limit(self):
|
|
ctx = DelegationContext(max_depth=3)
|
|
assert ctx.can_delegate()
|
|
ctx.depth = 3
|
|
assert not ctx.can_delegate()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delegation_depth_exceeded(self):
|
|
personas = [PersonaConfig(id="r", name="R", soul_md="Test.")]
|
|
mgr = PersonaManager(working_dir="/tmp/test", personas=personas)
|
|
ctx = DelegationContext(max_depth=0)
|
|
tool_fn = make_delegate_tool(mgr, delegation_ctx=ctx)
|
|
result = _tool_text(await tool_fn(agent_id="r", task="do something"))
|
|
assert "maximum delegation depth" in result.lower()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delegation_calls_executor(self):
|
|
personas = [PersonaConfig(id="r", name="R", soul_md="Test.")]
|
|
mgr = PersonaManager(working_dir="/tmp/test", personas=personas)
|
|
async def _mock_exec(*args, **kwargs): # noqa: ARG001
|
|
return "Result from sub-agent"
|
|
|
|
mock_exec = MagicMock(side_effect=_mock_exec)
|
|
with patch(
|
|
"adclaw.agents.tools.delegation_executor.execute_delegation",
|
|
mock_exec,
|
|
):
|
|
tool_fn = make_delegate_tool(mgr)
|
|
result = _tool_text(await tool_fn(agent_id="r", task="do something"))
|
|
assert result == "Result from sub-agent"
|
|
mock_exec.assert_called_once()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delegation_accepts_display_name(self):
|
|
personas = [
|
|
PersonaConfig(
|
|
id="content-writer",
|
|
name="Content Writer",
|
|
soul_md="Test.",
|
|
),
|
|
]
|
|
mgr = PersonaManager(working_dir="/tmp/test", personas=personas)
|
|
async def _mock_exec(*args, **kwargs): # noqa: ARG001
|
|
return "OK"
|
|
|
|
with patch(
|
|
"adclaw.agents.tools.delegation_executor.execute_delegation",
|
|
side_effect=_mock_exec,
|
|
):
|
|
tool_fn = make_delegate_tool(mgr)
|
|
result = _tool_text(
|
|
await tool_fn(agent_id="content writer", task="draft"),
|
|
)
|
|
assert result == "OK"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delegation_error_handling(self):
|
|
personas = [PersonaConfig(id="r", name="R", soul_md="Test.")]
|
|
mgr = PersonaManager(working_dir="/tmp/test", personas=personas)
|
|
with patch(
|
|
"adclaw.agents.tools.delegation_executor.execute_delegation",
|
|
side_effect=RuntimeError("LLM down"),
|
|
):
|
|
tool_fn = make_delegate_tool(mgr)
|
|
result = _tool_text(await tool_fn(agent_id="r", task="do something"))
|
|
assert "failed" in result.lower()
|
|
assert "LLM down" not in result
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delegation_depth_resets_after_call(self):
|
|
"""Verify depth is decremented even after successful delegation."""
|
|
personas = [PersonaConfig(id="r", name="R", soul_md="Test.")]
|
|
mgr = PersonaManager(working_dir="/tmp/test", personas=personas)
|
|
ctx = DelegationContext(max_depth=3)
|
|
async def _mock_exec(*args, **kwargs): # noqa: ARG001
|
|
return "OK"
|
|
|
|
with patch(
|
|
"adclaw.agents.tools.delegation_executor.execute_delegation",
|
|
side_effect=_mock_exec,
|
|
):
|
|
tool_fn = make_delegate_tool(mgr, delegation_ctx=ctx)
|
|
await tool_fn(agent_id="r", task="task1")
|
|
assert ctx.depth == 0
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delegation_depth_is_task_scoped(self):
|
|
personas = [PersonaConfig(id="r", name="R", soul_md="Test.")]
|
|
mgr = PersonaManager(working_dir="/tmp/test", personas=personas)
|
|
ctx = DelegationContext(max_depth=1)
|
|
|
|
async def _mock_exec(*args, **kwargs): # noqa: ARG001
|
|
await asyncio.sleep(0)
|
|
return "OK"
|
|
|
|
with patch(
|
|
"adclaw.agents.tools.delegation_executor.execute_delegation",
|
|
side_effect=_mock_exec,
|
|
):
|
|
tool_fn = make_delegate_tool(mgr, delegation_ctx=ctx)
|
|
first, second = await asyncio.gather(
|
|
tool_fn(agent_id="r", task="task1"),
|
|
tool_fn(agent_id="r", task="task2"),
|
|
)
|
|
|
|
assert _tool_text(first) == "OK"
|
|
assert _tool_text(second) == "OK"
|
|
assert ctx.depth == 0
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delegation_depth_resets_after_error(self):
|
|
"""Verify depth is decremented even after failed delegation."""
|
|
personas = [PersonaConfig(id="r", name="R", soul_md="Test.")]
|
|
mgr = PersonaManager(working_dir="/tmp/test", personas=personas)
|
|
ctx = DelegationContext(max_depth=3)
|
|
with patch(
|
|
"adclaw.agents.tools.delegation_executor.execute_delegation",
|
|
side_effect=RuntimeError("boom"),
|
|
):
|
|
tool_fn = make_delegate_tool(mgr, delegation_ctx=ctx)
|
|
await tool_fn(agent_id="r", task="task1")
|
|
assert ctx.depth == 0
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_unknown_agent_lists_available(self):
|
|
"""Error message should list available agent IDs."""
|
|
personas = [
|
|
PersonaConfig(id="alpha", name="A", soul_md="A."),
|
|
PersonaConfig(id="beta", name="B", soul_md="B."),
|
|
]
|
|
mgr = PersonaManager(working_dir="/tmp/test", personas=personas)
|
|
tool_fn = make_delegate_tool(mgr)
|
|
result = _tool_text(await tool_fn(agent_id="gamma", task="test"))
|
|
assert "alpha" in result
|
|
assert "beta" in result
|