Files
awoooi/apps/api/tests/test_channel_hub_grouped_alert_events.py

473 lines
16 KiB
Python

from __future__ import annotations
import inspect
import json
from uuid import uuid4
from src.services.channel_hub import (
_db_timestamp_now,
_send_telegram_interim,
build_alertmanager_provider_event_id,
build_alertmanager_run_id,
build_external_alert_provider_event_id,
build_grouped_alert_provider_event_id,
build_grouped_alert_run_id,
build_inbound_source_envelope,
ensure_completed_shadow_run,
format_alertmanager_event_content,
format_grouped_alert_digest_text,
format_grouped_alert_event_content,
mirror_inbound_event,
record_outbound_message,
)
from src.services.telegram_gateway import _telegram_destination_binding
_FAKE_SECRET_SHAPED_VALUE = "1234567890:" + "abcdefghijklmnopqrstuvwxyzABCDEFGH"
def test_db_timestamp_now_is_naive_utc_for_asyncpg() -> None:
assert _db_timestamp_now().tzinfo is None
async def test_send_telegram_interim_uses_gateway_mirror_path(monkeypatch) -> None:
calls: list[dict[str, object]] = []
class _FakeGateway:
async def send_text(
self,
text: str,
*,
chat_id: str | int | None = None,
parse_mode: str = "HTML",
disable_web_page_preview: bool = True,
interaction_kind: str | None = None,
inbound_chat_id: str | int | None = None,
inbound_message_id: int | None = None,
) -> dict[str, object]:
calls.append(
{
"text": text,
"chat_id": chat_id,
"parse_mode": parse_mode,
"disable_web_page_preview": disable_web_page_preview,
"interaction_kind": interaction_kind,
"inbound_chat_id": inbound_chat_id,
"inbound_message_id": inbound_message_id,
}
)
destination_binding = _telegram_destination_binding(chat_id)
destination_alias = "verified_inbound_chat"
return {
"ok": True,
"result": {
"message_id": 42,
"chat": {"id": chat_id},
},
"_awooop_delivery_status": "sent",
"_awooop_provider_send_performed": True,
"_awoooi_canonical_route_receipt": {
"schema_version": "telegram_canonical_egress_receipt_v1",
"decision": "allowed",
"provider_send_performed": True,
"sender_bot_alias": "tsenyang_bot",
"destination_alias": destination_alias,
"destination_binding": destination_binding,
},
"_awoooi_delivery_context": {
"schema_version": "telegram_delivery_context_v1",
"sender_bot_alias": "tsenyang_bot",
"destination_alias": destination_alias,
"destination_binding": destination_binding,
"payload_destination_binding": destination_binding,
"provider_destination_binding": destination_binding,
"destination_binding_verified": True,
},
}
monkeypatch.setattr(
"src.services.telegram_gateway.get_telegram_gateway",
lambda: _FakeGateway(),
)
delivered = await _send_telegram_interim(
channel_chat_id="123456",
content="AI 正在分析中,請稍候...",
run_id=uuid4(),
inbound_message_id=321,
)
assert delivered is True
assert calls == [
{
"text": "AI 正在分析中,請稍候...",
"chat_id": "123456",
"parse_mode": "HTML",
"disable_web_page_preview": True,
"interaction_kind": "operator_command_reply",
"inbound_chat_id": "123456",
"inbound_message_id": 321,
}
]
async def test_send_telegram_interim_fails_closed_without_verified_parent(
monkeypatch,
) -> None:
class _UnexpectedGateway:
async def send_text(self, *args, **kwargs):
raise AssertionError("gateway must not be called")
monkeypatch.setattr(
"src.services.telegram_gateway.get_telegram_gateway",
lambda: _UnexpectedGateway(),
)
delivered = await _send_telegram_interim(
channel_chat_id="123456",
content="must remain no-send",
run_id=uuid4(),
)
assert delivered is False
async def test_send_telegram_interim_rejects_structured_no_send(
monkeypatch,
) -> None:
class _BlockedGateway:
async def send_text(self, *args, **kwargs):
return {
"ok": False,
"_awooop_delivery_status": "blocked_no_egress",
"_awooop_provider_send_performed": False,
}
monkeypatch.setattr(
"src.services.telegram_gateway.get_telegram_gateway",
lambda: _BlockedGateway(),
)
delivered = await _send_telegram_interim(
channel_chat_id="123456",
content="must remain no-send",
run_id=uuid4(),
inbound_message_id=321,
)
assert delivered is False
def test_channel_hub_does_not_embed_direct_telegram_bot_api() -> None:
source = inspect.getsource(_send_telegram_interim)
assert "TELEGRAM_BOT_TOKEN" not in source
assert "api.telegram.org" not in source
def test_build_grouped_alert_provider_event_id_is_deterministic() -> None:
event_id = build_grouped_alert_provider_event_id(
"INC-20260507-ABCD12",
"1234567890abcdef" * 4,
)
assert event_id == "alert-group:INC-20260507-ABCD12:1234567890abcdef1234567890abcdef"
assert len(event_id) < 256
def test_build_grouped_alert_run_id_is_stable() -> None:
provider_event_id = build_grouped_alert_provider_event_id(
"INC-20260507-ABCD12",
"1234567890abcdef" * 4,
)
first = build_grouped_alert_run_id("awoooi", provider_event_id)
second = build_grouped_alert_run_id("awoooi", provider_event_id)
other_project = build_grouped_alert_run_id("ewoooc", provider_event_id)
assert first == second
assert first != other_project
def test_build_alertmanager_provider_event_id_keeps_fingerprint() -> None:
event_id = build_alertmanager_provider_event_id(
"alert-20260513213000",
"abcdef1234567890" * 4,
"incident_linked",
)
assert event_id == (
"alertmanager:incident_linked:alert-20260513213000:"
+ "abcdef1234567890abcdef1234567890"
)
assert len(event_id) < 256
def test_build_alertmanager_run_id_is_stable() -> None:
provider_event_id = build_alertmanager_provider_event_id(
"alert-20260513213000",
"abcdef1234567890" * 4,
"received",
)
assert build_alertmanager_run_id("awoooi", provider_event_id) == build_alertmanager_run_id(
"awoooi", provider_event_id
)
assert build_alertmanager_run_id("awoooi", provider_event_id) != build_alertmanager_run_id(
"ewoooc", provider_event_id
)
def test_build_external_alert_provider_event_id_is_stable() -> None:
event_id = build_external_alert_provider_event_id(
"Sentry",
"12345678901234567890",
"approval_linked",
)
assert event_id == "sentry:approval_linked:12345678901234567890"
assert len(event_id) < 256
def test_build_inbound_source_envelope_redacts_and_keeps_refs() -> None:
envelope = build_inbound_source_envelope(
provider="alertmanager",
stage="received",
provider_event_id="alertmanager:received:alert-1:fp-1",
raw_event_id="alert-1",
raw_content=(
"ACTION REQUIRED INC-20260513-ABCDEF token "
+ _FAKE_SECRET_SHAPED_VALUE
),
alertname="DockerContainerUnhealthy",
severity="warning",
namespace="default",
target_resource="bitan-pharmacy-bitan-1",
fingerprint="fp-1",
incident_id="INC-20260513-ABCDEF",
approval_id="approval-1",
labels={"token": "should-not-leak", "container": "bitan-pharmacy-bitan-1"},
)
rendered = json.dumps(envelope, ensure_ascii=False)
assert envelope["schema_version"] == "inbound_source_envelope_v1"
assert envelope["source_refs"]["event_ids"] == ["alert-1"]
assert envelope["source_refs"]["incident_ids"] == ["INC-20260513-ABCDEF"]
assert envelope["source_refs"]["approval_ids"] == ["approval-1"]
assert envelope["source_refs"]["alert_ids"] == ["alert-1", "alertmanager:received:alert-1:fp-1"]
assert envelope["source_refs"]["fingerprints"] == ["fp-1"]
assert len(envelope["content_sha256"]) == 64
assert "1234567890:" not in rendered
assert "should-not-leak" not in rendered
def test_format_alertmanager_event_content_keeps_incident_first() -> None:
content = format_alertmanager_event_content(
stage="incident_linked",
alert_id="alert-20260513213000",
alertname="DockerContainerExited",
severity="warning",
namespace="default",
target_resource="bitan-pharmacy-bitan-1",
fingerprint="fp-123",
notification_type="TYPE-3",
alert_category="host_resource",
incident_id="INC-20260513-ABCDEF",
approval_id="approval-1",
repeat_count=3,
)
assert content.startswith("Alertmanager inbound incident_linked\nIncident: INC-20260513-ABCDEF")
assert "Approval: approval-1" in content
assert "Fingerprint: fp-123" in content
assert "Repeat Count: 3" in content
def test_format_grouped_alert_event_content_keeps_operator_context() -> None:
content = format_grouped_alert_event_content(
alert_id="INC-20260507-ABCD12",
alertname="DockerContainerRestartSpike",
severity="critical",
namespace="default",
target_resource="sentry-self-hosted-events-consumer-1",
group_key="DockerContainerRestartSpike:default",
count=4,
parent_fingerprint="parent-fp",
fingerprint="child-fp",
)
assert "告警已收斂,不發 Telegram" in content
assert "Alert: DockerContainerRestartSpike" in content
assert "Target: sentry-self-hosted-events-consumer-1" in content
assert "Group Count: 4" in content
assert "Parent Fingerprint: parent-fp" in content
def test_format_grouped_alert_digest_text_is_html_safe() -> None:
content = format_grouped_alert_digest_text(
alertname="Docker<Restart>",
severity="critical",
namespace="default",
target_resource="sentry&snuba",
group_key="Docker<Restart>:default",
count=7,
)
assert "告警已收斂到父卡" in content
assert "Docker&lt;Restart&gt;" in content
assert "sentry&amp;snuba" in content
assert "<b>7</b> 筆同組告警" in content
assert "AwoooP Run 監控" in content
class _FakeResult:
def fetchone(self) -> tuple[str] | None:
return ("created",)
class _FakeSession:
def __init__(self) -> None:
self.statement = ""
self.params = {}
self.statements = []
self.param_sets = []
async def execute(self, statement, params): # noqa: ANN001
self.statement = str(statement)
self.params = params
self.statements.append(str(statement))
self.param_sets.append(params)
return _FakeResult()
async def test_completed_shadow_run_sets_run_state_not_null_defaults() -> None:
session = _FakeSession()
run_id = build_grouped_alert_run_id("awoooi", "provider-event")
inserted = await ensure_completed_shadow_run(
session, # type: ignore[arg-type]
project_id="awoooi",
run_id=run_id,
agent_id="legacy-telegram-gateway",
trigger_type="legacy_outbound",
trigger_ref="12713",
input_payload={"message_type": "final"},
)
assert inserted is True
assert "attempt_count, max_attempts, cost_usd, step_count" in session.statement
assert "0, 3, 0.0000, 0" in session.statement
assert session.params["project_id"] == "awoooi"
assert session.params["run_id"] == run_id
async def test_mirror_inbound_event_writes_redacted_replay_fields() -> None:
session = _FakeSession()
envelope = build_inbound_source_envelope(
provider="sentry",
stage="received",
provider_event_id="sentry:received:123",
raw_event_id="123",
raw_content="Sentry token " + _FAKE_SECRET_SHAPED_VALUE,
alertname="Sentry issue",
severity="error",
namespace="sentry",
target_resource="frontend",
)
await mirror_inbound_event(
session, # type: ignore[arg-type]
project_id="awoooi",
channel_type="internal",
provider_event_id="sentry:received:123",
platform_subject_id="sentry",
raw_content="Sentry token " + _FAKE_SECRET_SHAPED_VALUE,
source_envelope=envelope,
)
assert "content_redacted" in session.statement
assert "source_envelope" in session.statement
assert session.params["redaction_version"] == "audit_sink_v1"
assert len(json.loads(session.params["source_envelope"])["content_sha256"]) == 64
assert "1234567890:" not in session.params["content_redacted"]
assert "1234567890:" not in session.params["source_envelope"]
def test_sentry_and_signoz_source_refs_keep_raw_event_ids() -> None:
sentry_envelope = build_inbound_source_envelope(
provider="sentry",
stage="received",
provider_event_id="sentry:received:issue-123",
raw_event_id="issue-123",
raw_content="Sentry issue",
alertname="Sentry issue",
fingerprint="sentry-issue-123",
)
signoz_envelope = build_inbound_source_envelope(
provider="signoz",
stage="received",
provider_event_id="signoz:received:fp-456",
raw_event_id="fp-456",
raw_content="SignOz alert",
alertname="HighLatency",
fingerprint="fp-456",
)
assert sentry_envelope["source_refs"]["event_ids"] == ["issue-123"]
assert sentry_envelope["source_refs"]["sentry_issue_ids"] == [
"issue-123",
"sentry:received:issue-123",
]
assert signoz_envelope["source_refs"]["event_ids"] == ["fp-456"]
assert signoz_envelope["source_refs"]["signoz_alerts"] == ["HighLatency", "fp-456"]
def test_source_provider_heartbeat_refs_do_not_claim_real_provider_alerts() -> None:
sentry_envelope = build_inbound_source_envelope(
provider="sentry",
stage="heartbeat",
provider_event_id="sentry:heartbeat:heartbeat-1",
raw_event_id="heartbeat-1",
raw_content="Sentry heartbeat",
alertname="SourceProviderHeartbeat",
fingerprint="source-provider-heartbeat:sentry",
)
signoz_envelope = build_inbound_source_envelope(
provider="signoz",
stage="heartbeat",
provider_event_id="signoz:heartbeat:heartbeat-1",
raw_event_id="heartbeat-1",
raw_content="SignOz heartbeat",
alertname="SourceProviderHeartbeat",
fingerprint="source-provider-heartbeat:signoz",
)
assert sentry_envelope["source_refs"]["event_ids"] == ["heartbeat-1"]
assert sentry_envelope["source_refs"]["sentry_issue_ids"] == []
assert signoz_envelope["source_refs"]["event_ids"] == ["heartbeat-1"]
assert signoz_envelope["source_refs"]["signoz_alerts"] == []
async def test_record_outbound_message_sets_sent_at_for_sent_messages() -> None:
session = _FakeSession()
run_id = build_grouped_alert_run_id("awoooi", "telegram-message-13152")
await record_outbound_message(
session, # type: ignore[arg-type]
project_id="awoooi",
run_id=run_id,
channel_type="telegram",
channel_chat_id="-100123",
message_type="approval_request",
content="ACTION REQUIRED INC-20260513-9B082D",
provider_message_id="13152",
send_status="sent",
triggered_by_state="legacy_gateway",
is_shadow=False,
)
insert_statement = session.statements[-1]
assert "sent_at" in insert_statement
assert ":sent_at" in insert_statement
assert session.param_sets[-1]["send_status"] == "sent"
assert session.param_sets[-1]["sent_at"] is not None
assert session.param_sets[-1]["sent_at"].tzinfo is None