diff --git a/lib/crewai-tools/src/crewai_tools/rag/data_types.py b/lib/crewai-tools/src/crewai_tools/rag/data_types.py index 27ee48abb..0fcf07e12 100644 --- a/lib/crewai-tools/src/crewai_tools/rag/data_types.py +++ b/lib/crewai-tools/src/crewai_tools/rag/data_types.py @@ -135,7 +135,8 @@ class DataTypes: if "docs" in url.netloc or ("docs" in url.path and url.scheme != "file"): return DataType.DOCS_SITE - if "github.com" in url.netloc: + hostname = (url.hostname or "").lower() + if hostname == "github.com" or hostname.endswith(".github.com"): return DataType.GITHUB return DataType.WEBSITE diff --git a/lib/crewai-tools/tests/rag/test_data_types.py b/lib/crewai-tools/tests/rag/test_data_types.py new file mode 100644 index 000000000..edb398801 --- /dev/null +++ b/lib/crewai-tools/tests/rag/test_data_types.py @@ -0,0 +1,32 @@ +"""Tests for DataTypes content classification.""" + +from crewai_tools.rag.data_types import DataType, DataTypes + + +class TestDataTypesFromContentGitHub: + """GitHub URL detection must use hostname matching, not substrings.""" + + def test_github_com_url(self) -> None: + assert ( + DataTypes.from_content("https://github.com/crewai/crewai") + == DataType.GITHUB + ) + + def test_github_subdomain_url(self) -> None: + assert ( + DataTypes.from_content("https://gist.github.com/user/abc") + == DataType.GITHUB + ) + + def test_spoofed_github_hostname_is_website(self) -> None: + # Substring checks like `"github.com" in netloc` would misclassify this. + assert ( + DataTypes.from_content("https://github.com.evil.example/crewai") + == DataType.WEBSITE + ) + + def test_github_in_path_is_not_github(self) -> None: + assert ( + DataTypes.from_content("https://example.com/github.com/repo") + == DataType.WEBSITE + ) diff --git a/lib/crewai-tools/tests/tools/stagehand_tool_test.py b/lib/crewai-tools/tests/tools/stagehand_tool_test.py index 508b1357e..eefe1340e 100644 --- a/lib/crewai-tools/tests/tools/stagehand_tool_test.py +++ b/lib/crewai-tools/tests/tools/stagehand_tool_test.py @@ -163,8 +163,14 @@ def test_navigate_command(mock_run, stagehand_tool): command_type="navigate", ) - # Assertions - assert "https://example.com" in result + # Assertions — compare the full mocked result (avoid URL substring checks) + assert result == "Successfully navigated to https://example.com" + mock_run.assert_called_once_with( + stagehand_tool, + instruction="Go to example.com", + url="https://example.com", + command_type="navigate", + ) @patch( diff --git a/lib/crewai/tests/llms/test_tool_call_streaming.py b/lib/crewai/tests/llms/test_tool_call_streaming.py index 7985aecca..03bc0fd9b 100644 --- a/lib/crewai/tests/llms/test_tool_call_streaming.py +++ b/lib/crewai/tests/llms/test_tool_call_streaming.py @@ -38,18 +38,33 @@ def get_temperature_tool_schema() -> dict[str, Any]: @pytest.fixture def mock_emit() -> MagicMock: - """Mock the event bus emit function.""" - from crewai.events.event_bus import CrewAIEventsBus + """Mock the singleton event bus emit used by LLM providers. - with patch.object(CrewAIEventsBus, "emit") as mock: - yield mock + Patch the singleton instance (not only the class) so a leftover + instance-level ``emit`` from other tests cannot shadow the mock. + """ + from crewai.events.event_bus import CrewAIEventsBus, crewai_event_bus + + with ( + patch.object(CrewAIEventsBus, "emit") as class_mock, + patch.object(crewai_event_bus, "emit", new=class_mock), + ): + yield class_mock + + +def _event_from_emit_call(call: Any) -> Any: + """Return the event argument from an emit mock call.""" + event = call.kwargs.get("event") + if event is None and len(call.args) >= 2: + event = call.args[1] + return event def get_tool_call_events(mock_emit: MagicMock) -> list[LLMStreamChunkEvent]: """Extract tool call streaming events from mock emit calls.""" tool_call_events = [] for call in mock_emit.call_args_list: - event = call[1].get("event") if len(call) > 1 else None + event = _event_from_emit_call(call) if isinstance(event, LLMStreamChunkEvent) and event.call_type == LLMCallType.TOOL_CALL: tool_call_events.append(event) return tool_call_events @@ -59,7 +74,7 @@ def get_all_stream_events(mock_emit: MagicMock) -> list[LLMStreamChunkEvent]: """Extract all streaming events from mock emit calls.""" stream_events = [] for call in mock_emit.call_args_list: - event = call[1].get("event") if len(call) > 1 else None + event = _event_from_emit_call(call) if isinstance(event, LLMStreamChunkEvent): stream_events.append(event) return stream_events