Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

340 lines
13 KiB
Python

"""Redis E2E tests verifying integration with investigation pipeline.
Tests:
- Redis config resolution from store and env
- Redis verification (ping, server info)
- Redis source detection in investigation state
- Redis tools availability for query execution
"""
from __future__ import annotations
import json
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
from integrations.catalog import classify_integrations as _classify_integrations
from integrations.verify import verify_integrations
from tests.e2e.source_helpers import resolve_available_tool_sources
class TestRedisIntegrationResolution:
"""Test Redis config resolution from multiple sources."""
def test_redis_resolution_from_store(self):
"""Redis integration correctly resolved from local store."""
integrations = [
{
"id": "redis-prod",
"service": "redis",
"status": "active",
"credentials": {
"host": "prod-cache.redis.internal",
"port": 6380,
"username": "monitor",
"password": "s3cret",
"db": 1,
"ssl": True,
},
}
]
resolved = _classify_integrations(integrations)
assert "redis" in resolved
assert resolved["redis"]["host"] == "prod-cache.redis.internal"
assert resolved["redis"]["port"] == 6380
assert resolved["redis"]["username"] == "monitor"
assert resolved["redis"]["db"] == 1
assert resolved["redis"]["ssl"] is True
def test_redis_invalid_config_skipped(self):
"""Invalid Redis integration config is safely skipped."""
integrations = [
{
"id": "bad-redis",
"service": "redis",
"status": "active",
"credentials": {
"host": "",
},
}
]
resolved = _classify_integrations(integrations)
# Should not include Redis if host is empty
assert resolved.get("redis") is None
class TestRedisToolSourceAvailability:
"""Test Redis source availability in the tool-registry investigation path."""
def test_redis_tool_source_available_from_resolved_integration(self):
"""Redis source is available when a configured integration exists."""
resolved_integrations = {
"redis": {
"host": "localhost",
"port": 6379,
"username": "",
"password": "",
"db": 0,
"ssl": False,
}
}
sources = resolve_available_tool_sources(resolved_integrations)
assert "redis" in sources
assert sources["redis"]["host"] == "localhost"
assert sources["redis"]["port"] == 6379
def test_redis_tool_source_uses_configured_db(self):
"""Redis tool params come from the resolved integration config."""
resolved_integrations = {
"redis": {
"host": "localhost",
"port": 6379,
"username": "",
"password": "",
"db": 3,
"ssl": False,
}
}
sources = resolve_available_tool_sources(resolved_integrations)
assert "redis" in sources
assert sources["redis"]["db"] == 3
def test_redis_tool_source_unavailable_if_unconfigured(self):
"""Redis source is not included if not configured."""
resolved_integrations = {}
sources = resolve_available_tool_sources(resolved_integrations)
assert "redis" not in sources
class TestRedisVerification:
"""Test Redis integration verification flow."""
@patch("integrations.redis._get_client")
def test_verify_redis_success(self, mock_get_client):
"""Redis verification succeeds with valid config."""
mock_client = MagicMock()
mock_client.ping.return_value = True
mock_client.info.return_value = {"redis_version": "7.2.4"}
mock_get_client.return_value = mock_client
results = verify_integrations(service="redis")
assert len(results) >= 1
redis_result = next((r for r in results if r["service"] == "redis"), None)
assert redis_result is not None
# Status can be passed or missing depending on env config
assert redis_result["status"] in ("passed", "missing")
def test_verify_integrations_structure(self):
"""Verify integrations returns expected result structure."""
# Just verify the function exists and can be called - actual verification
# depends on environment setup (Redis connection available)
try:
results = verify_integrations(service="redis")
assert isinstance(results, list)
for result in results:
if result["service"] == "redis":
assert "status" in result
assert "detail" in result
assert result["status"] in ("passed", "missing", "failed")
except Exception as exc:
# If no Redis is configured, that's ok - just testing structure
assert exc.__class__.__name__
class TestRedisToolsAvailability:
"""Test Redis tools are available and configured."""
@pytest.fixture(autouse=True)
def _clear_registry_cache(self):
"""Force fresh tool discovery so registry assertions never depend on a
cache populated by a prior test (per the ``tests/`` convention that any
test calling ``get_registered_tools()`` clears the cache in a fixture)."""
from tools.registry import clear_tool_registry_cache
clear_tool_registry_cache()
yield
clear_tool_registry_cache()
def test_redis_tools_exist_as_modules(self):
"""Redis tools modules exist and are properly structured."""
try:
import importlib
redis_client_list_tool = importlib.import_module(
"integrations.redis.tools.redis_client_list_tool"
)
redis_key_scan_tool = importlib.import_module(
"integrations.redis.tools.redis_key_scan_tool"
)
redis_latency_doctor_tool = importlib.import_module(
"integrations.redis.tools.redis_latency_doctor_tool"
)
redis_list_depth_tool = importlib.import_module(
"integrations.redis.tools.redis_list_depth_tool"
)
redis_replication_tool = importlib.import_module(
"integrations.redis.tools.redis_replication_tool"
)
redis_server_info_tool = importlib.import_module(
"integrations.redis.tools.redis_server_info_tool"
)
redis_slowlog_tool = importlib.import_module(
"integrations.redis.tools.redis_slowlog_tool"
)
# All 7 tool modules should be importable (4 baseline + 3 P1)
assert redis_server_info_tool is not None
assert redis_slowlog_tool is not None
assert redis_replication_tool is not None
assert redis_key_scan_tool is not None
assert redis_client_list_tool is not None
assert redis_list_depth_tool is not None
assert redis_latency_doctor_tool is not None
except ImportError as e:
pytest.fail(f"Failed to import Redis tool modules: {e}")
def test_p1_tools_registered_on_investigation_surface(self):
"""The three new P1 tools are discoverable on the investigation and chat surfaces."""
from tools.registry import get_registered_tools
p1_tools = {
"get_redis_client_list",
"get_redis_list_depth",
"get_redis_latency_doctor",
}
for surface in ("investigation", "chat"):
names = {t.name for t in get_registered_tools(surface) if t.source == "redis"}
assert p1_tools <= names, (
f"missing P1 redis tools on {surface} surface: {p1_tools - names}"
)
def test_redis_integration_config_has_required_fields(self):
"""Redis integration provides required fields in resolved config."""
from integrations.config_models import RedisIntegrationConfig
config = RedisIntegrationConfig(
host="localhost",
port=6379,
username="monitor",
password="s3cret",
db=0,
ssl=True,
integration_id="test-id",
)
assert config.host == "localhost"
assert config.port == 6379
assert config.username == "monitor"
assert config.db == 0
assert config.ssl is True
assert config.integration_id == "test-id"
class TestRedisP1ToolPaths:
"""Exercise each new P1 tool end-to-end: tool fn -> helper -> client -> shape.
The Redis client is mocked at the transport boundary so the full tool path
(config build, command issue, response shaping, bounded sampling) is
covered without a live Redis, mirroring how a real investigation would call
these tools with credentials resolved from the integration config.
"""
@patch("integrations.redis._get_client")
def test_client_list_tool_path(self, mock_get_client):
from integrations.redis.tools.redis_client_list_tool import get_redis_client_list
mock_client = MagicMock()
mock_client.client_list.return_value = [
{"id": "1", "addr": "10.0.0.1:5000", "flags": "N", "idle": "0", "cmd": "get"},
{"id": "2", "addr": "10.0.0.1:5001", "flags": "b", "idle": "5", "cmd": "blpop"},
]
mock_get_client.return_value = mock_client
result = get_redis_client_list(host="prod-cache.redis.internal", port=6379, db=0)
assert result["available"] is True
assert result["total_clients"] == 2
assert result["blocked_clients"] == 1
assert result["address_breakdown"]["10.0.0.1"] == 2
@patch("integrations.redis._get_client")
def test_list_depth_tool_path(self, mock_get_client):
from integrations.redis.tools.redis_list_depth_tool import get_redis_list_depth
mock_client = MagicMock()
mock_client.type.return_value = "list"
mock_client.pipeline.return_value.execute.return_value = [1500, ["job-1"], ["job-1500"]]
mock_get_client.return_value = mock_client
result = get_redis_list_depth(
key="sidekiq:queue:default",
host="prod-cache.redis.internal",
head=1,
tail=1,
)
assert result["available"] is True
assert result["depth"] == 1500 # a real backlog
assert result["head"] == ["job-1"]
assert result["tail"] == ["job-1500"]
@patch("integrations.redis._get_client")
def test_latency_doctor_tool_path(self, mock_get_client):
from integrations.redis.tools.redis_latency_doctor_tool import get_redis_latency_doctor
mock_client = MagicMock()
mock_client.execute_command.return_value = "I detected spikes caused by fork."
mock_client.latency_latest.return_value = [["fork", 1700000000, 480, 1200]]
# Explicitly configure CONFIG GET so monitoring_active is driven by the
# real threshold (> 0) — not MagicMock's implicit __int__ == 1, which
# would make the assertion pass even if the threshold logic regressed.
mock_client.config_get.return_value = {"latency-monitor-threshold": "100"}
mock_get_client.return_value = mock_client
result = get_redis_latency_doctor(host="prod-cache.redis.internal")
assert result["available"] is True
assert result["monitoring_active"] is True
assert result["monitoring_threshold_ms"] == 100
assert result["latest"][0]["event"] == "fork"
assert "fork" in result["report"]
class TestRedisAlertFixture:
"""Test the Redis alert fixture is valid and parseable."""
def test_redis_alert_fixture_is_valid_json(self):
"""Redis alert fixture is valid JSON."""
fixture_path = Path(__file__).parent / "redis_alert.json"
assert fixture_path.exists(), f"Alert fixture not found at {fixture_path}"
with fixture_path.open() as f:
alert = json.load(f)
assert isinstance(alert, dict)
assert "state" in alert
assert "commonLabels" in alert
assert "commonAnnotations" in alert
def test_redis_alert_fixture_has_redis_context(self):
"""Redis alert fixture contains Redis-specific context."""
fixture_path = Path(__file__).parent / "redis_alert.json"
with fixture_path.open() as f:
alert = json.load(f)
labels = alert.get("commonLabels", {})
# Alert should have Redis-specific fields for source detection
assert "redis_instance" in labels