AlgoSensei / tests /test_nodes.py
uncertainrods's picture
init_code
e266561
"""
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"