"""Tests for the context-aware hints system.""" from code_review_graph.hints import ( _MAX_PER_CATEGORY, SessionState, generate_hints, get_session, infer_intent, reset_session, ) class TestSessionState: def test_fresh_session_exploring(self): """A brand-new session with no history should infer 'exploring'.""" session = SessionState() assert infer_intent(session) == "exploring" def test_review_intent_detected(self): """Recording review-oriented tools should infer 'reviewing'.""" session = SessionState() for tool in ("detect_changes", "get_review_context", "get_affected_flows"): session.record_tool_call(tool) assert infer_intent(session) == "reviewing" def test_debug_intent_detected(self): """Recording debug-oriented tools should infer 'debugging'.""" session = SessionState() for tool in ("query_graph", "get_flow", "semantic_search_nodes"): session.record_tool_call(tool) assert infer_intent(session) == "debugging" def test_refactoring_intent_detected(self): """Recording refactoring-oriented tools should infer 'refactoring'.""" session = SessionState() for tool in ("refactor", "find_dead_code", "suggest_refactorings"): session.record_tool_call(tool) assert infer_intent(session) == "refactoring" def test_session_caps_history(self): """tools_called should never exceed 100 entries (FIFO).""" session = SessionState() for i in range(150): session.record_tool_call(f"tool_{i}") assert len(session.tools_called) == 100 # Oldest entries should have been evicted assert "tool_0" not in session.tools_called assert "tool_149" in session.tools_called def test_nodes_capped_at_1000(self): """nodes_queried should stop growing at 1000.""" session = SessionState() session.record_nodes([f"node_{i}" for i in range(1200)]) assert len(session.nodes_queried) == 1000 class TestGenerateHints: def test_hints_no_repeat(self): """Already-called tools must not appear in next_steps.""" session = SessionState() # Call list_flows, then generate hints for it # list_flows suggests get_flow, get_affected_flows, get_architecture_overview generate_hints("list_flows", {"status": "ok"}, session) # Now call get_flow and regenerate hints for list_flows hints2 = generate_hints("list_flows", {"status": "ok"}, session) suggested_tools2 = {s["tool"] for s in hints2["next_steps"]} # list_flows itself was called, so it shouldn't be suggested by get_flow workflow # Also, the first list_flows call should be excluded from next suggestions assert "list_flows" not in suggested_tools2 def test_hints_max_three(self): """Each hints category should have at most 3 entries.""" session = SessionState() # detect_changes has 4 workflow entries result = { "status": "ok", "test_gaps": [{"name": f"gap_{i}"} for i in range(10)], "risk_score": 0.9, "warnings": ["coupling warning 1", "coupling warning 2"], } hints = generate_hints("detect_changes", result, session) assert len(hints["next_steps"]) <= _MAX_PER_CATEGORY assert len(hints["warnings"]) <= _MAX_PER_CATEGORY assert len(hints["related"]) <= _MAX_PER_CATEGORY def test_warnings_from_result_test_gaps(self): """test_gaps in result should produce a warning.""" session = SessionState() result = { "status": "ok", "test_gaps": [{"name": "untested_func"}, {"name": "another_func"}], } hints = generate_hints("detect_changes", result, session) assert any("Test coverage gaps" in w for w in hints["warnings"]) assert any("untested_func" in w for w in hints["warnings"]) def test_warnings_from_result_risk_score(self): """High risk_score in result should produce a warning.""" session = SessionState() result = {"status": "ok", "risk_score": 0.85} hints = generate_hints("detect_changes", result, session) assert any("High risk score" in w for w in hints["warnings"]) def test_warnings_low_risk_no_warning(self): """Low risk_score should NOT produce a warning.""" session = SessionState() result = {"status": "ok", "risk_score": 0.3} hints = generate_hints("detect_changes", result, session) assert not any("High risk score" in w for w in hints["warnings"]) def test_generate_hints_empty_result(self): """An empty/minimal result should still return valid hints structure.""" session = SessionState() hints = generate_hints("list_flows", {}, session) assert "next_steps" in hints assert "related" in hints assert "warnings" in hints assert isinstance(hints["next_steps"], list) assert isinstance(hints["related"], list) assert isinstance(hints["warnings"], list) def test_generate_hints_unknown_tool(self): """An unrecognized tool name should still return valid hints.""" session = SessionState() hints = generate_hints("nonexistent_tool", {"status": "ok"}, session) assert hints["next_steps"] == [] assert hints["warnings"] == [] def test_session_records_files(self): """Files from result should be tracked in session state.""" session = SessionState() result = {"status": "ok", "changed_files": ["a.py", "b.py"]} generate_hints("detect_changes", result, session) assert "a.py" in session.files_touched assert "b.py" in session.files_touched def test_session_records_nodes(self): """Nodes from result should be tracked in session state.""" session = SessionState() result = { "status": "ok", "results": [ {"qualified_name": "mod.py::Foo", "name": "Foo"}, {"qualified_name": "mod.py::Bar", "name": "Bar"}, ], } generate_hints("semantic_search_nodes", result, session) assert "mod.py::Foo" in session.nodes_queried assert "mod.py::Bar" in session.nodes_queried def test_related_suggests_untouched_files(self): """Related should suggest impacted files not yet touched.""" session = SessionState() session.record_files(["already_seen.py"]) result = { "status": "ok", "impacted_files": ["already_seen.py", "new_file.py", "other.py"], } hints = generate_hints("detect_changes", result, session) assert "already_seen.py" not in hints["related"] assert "new_file.py" in hints["related"] class TestGlobalSession: def test_get_session_returns_singleton(self): """get_session should return the same object each time.""" reset_session() s1 = get_session() s2 = get_session() assert s1 is s2 def test_reset_session_creates_new(self): """reset_session should replace the global session.""" reset_session() s1 = get_session() s1.record_tool_call("foo") reset_session() s2 = get_session() assert len(s2.tools_called) == 0 assert s1 is not s2 def test_warnings_from_arch_overview_dict(self): """Architecture warnings as dicts with 'message' key should be extracted.""" session = SessionState() result = { "status": "ok", "warnings": [ {"message": "High coupling between A and B"}, {"message": "Circular dependency detected"}, ], } hints = generate_hints("get_architecture_overview", result, session) assert any("High coupling" in w for w in hints["warnings"]) assert any("Circular dependency" in w for w in hints["warnings"])