Files
deerflow-code/offline-backend-20260512/backend/tests/test_skill_review_middleware.py
T

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})"