Spaces:
Sleeping
Sleeping
| """ | |
| 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" | |