109 lines
4.4 KiB
Python
109 lines
4.4 KiB
Python
"""Tests for SkillReviewMiddleware."""
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from langchain_core.messages import AIMessage, HumanMessage
|
|
|
|
from deerflow.agents.middlewares.skill_review_middleware import SkillReviewMiddleware
|
|
|
|
|
|
def _make_state(messages):
|
|
state = MagicMock()
|
|
state.get.side_effect = lambda key, default=None: messages if key == "messages" else default
|
|
return state
|
|
|
|
|
|
def _make_runtime(thread_id="thread-abc"):
|
|
runtime = MagicMock()
|
|
runtime.context = {"thread_id": thread_id} if thread_id else {}
|
|
return runtime
|
|
|
|
|
|
def test_after_agent_calls_maybe_start_review():
|
|
"""When enabled and tool calls exist, maybe_start_review is called with correct thread_id."""
|
|
middleware = SkillReviewMiddleware()
|
|
|
|
ai_msg = MagicMock(spec=AIMessage)
|
|
tc = MagicMock()
|
|
tc.name = "bash"
|
|
ai_msg.tool_calls = [tc]
|
|
|
|
state = _make_state([HumanMessage(content="hi"), ai_msg])
|
|
runtime = _make_runtime("thread-xyz")
|
|
|
|
app_cfg = MagicMock()
|
|
with patch("deerflow.agents.middlewares.skill_review_middleware.get_app_config", return_value=app_cfg):
|
|
with patch("deerflow.agents.middlewares.skill_review_middleware.maybe_start_review") as mock_review:
|
|
middleware.after_agent(state, runtime)
|
|
|
|
mock_review.assert_called_once()
|
|
call_args = mock_review.call_args
|
|
assert call_args[0][0] == "thread-xyz" # thread_id
|
|
assert call_args[0][2] is app_cfg # app_config
|
|
|
|
|
|
def test_after_agent_no_thread_id_skips_review():
|
|
"""When thread_id is missing, maybe_start_review is NOT called."""
|
|
middleware = SkillReviewMiddleware()
|
|
state = _make_state([HumanMessage(content="hi")])
|
|
runtime = _make_runtime("")
|
|
|
|
app_cfg = MagicMock()
|
|
with patch("deerflow.agents.middlewares.skill_review_middleware.get_app_config", return_value=app_cfg):
|
|
with patch("deerflow.agents.middlewares.skill_review_middleware.maybe_start_review") as mock_review:
|
|
middleware.after_agent(state, runtime)
|
|
|
|
mock_review.assert_not_called()
|
|
|
|
|
|
def test_after_agent_exception_is_swallowed():
|
|
"""Exceptions in after_agent must not propagate."""
|
|
middleware = SkillReviewMiddleware()
|
|
state = _make_state([HumanMessage(content="hi")])
|
|
runtime = _make_runtime("thread-err")
|
|
|
|
with patch("deerflow.agents.middlewares.skill_review_middleware.get_app_config", side_effect=RuntimeError("boom")):
|
|
# Must not raise
|
|
middleware.after_agent(state, runtime)
|
|
|
|
|
|
def test_skill_review_middleware_in_chain_when_enabled():
|
|
"""SkillReviewMiddleware appears in middleware chain before LoopDetectionMiddleware when enabled."""
|
|
from unittest.mock import patch, MagicMock
|
|
from deerflow.agents.lead_agent.agent import _build_middlewares
|
|
from deerflow.agents.middlewares.skill_review_middleware import SkillReviewMiddleware
|
|
|
|
config = {
|
|
"configurable": {
|
|
"thread_id": "test",
|
|
"is_plan_mode": False,
|
|
"is_bootstrap": False,
|
|
"is_scheduled_run": False,
|
|
}
|
|
}
|
|
|
|
app_cfg = MagicMock()
|
|
app_cfg.skill_evolution.enabled = True
|
|
app_cfg.skill_evolution.review_worker.enabled = True
|
|
app_cfg.token_usage.enabled = False
|
|
app_cfg.tool_search.enabled = False
|
|
app_cfg.skill_stop.enabled = False
|
|
app_cfg.summarization.enabled = False
|
|
app_cfg.memory = MagicMock()
|
|
app_cfg.guardrails.enabled = False
|
|
app_cfg.title = MagicMock()
|
|
app_cfg.title.model_name = None
|
|
|
|
with patch("deerflow.agents.lead_agent.agent.get_app_config", return_value=app_cfg):
|
|
with patch("deerflow.agents.lead_agent.agent.build_lead_runtime_middlewares", return_value=[]):
|
|
with patch("deerflow.agents.lead_agent.agent._create_summarization_middleware", return_value=None):
|
|
with patch("deerflow.agents.lead_agent.agent._create_todo_list_middleware", return_value=None):
|
|
with patch("deerflow.models.vision.vision_enabled", return_value=False):
|
|
middlewares = _build_middlewares(config, model_name=None, app_config=app_cfg)
|
|
|
|
type_names = [type(m).__name__ for m in middlewares]
|
|
assert "SkillReviewMiddleware" in type_names, f"SkillReviewMiddleware not in chain: {type_names}"
|
|
|
|
review_idx = type_names.index("SkillReviewMiddleware")
|
|
loop_idx = type_names.index("LoopDetectionMiddleware")
|
|
assert review_idx < loop_idx, f"SkillReviewMiddleware ({review_idx}) must be before LoopDetectionMiddleware ({loop_idx})"
|