""" test_nodes.py — Unit tests for all DSA Mentor agent node functions. Uses MagicMock to patch the LLM and avoid real API calls. Run with: pytest tests/ -v """ import pytest from unittest.mock import MagicMock, patch # ── Helpers ─────────────────────────────────────────────────────────────────── def _base_state(**overrides) -> dict: """Returns a minimal valid AgentState for testing.""" state = { "problem": "Given an array of integers, find two numbers that add to a target.", "user_thought": "I'll use a nested loop to check every pair.", "code": "", "strictness": "Moderate", "request_mode": "analyze", "session_id": "test-session-001", "problem_topic": "Two Sum", "identified_gap": "Not using a hashmap", "gap_magnitude": 6, "current_hint_level": 1, "turn_count": 0, "messages": [], "final_response": None, "test_pass_rate": None, "mistake": None, "why_wrong": None, "correct_thinking": None, } state.update(overrides) return state # ── Classify Node ───────────────────────────────────────────────────────────── def test_classify_problem_returns_topic(): """classify_problem should update problem_topic from LLM response.""" mock_response = MagicMock() mock_response.content = "Two Sum" with patch("agent.nodes.classify_node._llm") as mock_llm: mock_chain = MagicMock() mock_chain.invoke.return_value = mock_response mock_llm.__or__ = MagicMock(return_value=mock_chain) from agent.nodes.classify_node import classify_problem result = classify_problem(_base_state()) assert "problem_topic" in result assert isinstance(result["problem_topic"], str) # ── Gate Node ───────────────────────────────────────────────────────────────── def test_gate_allows_solution_when_gap_critical(): """gate_solution should allow solution when gap_magnitude > 7.""" from agent.nodes.gate_node import gate_solution state = _base_state(request_mode="solution", gap_magnitude=9, turn_count=0) result = gate_solution(state) assert result.get("request_mode") == "solution" def test_gate_blocks_solution_too_early(): """gate_solution should block solution when turn_count < 2 and gap <= 7.""" from agent.nodes.gate_node import gate_solution state = _base_state(request_mode="solution", gap_magnitude=5, turn_count=0) result = gate_solution(state) assert result.get("request_mode") == "hint_forced" def test_gate_passthrough_non_solution_mode(): """gate_solution should return empty dict for non-solution modes.""" from agent.nodes.gate_node import gate_solution state = _base_state(request_mode="analyze") result = gate_solution(state) assert result == {} # ── Validate Node ───────────────────────────────────────────────────────────── def test_validate_solution_returns_100(): """validate_solution should always return score=100.""" with patch("agent.nodes.validate_node.load_profile") as mock_load, \ patch("agent.nodes.validate_node.persist_profile"): mock_load.return_value = MagicMock(weak_topics={}, solved_problems=0, total_turns=0, avg_gap=0.0) from agent.nodes.validate_node import validate_solution result = validate_solution(_base_state()) assert result["final_response"]["score"] == 100 assert result["final_response"]["type"] == "Validation" def test_validate_solution_contains_hint_text(): """validate_solution final_response should have a 'hint' key.""" with patch("agent.nodes.validate_node.load_profile") as mock_load, \ patch("agent.nodes.validate_node.persist_profile"): mock_load.return_value = MagicMock(weak_topics={}, solved_problems=0, total_turns=0, avg_gap=0.0) from agent.nodes.validate_node import validate_solution result = validate_solution(_base_state()) assert "hint" in result["final_response"] # ── Hint Node ───────────────────────────────────────────────────────────────── def test_generate_hint_increments_hint_level(): """generate_hint should increment current_hint_level by 1.""" mock_hint = MagicMock() mock_hint.hint = "Think about what data structure gives O(1) lookup." mock_hint.type = "Data Structure" with patch("agent.nodes.hint_node._structured_llm") as mock_llm, \ patch("agent.nodes.hint_node.load_profile") as mock_load: mock_llm.invoke.return_value = mock_hint mock_load.return_value = MagicMock(weak_topics={}, avg_gap=5.0) from agent.nodes.hint_node import generate_hint result = generate_hint(_base_state(current_hint_level=1)) assert result["current_hint_level"] == 2 def test_generate_hint_increments_turn_count(): """generate_hint should increment turn_count.""" mock_hint = MagicMock() mock_hint.hint = "Consider a different data structure." mock_hint.type = "Conceptual" with patch("agent.nodes.hint_node._structured_llm") as mock_llm, \ patch("agent.nodes.hint_node.load_profile") as mock_load: mock_llm.invoke.return_value = mock_hint mock_load.return_value = MagicMock(weak_topics={}, avg_gap=5.0) from agent.nodes.hint_node import generate_hint result = generate_hint(_base_state(turn_count=1)) assert result["turn_count"] == 2 def test_generate_hint_score_formula(): """Score should be 100 - gap_magnitude * 10.""" mock_hint = MagicMock() mock_hint.hint = "Hint text" mock_hint.type = "Conceptual" with patch("agent.nodes.hint_node._structured_llm") as mock_llm, \ patch("agent.nodes.hint_node.load_profile") as mock_load: mock_llm.invoke.return_value = mock_hint mock_load.return_value = MagicMock(weak_topics={}, avg_gap=5.0) from agent.nodes.hint_node import generate_hint result = generate_hint(_base_state(gap_magnitude=4)) assert result["final_response"]["score"] == 60 # ── Solution Node ───────────────────────────────────────────────────────────── def test_reveal_solution_structure(): """reveal_solution should return solution, explanation, and complexity.""" mock_sol = MagicMock() mock_sol.solution_code = "def two_sum(nums, target): ..." mock_sol.explanation = "Use a hashmap to store complements." mock_sol.complexity_analysis = "Time: O(N), Space: O(N)" with patch("agent.nodes.solution_node._structured_llm") as mock_llm: mock_llm.invoke.return_value = mock_sol from agent.nodes.solution_node import reveal_solution result = reveal_solution(_base_state()) resp = result["final_response"] assert "solution" in resp assert "explanation" in resp assert "complexity" in resp assert resp["score"] == 0 assert resp["type"] == "Solution"