diff --git a/tests/tools/test_session_search.py b/tests/tools/test_session_search.py index 852ac7b9e..f5d75bb91 100644 --- a/tests/tools/test_session_search.py +++ b/tests/tools/test_session_search.py @@ -290,6 +290,63 @@ class TestSessionSearch: assert result["results"] == [] assert result["sessions_searched"] == 0 + def test_limit_none_coerced_to_default(self): + """Model sends limit=null → should fall back to 3, not TypeError.""" + from unittest.mock import MagicMock + from tools.session_search_tool import session_search + + mock_db = MagicMock() + mock_db.search_messages.return_value = [] + + result = json.loads(session_search( + query="test", db=mock_db, limit=None, + )) + assert result["success"] is True + + def test_limit_type_object_coerced_to_default(self): + """Model sends limit as a type object → should fall back to 3, not TypeError.""" + from unittest.mock import MagicMock + from tools.session_search_tool import session_search + + mock_db = MagicMock() + mock_db.search_messages.return_value = [] + + result = json.loads(session_search( + query="test", db=mock_db, limit=int, + )) + assert result["success"] is True + + def test_limit_string_coerced(self): + """Model sends limit as string '2' → should coerce to int.""" + from unittest.mock import MagicMock + from tools.session_search_tool import session_search + + mock_db = MagicMock() + mock_db.search_messages.return_value = [] + + result = json.loads(session_search( + query="test", db=mock_db, limit="2", + )) + assert result["success"] is True + + def test_limit_clamped_to_range(self): + """Negative or zero limit should be clamped to 1.""" + from unittest.mock import MagicMock + from tools.session_search_tool import session_search + + mock_db = MagicMock() + mock_db.search_messages.return_value = [] + + result = json.loads(session_search( + query="test", db=mock_db, limit=-5, + )) + assert result["success"] is True + + result = json.loads(session_search( + query="test", db=mock_db, limit=0, + )) + assert result["success"] is True + def test_current_root_session_excludes_child_lineage(self): """Delegation child hits should be excluded when they resolve to the current root session.""" from unittest.mock import MagicMock diff --git a/tools/session_search_tool.py b/tools/session_search_tool.py index 9be73a04a..1398bdfff 100644 --- a/tools/session_search_tool.py +++ b/tools/session_search_tool.py @@ -310,7 +310,15 @@ def session_search( if db is None: return tool_error("Session database not available.", success=False) - limit = min(limit, 5) # Cap at 5 sessions to avoid excessive LLM calls + # Defensive: models (especially open-source) may send non-int limit values + # (None when JSON null, string "int", or even a type object). Coerce to a + # safe integer before any arithmetic/comparison to prevent TypeError. + if not isinstance(limit, int): + try: + limit = int(limit) + except (TypeError, ValueError): + limit = 3 + limit = max(1, min(limit, 5)) # Clamp to [1, 5] # Recent sessions mode: when query is empty, return metadata for recent sessions. # No LLM calls — just DB queries for titles, previews, timestamps.