"""Tests unitaires du graphe LangGraph. Ces tests sont FOURNIS COMPLETS — ils servent de spécification. Ils doivent tous passer une fois react_agent.py implémenté. Le LLM est entièrement mocké : Ollama n'est pas nécessaire. Lancer : uv run pytest tests/unit/test_agent.py -v """ from unittest.mock import MagicMock, patch import pytest from langchain_core.messages import AIMessage, HumanMessage from src.agents.react_agent import AgentState, agent_node, should_continue # ── Helpers ─────────────────────────────────────────────────────────────────── def make_state(messages: list, iterations: int = 0) -> AgentState: return AgentState(messages=messages, iterations=iterations) def ai_with_tools() -> AIMessage: return AIMessage(content="", tool_calls=[ {"name": "search_product", "args": {"name": "laptop"}, "id": "c1", "type": "tool_call"} ]) def ai_final(content: str = "Voici la réponse.") -> AIMessage: return AIMessage(content=content) # ── Tests should_continue ───────────────────────────────────────────────────── class TestShouldContinue: def test_returns_tools_when_tool_calls_present(self) -> None: state = make_state([HumanMessage("test"), ai_with_tools()]) assert should_continue(state) == "tools" def test_returns_end_when_no_tool_calls(self) -> None: from langgraph.graph import END state = make_state([HumanMessage("test"), ai_final()]) assert should_continue(state) == END def test_returns_end_at_max_iterations(self) -> None: from langgraph.graph import END state = make_state([HumanMessage("test"), ai_with_tools()], iterations=20) assert should_continue(state) == END def test_continues_below_max_iterations(self) -> None: state = make_state([HumanMessage("test"), ai_with_tools()], iterations=5) assert should_continue(state) == "tools" # ── Tests agent_node ────────────────────────────────────────────────────────── class TestAgentNode: def test_increments_iterations(self) -> None: with patch("src.agents.react_agent.get_llm") as mock: mock.return_value.bind_tools.return_value.invoke.return_value = ai_final() result = agent_node(make_state([HumanMessage("test")], iterations=2)) assert result["iterations"] == 3 def test_appends_response_to_messages(self) -> None: response = ai_final("Réponse test") with patch("src.agents.react_agent.get_llm") as mock: mock.return_value.bind_tools.return_value.invoke.return_value = response result = agent_node(make_state([HumanMessage("test")])) assert response in result["messages"] def test_returns_messages_and_iterations_keys(self) -> None: with patch("src.agents.react_agent.get_llm") as mock: mock.return_value.bind_tools.return_value.invoke.return_value = ai_final() result = agent_node(make_state([HumanMessage("test")])) assert "messages" in result assert "iterations" in result def test_binds_tools_to_llm(self) -> None: with patch("src.agents.react_agent.get_llm") as mock: mock_llm = MagicMock() mock_llm.bind_tools.return_value.invoke.return_value = ai_final() mock.return_value = mock_llm agent_node(make_state([HumanMessage("test")])) mock_llm.bind_tools.assert_called_once() assert len(mock_llm.bind_tools.call_args[0][0]) > 0 # ── Tests build_agent ───────────────────────────────────────────────────────── class TestBuildAgent: def test_graph_compiles(self) -> None: from src.agents.react_agent import build_agent assert build_agent(use_memory=False) is not None def test_graph_has_agent_and_tools_nodes(self) -> None: from src.agents.react_agent import build_agent app = build_agent(use_memory=False) ids = list(app.get_graph().nodes.keys()) assert "agent" in ids assert "tools" in ids def test_mermaid_is_generated(self) -> None: from src.agents.react_agent import build_agent mermaid = build_agent(use_memory=False).get_graph().draw_mermaid() assert "agent" in mermaid assert "tools" in mermaid