8Phase 5C tests for :class:`oct.mcp.metrics.McpMetrics` (Prometheus
13- Verify no-op behaviour when ``prometheus_client`` is not installed.
14- Verify ``record_call`` increments counters and records histogram.
15- Verify ``record_rate_limit_hit`` and ``record_validation_failure``
17- Verify ``start_server`` launches a background daemon thread.
18- Verify end-to-end metric recording through ``_run_tool``.
25 L3 — assertion details
30- Tests mock ``prometheus_client`` imports; no real metrics server is
34from __future__
import annotations
37from pathlib
import Path
38from unittest.mock
import MagicMock, patch, call
51 import prometheus_client
58 """McpMetrics on a private CollectorRegistry — avoids
59 ``Duplicated timeseries`` collisions across tests when
60 ``prometheus_client`` is installed."""
61 if not PROMETHEUS_AVAILABLE:
63 from prometheus_client
import CollectorRegistry
64 return McpMetrics(registry=CollectorRegistry())
73 assert isinstance(PROMETHEUS_AVAILABLE, bool)
84 """When prometheus_client is not installed, all methods should be no-ops."""
87 """Return a McpMetrics instance pretending prometheus_client is absent."""
88 with patch(
"oct.mcp.metrics.PROMETHEUS_AVAILABLE",
False):
89 m = McpMetrics.__new__(McpMetrics)
94 with patch(
"oct.mcp.metrics.PROMETHEUS_AVAILABLE",
False):
96 metrics.record_call(
"oct_lint",
"allowed", 0.05, 0)
99 with patch(
"oct.mcp.metrics.PROMETHEUS_AVAILABLE",
False):
101 metrics.record_rate_limit_hit(
"oct_lint")
104 with patch(
"oct.mcp.metrics.PROMETHEUS_AVAILABLE",
False):
106 metrics.record_validation_failure(
"oct_lint")
109 with patch(
"oct.mcp.metrics.PROMETHEUS_AVAILABLE",
False):
111 metrics.start_server(9090)
119 """Tests using mock Prometheus Counter / Histogram objects."""
122 """Build McpMetrics with mocked prometheus_client objects."""
123 mock_counter_class = MagicMock()
124 mock_histogram_class = MagicMock()
127 mock_counter_instance = MagicMock()
128 mock_counter_instance.labels.return_value = mock_counter_instance
129 mock_counter_class.return_value = mock_counter_instance
131 mock_histogram_instance = MagicMock()
132 mock_histogram_instance.labels.return_value = mock_histogram_instance
133 mock_histogram_class.return_value = mock_histogram_instance
135 with patch(
"oct.mcp.metrics.PROMETHEUS_AVAILABLE",
True):
136 with patch(
"oct.mcp.metrics.Counter", mock_counter_class):
137 with patch(
"oct.mcp.metrics.Histogram", mock_histogram_class):
140 return metrics, mock_counter_instance, mock_histogram_instance
142 @pytest.mark.skipif(not _is_prometheus_installed(), reason=
"prometheus_client not installed")
146 metrics.record_call(
"oct_lint",
"allowed", 0.1, 0)
148 @pytest.mark.skipif(not _is_prometheus_installed(), reason=
"prometheus_client not installed")
151 metrics.record_call(
"oct_lint",
"allowed", 0.25, 0)
153 @pytest.mark.skipif(not _is_prometheus_installed(), reason=
"prometheus_client not installed")
156 metrics.record_rate_limit_hit(
"oct_lint")
158 @pytest.mark.skipif(not _is_prometheus_installed(), reason=
"prometheus_client not installed")
161 metrics.record_validation_failure(
"oct_lint")
165 pytest.skip(
"prometheus_client not installed")
170 def __init__(self, target, args, daemon, kwargs=None):
179 started_threads.append(self)
181 with patch(
"oct.mcp.metrics.PROMETHEUS_AVAILABLE",
True):
182 with patch(
"oct.mcp.metrics.threading.Thread", MockThread):
183 with patch(
"oct.mcp.metrics._start_http"):
185 metrics.start_server(9090)
187 assert len(started_threads) == 1
188 assert started_threads[0].daemon
is True
190 assert "registry" in started_threads[0].kwargs
191 assert started_threads[0].kwargs[
"registry"]
is metrics._registry
194 """start_server with PROMETHEUS_AVAILABLE=False must not start any thread."""
196 threads_before = threading.active_count()
197 with patch(
"oct.mcp.metrics.PROMETHEUS_AVAILABLE",
False):
199 metrics.start_server(9090)
200 threads_after = threading.active_count()
201 assert threads_after == threads_before
204 """Verify record_call calls .labels(tool=..., decision=...) on counter."""
205 mock_counter = MagicMock()
206 mock_histogram = MagicMock()
210 with patch(
"oct.mcp.metrics.PROMETHEUS_AVAILABLE",
True):
211 metrics = McpMetrics.__new__(McpMetrics)
212 metrics._tool_calls = mock_counter
213 metrics._tool_duration = mock_histogram
214 metrics._redactions = MagicMock()
215 metrics._rate_limit_hits = MagicMock()
216 metrics._validation_failures = MagicMock()
217 metrics.record_call(
"oct_lint",
"allowed", 0.1, 0)
219 mock_counter.labels.assert_called_with(tool=
"oct_lint", decision=
"allowed")
222 """When redactions > 0, _redactions counter must be incremented."""
223 mock_redact_counter = MagicMock()
225 with patch(
"oct.mcp.metrics.PROMETHEUS_AVAILABLE",
True):
226 metrics = McpMetrics.__new__(McpMetrics)
228 metrics._tool_calls = MagicMock()
229 metrics._tool_duration = MagicMock()
230 metrics._redactions = mock_redact_counter
231 metrics._rate_limit_hits = MagicMock()
232 metrics._validation_failures = MagicMock()
233 metrics.record_call(
"oct_lint",
"allowed", 0.1, 3)
235 mock_redact_counter.labels.assert_called_with(tool=
"oct_lint")
236 mock_redact_counter.labels.return_value.inc.assert_called_with(3)
239 """When redactions == 0, _redactions counter must NOT be incremented."""
240 mock_redact_counter = MagicMock()
242 with patch(
"oct.mcp.metrics.PROMETHEUS_AVAILABLE",
True):
243 metrics = McpMetrics.__new__(McpMetrics)
244 metrics._tool_calls = MagicMock()
245 metrics._tool_duration = MagicMock()
246 metrics._redactions = mock_redact_counter
247 metrics._rate_limit_hits = MagicMock()
248 metrics._validation_failures = MagicMock()
249 metrics.record_call(
"oct_lint",
"allowed", 0.1, 0)
251 mock_redact_counter.labels.assert_not_called()
259 """Verify that _run_tool() calls the right metrics methods."""
271 (tmp_path /
"pyproject.toml").write_text(
272 '[project]\nname="test"\nversion="0.1"\n'
274 mock_sandbox = MagicMock(spec=SandboxExecutor)
275 mock_sandbox.run.return_value = SandboxResult(
276 exit_code=0, stdout=
"ok", stderr=
""
280 project_root=tmp_path,
282 policy=McpPolicy(profile=
"default", safe_mode=
False),
283 executor=ToolExecutor(sandbox=mock_sandbox),
285 audit_logger=McpAuditLogger(audit_log_path=tmp_path /
"audit.log"),
286 rate_limiter=RateLimiter(limit_per_minute=30),
287 safety=SafetyGate(config),
288 metrics=mock_metrics,
289 session_id=
"metrics-test-session",
296 mock_metrics = MagicMock(spec=McpMetrics)
298 _server_module._server_state = state
299 _run_tool(
"oct_lint", {})
300 mock_metrics.record_call.assert_called_once()
307 mock_metrics = MagicMock(spec=McpMetrics)
310 exhausted_limiter = MagicMock(spec=RateLimiter)
311 exhausted_limiter.consume.return_value =
False
312 state.rate_limiter = exhausted_limiter
313 _server_module._server_state = state
314 _run_tool(
"oct_lint", {})
315 mock_metrics.record_rate_limit_hit.assert_called_once_with(
"oct_lint")
323 mock_metrics = MagicMock(spec=McpMetrics)
325 _server_module._server_state = state
327 _run_tool(
"oct_lint", {
"paths": [
"../../etc/passwd"]})
328 mock_metrics.record_validation_failure.assert_called_once_with(
"oct_lint")
_make_state(self, Path tmp_path, McpMetrics mock_metrics)
test_run_tool_rate_limited_calls_record_rate_limit_hit(self, Path tmp_path)
test_run_tool_calls_record_call(self, Path tmp_path)
test_run_tool_validation_failure_calls_record_validation_failure(self, Path tmp_path)
McpMetrics _make_metrics_without_prometheus(self)
test_no_op_start_server_does_not_raise(self)
test_no_op_record_call_does_not_raise(self)
test_no_op_record_validation_failure_does_not_raise(self)
test_no_op_record_rate_limit_hit_does_not_raise(self)
test_start_server_starts_daemon_thread(self)
test_record_call_observes_histogram(self)
test_start_server_noop_when_absent(self)
test_record_call_with_mock_counter_labels_called(self)
test_record_validation_failure(self)
test_record_call_increments_tool_calls_counter(self)
test_record_call_does_not_increment_redactions_when_zero(self)
test_record_call_increments_redactions_when_positive(self)
test_record_rate_limit_hit(self)
test_prometheus_available_is_bool(self)
test_prometheus_available_matches_import(self)
McpMetrics _isolated_metrics()
bool _is_prometheus_installed()