diff --git a/holmes/plugins/toolsets/prometheus/prometheus.py b/holmes/plugins/toolsets/prometheus/prometheus.py index ebc0ff6c3..4e05b5571 100644 --- a/holmes/plugins/toolsets/prometheus/prometheus.py +++ b/holmes/plugins/toolsets/prometheus/prometheus.py @@ -1611,7 +1611,7 @@ def _invoke(self, params: dict, context: ToolInvokeContext) -> StructuredToolRes # Get timeout parameter and enforce limits default_timeout = self.toolset.config.query_timeout_seconds_default max_timeout = self.toolset.config.query_timeout_seconds_hard_max - timeout = params.get("timeout", default_timeout) + timeout = params.get("timeout") or default_timeout if timeout > max_timeout: timeout = max_timeout logging.warning( @@ -1865,7 +1865,7 @@ def _invoke(self, params: dict, context: ToolInvokeContext) -> StructuredToolRes # Get timeout parameter and enforce limits default_timeout = self.toolset.config.query_timeout_seconds_default max_timeout = self.toolset.config.query_timeout_seconds_hard_max - timeout = params.get("timeout", default_timeout) + timeout = params.get("timeout") or default_timeout if timeout > max_timeout: timeout = max_timeout logging.warning( diff --git a/tests/plugins/toolsets/test_prometheus_unit.py b/tests/plugins/toolsets/test_prometheus_unit.py index a37e6fe53..3fe1d5f0c 100644 --- a/tests/plugins/toolsets/test_prometheus_unit.py +++ b/tests/plugins/toolsets/test_prometheus_unit.py @@ -1,9 +1,13 @@ import logging +from unittest.mock import MagicMock import pytest from holmes.plugins.toolsets.prometheus.prometheus import ( + DEFAULT_QUERY_TIMEOUT_SECONDS, AzurePrometheusConfig, + ExecuteInstantQuery, + ExecuteRangeQuery, PrometheusConfig, PrometheusToolset, adjust_step_for_max_points, @@ -329,3 +333,74 @@ def test_determine_prometheus_class_with_partial_azure_env( ) assert cls is PrometheusConfig assert "Partial Azure" in caplog.text + + +class TestNullTimeout: + """Regression: explicit null timeout from the LLM must not raise TypeError.""" + + def _make_toolset(self) -> PrometheusToolset: + toolset = PrometheusToolset() + toolset.config = PrometheusConfig( + prometheus_url="http://prometheus.example:9090" + ) + return toolset + + def _fake_response(self): + resp = MagicMock() + resp.status_code = 200 + resp.json.return_value = { + "status": "success", + "data": {"resultType": "vector", "result": []}, + } + return resp + + def test_instant_query_null_timeout_falls_back_to_default(self, monkeypatch): + toolset = self._make_toolset() + tool = ExecuteInstantQuery(toolset) + + captured: dict = {} + + def fake_do_request(*args, **kwargs): + captured["timeout"] = kwargs.get("timeout") + return self._fake_response() + + monkeypatch.setattr( + "holmes.plugins.toolsets.prometheus.prometheus.do_request", + fake_do_request, + ) + + result = tool._invoke( + {"query": "up", "description": "test", "timeout": None}, + context=MagicMock(), + ) + + assert "timeout" in captured + assert captured["timeout"] == DEFAULT_QUERY_TIMEOUT_SECONDS + + def test_range_query_null_timeout_falls_back_to_default(self, monkeypatch): + toolset = self._make_toolset() + tool = ExecuteRangeQuery(toolset) + + captured: dict = {} + + def fake_do_request(*args, **kwargs): + captured["timeout"] = kwargs.get("timeout") + return self._fake_response() + + monkeypatch.setattr( + "holmes.plugins.toolsets.prometheus.prometheus.do_request", + fake_do_request, + ) + + result = tool._invoke( + { + "query": "up", + "description": "test", + "output_type": "Plain", + "timeout": None, + }, + context=MagicMock(), + ) + + assert "timeout" in captured + assert captured["timeout"] == DEFAULT_QUERY_TIMEOUT_SECONDS