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.core.context as core_context import src.db.base as db_base import src.services.approval_db as approval_db import src.services.channel_hub as channel_hub 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(return_value=True), ) 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 receipt = result["_awoooi_canonical_route_receipt"] assert receipt["durable_receipt_required"] is True assert receipt["durable_receipt_persisted"] is True assert receipt["durable_receipt_status"] == "persisted" gateway._mirror_outbound_message.assert_awaited_once() assert gateway._mirror_outbound_message.await_args.kwargs["method"] == ( "sendPhoto" ) @pytest.mark.asyncio async def test_stockplatform_p1_operational_ingress_uses_tsenyang_sre_once( monkeypatch: pytest.MonkeyPatch, ) -> None: gateway, client = _prepared_gateway(monkeypatch) result = await gateway._send_request( "sendMessage", { "text": "StockPlatform 排程健康重大告警", gateway_module._CANONICAL_ROUTE_CONTEXT_KEY: { "product_id": "stockplatform-v2", "signal_family": "incident_lifecycle", "severity": "P1", }, }, ) assert result["_awooop_delivery_status"] == "sent" assert len(client.posts) == 1 assert client.posts[0][1]["chat_id"] == "test-room" receipt = result["_awoooi_canonical_route_receipt"] assert receipt["route_id"] == "stockplatform-operational-incident-lifecycle" assert receipt["receipt_backend"] == ( "awoooi.alert_operation_log+telegram_outbound_receipts" ) assert receipt["alert_category_zh_tw"] == "事故生命週期" assert receipt["destination_role"] == "shared_sre_war_room" assert receipt["destination_alias"] == "awoooi_sre_war_room" assert receipt["sender_bot_alias"] == "tsenyang_bot" assert receipt["durable_receipt_required"] is True assert receipt["durable_receipt_persisted"] is True assert receipt["durable_receipt_status"] == "persisted" assert result["_awooop_outbound_mirror_acknowledged"] is True assert gateway_module._telegram_send_delivery_succeeded(result) is True @pytest.mark.asyncio async def test_provider_ack_without_durable_mirror_is_non_terminal( monkeypatch: pytest.MonkeyPatch, ) -> None: gateway, client = _prepared_gateway(monkeypatch) monkeypatch.setattr( gateway, "_mirror_outbound_message", AsyncMock(return_value=False), ) result = await gateway._send_request( "sendMessage", { "text": "StockPlatform 排程健康重大告警", gateway_module._CANONICAL_ROUTE_CONTEXT_KEY: { "product_id": "stockplatform-v2", "signal_family": "incident_lifecycle", "severity": "P1", }, }, ) assert result["ok"] is True assert result["_awooop_provider_send_performed"] is True assert result["_awooop_outbound_mirror_acknowledged"] is False assert result["_awooop_delivery_status"] == ( "provider_sent_durable_receipt_pending" ) receipt = result["_awoooi_canonical_route_receipt"] assert receipt["provider_send_performed"] is True assert receipt["durable_receipt_required"] is True assert receipt["durable_receipt_persisted"] is False assert receipt["durable_receipt_status"] == ( "provider_sent_durable_receipt_pending" ) assert gateway_module._telegram_send_delivery_succeeded(result) is False assert len(client.posts) == 1 @pytest.mark.asyncio async def test_non_monitoring_message_keeps_best_effort_mirror_without_gate( monkeypatch: pytest.MonkeyPatch, ) -> None: gateway, client = _prepared_gateway(monkeypatch) mirror = AsyncMock(return_value=False) monkeypatch.setattr(gateway, "_mirror_outbound_message", mirror) result = await gateway._send_request( "sendMessage", { "chat_id": "operator-room", "text": "bounded operator reply", "reply_to_message_id": 42, 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"] == "sent" assert result["_awooop_provider_send_performed"] is True assert result["_awooop_outbound_mirror_acknowledged"] is False assert gateway_module._telegram_send_delivery_succeeded(result) is True receipt = result["_awoooi_canonical_route_receipt"] assert receipt["classifier"] == "non_monitoring_interaction_allowed" assert receipt["durable_receipt_required"] is False assert receipt["durable_receipt_persisted"] is False assert receipt["durable_receipt_status"] == "best_effort_mirror_failed" mirror.assert_awaited_once() assert len(client.posts) == 1 @pytest.mark.asyncio @pytest.mark.parametrize( ("method", "content_field"), [("sendMessage", "text"), ("sendPhoto", "caption")], ) async def test_mirror_outbound_message_persists_message_and_photo_receipts( monkeypatch: pytest.MonkeyPatch, method: str, content_field: str, ) -> None: captured: dict[str, object] = {} @asynccontextmanager async def fake_db_context(project_id: str): captured["project_id"] = project_id yield object() async def fake_record_outbound_message(_db, **kwargs): captured["record"] = kwargs return "durable-row-1" monkeypatch.setattr(core_context, "get_current_project_id", lambda: "awoooi") monkeypatch.setattr(db_base, "get_db_context", fake_db_context) monkeypatch.setattr( channel_hub, "record_outbound_message", fake_record_outbound_message, ) gateway = TelegramGateway() persisted = await gateway._mirror_outbound_message( method=method, payload={ "chat_id": "sre-room", content_field: "bounded operational card", }, provider_message_id="provider-42", source_envelope_extra={ "canonical_route_receipt": { "schema_version": "telegram_canonical_egress_receipt_v1", "decision": "allowed", "route_id": "awoooi-observability", "provider_send_performed": True, "durable_receipt_required": True, "durable_receipt_persisted": False, "durable_receipt_status": "provider_sent_mirror_pending", } }, ) assert persisted is True record = captured["record"] assert isinstance(record, dict) assert record["provider_message_id"] == "provider-42" assert record["send_status"] == "sent" assert record["content"] == "bounded operational card" envelope = record["source_envelope"] assert envelope["method"] == method mirror_receipt = envelope["canonical_route_receipt"] assert mirror_receipt["durable_receipt_required"] is True assert mirror_receipt["durable_receipt_persisted"] is True assert mirror_receipt["durable_receipt_status"] == "persisted" @pytest.mark.asyncio async def test_mirror_preserves_legacy_history_without_route_receipt( monkeypatch: pytest.MonkeyPatch, ) -> None: captured: dict[str, object] = {} @asynccontextmanager async def fake_db_context(project_id: str): captured["project_id"] = project_id yield object() async def fake_record_outbound_message(_db, **kwargs): captured["record"] = kwargs return "durable-row-legacy" monkeypatch.setattr(core_context, "get_current_project_id", lambda: "awoooi") monkeypatch.setattr(db_base, "get_db_context", fake_db_context) monkeypatch.setattr( channel_hub, "record_outbound_message", fake_record_outbound_message, ) persisted = await TelegramGateway()._mirror_outbound_message( method="sendMessage", payload={"chat_id": "operator-room", "text": "legacy history"}, provider_message_id="provider-legacy", source_envelope_extra={"callback_reply": {"status": "sent"}}, ) assert persisted is True record = captured["record"] assert isinstance(record, dict) assert record["send_status"] == "sent" assert record["provider_message_id"] == "provider-legacy" assert record["source_envelope"]["callback_reply"] == {"status": "sent"} assert "canonical_route_receipt" not in record["source_envelope"] @pytest.mark.asyncio async def test_mirror_failure_returns_false_without_logging_sensitive_error( monkeypatch: pytest.MonkeyPatch, ) -> None: warnings: list[dict[str, object]] = [] class CaptureLogger: def warning(self, _event: str, **kwargs) -> None: warnings.append(kwargs) @asynccontextmanager async def fake_db_context(_project_id: str): yield object() async def failing_record_outbound_message(_db, **_kwargs): raise RuntimeError("token=must-not-appear password=must-not-appear") monkeypatch.setattr(core_context, "get_current_project_id", lambda: "awoooi") monkeypatch.setattr(db_base, "get_db_context", fake_db_context) monkeypatch.setattr( channel_hub, "record_outbound_message", failing_record_outbound_message, ) monkeypatch.setattr(gateway_module, "logger", CaptureLogger()) persisted = await TelegramGateway()._mirror_outbound_message( method="sendMessage", payload={"chat_id": "private-runtime-id", "text": "safe card"}, provider_message_id="provider-42", source_envelope_extra={ "canonical_route_receipt": { "schema_version": "telegram_canonical_egress_receipt_v1", "decision": "allowed", } }, ) assert persisted is False assert warnings == [{ "method": "sendMessage", "destination_binding": gateway_module._telegram_destination_binding( "private-runtime-id" ), "provider_message_id": "provider-42", "error_code": "durable_mirror_persistence_failed", "error_type": "RuntimeError", "durable_receipt_persisted": False, }] assert "must-not-appear" not in str(warnings) assert "private-runtime-id" not in str(warnings) @pytest.mark.asyncio @pytest.mark.parametrize( ("signal_family", "severity"), [ ("incident_lifecycle", "P2"), ("incident_lifecycle", "P3"), ("business_report", "P1"), ("business_report", "P3"), ("market_data_and_recommendation_freshness", "P1"), ], ) async def test_stockplatform_non_operational_routes_never_reach_provider( monkeypatch: pytest.MonkeyPatch, signal_family: str, severity: str, ) -> None: gateway, client = _prepared_gateway(monkeypatch) result = await gateway._send_request( "sendMessage", { "text": "must remain product-routed or blocked", gateway_module._CANONICAL_ROUTE_CONTEXT_KEY: { "product_id": "stockplatform-v2", "signal_family": signal_family, "severity": severity, }, }, ) 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_stockplatform_operational_alert_cannot_override_to_direct_dm( monkeypatch: pytest.MonkeyPatch, ) -> None: gateway, client = _prepared_gateway(monkeypatch) result = await gateway._send_request( "sendMessage", { "chat_id": "private-bot-dm", "text": "must not DM", gateway_module._CANONICAL_ROUTE_CONTEXT_KEY: { "product_id": "stockplatform-v2", "signal_family": "incident_lifecycle", "severity": "P1", }, }, ) 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", "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_provider_username_resolution_preserves_verified_destination( monkeypatch: pytest.MonkeyPatch, ) -> None: gateway, client = _prepared_gateway(monkeypatch) monkeypatch.setattr( gateway_module.settings, "SRE_GROUP_CHAT_ID", "@AwoooiSRE", ) monkeypatch.setattr( _FakeResponse, "json", lambda _self: { "ok": True, "result": { "message_id": 11, "chat": {"id": -1001234567890, "username": "awoooisre"}, }, }, ) result = await gateway.send_escalation_card( incident_id="INC-TEST", original_alertname="Outage", duration_min=15, ) context = result[gateway_module._DELIVERY_CONTEXT_KEY] assert result["_awooop_delivery_status"] == "sent" assert context["destination_binding_verified"] is True assert context["provider_destination_identity_verified"] is True assert context["provider_destination_binding"] != context["destination_binding"] assert gateway_module._telegram_send_delivery_succeeded(result) is True assert len(client.posts) == 1 @pytest.mark.asyncio async def test_provider_username_proof_survives_lifecycle_restart_readback( monkeypatch: pytest.MonkeyPatch, ) -> None: gateway, client = _prepared_gateway(monkeypatch) monkeypatch.setattr( gateway_module.settings, "SRE_GROUP_CHAT_ID", "@AwoooiSRE", ) monkeypatch.setattr( _FakeResponse, "json", lambda _self: { "ok": True, "result": { "message_id": 8124, "chat": {"id": -1001234567890, "username": "awoooisre"}, }, }, ) reserve = AsyncMock( return_value={ "status": "reserved", "run_id": "00000000-0000-0000-0000-000000000201", "message_id": "00000000-0000-0000-0000-000000000202", "durable_reservation_committed": True, } ) finalize = AsyncMock(return_value=True) monkeypatch.setattr( gateway, "_reserve_controlled_apply_result_outbound", reserve, ) monkeypatch.setattr( gateway, "_finalize_controlled_apply_result_outbound", finalize, ) delivered = await gateway.send_agent99_lifecycle_receipt( delivery_id=( "agent99-lifecycle-0123456789abcdef0123456789abcdef01234567" ), incident_id="INC-20260718-ALIAS", state_key="recovered|info", text="Agent99 verified lifecycle", priority="P1", ) delivery_context = delivered[gateway_module._DELIVERY_CONTEXT_KEY] assert delivered["_awooop_delivery_status"] == "sent" assert delivery_context["destination_binding_verified"] is True assert delivery_context["provider_destination_binding"] != ( delivery_context["destination_binding"] ) finalize.assert_awaited_once() finalize_kwargs = finalize.await_args.kwargs assert finalize_kwargs["expected_destination_binding"] == ( delivery_context["destination_binding"] ) assert finalize_kwargs["provider_destination_binding"] == ( delivery_context["provider_destination_binding"] ) assert finalize_kwargs["provider_destination_verification_method"] == ( "requested_username_matches_provider_username" ) assert len(client.posts) == 1 restarted_gateway = object.__new__(TelegramGateway) durable_readback = AsyncMock( return_value={ "status": "sent", "provider_message_id": "8124", "destination_binding": delivery_context["destination_binding"], "provider_destination_binding": delivery_context[ "provider_destination_binding" ], "provider_destination_verification_method": ( "requested_username_matches_provider_username" ), "provider_destination_identity_verified": True, "visual_requested": False, } ) monkeypatch.setattr( restarted_gateway, "_reconcile_controlled_apply_result_outbound", durable_readback, ) reconciled = await restarted_gateway.reconcile_agent99_lifecycle_receipt( delivery_id=( "agent99-lifecycle-0123456789abcdef0123456789abcdef01234567" ), incident_id="INC-20260718-ALIAS", state_key="recovered|info", expected_provider_message_id="8124", expected_destination_binding=delivery_context["destination_binding"], expected_provider_destination_binding=delivery_context[ "provider_destination_binding" ], expected_provider_destination_verification_method=( "requested_username_matches_provider_username" ), ) assert reconciled["ok"] is True assert reconciled["_awooop_delivery_status"] == "sent_reused" assert reconciled["_awooop_delivery_context"][ "destination_binding_verified" ] is True @pytest.mark.asyncio @pytest.mark.parametrize( ("provider_chat", "expected_status"), [ ( {"id": -1001234567890, "username": "different_room"}, "provider_destination_mismatch", ), ({"username": "awoooisre"}, "provider_destination_unverified"), ], ) async def test_provider_username_resolution_fails_closed_on_identity_gap( monkeypatch: pytest.MonkeyPatch, provider_chat: dict[str, object], expected_status: str, ) -> None: gateway, client = _prepared_gateway(monkeypatch) monkeypatch.setattr( gateway_module.settings, "SRE_GROUP_CHAT_ID", "@AwoooiSRE", ) monkeypatch.setattr( _FakeResponse, "json", lambda _self: { "ok": True, "result": {"message_id": 11, "chat": provider_chat}, }, ) result = await gateway.send_escalation_card( incident_id="INC-TEST", original_alertname="Outage", duration_min=15, ) assert result["_awooop_delivery_status"] == expected_status 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( "🖼️ 圖片分析\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"