4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
295 lines
11 KiB
Python
295 lines
11 KiB
Python
"""Tests for KafkaConsumerGroupTool (function-based, @tool decorated).
|
|
|
|
Covers:
|
|
- BaseToolContract: name, description, input_schema, source metadata
|
|
- is_available: True / False / absent-key / falsy-verified
|
|
- extract_params: all connection fields forwarded correctly
|
|
- run happy path: active consumer lag, zero-lag (healthy) group
|
|
- run error path: integration returns available=False
|
|
- run not-configured: empty bootstrap_servers short-circuits before any broker contact
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
from integrations.kafka.tools.kafka_consumer_group_tool import get_kafka_consumer_group_lag
|
|
from tests.tools.conftest import BaseToolContract
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Shared fixture data
|
|
# ---------------------------------------------------------------------------
|
|
|
|
_KAFKA_SOURCES = {
|
|
"kafka": {
|
|
"connection_verified": True,
|
|
"bootstrap_servers": "broker1:9092,broker2:9092",
|
|
"security_protocol": "SASL_SSL",
|
|
"sasl_mechanism": "PLAIN",
|
|
"sasl_username": "alice",
|
|
"sasl_password": "s3cr3t",
|
|
}
|
|
}
|
|
|
|
_CONSUMER_GROUP_LAG_RESPONSE = {
|
|
"source": "kafka",
|
|
"available": True,
|
|
"group_id": "payments-consumer",
|
|
"total_lag": 1500,
|
|
"partitions": [
|
|
{
|
|
"topic": "payments",
|
|
"partition": 0,
|
|
"committed_offset": 8500,
|
|
"high_watermark": 9200,
|
|
"lag": 700,
|
|
},
|
|
{
|
|
"topic": "payments",
|
|
"partition": 1,
|
|
"committed_offset": 7800,
|
|
"high_watermark": 8600,
|
|
"lag": 800,
|
|
},
|
|
],
|
|
}
|
|
|
|
_CONSUMER_GROUP_ZERO_LAG_RESPONSE = {
|
|
"source": "kafka",
|
|
"available": True,
|
|
"group_id": "events-consumer",
|
|
"total_lag": 0,
|
|
"partitions": [
|
|
{
|
|
"topic": "events",
|
|
"partition": 0,
|
|
"committed_offset": 5000,
|
|
"high_watermark": 5000,
|
|
"lag": 0,
|
|
},
|
|
],
|
|
}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Contract
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestKafkaConsumerGroupToolContract(BaseToolContract):
|
|
def get_tool_under_test(self):
|
|
return get_kafka_consumer_group_lag.__opensre_registered_tool__
|
|
|
|
|
|
def test_metadata() -> None:
|
|
rt = get_kafka_consumer_group_lag.__opensre_registered_tool__
|
|
assert rt.name == "get_kafka_consumer_group_lag"
|
|
assert rt.source == "kafka"
|
|
assert "investigation" in rt.surfaces
|
|
assert "chat" in rt.surfaces
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# is_available
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestKafkaConsumerGroupIsAvailable:
|
|
def _rt(self):
|
|
return get_kafka_consumer_group_lag.__opensre_registered_tool__
|
|
|
|
def test_true_when_connection_verified(self) -> None:
|
|
assert self._rt().is_available({"kafka": {"connection_verified": True}}) is True
|
|
|
|
def test_false_when_connection_verified_is_false(self) -> None:
|
|
assert self._rt().is_available({"kafka": {"connection_verified": False}}) is False
|
|
|
|
def test_false_when_kafka_key_absent(self) -> None:
|
|
assert self._rt().is_available({}) is False
|
|
|
|
def test_false_when_kafka_is_empty_dict(self) -> None:
|
|
assert self._rt().is_available({"kafka": {}}) is False
|
|
|
|
def test_false_when_connection_verified_is_none(self) -> None:
|
|
assert self._rt().is_available({"kafka": {"connection_verified": None}}) is False
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# extract_params
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestKafkaConsumerGroupExtractParams:
|
|
def _rt(self):
|
|
return get_kafka_consumer_group_lag.__opensre_registered_tool__
|
|
|
|
def test_extracts_all_connection_fields(self) -> None:
|
|
params = self._rt().extract_params(_KAFKA_SOURCES)
|
|
assert params["bootstrap_servers"] == "broker1:9092,broker2:9092"
|
|
assert params["security_protocol"] == "SASL_SSL"
|
|
assert params["sasl_mechanism"] == "PLAIN"
|
|
assert params["sasl_username"] == "alice"
|
|
assert params["sasl_password"] == "s3cr3t"
|
|
|
|
def test_returns_empty_strings_when_kafka_absent(self) -> None:
|
|
params = self._rt().extract_params({})
|
|
assert params["bootstrap_servers"] == ""
|
|
# security_protocol defaults to PLAINTEXT when absent
|
|
assert params["security_protocol"] == "PLAINTEXT"
|
|
assert params["sasl_mechanism"] == ""
|
|
assert params["sasl_username"] == ""
|
|
assert params["sasl_password"] == ""
|
|
|
|
def test_strips_whitespace_from_bootstrap_servers(self) -> None:
|
|
sources = {"kafka": {"connection_verified": True, "bootstrap_servers": " broker:9092 "}}
|
|
params = self._rt().extract_params(sources)
|
|
assert params["bootstrap_servers"] == "broker:9092"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# run — happy paths
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestKafkaConsumerGroupRun:
|
|
def test_happy_path_returns_total_lag(self) -> None:
|
|
with patch(
|
|
"integrations.kafka.tools.kafka_consumer_group_tool.get_consumer_group_lag",
|
|
return_value=_CONSUMER_GROUP_LAG_RESPONSE,
|
|
):
|
|
result = get_kafka_consumer_group_lag(
|
|
bootstrap_servers="broker1:9092",
|
|
group_id="payments-consumer",
|
|
)
|
|
|
|
assert result["available"] is True
|
|
assert result["group_id"] == "payments-consumer"
|
|
assert result["total_lag"] == 1500
|
|
assert result["source"] == "kafka"
|
|
|
|
def test_happy_path_partition_level_lag_detail(self) -> None:
|
|
with patch(
|
|
"integrations.kafka.tools.kafka_consumer_group_tool.get_consumer_group_lag",
|
|
return_value=_CONSUMER_GROUP_LAG_RESPONSE,
|
|
):
|
|
result = get_kafka_consumer_group_lag(
|
|
bootstrap_servers="broker1:9092",
|
|
group_id="payments-consumer",
|
|
)
|
|
|
|
assert len(result["partitions"]) == 2
|
|
lags = {p["partition"]: p["lag"] for p in result["partitions"]}
|
|
assert lags[0] == 700
|
|
assert lags[1] == 800
|
|
|
|
def test_happy_path_zero_lag_healthy_group(self) -> None:
|
|
with patch(
|
|
"integrations.kafka.tools.kafka_consumer_group_tool.get_consumer_group_lag",
|
|
return_value=_CONSUMER_GROUP_ZERO_LAG_RESPONSE,
|
|
):
|
|
result = get_kafka_consumer_group_lag(
|
|
bootstrap_servers="broker1:9092",
|
|
group_id="events-consumer",
|
|
)
|
|
|
|
assert result["available"] is True
|
|
assert result["total_lag"] == 0
|
|
assert result["partitions"][0]["lag"] == 0
|
|
assert (
|
|
result["partitions"][0]["committed_offset"] == result["partitions"][0]["high_watermark"]
|
|
)
|
|
|
|
def test_happy_path_forwards_group_id_to_integration(self) -> None:
|
|
with patch(
|
|
"integrations.kafka.tools.kafka_consumer_group_tool.get_consumer_group_lag",
|
|
return_value=_CONSUMER_GROUP_ZERO_LAG_RESPONSE,
|
|
) as mock_fn:
|
|
get_kafka_consumer_group_lag(
|
|
bootstrap_servers="broker1:9092",
|
|
group_id="events-consumer",
|
|
)
|
|
|
|
_, call_kwargs = mock_fn.call_args
|
|
assert call_kwargs.get("group_id") == "events-consumer"
|
|
|
|
def test_happy_path_sasl_ssl_connection(self) -> None:
|
|
with patch(
|
|
"integrations.kafka.tools.kafka_consumer_group_tool.get_consumer_group_lag",
|
|
return_value=_CONSUMER_GROUP_LAG_RESPONSE,
|
|
) as mock_fn:
|
|
result = get_kafka_consumer_group_lag(
|
|
bootstrap_servers="broker1:9093",
|
|
group_id="payments-consumer",
|
|
security_protocol="SASL_SSL",
|
|
sasl_mechanism="PLAIN",
|
|
sasl_username="alice",
|
|
sasl_password="s3cr3t",
|
|
)
|
|
|
|
assert result["available"] is True
|
|
# Verify SASL credentials were wired into the KafkaConfig forwarded to the integration.
|
|
config_arg = mock_fn.call_args[0][0]
|
|
assert config_arg.security_protocol == "SASL_SSL"
|
|
assert config_arg.sasl_mechanism == "PLAIN"
|
|
assert config_arg.sasl_username == "alice"
|
|
assert config_arg.sasl_password == "s3cr3t"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# run — error / not-configured paths
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_error_path_returns_unavailable_dict(self) -> None:
|
|
fake_error = {
|
|
"source": "kafka",
|
|
"available": False,
|
|
"error": "Group 'stale-consumer' does not exist.",
|
|
}
|
|
with patch(
|
|
"integrations.kafka.tools.kafka_consumer_group_tool.get_consumer_group_lag",
|
|
return_value=fake_error,
|
|
):
|
|
result = get_kafka_consumer_group_lag(
|
|
bootstrap_servers="broker1:9092",
|
|
group_id="stale-consumer",
|
|
)
|
|
|
|
assert result["available"] is False
|
|
assert "error" in result
|
|
assert result["source"] == "kafka"
|
|
|
|
def test_error_path_propagates_exception_from_integration(self) -> None:
|
|
# If the integration ever raises instead of returning an error dict,
|
|
# the tool should let the exception propagate (no silent swallowing).
|
|
with (
|
|
patch(
|
|
"integrations.kafka.tools.kafka_consumer_group_tool.get_consumer_group_lag",
|
|
side_effect=RuntimeError("consumer group timeout"),
|
|
),
|
|
pytest.raises(RuntimeError, match="consumer group timeout"),
|
|
):
|
|
get_kafka_consumer_group_lag(
|
|
bootstrap_servers="broker1:9092",
|
|
group_id="payments-consumer",
|
|
)
|
|
|
|
def test_not_configured_returns_unavailable_without_broker_contact(self) -> None:
|
|
# Empty bootstrap_servers → KafkaConfig.is_configured is False.
|
|
# The integration short-circuits before touching confluent_kafka.
|
|
with patch(
|
|
"integrations.kafka.tools.kafka_consumer_group_tool.get_consumer_group_lag",
|
|
wraps=__import__(
|
|
"integrations.kafka", fromlist=["get_consumer_group_lag"]
|
|
).get_consumer_group_lag,
|
|
) as mock_fn:
|
|
result = get_kafka_consumer_group_lag(
|
|
bootstrap_servers="",
|
|
group_id="payments-consumer",
|
|
)
|
|
|
|
assert result["available"] is False
|
|
assert result["source"] == "kafka"
|
|
# Confirm the integration was entered but short-circuited before broker contact.
|
|
mock_fn.assert_called_once()
|