Some checks failed
CD Pipeline / workflow-shape (push) Successful in 1s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / build-and-deploy (push) Has been cancelled
CD Pipeline / tests (push) Has been cancelled
CD Pipeline / post-deploy-checks (push) Has been cancelled
877 lines
28 KiB
Python
877 lines
28 KiB
Python
from __future__ import annotations
|
|
|
|
import runpy
|
|
from contextlib import asynccontextmanager
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
from unittest.mock import AsyncMock
|
|
|
|
import pytest
|
|
|
|
import src.db.base as db_base
|
|
import src.services.approval_db as approval_db
|
|
import src.services.telegram_gateway as gateway_module
|
|
from src.services.telegram_gateway import TelegramGateway
|
|
|
|
_REPO_ROOT = Path(__file__).resolve().parents[3]
|
|
|
|
|
|
class _FakeResponse:
|
|
def __init__(self, payload: dict) -> None:
|
|
self._payload = payload
|
|
|
|
def raise_for_status(self) -> None:
|
|
return None
|
|
|
|
def json(self) -> dict:
|
|
return {
|
|
"ok": True,
|
|
"result": {
|
|
"message_id": 11,
|
|
"chat": {"id": self._payload.get("chat_id")},
|
|
},
|
|
}
|
|
|
|
|
|
class _FakeHttpClient:
|
|
def __init__(self) -> None:
|
|
self.posts: list[tuple[str, dict]] = []
|
|
|
|
async def post(self, url: str, json: dict) -> _FakeResponse:
|
|
self.posts.append((url, json))
|
|
return _FakeResponse(json)
|
|
|
|
|
|
def _prepared_gateway(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> tuple[TelegramGateway, _FakeHttpClient]:
|
|
gateway = TelegramGateway()
|
|
client = _FakeHttpClient()
|
|
gateway._initialized = True
|
|
gateway._http_client = client # type: ignore[assignment]
|
|
monkeypatch.setattr(gateway_module.settings, "SRE_GROUP_CHAT_ID", "test-room")
|
|
monkeypatch.setattr(gateway, "_attach_incident_thread_reply", AsyncMock())
|
|
monkeypatch.setattr(gateway, "_mirror_outbound_message", AsyncMock())
|
|
monkeypatch.setattr(
|
|
gateway._security,
|
|
"generate_callback_nonce",
|
|
lambda approval_id, action: f"{action}:test",
|
|
)
|
|
return gateway, client
|
|
|
|
|
|
def _notification_message():
|
|
from src.services.notifications.base import ExecutionStatus, NotificationMessage
|
|
|
|
return NotificationMessage(
|
|
execution_status=ExecutionStatus.SUCCESS,
|
|
action_title="bounded test",
|
|
action_description="no provider egress",
|
|
approval_id="APR-TEST",
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_notification_provider_does_not_report_success_for_gateway_no_send(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
from src.services.notifications import telegram as provider_module
|
|
from src.services.notifications.base import NotificationStatus
|
|
|
|
monkeypatch.setattr(provider_module.settings, "OPENCLAW_TG_BOT_TOKEN", "configured")
|
|
monkeypatch.setattr(provider_module.settings, "SRE_GROUP_CHAT_ID", "test-room")
|
|
gateway = SimpleNamespace(
|
|
send_alert_notification=AsyncMock(
|
|
return_value={
|
|
"ok": False,
|
|
"_awooop_delivery_status": "blocked_no_egress",
|
|
"_awooop_provider_send_performed": False,
|
|
}
|
|
)
|
|
)
|
|
monkeypatch.setattr(gateway_module, "get_telegram_gateway", lambda: gateway)
|
|
|
|
result = await provider_module.TelegramWebhookProvider().send(
|
|
_notification_message()
|
|
)
|
|
|
|
assert result.status is NotificationStatus.SKIPPED
|
|
assert result.message == "Telegram notification blocked before provider send"
|
|
assert result.response_data == {
|
|
"ok": False,
|
|
"_awooop_delivery_status": "blocked_no_egress",
|
|
"_awooop_provider_send_performed": False,
|
|
}
|
|
gateway.send_alert_notification.assert_awaited_once()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_notification_provider_requires_affirmative_delivery_ack(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
from src.services.notifications import telegram as provider_module
|
|
from src.services.notifications.base import NotificationStatus
|
|
|
|
monkeypatch.setattr(provider_module.settings, "OPENCLAW_TG_BOT_TOKEN", "configured")
|
|
monkeypatch.setattr(provider_module.settings, "SRE_GROUP_CHAT_ID", "test-room")
|
|
gateway = SimpleNamespace(
|
|
send_alert_notification=AsyncMock(
|
|
return_value={"ok": False, "description": "provider rejected"}
|
|
)
|
|
)
|
|
monkeypatch.setattr(gateway_module, "get_telegram_gateway", lambda: gateway)
|
|
|
|
result = await provider_module.TelegramWebhookProvider().send(
|
|
_notification_message()
|
|
)
|
|
|
|
assert result.status is NotificationStatus.FAILED
|
|
assert result.message == "Telegram provider did not acknowledge delivery"
|
|
assert result.error == "delivery_not_acknowledged"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_notification_connection_probe_rejects_structured_no_provider_send(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
from src.services.notifications import telegram as provider_module
|
|
|
|
monkeypatch.setattr(provider_module.settings, "OPENCLAW_TG_BOT_TOKEN", "configured")
|
|
monkeypatch.setattr(provider_module.settings, "SRE_GROUP_CHAT_ID", "test-room")
|
|
gateway = SimpleNamespace(
|
|
send_alert_notification=AsyncMock(
|
|
return_value={
|
|
"ok": True,
|
|
"_awooop_delivery_status": "blocked_no_egress",
|
|
"_awooop_provider_send_performed": False,
|
|
"_awoooi_canonical_route_receipt": {
|
|
"decision": "blocked_no_egress",
|
|
"provider_send_performed": False,
|
|
},
|
|
}
|
|
)
|
|
)
|
|
monkeypatch.setattr(gateway_module, "get_telegram_gateway", lambda: gateway)
|
|
|
|
assert await provider_module.TelegramWebhookProvider().test_connection() is False
|
|
gateway.send_alert_notification.assert_awaited_once()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_notification_connection_probe_accepts_complete_canonical_delivery(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
from src.services.notifications import telegram as provider_module
|
|
|
|
monkeypatch.setattr(provider_module.settings, "OPENCLAW_TG_BOT_TOKEN", "configured")
|
|
monkeypatch.setattr(provider_module.settings, "SRE_GROUP_CHAT_ID", "test-room")
|
|
chat_id = -1001
|
|
destination_binding = gateway_module._telegram_destination_binding(chat_id)
|
|
gateway = SimpleNamespace(
|
|
send_alert_notification=AsyncMock(
|
|
return_value={
|
|
"ok": True,
|
|
"_awooop_delivery_status": "sent",
|
|
"_awooop_provider_send_performed": True,
|
|
"result": {"message_id": 42, "chat": {"id": chat_id}},
|
|
"_awoooi_canonical_route_receipt": {
|
|
"schema_version": "telegram_canonical_egress_receipt_v1",
|
|
"decision": "allowed",
|
|
"destination_alias": "awoooi_sre_war_room",
|
|
"destination_binding": destination_binding,
|
|
"sender_bot_alias": "tsenyang_bot",
|
|
"provider_send_performed": True,
|
|
},
|
|
gateway_module._DELIVERY_CONTEXT_KEY: {
|
|
"schema_version": "telegram_delivery_context_v1",
|
|
"method": "sendMessage",
|
|
"sender_bot_alias": "tsenyang_bot",
|
|
"destination_alias": "awoooi_sre_war_room",
|
|
"destination_binding": destination_binding,
|
|
"payload_destination_binding": destination_binding,
|
|
"provider_destination_binding": destination_binding,
|
|
"destination_binding_verified": True,
|
|
},
|
|
}
|
|
)
|
|
)
|
|
monkeypatch.setattr(gateway_module, "get_telegram_gateway", lambda: gateway)
|
|
|
|
assert await provider_module.TelegramWebhookProvider().test_connection() is True
|
|
gateway.send_alert_notification.assert_awaited_once()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_unclassified_send_message_is_blocked_before_provider(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
gateway, client = _prepared_gateway(monkeypatch)
|
|
|
|
result = await gateway._send_request(
|
|
"sendMessage",
|
|
{"chat_id": "test-room", "text": "unclassified"},
|
|
)
|
|
|
|
assert result["_awooop_delivery_status"] == "blocked_no_egress"
|
|
assert result["_awooop_provider_send_performed"] is False
|
|
assert client.posts == []
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize(
|
|
"method",
|
|
sorted(gateway_module._GUARDED_TELEGRAM_BOT_METHODS),
|
|
)
|
|
async def test_every_guarded_method_without_context_is_blocked_before_provider(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
method: str,
|
|
) -> None:
|
|
gateway, client = _prepared_gateway(monkeypatch)
|
|
|
|
result = await gateway._send_request(
|
|
method,
|
|
{
|
|
"chat_id": "test-room",
|
|
"text": "unclassified",
|
|
"caption": "unclassified",
|
|
"photo": "file-placeholder",
|
|
"document": "file-placeholder",
|
|
"media": [],
|
|
"message_id": 42,
|
|
},
|
|
)
|
|
|
|
assert result["_awooop_delivery_status"] == "blocked_no_egress"
|
|
assert result["_awooop_provider_send_performed"] is False
|
|
assert client.posts == []
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_guarded_photo_accepts_only_a_valid_canonical_route(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
gateway, client = _prepared_gateway(monkeypatch)
|
|
|
|
result = await gateway._send_request(
|
|
"sendPhoto",
|
|
{
|
|
"chat_id": "test-room",
|
|
"photo": "file-placeholder",
|
|
gateway_module._CANONICAL_ROUTE_CONTEXT_KEY: {
|
|
"product_id": "awoooi",
|
|
"signal_family": "security_incident",
|
|
"severity": "P1",
|
|
},
|
|
},
|
|
)
|
|
|
|
assert result["_awooop_delivery_status"] == "sent"
|
|
assert gateway_module._telegram_send_delivery_succeeded(result) is True
|
|
assert len(client.posts) == 1
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize(
|
|
("method", "method_payload"),
|
|
[
|
|
("sendDocument", {"document": "file-placeholder", "reply_to_message_id": 42}),
|
|
("editMessageText", {"text": "updated", "message_id": 42}),
|
|
],
|
|
)
|
|
async def test_guarded_interaction_method_requires_exact_inbound_binding(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
method: str,
|
|
method_payload: dict,
|
|
) -> None:
|
|
gateway, client = _prepared_gateway(monkeypatch)
|
|
|
|
result = await gateway._send_request(
|
|
method,
|
|
{
|
|
"chat_id": "operator-room",
|
|
**method_payload,
|
|
gateway_module._NON_MONITORING_INTERACTION_KEY: {
|
|
"interaction_kind": "callback_reply",
|
|
"inbound_chat_id": "operator-room",
|
|
"inbound_message_id": 42,
|
|
},
|
|
},
|
|
)
|
|
|
|
assert result["_awooop_delivery_status"] == "sent"
|
|
assert gateway_module._telegram_send_delivery_succeeded(result) is True
|
|
assert len(client.posts) == 1
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_info_and_business_methods_are_blocked_before_provider(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
gateway, client = _prepared_gateway(monkeypatch)
|
|
|
|
info = await gateway.send_info_notification(
|
|
incident_id="INC-TEST",
|
|
title="FYI",
|
|
message="healthy",
|
|
)
|
|
business = await gateway.send_business_alert(
|
|
incident_id="INC-TEST",
|
|
alertname="BusinessMetric",
|
|
business_domain="test",
|
|
metric_name="rate",
|
|
current_value="1",
|
|
threshold="2",
|
|
)
|
|
|
|
assert info["_awooop_delivery_status"] == "blocked_no_egress"
|
|
assert business["_awooop_delivery_status"] == "blocked_no_egress"
|
|
assert client.posts == []
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_wrong_group_override_is_blocked_before_provider(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
gateway, client = _prepared_gateway(monkeypatch)
|
|
|
|
result = await gateway.send_escalation_card(
|
|
incident_id="INC-TEST",
|
|
original_alertname="Outage",
|
|
duration_min=15,
|
|
group_chat_id="wrong-room",
|
|
)
|
|
|
|
assert result["_awooop_delivery_status"] == "blocked_no_egress"
|
|
assert result["_awoooi_canonical_route_receipt"]["classifier"] == (
|
|
"caller_destination_mismatch"
|
|
)
|
|
assert client.posts == []
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize("method_name", ["meta", "secops", "escalation"])
|
|
async def test_allowed_legacy_monitoring_methods_use_canonical_provider_once(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
method_name: str,
|
|
) -> None:
|
|
gateway, client = _prepared_gateway(monkeypatch)
|
|
|
|
if method_name == "meta":
|
|
result = await gateway.send_meta_alert(
|
|
incident_id="INC-TEST",
|
|
approval_id="APR-TEST",
|
|
alertname="AlertChainBroken",
|
|
alert_category="alertchain_health",
|
|
diagnosis="test",
|
|
)
|
|
elif method_name == "secops":
|
|
result = await gateway.send_secops_card(
|
|
incident_id="INC-TEST",
|
|
approval_id="APR-TEST",
|
|
alertname="Threat",
|
|
threat_level="critical",
|
|
)
|
|
else:
|
|
result = await gateway.send_escalation_card(
|
|
incident_id="INC-TEST",
|
|
original_alertname="Outage",
|
|
duration_min=15,
|
|
)
|
|
|
|
assert result["ok"] is True
|
|
assert result["_awoooi_canonical_route_receipt"]["decision"] == "allowed"
|
|
assert result["_awoooi_canonical_route_receipt"]["provider_send_performed"] is True
|
|
assert gateway_module._telegram_send_delivery_succeeded(result) is True
|
|
assert len(client.posts) == 1
|
|
assert client.posts[0][1]["chat_id"] == "test-room"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_provider_ok_without_message_id_is_non_terminal(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
gateway, client = _prepared_gateway(monkeypatch)
|
|
monkeypatch.setattr(
|
|
_FakeResponse,
|
|
"json",
|
|
lambda _self: {"ok": True, "result": {}},
|
|
)
|
|
|
|
result = await gateway.send_escalation_card(
|
|
incident_id="INC-TEST",
|
|
original_alertname="Outage",
|
|
duration_min=15,
|
|
)
|
|
|
|
assert result["_awooop_delivery_status"] == "provider_message_id_missing"
|
|
assert result["_awooop_provider_send_performed"] is True
|
|
assert result["_awoooi_canonical_route_receipt"]["provider_send_performed"] is True
|
|
assert gateway_module._telegram_send_delivery_succeeded(result) is False
|
|
assert len(client.posts) == 1
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_provider_chat_mismatch_is_non_terminal_delivery(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
gateway, client = _prepared_gateway(monkeypatch)
|
|
monkeypatch.setattr(
|
|
_FakeResponse,
|
|
"json",
|
|
lambda _self: {
|
|
"ok": True,
|
|
"result": {"message_id": 11, "chat": {"id": "wrong-room"}},
|
|
},
|
|
)
|
|
|
|
result = await gateway.send_escalation_card(
|
|
incident_id="INC-TEST",
|
|
original_alertname="Outage",
|
|
duration_min=15,
|
|
)
|
|
|
|
assert result["_awooop_delivery_status"] == "provider_destination_mismatch"
|
|
assert result["_awooop_provider_send_performed"] is True
|
|
assert (
|
|
result[gateway_module._DELIVERY_CONTEXT_KEY][
|
|
"destination_binding_verified"
|
|
]
|
|
is False
|
|
)
|
|
assert gateway_module._telegram_send_delivery_succeeded(result) is False
|
|
assert len(client.posts) == 1
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_openclaw_cannot_own_tsenyang_monitoring_route(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
gateway, client = _prepared_gateway(monkeypatch)
|
|
monkeypatch.setattr(
|
|
gateway_module.settings,
|
|
"OPENCLAW_BOT_TOKEN",
|
|
"test-token-placeholder",
|
|
)
|
|
|
|
result = await gateway._send_as_bot(
|
|
token="test-token-placeholder",
|
|
text="must not send",
|
|
product_id="awoooi",
|
|
signal_family="security_incident",
|
|
severity="P1",
|
|
actual_bot_alias="openclaw_bot",
|
|
)
|
|
|
|
assert result["_awooop_delivery_status"] == "blocked_no_egress"
|
|
assert result["_awoooi_canonical_route_receipt"]["classifier"] == (
|
|
"sender_bot_alias_not_authorized_for_route"
|
|
)
|
|
assert client.posts == []
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_alternate_bot_uses_only_canonical_send_request(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
gateway = TelegramGateway()
|
|
canonical_send = AsyncMock(
|
|
return_value={
|
|
"ok": True,
|
|
"_awooop_delivery_status": "sent",
|
|
"_awooop_provider_send_performed": True,
|
|
"result": {"message_id": 11},
|
|
"_awoooi_canonical_route_receipt": {
|
|
"schema_version": "telegram_canonical_egress_receipt_v1",
|
|
"decision": "allowed",
|
|
"provider_send_performed": True,
|
|
},
|
|
}
|
|
)
|
|
monkeypatch.setattr(gateway, "_send_request", canonical_send)
|
|
|
|
result = await gateway._send_as_bot(
|
|
token="test-token-placeholder",
|
|
text="bounded reply",
|
|
reply_to_message_id=11,
|
|
actual_bot_alias="openclaw_bot",
|
|
inbound_chat_id="verified-room",
|
|
)
|
|
|
|
assert result["_awooop_delivery_status"] == "sent"
|
|
canonical_send.assert_awaited_once()
|
|
method, payload = canonical_send.await_args.args
|
|
assert method == "sendMessage"
|
|
assert payload[gateway_module._ACTUAL_BOT_ALIAS_KEY] == "openclaw_bot"
|
|
assert payload[gateway_module._BOT_TOKEN_OVERRIDE_KEY] == ("test-token-placeholder")
|
|
assert payload[gateway_module._NON_MONITORING_INTERACTION_KEY] == {
|
|
"interaction_kind": "bot_discussion_reply",
|
|
"inbound_chat_id": "verified-room",
|
|
"inbound_message_id": 11,
|
|
}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_alternate_bot_binding_mismatch_blocks_before_http(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
gateway, client = _prepared_gateway(monkeypatch)
|
|
monkeypatch.setattr(
|
|
gateway_module.settings,
|
|
"OPENCLAW_BOT_TOKEN",
|
|
"expected-placeholder",
|
|
)
|
|
|
|
result = await gateway._send_as_bot(
|
|
token="mismatched-placeholder",
|
|
text="must not send",
|
|
reply_to_message_id=11,
|
|
actual_bot_alias="openclaw_bot",
|
|
inbound_chat_id="test-room",
|
|
)
|
|
|
|
assert result["_awooop_delivery_status"] == "blocked_no_egress"
|
|
assert result["_awoooi_canonical_route_receipt"]["classifier"] == (
|
|
"sender_bot_binding_mismatch"
|
|
)
|
|
assert client.posts == []
|
|
|
|
|
|
def test_readability_guard_uses_complete_ast_function_boundary() -> None:
|
|
guard = runpy.run_path(
|
|
str(_REPO_ROOT / "scripts" / "security" / "telegram-alert-readability-guard.py")
|
|
)
|
|
long_body = "\n".join(f" value_{index} = {index}" for index in range(300))
|
|
source = (
|
|
'DECOY = "async def _send_request"\n'
|
|
"class Gateway:\n"
|
|
" async def _send_request(self, payload):\n"
|
|
f"{long_body}\n"
|
|
" return normalize_telegram_send_message_payload(\n"
|
|
' "sendMessage", payload\n'
|
|
" )\n\n"
|
|
" async def unrelated(self):\n"
|
|
" return False\n"
|
|
)
|
|
|
|
segment = guard["function_segment"](
|
|
source,
|
|
"async def _send_request",
|
|
)
|
|
|
|
assert len(segment) > 2200
|
|
assert "normalize_telegram_send_message_payload" in segment
|
|
assert "async def unrelated" not in segment
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_operator_reply_lane_and_runtime_edit_binding_remain_available(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
gateway, client = _prepared_gateway(monkeypatch)
|
|
|
|
assert gateway.chat_id == "test-room"
|
|
assert gateway.alert_chat_id == "test-room"
|
|
unverified = await gateway.send_notification(
|
|
"must not infer an operator reply",
|
|
chat_id="operator-room",
|
|
)
|
|
|
|
assert unverified["_awooop_delivery_status"] == "blocked_no_egress"
|
|
assert unverified["_awoooi_canonical_route_receipt"]["classifier"] == (
|
|
"missing_product_signal_severity_or_interaction_context"
|
|
)
|
|
assert client.posts == []
|
|
|
|
result = await gateway.send_notification(
|
|
"command reply",
|
|
chat_id="operator-room",
|
|
interaction_kind="operator_command_reply",
|
|
inbound_chat_id="operator-room",
|
|
inbound_message_id=42,
|
|
)
|
|
|
|
assert result["ok"] is True
|
|
receipt = result["_awoooi_canonical_route_receipt"]
|
|
assert receipt["classifier"] == "non_monitoring_interaction_allowed"
|
|
assert receipt["destination_alias"] == "inbound_interaction_reply_target"
|
|
assert client.posts[0][1]["chat_id"] == "operator-room"
|
|
assert client.posts[0][1]["reply_to_message_id"] == 42
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_interaction_reply_must_match_verified_inbound_context(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
gateway, client = _prepared_gateway(monkeypatch)
|
|
|
|
result = await gateway._send_request(
|
|
"sendMessage",
|
|
{
|
|
"chat_id": "operator-room",
|
|
"text": "mismatched reply",
|
|
"reply_to_message_id": 43,
|
|
gateway_module._NON_MONITORING_INTERACTION_KEY: {
|
|
"interaction_kind": "operator_command_reply",
|
|
"inbound_chat_id": "operator-room",
|
|
"inbound_message_id": 42,
|
|
},
|
|
},
|
|
)
|
|
|
|
assert result["_awooop_delivery_status"] == "blocked_no_egress"
|
|
assert result["_awoooi_canonical_route_receipt"]["classifier"] == (
|
|
"interaction_reply_context_missing_or_mismatched"
|
|
)
|
|
assert client.posts == []
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_t1_through_t6_use_only_canonical_monitoring_routes(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
gateway = TelegramGateway()
|
|
canonical_send = AsyncMock(return_value={"ok": True})
|
|
monkeypatch.setattr(gateway, "send_canonical_message", canonical_send)
|
|
|
|
await gateway.send_guardrail_blocked("api", "HighCpu", "protected")
|
|
await gateway.send_preflight_failed("api", 25.0, 24.0, "backup-1")
|
|
await gateway.send_backup_result("backup-1", True)
|
|
await gateway.send_multisig_waiting("repair", "api", 1, 2, "APR-1")
|
|
await gateway.send_multisig_approved("repair", "api")
|
|
await gateway.send_change_applied("operator", "repair", "2026-07-14T00:00:00Z")
|
|
|
|
assert canonical_send.await_count == 6
|
|
assert [
|
|
(
|
|
call.kwargs["product_id"],
|
|
call.kwargs["signal_family"],
|
|
call.kwargs["severity"],
|
|
)
|
|
for call in canonical_send.await_args_list
|
|
] == [
|
|
("awoooi", "security_incident", "P1"),
|
|
("awoooi", "disaster_recovery", "P1"),
|
|
("awoooi", "disaster_recovery", "P1"),
|
|
("awoooi", "incident_lifecycle", "P1"),
|
|
("awoooi", "incident_lifecycle", "P1"),
|
|
("awoooi", "incident_lifecycle", "P1"),
|
|
]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_image_analysis_reply_preserves_original_inbound_message_context(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
from src.services.image_analysis_service import ImageAnalysisService
|
|
|
|
gateway = SimpleNamespace(
|
|
initialize=AsyncMock(),
|
|
send_as_openclaw=AsyncMock(return_value={"ok": True}),
|
|
)
|
|
service = ImageAnalysisService()
|
|
monkeypatch.setattr(
|
|
gateway_module,
|
|
"get_telegram_gateway",
|
|
lambda: gateway,
|
|
)
|
|
monkeypatch.setattr(
|
|
service,
|
|
"_download_telegram_file",
|
|
AsyncMock(return_value=b"image"),
|
|
)
|
|
monkeypatch.setattr(
|
|
service,
|
|
"analyze",
|
|
AsyncMock(return_value="analysis"),
|
|
)
|
|
|
|
await service.download_and_analyze(
|
|
chat_id="test-room",
|
|
file_id="file-1",
|
|
original_message_id=77,
|
|
)
|
|
|
|
gateway.initialize.assert_awaited_once()
|
|
gateway.send_as_openclaw.assert_awaited_once_with(
|
|
"🖼️ <b>圖片分析</b>\nanalysis",
|
|
reply_to_message_id=77,
|
|
inbound_chat_id="test-room",
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_manual_repair_callback_replies_to_verified_original_message(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
gateway, client = _prepared_gateway(monkeypatch)
|
|
monkeypatch.setattr(
|
|
gateway._security,
|
|
"parse_callback_data",
|
|
lambda _data: {
|
|
"action": "log_manual_fix",
|
|
"approval_id": "APR-1",
|
|
"is_info_action": False,
|
|
"nonce": "nonce-1",
|
|
},
|
|
)
|
|
monkeypatch.setattr(
|
|
gateway._security,
|
|
"verify_callback",
|
|
AsyncMock(return_value={"id": 123, "username": "operator"}),
|
|
)
|
|
monkeypatch.setattr(
|
|
gateway,
|
|
"_check_incident_state_guard",
|
|
AsyncMock(return_value=None),
|
|
)
|
|
monkeypatch.setattr(gateway, "_answer_callback", AsyncMock())
|
|
monkeypatch.setattr(gateway_module, "get_redis", lambda: _FakeRedis())
|
|
|
|
result = await gateway.handle_callback(
|
|
callback_query_id="callback-1",
|
|
callback_data="log_manual_fix:APR-1:nonce-1",
|
|
user_id=123,
|
|
message_id=77,
|
|
chat_id="operator-room",
|
|
username="operator",
|
|
)
|
|
|
|
assert result["success"] is True
|
|
assert result["waiting_for_manual_fix"] is True
|
|
assert len(client.posts) == 1
|
|
payload = client.posts[0][1]
|
|
assert payload["chat_id"] == "operator-room"
|
|
assert payload["reply_to_message_id"] == 77
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_manual_repair_flow_blocks_before_side_effect_without_inbound_context(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
gateway, client = _prepared_gateway(monkeypatch)
|
|
|
|
result = await gateway.handle_manual_fix_done(
|
|
user_id=123,
|
|
username="operator",
|
|
fix_steps="restart",
|
|
)
|
|
|
|
assert result == {
|
|
"success": False,
|
|
"reason": "missing_verified_interaction_context",
|
|
}
|
|
assert client.posts == []
|
|
|
|
|
|
class _FakeDb:
|
|
def __init__(self) -> None:
|
|
self.params: list[dict] = []
|
|
|
|
async def execute(self, statement: object, params: dict) -> None:
|
|
del statement
|
|
self.params.append(params)
|
|
|
|
|
|
class _FakeRedis:
|
|
async def setex(self, key: str, ttl: int, value: str) -> None:
|
|
del key, ttl, value
|
|
|
|
|
|
def _patch_approval_dependencies(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
gateway: TelegramGateway,
|
|
) -> _FakeDb:
|
|
fake_db = _FakeDb()
|
|
|
|
@asynccontextmanager
|
|
async def fake_db_context(*args, **kwargs):
|
|
del args, kwargs
|
|
yield fake_db
|
|
|
|
monkeypatch.setattr(db_base, "get_db_context", fake_db_context)
|
|
monkeypatch.setattr(
|
|
approval_db,
|
|
"get_approval_service",
|
|
lambda: SimpleNamespace(update_telegram_message=True),
|
|
)
|
|
monkeypatch.setattr(gateway_module, "get_redis", lambda: _FakeRedis())
|
|
monkeypatch.setattr(
|
|
gateway_module,
|
|
"_fetch_remediation_summary_for_card",
|
|
AsyncMock(return_value=None),
|
|
)
|
|
monkeypatch.setattr(gateway, "_build_inline_keyboard", AsyncMock(return_value={}))
|
|
return fake_db
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_approval_missing_type_is_blocked_without_provider_call(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
gateway, client = _prepared_gateway(monkeypatch)
|
|
_patch_approval_dependencies(monkeypatch, gateway)
|
|
|
|
result = await gateway.send_approval_card(
|
|
approval_id="APR-TEST",
|
|
risk_level="critical",
|
|
resource_name="resource",
|
|
root_cause="cause",
|
|
suggested_action="review",
|
|
notification_type="",
|
|
)
|
|
|
|
assert result["_awooop_delivery_status"] == "blocked_no_egress"
|
|
assert client.posts == []
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_allowed_type3_approval_persists_runtime_chat_without_public_id(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
gateway, client = _prepared_gateway(monkeypatch)
|
|
fake_db = _patch_approval_dependencies(monkeypatch, gateway)
|
|
monkeypatch.setattr(gateway, "canonical_destination_chat_id", lambda **kwargs: "7")
|
|
|
|
result = await gateway.send_approval_card(
|
|
approval_id="APR-TEST",
|
|
risk_level="critical",
|
|
resource_name="resource",
|
|
root_cause="cause",
|
|
suggested_action="review",
|
|
notification_type="TYPE-3",
|
|
)
|
|
|
|
assert result["ok"] is True
|
|
assert len(client.posts) == 1
|
|
assert any(params.get("cid") == 7 for params in fake_db.params)
|
|
receipt = result["_awoooi_canonical_route_receipt"]
|
|
assert receipt["destination_alias"] == "awoooi_sre_war_room"
|
|
assert "chat_id" not in receipt
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_explicit_blocked_product_route_supersedes_legacy_type3(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
gateway, client = _prepared_gateway(monkeypatch)
|
|
_patch_approval_dependencies(monkeypatch, gateway)
|
|
|
|
result = await gateway.send_approval_card(
|
|
approval_id="APR-PRODUCT",
|
|
risk_level="medium",
|
|
resource_name="momo-crawler",
|
|
root_cause="freshness degraded",
|
|
suggested_action="inspect evidence",
|
|
notification_type="TYPE-3",
|
|
product_id="momo-pro",
|
|
signal_family="crawler_and_data_freshness",
|
|
route_severity="P2",
|
|
)
|
|
|
|
assert result["_awooop_delivery_status"] == "blocked_no_egress"
|
|
assert result["_awooop_provider_send_performed"] is False
|
|
assert client.posts == []
|
|
receipt = result["_awoooi_canonical_route_receipt"]
|
|
assert receipt["product_id"] == "momo-pro"
|
|
assert receipt["signal_family"] == "crawler_and_data_freshness"
|