Files
tracer-cloud--opensre/tests/integrations/incident_io/test_client.py
T
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

323 lines
11 KiB
Python

from __future__ import annotations
import threading
from datetime import UTC, datetime, timedelta
from unittest.mock import MagicMock
import httpx
import pytest
from integrations.config_models import IncidentIoIntegrationConfig
from integrations.incident_io.client import (
IncidentIoClient,
_get_incident_write_lock,
make_incident_io_client,
)
def _response(payload: dict) -> MagicMock:
response = MagicMock()
response.json.return_value = payload
response.raise_for_status.return_value = None
response.status_code = 200
return response
@pytest.fixture
def client() -> IncidentIoClient:
return IncidentIoClient(IncidentIoIntegrationConfig(api_key="test-key"))
def test_config_defaults_to_documented_public_api_url() -> None:
config = IncidentIoIntegrationConfig(api_key="test-key")
assert config.base_url == "https://api.incident.io"
def test_config_rejects_non_https_remote_base_url() -> None:
with pytest.raises(ValueError, match="must use https"):
IncidentIoIntegrationConfig(
api_key="test-key",
base_url="http://169.254.169.254/latest",
)
def test_make_client_allows_loopback_base_url() -> None:
created = make_incident_io_client(
"test-key",
base_url="http://localhost:8080",
)
assert created is not None
assert created.config.base_url == "http://localhost:8080"
def test_list_incidents_uses_status_category_filter(client: IncidentIoClient, monkeypatch) -> None:
calls: list[tuple[str, str, dict]] = []
def fake_request(method: str, path: str, **kwargs):
calls.append((method, path, kwargs))
return _response(
{
"incidents": [
{
"id": "inc-123",
"reference": "INC-123",
"name": "Database outage",
"incident_status": {"name": "Mitigating", "category": "live"},
"severity": {"name": "SEV1", "rank": 1},
}
],
"pagination_meta": {"after": "inc-123", "page_size": 1},
}
)
monkeypatch.setattr(client, "_request", fake_request)
result = client.list_incidents(status_category="live", page_size=1)
assert result["success"] is True
assert result["incidents"][0]["status"] == "Mitigating"
assert result["incidents"][0]["status_category"] == "live"
assert calls == [
(
"GET",
"/v2/incidents",
{"params": {"page_size": 1, "status_category[one_of]": "live"}},
)
]
def test_context_reads_incident_and_updates(client: IncidentIoClient, monkeypatch) -> None:
calls: list[tuple[str, str, dict]] = []
def fake_request(method: str, path: str, **kwargs):
calls.append((method, path, kwargs))
if path == "/v2/incidents/inc-123":
return _response(
{
"incident": {
"id": "inc-123",
"reference": "INC-123",
"name": "Checkout degraded",
"summary": "Investigating elevated latency",
"incident_status": {"name": "Investigating", "category": "live"},
}
}
)
return _response(
{
"incident_updates": [
{
"id": "upd-1",
"incident_id": "inc-123",
"message": "We are investigating",
"new_incident_status": {"name": "Investigating", "category": "live"},
}
]
}
)
monkeypatch.setattr(client, "_request", fake_request)
result = client.get_incident_context("inc-123", update_limit=10)
assert result["success"] is True
assert result["incident"]["summary"] == "Investigating elevated latency"
assert result["incident_updates"][0]["message"] == "We are investigating"
assert calls[0][1] == "/v2/incidents/inc-123"
assert calls[1] == (
"GET",
"/v2/incident_updates",
{"params": {"incident_id": "inc-123", "page_size": 10}},
)
def test_append_summary_update_uses_supported_edit_endpoint(
client: IncidentIoClient,
monkeypatch,
) -> None:
calls: list[tuple[str, str, dict]] = []
summary_store = {"text": "Existing summary"}
def fake_request(method: str, path: str, **kwargs):
calls.append((method, path, kwargs))
if method == "GET":
return _response({"incident": {"id": "inc-123", "summary": summary_store["text"]}})
payload = kwargs["json"]
summary_store["text"] = payload["incident"]["summary"]
return _response({"incident": {"id": "inc-123"}})
monkeypatch.setattr(client, "_request", fake_request)
result = client.append_summary_update(
"inc-123",
title="Root cause found",
body="Bad deploy increased DB latency.",
)
assert result["success"] is True
assert calls[1][0] == "POST"
assert calls[1][1] == "/v2/incidents/inc-123/actions/edit"
payload = calls[1][2]["json"]
assert payload["notify_incident_channel"] is False
assert "Existing summary" in payload["incident"]["summary"]
assert "Root cause found" in payload["incident"]["summary"]
assert calls[2][0] == "GET"
assert calls[2][1] == "/v2/incidents/inc-123"
def test_append_summary_update_retries_until_verify_sees_finding(
monkeypatch,
) -> None:
"""Patch class `_request`: instance patches can fail after sleeps in retry loops."""
client = IncidentIoClient(IncidentIoIntegrationConfig(api_key="test-key"))
calls: list[tuple[str, str, dict]] = []
merged_holder = {"text": ""}
def fake_request(self: IncidentIoClient, method: str, path: str, **kwargs):
calls.append((method, path, kwargs))
if method == "GET":
get_idx = sum(1 for c in calls if c[0] == "GET")
if get_idx in {1, 2}:
summary = "Existing summary"
else:
summary = merged_holder["text"]
return _response({"incident": {"id": "inc-123", "summary": summary}})
payload = kwargs["json"]
merged_holder["text"] = payload["incident"]["summary"]
return _response({"incident": {"id": "inc-123"}})
sleeps: list[float] = []
monkeypatch.setattr(IncidentIoClient, "_request", fake_request)
monkeypatch.setattr("integrations.incident_io.client.time.sleep", sleeps.append)
monkeypatch.setattr("integrations.incident_io.client.random.random", lambda: 0.0)
result = client.append_summary_update(
"inc-123",
title="Lag spike",
body="Retry path.",
)
assert result["success"] is True
assert sum(1 for c in calls if c[0] == "POST") == 1
assert len(sleeps) >= 1
def test_append_summary_update_skips_post_when_finding_already_present(
client: IncidentIoClient,
monkeypatch,
) -> None:
class FrozenDateTime(datetime):
@classmethod
def now(cls, _tz=None):
return datetime(2099, 1, 1, 0, 0, 0, tzinfo=UTC)
monkeypatch.setattr("integrations.incident_io.client.datetime", FrozenDateTime)
embedded = "Existing\n\n---\n**OpenSRE finding: Already posted** (2099-01-01 00:00:00 UTC)"
calls: list[tuple[str, str, dict]] = []
def fake_request(method: str, path: str, **kwargs):
calls.append((method, path, kwargs))
return _response({"incident": {"id": "inc-123", "summary": embedded}})
monkeypatch.setattr(client, "_request", fake_request)
result = client.append_summary_update(
"inc-123",
title="Already posted",
body="",
)
assert result["success"] is True
assert all(c[0] != "POST" for c in calls)
def test_concurrent_append_summary_uses_shared_module_level_lock() -> None:
"""Two separate client instances targeting the same incident must share one lock."""
client_a = IncidentIoClient(IncidentIoIntegrationConfig(api_key="key-a"))
client_b = IncidentIoClient(IncidentIoIntegrationConfig(api_key="key-b"))
lock_a = _get_incident_write_lock("inc-shared")
lock_b = _get_incident_write_lock("inc-shared")
assert lock_a is lock_b, "Different client instances must share the same lock per incident ID"
lock_other = _get_incident_write_lock("inc-other")
assert lock_other is not lock_a, "Different incident IDs must use different locks"
# Verify the clients themselves no longer carry a _write_lock attribute
assert not hasattr(client_a, "_write_lock")
assert not hasattr(client_b, "_write_lock")
# Confirm concurrent writes from two clients are serialised by the shared lock:
# the first writer holds the lock while the second waits.
order: list[str] = []
barrier = threading.Barrier(2)
lock = _get_incident_write_lock("inc-concurrent")
results: list[bool] = []
def holder() -> None:
with lock:
order.append("held")
barrier.wait()
def waiter() -> None:
barrier.wait()
acquired = lock.acquire(blocking=False)
results.append(acquired)
if acquired:
lock.release()
t1 = threading.Thread(target=holder)
t2 = threading.Thread(target=waiter)
t1.start()
t2.start()
t1.join()
t2.join()
assert results == [False], "Concurrent writer must be blocked while the lock is held"
def test_request_honors_documented_json_rate_limit_retry_after(
client: IncidentIoClient,
monkeypatch,
) -> None:
retry_at = (datetime.now(UTC) + timedelta(seconds=5)).strftime("%a, %d %b %Y %H:%M:%S UTC")
rate_limited = _response(
{
"type": "too_many_requests",
"status": 429,
"rate_limit": {
"name": "api_key_name",
"limit": 1200,
"remaining": 0,
"retry_after": retry_at,
},
}
)
rate_limited.status_code = 429
rate_limited.headers = {}
rate_limited.raise_for_status.side_effect = httpx.HTTPStatusError(
"rate limited",
request=MagicMock(),
response=rate_limited,
)
ok = _response({"incidents": []})
request = MagicMock(side_effect=[rate_limited, ok])
fake_http_client = MagicMock()
fake_http_client.request = request
monkeypatch.setattr(client, "_get_client", lambda: fake_http_client)
sleeps: list[float] = []
monkeypatch.setattr("integrations.incident_io.client.time.sleep", sleeps.append)
monkeypatch.setattr("integrations.incident_io.client.random.random", lambda: 0.0)
response = client._request("GET", "/v2/incidents")
assert response is ok
assert request.call_count == 2
assert sleeps
assert 0 <= sleeps[0] <= 5