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
536 lines
21 KiB
Python
536 lines
21 KiB
Python
"""Alertmanager E2E tests verifying integration with investigation pipeline.
|
|
|
|
Tests:
|
|
- Alertmanager config resolution from store and env
|
|
- Alertmanager verification (connectivity, status check)
|
|
- Alertmanager source detection in investigation context
|
|
- Alertmanager tools availability and structure
|
|
- Alert fixture validity (Alertmanager webhook format)
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
from integrations.catalog import (
|
|
classify_integrations as _classify_integrations,
|
|
)
|
|
from integrations.catalog import (
|
|
load_env_integrations as _load_env_integrations,
|
|
)
|
|
from tests.e2e.source_helpers import resolve_available_tool_sources
|
|
|
|
|
|
class TestAlertmanagerIntegrationResolution:
|
|
"""Test Alertmanager config resolution from multiple sources."""
|
|
|
|
def test_alertmanager_resolution_from_store(self):
|
|
"""Alertmanager integration correctly resolved from local store."""
|
|
integrations = [
|
|
{
|
|
"id": "alertmanager-prod",
|
|
"service": "alertmanager",
|
|
"status": "active",
|
|
"credentials": {
|
|
"base_url": "http://alertmanager.monitoring.svc:9093",
|
|
"bearer_token": "",
|
|
"username": "",
|
|
"password": "",
|
|
},
|
|
}
|
|
]
|
|
resolved = _classify_integrations(integrations)
|
|
|
|
assert "alertmanager" in resolved
|
|
assert resolved["alertmanager"]["base_url"] == "http://alertmanager.monitoring.svc:9093"
|
|
|
|
def test_alertmanager_resolution_with_bearer_token(self):
|
|
"""Alertmanager integration with bearer token is correctly resolved."""
|
|
integrations = [
|
|
{
|
|
"id": "alertmanager-auth",
|
|
"service": "alertmanager",
|
|
"status": "active",
|
|
"credentials": {
|
|
"base_url": "https://alertmanager.example.com",
|
|
"bearer_token": "my-secret-token",
|
|
},
|
|
}
|
|
]
|
|
resolved = _classify_integrations(integrations)
|
|
|
|
assert "alertmanager" in resolved
|
|
assert resolved["alertmanager"]["base_url"] == "https://alertmanager.example.com"
|
|
assert resolved["alertmanager"]["bearer_token"] == "my-secret-token"
|
|
|
|
def test_alertmanager_resolution_with_basic_auth(self):
|
|
"""Alertmanager integration with basic auth is correctly resolved."""
|
|
integrations = [
|
|
{
|
|
"id": "alertmanager-basic",
|
|
"service": "alertmanager",
|
|
"status": "active",
|
|
"credentials": {
|
|
"base_url": "https://alertmanager.example.com",
|
|
"username": "admin",
|
|
"password": "secret",
|
|
},
|
|
}
|
|
]
|
|
resolved = _classify_integrations(integrations)
|
|
|
|
assert "alertmanager" in resolved
|
|
assert resolved["alertmanager"]["username"] == "admin"
|
|
assert resolved["alertmanager"]["password"] == "secret"
|
|
|
|
def test_alertmanager_empty_base_url_skipped(self):
|
|
"""Alertmanager integration without base_url is safely skipped."""
|
|
integrations = [
|
|
{
|
|
"id": "bad-alertmanager",
|
|
"service": "alertmanager",
|
|
"status": "active",
|
|
"credentials": {
|
|
"base_url": "",
|
|
},
|
|
}
|
|
]
|
|
resolved = _classify_integrations(integrations)
|
|
|
|
assert "alertmanager" not in resolved
|
|
|
|
def test_alertmanager_inactive_integration_skipped(self):
|
|
"""Inactive Alertmanager integration is not resolved."""
|
|
integrations = [
|
|
{
|
|
"id": "alertmanager-inactive",
|
|
"service": "alertmanager",
|
|
"status": "inactive",
|
|
"credentials": {
|
|
"base_url": "http://alertmanager.example.com:9093",
|
|
},
|
|
}
|
|
]
|
|
resolved = _classify_integrations(integrations)
|
|
|
|
assert "alertmanager" not in resolved
|
|
|
|
def test_alertmanager_url_trailing_slash_normalized(self):
|
|
"""Trailing slash is stripped from Alertmanager base_url."""
|
|
integrations = [
|
|
{
|
|
"id": "alertmanager-slash",
|
|
"service": "alertmanager",
|
|
"status": "active",
|
|
"credentials": {
|
|
"base_url": "http://alertmanager.example.com:9093/",
|
|
},
|
|
}
|
|
]
|
|
resolved = _classify_integrations(integrations)
|
|
|
|
assert "alertmanager" in resolved
|
|
assert resolved["alertmanager"]["base_url"] == "http://alertmanager.example.com:9093"
|
|
|
|
|
|
class TestAlertmanagerEnvResolution:
|
|
"""Test Alertmanager config resolution from environment variables."""
|
|
|
|
def test_alertmanager_resolved_from_env(self, monkeypatch):
|
|
"""Alertmanager integration resolved from ALERTMANAGER_URL env var."""
|
|
monkeypatch.setenv("ALERTMANAGER_URL", "http://alertmanager.monitoring.svc:9093")
|
|
|
|
env_integrations = _load_env_integrations()
|
|
alertmanager_records = [i for i in env_integrations if i["service"] == "alertmanager"]
|
|
|
|
assert len(alertmanager_records) == 1
|
|
creds = alertmanager_records[0]["credentials"]
|
|
assert creds["base_url"] == "http://alertmanager.monitoring.svc:9093"
|
|
|
|
def test_alertmanager_bearer_token_from_env(self, monkeypatch):
|
|
"""Alertmanager bearer token loaded from env var."""
|
|
monkeypatch.setenv("ALERTMANAGER_URL", "https://alertmanager.example.com")
|
|
monkeypatch.setenv("ALERTMANAGER_BEARER_TOKEN", "env-token-123")
|
|
|
|
env_integrations = _load_env_integrations()
|
|
alertmanager_records = [i for i in env_integrations if i["service"] == "alertmanager"]
|
|
|
|
assert len(alertmanager_records) == 1
|
|
assert alertmanager_records[0]["credentials"]["bearer_token"] == "env-token-123"
|
|
|
|
def test_alertmanager_not_loaded_when_url_missing(self, monkeypatch):
|
|
"""Alertmanager integration is not loaded from env when ALERTMANAGER_URL is unset."""
|
|
monkeypatch.delenv("ALERTMANAGER_URL", raising=False)
|
|
|
|
env_integrations = _load_env_integrations()
|
|
alertmanager_records = [i for i in env_integrations if i["service"] == "alertmanager"]
|
|
|
|
assert len(alertmanager_records) == 0
|
|
|
|
|
|
class TestAlertmanagerToolSourceAvailability:
|
|
"""Test Alertmanager source availability in the tool-registry investigation path."""
|
|
|
|
def test_alertmanager_detected_when_configured(self):
|
|
"""Alertmanager source is available when integration is configured."""
|
|
resolved_integrations = {
|
|
"alertmanager": {
|
|
"base_url": "http://alertmanager.monitoring.svc:9093",
|
|
"bearer_token": "",
|
|
"username": "",
|
|
"password": "",
|
|
}
|
|
}
|
|
|
|
sources = resolve_available_tool_sources(resolved_integrations)
|
|
|
|
assert "alertmanager" in sources
|
|
assert sources["alertmanager"]["base_url"] == "http://alertmanager.monitoring.svc:9093"
|
|
|
|
def test_alertmanager_source_preserves_configured_filter_labels(self):
|
|
"""Alertmanager tool params preserve filter labels from resolved config."""
|
|
resolved_integrations = {
|
|
"alertmanager": {
|
|
"base_url": "http://alertmanager.monitoring.svc:9093",
|
|
"filter_labels": ['alertname="HighErrorRate"'],
|
|
}
|
|
}
|
|
|
|
sources = resolve_available_tool_sources(resolved_integrations)
|
|
|
|
assert "alertmanager" in sources
|
|
assert 'alertname="HighErrorRate"' in sources["alertmanager"]["filter_labels"]
|
|
|
|
def test_alertmanager_not_detected_when_unconfigured(self):
|
|
"""Alertmanager source is not included when integration is not configured."""
|
|
resolved_integrations = {}
|
|
|
|
sources = resolve_available_tool_sources(resolved_integrations)
|
|
|
|
assert "alertmanager" not in sources
|
|
|
|
def test_alertmanager_not_detected_when_base_url_empty(self):
|
|
"""Alertmanager source is not included when base_url is empty."""
|
|
resolved_integrations = {
|
|
"alertmanager": {
|
|
"base_url": "",
|
|
}
|
|
}
|
|
|
|
sources = resolve_available_tool_sources(resolved_integrations)
|
|
|
|
assert "alertmanager" not in sources
|
|
|
|
|
|
class TestAlertmanagerVerification:
|
|
"""Test Alertmanager integration verification flow."""
|
|
|
|
@patch("integrations.alertmanager.client.AlertmanagerClient.get_status")
|
|
def test_verify_alertmanager_success(self, mock_get_status, monkeypatch):
|
|
"""Alertmanager verification passes when status endpoint responds successfully."""
|
|
monkeypatch.setenv("ALERTMANAGER_URL", "http://alertmanager.monitoring.svc:9093")
|
|
mock_get_status.return_value = {
|
|
"success": True,
|
|
"status": {
|
|
"cluster": {"status": "ready"},
|
|
"versionInfo": {"version": "0.27.0"},
|
|
},
|
|
}
|
|
|
|
from integrations.verify import verify_integrations
|
|
|
|
results = verify_integrations(service="alertmanager")
|
|
|
|
assert len(results) >= 1
|
|
am_result = next((r for r in results if r["service"] == "alertmanager"), None)
|
|
assert am_result is not None
|
|
assert am_result["status"] == "passed"
|
|
assert "alertmanager" in am_result["detail"].lower()
|
|
|
|
@patch("integrations.alertmanager.client.AlertmanagerClient.get_status")
|
|
def test_verify_alertmanager_failure(self, mock_get_status, monkeypatch):
|
|
"""Alertmanager verification fails when status endpoint is unreachable."""
|
|
monkeypatch.setenv("ALERTMANAGER_URL", "http://alertmanager.monitoring.svc:9093")
|
|
mock_get_status.return_value = {
|
|
"success": False,
|
|
"error": "Connection refused",
|
|
}
|
|
|
|
from integrations.verify import verify_integrations
|
|
|
|
results = verify_integrations(service="alertmanager")
|
|
|
|
am_result = next((r for r in results if r["service"] == "alertmanager"), None)
|
|
assert am_result is not None
|
|
assert am_result["status"] == "failed"
|
|
|
|
def test_verify_alertmanager_missing_when_not_configured(self, monkeypatch):
|
|
"""Alertmanager verification returns 'missing' when not configured."""
|
|
monkeypatch.delenv("ALERTMANAGER_URL", raising=False)
|
|
|
|
from integrations.verify import verify_integrations
|
|
|
|
results = verify_integrations(service="alertmanager")
|
|
|
|
am_result = next((r for r in results if r["service"] == "alertmanager"), None)
|
|
assert am_result is not None
|
|
assert am_result["status"] == "missing"
|
|
|
|
def test_verify_integrations_result_structure(self, monkeypatch):
|
|
"""Verify integrations result has expected fields for alertmanager."""
|
|
monkeypatch.delenv("ALERTMANAGER_URL", raising=False)
|
|
|
|
from integrations.verify import verify_integrations
|
|
|
|
results = verify_integrations(service="alertmanager")
|
|
assert isinstance(results, list)
|
|
for result in results:
|
|
if result["service"] == "alertmanager":
|
|
assert "service" in result
|
|
assert "source" in result
|
|
assert "status" in result
|
|
assert "detail" in result
|
|
assert result["status"] in ("passed", "missing", "failed")
|
|
|
|
|
|
class TestAlertmanagerToolsAvailability:
|
|
"""Test Alertmanager tools are importable and structurally correct."""
|
|
|
|
def test_alertmanager_alerts_tool_importable(self):
|
|
"""AlertmanagerAlertsTool can be imported and is correctly typed."""
|
|
from integrations.alertmanager.tools import AlertmanagerAlertsTool, alertmanager_alerts
|
|
|
|
assert alertmanager_alerts is not None
|
|
assert isinstance(alertmanager_alerts, AlertmanagerAlertsTool)
|
|
assert alertmanager_alerts.name == "alertmanager_alerts"
|
|
assert alertmanager_alerts.source == "alertmanager"
|
|
|
|
def test_alertmanager_silences_tool_importable(self):
|
|
"""AlertmanagerSilencesTool can be imported and is correctly typed."""
|
|
from integrations.alertmanager.tools import (
|
|
AlertmanagerSilencesTool,
|
|
alertmanager_silences,
|
|
)
|
|
|
|
assert alertmanager_silences is not None
|
|
assert isinstance(alertmanager_silences, AlertmanagerSilencesTool)
|
|
assert alertmanager_silences.name == "alertmanager_silences"
|
|
assert alertmanager_silences.source == "alertmanager"
|
|
|
|
def test_alertmanager_alerts_tool_not_available_without_source(self):
|
|
"""AlertmanagerAlertsTool reports unavailable when source has no connection_verified."""
|
|
from integrations.alertmanager.tools import alertmanager_alerts
|
|
|
|
assert not alertmanager_alerts.is_available({})
|
|
assert not alertmanager_alerts.is_available({"alertmanager": {}})
|
|
assert not alertmanager_alerts.is_available(
|
|
{"alertmanager": {"connection_verified": False}}
|
|
)
|
|
|
|
def test_alertmanager_alerts_tool_available_with_verified_source(self):
|
|
"""AlertmanagerAlertsTool is available when alertmanager source is connection_verified."""
|
|
from integrations.alertmanager.tools import alertmanager_alerts
|
|
|
|
sources = {"alertmanager": {"connection_verified": True, "base_url": "http://am:9093"}}
|
|
assert alertmanager_alerts.is_available(sources)
|
|
|
|
def test_alertmanager_alerts_tool_extract_params(self):
|
|
"""AlertmanagerAlertsTool.extract_params returns correct fields from source."""
|
|
from integrations.alertmanager.tools import alertmanager_alerts
|
|
|
|
sources = {
|
|
"alertmanager": {
|
|
"base_url": "http://alertmanager.monitoring.svc:9093",
|
|
"bearer_token": "tok",
|
|
"username": "",
|
|
"password": "",
|
|
"filter_labels": ['alertname="HighErrorRate"'],
|
|
"connection_verified": True,
|
|
}
|
|
}
|
|
params = alertmanager_alerts.extract_params(sources)
|
|
|
|
assert params["base_url"] == "http://alertmanager.monitoring.svc:9093"
|
|
assert params["bearer_token"] == "tok"
|
|
assert params["active"] is True
|
|
|
|
@patch("integrations.alertmanager.client.AlertmanagerClient.list_alerts")
|
|
def test_alertmanager_alerts_tool_run_success(self, mock_list_alerts):
|
|
"""AlertmanagerAlertsTool.run returns structured result on success."""
|
|
from integrations.alertmanager.tools import alertmanager_alerts
|
|
|
|
mock_list_alerts.return_value = {
|
|
"success": True,
|
|
"alerts": [
|
|
{
|
|
"fingerprint": "abc123",
|
|
"status": "active",
|
|
"labels": {"alertname": "HighErrorRate", "job": "api"},
|
|
"annotations": {"summary": "High error rate"},
|
|
"starts_at": "2024-01-15T10:25:00Z",
|
|
"ends_at": "0001-01-01T00:00:00Z",
|
|
"generator_url": "http://prometheus:9090/graph",
|
|
}
|
|
],
|
|
"total": 1,
|
|
}
|
|
|
|
result = alertmanager_alerts.run(
|
|
base_url="http://alertmanager.monitoring.svc:9093",
|
|
)
|
|
|
|
assert result["available"] is True
|
|
assert result["source"] == "alertmanager"
|
|
assert result["total"] == 1
|
|
assert len(result["alerts"]) == 1
|
|
assert len(result["firing_alerts"]) == 1
|
|
|
|
def test_alertmanager_alerts_tool_run_missing_base_url(self):
|
|
"""AlertmanagerAlertsTool.run returns unavailable when base_url is empty."""
|
|
from integrations.alertmanager.tools import alertmanager_alerts
|
|
|
|
result = alertmanager_alerts.run(base_url="")
|
|
|
|
assert result["available"] is False
|
|
assert "not configured" in result["error"]
|
|
|
|
@patch("integrations.alertmanager.client.AlertmanagerClient.list_silences")
|
|
def test_alertmanager_silences_tool_run_success(self, mock_list_silences):
|
|
"""AlertmanagerSilencesTool.run returns structured result on success."""
|
|
from integrations.alertmanager.tools import alertmanager_silences
|
|
|
|
mock_list_silences.return_value = {
|
|
"success": True,
|
|
"silences": [
|
|
{
|
|
"id": "silence-123",
|
|
"status": "active",
|
|
"matchers": [{"name": "alertname", "value": "HighErrorRate", "isEqual": True}],
|
|
"comment": "Planned maintenance",
|
|
"created_by": "oncall-engineer",
|
|
"starts_at": "2024-01-15T08:00:00Z",
|
|
"ends_at": "2024-01-15T12:00:00Z",
|
|
}
|
|
],
|
|
"active_silences": [
|
|
{
|
|
"id": "silence-123",
|
|
"status": "active",
|
|
"matchers": [{"name": "alertname", "value": "HighErrorRate", "isEqual": True}],
|
|
"comment": "Planned maintenance",
|
|
"created_by": "oncall-engineer",
|
|
"starts_at": "2024-01-15T08:00:00Z",
|
|
"ends_at": "2024-01-15T12:00:00Z",
|
|
}
|
|
],
|
|
"total": 1,
|
|
}
|
|
|
|
result = alertmanager_silences.run(
|
|
base_url="http://alertmanager.monitoring.svc:9093",
|
|
)
|
|
|
|
assert result["available"] is True
|
|
assert result["source"] == "alertmanager_silences"
|
|
assert result["total"] == 1
|
|
assert len(result["active_silences"]) == 1
|
|
|
|
|
|
class TestAlertmanagerAlertFixture:
|
|
"""Test the Alertmanager alert fixture is valid and parseable."""
|
|
|
|
def test_alertmanager_alert_fixture_is_valid_json(self):
|
|
"""Alertmanager alert fixture is valid JSON."""
|
|
fixture_path = Path(__file__).parent / "alertmanager_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)
|
|
|
|
def test_alertmanager_alert_fixture_has_required_webhook_fields(self):
|
|
"""Alert fixture contains Alertmanager webhook format fields."""
|
|
fixture_path = Path(__file__).parent / "alertmanager_alert.json"
|
|
|
|
with fixture_path.open() as f:
|
|
alert = json.load(f)
|
|
|
|
assert "version" in alert
|
|
assert "status" in alert
|
|
assert "commonLabels" in alert
|
|
assert "commonAnnotations" in alert
|
|
assert "alerts" in alert
|
|
assert isinstance(alert["alerts"], list)
|
|
assert len(alert["alerts"]) > 0
|
|
|
|
def test_alertmanager_alert_fixture_individual_alerts_have_fingerprint(self):
|
|
"""Each alert in the fixture has Prometheus-specific fingerprint and generatorURL."""
|
|
fixture_path = Path(__file__).parent / "alertmanager_alert.json"
|
|
|
|
with fixture_path.open() as f:
|
|
alert = json.load(f)
|
|
|
|
for individual_alert in alert["alerts"]:
|
|
assert "fingerprint" in individual_alert
|
|
assert "generatorURL" in individual_alert
|
|
assert "startsAt" in individual_alert
|
|
|
|
def test_alertmanager_alert_fixture_has_service_labels(self):
|
|
"""Alert fixture contains service-level labels useful for investigation context."""
|
|
fixture_path = Path(__file__).parent / "alertmanager_alert.json"
|
|
|
|
with fixture_path.open() as f:
|
|
alert = json.load(f)
|
|
|
|
common_labels = alert.get("commonLabels", {})
|
|
assert "alertname" in common_labels
|
|
assert "severity" in common_labels
|
|
assert "job" in common_labels
|
|
|
|
|
|
class TestAlertmanagerIntegrationConfig:
|
|
"""Test AlertmanagerIntegrationConfig model validation."""
|
|
|
|
def test_alertmanager_config_creation(self):
|
|
"""AlertmanagerIntegrationConfig validates correctly with all fields."""
|
|
from integrations.config_models import AlertmanagerIntegrationConfig
|
|
|
|
config = AlertmanagerIntegrationConfig(
|
|
base_url="http://alertmanager.monitoring.svc:9093",
|
|
bearer_token="test-token",
|
|
integration_id="test-id",
|
|
)
|
|
|
|
assert config.base_url == "http://alertmanager.monitoring.svc:9093"
|
|
assert config.bearer_token == "test-token"
|
|
assert config.integration_id == "test-id"
|
|
assert config.username == ""
|
|
assert config.password == ""
|
|
|
|
def test_alertmanager_config_url_normalization(self):
|
|
"""AlertmanagerIntegrationConfig strips whitespace and trailing slash from base_url."""
|
|
from integrations.config_models import AlertmanagerIntegrationConfig
|
|
|
|
config = AlertmanagerIntegrationConfig(
|
|
base_url=" http://alertmanager.example.com:9093/ ",
|
|
)
|
|
|
|
assert config.base_url == "http://alertmanager.example.com:9093"
|
|
|
|
def test_alertmanager_config_basic_auth(self):
|
|
"""AlertmanagerIntegrationConfig stores basic auth credentials."""
|
|
from integrations.config_models import AlertmanagerIntegrationConfig
|
|
|
|
config = AlertmanagerIntegrationConfig(
|
|
base_url="http://alertmanager.example.com:9093",
|
|
username="admin",
|
|
password="secret",
|
|
)
|
|
|
|
assert config.username == "admin"
|
|
assert config.password == "secret"
|
|
assert config.bearer_token == ""
|