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