"""Unit tests for single-agent deep_agent flows with mocked tool results.""" from __future__ import annotations from datetime import date, timedelta from types import SimpleNamespace from unittest.mock import patch import pytest from langchain_core.messages import AIMessage, ToolMessage from app.core.deep_agent import _normalize_tagged_list_lines, run_floating_stream, run_home class _FakeTool: name = "list_tasks" async def ainvoke(self, args): return {"rows": [{"id": "task-1", "title": "Mock Task"}], "echo": args} class _FakeLLM: def __init__(self) -> None: self.calls = 0 def bind_tools(self, _tools): return self async def ainvoke(self, messages): self.calls += 1 if self.calls == 1: return AIMessage( content="", tool_calls=[ { "id": "call-1", "name": "list_tasks", "args": {"project_id": "proj-1"}, } ], ) tool_messages = [m for m in messages if isinstance(m, ToolMessage)] assert tool_messages, "Expected at least one tool message" return AIMessage(content=f"Final answer from mocked tool: {tool_messages[-1].content}") async def astream(self, _messages): yield SimpleNamespace(content="stream-") yield SimpleNamespace(content="ok") @pytest.mark.asyncio async def test_run_home_uses_mocked_tool_result(): fake_llm = _FakeLLM() with patch("app.core.deep_agent.get_llm", return_value=fake_llm), patch( "app.core.deep_agent._all_tools", return_value=[_FakeTool()] ): out = await run_home("user-1", "list my tasks", {}) assert "Final answer from mocked tool" in out assert "Mock Task" in out @pytest.mark.asyncio async def test_run_floating_stream_emits_domain_then_tokens_with_mocked_tool_result(): fake_llm = _FakeLLM() with patch("app.core.deep_agent.get_llm", return_value=fake_llm), patch( "app.core.deep_agent._all_tools", return_value=[_FakeTool()] ): events = [] async for event in run_floating_stream( "user-1", "show me timeline updates", {"scope": {"type": "timeline", "id": "tl-1"}}, ): events.append(event) assert events[0] == ("floating_domain", "timelines") assert ("token", "stream-") in events assert ("token", "ok") in events def test_normalize_tagged_list_lines_rewrites_mixed_task_lines_to_tag_only_lines(): raw = ( "Certo!\n\n" "1. **Task A** — priorita high [task-1]\n" "2. **Task B** — priorita medium [task-2]\n" ) out = _normalize_tagged_list_lines(raw, "quali sono le prossime attivita?") assert "[task-1]" in out assert "[task-2]" in out assert "Task A" not in out assert "Task B" not in out def test_normalize_tagged_list_lines_filters_upcoming_timeline_query_to_current_month_future_only(): today = date.today() tomorrow = today + timedelta(days=1) yesterday = today - timedelta(days=1) next_month = (today.replace(day=28) + timedelta(days=5)).replace(day=1) raw = "\n".join( [ f"- Milestone old — {yesterday.strftime('%d/%m/%Y')} [tl-old]", f"- Milestone next — {tomorrow.strftime('%d/%m/%Y')} [tl-next]", f"- Milestone future — {next_month.strftime('%d/%m/%Y')} [tl-future]", ] ) out = _normalize_tagged_list_lines(raw, "invece i miei eventi prossimi?") assert "[tl-next]" in out assert "[tl-old]" not in out assert "[tl-future]" not in out